diff --git a/.appveyor.yml b/.appveyor.yml index 9ec837996..a77033ec1 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -6,86 +6,51 @@ init: # Uncomment previous line to get RDP access during the build. environment: - X64_EXT: -x64 EXECUTABLE: python.exe - PIP_DIR: Scripts - VENV: NO TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/Python38 - - PYTHON: C:/Python38-x64 - - PYTHON: C:/Python37 - - PYTHON: C:/Python37-x64 - - PYTHON: C:/Python36 + - PYTHON: C:/Python39 + ARCHITECTURE: x86 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - PYTHON: C:/Python36-x64 - - PYTHON: C:/Python35 - - PYTHON: C:/Python35-x64 - - PYTHON: C:/msys64/mingw32 - EXECUTABLE: bin/python3 - PIP_DIR: bin - TEST_OPTIONS: --processes=0 - DEPLOY: NO - - PYTHON: C:/vp/pypy3 - EXECUTABLE: bin/pypy.exe - PIP_DIR: bin - VENV: YES + ARCHITECTURE: x64 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 install: - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip - 7z x pillow-depends.zip -oc:\ - mv c:\pillow-depends-master c:\pillow-depends -- xcopy c:\pillow-depends\*.zip c:\pillow\winbuild\ -- xcopy c:\pillow-depends\*.tar.gz c:\pillow\winbuild\ -- xcopy /s c:\pillow-depends\test_images\* c:\pillow\tests\images +- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images +- 7z x ..\pillow-depends\nasm-2.14.02-win64.zip -oc:\ +- ..\pillow-depends\gs9533w32.exe /S +- path c:\nasm-2.14.02;C:\Program Files (x86)\gs\gs9.53.3\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | - if ($env:PYTHON -eq "c:/vp/pypy3") - { - c:\pillow\winbuild\appveyor_install_pypy3.cmd - } -- ps: | - if ($env:PYTHON -eq "c:/msys64/mingw32") - { - c:\msys64\usr\bin\bash -l -c c:\\pillow\\winbuild\\appveyor_install_msys2_deps.sh - } - else - { - c:\python37\python.exe c:\pillow\winbuild\build_dep.py - c:\pillow\winbuild\build_deps.cmd + c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ + c:\pillow\winbuild\build\build_dep_all.cmd $host.SetShouldExit(0) - } -- curl -fsSL -o gs950.exe https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs950/gs950w32.exe -- gs950.exe /S -- path %path%;C:\Program Files (x86)\gs\gs9.50\bin +- path C:\pillow\winbuild\build\bin;%PATH% +- '%PYTHON%\%EXECUTABLE% -m pip install -U "setuptools>=49.3.2"' build_script: - ps: | - if ($env:PYTHON -eq "c:/msys64/mingw32") - { - c:\msys64\usr\bin\bash -l -c c:\\pillow\\winbuild\\appveyor_build_msys2.sh - Write-Host "through install" + c:\pillow\winbuild\build\build_pillow.cmd install $host.SetShouldExit(0) - } - else - { - & $env:PYTHON/$env:EXECUTABLE c:\pillow\winbuild\build.py - $host.SetShouldExit(0) - } - cd c:\pillow - '%PYTHON%\%EXECUTABLE% selftest.py --installed' test_script: - cd c:\pillow -- '%PYTHON%\%PIP_DIR%\pip.exe install pytest pytest-cov' +- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov' - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% -- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests' +- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' #- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? after_test: -- pip install codecov -- codecov --file coverage.xml --name %PYTHON% +- python -m pip install codecov +- codecov --file coverage.xml --name %PYTHON% --flags AppVeyor matrix: fast_finish: true @@ -101,9 +66,9 @@ artifacts: before_deploy: - cd c:\pillow - - '%PYTHON%\%PIP_DIR%\pip.exe install wheel' + - '%PYTHON%\%EXECUTABLE% -m pip install wheel' - cd c:\pillow\winbuild\ - - '%PYTHON%\%EXECUTABLE% c:\pillow\winbuild\build.py --wheel' + - c:\pillow\winbuild\build\build_pillow.cmd bdist_wheel - cd c:\pillow - ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } diff --git a/.azure-pipelines/jobs/lint.yml b/.azure-pipelines/jobs/lint.yml deleted file mode 100644 index d017590f8..000000000 --- a/.azure-pipelines/jobs/lint.yml +++ /dev/null @@ -1,28 +0,0 @@ -parameters: - name: '' # defaults for any parameters that aren't specified - vmImage: '' - -jobs: - -- job: ${{ parameters.name }} - pool: - vmImage: ${{ parameters.vmImage }} - - strategy: - matrix: - Python37: - python.version: '3.7' - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '$(python.version)' - architecture: 'x64' - - - script: | - python -m pip install --upgrade tox - displayName: 'Install dependencies' - - - script: | - tox -e lint - displayName: 'Lint' diff --git a/.azure-pipelines/jobs/test-docker.yml b/.azure-pipelines/jobs/test-docker.yml deleted file mode 100644 index 41dc2daec..000000000 --- a/.azure-pipelines/jobs/test-docker.yml +++ /dev/null @@ -1,22 +0,0 @@ -parameters: - docker: '' # defaults for any parameters that aren't specified - dockerTag: 'master' - name: '' - vmImage: 'Ubuntu-16.04' - -jobs: - -- job: ${{ parameters.name }} - pool: - vmImage: ${{ parameters.vmImage }} - - steps: - - script: | - docker pull pythonpillow/${{ parameters.docker }}:${{ parameters.dockerTag }} - displayName: 'Docker pull' - - - script: | - # The Pillow user in the docker container is UID 1000 - sudo chown -R 1000 $(Build.SourcesDirectory) - docker run -v $(Build.SourcesDirectory):/Pillow pythonpillow/${{ parameters.docker }}:${{ parameters.dockerTag }} - displayName: 'Docker build' diff --git a/.ci/after_success.sh b/.ci/after_success.sh new file mode 100755 index 000000000..762670f10 --- /dev/null +++ b/.ci/after_success.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# gather the coverage data +pip3 install codecov +if [[ $MATRIX_DOCKER ]]; then + coverage xml --ignore-errors +else + coverage xml +fi + +if [[ $TRAVIS ]]; then + codecov --flags TravisCI +fi + +if [ "$TRAVIS_PYTHON_VERSION" == "3.9" ]; then + # Coverage and quality reports on just the latest diff. + depends/diffcover-install.sh + depends/diffcover-run.sh +fi diff --git a/.travis/build.sh b/.ci/build.sh similarity index 100% rename from .travis/build.sh rename to .ci/build.sh diff --git a/.ci/install.sh b/.ci/install.sh new file mode 100755 index 000000000..77dccaa4a --- /dev/null +++ b/.ci/install.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +aptget_update() +{ + if [ ! -z $1 ]; then + echo "" + echo "Retrying apt-get update..." + echo "" + fi + output=`sudo apt-get update 2>&1` + echo "$output" + if [[ $output == *[WE]:\ * ]]; then + return 1 + fi +} +aptget_update || aptget_update retry || aptget_update retry + +set -e + +sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ + ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ + cmake imagemagick libharfbuzz-dev libfribidi-dev + +if [[ $TRAVIS_CPU_ARCH == "s390x" ]]; then sudo chown $USER ~/.cache/pip/wheels ; fi + +python3 -m pip install --upgrade pip +PYTHONOPTIMIZE=0 python3 -m pip install cffi +python3 -m pip install coverage +python3 -m pip install olefile +python3 -m pip install -U pytest +python3 -m pip install -U pytest-cov +python3 -m pip install pyroma +python3 -m pip install test-image-results +# TODO Remove condition when numpy supports 3.10 +if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi + +# TODO Remove when 3.8 / 3.9 / PyPy3 includes setuptools 49.3.2+: +if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi +if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi +if [ "$TRAVIS_PYTHON_VERSION" == "pypy3.6-7.3.1" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi + +if [[ $TRAVIS_PYTHON_VERSION == 3.* ]]; then + # arm64, ppc64le, s390x CPUs: + # "ERROR: Could not find a version that satisfies the requirement pyqt5" + if [[ $TRAVIS_CPU_ARCH == "amd64" ]]; then + sudo apt-get -qq install libxcb-xinerama0 pyqt5-dev-tools + python3 -m pip install pyqt5 + fi +fi + +# docs only on Python 3.9 +if [ "$TRAVIS_PYTHON_VERSION" == "3.9" ]; then python3 -m pip install -r requirements.txt ; fi + +# webp +pushd depends && ./install_webp.sh && popd + +# libimagequant +pushd depends && ./install_imagequant.sh && popd + +# raqm +pushd depends && ./install_raqm.sh && popd + +# extra test images +pushd depends && ./install_extra_test_images.sh && popd diff --git a/.ci/test.sh b/.ci/test.sh new file mode 100755 index 000000000..1396445e0 --- /dev/null +++ b/.ci/test.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +python -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests + +# Docs +if [ "$TRAVIS_PYTHON_VERSION" == "3.9" ] && [ "$TRAVIS_CPU_ARCH" == "amd64" ]; then + make doccheck +fi diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index a93095486..000000000 --- a/.codecov.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Documentation: https://docs.codecov.io/docs/codecov-yaml - -codecov: - # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" - # https://github.com/codecov/support/issues/363 - # https://docs.codecov.io/v4.3.6/docs/comparing-commits - allow_coverage_offsets: true - - token: 6dafc396-e7f5-4221-a38a-8b07a49fbdae - -comment: off diff --git a/.coveragerc b/.coveragerc index ea79190ae..f71b6b1a2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,5 +10,11 @@ exclude_lines = if 0: if __name__ == .__main__.: # Don't complain about debug code - if Image.DEBUG: if DEBUG: + +[run] +omit = + Tests/32bit_segfault_check.py + Tests/bench_cffi_access.py + Tests/check_*.py + Tests/createfontdatachunk.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3d27b5d88..a65487d5e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -9,7 +9,7 @@ Please send a pull request to the master branch. Please include [documentation]( - Fork the Pillow repository. - Create a branch from master. - Develop bug fixes, features, tests, etc. -- Run the test suite. You can enable [Travis CI](https://travis-ci.org/profile/) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. +- Run the test suite. You can enable [Travis CI](https://travis-ci.com/account/repositories) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. - Create a pull request to pull the changes from your branch to the Pillow master. ### Guidelines @@ -17,7 +17,7 @@ Please send a pull request to the master branch. Please include [documentation]( - Separate code commits from reformatting commits. - Provide tests for any newly added code. - Follow PEP 8. -- When committing only documentation changes please include [ci skip] in the commit message to avoid running tests on Travis-CI and AppVeyor. +- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on Travis CI and AppVeyor. ## Reporting Issues diff --git a/.github/mergify.yml b/.github/mergify.yml new file mode 100644 index 000000000..fd4b5271f --- /dev/null +++ b/.github/mergify.yml @@ -0,0 +1,14 @@ +pull_request_rules: + - name: Automatic merge + conditions: + - "#approved-reviews-by>=1" + - label=automerge + - status-success=Lint + - status-success=Test Successful + - status-success=Docker Test Successful + - status-success=Windows Test Successful + - status-success=Travis CI - Pull Request + - status-success=continuous-integration/appveyor/pr + actions: + merge: + method: merge diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 195ea3865..3c658293e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,24 +6,43 @@ jobs: build: runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.8"] - name: Python ${{ matrix.python-version }} + name: Lint steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + - name: pip cache + uses: actions/cache@v2 with: - python-version: ${{ matrix.python-version }} + path: ~/.cache/pip + key: lint-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + lint-pip- + + - name: pre-commit cache + uses: actions/cache@v2 + with: + path: ~/.cache/pre-commit + key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} + restore-keys: | + lint-pre-commit- + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Build system information + run: python .github/workflows/system-info.py - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install --upgrade tox + python -m pip install -U pip + python -m pip install -U tox - name: Lint run: tox -e lint + env: + PRE_COMMIT_COLOR: always + diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 6cd9dadf3..fb8869c7d 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,16 +2,23 @@ set -e -brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype +brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype openblas -PYTHONOPTIMIZE=0 pip install cffi -pip install coverage -pip install olefile -pip install -U pytest -pip install -U pytest-cov -pip install pyroma -pip install test-image-results -pip install numpy +PYTHONOPTIMIZE=0 python3 -m pip install cffi +python3 -m pip install coverage +python3 -m pip install olefile +python3 -m pip install -U pytest +python3 -m pip install -U pytest-cov +python3 -m pip install pyroma +python3 -m pip install test-image-results + +echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg +# TODO Remove condition when numpy supports 3.10 +if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi + +# TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: +if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi +if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/system-info.py b/.github/workflows/system-info.py new file mode 100644 index 000000000..8e840319a --- /dev/null +++ b/.github/workflows/system-info.py @@ -0,0 +1,25 @@ +""" +Print out some handy system info like Travis CI does. + +This sort of info is missing from GitHub Actions. + +Requested here: +https://github.com/actions/virtual-environments/issues/79 +""" +import os +import platform +import sys + +print("Build system information") +print() + +print("sys.version\t\t", sys.version.split("\n")) +print("os.name\t\t\t", os.name) +print("sys.platform\t\t", sys.platform) +print("platform.system()\t", platform.system()) +print("platform.machine()\t", platform.machine()) +print("platform.platform()\t", platform.platform()) +print("platform.version()\t", platform.version()) +print("platform.uname()\t", platform.uname()) +if sys.platform == "darwin": + print("platform.mac_ver()\t", platform.mac_ver()) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 4121eb5e9..ddec1bee4 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -7,27 +7,31 @@ jobs: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: docker: [ alpine, arch, - ubuntu-16.04-xenial-amd64, ubuntu-18.04-bionic-amd64, - debian-9-stretch-x86, + ubuntu-20.04-focal-amd64, debian-10-buster-x86, centos-6-amd64, centos-7-amd64, + centos-8-amd64, amazon-1-amd64, amazon-2-amd64, - fedora-30-amd64, - fedora-31-amd64, + fedora-32-amd64, + fedora-33-amd64, ] dockerTag: [master] name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 + + - name: Build system information + run: python .github/workflows/system-info.py - name: Docker pull run: | @@ -37,4 +41,31 @@ jobs: run: | # The Pillow user in the docker container is UID 1000 sudo chown -R 1000 $GITHUB_WORKSPACE - docker run -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + sudo chown -R runner $GITHUB_WORKSPACE + + - name: After success + run: | + PATH="$PATH:~/.local/bin" + docker start pillow_container + pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'` + docker stop pillow_container + sudo mkdir -p $pil_path + sudo cp src/PIL/*.py $pil_path + .ci/after_success.sh + env: + MATRIX_DOCKER: ${{ matrix.docker }} + + - name: Upload coverage + uses: codecov/codecov-action@v1 + with: + flags: GHA_Docker + name: ${{ matrix.docker }} + + success: + needs: build + runs-on: ubuntu-latest + name: Docker Test Successful + steps: + - name: Success + run: echo Docker Test Successful diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index cc7d31ef0..4d3619edb 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -4,12 +4,11 @@ on: [push, pull_request] jobs: build: - runs-on: windows-2019 strategy: fail-fast: false matrix: - python-version: ["3.5", "3.6", "3.7", "3.8", "pypy3.6"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev", "pypy3"] architecture: ["x86", "x64"] include: - architecture: "x86" @@ -18,363 +17,262 @@ jobs: - architecture: "x64" platform-vcvars: "x86_amd64" platform-msbuild: "x64" - - python-version: "pypy3.6" - pypy-version: "pypy3.6-v7.2.0-win32" - pypy-url: "https://bitbucket.org/pypy/pypy/downloads/pypy3.6-v7.2.0-win32.zip" exclude: - - python-version: "pypy3.6" + # PyPy does not support 64-bit on Windows + - python-version: "pypy3" architecture: "x64" timeout-minutes: 30 name: Python ${{ matrix.python-version }} ${{ matrix.architecture }} steps: - - uses: actions/checkout@v1 + - name: Checkout Pillow + uses: actions/checkout@v2 - - uses: actions/checkout@v1 + - name: Checkout cached dependencies + uses: actions/checkout@v2 with: repository: python-pillow/pillow-depends - ref: master + path: winbuild\depends - - name: Install PyPy - if: "contains(matrix.python-version, 'pypy')" - run: | - curl -fsSL -o pypy3.zip "${{ matrix.pypy-url }}" - 7z x pypy3.zip "-o$env:RUNNER_WORKSPACE\" - mv "$env:RUNNER_WORKSPACE\${{ matrix.pypy-version }}" "$env:RUNNER_WORKSPACE\${{ matrix.python-version }}" - $env:PYTHON="$env:RUNNER_WORKSPACE\${{ matrix.python-version }}" - # set env: pythonLocation - Write-Host "`#`#[set-env name=pythonLocation;]$env:PYTHON" # old syntax https://github.com/actions/toolkit/issues/61 - Write-Host "::set-env name=pythonLocation::$env:PYTHON" # new syntax https://github.com/actions/toolkit/blob/5bb77ec03fea98332e41f9347c8fbb1ce1e48f4a/docs/commands.md - New-Item -ItemType SymbolicLink -Path "$env:PYTHON\python.exe" -Target "$env:PYTHON\pypy3.exe" - curl -fsSL -o get-pip.py https://bootstrap.pypa.io/get-pip.py - $env:PATH = "$env:PYTHON\bin;$env:PATH" - & $env:PYTHON\python.exe get-pip.py - shell: pwsh + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~\AppData\Local\pip\Cache + key: + ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.architecture }}-${{ hashFiles('**/.github/workflows/test-windows.yml') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.architecture }}- + ${{ runner.os }}-${{ matrix.python-version }}- # sets env: pythonLocation - name: Set up Python - if: "!contains(matrix.python-version, 'pypy')" - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.architecture }} - - name: pip install wheel pytest pytest-cov codecov + - name: Print build system information + run: python .github/workflows/system-info.py + + - name: python -m pip install wheel pytest pytest-cov + run: python -m pip install wheel pytest pytest-cov + + # TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: + - name: Upgrade setuptools + if: "contains(matrix.python-version, '3.8') || contains(matrix.python-version, '3.9')" + run: python -m pip install -U "setuptools>=49.3.2" + + - name: Install dependencies run: | - "%pythonLocation%\python.exe" -m pip install wheel pytest pytest-cov - pip install codecov - shell: cmd + 7z x winbuild\depends\nasm-2.14.02-win64.zip "-o$env:RUNNER_WORKSPACE\" + echo "$env:RUNNER_WORKSPACE\nasm-2.14.02" >> $env:GITHUB_PATH - - name: Fetch dependencies - run: | - 7z x ..\pillow-depends\nasm-2.14.02-win64.zip "-o$env:RUNNER_WORKSPACE\" - Write-Host "`#`#[add-path]$env:RUNNER_WORKSPACE\nasm-2.14.02" - Write-Host "::add-path::$env:RUNNER_WORKSPACE\nasm-2.14.02" + winbuild\depends\gs9533w32.exe /S + echo "C:\Program Files (x86)\gs\gs9.53.3\bin" >> $env:GITHUB_PATH - ..\pillow-depends\gs950w32.exe /S - Write-Host "`#`#[add-path]C:\Program Files (x86)\gs\gs9.50\bin" - Write-Host "::add-path::C:\Program Files (x86)\gs\gs9.50\bin" - - $env:PYTHON=$env:pythonLocation - xcopy ..\pillow-depends\*.zip $env:GITHUB_WORKSPACE\winbuild\ - xcopy ..\pillow-depends\*.tar.gz $env:GITHUB_WORKSPACE\winbuild\ - xcopy /s ..\pillow-depends\test_images\* $env:GITHUB_WORKSPACE\tests\images\ - cd $env:GITHUB_WORKSPACE/winbuild/ - python.exe $env:GITHUB_WORKSPACE\winbuild\build_dep.py - env: - EXECUTABLE: bin\python.exe + xcopy /S /Y winbuild\depends\test_images\* Tests\images\ shell: pwsh - - name: Build dependencies / libjpeg - if: false - run: | - REM FIXME uses /MT not /MD, see makefile.vc and win32.mak for more info + - name: Cache build + id: build-cache + uses: actions/cache@v2 + with: + path: winbuild\build + key: + ${{ hashFiles('winbuild\build_prepare.py') }}-${{ hashFiles('.github\workflows\test-windows.yml') }}-${{ env.pythonLocation }} - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - set BUILD=%GITHUB_WORKSPACE%\winbuild\build - cd /D %BUILD%\jpeg-9c - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - nmake -nologo -f makefile.vc setup-vc6 - nmake -nologo -f makefile.vc clean - nmake -nologo -f makefile.vc nodebug=1 libjpeg.lib cjpeg.exe djpeg.exe - copy /Y /B j*.h %INCLIB% - copy /Y /B *.lib %INCLIB% - copy /Y /B *.exe %INCLIB% - shell: cmd + - name: Prepare build + if: steps.build-cache.outputs.cache-hit != 'true' + run: | + & python.exe winbuild\build_prepare.py -v --python=$env:pythonLocation --srcdir + shell: pwsh - name: Build dependencies / libjpeg-turbo - run: | - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - set BUILD=%GITHUB_WORKSPACE%\winbuild\build - cd /D %BUILD%\libjpeg-turbo-2.0.3 - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF - set CMAKE=%CMAKE% -DENABLE_SHARED:BOOL=OFF -DWITH_JPEG8:BOOL=TRUE -DWITH_CRT_DLL:BOOL=TRUE -DCMAKE_BUILD_TYPE=Release - %CMAKE% -G "NMake Makefiles" . - nmake -nologo -f Makefile clean - nmake -nologo -f Makefile jpeg-static cjpeg-static djpeg-static - copy /Y /B j*.h %INCLIB% - copy /Y /B jpeg-static.lib %INCLIB%\libjpeg.lib - copy /Y /B cjpeg-static.exe %INCLIB%\cjpeg.exe - copy /Y /B djpeg-static.exe %INCLIB%\djpeg.exe - shell: cmd - + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libjpeg.cmd" - name: Build dependencies / zlib - run: | - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - set BUILD=%GITHUB_WORKSPACE%\winbuild\build - cd /D %BUILD%\zlib-1.2.11 - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - nmake -nologo -f win32\Makefile.msc clean - nmake -nologo -f win32\Makefile.msc zlib.lib - copy /Y /B z*.h %INCLIB% - copy /Y /B *.lib %INCLIB% - copy /Y /B zlib.lib %INCLIB%\z.lib - shell: cmd - - - name: Build dependencies / LibTIFF - run: | - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - set BUILD=%GITHUB_WORKSPACE%\winbuild\build - cd /D %BUILD%\tiff-4.1.0 - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - copy %GITHUB_WORKSPACE%\winbuild\tiff.opt nmake.opt - nmake -nologo -f makefile.vc clean - nmake -nologo -f makefile.vc lib - copy /Y /B libtiff\tiff*.h %INCLIB% - copy /Y /B libtiff\*.dll %INCLIB% - copy /Y /B libtiff\*.lib %INCLIB% - shell: cmd - + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_zlib.cmd" + - name: Build dependencies / LibTiff + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libtiff.cmd" - name: Build dependencies / WebP - run: | - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - set BUILD=%GITHUB_WORKSPACE%\winbuild\build - cd /D %BUILD%\libwebp-1.0.3 - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - rmdir /S /Q output\release-static - nmake -nologo -f Makefile.vc CFG=release-static OBJDIR=output ARCH=${{ matrix.architecture }} all - mkdir %INCLIB%\webp - copy /Y /B src\webp\*.h %INCLIB%\webp - copy /Y /B output\release-static\${{ matrix.architecture }}\lib\* %INCLIB% - shell: cmd - + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libwebp.cmd" + # for FreeType CBDT font support + - name: Build dependencies / libpng + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libpng.cmd" - name: Build dependencies / FreeType - run: | - REM Toolkit v100 not available; missing VCTargetsPath; Clean fails - - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - set BUILD=%GITHUB_WORKSPACE%\winbuild\build - cd /D %BUILD%\freetype-2.10.1 - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - rmdir /S /Q objs - set DefaultPlatformToolset=v142 - set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Microsoft\VC\v160\ - set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe" - powershell -Command "(gc builds\windows\vc2010\freetype.vcxproj) -replace 'MultiThreaded<', 'MultiThreadedDLL<' | Out-File -encoding ASCII builds\windows\vc2010\freetype.vcxproj" - %MSBUILD% builds\windows\vc2010\freetype.sln /t:Build /p:Configuration="Release Static" /p:Platform=${{ matrix.platform-msbuild }} /m - xcopy /Y /E /Q include %INCLIB% - copy /Y /B "objs\${{ matrix.platform-msbuild }}\Release Static\freetype.lib" %INCLIB% - shell: cmd - + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_freetype.cmd" - name: Build dependencies / LCMS2 - run: | - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - set BUILD=%GITHUB_WORKSPACE%\winbuild\build - cd /D %BUILD%\lcms2-2.8 - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - rmdir /S /Q Lib - rmdir /S /Q Projects\VC2015\Release - set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Microsoft\VC\v160\ - set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe" - powershell %GITHUB_WORKSPACE%\winbuild\lcms2_patch.ps1 - %MSBUILD% Projects\VC2015\lcms2.sln /t:Clean;lcms2_static /p:Configuration="Release" /p:Platform=${{ matrix.platform-msbuild }} /m - xcopy /Y /E /Q include %INCLIB% - copy /Y /B Lib\MS\*.lib %INCLIB% - shell: cmd - + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_lcms2.cmd" - name: Build dependencies / OpenJPEG - run: | - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - set BUILD=%GITHUB_WORKSPACE%\winbuild\build - cd /D %BUILD%\openjpeg-2.3.1msvcr10-x32 - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF - set CMAKE=%CMAKE% -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF - set CMAKE=%CMAKE% -DCMAKE_BUILD_TYPE=Release - %CMAKE% -G "NMake Makefiles" . - nmake -nologo -f Makefile clean - nmake -nologo -f Makefile - mkdir %INCLIB%\openjpeg-2.3.1 - copy /Y /B src\lib\openjp2\*.h %INCLIB%\openjpeg-2.3.1 - copy /Y /B bin\*.lib %INCLIB% - shell: cmd + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_openjpeg.cmd" - # GPL licensed; skip if building wheels + # GPL licensed - name: Build dependencies / libimagequant - if: "github.event_name != 'push' || contains(matrix.python-version, 'pypy')" - run: | - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - set BUILD=%GITHUB_WORKSPACE%\winbuild\build - rem ba653c8: Merge tag '2.12.5' into msvc - cd /D %BUILD%\libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971 - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - echo (gc CMakeLists.txt) -replace 'add_library', "add_compile_options(-openmp-)`r`nadd_library" ^| Out-File -encoding ASCII CMakeLists.txt > patch.ps1 - echo (gc CMakeLists.txt) -replace ' SHARED', ' STATIC' ^| Out-File -encoding ASCII CMakeLists.txt >> patch.ps1 - powershell .\patch.ps1 - set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF - set CMAKE=%CMAKE% -DCMAKE_BUILD_TYPE=Release - %CMAKE% -G "NMake Makefiles" . - nmake -nologo -f Makefile clean - nmake -nologo -f Makefile - copy /Y /B *.h %INCLIB% - copy /Y /B *.lib %INCLIB% - shell: cmd + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libimagequant.cmd" - # for Raqm + # Raqm dependencies - name: Build dependencies / HarfBuzz - run: | - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - set BUILD=%GITHUB_WORKSPACE%\winbuild\build - set INCLUDE=%INCLUDE%;%INCLIB% - set LIB=%LIB%;%INCLIB% - cd /D %BUILD%\harfbuzz-2.6.1 - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF - set CMAKE=%CMAKE% -DHB_HAVE_FREETYPE:BOOL=ON -DCMAKE_BUILD_TYPE=Release - %CMAKE% -G "NMake Makefiles" . - nmake -nologo -f Makefile clean - nmake -nologo -f Makefile harfbuzz - copy /Y /B src\*.h %INCLIB% - copy /Y /B *.lib %INCLIB% - shell: cmd - - # for Raqm + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_harfbuzz.cmd" - name: Build dependencies / FriBidi - run: | - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - set BUILD=%GITHUB_WORKSPACE%\winbuild\build - cd /D %BUILD%\fribidi-1.0.7 - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - copy /Y /B %GITHUB_WORKSPACE%\winbuild\fribidi.cmake CMakeLists.txt - set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF - set CMAKE=%CMAKE% -DCMAKE_BUILD_TYPE=Release - %CMAKE% -G "NMake Makefiles" . - nmake -nologo -f Makefile clean - nmake -nologo -f Makefile fribidi - copy /Y /B lib\*.h %INCLIB% - copy /Y /B *.lib %INCLIB% - shell: cmd - + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_fribidi.cmd" - name: Build dependencies / Raqm - run: | - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - set BUILD=%GITHUB_WORKSPACE%\winbuild\build - set INCLUDE=%INCLUDE%;%INCLIB% - set LIB=%LIB%;%INCLIB% - cd /D %BUILD%\libraqm-0.7.0 - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - copy /Y /B %GITHUB_WORKSPACE%\winbuild\raqm.cmake CMakeLists.txt - set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF - set CMAKE=%CMAKE% -DCMAKE_BUILD_TYPE=Release - %CMAKE% -G "NMake Makefiles" . - nmake -nologo -f Makefile clean - nmake -nologo -f Makefile libraqm - copy /Y /B src\*.h %INCLIB% - copy /Y /B libraqm.dll %INCLIB% + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libraqm.cmd" + + # trim ~150MB x 9 + - name: Optimize build cache + if: steps.build-cache.outputs.cache-hit != 'true' + run: rmdir /S /Q winbuild\build\src shell: cmd - name: Build Pillow run: | - set PYTHON=%pythonLocation% - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set MPLSRC=%GITHUB_WORKSPACE% - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - cd /D %GITHUB_WORKSPACE% - set LIB=%INCLIB%;%PYTHON%\tcl - set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - set MSSdk=1 - set DISTUTILS_USE_SDK=1 - set py_vcruntime_redist=true - %PYTHON%\python.exe setup.py build_ext install - rem Add libraqm.dll (copied to INCLIB) to PATH. - path %INCLIB%;%PATH% - %PYTHON%\python.exe selftest.py --installed - shell: cmd + $FLAGS="" + if ('${{ github.event_name }}' -eq 'push') { $FLAGS="--disable-imagequant" } + & winbuild\build\build_pillow.cmd $FLAGS install + & $env:pythonLocation\python.exe selftest.py --installed + shell: pwsh # failing with PyPy3 - name: Enable heap verification if: "!contains(matrix.python-version, 'pypy')" - run: | - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\python.exe - shell: cmd + run: "& 'C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x86\\gflags.exe' /p /enable $env:pythonLocation\\python.exe" - name: Test Pillow run: | - set PYTHON=%pythonLocation% - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - rem Add libraqm.dll (copied to INCLIB) to PATH. - path %INCLIB%;%PATH% - cd /D %GITHUB_WORKSPACE% - %PYTHON%\python.exe -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests + path %GITHUB_WORKSPACE%\\winbuild\\build\\bin;%PATH% + python.exe -m pytest -vx -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests shell: cmd + - name: Prepare to upload errors + if: failure() + run: | + mkdir -p Tests/errors + shell: pwsh + - name: Upload errors - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 if: failure() with: name: errors path: Tests/errors + - name: After success + run: | + .ci/after_success.sh + shell: pwsh + - name: Upload coverage - run: 'codecov --file "%GITHUB_WORKSPACE%\coverage.xml" --name "%pythonLocation%"' - shell: cmd + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml + flags: GHA_Windows + name: ${{ runner.os }} Python ${{ matrix.python-version }} ${{ matrix.architecture }} - name: Build wheel id: wheel - if: "github.event_name == 'push' && !contains(matrix.python-version, 'pypy')" + # Skip wheels on 3.10 due to https://github.com/pypa/wheel/issues/354 + if: "github.event_name == 'push' && !contains(matrix.python-version, '3.10')" run: | - for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ##[set-output name=dist;]dist-%%a for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a - set PYTHON=%pythonLocation% - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include - set MPLSRC=%GITHUB_WORKSPACE% - set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - cd /D %GITHUB_WORKSPACE% - set LIB=%INCLIB%;%PYTHON%\tcl - set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - %PYTHON%\python.exe setup.py bdist_wheel + winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel shell: cmd - - uses: actions/upload-artifact@v1 - if: "github.event_name == 'push' && !contains(matrix.python-version, 'pypy')" + - uses: actions/upload-artifact@v2 + # Skip wheels on 3.10 due to https://github.com/pypa/wheel/issues/354 + if: "github.event_name == 'push' && !contains(matrix.python-version, '3.10')" with: name: ${{ steps.wheel.outputs.dist }} - path: dist + path: dist\*.whl + + msys: + runs-on: windows-2019 + + strategy: + fail-fast: false + matrix: + mingw: ["MINGW32", "MINGW64"] + include: + - mingw: "MINGW32" + name: "MSYS2 MinGW 32-bit" + package: "mingw-w64-i686" + - mingw: "MINGW64" + name: "MSYS2 MinGW 64-bit" + package: "mingw-w64-x86_64" + + defaults: + run: + shell: bash.exe --login -eo pipefail "{0}" + env: + MSYSTEM: ${{ matrix.mingw }} + CHERE_INVOKING: 1 + + timeout-minutes: 30 + name: ${{ matrix.name }} + + steps: + - uses: actions/checkout@v2 + + - name: Set up shell + run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH + shell: pwsh + + - name: Install Dependencies + run: | + pacman -S --noconfirm \ + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python3-pyqt5 \ + ${{ matrix.package }}-python3-pytest \ + ${{ matrix.package }}-python3-pytest-cov \ + ${{ matrix.package }}-python3-setuptools \ + ${{ matrix.package }}-freetype \ + ${{ matrix.package }}-ghostscript \ + ${{ matrix.package }}-lcms2 \ + ${{ matrix.package }}-libimagequant \ + ${{ matrix.package }}-libjpeg-turbo \ + ${{ matrix.package }}-libraqm \ + ${{ matrix.package }}-libtiff \ + ${{ matrix.package }}-libwebp \ + ${{ matrix.package }}-openjpeg2 \ + subversion + + python3 -m pip install pyroma + + pushd depends && ./install_extra_test_images.sh && popd + + - name: Build Pillow + run: CFLAGS="-coverage" python3 setup.py build_ext install + + - name: Test Pillow + run: | + python3 selftest.py --installed + python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests + + - name: Upload coverage + run: | + python3 -m pip install codecov + bash <(curl -s https://codecov.io/bash) -F GHA_Windows + env: + CODECOV_NAME: ${{ matrix.name }} + + success: + needs: [build, msys] + runs-on: ubuntu-latest + name: Windows Test Successful + steps: + - name: Success + run: echo Windows Test Successful diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 06caa4316..3da945f39 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,63 +14,105 @@ jobs: ] python-version: [ "pypy3", + "3.10-dev", + "3.9", "3.8", "3.7", "3.6", - "3.5", ] include: - - python-version: "3.5" - env: PYTHONOPTIMIZE=2 - python-version: "3.6" env: PYTHONOPTIMIZE=1 + - python-version: "3.7" + env: PYTHONOPTIMIZE=2 + # Include new variables for Codecov + - os: ubuntu-latest + codecov-flag: GHA_Ubuntu + - os: macOS-latest + codecov-flag: GHA_macOS + runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(python3 -m pip cache dir)" + + - name: pip cache + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: + ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/.ci/*.sh') }} + restore-keys: | + ${{ matrix.os }}-${{ matrix.python-version }}- + + - name: Build system information + run: python .github/workflows/system-info.py + - name: Install Linux dependencies if: startsWith(matrix.os, 'ubuntu') run: | - .travis/install.sh + .ci/install.sh + env: + GHA_PYTHON_VERSION: ${{ matrix.python-version }} - name: Install macOS dependencies if: startsWith(matrix.os, 'macOS') run: | .github/workflows/macos-install.sh + env: + GHA_PYTHON_VERSION: ${{ matrix.python-version }} - name: Build run: | - .travis/build.sh + .ci/build.sh - name: Test run: | - .travis/test.sh + .ci/test.sh + + - name: Prepare to upload errors + if: failure() + run: | + mkdir -p Tests/errors + shell: pwsh - name: Upload errors - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 if: failure() with: name: errors path: Tests/errors - name: Docs - if: matrix.python-version == 3.8 + if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9 run: | - pip install sphinx-rtd-theme + python3 -m pip install sphinx-removed-in sphinx-rtd-theme make doccheck - name: After success - if: success() run: | - .travis/after_success.sh + .ci/after_success.sh + + - name: Upload coverage + run: bash <(curl -s https://codecov.io/bash) -F ${{ matrix.codecov-flag }} env: - MATRIX_OS: ${{ matrix.os }} - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + CODECOV_NAME: ${{ matrix.os }} Python ${{ matrix.python-version }} + + success: + needs: build + runs-on: ubuntu-latest + name: Test Successful + steps: + - name: Success + run: echo Test Successful diff --git a/.gitignore b/.gitignore index 585120056..b4205c443 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,10 @@ pip-delete-this-directory.txt # Extra test images installed from pillow-depends/test_images /depends/test_images /Tests/images/README.md +/Tests/images/crash_1.tif +/Tests/images/crash_2.tif +/Tests/images/string_dimension.tiff +/Tests/images/jpeg2000 /Tests/images/msp /Tests/images/picins /Tests/images/sunraster @@ -83,4 +87,4 @@ pip-delete-this-directory.txt .DS_Store # JetBrains -.idea +.idea \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..459251d77 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,43 @@ +repos: + - repo: https://github.com/psf/black + rev: e66be67b9b6811913470f70c28b4d50f94d05b22 # frozen: 20.8b1 + hooks: + - id: black + args: ["--target-version", "py36"] + # Only .py files, until https://github.com/psf/black/issues/402 resolved + files: \.py$ + types: [] + + - repo: https://github.com/timothycrosley/isort + rev: 377d260ffa6f746693f97b46d95025afc4bd8275 # frozen: 5.4.2 + hooks: + - id: isort + + - repo: https://github.com/asottile/yesqa + rev: 7a009f3ee493c796827ee334f9058b110a0e0db8 # frozen: v1.2.1 + hooks: + - id: yesqa + + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: f30f4974a08a6b2f6a1eeaf30a4d501cf909163a # frozen: v1.1.9 + hooks: + - id: remove-tabs + exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) + + - repo: https://gitlab.com/pycqa/flake8 + rev: 05f6544aef321e2fee03a1277ce2eef8880fb927 # frozen: 3.8.3 + hooks: + - id: flake8 + additional_dependencies: [flake8-2020, flake8-implicit-str-concat] + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: eae6397e4c259ed3d057511f6dd5330b92867e62 # frozen: v1.6.0 + hooks: + - id: python-check-blanket-noqa + - id: rst-backticks + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: e1668fe86af3810fbca72b8653fe478e66a0afdc # frozen: v3.2.0 + hooks: + - id: check-merge-conflict + - id: check-yaml diff --git a/.travis.yml b/.travis.yml index 9dea5fb14..2966cdf19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,16 @@ dist: xenial language: python -cache: pip +cache: + pip: true + directories: + - $HOME/.cache/pre-commit notifications: irc: "chat.freenode.net#pil" # Run fast lint first to get fast feedback. -# Run slow PyPy next, to give it a headstart and reduce waiting time. -# Run latest 3.x next, to get quick compatibility results. -# Then run the remainder, with fastest Docker jobs last. +# Run slower CPUs next, to give them a headstart and reduce waiting time. +# Then run the remainder. matrix: fast_finish: true @@ -16,64 +18,50 @@ matrix: - python: "3.6" name: "Lint" env: LINT="true" - - python: "pypy3" + + - python: "3.6" + arch: arm64 + - python: "3.7" + arch: ppc64le + - python: "3.8" + arch: s390x + + - python: "pypy3.6-7.3.1" name: "PyPy3 Xenial" + - python: "3.9" + name: "3.9 Xenial" + services: xvfb - python: "3.8" name: "3.8 Xenial" services: xvfb - python: '3.7' - name: "3.7 Xenial" + name: "3.7 Xenial PYTHONOPTIMIZE=2" + env: PYTHONOPTIMIZE=2 services: xvfb - python: '3.6' name: "3.6 Xenial PYTHONOPTIMIZE=1" env: PYTHONOPTIMIZE=1 services: xvfb - - python: '3.5' - name: "3.5 Xenial PYTHONOPTIMIZE=2" - env: PYTHONOPTIMIZE=2 - services: xvfb - - env: DOCKER="alpine" DOCKER_TAG="master" - - env: DOCKER="arch" DOCKER_TAG="master" # contains PyQt5 - - env: DOCKER="ubuntu-16.04-xenial-amd64" DOCKER_TAG="master" - - env: DOCKER="ubuntu-18.04-bionic-amd64" DOCKER_TAG="master" - - env: DOCKER="debian-9-stretch-x86" DOCKER_TAG="master" - - env: DOCKER="debian-10-buster-x86" DOCKER_TAG="master" - - env: DOCKER="centos-6-amd64" DOCKER_TAG="master" - - env: DOCKER="centos-7-amd64" DOCKER_TAG="master" - - env: DOCKER="amazon-1-amd64" DOCKER_TAG="master" - - env: DOCKER="amazon-2-amd64" DOCKER_TAG="master" - - env: DOCKER="fedora-30-amd64" DOCKER_TAG="master" - - env: DOCKER="fedora-31-amd64" DOCKER_TAG="master" - -services: - - docker - -before_install: - - if [ "$DOCKER" ]; then travis_retry docker pull pythonpillow/$DOCKER:$DOCKER_TAG; fi install: - | if [ "$LINT" == "true" ]; then - pip install tox - elif [ "$DOCKER" == "" ]; then - .travis/install.sh; + python3 -m pip install tox + else + .ci/install.sh; fi script: - | if [ "$LINT" == "true" ]; then tox -e lint - elif [ "$DOCKER" == "" ]; then - .travis/build.sh - .travis/test.sh - elif [ "$DOCKER" ]; then - # the Pillow user in the docker container is UID 1000 - sudo chown -R 1000 $TRAVIS_BUILD_DIR - docker run -v $TRAVIS_BUILD_DIR:/Pillow pythonpillow/$DOCKER:$DOCKER_TAG + else + .ci/build.sh + .ci/test.sh fi after_success: - | if [ "$LINT" == "" ]; then - .travis/after_success.sh + .ci/after_success.sh fi diff --git a/.travis/after_success.sh b/.travis/after_success.sh deleted file mode 100755 index badf04484..000000000 --- a/.travis/after_success.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -# gather the coverage data -if [[ "$MATRIX_OS" == "macOS-latest" ]]; then - brew install lcov -else - sudo apt-get -qq install lcov -fi - -lcov --capture --directory . -b . --output-file coverage.info -# filter to remove system headers -lcov --remove coverage.info '/usr/*' -o coverage.filtered.info -# convert to json -gem install coveralls-lcov -coveralls-lcov -v -n coverage.filtered.info > coverage.c.json - -coverage report -pip install codecov -pip install coveralls-merge -coveralls-merge coverage.c.json -codecov - -if [ "$TRAVIS_PYTHON_VERSION" == "3.7" ] && [ "$DOCKER" == "" ]; then - # Coverage and quality reports on just the latest diff. - depends/diffcover-install.sh - depends/diffcover-run.sh -fi diff --git a/.travis/install.sh b/.travis/install.sh deleted file mode 100755 index 747acb448..000000000 --- a/.travis/install.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -set -e - -sudo apt-get update -sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk\ - ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ - cmake imagemagick libharfbuzz-dev libfribidi-dev - -PYTHONOPTIMIZE=0 pip install cffi -pip install coverage -pip install olefile -pip install -U pytest -pip install -U pytest-cov -pip install pyroma -pip install test-image-results -pip install numpy -if [[ $TRAVIS_PYTHON_VERSION == 3.* ]]; then - sudo apt-get -qq install pyqt5-dev-tools - pip install pyqt5 -fi - -# docs only on Python 3.7 -if [ "$TRAVIS_PYTHON_VERSION" == "3.7" ]; then pip install -r requirements.txt ; fi - -# webp -pushd depends && ./install_webp.sh && popd - -# libimagequant -pushd depends && ./install_imagequant.sh && popd - -# raqm -pushd depends && ./install_raqm.sh && popd - -# extra test images -pushd depends && ./install_extra_test_images.sh && popd diff --git a/.travis/test.sh b/.travis/test.sh deleted file mode 100755 index a65e620b8..000000000 --- a/.travis/test.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -set -e - -python -m pytest -v -x --cov PIL --cov-report term Tests - -# Docs -if [ "$TRAVIS_PYTHON_VERSION" == "3.7" ]; then make doccheck; fi diff --git a/CHANGES.rst b/CHANGES.rst index d3922f328..e7090219f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,12 +2,366 @@ Changelog (Pillow) ================== -7.0.0 (unreleased) +8.1.0 (unreleased) +------------------ + +- Support raw rgba8888 for DDS #4760 + [qiankanglai] + +8.0.1 (2020-10-22) +------------------ + +- Update FreeType used in binary wheels to 2.10.4 to fix CVE-2020-15999. + [radarhere] + +- Moved string_dimension image to pillow-depends #4993 + [radarhere] + +8.0.0 (2020-10-15) +------------------ + +- Drop support for EOL Python 3.5 #4746, #4794 + [hugovk, radarhere, nulano] + +- Drop support for PyPy3 < 7.2.0 #4964 + [nulano] + +- Remove ImageCms.CmsProfile attributes deprecated since 3.2.0 #4768 + [hugovk, radarhere] + +- Remove long-deprecated Image.py functions #4798 + [hugovk, nulano, radarhere] + +- Add support for 16-bit precision JPEG quantization values #4918 + [gofr] + +- Added reading of IFD tag type #4979 + [radarhere] + +- Initialize offset memory for PyImagingPhotoPut #4806 + [nqbit] + +- Fix TiffDecode comparison warnings #4756 + [nulano] + +- Docs: Add dark mode #4968 + [hugovk, nulano] + +- Added macOS SDK install path to library and include directories #4974 + [radarhere, fxcoudert] + +- Imaging.h: prevent confusion with system #4923 + [ax3l, ,radarhere] + +- Avoid using pkg_resources in PIL.features.pilinfo #4975 + [nulano] + +- Add getlength and getbbox functions for TrueType fonts #4959 + [nulano, radarhere, hugovk] + +- Allow tuples with one item to give single color value in getink #4927 + [radarhere, nulano] + +- Add support for CBDT and COLR fonts #4955 + [nulano, hugovk] + +- Removed OSError in favour of DecompressionBombError for BMP #4966 + [radarhere] + +- Implemented another ellipse drawing algorithm #4523 + [xtsm, radarhere] + +- Removed unused JpegImagePlugin._fixup_dict function #4957 + [radarhere] + +- Added reading and writing of private PNG chunks #4292 + [radarhere] + +- Implement anchor for TrueType fonts #4930 + [nulano, hugovk] + +- Fixed bug in Exif __delitem__ #4942 + [radarhere] + +- Fix crash in ImageTk.PhotoImage on MinGW 64-bit #4946 + [nulano] + +- Moved CVE images to pillow-depends #4929 + [radarhere] + +- Refactor font_getsize and font_render #4910 + [nulano] + +- Fixed loading profile with non-ASCII path on Windows #4914 + [radarhere] + +- Fixed effect_spread bug for zero distance #4908 + [radarhere, hugovk] + +- Added formats parameter to Image.open #4837 + [nulano, radarhere] + +- Added regular_polygon draw method #4846 + [comhar] + +- Raise proper TypeError in putpixel #4882 + [nulano, hugovk] + +- Added writing of subIFDs #4862 + [radarhere] + +- Fix IFDRational __eq__ bug #4888 + [luphord, radarhere] + +- Fixed duplicate variable name #4885 + [liZe, radarhere] + +- Added homebrew zlib include directory #4842 + [radarhere] + +- Corrected inverted PDF CMYK colors #4866 + [radarhere] + +- Do not try to close file pointer if file pointer is empty #4823 + [radarhere] + +- ImageOps.autocontrast: add mask parameter #4843 + [navneeth, hugovk] + +- Read EXIF data tEXt chunk into info as bytes instead of string #4828 + [radarhere] + +- Replaced distutils with setuptools #4797, #4809, #4814, #4817, #4829, #4890 + [hugovk, radarhere] + +- Add MIME type to PsdImagePlugin #4788 + [samamorgan] + +- Allow ImageOps.autocontrast to specify low and high cutoffs separately #4749 + [millionhz, radarhere] + +7.2.0 (2020-07-01) +------------------ + +- Do not convert I;16 images when showing PNGs #4744 + [radarhere] + +- Fixed ICNS file pointer saving #4741 + [radarhere] + +- Fixed loading non-RGBA mode APNGs with dispose background #4742 + [radarhere] + +- Deprecated _showxv #4714 + [radarhere] + +- Deprecate Image.show(command="...") #4646 + [nulano, hugovk, radarhere] + +- Updated JPEG magic number #4707 + [Cykooz, radarhere] + +- Change STRIPBYTECOUNTS to LONG if necessary when saving #4626 + [radarhere, hugovk] + +- Write JFIF header when saving JPEG #4639 + [radarhere] + +- Replaced tiff_jpeg with jpeg compression when saving TIFF images #4627 + [radarhere] + +- Writing TIFF tags: improved BYTE, added UNDEFINED #4605 + [radarhere] + +- Consider transparency when pasting text on an RGBA image #4566 + [radarhere] + +- Added method argument to single frame WebP saving #4547 + [radarhere] + +- Use ImageFileDirectory_v2 in Image.Exif #4637 + [radarhere] + +- Corrected reading EXIF metadata without prefix #4677 + [radarhere] + +- Fixed drawing a jointed line with a sequence of numeric values #4580 + [radarhere] + +- Added support for 1-D NumPy arrays #4608 + [radarhere] + +- Parse orientation from XMP tags #4560 + [radarhere] + +- Speed up text layout by not rendering glyphs #4652 + [nulano] + +- Fixed ZeroDivisionError in Image.thumbnail #4625 + [radarhere] + +- Replaced TiffImagePlugin DEBUG with logging #4550 + [radarhere] + +- Fix repeatedly loading .gbr #4620 + [ElinksFr, radarhere] + +- JPEG: Truncate icclist instead of setting to None #4613 + [homm] + +- Fixes default offset for Exif #4594 + [rodrigob, radarhere] + +- Fixed bug when unpickling TIFF images #4565 + [radarhere] + +- Fix pickling WebP #4561 + [hugovk, radarhere] + +- Replace IOError and WindowsError aliases with OSError #4536 + [hugovk, radarhere] + +7.1.2 (2020-04-25) +------------------ + +- Raise an EOFError when seeking too far in PNG #4528 + [radarhere] + +7.1.1 (2020-04-02) +------------------ + +- Fix regression seeking and telling PNGs #4512 #4514 + [hugovk, radarhere] + +7.1.0 (2020-04-01) +------------------ + +- Fix multiple OOB reads in FLI decoding #4503 + [wiredfool] + +- Fix buffer overflow in SGI-RLE decoding #4504 + [wiredfool, hugovk] + +- Fix bounds overflow in JPEG 2000 decoding #4505 + [wiredfool] + +- Fix bounds overflow in PCX decoding #4506 + [wiredfool] + +- Fix 2 buffer overflows in TIFF decoding #4507 + [wiredfool] + +- Add APNG support #4243 + [pmrowla, radarhere, hugovk] + +- ImageGrab.grab() for Linux with XCB #4260 + [nulano, radarhere] + +- Added three new channel operations #4230 + [dwastberg, radarhere] + +- Prevent masking of Image reduce method in Jpeg2KImagePlugin #4474 + [radarhere, homm] + +- Added reading of earlier ImageMagick PNG EXIF data #4471 + [radarhere] + +- Fixed endian handling for I;16 getextrema #4457 + [radarhere] + +- Release buffer if function returns prematurely #4381 + [radarhere] + +- Add JPEG comment to info dictionary #4455 + [radarhere] + +- Fix size calculation of Image.thumbnail() #4404 + [orlnub123] + +- Fixed stroke on FreeType < 2.9 #4401 + [radarhere] + +- If present, only use alpha channel for bounding box #4454 + [radarhere] + +- Warn if an unknown feature is passed to features.check() #4438 + [jdufresne] + +- Fix Name field length when saving IM images #4424 + [hugovk, radarhere] + +- Allow saving of zero quality JPEG images #4440 + [radarhere] + +- Allow explicit zero width to hide outline #4334 + [radarhere] + +- Change ContainerIO return type to match file object mode #4297 + [jdufresne, radarhere] + +- Only draw each polygon pixel once #4333 + [radarhere] + +- Add support for shooting situation Exif IFD tags #4398 + [alexagv] + +- Handle multiple and malformed JPEG APP13 markers #4370 + [homm] + +- Depends: Update libwebp to 1.1.0 #4342, libjpeg to 9d #4352 + [radarhere] + +7.0.0 (2020-01-02) ------------------ - Drop support for EOL Python 2.7 #4109 [hugovk, radarhere, jdufresne] +- Fix rounding error on RGB to L conversion #4320 + [homm] + +- Exif writing fixes: Rational boundaries and signed/unsigned types #3980 + [kkopachev, radarhere] + +- Allow loading of WMF images at a given DPI #4311 + [radarhere] + +- Added reduce operation #4251 + [homm] + +- Raise ValueError for io.StringIO in Image.open #4302 + [radarhere, hugovk] + +- Fix thumbnail geometry when DCT scaling is used #4231 + [homm, radarhere] + +- Use default DPI when exif provides invalid x_resolution #4147 + [beipang2, radarhere] + +- Change default resize resampling filter from NEAREST to BICUBIC #4255 + [homm] + +- Fixed black lines on upscaled images with the BOX filter #4278 + [homm] + +- Better thumbnail aspect ratio preservation #4256 + [homm] + +- Add La mode packing and unpacking #4248 + [homm] + +- Include tests in coverage reports #4173 + [hugovk] + +- Handle broken Photoshop data #4239 + [radarhere] + +- Raise a specific exception if no data is found for an MPO frame #4240 + [radarhere] + +- Fix Unicode support for PyPy #4145 + [nulano] + - Added UnidentifiedImageError #4182 [radarhere, hugovk] @@ -23,7 +377,7 @@ Changelog (Pillow) - Corrected DdsImagePlugin setting info gamma #4171 [radarhere] -- Depends: Update libtiff to 4.1.0 #4195, Tk Tcl to 8.6.10 #4229 +- Depends: Update libtiff to 4.1.0 #4195, Tk Tcl to 8.6.10 #4229, libimagequant to 2.12.6 #4318 [radarhere] - Improve handling of file resources #3577 @@ -47,11 +401,29 @@ Changelog (Pillow) - Changed default frombuffer raw decoder args #1730 [radarhere] -6.2.1 (2019-10-21) +6.2.2 (2020-01-02) ------------------ - This is the last Pillow release to support Python 2.7 #3642 +- Overflow checks for realloc for tiff decoding. CVE-2020-5310 + [wiredfool, radarhere] + +- Catch SGI buffer overrun. CVE-2020-5311 + [radarhere] + +- Catch PCX P mode buffer overrun. CVE-2020-5312 + [radarhere] + +- Catch FLI buffer overrun. CVE-2020-5313 + [radarhere] + +- Raise an error for an invalid number of bands in FPX image. CVE-2019-19911 + [wiredfool, radarhere] + +6.2.1 (2019-10-21) +------------------ + - Add support for Python 3.8 #4141 [hugovk] @@ -3714,7 +4086,7 @@ Pre-fork This section may not be fully complete. For changes since this file was last updated, see the repository revision history: - https://bitbucket.org/effbot/pil-2009-raclette/commits/all + http://svn.effbot.org/public/pil/ (1.1.7 final) @@ -5148,23 +5520,23 @@ Pre-fork + Added keyword options to the "save" method. The following options are currently supported: - format option description + Format Option Description -------------------------------------------------------- - JPEG optimize minimize output file at the - expense of compression speed. + JPEG optimize Minimize output file at the + expense of compression speed. - JPEG progressive enable progressive output. the - option value is ignored. + JPEG progressive Enable progressive output. + The option value is ignored. - JPEG quality set compression quality (1-100). - the default value is 75. + JPEG quality Set compression quality (1-100). + The default value is 75. - JPEG smooth smooth dithered images. value - is strength (1-100). default is - off (0). + JPEG smooth Smooth dithered images. + Value is strength (1-100). + Default is off (0). - PNG optimize minimize output file at the - expense of compression speed. + PNG optimize Minimize output file at the + expense of compression speed. Expect more options in future releases. Also note that file writers silently ignore unknown options. @@ -5185,31 +5557,31 @@ Pre-fork + Various improvements to the sample scripts: "pilconvert" Carries out some extra tricks in order to make - the resulting file as small as possible. + the resulting file as small as possible. - "explode" (NEW) Split an image sequence into individual frames. + "explode" (NEW) Split an image sequence into individual frames. - "gifmaker" (NEW) Convert a sequence file into a GIF animation. - Note that the GIF encoder create "uncompressed" GIF - files, so animations created by this script are - rather large (typically 2-5 times the compressed - sizes). + "gifmaker" (NEW) Convert a sequence file into a GIF animation. + Note that the GIF encoder create "uncompressed" GIF + files, so animations created by this script are + rather large (typically 2-5 times the compressed + sizes). - "image2py" (NEW) Convert a single image to a python module. See - comments in this script for details. + "image2py" (NEW) Convert a single image to a python module. See + comments in this script for details. - "player" If multiple images are given on the command line, - they are interpreted as frames in a sequence. The - script assumes that they all have the same size. - Also note that this script now can play FLI/FLC - and GIF animations. + "player" If multiple images are given on the command line, + they are interpreted as frames in a sequence. The + script assumes that they all have the same size. + Also note that this script now can play FLI/FLC + and GIF animations. This player can also execute embedded Python animation applets (ARG format only). - "viewer" Transparent images ("P" with transparency property, - and "RGBA") are superimposed on the standard Tk back- - ground. + "viewer" Transparent images ("P" with transparency property, + and "RGBA") are superimposed on the standard Tk back- + ground. + Fixed colour argument to "new". For multilayer images, pass a tuple: (Red, Green, Blue), (Red, Green, Blue, Alpha), or (Cyan, @@ -5347,7 +5719,7 @@ Pre-fork any other pixel value means opaque. This is faster than using an "L" transparency mask. - + Properly writes EPS files (and properly prints images to postscript + + Properly writes EPS files (and properly prints images to PostScript printers as well). + Reads 4-bit BMP files, as well as 4 and 8-bit Windows ICO and CUR @@ -5430,7 +5802,7 @@ Pre-fork + Added the "pilfile" utility, which quickly identifies image files (without loading them, in most cases). - + Added the "pilprint" utility, which prints image files to Postscript + + Added the "pilprint" utility, which prints image files to PostScript printers. + Added a rudimentary version of the "pilview" utility, which is @@ -5444,5 +5816,5 @@ Pre-fork Jack). This allows you to read images through the Img extensions file format handlers. See the file "Lib/ImgExtImagePlugin.py" for details. - + Postscript printing is provided through the PSDraw module. See the + + PostScript printing is provided through the PSDraw module. See the handbook for details. diff --git a/LICENSE b/LICENSE index c106eeb1a..eb9c41f56 100644 --- a/LICENSE +++ b/LICENSE @@ -5,12 +5,26 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2019 by Alex Clark and contributors + Copyright © 2010-2020 by Alex Clark and contributors -Like PIL, Pillow is licensed under the open source PIL Software License: +Like PIL, Pillow is licensed under the open source HPND License: -By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: +By obtaining, using, and/or copying this software and/or its associated +documentation, you agree that you have read, understood, and will comply +with the following terms and conditions: -Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. +Permission to use, copy, modify, and distribute this software and its +associated documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appears in all copies, and that +both that copyright notice and this permission notice appear in supporting +documentation, and that the name of Secret Labs AB or the author not be +used in advertising or publicity pertaining to distribution of the software +without specific, written prior permission. -SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, +INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index 79f4e2adb..f5d367fdd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,7 @@ include *.py include *.rst include *.sh include *.txt +include *.yaml include LICENSE include Makefile include tox.ini @@ -18,12 +19,10 @@ graft docs # build/src control detritus exclude .appveyor.yml exclude .coveragerc -exclude .codecov.yml exclude .editorconfig exclude .readthedocs.yml -exclude azure-pipelines.yml +exclude codecov.yml global-exclude .git* global-exclude *.pyc global-exclude *.so -prune .azure-pipelines -prune .travis +prune .ci diff --git a/Makefile b/Makefile index fdde3416b..99232cac9 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ -# https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html -.PHONY: clean coverage doc docserve help inplace install install-req release-test sdist test upload upload-test .DEFAULT_GOAL := release-test +.PHONY: clean clean: python3 setup.py clean rm src/PIL/*.so || true @@ -9,29 +8,34 @@ clean: find . -name __pycache__ | xargs rm -r || true BRANCHES=`git branch -a | grep -v HEAD | grep -v master | grep remote` +.PHONY: co co: -for i in $(BRANCHES) ; do \ git checkout -t $$i ; \ done +.PHONY: coverage coverage: - python3 selftest.py - python3 setup.py test + pytest -qq rm -r htmlcov || true coverage report +.PHONY: doc doc: $(MAKE) -C docs html +.PHONY: doccheck doccheck: $(MAKE) -C docs html # Don't make our tests rely on the links in the docs being up every single build. # We don't control them. But do check, and update them to the target of their redirects. $(MAKE) -C docs linkcheck || true +.PHONY: docserve docserve: cd docs/_build/html && python3 -mSimpleHTTPServer 2> /dev/null& +.PHONY: help help: @echo "Welcome to Pillow development. Please use \`make \` where is one of" @echo " clean remove build products" @@ -49,17 +53,21 @@ help: @echo " upload build and upload sdists to PyPI" @echo " upload-test build and upload sdists to test.pythonpackages.com" +.PHONY: inplace inplace: clean python3 setup.py develop build_ext --inplace +.PHONY: install install: python3 setup.py install python3 selftest.py +.PHONY: install-coverage install-coverage: - CFLAGS="-coverage" python3 setup.py build_ext install + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 setup.py build_ext install python3 selftest.py +.PHONY: debug debug: # make a debug version if we don't have a -dbg python. Leaves in symbols # for our stuff, kills optimization, and redirects to dev null so we @@ -67,40 +75,37 @@ debug: make clean > /dev/null CFLAGS='-g -O0' python3 setup.py build_ext install > /dev/null +.PHONY: install-req install-req: - pip install -r requirements.txt + python3 -m pip install -r requirements.txt +.PHONY: install-venv install-venv: virtualenv . bin/pip install -r requirements.txt +.PHONY: release-test release-test: $(MAKE) install-req python3 setup.py develop python3 selftest.py python3 -m pytest Tests python3 setup.py install + -rm dist/*.egg + -rmdir dist python3 -m pytest -qq check-manifest pyroma . viewdoc +.PHONY: sdist sdist: python3 setup.py sdist --format=gztar +.PHONY: test test: pytest -qq -# https://docs.python.org/3/distutils/packageindex.html#the-pypirc-file -upload-test: -# [test] -# username: -# password: -# repository = http://test.pythonpackages.com - python3 setup.py sdist --format=gztar upload -r test - -upload: - python3 setup.py sdist --format=gztar upload - +.PHONY: readme readme: viewdoc diff --git a/README.md b/README.md new file mode 100644 index 000000000..9ead486a0 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +

+ Pillow logo +

+ +# Pillow + +## Python Imaging Library (Fork) + +Pillow is the friendly PIL fork by [Alex Clark and +Contributors](https://github.com/python-pillow/Pillow/graphs/contributors). +PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +As of 2019, Pillow development is +[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). + + + + + + + + + + + + + + + + + + +
docs + Documentation Status +
tests + Travis CI build status (Linux) + Travis CI build status (macOS) + AppVeyor CI build status (Windows) + GitHub Actions build status (Lint) + GitHub Actions build status (Test Linux and macOS) + GitHub Actions build status (Test Windows) + GitHub Actions build status (Test Docker) + Code coverage +
package + Zenodo + Tidelift + Newest PyPI version + Number of PyPI downloads +
social + Join the chat at https://gitter.im/python-pillow/Pillow + Follow on https://twitter.com/PythonPillow +
+ +## Overview + +The Python Imaging Library adds image processing capabilities to your Python interpreter. + +This library provides extensive file format support, an efficient internal representation, and fairly powerful image processing capabilities. + +The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. + +## More Information + +- [Documentation](https://pillow.readthedocs.io/) + - [Installation](https://pillow.readthedocs.io/en/latest/installation.html) + - [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html) +- [Contribute](https://github.com/python-pillow/Pillow/blob/master/.github/CONTRIBUTING.md) + - [Issues](https://github.com/python-pillow/Pillow/issues) + - [Pull requests](https://github.com/python-pillow/Pillow/pulls) +- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst) + - [Pre-fork](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork) + +## Report a Vulnerability + +To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security). diff --git a/README.rst b/README.rst deleted file mode 100644 index b8cad5e9d..000000000 --- a/README.rst +++ /dev/null @@ -1,99 +0,0 @@ -Pillow -====== - -Python Imaging Library (Fork) ------------------------------ - -Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. As of 2019, Pillow development is `supported by Tidelift `_. - -.. start-badges - -.. list-table:: - :stub-columns: 1 - - * - docs - - |docs| - * - tests - - |linux| |macos| |windows| |gha_lint| |gha| |gha_windows| |gha_docker| |coverage| - * - package - - |zenodo| |tidelift| |version| |downloads| - * - social - - |gitter| |twitter| - -.. end-badges - -More Information ----------------- - -- `Documentation `_ - - - `Installation `_ - - `Handbook `_ - -- `Contribute `_ - - - `Issues `_ - - `Pull requests `_ - -- `Changelog `_ - - - `Pre-fork `_ - -Report a Vulnerability ----------------------- - -To report a security vulnerability, please follow the procedure described in the `Tidelift security policy `_. - -.. |docs| image:: https://readthedocs.org/projects/pillow/badge/?version=latest - :target: https://pillow.readthedocs.io/?badge=latest - :alt: Documentation Status - -.. |linux| image:: https://img.shields.io/travis/python-pillow/Pillow/master.svg?label=Linux%20build - :target: https://travis-ci.org/python-pillow/Pillow - :alt: Travis CI build status (Linux) - -.. |macos| image:: https://img.shields.io/travis/python-pillow/pillow-wheels/master.svg?label=macOS%20build - :target: https://travis-ci.org/python-pillow/pillow-wheels - :alt: Travis CI build status (macOS) - -.. |windows| image:: https://img.shields.io/appveyor/ci/python-pillow/Pillow/master.svg?label=Windows%20build - :target: https://ci.appveyor.com/project/python-pillow/Pillow - :alt: AppVeyor CI build status (Windows) - -.. |gha_lint| image:: https://github.com/python-pillow/Pillow/workflows/Lint/badge.svg - :alt: GitHub Actions build status (Lint) - -.. |gha_docker| image:: https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg - :alt: GitHub Actions build status (Test Docker) - -.. |gha| image:: https://github.com/python-pillow/Pillow/workflows/Test/badge.svg - :alt: GitHub Actions build status (Test Linux and macOS) - -.. |gha_windows| image:: https://github.com/python-pillow/Pillow/workflows/Test%20Windows/badge.svg - :alt: GitHub Actions build status (Test Windows) - -.. |coverage| image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg - :target: https://codecov.io/gh/python-pillow/Pillow - :alt: Code coverage - -.. |zenodo| image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg - :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow - -.. |tidelift| image:: https://tidelift.com/badges/package/pypi/Pillow?style=flat - :target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=referral&utm_campaign=readme - -.. |version| image:: https://img.shields.io/pypi/v/pillow.svg - :target: https://pypi.org/project/Pillow/ - :alt: Latest PyPI version - -.. |downloads| image:: https://img.shields.io/pypi/dm/pillow.svg - :target: https://pypi.org/project/Pillow/ - :alt: Number of PyPI downloads - -.. |gitter| image:: https://badges.gitter.im/python-pillow/Pillow.svg - :target: https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge - :alt: Join the chat at https://gitter.im/python-pillow/Pillow - -.. |twitter| image:: https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg - :target: https://twitter.com/PythonPillow - :alt: Follow on https://twitter.com/PythonPillow diff --git a/RELEASING.md b/RELEASING.md index 9614b133f..dac0ba731 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,11 +2,11 @@ ## Main Release -Released quarterly on the first day of January, April, July, October. +Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Develop and prepare release in `master` branch. -* [ ] Check [Travis CI](https://travis-ci.org/python-pillow/Pillow) and [AppVeyor CI](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `master` branch. +* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions), [Travis CI](https://travis-ci.com/github/python-pillow/Pillow) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `master` branch. * [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI. * [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Update `CHANGES.rst`. @@ -37,13 +37,17 @@ Released as needed for security, installation or critical bug fixes. ```bash git checkout -t remotes/origin/5.2.x ``` -* [ ] Cherry pick individual commits from `master` branch to release branch e.g. `5.2.x`. -* [ ] Check [Travis CI](https://travis-ci.org/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`. +* [ ] Cherry pick individual commits from `master` branch to release branch e.g. `5.2.x`, then `git push`. + + + +* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions), [Travis CI](https://travis-ci.com/github/python-pillow/Pillow) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`. * [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Run pre-release check via `make release-test`. * [ ] Create tag for release e.g.: ```bash git tag 5.2.1 + git push git push --tags ``` * [ ] Create source distributions e.g.: @@ -91,11 +95,7 @@ Released as needed privately to individual vendors for critical security-related cd pillow-wheels ./update-pillow-tag.sh [[release tag]] ``` -* [ ] Download distributions from the [Pillow Wheel Builder container](http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com/). - ```bash - wget -m -A 'Pillow--*' \ - http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com - ``` +* [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases). ## Publicize Release @@ -104,3 +104,12 @@ Released as needed privately to individual vendors for critical security-related ## Documentation * [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes + +## Docker Images + +* [ ] Update Pillow in the Docker Images repository + ```bash + git clone https://github.com/python-pillow/docker-images + cd docker-images + ./update-pillow-tag.sh [[release tag]] + ``` diff --git a/Tests/README.rst b/Tests/README.rst index da3297bce..554645787 100644 --- a/Tests/README.rst +++ b/Tests/README.rst @@ -1,14 +1,14 @@ Pillow Tests ============ -Test scripts are named ``test_xxx.py`` and use the ``unittest`` module. A base class and helper functions can be found in ``helper.py``. +Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``. Dependencies ------------ Install:: - pip install pytest pytest-cov + python3 -m pip install pytest pytest-cov Execution --------- @@ -27,6 +27,6 @@ Run all the tests from the root of the Pillow source distribution:: Or with coverage:: - pytest --cov PIL --cov-report term + pytest --cov PIL --cov Tests --cov-report term coverage html open htmlcov/index.html diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 1797d34fc..f9edcf09a 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -1,9 +1,8 @@ import time -import unittest from PIL import PyAccess -from .helper import PillowTestCase, hopper +from .helper import hopper # Not running this test by default. No DOS against Travis CI. @@ -29,34 +28,31 @@ def timer(func, label, *args): func(*args) if time.time() - starttime > 10: print( - "%s: breaking at %s iterations, %.6f per iteration" - % (label, x + 1, (time.time() - starttime) / (x + 1.0)) + "{}: breaking at {} iterations, {:.6f} per iteration".format( + label, x + 1, (time.time() - starttime) / (x + 1.0) + ) ) break if x == iterations - 1: endtime = time.time() print( - "%s: %.4f s %.6f per iteration" - % (label, endtime - starttime, (endtime - starttime) / (x + 1.0)) + "{}: {:.4f} s {:.6f} per iteration".format( + label, endtime - starttime, (endtime - starttime) / (x + 1.0) + ) ) -class BenchCffiAccess(PillowTestCase): - def test_direct(self): - im = hopper() - im.load() - # im = Image.new( "RGB", (2000, 2000), (1, 3, 2)) - caccess = im.im.pixel_access(False) - access = PyAccess.new(im, False) +def test_direct(): + im = hopper() + im.load() + # im = Image.new( "RGB", (2000, 2000), (1, 3, 2)) + caccess = im.im.pixel_access(False) + access = PyAccess.new(im, False) - self.assertEqual(caccess[(0, 0)], access[(0, 0)]) + assert caccess[(0, 0)] == access[(0, 0)] - print("Size: %sx%s" % im.size) - timer(iterate_get, "PyAccess - get", im.size, access) - timer(iterate_set, "PyAccess - set", im.size, access) - timer(iterate_get, "C-api - get", im.size, caccess) - timer(iterate_set, "C-api - set", im.size, caccess) - - -if __name__ == "__main__": - unittest.main() + print("Size: %sx%s" % im.size) + timer(iterate_get, "PyAccess - get", im.size, access) + timer(iterate_set, "PyAccess - set", im.size, access) + timer(iterate_get, "C-api - get", im.size, caccess) + timer(iterate_set, "C-api - set", im.size, caccess) diff --git a/Tests/bench_get.py b/Tests/bench_get.py deleted file mode 100644 index 8a54ff921..000000000 --- a/Tests/bench_get.py +++ /dev/null @@ -1,23 +0,0 @@ -import sys -import timeit - -from . import helper - -sys.path.insert(0, ".") - - -def bench(mode): - im = helper.hopper(mode) - get = im.im.getpixel - xy = 50, 50 # position shouldn't really matter - t0 = timeit.default_timer() - for _ in range(1000000): - get(xy) - print(mode, timeit.default_timer() - t0, "us") - - -bench("L") -bench("I") -bench("I;16") -bench("F") -bench("RGB") diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py new file mode 100644 index 000000000..739ad224e --- /dev/null +++ b/Tests/check_fli_oob.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +from PIL import Image + +repro_ss2 = ( + "images/fli_oob/06r/06r00.fli", + "images/fli_oob/06r/others/06r01.fli", + "images/fli_oob/06r/others/06r02.fli", + "images/fli_oob/06r/others/06r03.fli", + "images/fli_oob/06r/others/06r04.fli", +) + +repro_lc = ( + "images/fli_oob/05r/05r00.fli", + "images/fli_oob/05r/others/05r03.fli", + "images/fli_oob/05r/others/05r06.fli", + "images/fli_oob/05r/others/05r05.fli", + "images/fli_oob/05r/others/05r01.fli", + "images/fli_oob/05r/others/05r04.fli", + "images/fli_oob/05r/others/05r02.fli", + "images/fli_oob/05r/others/05r07.fli", + "images/fli_oob/patch0/000000", + "images/fli_oob/patch0/000001", + "images/fli_oob/patch0/000002", + "images/fli_oob/patch0/000003", +) + + +repro_advance = ( + "images/fli_oob/03r/03r00.fli", + "images/fli_oob/03r/others/03r01.fli", + "images/fli_oob/03r/others/03r09.fli", + "images/fli_oob/03r/others/03r11.fli", + "images/fli_oob/03r/others/03r05.fli", + "images/fli_oob/03r/others/03r10.fli", + "images/fli_oob/03r/others/03r06.fli", + "images/fli_oob/03r/others/03r08.fli", + "images/fli_oob/03r/others/03r03.fli", + "images/fli_oob/03r/others/03r07.fli", + "images/fli_oob/03r/others/03r02.fli", + "images/fli_oob/03r/others/03r04.fli", +) + +repro_brun = ( + "images/fli_oob/04r/initial.fli", + "images/fli_oob/04r/others/04r02.fli", + "images/fli_oob/04r/others/04r05.fli", + "images/fli_oob/04r/others/04r04.fli", + "images/fli_oob/04r/others/04r03.fli", + "images/fli_oob/04r/others/04r01.fli", + "images/fli_oob/04r/04r00.fli", +) + +repro_copy = ( + "images/fli_oob/02r/others/02r02.fli", + "images/fli_oob/02r/others/02r04.fli", + "images/fli_oob/02r/others/02r03.fli", + "images/fli_oob/02r/others/02r01.fli", + "images/fli_oob/02r/02r00.fli", +) + + +for path in repro_ss2 + repro_lc + repro_advance + repro_brun + repro_copy: + im = Image.open(path) + try: + im.load() + except Exception as msg: + print(msg) diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index fa6037c2e..08a55d349 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -1,19 +1,10 @@ -import unittest - from PIL import Image -from .helper import PillowTestCase - TEST_FILE = "Tests/images/fli_overflow.fli" -class TestFliOverflow(PillowTestCase): - def test_fli_overflow(self): +def test_fli_overflow(): - # this should not crash with a malloc error or access violation - im = Image.open(TEST_FILE) + # this should not crash with a malloc error or access violation + with Image.open(TEST_FILE) as im: im.load() - - -if __name__ == "__main__": - unittest.main() diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 2c1793a4f..407f3ea80 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,46 +1,45 @@ #!/usr/bin/env python -import unittest +import pytest from PIL import Image -from .helper import PillowTestCase, is_win32 +from .helper import is_win32 min_iterations = 100 max_iterations = 10000 - -@unittest.skipIf(is_win32(), "requires Unix or macOS") -class TestImagingLeaks(PillowTestCase): - def _get_mem_usage(self): - from resource import getpagesize, getrusage, RUSAGE_SELF - - mem = getrusage(RUSAGE_SELF).ru_maxrss - return mem * getpagesize() / 1024 / 1024 - - def _test_leak(self, min_iterations, max_iterations, fn, *args, **kwargs): - mem_limit = None - for i in range(max_iterations): - fn(*args, **kwargs) - mem = self._get_mem_usage() - if i < min_iterations: - mem_limit = mem + 1 - continue - msg = "memory usage limit exceeded after %d iterations" % (i + 1) - self.assertLessEqual(mem, mem_limit, msg) - - def test_leak_putdata(self): - im = Image.new("RGB", (25, 25)) - self._test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) - - def test_leak_getlist(self): - im = Image.new("P", (25, 25)) - self._test_leak( - min_iterations, - max_iterations, - # Pass a new list at each iteration. - lambda: im.point(range(256)), - ) +pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -if __name__ == "__main__": - unittest.main() +def _get_mem_usage(): + 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): + mem_limit = None + for i in range(max_iterations): + fn(*args, **kwargs) + 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 <= mem_limit, msg + + +def test_leak_putdata(): + im = Image.new("RGB", (25, 25)) + _test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) + + +def test_leak_getlist(): + im = Image.new("P", (25, 25)) + _test_leak( + min_iterations, + max_iterations, + # Pass a new list at each iteration. + lambda: im.point(range(256)), + ) diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index 1635f1001..afe5836f3 100755 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,46 +1,42 @@ -import unittest from io import BytesIO +import pytest + from PIL import Image -from .helper import PillowTestCase, is_win32 +from .helper import is_win32, skip_unless_feature # Limits for testing the leak mem_limit = 1024 * 1048576 stack_size = 8 * 1048576 iterations = int((mem_limit / stack_size) * 2) -codecs = dir(Image.core) test_file = "Tests/images/rgb_trns_ycbc.jp2" - -@unittest.skipIf(is_win32(), "requires Unix or macOS") -class TestJpegLeaks(PillowTestCase): - def setUp(self): - if "jpeg2k_encoder" not in codecs or "jpeg2k_decoder" not in codecs: - self.skipTest("JPEG 2000 support not available") - - def test_leak_load(self): - from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK - - setrlimit(RLIMIT_STACK, (stack_size, stack_size)) - setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) - for _ in range(iterations): - with Image.open(test_file) as im: - im.load() - - def test_leak_save(self): - from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK - - setrlimit(RLIMIT_STACK, (stack_size, stack_size)) - setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) - for _ in range(iterations): - with Image.open(test_file) as im: - im.load() - test_output = BytesIO() - im.save(test_output, "JPEG2000") - test_output.seek(0) - test_output.read() +pytestmark = [ + pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"), + skip_unless_feature("jpg_2000"), +] -if __name__ == "__main__": - unittest.main() +def test_leak_load(): + from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit + + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for _ in range(iterations): + with Image.open(test_file) as im: + im.load() + + +def test_leak_save(): + from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit + + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for _ in range(iterations): + with Image.open(test_file) as im: + im.load() + test_output = BytesIO() + im.save(test_output, "JPEG2000") + test_output.seek(0) + test_output.read() diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index d5b6e455f..b16412898 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,18 +1,10 @@ -import unittest +import pytest from PIL import Image -from .helper import PillowTestCase - -class TestJ2kEncodeOverflow(PillowTestCase): - def test_j2k_overflow(self): - - im = Image.new("RGBA", (1024, 131584)) - target = self.tempfile("temp.jpc") - with self.assertRaises(IOError): - im.save(target) - - -if __name__ == "__main__": - unittest.main() +def test_j2k_overflow(tmp_path): + im = Image.new("RGBA", (1024, 131584)) + target = str(tmp_path / "temp.jpc") + with pytest.raises(OSError): + im.save(target) diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py new file mode 100755 index 000000000..a7a343c98 --- /dev/null +++ b/Tests/check_jp2_overflow.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +# Reproductions/tests for OOB read errors in FliDecode.c + +# When run in python, all of these images should fail for +# one reason or another, either as a buffer overrun, +# unrecognized datastream, or truncated image file. +# There shouldn't be any segfaults. +# +# if run like +# `valgrind --tool=memcheck python check_jp2_overflow.py 2>&1 | grep Decode.c` +# the output should be empty. There may be python issues +# in the valgrind especially if run in a debug python +# version. + + +from PIL import Image + +repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") + +for path in repro: + im = Image.open(path) + try: + im.load() + except Exception as msg: + print(msg) diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 6b2801a21..ab8d77719 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,7 +1,8 @@ -import unittest from io import BytesIO -from .helper import PillowTestCase, hopper, is_win32 +import pytest + +from .helper import hopper, is_win32 iterations = 5000 @@ -15,10 +16,9 @@ valgrind --tool=massif python test-installed.py -s -v Tests/check_jpeg_leaks.py """ -@unittest.skipIf(is_win32(), "requires Unix or macOS") -class TestJpegLeaks(PillowTestCase): +pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") - """ +""" pre patch: MB @@ -74,143 +74,140 @@ post-patch: """ - def test_qtables_leak(self): - im = hopper("RGB") - standard_l_qtable = [ - int(s) - for s in """ - 16 11 10 16 24 40 51 61 - 12 12 14 19 26 58 60 55 - 14 13 16 24 40 57 69 56 - 14 17 22 29 51 87 80 62 - 18 22 37 56 68 109 103 77 - 24 35 55 64 81 104 113 92 - 49 64 78 87 103 121 120 101 - 72 92 95 98 112 100 103 99 - """.split( - None - ) - ] +def test_qtables_leak(): + im = hopper("RGB") - standard_chrominance_qtable = [ - int(s) - for s in """ - 17 18 24 47 99 99 99 99 - 18 21 26 66 99 99 99 99 - 24 26 56 99 99 99 99 99 - 47 66 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - """.split( - None - ) - ] + standard_l_qtable = [ + int(s) + for s in """ + 16 11 10 16 24 40 51 61 + 12 12 14 19 26 58 60 55 + 14 13 16 24 40 57 69 56 + 14 17 22 29 51 87 80 62 + 18 22 37 56 68 109 103 77 + 24 35 55 64 81 104 113 92 + 49 64 78 87 103 121 120 101 + 72 92 95 98 112 100 103 99 + """.split( + None + ) + ] - qtables = [standard_l_qtable, standard_chrominance_qtable] + standard_chrominance_qtable = [ + int(s) + for s in """ + 17 18 24 47 99 99 99 99 + 18 21 26 66 99 99 99 99 + 24 26 56 99 99 99 99 99 + 47 66 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + """.split( + None + ) + ] - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", qtables=qtables) + qtables = [standard_l_qtable, standard_chrominance_qtable] - def test_exif_leak(self): - """ -pre patch: - - MB -177.1^ # - | @@@# - | :@@@@@@# - | ::::@@@@@@# - | ::::::::@@@@@@# - | @@::::: ::::@@@@@@# - | @@@@ ::::: ::::@@@@@@# - | @@@@@@@ ::::: ::::@@@@@@# - | @@::@@@@@@@ ::::: ::::@@@@@@# - | @@@@ : @@@@@@@ ::::: ::::@@@@@@# - | @@@@@@ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @@@@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @::@@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | ::::@: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | :@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | ::@@::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @@::: @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @::@ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | :::@: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @@@:: @: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - 0 +----------------------------------------------------------------------->Gi - 0 11.37 + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", qtables=qtables) -post patch: +def test_exif_leak(): + """ + pre patch: - MB -21.06^ ::::::::::::::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | ##::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - 0 +----------------------------------------------------------------------->Gi - 0 11.33 - -""" - im = hopper("RGB") - exif = b"12345678" * 4096 - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", exif=exif) - - def test_base_save(self): - """ -base case: - MB -20.99^ ::::: :::::::::::::::::::::::::::::::::::::::::::@::: - | ##: : ::::::@::::::: :::: :::: : : : : : : :::::::::::: :::@::: - | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@# : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@@ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@@@@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - 0 +----------------------------------------------------------------------->Gi - 0 7.882 -""" - im = hopper("RGB") - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG") + MB + 177.1^ # + | @@@# + | :@@@@@@# + | ::::@@@@@@# + | ::::::::@@@@@@# + | @@::::: ::::@@@@@@# + | @@@@ ::::: ::::@@@@@@# + | @@@@@@@ ::::: ::::@@@@@@# + | @@::@@@@@@@ ::::: ::::@@@@@@# + | @@@@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@@@@ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @::@@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | ::::@: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | :@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | ::@@::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@::: @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @::@ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | :::@: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@:: @: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + 0 +----------------------------------------------------------------------->Gi + 0 11.37 -if __name__ == "__main__": - unittest.main() + post patch: + + MB + 21.06^ ::::::::::::::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | ##::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + 0 +----------------------------------------------------------------------->Gi + 0 11.33 + """ + im = hopper("RGB") + exif = b"12345678" * 4096 + + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", exif=exif) + + +def test_base_save(): + """ + base case: + MB + 20.99^ ::::: :::::::::::::::::::::::::::::::::::::::::::@::: + | ##: : ::::::@::::::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@# : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@@ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@@@@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + 0 +----------------------------------------------------------------------->Gi + 0 7.882""" + im = hopper("RGB") + + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG") diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 7fcaa4cf9..723a1a21e 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,10 +1,9 @@ import sys -import unittest + +import pytest from PIL import Image -from .helper import PillowTestCase - # This test is not run automatically. # # It requires > 2gb memory for the >2 gigapixel image generated in the @@ -24,26 +23,26 @@ YDIM = 32769 XDIM = 48000 -@unittest.skipIf(sys.maxsize <= 2 ** 32, "requires 64-bit system") -class LargeMemoryTest(PillowTestCase): - def _write_png(self, xdim, ydim): - f = self.tempfile("temp.png") - im = Image.new("L", (xdim, ydim), 0) - im.save(f) - - def test_large(self): - """ succeeded prepatch""" - self._write_png(XDIM, YDIM) - - def test_2gpx(self): - """failed prepatch""" - self._write_png(XDIM, XDIM) - - @unittest.skipIf(numpy is None, "Numpy is not installed") - def test_size_greater_than_int(self): - arr = numpy.ndarray(shape=(16394, 16394)) - Image.fromarray(arr) +pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") -if __name__ == "__main__": - unittest.main() +def _write_png(tmp_path, xdim, ydim): + f = str(tmp_path / "temp.png") + im = Image.new("L", (xdim, ydim), 0) + im.save(f) + + +def test_large(tmp_path): + """ succeeded prepatch""" + _write_png(tmp_path, XDIM, YDIM) + + +def test_2gpx(tmp_path): + """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(): + 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 8e65dc1cb..79d1cfd5b 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,10 +1,9 @@ import sys -import unittest + +import pytest from PIL import Image -from .helper import PillowTestCase - # This test is not run automatically. # # It requires > 2gb memory for the >2 gigapixel image generated in the @@ -14,32 +13,28 @@ from .helper import PillowTestCase # Raspberry Pis). -try: - import numpy as np -except ImportError: - raise unittest.SkipTest("numpy not installed") +np = pytest.importorskip("numpy", reason="NumPy not installed") YDIM = 32769 XDIM = 48000 -@unittest.skipIf(sys.maxsize <= 2 ** 32, "requires 64-bit system") -class LargeMemoryNumpyTest(PillowTestCase): - def _write_png(self, xdim, ydim): - dtype = np.uint8 - a = np.zeros((xdim, ydim), dtype=dtype) - f = self.tempfile("temp.png") - im = Image.fromarray(a, "L") - im.save(f) - - def test_large(self): - """ succeeded prepatch""" - self._write_png(XDIM, YDIM) - - def test_2gpx(self): - """failed prepatch""" - self._write_png(XDIM, XDIM) +pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") -if __name__ == "__main__": - unittest.main() +def _write_png(tmp_path, xdim, ydim): + dtype = np.uint8 + a = np.zeros((xdim, ydim), dtype=dtype) + f = str(tmp_path / "temp.png") + im = Image.fromarray(a, "L") + im.save(f) + + +def test_large(tmp_path): + """ succeeded prepatch""" + _write_png(tmp_path, XDIM, YDIM) + + +def test_2gpx(tmp_path): + """failed prepatch""" + _write_png(tmp_path, XDIM, XDIM) diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index a0263f725..bd7f407e4 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -1,22 +1,15 @@ -import unittest +import pytest from PIL import Image -from .helper import PillowTestCase - TEST_FILE = "Tests/images/libtiff_segfault.tif" -class TestLibtiffSegfault(PillowTestCase): - def test_segfault(self): - """ This test should not segfault. It will on Pillow <= 3.1.0 and - libtiff >= 4.0.0 - """ +def test_libtiff_segfault(): + """This test should not segfault. It will on Pillow <= 3.1.0 and + libtiff >= 4.0.0 + """ - with self.assertRaises(IOError): - im = Image.open(TEST_FILE) + with pytest.raises(OSError): + with Image.open(TEST_FILE) as im: im.load() - - -if __name__ == "__main__": - unittest.main() diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index f133b2695..d8d645189 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -1,69 +1,61 @@ -import unittest import zlib from io import BytesIO from PIL import Image, ImageFile, PngImagePlugin -from .helper import PillowTestCase - TEST_FILE = "Tests/images/png_decompression_dos.png" -class TestPngDos(PillowTestCase): - def test_ignore_dos_text(self): - ImageFile.LOAD_TRUNCATED_IMAGES = True +def test_ignore_dos_text(): + ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im = Image.open(TEST_FILE) - im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + try: + im = Image.open(TEST_FILE) + im.load() + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False - for s in im.text.values(): - self.assertLess(len(s), 1024 * 1024, "Text chunk larger than 1M") + for s in im.text.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" - for s in im.info.values(): - self.assertLess(len(s), 1024 * 1024, "Text chunk larger than 1M") - - def test_dos_text(self): - - try: - im = Image.open(TEST_FILE) - im.load() - except ValueError as msg: - self.assertTrue(msg, "Decompressed Data Too Large") - return - - for s in im.text.values(): - self.assertLess(len(s), 1024 * 1024, "Text chunk larger than 1M") - - def test_dos_total_memory(self): - im = Image.new("L", (1, 1)) - compressed_data = zlib.compress(b"a" * 1024 * 1023) - - info = PngImagePlugin.PngInfo() - - for x in range(64): - info.add_text("t%s" % x, compressed_data, zip=True) - info.add_itxt("i%s" % x, compressed_data, zip=True) - - b = BytesIO() - im.save(b, "PNG", pnginfo=info) - b.seek(0) - - try: - im2 = Image.open(b) - except ValueError as msg: - self.assertIn("Too much memory", msg) - return - - total_len = 0 - for txt in im2.text.values(): - total_len += len(txt) - self.assertLess( - total_len, 64 * 1024 * 1024, "Total text chunks greater than 64M" - ) + for s in im.info.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" -if __name__ == "__main__": - unittest.main() +def test_dos_text(): + + try: + im = Image.open(TEST_FILE) + im.load() + except ValueError as msg: + assert msg, "Decompressed Data Too Large" + return + + for s in im.text.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" + + +def test_dos_total_memory(): + im = Image.new("L", (1, 1)) + compressed_data = zlib.compress(b"a" * 1024 * 1023) + + info = PngImagePlugin.PngInfo() + + for x in range(64): + info.add_text(f"t{x}", compressed_data, zip=True) + info.add_itxt(f"i{x}", compressed_data, zip=True) + + b = BytesIO() + im.save(b, "PNG", pnginfo=info) + b.seek(0) + + try: + im2 = Image.open(b) + except ValueError as msg: + assert "Too much memory" in msg + return + + total_len = 0 + for txt in im2.text.values(): + total_len += len(txt) + assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M" diff --git a/Tests/conftest.py b/Tests/conftest.py index 2b796148d..082f2f7c3 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -1,6 +1,7 @@ -def pytest_report_header(config): - import io +import io + +def pytest_report_header(config): try: from PIL import features @@ -8,4 +9,4 @@ def pytest_report_header(config): features.pilinfo(out=out, supported_formats=False) return out.getvalue() except Exception as e: - return "pytest_report_header failed: %s" % str(e) + return f"pytest_report_header failed: {e}" diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index c7055995e..011bb0bed 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -6,7 +6,7 @@ if __name__ == "__main__": # create font data chunk for embedding font = "Tests/images/courB08" print(" f._load_pilfont_data(") - print(" # %s" % os.path.basename(font)) + print(f" # {os.path.basename(font)}") print(" BytesIO(base64.decodestring(b'''") with open(font + ".pil", "rb") as fp: print(base64.b64encode(fp.read()).decode()) diff --git a/Tests/fonts/BungeeColor-Regular_colr_Windows.ttf b/Tests/fonts/BungeeColor-Regular_colr_Windows.ttf new file mode 100644 index 000000000..d8eabb3b6 Binary files /dev/null and b/Tests/fonts/BungeeColor-Regular_colr_Windows.ttf differ diff --git a/Tests/fonts/DejaVuSans-24-1-stripped.ttf b/Tests/fonts/DejaVuSans-24-1-stripped.ttf new file mode 100644 index 000000000..8eaf1ee08 Binary files /dev/null and b/Tests/fonts/DejaVuSans-24-1-stripped.ttf differ diff --git a/Tests/fonts/DejaVuSans-24-2-stripped.ttf b/Tests/fonts/DejaVuSans-24-2-stripped.ttf new file mode 100644 index 000000000..233667251 Binary files /dev/null and b/Tests/fonts/DejaVuSans-24-2-stripped.ttf differ diff --git a/Tests/fonts/DejaVuSans-24-4-stripped.ttf b/Tests/fonts/DejaVuSans-24-4-stripped.ttf new file mode 100644 index 000000000..9accc9ebc Binary files /dev/null and b/Tests/fonts/DejaVuSans-24-4-stripped.ttf differ diff --git a/Tests/fonts/DejaVuSans-24-8-stripped.ttf b/Tests/fonts/DejaVuSans-24-8-stripped.ttf new file mode 100644 index 000000000..0f9344267 Binary files /dev/null and b/Tests/fonts/DejaVuSans-24-8-stripped.ttf differ diff --git a/Tests/fonts/DejaVuSans-bitmap.ttf b/Tests/fonts/DejaVuSans-bitmap.ttf deleted file mode 100644 index 702cce37d..000000000 Binary files a/Tests/fonts/DejaVuSans-bitmap.ttf and /dev/null differ diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index 726d5d797..06eaa9a4e 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -1,12 +1,22 @@ 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 +ter-x20b.pcf, from http://terminus-font.sourceforge.net/ +BungeeColor-Regular_colr_Windows.ttf, from https://github.com/djrrb/bungee All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. +FreeMono.ttf is licensed under GPLv3, with the GPL font exception. + +OpenSansCondensed-LightItalic.tt, from https://fonts.google.com/specimen/Open+Sans, under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + +DejaVuSans-24-{1,2,4,8}-stripped.ttf are based on DejaVuSans.ttf converted using FontForge to add bitmap strikes and keep only the ASCII range. + 10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base diff --git a/Tests/fonts/NotoColorEmoji.ttf b/Tests/fonts/NotoColorEmoji.ttf new file mode 100644 index 000000000..ef7b72575 Binary files /dev/null and b/Tests/fonts/NotoColorEmoji.ttf differ diff --git a/Tests/fonts/NotoSans-Regular.ttf b/Tests/fonts/NotoSans-Regular.ttf new file mode 100644 index 000000000..a1b8994ed Binary files /dev/null and b/Tests/fonts/NotoSans-Regular.ttf differ diff --git a/Tests/fonts/OpenSansCondensed-LightItalic.ttf b/Tests/fonts/OpenSansCondensed-LightItalic.ttf new file mode 100644 index 000000000..b4ee4951f Binary files /dev/null and b/Tests/fonts/OpenSansCondensed-LightItalic.ttf differ diff --git a/Tests/fonts/ter-x20b-cp1250.pbm b/Tests/fonts/ter-x20b-cp1250.pbm new file mode 100644 index 000000000..fe7e2c4dc Binary files /dev/null and b/Tests/fonts/ter-x20b-cp1250.pbm differ diff --git a/Tests/fonts/ter-x20b-cp1250.pil b/Tests/fonts/ter-x20b-cp1250.pil new file mode 100644 index 000000000..4da49e5fd Binary files /dev/null and b/Tests/fonts/ter-x20b-cp1250.pil differ diff --git a/Tests/fonts/ter-x20b-iso8859-1.pbm b/Tests/fonts/ter-x20b-iso8859-1.pbm new file mode 100644 index 000000000..ffd840ae9 Binary files /dev/null and b/Tests/fonts/ter-x20b-iso8859-1.pbm differ diff --git a/Tests/fonts/ter-x20b-iso8859-1.pil b/Tests/fonts/ter-x20b-iso8859-1.pil new file mode 100644 index 000000000..14d6e8be7 Binary files /dev/null and b/Tests/fonts/ter-x20b-iso8859-1.pil differ diff --git a/Tests/fonts/ter-x20b-iso8859-2.pbm b/Tests/fonts/ter-x20b-iso8859-2.pbm new file mode 100644 index 000000000..ad5b3af8d Binary files /dev/null and b/Tests/fonts/ter-x20b-iso8859-2.pbm differ diff --git a/Tests/fonts/ter-x20b-iso8859-2.pil b/Tests/fonts/ter-x20b-iso8859-2.pil new file mode 100644 index 000000000..14d6e8be7 Binary files /dev/null and b/Tests/fonts/ter-x20b-iso8859-2.pil differ diff --git a/Tests/fonts/ter-x20b.pcf b/Tests/fonts/ter-x20b.pcf new file mode 100644 index 000000000..962bcca6a Binary files /dev/null and b/Tests/fonts/ter-x20b.pcf differ diff --git a/Tests/helper.py b/Tests/helper.py index 0f8e05c19..3da2571f2 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -4,13 +4,16 @@ Helper functions. import logging import os -import subprocess +import shutil import sys +import sysconfig import tempfile -import unittest from io import BytesIO -from PIL import Image, ImageMath +import pytest +from packaging.version import parse as parse_version + +from PIL import Image, ImageMath, features logger = logging.getLogger(__name__) @@ -64,173 +67,114 @@ def convert_to_comparable(a, b): return new_a, new_b -class PillowTestCase(unittest.TestCase): - def delete_tempfile(self, path): - try: - os.remove(path) - except OSError: - pass # report? +def assert_deep_equal(a, b, msg=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_deep_equal(self, a, b, msg=None): - try: - self.assertEqual( - len(a), - len(b), - msg or "got length {}, expected {}".format(len(a), len(b)), - ) - self.assertTrue( - all(x == y for x, y in zip(a, b)), - msg or "got {}, expected {}".format(a, b), - ) - except Exception: - self.assertEqual(a, b, msg) - def assert_image(self, im, mode, size, msg=None): - if mode is not None: - self.assertEqual( - im.mode, - mode, - msg or "got mode {!r}, expected {!r}".format(im.mode, mode), - ) - - if size is not None: - self.assertEqual( - im.size, - size, - msg or "got size {!r}, expected {!r}".format(im.size, size), - ) - - def assert_image_equal(self, a, b, msg=None): - self.assertEqual( - a.mode, b.mode, msg or "got mode {!r}, expected {!r}".format(a.mode, b.mode) - ) - self.assertEqual( - a.size, b.size, msg or "got size {!r}, expected {!r}".format(a.size, b.size) - ) - if a.tobytes() != b.tobytes(): - if HAS_UPLOADER: - try: - url = test_image_results.upload(a, b) - logger.error("Url for test images: %s" % url) - except Exception: - pass - - self.fail(msg or "got different content") - - def assert_image_equal_tofile(self, a, filename, msg=None, mode=None): - with Image.open(filename) as img: - if mode: - img = img.convert(mode) - self.assert_image_equal(a, img, msg) - - def assert_image_similar(self, a, b, epsilon, msg=None): - epsilon = float(epsilon) - self.assertEqual( - a.mode, b.mode, msg or "got mode {!r}, expected {!r}".format(a.mode, b.mode) - ) - self.assertEqual( - a.size, b.size, msg or "got size {!r}, expected {!r}".format(a.size, b.size) +def assert_image(im, mode, size, msg=None): + if mode is not None: + assert im.mode == mode, ( + msg or f"got mode {repr(im.mode)}, expected {repr(mode)}" ) - a, b = convert_to_comparable(a, b) - - diff = 0 - for ach, bch in zip(a.split(), b.split()): - chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert("L") - diff += sum(i * num for i, num in enumerate(chdiff.histogram())) - - ave_diff = float(diff) / (a.size[0] * a.size[1]) - try: - self.assertGreaterEqual( - epsilon, - ave_diff, - (msg or "") - + " average pixel value difference %.4f > epsilon %.4f" - % (ave_diff, epsilon), - ) - except Exception as e: - if HAS_UPLOADER: - try: - url = test_image_results.upload(a, b) - logger.error("Url for test images: %s" % url) - except Exception: - pass - raise e - - def assert_image_similar_tofile(self, a, filename, epsilon, msg=None, mode=None): - with Image.open(filename) as img: - if mode: - img = img.convert(mode) - self.assert_image_similar(a, img, epsilon, msg) - - def assert_warning(self, warn_class, func, *args, **kwargs): - import warnings - - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - - # Hopefully trigger a warning. - result = func(*args, **kwargs) - - # Verify some things. - if warn_class is None: - self.assertEqual( - len(w), 0, "Expected no warnings, got %s" % [v.category for v in w] - ) - else: - self.assertGreaterEqual(len(w), 1) - found = False - for v in w: - if issubclass(v.category, warn_class): - found = True - break - self.assertTrue(found) - return result - - def assert_all_same(self, items, msg=None): - self.assertEqual(items.count(items[0]), len(items), msg) - - def assert_not_all_same(self, items, msg=None): - self.assertNotEqual(items.count(items[0]), len(items), msg) - - def assert_tuple_approx_equal(self, actuals, targets, threshold, msg): - """Tests if actuals has values within threshold from targets""" - - value = True - for i, target in enumerate(targets): - value *= target - threshold <= actuals[i] <= target + threshold - - self.assertTrue(value, msg + ": " + repr(actuals) + " != " + repr(targets)) - - def skipKnownBadTest(self, msg=None): - # Skip if PILLOW_RUN_KNOWN_BAD is not true in the environment. - if os.environ.get("PILLOW_RUN_KNOWN_BAD", False): - print(os.environ.get("PILLOW_RUN_KNOWN_BAD", False)) - return - - self.skipTest(msg or "Known Bad Test") - - def tempfile(self, template): - assert template[:5] in ("temp.", "temp_") - fd, path = tempfile.mkstemp(template[4:], template[:4]) - os.close(fd) - - self.addCleanup(self.delete_tempfile, path) - return path - - def open_withImagemagick(self, f): - if not imagemagick_available(): - raise OSError() - - outfile = self.tempfile("temp.png") - if command_succeeds([IMCONVERT, f, outfile]): - return Image.open(outfile) - raise OSError() + if size is not None: + assert im.size == size, ( + msg or f"got size {repr(im.size)}, expected {repr(size)}" + ) -@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") -class PillowLeakTestCase(PillowTestCase): +def assert_image_equal(a, b, msg=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) + logger.error(f"Url for test images: {url}") + except Exception: + pass + + assert False, msg or "got different content" + + +def assert_image_equal_tofile(a, filename, msg=None, mode=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): + 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)}" + + a, b = convert_to_comparable(a, b) + + diff = 0 + for ach, bch in zip(a.split(), b.split()): + chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert("L") + diff += sum(i * num for i, num in enumerate(chdiff.histogram())) + + ave_diff = diff / (a.size[0] * a.size[1]) + try: + assert epsilon >= ave_diff, ( + (msg or "") + + 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) + logger.error(f"Url for test images: {url}") + except Exception: + pass + raise e + + +def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=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): + assert items.count(items[0]) == len(items), msg + + +def assert_not_all_same(items, msg=None): + assert items.count(items[0]) != len(items), msg + + +def assert_tuple_approx_equal(actuals, targets, threshold, msg): + """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) + + +def skip_unless_feature(feature): + 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): + 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) + version_available = parse_version(features.version(feature)) + return pytest.mark.skipif(version_available < version_required, reason=reason) + + +@pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") +class PillowLeakTestCase: # requires unix/macOS iterations = 100 # count mem_limit = 512 # k @@ -243,7 +187,7 @@ class PillowLeakTestCase(PillowTestCase): :returns: memory usage in kilobytes """ - from resource import getrusage, RUSAGE_SELF + from resource import RUSAGE_SELF, getrusage mem = getrusage(RUSAGE_SELF).ru_maxrss if sys.platform == "darwin": @@ -263,8 +207,8 @@ class PillowLeakTestCase(PillowTestCase): for cycle in range(self.iterations): core() mem = self._get_mem_usage() - start_mem - msg = "memory usage limit exceeded in iteration %d" % cycle - self.assertLess(mem, self.mem_limit, msg) + msg = f"memory usage limit exceeded in iteration {cycle}" + assert mem < self.mem_limit, msg # helpers @@ -301,34 +245,20 @@ def hopper(mode=None, cache={}): return im.copy() -def command_succeeds(cmd): - """ - Runs the command, which must be a list of strings. Returns True if the - command succeeds, or False if an OSError was raised by subprocess.Popen. - """ - try: - subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) - except OSError: - return False - return True - - def djpeg_available(): - return command_succeeds(["djpeg", "-version"]) + return bool(shutil.which("djpeg")) def cjpeg_available(): - return command_succeeds(["cjpeg", "-version"]) + return bool(shutil.which("cjpeg")) def netpbm_available(): - return command_succeeds(["ppmquant", "--version"]) and command_succeeds( - ["ppmtogif", "--version"] - ) + return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) def imagemagick_available(): - return IMCONVERT and command_succeeds([IMCONVERT, "-version"]) + return bool(IMCONVERT and shutil.which(IMCONVERT)) def on_appveyor(): @@ -340,12 +270,12 @@ def on_github_actions(): def on_ci(): - # Travis and AppVeyor have "CI" - # Azure Pipelines has "TF_BUILD" - # GitHub Actions has "GITHUB_ACTIONS" - return ( - "CI" in os.environ or "TF_BUILD" in os.environ or "GITHUB_ACTIONS" in os.environ - ) + # GitHub Actions, Travis and AppVeyor have "CI" + return "CI" in os.environ + + +def is_big_endian(): + return sys.byteorder == "big" def is_win32(): @@ -356,6 +286,10 @@ def is_pypy(): return hasattr(sys, "pypy_translation_info") +def is_mingw(): + return sysconfig.get_platform() == "mingw" + + if sys.platform == "win32": IMCONVERT = os.environ.get("MAGICK_HOME", "") if IMCONVERT: @@ -364,14 +298,6 @@ else: IMCONVERT = "convert" -def distro(): - if os.path.exists("/etc/os-release"): - with open("/etc/os-release", "r") as f: - for line in f: - if "ID=" in line: - return line.strip().split("=")[1] - - class cached_property: def __init__(self, func): self.func = func diff --git a/Tests/images/00r0_gray_l.jp2 b/Tests/images/00r0_gray_l.jp2 new file mode 100644 index 000000000..28612238a Binary files /dev/null and b/Tests/images/00r0_gray_l.jp2 differ diff --git a/Tests/images/00r1_graya_la.jp2 b/Tests/images/00r1_graya_la.jp2 new file mode 100644 index 000000000..f3f840a08 Binary files /dev/null and b/Tests/images/00r1_graya_la.jp2 differ diff --git a/Tests/images/01r_00.pcx b/Tests/images/01r_00.pcx new file mode 100644 index 000000000..f40777ac5 Binary files /dev/null and b/Tests/images/01r_00.pcx differ diff --git a/Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds b/Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds new file mode 100644 index 000000000..1da9293de Binary files /dev/null and b/Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds differ diff --git a/Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.png b/Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.png new file mode 100644 index 000000000..57177fe2b Binary files /dev/null and b/Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.png differ diff --git a/Tests/images/apng/blend_op_over.png b/Tests/images/apng/blend_op_over.png new file mode 100644 index 000000000..3fe0f4ca7 Binary files /dev/null and b/Tests/images/apng/blend_op_over.png differ diff --git a/Tests/images/apng/blend_op_over_near_transparent.png b/Tests/images/apng/blend_op_over_near_transparent.png new file mode 100644 index 000000000..3ee5fe3bf Binary files /dev/null and b/Tests/images/apng/blend_op_over_near_transparent.png differ diff --git a/Tests/images/apng/blend_op_source_near_transparent.png b/Tests/images/apng/blend_op_source_near_transparent.png new file mode 100644 index 000000000..1af30f81f Binary files /dev/null and b/Tests/images/apng/blend_op_source_near_transparent.png differ diff --git a/Tests/images/apng/blend_op_source_solid.png b/Tests/images/apng/blend_op_source_solid.png new file mode 100644 index 000000000..d90c54967 Binary files /dev/null and b/Tests/images/apng/blend_op_source_solid.png differ diff --git a/Tests/images/apng/blend_op_source_transparent.png b/Tests/images/apng/blend_op_source_transparent.png new file mode 100644 index 000000000..0f290fd7f Binary files /dev/null and b/Tests/images/apng/blend_op_source_transparent.png differ diff --git a/Tests/images/apng/chunk_actl_after_idat.png b/Tests/images/apng/chunk_actl_after_idat.png new file mode 100644 index 000000000..296a29d4c Binary files /dev/null and b/Tests/images/apng/chunk_actl_after_idat.png differ diff --git a/Tests/images/apng/chunk_multi_actl.png b/Tests/images/apng/chunk_multi_actl.png new file mode 100644 index 000000000..213f88549 Binary files /dev/null and b/Tests/images/apng/chunk_multi_actl.png differ diff --git a/Tests/images/apng/chunk_no_actl.png b/Tests/images/apng/chunk_no_actl.png new file mode 100644 index 000000000..5b68c7b44 Binary files /dev/null and b/Tests/images/apng/chunk_no_actl.png differ diff --git a/Tests/images/apng/chunk_no_fctl.png b/Tests/images/apng/chunk_no_fctl.png new file mode 100644 index 000000000..58ca904ab Binary files /dev/null and b/Tests/images/apng/chunk_no_fctl.png differ diff --git a/Tests/images/apng/chunk_no_fdat.png b/Tests/images/apng/chunk_no_fdat.png new file mode 100644 index 000000000..af42766b5 Binary files /dev/null and b/Tests/images/apng/chunk_no_fdat.png differ diff --git a/Tests/images/apng/chunk_repeat_fctl.png b/Tests/images/apng/chunk_repeat_fctl.png new file mode 100644 index 000000000..a5779855f Binary files /dev/null and b/Tests/images/apng/chunk_repeat_fctl.png differ diff --git a/Tests/images/apng/delay.png b/Tests/images/apng/delay.png new file mode 100644 index 000000000..64cceaae8 Binary files /dev/null and b/Tests/images/apng/delay.png differ diff --git a/Tests/images/apng/delay_round.png b/Tests/images/apng/delay_round.png new file mode 100644 index 000000000..3f082665c Binary files /dev/null and b/Tests/images/apng/delay_round.png differ diff --git a/Tests/images/apng/delay_short_max.png b/Tests/images/apng/delay_short_max.png new file mode 100644 index 000000000..99d53b718 Binary files /dev/null and b/Tests/images/apng/delay_short_max.png differ diff --git a/Tests/images/apng/delay_zero_denom.png b/Tests/images/apng/delay_zero_denom.png new file mode 100644 index 000000000..bad60c767 Binary files /dev/null and b/Tests/images/apng/delay_zero_denom.png differ diff --git a/Tests/images/apng/delay_zero_numer.png b/Tests/images/apng/delay_zero_numer.png new file mode 100644 index 000000000..a029a959b Binary files /dev/null and b/Tests/images/apng/delay_zero_numer.png differ diff --git a/Tests/images/apng/dispose_op_background.png b/Tests/images/apng/dispose_op_background.png new file mode 100644 index 000000000..b63ebc0b3 Binary files /dev/null and b/Tests/images/apng/dispose_op_background.png differ diff --git a/Tests/images/apng/dispose_op_background_before_region.png b/Tests/images/apng/dispose_op_background_before_region.png new file mode 100644 index 000000000..427b829a0 Binary files /dev/null and b/Tests/images/apng/dispose_op_background_before_region.png differ diff --git a/Tests/images/apng/dispose_op_background_final.png b/Tests/images/apng/dispose_op_background_final.png new file mode 100644 index 000000000..77694ff1d Binary files /dev/null and b/Tests/images/apng/dispose_op_background_final.png differ diff --git a/Tests/images/apng/dispose_op_background_p_mode.png b/Tests/images/apng/dispose_op_background_p_mode.png new file mode 100644 index 000000000..e5fb4784d Binary files /dev/null and b/Tests/images/apng/dispose_op_background_p_mode.png differ diff --git a/Tests/images/apng/dispose_op_background_region.png b/Tests/images/apng/dispose_op_background_region.png new file mode 100644 index 000000000..05948d44a Binary files /dev/null and b/Tests/images/apng/dispose_op_background_region.png differ diff --git a/Tests/images/apng/dispose_op_none.png b/Tests/images/apng/dispose_op_none.png new file mode 100644 index 000000000..3094c1d23 Binary files /dev/null and b/Tests/images/apng/dispose_op_none.png differ diff --git a/Tests/images/apng/dispose_op_none_region.png b/Tests/images/apng/dispose_op_none_region.png new file mode 100644 index 000000000..4e1dbf77e Binary files /dev/null and b/Tests/images/apng/dispose_op_none_region.png differ diff --git a/Tests/images/apng/dispose_op_previous.png b/Tests/images/apng/dispose_op_previous.png new file mode 100644 index 000000000..1c15f132f Binary files /dev/null and b/Tests/images/apng/dispose_op_previous.png differ diff --git a/Tests/images/apng/dispose_op_previous_final.png b/Tests/images/apng/dispose_op_previous_final.png new file mode 100644 index 000000000..858f6f038 Binary files /dev/null and b/Tests/images/apng/dispose_op_previous_final.png differ diff --git a/Tests/images/apng/dispose_op_previous_first.png b/Tests/images/apng/dispose_op_previous_first.png new file mode 100644 index 000000000..3f9b3cfae Binary files /dev/null and b/Tests/images/apng/dispose_op_previous_first.png differ diff --git a/Tests/images/apng/dispose_op_previous_region.png b/Tests/images/apng/dispose_op_previous_region.png new file mode 100644 index 000000000..f326afa5c Binary files /dev/null and b/Tests/images/apng/dispose_op_previous_region.png differ diff --git a/Tests/images/apng/fctl_actl.png b/Tests/images/apng/fctl_actl.png new file mode 100644 index 000000000..d0418ddd7 Binary files /dev/null and b/Tests/images/apng/fctl_actl.png differ diff --git a/Tests/images/apng/mode_16bit.png b/Tests/images/apng/mode_16bit.png new file mode 100644 index 000000000..1210e3737 Binary files /dev/null and b/Tests/images/apng/mode_16bit.png differ diff --git a/Tests/images/apng/mode_greyscale.png b/Tests/images/apng/mode_greyscale.png new file mode 100644 index 000000000..29ed7d1ea Binary files /dev/null and b/Tests/images/apng/mode_greyscale.png differ diff --git a/Tests/images/apng/mode_greyscale_alpha.png b/Tests/images/apng/mode_greyscale_alpha.png new file mode 100644 index 000000000..f9307f635 Binary files /dev/null and b/Tests/images/apng/mode_greyscale_alpha.png differ diff --git a/Tests/images/apng/mode_palette.png b/Tests/images/apng/mode_palette.png new file mode 100644 index 000000000..11ccfb6cb Binary files /dev/null and b/Tests/images/apng/mode_palette.png differ diff --git a/Tests/images/apng/mode_palette_1bit_alpha.png b/Tests/images/apng/mode_palette_1bit_alpha.png new file mode 100644 index 000000000..e95425ac1 Binary files /dev/null and b/Tests/images/apng/mode_palette_1bit_alpha.png differ diff --git a/Tests/images/apng/mode_palette_alpha.png b/Tests/images/apng/mode_palette_alpha.png new file mode 100644 index 000000000..f3c4c9f9e Binary files /dev/null and b/Tests/images/apng/mode_palette_alpha.png differ diff --git a/Tests/images/apng/num_plays.png b/Tests/images/apng/num_plays.png new file mode 100644 index 000000000..4d76802e4 Binary files /dev/null and b/Tests/images/apng/num_plays.png differ diff --git a/Tests/images/apng/num_plays_1.png b/Tests/images/apng/num_plays_1.png new file mode 100644 index 000000000..fb2539430 Binary files /dev/null and b/Tests/images/apng/num_plays_1.png differ diff --git a/Tests/images/apng/sequence_fdat_fctl.png b/Tests/images/apng/sequence_fdat_fctl.png new file mode 100644 index 000000000..29ac75e16 Binary files /dev/null and b/Tests/images/apng/sequence_fdat_fctl.png differ diff --git a/Tests/images/apng/sequence_gap.png b/Tests/images/apng/sequence_gap.png new file mode 100644 index 000000000..25dd9bcd8 Binary files /dev/null and b/Tests/images/apng/sequence_gap.png differ diff --git a/Tests/images/apng/sequence_reorder.png b/Tests/images/apng/sequence_reorder.png new file mode 100644 index 000000000..dc78e9bb1 Binary files /dev/null and b/Tests/images/apng/sequence_reorder.png differ diff --git a/Tests/images/apng/sequence_reorder_chunk.png b/Tests/images/apng/sequence_reorder_chunk.png new file mode 100644 index 000000000..5d951ffe2 Binary files /dev/null and b/Tests/images/apng/sequence_reorder_chunk.png differ diff --git a/Tests/images/apng/sequence_repeat.png b/Tests/images/apng/sequence_repeat.png new file mode 100644 index 000000000..d5cf83f9f Binary files /dev/null and b/Tests/images/apng/sequence_repeat.png differ diff --git a/Tests/images/apng/sequence_repeat_chunk.png b/Tests/images/apng/sequence_repeat_chunk.png new file mode 100644 index 000000000..27d1d3eb5 Binary files /dev/null and b/Tests/images/apng/sequence_repeat_chunk.png differ diff --git a/Tests/images/apng/sequence_start.png b/Tests/images/apng/sequence_start.png new file mode 100644 index 000000000..5e040743a Binary files /dev/null and b/Tests/images/apng/sequence_start.png differ diff --git a/Tests/images/apng/single_frame.png b/Tests/images/apng/single_frame.png new file mode 100644 index 000000000..0cd5bea85 Binary files /dev/null and b/Tests/images/apng/single_frame.png differ diff --git a/Tests/images/apng/single_frame_default.png b/Tests/images/apng/single_frame_default.png new file mode 100644 index 000000000..db7581fbd Binary files /dev/null and b/Tests/images/apng/single_frame_default.png differ diff --git a/Tests/images/apng/split_fdat.png b/Tests/images/apng/split_fdat.png new file mode 100644 index 000000000..2dc58b929 Binary files /dev/null and b/Tests/images/apng/split_fdat.png differ diff --git a/Tests/images/apng/split_fdat_zero_chunk.png b/Tests/images/apng/split_fdat_zero_chunk.png new file mode 100644 index 000000000..14a76d9d6 Binary files /dev/null and b/Tests/images/apng/split_fdat_zero_chunk.png differ diff --git a/Tests/images/apng/syntax_num_frames_high.png b/Tests/images/apng/syntax_num_frames_high.png new file mode 100644 index 000000000..bba9cdfd5 Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_high.png differ diff --git a/Tests/images/apng/syntax_num_frames_invalid.png b/Tests/images/apng/syntax_num_frames_invalid.png new file mode 100644 index 000000000..ca7b13ab8 Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_invalid.png differ diff --git a/Tests/images/apng/syntax_num_frames_low.png b/Tests/images/apng/syntax_num_frames_low.png new file mode 100644 index 000000000..6f895f91d Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_low.png differ diff --git a/Tests/images/apng/syntax_num_frames_zero.png b/Tests/images/apng/syntax_num_frames_zero.png new file mode 100644 index 000000000..0cb7ea36e Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_zero.png differ diff --git a/Tests/images/apng/syntax_num_frames_zero_default.png b/Tests/images/apng/syntax_num_frames_zero_default.png new file mode 100644 index 000000000..89f2b75e2 Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_zero_default.png differ diff --git a/Tests/images/app13-multiple.jpg b/Tests/images/app13-multiple.jpg new file mode 100644 index 000000000..8341383a0 Binary files /dev/null and b/Tests/images/app13-multiple.jpg differ diff --git a/Tests/images/argb-32bpp_MipMaps-1.dds b/Tests/images/argb-32bpp_MipMaps-1.dds new file mode 100644 index 000000000..d1d1998b1 Binary files /dev/null and b/Tests/images/argb-32bpp_MipMaps-1.dds differ diff --git a/Tests/images/argb-32bpp_MipMaps-1.png b/Tests/images/argb-32bpp_MipMaps-1.png new file mode 100644 index 000000000..3570ccf35 Binary files /dev/null and b/Tests/images/argb-32bpp_MipMaps-1.png differ diff --git a/Tests/images/bitmap_font_1_basic.png b/Tests/images/bitmap_font_1_basic.png new file mode 100644 index 000000000..01a05606c Binary files /dev/null and b/Tests/images/bitmap_font_1_basic.png differ diff --git a/Tests/images/bitmap_font_1_raqm.png b/Tests/images/bitmap_font_1_raqm.png new file mode 100644 index 000000000..560efb685 Binary files /dev/null and b/Tests/images/bitmap_font_1_raqm.png differ diff --git a/Tests/images/bitmap_font_2_basic.png b/Tests/images/bitmap_font_2_basic.png new file mode 100644 index 000000000..44d137dd6 Binary files /dev/null and b/Tests/images/bitmap_font_2_basic.png differ diff --git a/Tests/images/bitmap_font_2_raqm.png b/Tests/images/bitmap_font_2_raqm.png new file mode 100644 index 000000000..7a40bd6c2 Binary files /dev/null and b/Tests/images/bitmap_font_2_raqm.png differ diff --git a/Tests/images/bitmap_font_4_basic.png b/Tests/images/bitmap_font_4_basic.png new file mode 100644 index 000000000..e79d86aa8 Binary files /dev/null and b/Tests/images/bitmap_font_4_basic.png differ diff --git a/Tests/images/bitmap_font_4_raqm.png b/Tests/images/bitmap_font_4_raqm.png new file mode 100644 index 000000000..d98a3bc3e Binary files /dev/null and b/Tests/images/bitmap_font_4_raqm.png differ diff --git a/Tests/images/bitmap_font_8_basic.png b/Tests/images/bitmap_font_8_basic.png new file mode 100644 index 000000000..15a7c9809 Binary files /dev/null and b/Tests/images/bitmap_font_8_basic.png differ diff --git a/Tests/images/bitmap_font_8_raqm.png b/Tests/images/bitmap_font_8_raqm.png new file mode 100644 index 000000000..1ad088c93 Binary files /dev/null and b/Tests/images/bitmap_font_8_raqm.png differ diff --git a/Tests/images/cbdt_notocoloremoji.png b/Tests/images/cbdt_notocoloremoji.png new file mode 100644 index 000000000..1da12fba1 Binary files /dev/null and b/Tests/images/cbdt_notocoloremoji.png differ diff --git a/Tests/images/cbdt_notocoloremoji_mask.png b/Tests/images/cbdt_notocoloremoji_mask.png new file mode 100644 index 000000000..6d036a0b6 Binary files /dev/null and b/Tests/images/cbdt_notocoloremoji_mask.png differ diff --git a/Tests/images/colr_bungee.png b/Tests/images/colr_bungee.png new file mode 100644 index 000000000..b10a60be0 Binary files /dev/null and b/Tests/images/colr_bungee.png differ diff --git a/Tests/images/colr_bungee_mask.png b/Tests/images/colr_bungee_mask.png new file mode 100644 index 000000000..f13e17677 Binary files /dev/null and b/Tests/images/colr_bungee_mask.png differ diff --git a/Tests/images/drawing_wmf_ref_144.png b/Tests/images/drawing_wmf_ref_144.png new file mode 100644 index 000000000..20ed9ce59 Binary files /dev/null and b/Tests/images/drawing_wmf_ref_144.png differ diff --git a/Tests/images/empty_gps_ifd.jpg b/Tests/images/empty_gps_ifd.jpg new file mode 100644 index 000000000..28f180b87 Binary files /dev/null and b/Tests/images/empty_gps_ifd.jpg differ diff --git a/Tests/images/exif_imagemagick.png b/Tests/images/exif_imagemagick.png new file mode 100644 index 000000000..6f59224c8 Binary files /dev/null and b/Tests/images/exif_imagemagick.png differ diff --git a/Tests/images/exif_text.png b/Tests/images/exif_text.png new file mode 100644 index 000000000..e2d8dc0ff Binary files /dev/null and b/Tests/images/exif_text.png differ diff --git a/Tests/images/fli_oob/02r/02r00.fli b/Tests/images/fli_oob/02r/02r00.fli new file mode 100644 index 000000000..eac0e4304 Binary files /dev/null and b/Tests/images/fli_oob/02r/02r00.fli differ diff --git a/Tests/images/fli_oob/02r/notes b/Tests/images/fli_oob/02r/notes new file mode 100644 index 000000000..49f92b19b --- /dev/null +++ b/Tests/images/fli_oob/02r/notes @@ -0,0 +1 @@ +Is this because a file-originating field is being interpreted as a *signed* int32, allowing it to provide negative values for 'advance'? diff --git a/Tests/images/fli_oob/02r/others/02r01.fli b/Tests/images/fli_oob/02r/others/02r01.fli new file mode 100644 index 000000000..3a5864c84 Binary files /dev/null and b/Tests/images/fli_oob/02r/others/02r01.fli differ diff --git a/Tests/images/fli_oob/02r/others/02r02.fli b/Tests/images/fli_oob/02r/others/02r02.fli new file mode 100644 index 000000000..2b3d15b55 Binary files /dev/null and b/Tests/images/fli_oob/02r/others/02r02.fli differ diff --git a/Tests/images/fli_oob/02r/others/02r03.fli b/Tests/images/fli_oob/02r/others/02r03.fli new file mode 100644 index 000000000..a63172132 Binary files /dev/null and b/Tests/images/fli_oob/02r/others/02r03.fli differ diff --git a/Tests/images/fli_oob/02r/others/02r04.fli b/Tests/images/fli_oob/02r/others/02r04.fli new file mode 100644 index 000000000..4c17cbb3d Binary files /dev/null and b/Tests/images/fli_oob/02r/others/02r04.fli differ diff --git a/Tests/images/fli_oob/02r/reproducing b/Tests/images/fli_oob/02r/reproducing new file mode 100644 index 000000000..3286d94f1 --- /dev/null +++ b/Tests/images/fli_oob/02r/reproducing @@ -0,0 +1 @@ +Image.open(...).seek(212) diff --git a/Tests/images/fli_oob/03r/03r00.fli b/Tests/images/fli_oob/03r/03r00.fli new file mode 100644 index 000000000..7972880ce Binary files /dev/null and b/Tests/images/fli_oob/03r/03r00.fli differ diff --git a/Tests/images/fli_oob/03r/notes b/Tests/images/fli_oob/03r/notes new file mode 100644 index 000000000..d75605cea --- /dev/null +++ b/Tests/images/fli_oob/03r/notes @@ -0,0 +1 @@ +ridiculous bytes value passed to ImagingFliDecode diff --git a/Tests/images/fli_oob/03r/others/03r01.fli b/Tests/images/fli_oob/03r/others/03r01.fli new file mode 100644 index 000000000..1102c69ca Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r01.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r02.fli b/Tests/images/fli_oob/03r/others/03r02.fli new file mode 100644 index 000000000..d30326fe0 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r02.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r03.fli b/Tests/images/fli_oob/03r/others/03r03.fli new file mode 100644 index 000000000..7f3db178e Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r03.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r04.fli b/Tests/images/fli_oob/03r/others/03r04.fli new file mode 100644 index 000000000..f05375e84 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r04.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r05.fli b/Tests/images/fli_oob/03r/others/03r05.fli new file mode 100644 index 000000000..037944324 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r05.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r06.fli b/Tests/images/fli_oob/03r/others/03r06.fli new file mode 100644 index 000000000..1527cbf91 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r06.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r07.fli b/Tests/images/fli_oob/03r/others/03r07.fli new file mode 100644 index 000000000..c9dea4135 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r07.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r08.fli b/Tests/images/fli_oob/03r/others/03r08.fli new file mode 100644 index 000000000..698101443 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r08.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r09.fli b/Tests/images/fli_oob/03r/others/03r09.fli new file mode 100644 index 000000000..12058480a Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r09.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r10.fli b/Tests/images/fli_oob/03r/others/03r10.fli new file mode 100644 index 000000000..448b0a812 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r10.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r11.fli b/Tests/images/fli_oob/03r/others/03r11.fli new file mode 100644 index 000000000..db1b5fe58 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r11.fli differ diff --git a/Tests/images/fli_oob/03r/reproducing b/Tests/images/fli_oob/03r/reproducing new file mode 100644 index 000000000..145b8b074 --- /dev/null +++ b/Tests/images/fli_oob/03r/reproducing @@ -0,0 +1 @@ +im = Image.open(d); im.seek(0); im.getdata() diff --git a/Tests/images/fli_oob/04r/04r00.fli b/Tests/images/fli_oob/04r/04r00.fli new file mode 100644 index 000000000..c4e416f39 Binary files /dev/null and b/Tests/images/fli_oob/04r/04r00.fli differ diff --git a/Tests/images/fli_oob/04r/initial.fli b/Tests/images/fli_oob/04r/initial.fli new file mode 100644 index 000000000..5a8659f7c Binary files /dev/null and b/Tests/images/fli_oob/04r/initial.fli differ diff --git a/Tests/images/fli_oob/04r/notes b/Tests/images/fli_oob/04r/notes new file mode 100644 index 000000000..7922e0ba8 --- /dev/null +++ b/Tests/images/fli_oob/04r/notes @@ -0,0 +1 @@ +failure to check input buffer (`data`) boundaries in BRUN chunk diff --git a/Tests/images/fli_oob/04r/others/04r01.fli b/Tests/images/fli_oob/04r/others/04r01.fli new file mode 100644 index 000000000..af968970b Binary files /dev/null and b/Tests/images/fli_oob/04r/others/04r01.fli differ diff --git a/Tests/images/fli_oob/04r/others/04r02.fli b/Tests/images/fli_oob/04r/others/04r02.fli new file mode 100644 index 000000000..ae027fc11 Binary files /dev/null and b/Tests/images/fli_oob/04r/others/04r02.fli differ diff --git a/Tests/images/fli_oob/04r/others/04r03.fli b/Tests/images/fli_oob/04r/others/04r03.fli new file mode 100644 index 000000000..ab92f4b6a Binary files /dev/null and b/Tests/images/fli_oob/04r/others/04r03.fli differ diff --git a/Tests/images/fli_oob/04r/others/04r04.fli b/Tests/images/fli_oob/04r/others/04r04.fli new file mode 100644 index 000000000..533ffa027 Binary files /dev/null and b/Tests/images/fli_oob/04r/others/04r04.fli differ diff --git a/Tests/images/fli_oob/04r/others/04r05.fli b/Tests/images/fli_oob/04r/others/04r05.fli new file mode 100644 index 000000000..b07ef6496 Binary files /dev/null and b/Tests/images/fli_oob/04r/others/04r05.fli differ diff --git a/Tests/images/fli_oob/04r/reproducing b/Tests/images/fli_oob/04r/reproducing new file mode 100644 index 000000000..145b8b074 --- /dev/null +++ b/Tests/images/fli_oob/04r/reproducing @@ -0,0 +1 @@ +im = Image.open(d); im.seek(0); im.getdata() diff --git a/Tests/images/fli_oob/05r/05r00.fli b/Tests/images/fli_oob/05r/05r00.fli new file mode 100644 index 000000000..dff5a01e8 Binary files /dev/null and b/Tests/images/fli_oob/05r/05r00.fli differ diff --git a/Tests/images/fli_oob/05r/notes b/Tests/images/fli_oob/05r/notes new file mode 100644 index 000000000..bec9db779 --- /dev/null +++ b/Tests/images/fli_oob/05r/notes @@ -0,0 +1 @@ +failure to check input buffer (`data`) boundaries in LC chunk diff --git a/Tests/images/fli_oob/05r/others/05r01.fli b/Tests/images/fli_oob/05r/others/05r01.fli new file mode 100644 index 000000000..1ad3444fe Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r01.fli differ diff --git a/Tests/images/fli_oob/05r/others/05r02.fli b/Tests/images/fli_oob/05r/others/05r02.fli new file mode 100644 index 000000000..cd6429884 Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r02.fli differ diff --git a/Tests/images/fli_oob/05r/others/05r03.fli b/Tests/images/fli_oob/05r/others/05r03.fli new file mode 100644 index 000000000..2a4be914c Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r03.fli differ diff --git a/Tests/images/fli_oob/05r/others/05r04.fli b/Tests/images/fli_oob/05r/others/05r04.fli new file mode 100644 index 000000000..0b547c7e2 Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r04.fli differ diff --git a/Tests/images/fli_oob/05r/others/05r05.fli b/Tests/images/fli_oob/05r/others/05r05.fli new file mode 100644 index 000000000..0bf775230 Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r05.fli differ diff --git a/Tests/images/fli_oob/05r/others/05r06.fli b/Tests/images/fli_oob/05r/others/05r06.fli new file mode 100644 index 000000000..c35b8e232 Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r06.fli differ diff --git a/Tests/images/fli_oob/05r/others/05r07.fli b/Tests/images/fli_oob/05r/others/05r07.fli new file mode 100644 index 000000000..b99ce01b3 Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r07.fli differ diff --git a/Tests/images/fli_oob/05r/reproducing b/Tests/images/fli_oob/05r/reproducing new file mode 100644 index 000000000..145b8b074 --- /dev/null +++ b/Tests/images/fli_oob/05r/reproducing @@ -0,0 +1 @@ +im = Image.open(d); im.seek(0); im.getdata() diff --git a/Tests/images/fli_oob/06r/06r00.fli b/Tests/images/fli_oob/06r/06r00.fli new file mode 100644 index 000000000..9189d6ed0 Binary files /dev/null and b/Tests/images/fli_oob/06r/06r00.fli differ diff --git a/Tests/images/fli_oob/06r/notes b/Tests/images/fli_oob/06r/notes new file mode 100644 index 000000000..397ad4748 --- /dev/null +++ b/Tests/images/fli_oob/06r/notes @@ -0,0 +1 @@ +failure to check input buffer (`data`) boundaries in SS2 chunk diff --git a/Tests/images/fli_oob/06r/others/06r01.fli b/Tests/images/fli_oob/06r/others/06r01.fli new file mode 100644 index 000000000..24a99dacc Binary files /dev/null and b/Tests/images/fli_oob/06r/others/06r01.fli differ diff --git a/Tests/images/fli_oob/06r/others/06r02.fli b/Tests/images/fli_oob/06r/others/06r02.fli new file mode 100644 index 000000000..02067a32c Binary files /dev/null and b/Tests/images/fli_oob/06r/others/06r02.fli differ diff --git a/Tests/images/fli_oob/06r/others/06r03.fli b/Tests/images/fli_oob/06r/others/06r03.fli new file mode 100644 index 000000000..649668c0a Binary files /dev/null and b/Tests/images/fli_oob/06r/others/06r03.fli differ diff --git a/Tests/images/fli_oob/06r/others/06r04.fli b/Tests/images/fli_oob/06r/others/06r04.fli new file mode 100644 index 000000000..bff28ccfc Binary files /dev/null and b/Tests/images/fli_oob/06r/others/06r04.fli differ diff --git a/Tests/images/fli_oob/06r/reproducing b/Tests/images/fli_oob/06r/reproducing new file mode 100644 index 000000000..145b8b074 --- /dev/null +++ b/Tests/images/fli_oob/06r/reproducing @@ -0,0 +1 @@ +im = Image.open(d); im.seek(0); im.getdata() diff --git a/Tests/images/fli_oob/patch0/000000 b/Tests/images/fli_oob/patch0/000000 new file mode 100644 index 000000000..e074e4a76 Binary files /dev/null and b/Tests/images/fli_oob/patch0/000000 differ diff --git a/Tests/images/fli_oob/patch0/000001 b/Tests/images/fli_oob/patch0/000001 new file mode 100644 index 000000000..6cfd7f647 Binary files /dev/null and b/Tests/images/fli_oob/patch0/000001 differ diff --git a/Tests/images/fli_oob/patch0/000002 b/Tests/images/fli_oob/patch0/000002 new file mode 100644 index 000000000..ff5a6b63b Binary files /dev/null and b/Tests/images/fli_oob/patch0/000002 differ diff --git a/Tests/images/fli_oob/patch0/000003 b/Tests/images/fli_oob/patch0/000003 new file mode 100644 index 000000000..12c15b43e Binary files /dev/null and b/Tests/images/fli_oob/patch0/000003 differ diff --git a/Tests/images/fli_overrun2.bin b/Tests/images/fli_overrun2.bin new file mode 100644 index 000000000..4afdb6f89 Binary files /dev/null and b/Tests/images/fli_overrun2.bin differ diff --git a/Tests/images/hopper_16bit_qtables.jpg b/Tests/images/hopper_16bit_qtables.jpg new file mode 100644 index 000000000..88abef943 Binary files /dev/null and b/Tests/images/hopper_16bit_qtables.jpg differ diff --git a/Tests/images/hopper_long_name.im b/Tests/images/hopper_long_name.im new file mode 100644 index 000000000..ff45b7c75 Binary files /dev/null and b/Tests/images/hopper_long_name.im differ diff --git a/Tests/images/icc-after-SOF.jpg b/Tests/images/icc-after-SOF.jpg new file mode 100644 index 000000000..a284a2298 Binary files /dev/null and b/Tests/images/icc-after-SOF.jpg differ diff --git a/Tests/images/ifd_tag_type.tiff b/Tests/images/ifd_tag_type.tiff new file mode 100644 index 000000000..316d2089e Binary files /dev/null and b/Tests/images/ifd_tag_type.tiff differ diff --git a/Tests/images/imagedraw_arc.png b/Tests/images/imagedraw_arc.png index b09774389..967e214d9 100644 Binary files a/Tests/images/imagedraw_arc.png and b/Tests/images/imagedraw_arc.png differ diff --git a/Tests/images/imagedraw_arc_end_le_start.png b/Tests/images/imagedraw_arc_end_le_start.png index aee48e1c6..191cc0b3a 100644 Binary files a/Tests/images/imagedraw_arc_end_le_start.png and b/Tests/images/imagedraw_arc_end_le_start.png differ diff --git a/Tests/images/imagedraw_arc_high.png b/Tests/images/imagedraw_arc_high.png new file mode 100644 index 000000000..e3fb66cd0 Binary files /dev/null and b/Tests/images/imagedraw_arc_high.png differ diff --git a/Tests/images/imagedraw_arc_no_loops.png b/Tests/images/imagedraw_arc_no_loops.png index e45ad57a5..03bbd4b43 100644 Binary files a/Tests/images/imagedraw_arc_no_loops.png and b/Tests/images/imagedraw_arc_no_loops.png differ diff --git a/Tests/images/imagedraw_arc_width.png b/Tests/images/imagedraw_arc_width.png index ff3f1f0b2..70dae7d5f 100644 Binary files a/Tests/images/imagedraw_arc_width.png and b/Tests/images/imagedraw_arc_width.png differ diff --git a/Tests/images/imagedraw_arc_width_fill.png b/Tests/images/imagedraw_arc_width_fill.png index 9572a6059..6c135ab76 100644 Binary files a/Tests/images/imagedraw_arc_width_fill.png and b/Tests/images/imagedraw_arc_width_fill.png differ diff --git a/Tests/images/imagedraw_arc_width_non_whole_angle.png b/Tests/images/imagedraw_arc_width_non_whole_angle.png index 1fb9a3c86..f54eb1c29 100644 Binary files a/Tests/images/imagedraw_arc_width_non_whole_angle.png and b/Tests/images/imagedraw_arc_width_non_whole_angle.png differ diff --git a/Tests/images/imagedraw_arc_width_pieslice.png b/Tests/images/imagedraw_arc_width_pieslice.png index 950d95dd6..e1aa95e88 100644 Binary files a/Tests/images/imagedraw_arc_width_pieslice.png and b/Tests/images/imagedraw_arc_width_pieslice.png differ diff --git a/Tests/images/imagedraw_chord_L.png b/Tests/images/imagedraw_chord_L.png index a5a0078d0..6c89da9ea 100644 Binary files a/Tests/images/imagedraw_chord_L.png and b/Tests/images/imagedraw_chord_L.png differ diff --git a/Tests/images/imagedraw_chord_RGB.png b/Tests/images/imagedraw_chord_RGB.png index af6fc7660..d4592cda1 100644 Binary files a/Tests/images/imagedraw_chord_RGB.png and b/Tests/images/imagedraw_chord_RGB.png differ diff --git a/Tests/images/imagedraw_chord_too_fat.png b/Tests/images/imagedraw_chord_too_fat.png new file mode 100644 index 000000000..2021202fe Binary files /dev/null and b/Tests/images/imagedraw_chord_too_fat.png differ diff --git a/Tests/images/imagedraw_chord_width.png b/Tests/images/imagedraw_chord_width.png index 33a59b487..04d3dadf8 100644 Binary files a/Tests/images/imagedraw_chord_width.png and b/Tests/images/imagedraw_chord_width.png differ diff --git a/Tests/images/imagedraw_chord_width_fill.png b/Tests/images/imagedraw_chord_width_fill.png index 809c3ea1c..77475d266 100644 Binary files a/Tests/images/imagedraw_chord_width_fill.png and b/Tests/images/imagedraw_chord_width_fill.png differ diff --git a/Tests/images/imagedraw_chord_zero_width.png b/Tests/images/imagedraw_chord_zero_width.png new file mode 100644 index 000000000..c8ebe3917 Binary files /dev/null and b/Tests/images/imagedraw_chord_zero_width.png differ diff --git a/Tests/images/imagedraw_ellipse_L.png b/Tests/images/imagedraw_ellipse_L.png index e47e6e441..d5959cc08 100644 Binary files a/Tests/images/imagedraw_ellipse_L.png and b/Tests/images/imagedraw_ellipse_L.png differ diff --git a/Tests/images/imagedraw_ellipse_RGB.png b/Tests/images/imagedraw_ellipse_RGB.png index b52b12802..1d310ce11 100644 Binary files a/Tests/images/imagedraw_ellipse_RGB.png and b/Tests/images/imagedraw_ellipse_RGB.png differ diff --git a/Tests/images/imagedraw_ellipse_edge.png b/Tests/images/imagedraw_ellipse_edge.png index 25a95a601..a2235115a 100644 Binary files a/Tests/images/imagedraw_ellipse_edge.png and b/Tests/images/imagedraw_ellipse_edge.png differ diff --git a/Tests/images/imagedraw_ellipse_translucent.png b/Tests/images/imagedraw_ellipse_translucent.png new file mode 100644 index 000000000..964dce678 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_translucent.png differ diff --git a/Tests/images/imagedraw_ellipse_various_sizes.png b/Tests/images/imagedraw_ellipse_various_sizes.png new file mode 100644 index 000000000..11a1be6fa Binary files /dev/null and b/Tests/images/imagedraw_ellipse_various_sizes.png differ diff --git a/Tests/images/imagedraw_ellipse_various_sizes_filled.png b/Tests/images/imagedraw_ellipse_various_sizes_filled.png new file mode 100644 index 000000000..d71e175b8 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_various_sizes_filled.png differ diff --git a/Tests/images/imagedraw_ellipse_width.png b/Tests/images/imagedraw_ellipse_width.png index ec0ca6731..54cfc291c 100644 Binary files a/Tests/images/imagedraw_ellipse_width.png and b/Tests/images/imagedraw_ellipse_width.png differ diff --git a/Tests/images/imagedraw_ellipse_width_fill.png b/Tests/images/imagedraw_ellipse_width_fill.png index 9b7be6029..4a1edc379 100644 Binary files a/Tests/images/imagedraw_ellipse_width_fill.png and b/Tests/images/imagedraw_ellipse_width_fill.png differ diff --git a/Tests/images/imagedraw_ellipse_width_large.png b/Tests/images/imagedraw_ellipse_width_large.png index 9d3c3326b..e95186019 100644 Binary files a/Tests/images/imagedraw_ellipse_width_large.png and b/Tests/images/imagedraw_ellipse_width_large.png differ diff --git a/Tests/images/imagedraw_ellipse_zero_width.png b/Tests/images/imagedraw_ellipse_zero_width.png new file mode 100644 index 000000000..6661b7d99 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_zero_width.png differ diff --git a/Tests/images/imagedraw_floodfill_L.png b/Tests/images/imagedraw_floodfill_L.png index 4139e66d8..daaf9422d 100644 Binary files a/Tests/images/imagedraw_floodfill_L.png and b/Tests/images/imagedraw_floodfill_L.png differ diff --git a/Tests/images/imagedraw_outline_chord_RGB.png b/Tests/images/imagedraw_outline_chord_RGB.png index 9e9cb5af0..3c71312c7 100644 Binary files a/Tests/images/imagedraw_outline_chord_RGB.png and b/Tests/images/imagedraw_outline_chord_RGB.png differ diff --git a/Tests/images/imagedraw_pieslice.png b/Tests/images/imagedraw_pieslice.png index 2f8c09191..41c786e77 100644 Binary files a/Tests/images/imagedraw_pieslice.png and b/Tests/images/imagedraw_pieslice.png differ diff --git a/Tests/images/imagedraw_pieslice_wide.png b/Tests/images/imagedraw_pieslice_wide.png new file mode 100644 index 000000000..446874788 Binary files /dev/null and b/Tests/images/imagedraw_pieslice_wide.png differ diff --git a/Tests/images/imagedraw_pieslice_width.png b/Tests/images/imagedraw_pieslice_width.png index 3bd69222c..422d92f3b 100644 Binary files a/Tests/images/imagedraw_pieslice_width.png and b/Tests/images/imagedraw_pieslice_width.png differ diff --git a/Tests/images/imagedraw_pieslice_width_fill.png b/Tests/images/imagedraw_pieslice_width_fill.png index c5a34e0f3..bee6aac3b 100644 Binary files a/Tests/images/imagedraw_pieslice_width_fill.png and b/Tests/images/imagedraw_pieslice_width_fill.png differ diff --git a/Tests/images/imagedraw_pieslice_zero_width.png b/Tests/images/imagedraw_pieslice_zero_width.png new file mode 100644 index 000000000..d6ceb0b5a Binary files /dev/null and b/Tests/images/imagedraw_pieslice_zero_width.png differ diff --git a/Tests/images/imagedraw_polygon_1px_high.png b/Tests/images/imagedraw_polygon_1px_high.png new file mode 100644 index 000000000..e06508a0a Binary files /dev/null and b/Tests/images/imagedraw_polygon_1px_high.png differ diff --git a/Tests/images/imagedraw_polygon_kite_L.png b/Tests/images/imagedraw_polygon_kite_L.png index 241d86bf4..0d9a1c8f8 100644 Binary files a/Tests/images/imagedraw_polygon_kite_L.png and b/Tests/images/imagedraw_polygon_kite_L.png differ diff --git a/Tests/images/imagedraw_rectangle_zero_width.png b/Tests/images/imagedraw_rectangle_zero_width.png new file mode 100644 index 000000000..989c95761 Binary files /dev/null and b/Tests/images/imagedraw_rectangle_zero_width.png differ diff --git a/Tests/images/imagedraw_regular_octagon.png b/Tests/images/imagedraw_regular_octagon.png new file mode 100644 index 000000000..7f215dc08 Binary files /dev/null and b/Tests/images/imagedraw_regular_octagon.png differ diff --git a/Tests/images/imagedraw_square.png b/Tests/images/imagedraw_square.png new file mode 100644 index 000000000..fd75f2f3b Binary files /dev/null and b/Tests/images/imagedraw_square.png differ diff --git a/Tests/images/imagedraw_square_rotate_45.png b/Tests/images/imagedraw_square_rotate_45.png new file mode 100644 index 000000000..8ab0e3c18 Binary files /dev/null and b/Tests/images/imagedraw_square_rotate_45.png differ diff --git a/Tests/images/imagedraw_stroke_descender.png b/Tests/images/imagedraw_stroke_descender.png new file mode 100644 index 000000000..93462334a Binary files /dev/null and b/Tests/images/imagedraw_stroke_descender.png differ diff --git a/Tests/images/imagedraw_wide_line_larger_than_int.png b/Tests/images/imagedraw_wide_line_larger_than_int.png new file mode 100644 index 000000000..68073ce48 Binary files /dev/null and b/Tests/images/imagedraw_wide_line_larger_than_int.png differ diff --git a/Tests/images/imageops_pad_h_0.jpg b/Tests/images/imageops_pad_h_0.jpg index f9fcb1cdb..7afbbb96a 100644 Binary files a/Tests/images/imageops_pad_h_0.jpg and b/Tests/images/imageops_pad_h_0.jpg differ diff --git a/Tests/images/imageops_pad_h_1.jpg b/Tests/images/imageops_pad_h_1.jpg index 4b9b9ebc4..b9bf8a49a 100644 Binary files a/Tests/images/imageops_pad_h_1.jpg and b/Tests/images/imageops_pad_h_1.jpg differ diff --git a/Tests/images/imageops_pad_h_2.jpg b/Tests/images/imageops_pad_h_2.jpg index 2c8224892..7e0eb9599 100644 Binary files a/Tests/images/imageops_pad_h_2.jpg and b/Tests/images/imageops_pad_h_2.jpg differ diff --git a/Tests/images/imageops_pad_v_0.jpg b/Tests/images/imageops_pad_v_0.jpg index caf435796..73a96c86c 100644 Binary files a/Tests/images/imageops_pad_v_0.jpg and b/Tests/images/imageops_pad_v_0.jpg differ diff --git a/Tests/images/imageops_pad_v_1.jpg b/Tests/images/imageops_pad_v_1.jpg index 4a6698e91..04545f817 100644 Binary files a/Tests/images/imageops_pad_v_1.jpg and b/Tests/images/imageops_pad_v_1.jpg differ diff --git a/Tests/images/imageops_pad_v_2.jpg b/Tests/images/imageops_pad_v_2.jpg index 792952bcd..f3e399d7b 100644 Binary files a/Tests/images/imageops_pad_v_2.jpg and b/Tests/images/imageops_pad_v_2.jpg differ diff --git a/Tests/images/input_bw_five_bands.fpx b/Tests/images/input_bw_five_bands.fpx new file mode 100644 index 000000000..5fcb144ae Binary files /dev/null and b/Tests/images/input_bw_five_bands.fpx differ diff --git a/Tests/images/invalid-exif-without-x-resolution.jpg b/Tests/images/invalid-exif-without-x-resolution.jpg new file mode 100644 index 000000000..00f6bd2f3 Binary files /dev/null and b/Tests/images/invalid-exif-without-x-resolution.jpg differ diff --git a/Tests/images/pcx_overrun2.bin b/Tests/images/pcx_overrun2.bin new file mode 100644 index 000000000..5f00b5059 Binary files /dev/null and b/Tests/images/pcx_overrun2.bin differ diff --git a/Tests/images/photoshop-200dpi-broken.jpg b/Tests/images/photoshop-200dpi-broken.jpg new file mode 100644 index 000000000..a574872f2 Binary files /dev/null and b/Tests/images/photoshop-200dpi-broken.jpg differ diff --git a/Tests/images/radial_gradients.png b/Tests/images/radial_gradients.png new file mode 100644 index 000000000..39a02fbbf Binary files /dev/null and b/Tests/images/radial_gradients.png differ diff --git a/Tests/images/sgi_crash.bin b/Tests/images/sgi_crash.bin new file mode 100644 index 000000000..9b138f6fe Binary files /dev/null and b/Tests/images/sgi_crash.bin differ diff --git a/Tests/images/sgi_overrun_expandrow.bin b/Tests/images/sgi_overrun_expandrow.bin new file mode 100644 index 000000000..316d61881 Binary files /dev/null and b/Tests/images/sgi_overrun_expandrow.bin differ diff --git a/Tests/images/sgi_overrun_expandrow2.bin b/Tests/images/sgi_overrun_expandrow2.bin new file mode 100644 index 000000000..f70e03a39 Binary files /dev/null and b/Tests/images/sgi_overrun_expandrow2.bin differ diff --git a/Tests/images/sgi_overrun_expandrowF04.bin b/Tests/images/sgi_overrun_expandrowF04.bin new file mode 100644 index 000000000..1907d5d3d Binary files /dev/null and b/Tests/images/sgi_overrun_expandrowF04.bin differ diff --git a/Tests/images/standard_embedded.png b/Tests/images/standard_embedded.png new file mode 100644 index 000000000..890532531 Binary files /dev/null and b/Tests/images/standard_embedded.png differ diff --git a/Tests/images/string_dimension.tiff b/Tests/images/string_dimension.tiff deleted file mode 100644 index d0b558301..000000000 Binary files a/Tests/images/string_dimension.tiff and /dev/null differ diff --git a/Tests/images/sugarshack_no_data.mpo b/Tests/images/sugarshack_no_data.mpo new file mode 100644 index 000000000..d94bad53b Binary files /dev/null and b/Tests/images/sugarshack_no_data.mpo differ diff --git a/Tests/images/test_anchor_multiline_lm_center.png b/Tests/images/test_anchor_multiline_lm_center.png new file mode 100644 index 000000000..6fff287e4 Binary files /dev/null and b/Tests/images/test_anchor_multiline_lm_center.png differ diff --git a/Tests/images/test_anchor_multiline_lm_left.png b/Tests/images/test_anchor_multiline_lm_left.png new file mode 100644 index 000000000..b76a81b81 Binary files /dev/null and b/Tests/images/test_anchor_multiline_lm_left.png differ diff --git a/Tests/images/test_anchor_multiline_lm_right.png b/Tests/images/test_anchor_multiline_lm_right.png new file mode 100644 index 000000000..c12a8d63e Binary files /dev/null and b/Tests/images/test_anchor_multiline_lm_right.png differ diff --git a/Tests/images/test_anchor_multiline_ma_center.png b/Tests/images/test_anchor_multiline_ma_center.png new file mode 100644 index 000000000..4f35d781f Binary files /dev/null and b/Tests/images/test_anchor_multiline_ma_center.png differ diff --git a/Tests/images/test_anchor_multiline_md_center.png b/Tests/images/test_anchor_multiline_md_center.png new file mode 100644 index 000000000..8290d045c Binary files /dev/null and b/Tests/images/test_anchor_multiline_md_center.png differ diff --git a/Tests/images/test_anchor_multiline_mm_center.png b/Tests/images/test_anchor_multiline_mm_center.png new file mode 100644 index 000000000..773cf2a4a Binary files /dev/null and b/Tests/images/test_anchor_multiline_mm_center.png differ diff --git a/Tests/images/test_anchor_multiline_mm_left.png b/Tests/images/test_anchor_multiline_mm_left.png new file mode 100644 index 000000000..87d56636a Binary files /dev/null and b/Tests/images/test_anchor_multiline_mm_left.png differ diff --git a/Tests/images/test_anchor_multiline_mm_right.png b/Tests/images/test_anchor_multiline_mm_right.png new file mode 100644 index 000000000..cf002b12c Binary files /dev/null and b/Tests/images/test_anchor_multiline_mm_right.png differ diff --git a/Tests/images/test_anchor_multiline_rm_center.png b/Tests/images/test_anchor_multiline_rm_center.png new file mode 100644 index 000000000..98073144b Binary files /dev/null and b/Tests/images/test_anchor_multiline_rm_center.png differ diff --git a/Tests/images/test_anchor_multiline_rm_left.png b/Tests/images/test_anchor_multiline_rm_left.png new file mode 100644 index 000000000..838fd7858 Binary files /dev/null and b/Tests/images/test_anchor_multiline_rm_left.png differ diff --git a/Tests/images/test_anchor_multiline_rm_right.png b/Tests/images/test_anchor_multiline_rm_right.png new file mode 100644 index 000000000..290f58417 Binary files /dev/null and b/Tests/images/test_anchor_multiline_rm_right.png differ diff --git a/Tests/images/test_anchor_quick_ls.png b/Tests/images/test_anchor_quick_ls.png new file mode 100644 index 000000000..524c417c3 Binary files /dev/null and b/Tests/images/test_anchor_quick_ls.png differ diff --git a/Tests/images/test_anchor_quick_ma.png b/Tests/images/test_anchor_quick_ma.png new file mode 100644 index 000000000..cfff27f7d Binary files /dev/null and b/Tests/images/test_anchor_quick_ma.png differ diff --git a/Tests/images/test_anchor_quick_mb.png b/Tests/images/test_anchor_quick_mb.png new file mode 100644 index 000000000..ff11f478e Binary files /dev/null and b/Tests/images/test_anchor_quick_mb.png differ diff --git a/Tests/images/test_anchor_quick_md.png b/Tests/images/test_anchor_quick_md.png new file mode 100644 index 000000000..5cbccb170 Binary files /dev/null and b/Tests/images/test_anchor_quick_md.png differ diff --git a/Tests/images/test_anchor_quick_mm.png b/Tests/images/test_anchor_quick_mm.png new file mode 100644 index 000000000..500294c3b Binary files /dev/null and b/Tests/images/test_anchor_quick_mm.png differ diff --git a/Tests/images/test_anchor_quick_ms.png b/Tests/images/test_anchor_quick_ms.png new file mode 100644 index 000000000..b1012463e Binary files /dev/null and b/Tests/images/test_anchor_quick_ms.png differ diff --git a/Tests/images/test_anchor_quick_mt.png b/Tests/images/test_anchor_quick_mt.png new file mode 100644 index 000000000..19423e51a Binary files /dev/null and b/Tests/images/test_anchor_quick_mt.png differ diff --git a/Tests/images/test_anchor_quick_rs.png b/Tests/images/test_anchor_quick_rs.png new file mode 100644 index 000000000..20a5e6c6e Binary files /dev/null and b/Tests/images/test_anchor_quick_rs.png differ diff --git a/Tests/images/test_anchor_ttb_f_lt.png b/Tests/images/test_anchor_ttb_f_lt.png new file mode 100644 index 000000000..5f70a65c4 Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_lt.png differ diff --git a/Tests/images/test_anchor_ttb_f_mm.png b/Tests/images/test_anchor_ttb_f_mm.png new file mode 100644 index 000000000..e7be557d2 Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_mm.png differ diff --git a/Tests/images/test_anchor_ttb_f_rb.png b/Tests/images/test_anchor_ttb_f_rb.png new file mode 100644 index 000000000..b78e2f954 Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_rb.png differ diff --git a/Tests/images/test_anchor_ttb_f_sm.png b/Tests/images/test_anchor_ttb_f_sm.png new file mode 100644 index 000000000..f6dc7c70f Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_sm.png differ diff --git a/Tests/images/test_arabictext_features.png b/Tests/images/test_arabictext_features.png index 9bfa5a931..a03845ace 100644 Binary files a/Tests/images/test_arabictext_features.png and b/Tests/images/test_arabictext_features.png differ diff --git a/Tests/images/test_combine_caron.png b/Tests/images/test_combine_caron.png new file mode 100644 index 000000000..1097f4be5 Binary files /dev/null and b/Tests/images/test_combine_caron.png differ diff --git a/Tests/images/test_combine_caron_below.png b/Tests/images/test_combine_caron_below.png new file mode 100644 index 000000000..6e7d88a92 Binary files /dev/null and b/Tests/images/test_combine_caron_below.png differ diff --git a/Tests/images/test_combine_caron_below_lb.png b/Tests/images/test_combine_caron_below_lb.png new file mode 100644 index 000000000..f59e722b2 Binary files /dev/null and b/Tests/images/test_combine_caron_below_lb.png differ diff --git a/Tests/images/test_combine_caron_below_ld.png b/Tests/images/test_combine_caron_below_ld.png new file mode 100644 index 000000000..540ab7d42 Binary files /dev/null and b/Tests/images/test_combine_caron_below_ld.png differ diff --git a/Tests/images/test_combine_caron_below_ls.png b/Tests/images/test_combine_caron_below_ls.png new file mode 100644 index 000000000..1109b4ee6 Binary files /dev/null and b/Tests/images/test_combine_caron_below_ls.png differ diff --git a/Tests/images/test_combine_caron_below_ttb.png b/Tests/images/test_combine_caron_below_ttb.png new file mode 100644 index 000000000..5c7576de0 Binary files /dev/null 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 new file mode 100644 index 000000000..bacd6a141 Binary files /dev/null and b/Tests/images/test_combine_caron_below_ttb_lb.png differ diff --git a/Tests/images/test_combine_caron_la.png b/Tests/images/test_combine_caron_la.png new file mode 100644 index 000000000..1097f4be5 Binary files /dev/null and b/Tests/images/test_combine_caron_la.png differ diff --git a/Tests/images/test_combine_caron_ls.png b/Tests/images/test_combine_caron_ls.png new file mode 100644 index 000000000..1a721873c Binary files /dev/null and b/Tests/images/test_combine_caron_ls.png differ diff --git a/Tests/images/test_combine_caron_lt.png b/Tests/images/test_combine_caron_lt.png new file mode 100644 index 000000000..91e50d45f Binary files /dev/null and b/Tests/images/test_combine_caron_lt.png differ diff --git a/Tests/images/test_combine_caron_ttb.png b/Tests/images/test_combine_caron_ttb.png new file mode 100644 index 000000000..a94be2f0a Binary files /dev/null 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 new file mode 100644 index 000000000..a94be2f0a Binary files /dev/null and b/Tests/images/test_combine_caron_ttb_lt.png differ diff --git a/Tests/images/test_combine_double_breve_below.png b/Tests/images/test_combine_double_breve_below.png new file mode 100644 index 000000000..30252107f Binary files /dev/null and b/Tests/images/test_combine_double_breve_below.png differ diff --git a/Tests/images/test_combine_double_breve_below_ma.png b/Tests/images/test_combine_double_breve_below_ma.png new file mode 100644 index 000000000..aea09538f Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ma.png differ diff --git a/Tests/images/test_combine_double_breve_below_ra.png b/Tests/images/test_combine_double_breve_below_ra.png new file mode 100644 index 000000000..febd3ab67 Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ra.png differ diff --git a/Tests/images/test_combine_double_breve_below_ttb.png b/Tests/images/test_combine_double_breve_below_ttb.png new file mode 100644 index 000000000..8e42c0d16 Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb.png differ diff --git a/Tests/images/test_combine_double_breve_below_ttb_mt.png b/Tests/images/test_combine_double_breve_below_ttb_mt.png new file mode 100644 index 000000000..9a755e8f8 Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb_mt.png differ diff --git a/Tests/images/test_combine_double_breve_below_ttb_rt.png b/Tests/images/test_combine_double_breve_below_ttb_rt.png new file mode 100644 index 000000000..b954a5415 Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb_rt.png differ diff --git a/Tests/images/test_combine_double_breve_below_ttb_st.png b/Tests/images/test_combine_double_breve_below_ttb_st.png new file mode 100644 index 000000000..b6b08145e Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb_st.png differ diff --git a/Tests/images/test_combine_multiline_lm_center.png b/Tests/images/test_combine_multiline_lm_center.png new file mode 100644 index 000000000..7b1e9c4e4 Binary files /dev/null and b/Tests/images/test_combine_multiline_lm_center.png differ diff --git a/Tests/images/test_combine_multiline_lm_left.png b/Tests/images/test_combine_multiline_lm_left.png new file mode 100644 index 000000000..a26996c2d Binary files /dev/null and b/Tests/images/test_combine_multiline_lm_left.png differ diff --git a/Tests/images/test_combine_multiline_lm_right.png b/Tests/images/test_combine_multiline_lm_right.png new file mode 100644 index 000000000..7caf5cb74 Binary files /dev/null and b/Tests/images/test_combine_multiline_lm_right.png differ diff --git a/Tests/images/test_combine_multiline_mm_center.png b/Tests/images/test_combine_multiline_mm_center.png new file mode 100644 index 000000000..a859e9570 Binary files /dev/null and b/Tests/images/test_combine_multiline_mm_center.png differ diff --git a/Tests/images/test_combine_multiline_mm_left.png b/Tests/images/test_combine_multiline_mm_left.png new file mode 100644 index 000000000..aadb5191f Binary files /dev/null and b/Tests/images/test_combine_multiline_mm_left.png differ diff --git a/Tests/images/test_combine_multiline_mm_right.png b/Tests/images/test_combine_multiline_mm_right.png new file mode 100644 index 000000000..8238d4ec8 Binary files /dev/null and b/Tests/images/test_combine_multiline_mm_right.png differ diff --git a/Tests/images/test_combine_multiline_rm_center.png b/Tests/images/test_combine_multiline_rm_center.png new file mode 100644 index 000000000..7568dd63a Binary files /dev/null and b/Tests/images/test_combine_multiline_rm_center.png differ diff --git a/Tests/images/test_combine_multiline_rm_left.png b/Tests/images/test_combine_multiline_rm_left.png new file mode 100644 index 000000000..b8c3b5b14 Binary files /dev/null and b/Tests/images/test_combine_multiline_rm_left.png differ diff --git a/Tests/images/test_combine_multiline_rm_right.png b/Tests/images/test_combine_multiline_rm_right.png new file mode 100644 index 000000000..14c478a72 Binary files /dev/null and b/Tests/images/test_combine_multiline_rm_right.png differ diff --git a/Tests/images/test_combine_overline.png b/Tests/images/test_combine_overline.png new file mode 100644 index 000000000..dc5e86361 Binary files /dev/null and b/Tests/images/test_combine_overline.png differ diff --git a/Tests/images/test_combine_overline_la.png b/Tests/images/test_combine_overline_la.png new file mode 100644 index 000000000..dc5e86361 Binary files /dev/null and b/Tests/images/test_combine_overline_la.png differ diff --git a/Tests/images/test_combine_overline_ra.png b/Tests/images/test_combine_overline_ra.png new file mode 100644 index 000000000..cbb2d472d Binary files /dev/null and b/Tests/images/test_combine_overline_ra.png differ diff --git a/Tests/images/test_combine_overline_ttb.png b/Tests/images/test_combine_overline_ttb.png new file mode 100644 index 000000000..f74538d9c Binary files /dev/null and b/Tests/images/test_combine_overline_ttb.png differ diff --git a/Tests/images/test_combine_overline_ttb_mt.png b/Tests/images/test_combine_overline_ttb_mt.png new file mode 100644 index 000000000..e915543d6 Binary files /dev/null and b/Tests/images/test_combine_overline_ttb_mt.png differ diff --git a/Tests/images/test_combine_overline_ttb_rt.png b/Tests/images/test_combine_overline_ttb_rt.png new file mode 100644 index 000000000..186d6ee84 Binary files /dev/null and b/Tests/images/test_combine_overline_ttb_rt.png differ diff --git a/Tests/images/test_combine_overline_ttb_st.png b/Tests/images/test_combine_overline_ttb_st.png new file mode 100644 index 000000000..e915543d6 Binary files /dev/null and b/Tests/images/test_combine_overline_ttb_st.png differ diff --git a/Tests/images/test_complex_unicode_text.png b/Tests/images/test_complex_unicode_text.png index f1a6f7ec6..61174d75f 100644 Binary files a/Tests/images/test_complex_unicode_text.png and b/Tests/images/test_complex_unicode_text.png differ diff --git a/Tests/images/test_complex_unicode_text2.png b/Tests/images/test_complex_unicode_text2.png index 543b174c0..0526233c0 100644 Binary files a/Tests/images/test_complex_unicode_text2.png and b/Tests/images/test_complex_unicode_text2.png differ diff --git a/Tests/images/test_direction_ltr.png b/Tests/images/test_direction_ltr.png index 42239334d..b30fcd5d8 100644 Binary files a/Tests/images/test_direction_ltr.png and b/Tests/images/test_direction_ltr.png differ diff --git a/Tests/images/test_direction_rtl.png b/Tests/images/test_direction_rtl.png index 966b67d6b..282eed883 100644 Binary files a/Tests/images/test_direction_rtl.png and b/Tests/images/test_direction_rtl.png differ diff --git a/Tests/images/test_direction_ttb.png b/Tests/images/test_direction_ttb.png index 825f3213e..52dbf5723 100644 Binary files a/Tests/images/test_direction_ttb.png and b/Tests/images/test_direction_ttb.png differ diff --git a/Tests/images/test_direction_ttb_stroke.png b/Tests/images/test_direction_ttb_stroke.png index 3fa844e9a..4b689c38e 100644 Binary files a/Tests/images/test_direction_ttb_stroke.png and b/Tests/images/test_direction_ttb_stroke.png differ diff --git a/Tests/images/test_draw_pbm_ter_en_target.png b/Tests/images/test_draw_pbm_ter_en_target.png new file mode 100644 index 000000000..f1fa25b55 Binary files /dev/null and b/Tests/images/test_draw_pbm_ter_en_target.png differ diff --git a/Tests/images/test_draw_pbm_ter_pl_target.png b/Tests/images/test_draw_pbm_ter_pl_target.png new file mode 100644 index 000000000..503337d2b Binary files /dev/null and b/Tests/images/test_draw_pbm_ter_pl_target.png differ diff --git a/Tests/images/test_kerning_features.png b/Tests/images/test_kerning_features.png index ca895735c..78bcd951b 100644 Binary files a/Tests/images/test_kerning_features.png and b/Tests/images/test_kerning_features.png differ diff --git a/Tests/images/test_language.png b/Tests/images/test_language.png index 8daf007b0..c77215318 100644 Binary files a/Tests/images/test_language.png and b/Tests/images/test_language.png differ diff --git a/Tests/images/test_ligature_features.png b/Tests/images/test_ligature_features.png index 664e9929d..89ea648bf 100644 Binary files a/Tests/images/test_ligature_features.png and b/Tests/images/test_ligature_features.png differ diff --git a/Tests/images/test_text.png b/Tests/images/test_text.png index c156399cd..5888e52e5 100644 Binary files a/Tests/images/test_text.png and b/Tests/images/test_text.png differ diff --git a/Tests/images/test_x_max_and_y_offset.png b/Tests/images/test_x_max_and_y_offset.png index f8bec3e95..21401813f 100644 Binary files a/Tests/images/test_x_max_and_y_offset.png and b/Tests/images/test_x_max_and_y_offset.png differ diff --git a/Tests/images/test_y_offset.png b/Tests/images/test_y_offset.png index 2d57890cb..bda2490df 100644 Binary files a/Tests/images/test_y_offset.png and b/Tests/images/test_y_offset.png differ diff --git a/Tests/images/text_mono.gif b/Tests/images/text_mono.gif new file mode 100644 index 000000000..b350c10e6 Binary files /dev/null and b/Tests/images/text_mono.gif differ diff --git a/Tests/images/tiff_overflow_rows_per_strip.tif b/Tests/images/tiff_overflow_rows_per_strip.tif new file mode 100644 index 000000000..979c7f176 Binary files /dev/null and b/Tests/images/tiff_overflow_rows_per_strip.tif differ diff --git a/Tests/images/transparent_background_text.png b/Tests/images/transparent_background_text.png new file mode 100644 index 000000000..40acd92b6 Binary files /dev/null and b/Tests/images/transparent_background_text.png differ diff --git a/Tests/images/variation_adobe.png b/Tests/images/variation_adobe.png index 71b879bc5..e9cfafb48 100644 Binary files a/Tests/images/variation_adobe.png and b/Tests/images/variation_adobe.png differ diff --git a/Tests/images/variation_adobe_axes.png b/Tests/images/variation_adobe_axes.png index 9376c1d7b..ad3a3a960 100644 Binary files a/Tests/images/variation_adobe_axes.png and b/Tests/images/variation_adobe_axes.png differ diff --git a/Tests/images/variation_adobe_name.png b/Tests/images/variation_adobe_name.png index 9e5fe70e5..11ceaf6e6 100644 Binary files a/Tests/images/variation_adobe_name.png and b/Tests/images/variation_adobe_name.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz.png b/Tests/images/variation_adobe_older_harfbuzz.png new file mode 100644 index 000000000..5abc907ca Binary files /dev/null and b/Tests/images/variation_adobe_older_harfbuzz.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz_axes.png b/Tests/images/variation_adobe_older_harfbuzz_axes.png new file mode 100644 index 000000000..b39d460f9 Binary files /dev/null and b/Tests/images/variation_adobe_older_harfbuzz_axes.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz_name.png b/Tests/images/variation_adobe_older_harfbuzz_name.png new file mode 100644 index 000000000..2adb517a7 Binary files /dev/null and b/Tests/images/variation_adobe_older_harfbuzz_name.png differ diff --git a/Tests/images/variation_tiny_axes.png b/Tests/images/variation_tiny_axes.png index d06ac7a60..8cb6d1f62 100644 Binary files a/Tests/images/variation_tiny_axes.png and b/Tests/images/variation_tiny_axes.png differ diff --git a/Tests/images/variation_tiny_name.png b/Tests/images/variation_tiny_name.png index a0c6ffe3f..69f1550db 100644 Binary files a/Tests/images/variation_tiny_name.png and b/Tests/images/variation_tiny_name.png differ diff --git a/Tests/images/xmp_tags_orientation.png b/Tests/images/xmp_tags_orientation.png new file mode 100644 index 000000000..c1be1665f Binary files /dev/null and b/Tests/images/xmp_tags_orientation.png differ diff --git a/Tests/import_all.py b/Tests/import_all.py deleted file mode 100644 index 33b07f9a2..000000000 --- a/Tests/import_all.py +++ /dev/null @@ -1,14 +0,0 @@ -import glob -import os -import sys -import traceback - -sys.path.insert(0, ".") - -for file in glob.glob("src/PIL/*.py"): - module = os.path.basename(file)[:-3] - try: - exec("from PIL import " + module) - except (ImportError, SyntaxError): - print("===", "failed to import", module) - traceback.print_exc() diff --git a/Tests/make_hash.py b/Tests/make_hash.py deleted file mode 100644 index 7199f8c7f..000000000 --- a/Tests/make_hash.py +++ /dev/null @@ -1,66 +0,0 @@ -# brute-force search for access descriptor hash table - -modes = [ - "1", - "L", - "LA", - "La", - "I", - "I;16", - "I;16L", - "I;16B", - "I;32L", - "I;32B", - "F", - "P", - "PA", - "RGB", - "RGBA", - "RGBa", - "RGBX", - "CMYK", - "YCbCr", - "LAB", - "HSV", -] - - -def hash(s, i): - # djb2 hash: multiply by 33 and xor character - for c in s: - i = (((i << 5) + i) ^ ord(c)) & 0xFFFFFFFF - return i - - -def check(size, i0): - h = [None] * size - for m in modes: - i = hash(m, i0) - i = i % size - if h[i]: - return 0 - h[i] = m - return h - - -min_start = 0 - -# 1) find the smallest table size with no collisions -for min_size in range(len(modes), 16384): - if check(min_size, 0): - print(len(modes), "modes fit in", min_size, "slots") - break - -# 2) see if we can do better with a different initial value -for i0 in range(65556): - for size in range(1, min_size): - if check(size, i0): - if size < min_size: - print(len(modes), "modes fit in", size, "slots with start", i0) - min_size = size - min_start = i0 - -print() - -print("#define ACCESS_TABLE_SIZE", min_size) -print("#define ACCESS_TABLE_HASH", min_start) diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index a6143f084..59fbac527 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -1,23 +1,19 @@ import PIL import PIL.Image -from .helper import PillowTestCase +def test_sanity(): + # Make sure we have the binary extension + PIL.Image.core.new("L", (100, 100)) -class TestSanity(PillowTestCase): - def test_sanity(self): + # Create an image and do stuff with it. + im = PIL.Image.new("1", (100, 100)) + assert (im.mode, im.size) == ("1", (100, 100)) + assert len(im.tobytes()) == 1300 - # Make sure we have the binary extension - PIL.Image.core.new("L", (100, 100)) - - # Create an image and do stuff with it. - im = PIL.Image.new("1", (100, 100)) - self.assertEqual((im.mode, im.size), ("1", (100, 100))) - self.assertEqual(len(im.tobytes()), 1300) - - # Create images in all remaining major modes. - PIL.Image.new("L", (100, 100)) - PIL.Image.new("P", (100, 100)) - PIL.Image.new("RGB", (100, 100)) - PIL.Image.new("I", (100, 100)) - PIL.Image.new("F", (100, 100)) + # Create images in all remaining major modes. + PIL.Image.new("L", (100, 100)) + PIL.Image.new("P", (100, 100)) + PIL.Image.new("RGB", (100, 100)) + PIL.Image.new("I", (100, 100)) + PIL.Image.new("F", (100, 100)) diff --git a/Tests/test_binary.py b/Tests/test_binary.py index 79d5d2bcb..4882e65e6 100644 --- a/Tests/test_binary.py +++ b/Tests/test_binary.py @@ -1,23 +1,22 @@ from PIL import _binary -from .helper import PillowTestCase + +def test_standard(): + assert _binary.i8(b"*") == 42 + assert _binary.o8(42) == b"*" -class TestBinary(PillowTestCase): - def test_standard(self): - self.assertEqual(_binary.i8(b"*"), 42) - self.assertEqual(_binary.o8(42), b"*") +def test_little_endian(): + assert _binary.i16le(b"\xff\xff\x00\x00") == 65535 + assert _binary.i32le(b"\xff\xff\x00\x00") == 65535 - def test_little_endian(self): - self.assertEqual(_binary.i16le(b"\xff\xff\x00\x00"), 65535) - self.assertEqual(_binary.i32le(b"\xff\xff\x00\x00"), 65535) + assert _binary.o16le(65535) == b"\xff\xff" + assert _binary.o32le(65535) == b"\xff\xff\x00\x00" - self.assertEqual(_binary.o16le(65535), b"\xff\xff") - self.assertEqual(_binary.o32le(65535), b"\xff\xff\x00\x00") - def test_big_endian(self): - self.assertEqual(_binary.i16be(b"\x00\x00\xff\xff"), 0) - self.assertEqual(_binary.i32be(b"\x00\x00\xff\xff"), 65535) +def test_big_endian(): + assert _binary.i16be(b"\x00\x00\xff\xff") == 0 + assert _binary.i32be(b"\x00\x00\xff\xff") == 65535 - self.assertEqual(_binary.o16be(65535), b"\xff\xff") - self.assertEqual(_binary.o32be(65535), b"\x00\x00\xff\xff") + assert _binary.o16be(65535) == b"\xff\xff" + assert _binary.o32be(65535) == b"\x00\x00\xff\xff" diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 3a33d2c8a..19602c1e7 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,110 +1,111 @@ import os +import pytest + from PIL import Image -from .helper import PillowTestCase +from .helper import assert_image_similar base = os.path.join("Tests", "images", "bmp") -class TestBmpReference(PillowTestCase): - def get_files(self, d, ext=".bmp"): - return [ - os.path.join(base, d, f) - for f in os.listdir(os.path.join(base, d)) - if ext in f - ] +def get_files(d, ext=".bmp"): + return [ + os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f + ] - def test_bad(self): - """ These shouldn't crash/dos, but they shouldn't return anything - either """ - for f in self.get_files("b"): - def open(f): - try: - with Image.open(f) as im: - im.load() - except Exception: # as msg: - pass +def test_bad(): + """These shouldn't crash/dos, but they shouldn't return anything + either""" + for f in get_files("b"): - # Assert that there is no unclosed file warning - self.assert_warning(None, open, f) - - def test_questionable(self): - """ These shouldn't crash/dos, but it's not well defined that these - are in spec """ - supported = [ - "pal8os2v2.bmp", - "rgb24prof.bmp", - "pal1p1.bmp", - "pal8offs.bmp", - "rgb24lprof.bmp", - "rgb32fakealpha.bmp", - "rgb24largepal.bmp", - "pal8os2sp.bmp", - "rgb32bf-xbgr.bmp", - ] - for f in self.get_files("q"): + def open(f): try: with Image.open(f) as im: im.load() - if os.path.basename(f) not in supported: - print("Please add %s to the partially supported bmp specs." % f) except Exception: # as msg: - if os.path.basename(f) in supported: - raise + pass - def test_good(self): - """ These should all work. There's a set of target files in the - html directory that we can compare against. """ + # Assert that there is no unclosed file warning + pytest.warns(None, open, f) - # Target files, if they're not just replacing the extension - file_map = { - "pal1wb.bmp": "pal1.png", - "pal4rle.bmp": "pal4.png", - "pal8-0.bmp": "pal8.png", - "pal8rle.bmp": "pal8.png", - "pal8topdown.bmp": "pal8.png", - "pal8nonsquare.bmp": "pal8nonsquare-v.png", - "pal8os2.bmp": "pal8.png", - "pal8os2sp.bmp": "pal8.png", - "pal8os2v2.bmp": "pal8.png", - "pal8os2v2-16.bmp": "pal8.png", - "pal8v4.bmp": "pal8.png", - "pal8v5.bmp": "pal8.png", - "rgb16-565pal.bmp": "rgb16-565.png", - "rgb24pal.bmp": "rgb24.png", - "rgb32.bmp": "rgb24.png", - "rgb32bf.bmp": "rgb24.png", - } - def get_compare(f): - name = os.path.split(f)[1] - if name in file_map: - return os.path.join(base, "html", file_map[name]) - name = os.path.splitext(name)[0] - return os.path.join(base, "html", "%s.png" % name) +def test_questionable(): + """These shouldn't crash/dos, but it's not well defined that these + are in spec""" + supported = [ + "pal8os2v2.bmp", + "rgb24prof.bmp", + "pal1p1.bmp", + "pal8offs.bmp", + "rgb24lprof.bmp", + "rgb32fakealpha.bmp", + "rgb24largepal.bmp", + "pal8os2sp.bmp", + "rgb32bf-xbgr.bmp", + ] + for f in get_files("q"): + try: + with Image.open(f) as im: + im.load() + if os.path.basename(f) not in supported: + print(f"Please add {f} to the partially supported bmp specs.") + except Exception: # as msg: + if os.path.basename(f) in supported: + raise - for f in self.get_files("g"): - try: - with Image.open(f) as im: - im.load() - with Image.open(get_compare(f)) as compare: - compare.load() - if im.mode == "P": - # assert image similar doesn't really work - # with paletized image, since the palette might - # be differently ordered for an equivalent image. - im = im.convert("RGBA") - compare = im.convert("RGBA") - self.assert_image_similar(im, compare, 5) - except Exception as msg: - # there are three here that are unsupported: - unsupported = ( - os.path.join(base, "g", "rgb32bf.bmp"), - os.path.join(base, "g", "pal8rle.bmp"), - os.path.join(base, "g", "pal4rle.bmp"), - ) - if f not in unsupported: - self.fail("Unsupported Image {}: {}".format(f, msg)) +def test_good(): + """These should all work. There's a set of target files in the + html directory that we can compare against.""" + + # Target files, if they're not just replacing the extension + file_map = { + "pal1wb.bmp": "pal1.png", + "pal4rle.bmp": "pal4.png", + "pal8-0.bmp": "pal8.png", + "pal8rle.bmp": "pal8.png", + "pal8topdown.bmp": "pal8.png", + "pal8nonsquare.bmp": "pal8nonsquare-v.png", + "pal8os2.bmp": "pal8.png", + "pal8os2sp.bmp": "pal8.png", + "pal8os2v2.bmp": "pal8.png", + "pal8os2v2-16.bmp": "pal8.png", + "pal8v4.bmp": "pal8.png", + "pal8v5.bmp": "pal8.png", + "rgb16-565pal.bmp": "rgb16-565.png", + "rgb24pal.bmp": "rgb24.png", + "rgb32.bmp": "rgb24.png", + "rgb32bf.bmp": "rgb24.png", + } + + def get_compare(f): + name = os.path.split(f)[1] + if name in file_map: + return os.path.join(base, "html", file_map[name]) + name = os.path.splitext(name)[0] + return os.path.join(base, "html", f"{name}.png") + + for f in get_files("g"): + try: + with Image.open(f) as im: + im.load() + with Image.open(get_compare(f)) as compare: + compare.load() + if im.mode == "P": + # assert image similar doesn't really work + # with paletized image, since the palette might + # be differently ordered for an equivalent image. + im = im.convert("RGBA") + compare = im.convert("RGBA") + assert_image_similar(im, compare, 5) + + except Exception as msg: + # there are three here that are unsupported: + unsupported = ( + os.path.join(base, "g", "rgb32bf.bmp"), + os.path.join(base, "g", "pal8rle.bmp"), + os.path.join(base, "g", "pal4rle.bmp"), + ) + assert f in unsupported, f"Unsupported Image {f}: {msg}" diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index c17e7996d..94f504e0b 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -1,6 +1,6 @@ -from PIL import Image, ImageFilter +import pytest -from .helper import PillowTestCase +from PIL import Image, ImageFilter sample = Image.new("L", (7, 5)) # fmt: off @@ -14,234 +14,251 @@ sample.putdata(sum([ # fmt: on -class TestBoxBlurApi(PillowTestCase): - def test_imageops_box_blur(self): - i = sample.filter(ImageFilter.BoxBlur(1)) - self.assertEqual(i.mode, sample.mode) - self.assertEqual(i.size, sample.size) - self.assertIsInstance(i, Image.Image) +def test_imageops_box_blur(): + i = sample.filter(ImageFilter.BoxBlur(1)) + assert i.mode == sample.mode + assert i.size == sample.size + assert isinstance(i, Image.Image) -class TestBoxBlur(PillowTestCase): - def box_blur(self, image, radius=1, n=1): - return image._new(image.im.box_blur(radius, n)) +def box_blur(image, radius=1, n=1): + return image._new(image.im.box_blur(radius, n)) - def assertImage(self, im, data, delta=0): - it = iter(im.getdata()) - for data_row in data: - im_row = [next(it) for _ in range(im.size[0])] - if any( - abs(data_v - im_v) > delta for data_v, im_v in zip(data_row, im_row) - ): - self.assertEqual(im_row, data_row) - self.assertRaises(StopIteration, next, it) - def assertBlur(self, im, radius, data, passes=1, delta=0): - # check grayscale image - self.assertImage(self.box_blur(im, radius, passes), data, delta) - rgba = Image.merge("RGBA", (im, im, im, im)) - for band in self.box_blur(rgba, radius, passes).split(): - self.assertImage(band, data, delta) +def assertImage(im, data, delta=0): + it = iter(im.getdata()) + for data_row in data: + im_row = [next(it) for _ in range(im.size[0])] + if any(abs(data_v - im_v) > delta for data_v, im_v in zip(data_row, im_row)): + assert im_row == data_row + with pytest.raises(StopIteration): + next(it) - def test_color_modes(self): - self.assertRaises(ValueError, self.box_blur, sample.convert("1")) - self.assertRaises(ValueError, self.box_blur, sample.convert("P")) - self.box_blur(sample.convert("L")) - self.box_blur(sample.convert("LA")) - self.box_blur(sample.convert("LA").convert("La")) - self.assertRaises(ValueError, self.box_blur, sample.convert("I")) - self.assertRaises(ValueError, self.box_blur, sample.convert("F")) - self.box_blur(sample.convert("RGB")) - self.box_blur(sample.convert("RGBA")) - self.box_blur(sample.convert("RGBA").convert("RGBa")) - self.box_blur(sample.convert("CMYK")) - self.assertRaises(ValueError, self.box_blur, sample.convert("YCbCr")) - def test_radius_0(self): - self.assertBlur( - sample, - 0, - [ - # fmt: off - [210, 50, 20, 10, 220, 230, 80], - [190, 210, 20, 180, 170, 40, 110], - [120, 210, 250, 60, 220, 0, 220], - [220, 40, 230, 80, 130, 250, 40], - [250, 0, 80, 30, 60, 20, 110], - # fmt: on - ], - ) +def assertBlur(im, radius, data, passes=1, delta=0): + # check grayscale image + assertImage(box_blur(im, radius, passes), data, delta) + rgba = Image.merge("RGBA", (im, im, im, im)) + for band in box_blur(rgba, radius, passes).split(): + assertImage(band, data, delta) - def test_radius_0_02(self): - self.assertBlur( - sample, - 0.02, - [ - # fmt: off - [206, 55, 20, 17, 215, 223, 83], - [189, 203, 31, 171, 169, 46, 110], - [125, 206, 241, 69, 210, 13, 210], - [215, 49, 221, 82, 131, 235, 48], - [244, 7, 80, 32, 60, 27, 107], - # fmt: on - ], - delta=2, - ) - def test_radius_0_05(self): - self.assertBlur( - sample, - 0.05, - [ - # fmt: off - [202, 62, 22, 27, 209, 215, 88], - [188, 194, 44, 161, 168, 56, 111], - [131, 201, 229, 81, 198, 31, 198], - [209, 62, 209, 86, 133, 216, 59], - [237, 17, 80, 36, 60, 35, 103], - # fmt: on - ], - delta=2, - ) +def test_color_modes(): + with pytest.raises(ValueError): + box_blur(sample.convert("1")) + with pytest.raises(ValueError): + box_blur(sample.convert("P")) + box_blur(sample.convert("L")) + box_blur(sample.convert("LA")) + box_blur(sample.convert("LA").convert("La")) + with pytest.raises(ValueError): + box_blur(sample.convert("I")) + with pytest.raises(ValueError): + box_blur(sample.convert("F")) + box_blur(sample.convert("RGB")) + box_blur(sample.convert("RGBA")) + box_blur(sample.convert("RGBA").convert("RGBa")) + box_blur(sample.convert("CMYK")) + with pytest.raises(ValueError): + box_blur(sample.convert("YCbCr")) - def test_radius_0_1(self): - self.assertBlur( - sample, - 0.1, - [ - # fmt: off - [196, 72, 24, 40, 200, 203, 93], - [187, 183, 62, 148, 166, 68, 111], - [139, 193, 213, 96, 182, 54, 182], - [201, 78, 193, 91, 133, 191, 73], - [227, 31, 80, 42, 61, 47, 99], - # fmt: on - ], - delta=1, - ) - def test_radius_0_5(self): - self.assertBlur( - sample, - 0.5, - [ - # fmt: off - [176, 101, 46, 83, 163, 165, 111], - [176, 149, 108, 122, 144, 120, 117], - [164, 171, 159, 141, 134, 119, 129], - [170, 136, 133, 114, 116, 124, 109], - [184, 95, 72, 70, 69, 81, 89], - # fmt: on - ], - delta=1, - ) +def test_radius_0(): + assertBlur( + sample, + 0, + [ + # fmt: off + [210, 50, 20, 10, 220, 230, 80], + [190, 210, 20, 180, 170, 40, 110], + [120, 210, 250, 60, 220, 0, 220], + [220, 40, 230, 80, 130, 250, 40], + [250, 0, 80, 30, 60, 20, 110], + # fmt: on + ], + ) - def test_radius_1(self): - self.assertBlur( - sample, - 1, - [ - # fmt: off - [170, 109, 63, 97, 146, 153, 116], - [168, 142, 112, 128, 126, 143, 121], - [169, 166, 142, 149, 126, 131, 114], - [159, 156, 109, 127, 94, 117, 112], - [164, 128, 63, 87, 76, 89, 90], - # fmt: on - ], - delta=1, - ) - def test_radius_1_5(self): - self.assertBlur( - sample, - 1.5, - [ - # fmt: off - [155, 120, 105, 112, 124, 137, 130], - [160, 136, 124, 125, 127, 134, 130], - [166, 147, 130, 125, 120, 121, 119], - [168, 145, 119, 109, 103, 105, 110], - [168, 134, 96, 85, 85, 89, 97], - # fmt: on - ], - delta=1, - ) +def test_radius_0_02(): + assertBlur( + sample, + 0.02, + [ + # fmt: off + [206, 55, 20, 17, 215, 223, 83], + [189, 203, 31, 171, 169, 46, 110], + [125, 206, 241, 69, 210, 13, 210], + [215, 49, 221, 82, 131, 235, 48], + [244, 7, 80, 32, 60, 27, 107], + # fmt: on + ], + delta=2, + ) - def test_radius_bigger_then_half(self): - self.assertBlur( - sample, - 3, - [ - # fmt: off - [144, 145, 142, 128, 114, 115, 117], - [148, 145, 137, 122, 109, 111, 112], - [152, 145, 131, 117, 103, 107, 108], - [156, 144, 126, 111, 97, 102, 103], - [160, 144, 121, 106, 92, 98, 99], - # fmt: on - ], - delta=1, - ) - def test_radius_bigger_then_width(self): - self.assertBlur( - sample, - 10, - [ - [158, 153, 147, 141, 135, 129, 123], - [159, 153, 147, 141, 136, 130, 124], - [159, 154, 148, 142, 136, 130, 124], - [160, 154, 148, 142, 137, 131, 125], - [160, 155, 149, 143, 137, 131, 125], - ], - delta=0, - ) +def test_radius_0_05(): + assertBlur( + sample, + 0.05, + [ + # fmt: off + [202, 62, 22, 27, 209, 215, 88], + [188, 194, 44, 161, 168, 56, 111], + [131, 201, 229, 81, 198, 31, 198], + [209, 62, 209, 86, 133, 216, 59], + [237, 17, 80, 36, 60, 35, 103], + # fmt: on + ], + delta=2, + ) - def test_extreme_large_radius(self): - self.assertBlur( - sample, - 600, - [ - [162, 162, 162, 162, 162, 162, 162], - [162, 162, 162, 162, 162, 162, 162], - [162, 162, 162, 162, 162, 162, 162], - [162, 162, 162, 162, 162, 162, 162], - [162, 162, 162, 162, 162, 162, 162], - ], - delta=1, - ) - def test_two_passes(self): - self.assertBlur( - sample, - 1, - [ - # fmt: off - [153, 123, 102, 109, 132, 135, 129], - [159, 138, 123, 121, 133, 131, 126], - [162, 147, 136, 124, 127, 121, 121], - [159, 140, 125, 108, 111, 106, 108], - [154, 126, 105, 87, 94, 93, 97], - # fmt: on - ], - passes=2, - delta=1, - ) +def test_radius_0_1(): + assertBlur( + sample, + 0.1, + [ + # fmt: off + [196, 72, 24, 40, 200, 203, 93], + [187, 183, 62, 148, 166, 68, 111], + [139, 193, 213, 96, 182, 54, 182], + [201, 78, 193, 91, 133, 191, 73], + [227, 31, 80, 42, 61, 47, 99], + # fmt: on + ], + delta=1, + ) - def test_three_passes(self): - self.assertBlur( - sample, - 1, - [ - # fmt: off - [146, 131, 116, 118, 126, 131, 130], - [151, 138, 125, 123, 126, 128, 127], - [154, 143, 129, 123, 120, 120, 119], - [152, 139, 122, 113, 108, 108, 108], - [148, 132, 112, 102, 97, 99, 100], - # fmt: on - ], - passes=3, - delta=1, - ) + +def test_radius_0_5(): + assertBlur( + sample, + 0.5, + [ + # fmt: off + [176, 101, 46, 83, 163, 165, 111], + [176, 149, 108, 122, 144, 120, 117], + [164, 171, 159, 141, 134, 119, 129], + [170, 136, 133, 114, 116, 124, 109], + [184, 95, 72, 70, 69, 81, 89], + # fmt: on + ], + delta=1, + ) + + +def test_radius_1(): + assertBlur( + sample, + 1, + [ + # fmt: off + [170, 109, 63, 97, 146, 153, 116], + [168, 142, 112, 128, 126, 143, 121], + [169, 166, 142, 149, 126, 131, 114], + [159, 156, 109, 127, 94, 117, 112], + [164, 128, 63, 87, 76, 89, 90], + # fmt: on + ], + delta=1, + ) + + +def test_radius_1_5(): + assertBlur( + sample, + 1.5, + [ + # fmt: off + [155, 120, 105, 112, 124, 137, 130], + [160, 136, 124, 125, 127, 134, 130], + [166, 147, 130, 125, 120, 121, 119], + [168, 145, 119, 109, 103, 105, 110], + [168, 134, 96, 85, 85, 89, 97], + # fmt: on + ], + delta=1, + ) + + +def test_radius_bigger_then_half(): + assertBlur( + sample, + 3, + [ + # fmt: off + [144, 145, 142, 128, 114, 115, 117], + [148, 145, 137, 122, 109, 111, 112], + [152, 145, 131, 117, 103, 107, 108], + [156, 144, 126, 111, 97, 102, 103], + [160, 144, 121, 106, 92, 98, 99], + # fmt: on + ], + delta=1, + ) + + +def test_radius_bigger_then_width(): + assertBlur( + sample, + 10, + [ + [158, 153, 147, 141, 135, 129, 123], + [159, 153, 147, 141, 136, 130, 124], + [159, 154, 148, 142, 136, 130, 124], + [160, 154, 148, 142, 137, 131, 125], + [160, 155, 149, 143, 137, 131, 125], + ], + delta=0, + ) + + +def test_extreme_large_radius(): + assertBlur( + sample, + 600, + [ + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + ], + delta=1, + ) + + +def test_two_passes(): + assertBlur( + sample, + 1, + [ + # fmt: off + [153, 123, 102, 109, 132, 135, 129], + [159, 138, 123, 121, 133, 131, 126], + [162, 147, 136, 124, 127, 121, 121], + [159, 140, 125, 108, 111, 106, 108], + [154, 126, 105, 87, 94, 93, 97], + # fmt: on + ], + passes=2, + delta=1, + ) + + +def test_three_passes(): + assertBlur( + sample, + 1, + [ + # fmt: off + [146, 131, 116, 118, 126, 131, 130], + [151, 138, 125, 123, 126, 128, 127], + [154, 143, 129, 123, 120, 120, 119], + [152, 139, 122, 113, 108, 108, 108], + [148, 132, 112, 102, 97, 99, 100], + # fmt: on + ], + passes=3, + delta=1, + ) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 96461ca3a..99776ce58 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,9 +1,10 @@ -import unittest from array import array +import pytest + from PIL import Image, ImageFilter -from .helper import PillowTestCase +from .helper import assert_image_equal try: import numpy @@ -11,7 +12,7 @@ except ImportError: numpy = None -class TestColorLut3DCoreAPI(PillowTestCase): +class TestColorLut3DCoreAPI: def generate_identity_table(self, channels, size): if isinstance(size, tuple): size1D, size2D, size3D = size @@ -20,11 +21,11 @@ class TestColorLut3DCoreAPI(PillowTestCase): table = [ [ - r / float(size1D - 1) if size1D != 1 else 0, - g / float(size2D - 1) if size2D != 1 else 0, - b / float(size3D - 1) if size3D != 1 else 0, - r / float(size1D - 1) if size1D != 1 else 0, - g / float(size2D - 1) if size2D != 1 else 0, + r / (size1D - 1) if size1D != 1 else 0, + g / (size2D - 1) if size2D != 1 else 0, + b / (size3D - 1) if size3D != 1 else 0, + r / (size1D - 1) if size1D != 1 else 0, + g / (size2D - 1) if size2D != 1 else 0, ][:channels] for b in range(size3D) for g in range(size2D) @@ -41,43 +42,43 @@ class TestColorLut3DCoreAPI(PillowTestCase): def test_wrong_args(self): im = Image.new("RGB", (10, 10), 0) - with self.assertRaisesRegex(ValueError, "filter"): + with pytest.raises(ValueError, match="filter"): im.im.color_lut_3d("RGB", Image.CUBIC, *self.generate_identity_table(3, 3)) - with self.assertRaisesRegex(ValueError, "image mode"): + with pytest.raises(ValueError, match="image mode"): im.im.color_lut_3d( "wrong", Image.LINEAR, *self.generate_identity_table(3, 3) ) - with self.assertRaisesRegex(ValueError, "table_channels"): + with pytest.raises(ValueError, match="table_channels"): im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(5, 3)) - with self.assertRaisesRegex(ValueError, "table_channels"): + with pytest.raises(ValueError, match="table_channels"): im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(1, 3)) - with self.assertRaisesRegex(ValueError, "table_channels"): + with pytest.raises(ValueError, match="table_channels"): im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(2, 3)) - with self.assertRaisesRegex(ValueError, "Table size"): + with pytest.raises(ValueError, match="Table size"): im.im.color_lut_3d( "RGB", Image.LINEAR, *self.generate_identity_table(3, (1, 3, 3)) ) - with self.assertRaisesRegex(ValueError, "Table size"): + with pytest.raises(ValueError, match="Table size"): im.im.color_lut_3d( "RGB", Image.LINEAR, *self.generate_identity_table(3, (66, 3, 3)) ) - with self.assertRaisesRegex(ValueError, r"size1D \* size2D \* size3D"): + with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 7) - with self.assertRaisesRegex(ValueError, r"size1D \* size2D \* size3D"): + with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 9) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, 16) def test_correct_args(self): @@ -104,25 +105,25 @@ class TestColorLut3DCoreAPI(PillowTestCase): ) def test_wrong_mode(self): - with self.assertRaisesRegex(ValueError, "wrong mode"): + with pytest.raises(ValueError, match="wrong mode"): im = Image.new("L", (10, 10), 0) im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3)) - with self.assertRaisesRegex(ValueError, "wrong mode"): + with pytest.raises(ValueError, match="wrong mode"): im = Image.new("RGB", (10, 10), 0) im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3)) - with self.assertRaisesRegex(ValueError, "wrong mode"): + with pytest.raises(ValueError, match="wrong mode"): im = Image.new("L", (10, 10), 0) im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3)) - with self.assertRaisesRegex(ValueError, "wrong mode"): + with pytest.raises(ValueError, match="wrong mode"): im = Image.new("RGB", (10, 10), 0) im.im.color_lut_3d( "RGBA", Image.LINEAR, *self.generate_identity_table(3, 3) ) - with self.assertRaisesRegex(ValueError, "wrong mode"): + with pytest.raises(ValueError, match="wrong mode"): im = Image.new("RGB", (10, 10), 0) im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(4, 3)) @@ -147,7 +148,7 @@ class TestColorLut3DCoreAPI(PillowTestCase): # Fast test with small cubes for size in [2, 3, 5, 7, 11, 16, 17]: - self.assert_image_equal( + assert_image_equal( im, im._new( im.im.color_lut_3d( @@ -157,7 +158,7 @@ class TestColorLut3DCoreAPI(PillowTestCase): ) # Not so fast - self.assert_image_equal( + assert_image_equal( im, im._new( im.im.color_lut_3d( @@ -173,7 +174,7 @@ class TestColorLut3DCoreAPI(PillowTestCase): ) # Red channel copied to alpha - self.assert_image_equal( + assert_image_equal( Image.merge("RGBA", (im.split() * 2)[:4]), im._new( im.im.color_lut_3d( @@ -194,7 +195,7 @@ class TestColorLut3DCoreAPI(PillowTestCase): ], ) - self.assert_image_equal( + assert_image_equal( im, im._new( im.im.color_lut_3d( @@ -211,7 +212,7 @@ class TestColorLut3DCoreAPI(PillowTestCase): # Reverse channels by splitting and using table # fmt: off - self.assert_image_equal( + assert_image_equal( Image.merge('RGB', im.split()[::-1]), im._new(im.im.color_lut_3d('RGB', Image.LINEAR, 3, 2, 2, 2, [ @@ -240,14 +241,14 @@ class TestColorLut3DCoreAPI(PillowTestCase): -1, 2, 2, 2, 2, 2, ])).load() # fmt: on - self.assertEqual(transformed[0, 0], (0, 0, 255)) - self.assertEqual(transformed[50, 50], (0, 0, 255)) - self.assertEqual(transformed[255, 0], (0, 255, 255)) - self.assertEqual(transformed[205, 50], (0, 255, 255)) - self.assertEqual(transformed[0, 255], (255, 0, 0)) - self.assertEqual(transformed[50, 205], (255, 0, 0)) - self.assertEqual(transformed[255, 255], (255, 255, 0)) - self.assertEqual(transformed[205, 205], (255, 255, 0)) + assert transformed[0, 0] == (0, 0, 255) + assert transformed[50, 50] == (0, 0, 255) + assert transformed[255, 0] == (0, 255, 255) + assert transformed[205, 50] == (0, 255, 255) + assert transformed[0, 255] == (255, 0, 0) + assert transformed[50, 205] == (255, 0, 0) + assert transformed[255, 255] == (255, 255, 0) + assert transformed[205, 205] == (255, 255, 0) # fmt: off transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, @@ -260,96 +261,96 @@ class TestColorLut3DCoreAPI(PillowTestCase): -3, 5, 5, 5, 5, 5, ])).load() # fmt: on - self.assertEqual(transformed[0, 0], (0, 0, 255)) - self.assertEqual(transformed[50, 50], (0, 0, 255)) - self.assertEqual(transformed[255, 0], (0, 255, 255)) - self.assertEqual(transformed[205, 50], (0, 255, 255)) - self.assertEqual(transformed[0, 255], (255, 0, 0)) - self.assertEqual(transformed[50, 205], (255, 0, 0)) - self.assertEqual(transformed[255, 255], (255, 255, 0)) - self.assertEqual(transformed[205, 205], (255, 255, 0)) + assert transformed[0, 0] == (0, 0, 255) + assert transformed[50, 50] == (0, 0, 255) + assert transformed[255, 0] == (0, 255, 255) + assert transformed[205, 50] == (0, 255, 255) + assert transformed[0, 255] == (255, 0, 0) + assert transformed[50, 205] == (255, 0, 0) + assert transformed[255, 255] == (255, 255, 0) + assert transformed[205, 205] == (255, 255, 0) -class TestColorLut3DFilter(PillowTestCase): +class TestColorLut3DFilter: def test_wrong_args(self): - with self.assertRaisesRegex(ValueError, "should be either an integer"): + with pytest.raises(ValueError, match="should be either an integer"): ImageFilter.Color3DLUT("small", [1]) - with self.assertRaisesRegex(ValueError, "should be either an integer"): + with pytest.raises(ValueError, match="should be either an integer"): ImageFilter.Color3DLUT((11, 11), [1]) - with self.assertRaisesRegex(ValueError, r"in \[2, 65\] range"): + with pytest.raises(ValueError, match=r"in \[2, 65\] range"): ImageFilter.Color3DLUT((11, 11, 1), [1]) - with self.assertRaisesRegex(ValueError, r"in \[2, 65\] range"): + with pytest.raises(ValueError, match=r"in \[2, 65\] range"): ImageFilter.Color3DLUT((11, 11, 66), [1]) - with self.assertRaisesRegex(ValueError, "table should have .+ items"): + with pytest.raises(ValueError, match="table should have .+ items"): ImageFilter.Color3DLUT((3, 3, 3), [1, 1, 1]) - with self.assertRaisesRegex(ValueError, "table should have .+ items"): + with pytest.raises(ValueError, match="table should have .+ items"): ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 2) - with self.assertRaisesRegex(ValueError, "should have a length of 4"): + with pytest.raises(ValueError, match="should have a length of 4"): ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 27, channels=4) - with self.assertRaisesRegex(ValueError, "should have a length of 3"): + with pytest.raises(ValueError, match="should have a length of 3"): ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8) - with self.assertRaisesRegex(ValueError, "Only 3 or 4 output"): + 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): lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) - self.assertEqual(tuple(lut.size), (2, 2, 2)) - self.assertEqual(lut.name, "Color 3D LUT") + assert tuple(lut.size) == (2, 2, 2) + assert lut.name == "Color 3D LUT" # fmt: off lut = ImageFilter.Color3DLUT((2, 2, 2), [ (0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11), (12, 13, 14), (15, 16, 17), (18, 19, 20), (21, 22, 23)]) # fmt: on - self.assertEqual(tuple(lut.size), (2, 2, 2)) - self.assertEqual(lut.table, list(range(24))) + assert tuple(lut.size) == (2, 2, 2) + assert lut.table == list(range(24)) lut = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8, channels=4) - self.assertEqual(tuple(lut.size), (2, 2, 2)) - self.assertEqual(lut.table, list(range(4)) * 8) + assert tuple(lut.size) == (2, 2, 2) + assert lut.table == list(range(4)) * 8 - @unittest.skipIf(numpy is None, "Numpy is not installed") + @pytest.mark.skipif(numpy is None, reason="NumPy not installed") def test_numpy_sources(self): table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) - with self.assertRaisesRegex(ValueError, "should have either channels"): + with pytest.raises(ValueError, match="should have either channels"): lut = ImageFilter.Color3DLUT((5, 6, 7), table) table = numpy.ones((7, 6, 5, 3), dtype=numpy.float16) lut = ImageFilter.Color3DLUT((5, 6, 7), table) - self.assertIsInstance(lut.table, numpy.ndarray) - self.assertEqual(lut.table.dtype, table.dtype) - self.assertEqual(lut.table.shape, (table.size,)) + assert isinstance(lut.table, numpy.ndarray) + assert lut.table.dtype == table.dtype + assert lut.table.shape == (table.size,) table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) lut = ImageFilter.Color3DLUT((5, 6, 7), table) - self.assertEqual(lut.table.shape, (table.size,)) + assert lut.table.shape == (table.size,) table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) lut = ImageFilter.Color3DLUT((5, 6, 7), table) - self.assertEqual(lut.table.shape, (table.size,)) + assert lut.table.shape == (table.size,) # Check application Image.new("RGB", (10, 10), 0).filter(lut) # Check copy table[0] = 33 - self.assertEqual(lut.table[0], 1) + assert lut.table[0] == 1 # Check not copy table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) lut = ImageFilter.Color3DLUT((5, 6, 7), table, _copy_table=False) table[0] = 33 - self.assertEqual(lut.table[0], 33) + assert lut.table[0] == 33 - @unittest.skipIf(numpy is None, "Numpy is not installed") + @pytest.mark.skipif(numpy is None, reason="NumPy not installed") def test_numpy_formats(self): g = Image.linear_gradient("L") im = Image.merge( @@ -358,25 +359,25 @@ class TestColorLut3DFilter(PillowTestCase): lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float32)[:-1] - with self.assertRaisesRegex(ValueError, "should have table_channels"): + with pytest.raises(ValueError, match="should have table_channels"): im.filter(lut) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float32).reshape((7 * 9 * 11), 3) - with self.assertRaisesRegex(ValueError, "should have table_channels"): + with pytest.raises(ValueError, match="should have table_channels"): im.filter(lut) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float16) - self.assert_image_equal(im, im.filter(lut)) + assert_image_equal(im, im.filter(lut)) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float32) - self.assert_image_equal(im, im.filter(lut)) + assert_image_equal(im, im.filter(lut)) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float64) - self.assert_image_equal(im, im.filter(lut)) + assert_image_equal(im, im.filter(lut)) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.int32) @@ -386,7 +387,7 @@ class TestColorLut3DFilter(PillowTestCase): def test_repr(self): lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) - self.assertEqual(repr(lut), "") + assert repr(lut) == "" lut = ImageFilter.Color3DLUT( (3, 4, 5), @@ -395,47 +396,48 @@ class TestColorLut3DFilter(PillowTestCase): target_mode="YCbCr", _copy_table=False, ) - self.assertEqual( - repr(lut), "" + assert ( + repr(lut) + == "" ) -class TestGenerateColorLut3D(PillowTestCase): +class TestGenerateColorLut3D: def test_wrong_channels_count(self): - with self.assertRaisesRegex(ValueError, "3 or 4 output channels"): + with pytest.raises(ValueError, match="3 or 4 output channels"): ImageFilter.Color3DLUT.generate( 5, channels=2, callback=lambda r, g, b: (r, g, b) ) - with self.assertRaisesRegex(ValueError, "should have either channels"): + with pytest.raises(ValueError, match="should have either channels"): ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b, r)) - with self.assertRaisesRegex(ValueError, "should have either channels"): + with pytest.raises(ValueError, match="should have either channels"): ImageFilter.Color3DLUT.generate( 5, channels=4, callback=lambda r, g, b: (r, g, b) ) def test_3_channels(self): lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) - self.assertEqual(tuple(lut.size), (5, 5, 5)) - self.assertEqual(lut.name, "Color 3D LUT") + assert tuple(lut.size) == (5, 5, 5) + assert lut.name == "Color 3D LUT" # fmt: off - self.assertEqual(lut.table[:24], [ + assert lut.table[:24] == [ 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.5, 0.0, 0.0, 0.75, 0.0, 0.0, - 1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0]) + 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): lut = ImageFilter.Color3DLUT.generate( 5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2) ) - self.assertEqual(tuple(lut.size), (5, 5, 5)) - self.assertEqual(lut.name, "Color 3D LUT") + assert tuple(lut.size) == (5, 5, 5) + assert lut.name == "Color 3D LUT" # fmt: off - self.assertEqual(lut.table[:24], [ + assert lut.table[:24] == [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.125, 0.0, 0.5, 0.0, 0.25, 0.0, 0.75, 0.0, 0.375, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 0.25, 0.125 - ]) + ] # fmt: on def test_apply(self): @@ -445,23 +447,23 @@ class TestGenerateColorLut3D(PillowTestCase): im = Image.merge( "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] ) - self.assertEqual(im, im.filter(lut)) + assert im == im.filter(lut) -class TestTransformColorLut3D(PillowTestCase): +class TestTransformColorLut3D: def test_wrong_args(self): source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) - with self.assertRaisesRegex(ValueError, "Only 3 or 4 output"): + with pytest.raises(ValueError, match="Only 3 or 4 output"): source.transform(lambda r, g, b: (r, g, b), channels=8) - with self.assertRaisesRegex(ValueError, "should have either channels"): + with pytest.raises(ValueError, match="should have either channels"): source.transform(lambda r, g, b: (r, g, b), channels=4) - with self.assertRaisesRegex(ValueError, "should have either channels"): + with pytest.raises(ValueError, match="should have either channels"): source.transform(lambda r, g, b: (r, g, b, 1)) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): source.transform(lambda r, g, b, a: (r, g, b)) def test_target_mode(self): @@ -470,31 +472,29 @@ class TestTransformColorLut3D(PillowTestCase): ) lut = source.transform(lambda r, g, b: (r, g, b)) - self.assertEqual(lut.mode, "HSV") + assert lut.mode == "HSV" lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB") - self.assertEqual(lut.mode, "RGB") + assert lut.mode == "RGB" def test_3_to_3_channels(self): 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)) - self.assertEqual(tuple(lut.size), tuple(source.size)) - self.assertEqual(len(lut.table), len(source.table)) - self.assertNotEqual(lut.table, source.table) - self.assertEqual( - lut.table[0:10], [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] - ) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) == len(source.table) + assert lut.table != source.table + assert lut.table[0:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] def test_3_to_4_channels(self): 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) - self.assertEqual(tuple(lut.size), tuple(source.size)) - self.assertNotEqual(len(lut.table), len(source.table)) - self.assertNotEqual(lut.table, source.table) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) != len(source.table) + assert lut.table != source.table # fmt: off - self.assertEqual(lut.table[0:16], [ + assert lut.table[0:16] == [ 0.0, 0.0, 0.0, 1, 0.2**2, 0.0, 0.0, 1, - 0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1]) + 0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1] # fmt: on def test_4_to_3_channels(self): @@ -504,13 +504,13 @@ class TestTransformColorLut3D(PillowTestCase): lut = source.transform( lambda r, g, b, a: (a - r * r, a - g * g, a - b * b), channels=3 ) - self.assertEqual(tuple(lut.size), tuple(source.size)) - self.assertNotEqual(len(lut.table), len(source.table)) - self.assertNotEqual(lut.table, source.table) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) != len(source.table) + assert lut.table != source.table # fmt: off - self.assertEqual(lut.table[0:18], [ + assert lut.table[0:18] == [ 1.0, 1.0, 1.0, 0.75, 1.0, 1.0, 0.0, 1.0, 1.0, - 1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0]) + 1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0] # fmt: on def test_4_to_4_channels(self): @@ -518,13 +518,13 @@ class TestTransformColorLut3D(PillowTestCase): (6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4 ) lut = source.transform(lambda r, g, b, a: (r * r, g * g, b * b, a - 0.5)) - self.assertEqual(tuple(lut.size), tuple(source.size)) - self.assertEqual(len(lut.table), len(source.table)) - self.assertNotEqual(lut.table, source.table) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) == len(source.table) + assert lut.table != source.table # fmt: off - self.assertEqual(lut.table[0:16], [ + assert lut.table[0:16] == [ 0.0, 0.0, 0.0, 0.5, 0.2**2, 0.0, 0.0, 0.5, - 0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5]) + 0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5] # fmt: on def test_with_normals_3_channels(self): @@ -534,13 +534,13 @@ class TestTransformColorLut3D(PillowTestCase): lut = source.transform( lambda nr, ng, nb, r, g, b: (nr - r, ng - g, nb - b), with_normals=True ) - self.assertEqual(tuple(lut.size), tuple(source.size)) - self.assertEqual(len(lut.table), len(source.table)) - self.assertNotEqual(lut.table, source.table) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) == len(source.table) + assert lut.table != source.table # fmt: off - self.assertEqual(lut.table[0:18], [ + assert lut.table[0:18] == [ 0.0, 0.0, 0.0, 0.16, 0.0, 0.0, 0.24, 0.0, 0.0, - 0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0]) + 0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0] # fmt: on def test_with_normals_4_channels(self): @@ -551,11 +551,11 @@ class TestTransformColorLut3D(PillowTestCase): lambda nr, ng, nb, r, g, b, a: (nr - r, ng - g, nb - b, a - 0.5), with_normals=True, ) - self.assertEqual(tuple(lut.size), tuple(source.size)) - self.assertEqual(len(lut.table), len(source.table)) - self.assertNotEqual(lut.table, source.table) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) == len(source.table) + assert lut.table != source.table # fmt: off - self.assertEqual(lut.table[0:16], [ + assert lut.table[0:16] == [ 0.0, 0.0, 0.0, 0.5, 0.25, 0.0, 0.0, 0.5, - 0.0, 0.0, 0.0, 0.5, 0.0, 0.16, 0.0, 0.5]) + 0.0, 0.0, 0.0, 0.5, 0.0, 0.16, 0.0, 0.5] # fmt: on diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index ac2970de2..6c52d25a4 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,38 +1,39 @@ import sys -import unittest + +import pytest from PIL import Image -from .helper import PillowTestCase, is_pypy +from .helper import is_pypy -class TestCoreStats(PillowTestCase): - def test_get_stats(self): - # Create at least one image - Image.new("RGB", (10, 10)) +def test_get_stats(): + # Create at least one image + Image.new("RGB", (10, 10)) - stats = Image.core.get_stats() - self.assertIn("new_count", stats) - self.assertIn("reused_blocks", stats) - self.assertIn("freed_blocks", stats) - self.assertIn("allocated_blocks", stats) - self.assertIn("reallocated_blocks", stats) - self.assertIn("blocks_cached", stats) - - def test_reset_stats(self): - Image.core.reset_stats() - - stats = Image.core.get_stats() - self.assertEqual(stats["new_count"], 0) - self.assertEqual(stats["reused_blocks"], 0) - self.assertEqual(stats["freed_blocks"], 0) - self.assertEqual(stats["allocated_blocks"], 0) - self.assertEqual(stats["reallocated_blocks"], 0) - self.assertEqual(stats["blocks_cached"], 0) + stats = Image.core.get_stats() + assert "new_count" in stats + assert "reused_blocks" in stats + assert "freed_blocks" in stats + assert "allocated_blocks" in stats + assert "reallocated_blocks" in stats + assert "blocks_cached" in stats -class TestCoreMemory(PillowTestCase): - def tearDown(self): +def test_reset_stats(): + Image.core.reset_stats() + + stats = Image.core.get_stats() + assert stats["new_count"] == 0 + assert stats["reused_blocks"] == 0 + assert stats["freed_blocks"] == 0 + assert stats["allocated_blocks"] == 0 + assert stats["reallocated_blocks"] == 0 + assert stats["blocks_cached"] == 0 + + +class TestCoreMemory: + def teardown_method(self): # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) @@ -42,38 +43,44 @@ class TestCoreMemory(PillowTestCase): def test_get_alignment(self): alignment = Image.core.get_alignment() - self.assertGreater(alignment, 0) + assert alignment > 0 def test_set_alignment(self): for i in [1, 2, 4, 8, 16, 32]: Image.core.set_alignment(i) alignment = Image.core.get_alignment() - self.assertEqual(alignment, i) + assert alignment == i # Try to construct new image Image.new("RGB", (10, 10)) - self.assertRaises(ValueError, Image.core.set_alignment, 0) - self.assertRaises(ValueError, Image.core.set_alignment, -1) - self.assertRaises(ValueError, Image.core.set_alignment, 3) + with pytest.raises(ValueError): + Image.core.set_alignment(0) + with pytest.raises(ValueError): + Image.core.set_alignment(-1) + with pytest.raises(ValueError): + Image.core.set_alignment(3) def test_get_block_size(self): block_size = Image.core.get_block_size() - self.assertGreaterEqual(block_size, 4096) + assert block_size >= 4096 def test_set_block_size(self): for i in [4096, 2 * 4096, 3 * 4096]: Image.core.set_block_size(i) block_size = Image.core.get_block_size() - self.assertEqual(block_size, i) + assert block_size == i # Try to construct new image Image.new("RGB", (10, 10)) - self.assertRaises(ValueError, Image.core.set_block_size, 0) - self.assertRaises(ValueError, Image.core.set_block_size, -1) - self.assertRaises(ValueError, Image.core.set_block_size, 4000) + with pytest.raises(ValueError): + Image.core.set_block_size(0) + with pytest.raises(ValueError): + Image.core.set_block_size(-1) + with pytest.raises(ValueError): + Image.core.set_block_size(4000) def test_set_block_size_stats(self): Image.core.reset_stats() @@ -82,30 +89,32 @@ class TestCoreMemory(PillowTestCase): Image.new("RGB", (256, 256)) stats = Image.core.get_stats() - self.assertGreaterEqual(stats["new_count"], 1) - self.assertGreaterEqual(stats["allocated_blocks"], 64) + assert stats["new_count"] >= 1 + assert stats["allocated_blocks"] >= 64 if not is_pypy(): - self.assertGreaterEqual(stats["freed_blocks"], 64) + assert stats["freed_blocks"] >= 64 def test_get_blocks_max(self): blocks_max = Image.core.get_blocks_max() - self.assertGreaterEqual(blocks_max, 0) + assert blocks_max >= 0 def test_set_blocks_max(self): for i in [0, 1, 10]: Image.core.set_blocks_max(i) blocks_max = Image.core.get_blocks_max() - self.assertEqual(blocks_max, i) + assert blocks_max == i # Try to construct new image Image.new("RGB", (10, 10)) - self.assertRaises(ValueError, Image.core.set_blocks_max, -1) + with pytest.raises(ValueError): + Image.core.set_blocks_max(-1) if sys.maxsize < 2 ** 32: - self.assertRaises(ValueError, Image.core.set_blocks_max, 2 ** 29) + with pytest.raises(ValueError): + Image.core.set_blocks_max(2 ** 29) - @unittest.skipIf(is_pypy(), "images are not collected") + @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_set_blocks_max_stats(self): Image.core.reset_stats() Image.core.set_blocks_max(128) @@ -114,13 +123,13 @@ class TestCoreMemory(PillowTestCase): Image.new("RGB", (256, 256)) stats = Image.core.get_stats() - self.assertGreaterEqual(stats["new_count"], 2) - self.assertGreaterEqual(stats["allocated_blocks"], 64) - self.assertGreaterEqual(stats["reused_blocks"], 64) - self.assertEqual(stats["freed_blocks"], 0) - self.assertEqual(stats["blocks_cached"], 64) + assert stats["new_count"] >= 2 + assert stats["allocated_blocks"] >= 64 + assert stats["reused_blocks"] >= 64 + assert stats["freed_blocks"] == 0 + assert stats["blocks_cached"] == 64 - @unittest.skipIf(is_pypy(), "images are not collected") + @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_clear_cache_stats(self): Image.core.reset_stats() Image.core.clear_cache() @@ -132,11 +141,11 @@ class TestCoreMemory(PillowTestCase): Image.core.clear_cache(16) stats = Image.core.get_stats() - self.assertGreaterEqual(stats["new_count"], 2) - self.assertGreaterEqual(stats["allocated_blocks"], 64) - self.assertGreaterEqual(stats["reused_blocks"], 64) - self.assertGreaterEqual(stats["freed_blocks"], 48) - self.assertEqual(stats["blocks_cached"], 16) + assert stats["new_count"] >= 2 + assert stats["allocated_blocks"] >= 64 + assert stats["reused_blocks"] >= 64 + assert stats["freed_blocks"] >= 48 + assert stats["blocks_cached"] == 16 def test_large_images(self): Image.core.reset_stats() @@ -146,16 +155,16 @@ class TestCoreMemory(PillowTestCase): Image.core.clear_cache() stats = Image.core.get_stats() - self.assertGreaterEqual(stats["new_count"], 1) - self.assertGreaterEqual(stats["allocated_blocks"], 16) - self.assertGreaterEqual(stats["reused_blocks"], 0) - self.assertEqual(stats["blocks_cached"], 0) + assert stats["new_count"] >= 1 + assert stats["allocated_blocks"] >= 16 + assert stats["reused_blocks"] >= 0 + assert stats["blocks_cached"] == 0 if not is_pypy(): - self.assertGreaterEqual(stats["freed_blocks"], 16) + assert stats["freed_blocks"] >= 16 -class TestEnvVars(PillowTestCase): - def tearDown(self): +class TestEnvVars: + def teardown_method(self): # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) @@ -164,17 +173,17 @@ class TestEnvVars(PillowTestCase): def test_units(self): Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"}) - self.assertEqual(Image.core.get_blocks_max(), 2 * 1024) + assert Image.core.get_blocks_max() == 2 * 1024 Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) - self.assertEqual(Image.core.get_block_size(), 2 * 1024 * 1024) + assert Image.core.get_block_size() == 2 * 1024 * 1024 def test_warnings(self): - self.assert_warning( + pytest.warns( UserWarning, Image._apply_env_variables, {"PILLOW_ALIGNMENT": "15"} ) - self.assert_warning( + pytest.warns( UserWarning, Image._apply_env_variables, {"PILLOW_BLOCK_SIZE": "1024"} ) - self.assert_warning( + pytest.warns( UserWarning, Image._apply_env_variables, {"PILLOW_BLOCKS_MAX": "wat"} ) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 5d0d37099..132269933 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -1,14 +1,17 @@ +import pytest + from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import hopper TEST_FILE = "Tests/images/hopper.ppm" ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS -class TestDecompressionBomb(PillowTestCase): - def tearDown(self): +class TestDecompressionBomb: + @classmethod + def teardown_class(self): Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT def test_no_warning_small_file(self): @@ -21,7 +24,7 @@ class TestDecompressionBomb(PillowTestCase): # Arrange # Turn limit off Image.MAX_IMAGE_PIXELS = None - self.assertIsNone(Image.MAX_IMAGE_PIXELS) + assert Image.MAX_IMAGE_PIXELS is None # Act / Assert # Implicit assert: no warning. @@ -32,46 +35,52 @@ class TestDecompressionBomb(PillowTestCase): def test_warning(self): # Set limit to trigger warning on the test file Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 - self.assertEqual(Image.MAX_IMAGE_PIXELS, 128 * 128 - 1) + assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 def open(): with Image.open(TEST_FILE): pass - self.assert_warning(Image.DecompressionBombWarning, open) + pytest.warns(Image.DecompressionBombWarning, open) def test_exception(self): # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 - self.assertEqual(Image.MAX_IMAGE_PIXELS, 64 * 128 - 1) + assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 - with self.assertRaises(Image.DecompressionBombError): + with pytest.raises(Image.DecompressionBombError): with Image.open(TEST_FILE): pass def test_exception_ico(self): - with self.assertRaises(Image.DecompressionBombError): + with pytest.raises(Image.DecompressionBombError): Image.open("Tests/images/decompression_bomb.ico") def test_exception_gif(self): - with self.assertRaises(Image.DecompressionBombError): + with pytest.raises(Image.DecompressionBombError): Image.open("Tests/images/decompression_bomb.gif") + def test_exception_bmp(self): + with pytest.raises(Image.DecompressionBombError): + Image.open("Tests/images/bmp/b/reallybig.bmp") -class TestDecompressionCrop(PillowTestCase): - def setUp(self): - self.src = hopper() - self.addCleanup(self.src.close) - Image.MAX_IMAGE_PIXELS = self.src.height * self.src.width * 4 - 1 - def tearDown(self): +class TestDecompressionCrop: + @classmethod + def setup_class(self): + width, height = 128, 128 + Image.MAX_IMAGE_PIXELS = height * width * 4 - 1 + + @classmethod + def teardown_class(self): Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT def testEnlargeCrop(self): # Crops can extend the extents, therefore we should have the # same decompression bomb warnings on them. - box = (0, 0, self.src.width * 2, self.src.height * 2) - self.assert_warning(Image.DecompressionBombWarning, self.src.crop, box) + with hopper() as src: + box = (0, 0, src.width * 2, src.height * 2) + pytest.warns(Image.DecompressionBombWarning, src.crop, box) def test_crop_decompression_checks(self): @@ -84,11 +93,11 @@ class TestDecompressionCrop(PillowTestCase): error_values = ((-99909, -99990, 99999, 99999), (99909, 99990, -99999, -99999)) for value in good_values: - self.assertEqual(im.crop(value).size, (9, 9)) + assert im.crop(value).size == (9, 9) for value in warning_values: - self.assert_warning(Image.DecompressionBombWarning, im.crop, value) + pytest.warns(Image.DecompressionBombWarning, im.crop, value) for value in error_values: - with self.assertRaises(Image.DecompressionBombError): + with pytest.raises(Image.DecompressionBombError): im.crop(value) diff --git a/Tests/test_features.py b/Tests/test_features.py index eb51407a1..284f72205 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,90 +1,142 @@ import io -import unittest +import re + +import pytest from PIL import features -from .helper import PillowTestCase +from .helper import skip_unless_feature try: from PIL import _webp - - HAVE_WEBP = True except ImportError: - HAVE_WEBP = False + pass -class TestFeatures(PillowTestCase): - def test_check(self): - # Check the correctness of the convenience function - for module in features.modules: - self.assertEqual(features.check_module(module), features.check(module)) - for codec in features.codecs: - self.assertEqual(features.check_codec(codec), features.check(codec)) - for feature in features.features: - self.assertEqual(features.check_feature(feature), features.check(feature)) +def test_check(): + # Check the correctness of the convenience function + for module in features.modules: + assert features.check_module(module) == features.check(module) + for codec in features.codecs: + assert features.check_codec(codec) == features.check(codec) + for feature in features.features: + assert features.check_feature(feature) == features.check(feature) - @unittest.skipUnless(HAVE_WEBP, "WebP not available") - def test_webp_transparency(self): - self.assertEqual( - features.check("transp_webp"), not _webp.WebPDecoderBuggyAlpha() - ) - self.assertEqual(features.check("transp_webp"), _webp.HAVE_TRANSPARENCY) - @unittest.skipUnless(HAVE_WEBP, "WebP not available") - def test_webp_mux(self): - self.assertEqual(features.check("webp_mux"), _webp.HAVE_WEBPMUX) +def test_version(): + # Check the correctness of the convenience function + # and the format of version numbers - @unittest.skipUnless(HAVE_WEBP, "WebP not available") - def test_webp_anim(self): - self.assertEqual(features.check("webp_anim"), _webp.HAVE_WEBPANIM) + def test(name, function): + version = features.version(name) + if not features.check(name): + assert version is None + else: + assert function(name) == version + if name != "PIL": + assert version is None or re.search(r"\d+(\.\d+)*$", version) - def test_check_modules(self): - for feature in features.modules: - self.assertIn(features.check_module(feature), [True, False]) - for feature in features.codecs: - self.assertIn(features.check_codec(feature), [True, False]) + for module in features.modules: + test(module, features.version_module) + for codec in features.codecs: + test(codec, features.version_codec) + for feature in features.features: + test(feature, features.version_feature) - def test_supported_modules(self): - self.assertIsInstance(features.get_supported_modules(), list) - self.assertIsInstance(features.get_supported_codecs(), list) - self.assertIsInstance(features.get_supported_features(), list) - self.assertIsInstance(features.get_supported(), list) - def test_unsupported_codec(self): - # Arrange - codec = "unsupported_codec" - # Act / Assert - self.assertRaises(ValueError, features.check_codec, codec) +@skip_unless_feature("webp") +def test_webp_transparency(): + assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() + assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY - def test_unsupported_module(self): - # Arrange - module = "unsupported_module" - # Act / Assert - self.assertRaises(ValueError, features.check_module, module) - def test_pilinfo(self): - buf = io.StringIO() - features.pilinfo(buf) - out = buf.getvalue() - lines = out.splitlines() - self.assertEqual(lines[0], "-" * 68) - self.assertTrue(lines[1].startswith("Pillow ")) - self.assertTrue(lines[2].startswith("Python ")) - lines = lines[3:] - while lines[0].startswith(" "): - lines = lines[1:] - self.assertEqual(lines[0], "-" * 68) - self.assertTrue(lines[1].startswith("Python modules loaded from ")) - self.assertTrue(lines[2].startswith("Binary modules loaded from ")) - self.assertEqual(lines[3], "-" * 68) - jpeg = ( - "\n" - + "-" * 68 - + "\n" - + "JPEG image/jpeg\n" - + "Extensions: .jfif, .jpe, .jpeg, .jpg\n" - + "Features: open, save\n" - + "-" * 68 - + "\n" - ) - self.assertIn(jpeg, out) +@skip_unless_feature("webp") +def test_webp_mux(): + assert features.check("webp_mux") == _webp.HAVE_WEBPMUX + + +@skip_unless_feature("webp") +def test_webp_anim(): + assert features.check("webp_anim") == _webp.HAVE_WEBPANIM + + +@skip_unless_feature("libjpeg_turbo") +def test_libjpeg_turbo_version(): + assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo")) + + +@skip_unless_feature("libimagequant") +def test_libimagequant_version(): + assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) + + +def test_check_modules(): + for feature in features.modules: + assert features.check_module(feature) in [True, False] + + +def test_check_codecs(): + for feature in features.codecs: + assert features.check_codec(feature) in [True, False] + + +def test_check_warns_on_nonexistent(): + 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(): + 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(): + # Arrange + codec = "unsupported_codec" + # Act / Assert + with pytest.raises(ValueError): + features.check_codec(codec) + with pytest.raises(ValueError): + features.version_codec(codec) + + +def test_unsupported_module(): + # Arrange + module = "unsupported_module" + # Act / Assert + with pytest.raises(ValueError): + features.check_module(module) + with pytest.raises(ValueError): + features.version_module(module) + + +def test_pilinfo(): + buf = io.StringIO() + features.pilinfo(buf) + out = buf.getvalue() + lines = out.splitlines() + assert lines[0] == "-" * 68 + assert lines[1].startswith("Pillow ") + assert lines[2].startswith("Python ") + lines = lines[3:] + while lines[0].startswith(" "): + lines = lines[1:] + assert lines[0] == "-" * 68 + assert lines[1].startswith("Python modules loaded from ") + assert lines[2].startswith("Binary modules loaded from ") + assert lines[3] == "-" * 68 + jpeg = ( + "\n" + + "-" * 68 + + "\n" + + "JPEG image/jpeg\n" + + "Extensions: .jfif, .jpe, .jpeg, .jpg\n" + + "Features: open, save\n" + + "-" * 68 + + "\n" + ) + assert jpeg in out diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py new file mode 100644 index 000000000..2d50748bd --- /dev/null +++ b/Tests/test_file_apng.py @@ -0,0 +1,586 @@ +import pytest + +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(): + with Image.open("Tests/images/apng/single_frame.png") as im: + assert not im.is_animated + assert im.n_frames == 1 + assert im.get_format_mimetype() == "image/apng" + assert im.info.get("default_image") is None + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/single_frame_default.png") as im: + assert im.is_animated + assert im.n_frames == 2 + assert im.get_format_mimetype() == "image/apng" + assert im.info.get("default_image") + assert im.getpixel((0, 0)) == (255, 0, 0, 255) + assert im.getpixel((64, 32)) == (255, 0, 0, 255) + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test out of bounds seek + with pytest.raises(EOFError): + im.seek(2) + + # test rewind support + im.seek(0) + assert im.getpixel((0, 0)) == (255, 0, 0, 255) + assert im.getpixel((64, 32)) == (255, 0, 0, 255) + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_fdat(): + with Image.open("Tests/images/apng/split_fdat.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/split_fdat_zero_chunk.png") as im: + 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(): + 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) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_background.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + with Image.open("Tests/images/apng/dispose_op_background_final.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_previous.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + +def test_apng_dispose_region(): + 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) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + with Image.open("Tests/images/apng/dispose_op_background_region.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 255, 255) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + with Image.open("Tests/images/apng/dispose_op_previous_region.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_dispose_op_background_p_mode(): + 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(): + 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) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 2) + assert im.getpixel((64, 32)) == (0, 255, 0, 2) + + with Image.open("Tests/images/apng/blend_op_over.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 97) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_chunk_order(): + 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(): + with Image.open("Tests/images/apng/delay.png") as im: + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + im.seek(3) + assert im.info.get("duration") == 500.0 + im.seek(4) + assert im.info.get("duration") == 1000.0 + + with Image.open("Tests/images/apng/delay_round.png") as im: + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + + with Image.open("Tests/images/apng/delay_short_max.png") as im: + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + + with Image.open("Tests/images/apng/delay_zero_denom.png") as im: + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + + with Image.open("Tests/images/apng/delay_zero_numer.png") as im: + im.seek(1) + assert im.info.get("duration") == 0.0 + im.seek(2) + assert im.info.get("duration") == 0.0 + im.seek(3) + assert im.info.get("duration") == 500.0 + im.seek(4) + assert im.info.get("duration") == 1000.0 + + +def test_apng_num_plays(): + with Image.open("Tests/images/apng/num_plays.png") as im: + assert im.info.get("loop") == 0 + + with Image.open("Tests/images/apng/num_plays_1.png") as im: + assert im.info.get("loop") == 1 + + +def test_apng_mode(): + with Image.open("Tests/images/apng/mode_16bit.png") as im: + assert im.mode == "RGBA" + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 128, 191) + assert im.getpixel((64, 32)) == (0, 0, 128, 191) + + with Image.open("Tests/images/apng/mode_greyscale.png") as im: + assert im.mode == "L" + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == 128 + assert im.getpixel((64, 32)) == 255 + + with Image.open("Tests/images/apng/mode_greyscale_alpha.png") as im: + assert im.mode == "LA" + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (128, 191) + assert im.getpixel((64, 32)) == (128, 191) + + with Image.open("Tests/images/apng/mode_palette.png") as im: + assert im.mode == "P" + im.seek(im.n_frames - 1) + im = im.convert("RGB") + assert im.getpixel((0, 0)) == (0, 255, 0) + assert im.getpixel((64, 32)) == (0, 255, 0) + + with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: + assert im.mode == "P" + im.seek(im.n_frames - 1) + im = im.convert("RGBA") + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: + assert im.mode == "P" + im.seek(im.n_frames - 1) + im = im.convert("RGBA") + assert im.getpixel((0, 0)) == (0, 0, 255, 128) + assert im.getpixel((64, 32)) == (0, 0, 255, 128) + + +def test_apng_chunk_errors(): + with Image.open("Tests/images/apng/chunk_no_actl.png") as im: + assert not im.is_animated + + def open(): + with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: + im.load() + assert not im.is_animated + + pytest.warns(UserWarning, open) + + with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: + assert not im.is_animated + + with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: + with pytest.raises(SyntaxError): + im.seek(im.n_frames - 1) + + with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: + with pytest.raises(SyntaxError): + im.seek(im.n_frames - 1) + + with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: + with pytest.raises(SyntaxError): + im.seek(im.n_frames - 1) + + +def test_apng_syntax_errors(): + def open_frames_zero(): + with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: + assert not im.is_animated + with pytest.raises(OSError): + im.load() + + pytest.warns(UserWarning, open_frames_zero) + + def open_frames_zero_default(): + with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: + assert not im.is_animated + im.load() + + pytest.warns(UserWarning, open_frames_zero_default) + + # we can handle this case gracefully + exception = None + with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: + try: + im.seek(im.n_frames - 1) + except Exception as e: + exception = e + assert exception is None + + with pytest.raises(SyntaxError): + with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: + im.seek(im.n_frames - 1) + im.load() + + def open(): + with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: + assert not im.is_animated + im.load() + + pytest.warns(UserWarning, open) + + +def test_apng_sequence_errors(): + test_files = [ + "sequence_start.png", + "sequence_gap.png", + "sequence_repeat.png", + "sequence_repeat_chunk.png", + "sequence_reorder.png", + "sequence_reorder_chunk.png", + "sequence_fdat_fctl.png", + ] + for f in test_files: + with pytest.raises(SyntaxError): + with Image.open(f"Tests/images/apng/{f}") as im: + im.seek(im.n_frames - 1) + im.load() + + +def test_apng_save(tmp_path): + 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) + + with Image.open(test_file) as im: + im.load() + assert not im.is_animated + assert im.n_frames == 1 + assert im.get_format_mimetype() == "image/apng" + assert im.info.get("default_image") is None + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/single_frame_default.png") as im: + frames = [] + for frame_im in ImageSequence.Iterator(im): + frames.append(frame_im.copy()) + frames[0].save( + test_file, save_all=True, default_image=True, append_images=frames[1:] + ) + + with Image.open(test_file) as im: + im.load() + assert im.is_animated + assert im.n_frames == 2 + assert im.get_format_mimetype() == "image/apng" + assert im.info.get("default_image") + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_save_split_fdat(tmp_path): + # 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 + # data chunks) + test_file = str(tmp_path / "temp.png") + with Image.open("Tests/images/old-style-jpeg-compression.png") as im: + frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] + im.save( + test_file, + save_all=True, + default_image=True, + append_images=frames, + ) + with Image.open(test_file) as im: + exception = None + try: + im.seek(im.n_frames - 1) + im.load() + except Exception as e: + exception = e + assert exception is None + + +def test_apng_save_duration_loop(tmp_path): + test_file = str(tmp_path / "temp.png") + with Image.open("Tests/images/apng/delay.png") as im: + frames = [] + durations = [] + loop = im.info.get("loop") + default_image = im.info.get("default_image") + for i, frame_im in enumerate(ImageSequence.Iterator(im)): + frames.append(frame_im.copy()) + if i != 0 or not default_image: + durations.append(frame_im.info.get("duration", 0)) + frames[0].save( + test_file, + save_all=True, + default_image=default_image, + append_images=frames[1:], + duration=durations, + loop=loop, + ) + + with Image.open(test_file) as im: + im.load() + assert im.info.get("loop") == loop + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + im.seek(3) + assert im.info.get("duration") == 500.0 + im.seek(4) + assert im.info.get("duration") == 1000.0 + + # test removal of duplicated frames + frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255)) + frame.save(test_file, save_all=True, append_images=[frame], duration=[500, 250]) + with Image.open(test_file) as im: + im.load() + assert im.n_frames == 1 + assert im.info.get("duration") == 750 + + +def test_apng_save_disposal(tmp_path): + test_file = str(tmp_path / "temp.png") + size = (128, 64) + red = Image.new("RGBA", size, (255, 0, 0, 255)) + green = Image.new("RGBA", size, (0, 255, 0, 255)) + transparent = Image.new("RGBA", size, (0, 0, 0, 0)) + + # test APNG_DISPOSE_OP_NONE + red.save( + test_file, + save_all=True, + append_images=[green, transparent], + disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, + blend=PngImagePlugin.APNG_BLEND_OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test APNG_DISPOSE_OP_BACKGROUND + disposal = [ + PngImagePlugin.APNG_DISPOSE_OP_NONE, + PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, + PngImagePlugin.APNG_DISPOSE_OP_NONE, + ] + red.save( + test_file, + save_all=True, + append_images=[red, transparent], + disposal=disposal, + blend=PngImagePlugin.APNG_BLEND_OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + disposal = [ + PngImagePlugin.APNG_DISPOSE_OP_NONE, + PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, + ] + red.save( + test_file, + save_all=True, + append_images=[green], + disposal=disposal, + blend=PngImagePlugin.APNG_BLEND_OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test APNG_DISPOSE_OP_PREVIOUS + disposal = [ + PngImagePlugin.APNG_DISPOSE_OP_NONE, + PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, + PngImagePlugin.APNG_DISPOSE_OP_NONE, + ] + red.save( + test_file, + save_all=True, + append_images=[green, red, transparent], + default_image=True, + disposal=disposal, + blend=PngImagePlugin.APNG_BLEND_OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(3) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + disposal = [ + PngImagePlugin.APNG_DISPOSE_OP_NONE, + PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, + ] + red.save( + test_file, + save_all=True, + append_images=[green], + disposal=disposal, + blend=PngImagePlugin.APNG_BLEND_OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_save_disposal_previous(tmp_path): + test_file = str(tmp_path / "temp.png") + size = (128, 64) + transparent = Image.new("RGBA", size, (0, 0, 0, 0)) + red = Image.new("RGBA", size, (255, 0, 0, 255)) + green = Image.new("RGBA", size, (0, 255, 0, 255)) + + # test APNG_DISPOSE_OP_NONE + transparent.save( + test_file, + save_all=True, + append_images=[red, green], + disposal=PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_save_blend(tmp_path): + test_file = str(tmp_path / "temp.png") + size = (128, 64) + red = Image.new("RGBA", size, (255, 0, 0, 255)) + green = Image.new("RGBA", size, (0, 255, 0, 255)) + transparent = Image.new("RGBA", size, (0, 0, 0, 0)) + + # test APNG_BLEND_OP_SOURCE on solid color + blend = [ + PngImagePlugin.APNG_BLEND_OP_OVER, + PngImagePlugin.APNG_BLEND_OP_SOURCE, + ] + red.save( + test_file, + save_all=True, + append_images=[red, green], + default_image=True, + disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, + blend=blend, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test APNG_BLEND_OP_SOURCE on transparent color + blend = [ + PngImagePlugin.APNG_BLEND_OP_OVER, + PngImagePlugin.APNG_BLEND_OP_SOURCE, + ] + red.save( + test_file, + save_all=True, + append_images=[red, transparent], + default_image=True, + disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, + blend=blend, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + # test APNG_BLEND_OP_OVER + red.save( + test_file, + save_all=True, + append_images=[green, transparent], + default_image=True, + disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, + blend=PngImagePlugin.APNG_BLEND_OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 8dbd82986..94c469c7f 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,20 +1,21 @@ from PIL import Image -from .helper import PillowTestCase +from .helper import assert_image_equal -class TestFileBlp(PillowTestCase): - def test_load_blp2_raw(self): - with Image.open("Tests/images/blp/blp2_raw.blp") as im: - with Image.open("Tests/images/blp/blp2_raw.png") as target: - self.assert_image_equal(im, target) +def test_load_blp2_raw(): + with Image.open("Tests/images/blp/blp2_raw.blp") as im: + with Image.open("Tests/images/blp/blp2_raw.png") as target: + assert_image_equal(im, target) - def test_load_blp2_dxt1(self): - with Image.open("Tests/images/blp/blp2_dxt1.blp") as im: - with Image.open("Tests/images/blp/blp2_dxt1.png") as target: - self.assert_image_equal(im, target) - def test_load_blp2_dxt1a(self): - im = Image.open("Tests/images/blp/blp2_dxt1a.blp") - target = Image.open("Tests/images/blp/blp2_dxt1a.png") - self.assert_image_equal(im, target) +def test_load_blp2_dxt1(): + with Image.open("Tests/images/blp/blp2_dxt1.blp") as im: + with Image.open("Tests/images/blp/blp2_dxt1.png") as target: + assert_image_equal(im, target) + + +def test_load_blp2_dxt1a(): + with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: + with Image.open("Tests/images/blp/blp2_dxt1a.png") as target: + assert_image_equal(im, target) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index e7f2c9315..e2381df1e 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,123 +1,140 @@ import io +import pytest + from PIL import BmpImagePlugin, Image -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper -class TestFileBmp(PillowTestCase): - def roundtrip(self, im): - outfile = self.tempfile("temp.bmp") +def test_sanity(tmp_path): + def roundtrip(im): + outfile = str(tmp_path / "temp.bmp") im.save(outfile, "BMP") - reloaded = Image.open(outfile) - reloaded.load() - self.assertEqual(im.mode, reloaded.mode) - self.assertEqual(im.size, reloaded.size) - self.assertEqual(reloaded.format, "BMP") - self.assertEqual(reloaded.get_format_mimetype(), "image/bmp") + with Image.open(outfile) as reloaded: + reloaded.load() + assert im.mode == reloaded.mode + assert im.size == reloaded.size + assert reloaded.format == "BMP" + assert reloaded.get_format_mimetype() == "image/bmp" - def test_sanity(self): - self.roundtrip(hopper()) + roundtrip(hopper()) - self.roundtrip(hopper("1")) - self.roundtrip(hopper("L")) - self.roundtrip(hopper("P")) - self.roundtrip(hopper("RGB")) + roundtrip(hopper("1")) + roundtrip(hopper("L")) + roundtrip(hopper("P")) + roundtrip(hopper("RGB")) - def test_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, BmpImagePlugin.BmpImageFile, fp) - def test_save_to_bytes(self): - output = io.BytesIO() - im = hopper() - im.save(output, "BMP") +def test_invalid_file(): + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + BmpImagePlugin.BmpImageFile(fp) - output.seek(0) - reloaded = Image.open(output) - self.assertEqual(im.mode, reloaded.mode) - self.assertEqual(im.size, reloaded.size) - self.assertEqual(reloaded.format, "BMP") +def test_save_to_bytes(): + output = io.BytesIO() + im = hopper() + im.save(output, "BMP") - def test_dpi(self): - dpi = (72, 72) + output.seek(0) + with Image.open(output) as reloaded: + assert im.mode == reloaded.mode + assert im.size == reloaded.size + assert reloaded.format == "BMP" - output = io.BytesIO() - with hopper() as im: - im.save(output, "BMP", dpi=dpi) - output.seek(0) - with Image.open(output) as reloaded: - self.assertEqual(reloaded.info["dpi"], dpi) +def test_save_too_large(tmp_path): + outfile = str(tmp_path / "temp.bmp") + with Image.new("RGB", (1, 1)) as im: + im._size = (37838, 37838) + with pytest.raises(ValueError): + im.save(outfile) - def test_save_bmp_with_dpi(self): - # Test for #1301 - # Arrange - outfile = self.tempfile("temp.jpg") - im = Image.open("Tests/images/hopper.bmp") + +def test_dpi(): + dpi = (72, 72) + + output = io.BytesIO() + with hopper() as im: + im.save(output, "BMP", dpi=dpi) + + output.seek(0) + with Image.open(output) as reloaded: + assert reloaded.info["dpi"] == dpi + + +def test_save_bmp_with_dpi(tmp_path): + # Test for #1301 + # Arrange + outfile = str(tmp_path / "temp.jpg") + with Image.open("Tests/images/hopper.bmp") as im: # Act im.save(outfile, "JPEG", dpi=im.info["dpi"]) # Assert - reloaded = Image.open(outfile) - reloaded.load() - self.assertEqual(im.info["dpi"], reloaded.info["dpi"]) - self.assertEqual(im.size, reloaded.size) - self.assertEqual(reloaded.format, "JPEG") + with Image.open(outfile) as reloaded: + reloaded.load() + assert im.info["dpi"] == reloaded.info["dpi"] + assert im.size == reloaded.size + assert reloaded.format == "JPEG" - def test_load_dpi_rounding(self): - # Round up - with Image.open("Tests/images/hopper.bmp") as im: - self.assertEqual(im.info["dpi"], (96, 96)) - # Round down - with Image.open("Tests/images/hopper_roundDown.bmp") as im: - self.assertEqual(im.info["dpi"], (72, 72)) +def test_load_dpi_rounding(): + # Round up + with Image.open("Tests/images/hopper.bmp") as im: + assert im.info["dpi"] == (96, 96) - def test_save_dpi_rounding(self): - outfile = self.tempfile("temp.bmp") - im = Image.open("Tests/images/hopper.bmp") + # Round down + with Image.open("Tests/images/hopper_roundDown.bmp") as im: + assert im.info["dpi"] == (72, 72) + +def test_save_dpi_rounding(tmp_path): + outfile = str(tmp_path / "temp.bmp") + with Image.open("Tests/images/hopper.bmp") as im: im.save(outfile, dpi=(72.2, 72.2)) with Image.open(outfile) as reloaded: - self.assertEqual(reloaded.info["dpi"], (72, 72)) + assert reloaded.info["dpi"] == (72, 72) - im.save(outfile, dpi=(72.8, 72.8)) - with Image.open(outfile) as reloaded: - self.assertEqual(reloaded.info["dpi"], (73, 73)) + im.save(outfile, dpi=(72.8, 72.8)) + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == (73, 73) - def test_load_dib(self): - # test for #1293, Imagegrab returning Unsupported Bitfields Format - im = Image.open("Tests/images/clipboard.dib") - self.assertEqual(im.format, "DIB") - self.assertEqual(im.get_format_mimetype(), "image/bmp") - target = Image.open("Tests/images/clipboard_target.png") - self.assert_image_equal(im, target) +def test_load_dib(): + # test for #1293, Imagegrab returning Unsupported Bitfields Format + with Image.open("Tests/images/clipboard.dib") as im: + assert im.format == "DIB" + assert im.get_format_mimetype() == "image/bmp" - def test_save_dib(self): - outfile = self.tempfile("temp.dib") + with Image.open("Tests/images/clipboard_target.png") as target: + assert_image_equal(im, target) - im = Image.open("Tests/images/clipboard.dib") + +def test_save_dib(tmp_path): + outfile = str(tmp_path / "temp.dib") + + with Image.open("Tests/images/clipboard.dib") as im: im.save(outfile) - reloaded = Image.open(outfile) - self.assertEqual(reloaded.format, "DIB") - self.assertEqual(reloaded.get_format_mimetype(), "image/bmp") - self.assert_image_equal(im, reloaded) + with Image.open(outfile) as reloaded: + assert reloaded.format == "DIB" + assert reloaded.get_format_mimetype() == "image/bmp" + assert_image_equal(im, reloaded) - def test_rgba_bitfields(self): - # This test image has been manually hexedited - # to change the bitfield compression in the header from XBGR to RGBA - im = Image.open("Tests/images/rgb32bf-rgba.bmp") + +def test_rgba_bitfields(): + # 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: # So before the comparing the image, swap the channels b, g, r = im.split()[1:] im = Image.merge("RGB", (r, g, b)) - target = Image.open("Tests/images/bmp/q/rgb32bf-xbgr.bmp") - self.assert_image_equal(im, target) + with Image.open("Tests/images/bmp/q/rgb32bf-xbgr.bmp") as target: + assert_image_equal(im, target) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 540d89719..11acc1c88 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,42 +1,47 @@ +import pytest + from PIL import BufrStubImagePlugin, Image -from .helper import PillowTestCase, hopper +from .helper import hopper TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" -class TestFileBufrStub(PillowTestCase): - def test_open(self): - # Act - with Image.open(TEST_FILE) as im: +def test_open(): + # Act + with Image.open(TEST_FILE) as im: - # Assert - self.assertEqual(im.format, "BUFR") + # Assert + assert im.format == "BUFR" - # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + # Dummy data from the stub + assert im.mode == "F" + assert im.size == (1, 1) - def test_invalid_file(self): - # Arrange - invalid_file = "Tests/images/flower.jpg" - # Act / Assert - self.assertRaises( - SyntaxError, BufrStubImagePlugin.BufrStubImageFile, invalid_file - ) +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" - def test_load(self): - # Arrange - with Image.open(TEST_FILE) as im: + # Act / Assert + with pytest.raises(SyntaxError): + BufrStubImagePlugin.BufrStubImageFile(invalid_file) - # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) - def test_save(self): - # Arrange - im = hopper() - tmpfile = self.tempfile("temp.bufr") +def test_load(): + # Arrange + with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot save without an implemented handler - self.assertRaises(IOError, im.save, tmpfile) + # Act / Assert: stub cannot load without an implemented handler + with pytest.raises(OSError): + im.load() + + +def test_save(tmp_path): + # Arrange + im = hopper() + tmpfile = str(tmp_path / "temp.bufr") + + # Act / Assert: stub cannot save without an implemented handler + with pytest.raises(OSError): + im.save(tmpfile) diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 2f931fb68..b752e217f 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,63 +1,68 @@ from PIL import ContainerIO, Image -from .helper import PillowTestCase, hopper +from .helper import hopper TEST_FILE = "Tests/images/dummy.container" -class TestFileContainer(PillowTestCase): - def test_sanity(self): - dir(Image) - dir(ContainerIO) +def test_sanity(): + dir(Image) + dir(ContainerIO) - def test_isatty(self): - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) - self.assertFalse(container.isatty()) +def test_isatty(): + with hopper() as im: + container = ContainerIO.ContainerIO(im, 0, 0) - def test_seek_mode_0(self): - # Arrange - mode = 0 - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) + assert container.isatty() is False - # Act - container.seek(33, mode) - container.seek(33, mode) - # Assert - self.assertEqual(container.tell(), 33) +def test_seek_mode_0(): + # Arrange + mode = 0 + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - def test_seek_mode_1(self): - # Arrange - mode = 1 - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) + # Act + container.seek(33, mode) + container.seek(33, mode) - # Act - container.seek(33, mode) - container.seek(33, mode) + # Assert + assert container.tell() == 33 - # Assert - self.assertEqual(container.tell(), 66) - def test_seek_mode_2(self): - # Arrange - mode = 2 - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) +def test_seek_mode_1(): + # Arrange + mode = 1 + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - # Act - container.seek(33, mode) - container.seek(33, mode) + # Act + container.seek(33, mode) + container.seek(33, mode) - # Assert - self.assertEqual(container.tell(), 100) + # Assert + assert container.tell() == 66 - def test_read_n0(self): - # Arrange - with open(TEST_FILE) as fh: + +def test_seek_mode_2(): + # Arrange + mode = 2 + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) + + # Act + container.seek(33, mode) + container.seek(33, mode) + + # Assert + assert container.tell() == 100 + + +def test_read_n0(): + # Arrange + for bytesmode in (True, False): + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) # Act @@ -65,11 +70,15 @@ class TestFileContainer(PillowTestCase): data = container.read() # Assert - self.assertEqual(data, "7\nThis is line 8\n") + if bytesmode: + data = data.decode() + assert data == "7\nThis is line 8\n" - def test_read_n(self): - # Arrange - with open(TEST_FILE) as fh: + +def test_read_n(): + # Arrange + for bytesmode in (True, False): + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) # Act @@ -77,11 +86,15 @@ class TestFileContainer(PillowTestCase): data = container.read(3) # Assert - self.assertEqual(data, "7\nT") + if bytesmode: + data = data.decode() + assert data == "7\nT" - def test_read_eof(self): - # Arrange - with open(TEST_FILE) as fh: + +def test_read_eof(): + # Arrange + for bytesmode in (True, False): + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) # Act @@ -89,21 +102,29 @@ class TestFileContainer(PillowTestCase): data = container.read() # Assert - self.assertEqual(data, "") + if bytesmode: + data = data.decode() + assert data == "" - def test_readline(self): - # Arrange - with open(TEST_FILE) as fh: + +def test_readline(): + # Arrange + for bytesmode in (True, False): + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 0, 120) # Act data = container.readline() # Assert - self.assertEqual(data, "This is line 1\n") + if bytesmode: + data = data.decode() + assert data == "This is line 1\n" - def test_readlines(self): - # Arrange + +def test_readlines(): + # Arrange + for bytesmode in (True, False): expected = [ "This is line 1\n", "This is line 2\n", @@ -114,12 +135,13 @@ class TestFileContainer(PillowTestCase): "This is line 7\n", "This is line 8\n", ] - with open(TEST_FILE) as fh: + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 0, 120) # Act data = container.readlines() # Assert - - self.assertEqual(data, expected) + if bytesmode: + data = [line.decode() for line in data] + assert data == expected diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 0b2f7a98c..f04a20a22 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -1,29 +1,30 @@ -from PIL import CurImagePlugin, Image +import pytest -from .helper import PillowTestCase +from PIL import CurImagePlugin, Image TEST_FILE = "Tests/images/deerstalker.cur" -class TestFileCur(PillowTestCase): - def test_sanity(self): - im = Image.open(TEST_FILE) - - self.assertEqual(im.size, (32, 32)) - self.assertIsInstance(im, CurImagePlugin.CurImageFile) +def test_sanity(): + with Image.open(TEST_FILE) as im: + assert im.size == (32, 32) + assert isinstance(im, CurImagePlugin.CurImageFile) # Check some pixel colors to ensure image is loaded properly - self.assertEqual(im.getpixel((10, 1)), (0, 0, 0, 0)) - self.assertEqual(im.getpixel((11, 1)), (253, 254, 254, 1)) - self.assertEqual(im.getpixel((16, 16)), (84, 87, 86, 255)) + assert im.getpixel((10, 1)) == (0, 0, 0, 0) + assert im.getpixel((11, 1)) == (253, 254, 254, 1) + assert im.getpixel((16, 16)) == (84, 87, 86, 255) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, CurImagePlugin.CurImageFile, invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - no_cursors_file = "Tests/images/no_cursors.cur" + with pytest.raises(SyntaxError): + CurImagePlugin.CurImageFile(invalid_file) - cur = CurImagePlugin.CurImageFile(TEST_FILE) - cur.fp.close() - with open(no_cursors_file, "rb") as cur.fp: - self.assertRaises(TypeError, cur._open) + no_cursors_file = "Tests/images/no_cursors.cur" + + cur = CurImagePlugin.CurImageFile(TEST_FILE) + cur.fp.close() + with open(no_cursors_file, "rb") as cur.fp: + with pytest.raises(TypeError): + cur._open() diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index e9411dbf8..818d6ed5e 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,83 +1,93 @@ -import unittest +import pytest from PIL import DcxImagePlugin, Image -from .helper import PillowTestCase, hopper, is_pypy +from .helper import assert_image_equal, hopper, is_pypy # Created with ImageMagick: convert hopper.ppm hopper.dcx TEST_FILE = "Tests/images/hopper.dcx" -class TestFileDcx(PillowTestCase): - def test_sanity(self): - # Arrange +def test_sanity(): + # Arrange + + # Act + with Image.open(TEST_FILE) as im: + + # Assert + assert im.size == (128, 128) + assert isinstance(im, DcxImagePlugin.DcxImageFile) + orig = hopper() + assert_image_equal(im, orig) + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + im = Image.open(TEST_FILE) + im.load() + + pytest.warns(ResourceWarning, open) + + +def test_closed_file(): + def open(): + im = Image.open(TEST_FILE) + im.load() + im.close() + + pytest.warns(None, open) + + +def test_context_manager(): + def open(): + with Image.open(TEST_FILE) as im: + im.load() + + pytest.warns(None, open) + + +def test_invalid_file(): + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + DcxImagePlugin.DcxImageFile(fp) + + +def test_tell(): + # Arrange + with Image.open(TEST_FILE) as im: # Act - with Image.open(TEST_FILE) as im: + frame = im.tell() - # Assert - self.assertEqual(im.size, (128, 128)) - self.assertIsInstance(im, DcxImagePlugin.DcxImageFile) - orig = hopper() - self.assert_image_equal(im, orig) + # Assert + assert frame == 0 - @unittest.skipIf(is_pypy(), "Requires CPython") - def test_unclosed_file(self): - def open(): - im = Image.open(TEST_FILE) - im.load() - self.assert_warning(ResourceWarning, open) +def test_n_frames(): + with Image.open(TEST_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated - def test_closed_file(self): - def open(): - im = Image.open(TEST_FILE) - im.load() - im.close() - self.assert_warning(None, open) +def test_eoferror(): + with Image.open(TEST_FILE) as im: + n_frames = im.n_frames - def test_context_manager(self): - def open(): - with Image.open(TEST_FILE) as im: - im.load() + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames - self.assert_warning(None, open) + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) - def test_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, DcxImagePlugin.DcxImageFile, fp) - def test_tell(self): - # Arrange - with Image.open(TEST_FILE) as im: +def test_seek_too_far(): + # Arrange + with Image.open(TEST_FILE) as im: + frame = 999 # too big on purpose - # Act - frame = im.tell() - - # Assert - self.assertEqual(frame, 0) - - def test_n_frames(self): - with Image.open(TEST_FILE) as im: - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) - - def test_eoferror(self): - with Image.open(TEST_FILE) as im: - n_frames = im.n_frames - - # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) - - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) - - def test_seek_too_far(self): - # Arrange - with Image.open(TEST_FILE) as im: - frame = 999 # too big on purpose - - # Act / Assert - self.assertRaises(EOFError, im.seek, frame) + # Act / Assert + with pytest.raises(EOFError): + im.seek(frame) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 8ef90e86e..1cd7a1be7 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,158 +1,195 @@ +"""Test DdsImagePlugin""" from io import BytesIO +import pytest + from PIL import DdsImagePlugin, Image -from .helper import PillowTestCase +from .helper import assert_image_equal TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" +TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" +TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/uncompressed_rgb.dds" -class TestFileDds(PillowTestCase): - """Test DdsImagePlugin""" - - def test_sanity_dxt1(self): - """Check DXT1 images can be opened""" - target = Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) - - im = Image.open(TEST_FILE_DXT1) +def test_sanity_dxt1(): + """Check DXT1 images can be opened""" + with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: + target = target.convert("RGBA") + with Image.open(TEST_FILE_DXT1) as im: im.load() - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (256, 256)) + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) - self.assert_image_equal(target.convert("RGBA"), im) + assert_image_equal(im, target) - def test_sanity_dxt5(self): - """Check DXT5 images can be opened""" - target = Image.open(TEST_FILE_DXT5.replace(".dds", ".png")) +def test_sanity_dxt5(): + """Check DXT5 images can be opened""" - im = Image.open(TEST_FILE_DXT5) + with Image.open(TEST_FILE_DXT5) as im: im.load() - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (256, 256)) + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) - self.assert_image_equal(target, im) + with Image.open(TEST_FILE_DXT5.replace(".dds", ".png")) as target: + assert_image_equal(target, im) - def test_sanity_dxt3(self): - """Check DXT3 images can be opened""" - target = Image.open(TEST_FILE_DXT3.replace(".dds", ".png")) +def test_sanity_dxt3(): + """Check DXT3 images can be opened""" - im = Image.open(TEST_FILE_DXT3) - im.load() - - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (256, 256)) - - self.assert_image_equal(target, im) - - def test_dx10_bc7(self): - """Check DX10 images can be opened""" - - target = Image.open(TEST_FILE_DX10_BC7.replace(".dds", ".png")) - - im = Image.open(TEST_FILE_DX10_BC7) - im.load() - - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (256, 256)) - - self.assert_image_equal(target, im) - - def test_dx10_bc7_unorm_srgb(self): - """Check DX10 unsigned normalized integer images can be opened""" - - target = Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB.replace(".dds", ".png")) - - im = Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) - im.load() - - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (16, 16)) - self.assertEqual(im.info["gamma"], 1 / 2.2) - - self.assert_image_equal(target, im) - - def test_unimplemented_dxgi_format(self): - self.assertRaises( - NotImplementedError, - Image.open, - "Tests/images/unimplemented_dxgi_format.dds", - ) - - def test_uncompressed_rgb(self): - """Check uncompressed RGB images can be opened""" - - target = Image.open(TEST_FILE_UNCOMPRESSED_RGB.replace(".dds", ".png")) - - im = Image.open(TEST_FILE_UNCOMPRESSED_RGB) - im.load() - - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (800, 600)) - - self.assert_image_equal(target, im) - - def test__validate_true(self): - """Check valid prefix""" - # Arrange - prefix = b"DDS etc" - - # Act - output = DdsImagePlugin._validate(prefix) - - # Assert - self.assertTrue(output) - - def test__validate_false(self): - """Check invalid prefix""" - # Arrange - prefix = b"something invalid" - - # Act - output = DdsImagePlugin._validate(prefix) - - # Assert - self.assertFalse(output) - - def test_short_header(self): - """ Check a short header""" - with open(TEST_FILE_DXT5, "rb") as f: - img_file = f.read() - - def short_header(): - Image.open(BytesIO(img_file[:119])) - - self.assertRaises(IOError, short_header) - - def test_short_file(self): - """ 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(): - im = Image.open(BytesIO(img_file[:-100])) + with Image.open(TEST_FILE_DXT3.replace(".dds", ".png")) as target: + with Image.open(TEST_FILE_DXT3) as im: im.load() - self.assertRaises(IOError, short_file) + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) - def test_unimplemented_pixel_format(self): - self.assertRaises( - NotImplementedError, - Image.open, - "Tests/images/unimplemented_pixel_format.dds", - ) + assert_image_equal(target, im) + + +def test_dx10_bc7(): + """Check DX10 images can be opened""" + + with Image.open(TEST_FILE_DX10_BC7) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) + + with Image.open(TEST_FILE_DX10_BC7.replace(".dds", ".png")) as target: + assert_image_equal(target, im) + + +def test_dx10_bc7_unorm_srgb(): + """Check DX10 unsigned normalized integer images can be opened""" + + with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (16, 16) + assert im.info["gamma"] == 1 / 2.2 + + with Image.open( + TEST_FILE_DX10_BC7_UNORM_SRGB.replace(".dds", ".png") + ) as target: + assert_image_equal(target, im) + + +def test_dx10_r8g8b8a8(): + """Check DX10 images can be opened""" + + with Image.open(TEST_FILE_DX10_R8G8B8A8) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) + + with Image.open(TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png")) as target: + assert_image_equal(target, im) + + +def test_dx10_r8g8b8a8_unorm_srgb(): + """Check DX10 unsigned normalized integer images can be opened""" + + with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (16, 16) + assert im.info["gamma"] == 1 / 2.2 + + with Image.open( + TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB.replace(".dds", ".png") + ) as target: + assert_image_equal(target, im) + + +def test_unimplemented_dxgi_format(): + with pytest.raises(NotImplementedError): + Image.open("Tests/images/unimplemented_dxgi_format.dds") + + +def test_uncompressed_rgb(): + """Check uncompressed RGB images can be opened""" + + with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (800, 600) + + with Image.open(TEST_FILE_UNCOMPRESSED_RGB.replace(".dds", ".png")) as target: + assert_image_equal(target, im) + + +def test__validate_true(): + """Check valid prefix""" + # Arrange + prefix = b"DDS etc" + + # Act + output = DdsImagePlugin._validate(prefix) + + # Assert + assert output + + +def test__validate_false(): + """Check invalid prefix""" + # Arrange + prefix = b"something invalid" + + # Act + output = DdsImagePlugin._validate(prefix) + + # Assert + assert not output + + +def test_short_header(): + """ Check a short header""" + with open(TEST_FILE_DXT5, "rb") as f: + img_file = f.read() + + def short_header(): + Image.open(BytesIO(img_file[:119])) + + with pytest.raises(OSError): + short_header() + + +def test_short_file(): + """ 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(): + with Image.open(BytesIO(img_file[:-100])) as im: + im.load() + + with pytest.raises(OSError): + short_file() + + +def test_unimplemented_pixel_format(): + with pytest.raises(NotImplementedError): + Image.open("Tests/images/unimplemented_pixel_format.dds") diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 0ab14e4eb..f585a0669 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,241 +1,259 @@ import io -import unittest -from PIL import EpsImagePlugin, Image +import pytest -from .helper import PillowTestCase, hopper +from PIL import EpsImagePlugin, Image, features + +from .helper import assert_image_similar, hopper, skip_unless_feature HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() # Our two EPS test files (they are identical except for their bounding boxes) -file1 = "Tests/images/zero_bb.eps" -file2 = "Tests/images/non_zero_bb.eps" +FILE1 = "Tests/images/zero_bb.eps" +FILE2 = "Tests/images/non_zero_bb.eps" # Due to palletization, we'll need to convert these to RGB after load -file1_compare = "Tests/images/zero_bb.png" -file1_compare_scale2 = "Tests/images/zero_bb_scale2.png" +FILE1_COMPARE = "Tests/images/zero_bb.png" +FILE1_COMPARE_SCALE2 = "Tests/images/zero_bb_scale2.png" -file2_compare = "Tests/images/non_zero_bb.png" -file2_compare_scale2 = "Tests/images/non_zero_bb_scale2.png" +FILE2_COMPARE = "Tests/images/non_zero_bb.png" +FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png" # EPS test files with binary preview -file3 = "Tests/images/binary_preview_map.eps" +FILE3 = "Tests/images/binary_preview_map.eps" -class TestFileEps(PillowTestCase): - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_sanity(self): - # Regular scale - with Image.open(file1) as image1: - image1.load() - self.assertEqual(image1.mode, "RGB") - self.assertEqual(image1.size, (460, 352)) - self.assertEqual(image1.format, "EPS") +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_sanity(): + # Regular scale + with Image.open(FILE1) as image1: + image1.load() + assert image1.mode == "RGB" + assert image1.size == (460, 352) + assert image1.format == "EPS" - with Image.open(file2) as image2: - image2.load() - self.assertEqual(image2.mode, "RGB") - self.assertEqual(image2.size, (360, 252)) - self.assertEqual(image2.format, "EPS") + with Image.open(FILE2) as image2: + image2.load() + assert image2.mode == "RGB" + assert image2.size == (360, 252) + assert image2.format == "EPS" - # Double scale - with Image.open(file1) as image1_scale2: - image1_scale2.load(scale=2) - self.assertEqual(image1_scale2.mode, "RGB") - self.assertEqual(image1_scale2.size, (920, 704)) - self.assertEqual(image1_scale2.format, "EPS") + # Double scale + with Image.open(FILE1) as image1_scale2: + image1_scale2.load(scale=2) + assert image1_scale2.mode == "RGB" + assert image1_scale2.size == (920, 704) + assert image1_scale2.format == "EPS" - with Image.open(file2) as image2_scale2: - image2_scale2.load(scale=2) - self.assertEqual(image2_scale2.mode, "RGB") - self.assertEqual(image2_scale2.size, (720, 504)) - self.assertEqual(image2_scale2.format, "EPS") + with Image.open(FILE2) as image2_scale2: + image2_scale2.load(scale=2) + assert image2_scale2.mode == "RGB" + assert image2_scale2.size == (720, 504) + assert image2_scale2.format == "EPS" - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, EpsImagePlugin.EpsImageFile, invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_cmyk(self): - with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: + with pytest.raises(SyntaxError): + EpsImagePlugin.EpsImageFile(invalid_file) - self.assertEqual(cmyk_image.mode, "CMYK") - self.assertEqual(cmyk_image.size, (100, 100)) - self.assertEqual(cmyk_image.format, "EPS") - cmyk_image.load() - self.assertEqual(cmyk_image.mode, "RGB") +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_cmyk(): + with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: - if "jpeg_decoder" in dir(Image.core): - target = Image.open("Tests/images/pil_sample_rgb.jpg") - self.assert_image_similar(cmyk_image, target, 10) + assert cmyk_image.mode == "CMYK" + assert cmyk_image.size == (100, 100) + assert cmyk_image.format == "EPS" - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_showpage(self): - # 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: - # should not crash/hang - plot_image.load() - # fonts could be slightly different - self.assert_image_similar(plot_image, target, 6) + cmyk_image.load() + assert cmyk_image.mode == "RGB" - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_file_object(self): - # issue 479 - with Image.open(file1) as image1: - with open(self.tempfile("temp_file.eps"), "wb") as fh: - image1.save(fh, "EPS") + if features.check("jpg"): + with Image.open("Tests/images/pil_sample_rgb.jpg") as target: + assert_image_similar(cmyk_image, target, 10) - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_iobase_object(self): - # issue 479 - with Image.open(file1) as image1: - with open(self.tempfile("temp_iobase.eps"), "wb") as fh: - image1.save(fh, "EPS") - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_bytesio_object(self): - with open(file1, "rb") as f: - img_bytes = io.BytesIO(f.read()) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_showpage(): + # 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: + # should not crash/hang + plot_image.load() + # fonts could be slightly different + assert_image_similar(plot_image, target, 6) - img = Image.open(img_bytes) + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_file_object(tmp_path): + # issue 479 + with Image.open(FILE1) as image1: + with open(str(tmp_path / "temp.eps"), "wb") as fh: + image1.save(fh, "EPS") + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_iobase_object(tmp_path): + # issue 479 + with Image.open(FILE1) as image1: + with open(str(tmp_path / "temp_iobase.eps"), "wb") as fh: + image1.save(fh, "EPS") + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_bytesio_object(): + with open(FILE1, "rb") as f: + img_bytes = io.BytesIO(f.read()) + + with Image.open(img_bytes) as img: img.load() - image1_scale1_compare = Image.open(file1_compare).convert("RGB") + with Image.open(FILE1_COMPARE) as image1_scale1_compare: + image1_scale1_compare = image1_scale1_compare.convert("RGB") image1_scale1_compare.load() - self.assert_image_similar(img, image1_scale1_compare, 5) + assert_image_similar(img, image1_scale1_compare, 5) - def test_image_mode_not_supported(self): - im = hopper("RGBA") - tmpfile = self.tempfile("temp.eps") - self.assertRaises(ValueError, im.save, tmpfile) - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_render_scale1(self): - # We need png support for these render test - codecs = dir(Image.core) - if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - self.skipTest("zip/deflate support not available") +def test_image_mode_not_supported(tmp_path): + im = hopper("RGBA") + tmpfile = str(tmp_path / "temp.eps") + with pytest.raises(ValueError): + im.save(tmpfile) - # Zero bounding box - with Image.open(file1) as image1_scale1: - image1_scale1.load() - image1_scale1_compare = Image.open(file1_compare).convert("RGB") - image1_scale1_compare.load() - self.assert_image_similar(image1_scale1, image1_scale1_compare, 5) - # Non-Zero bounding box - with Image.open(file2) as image2_scale1: - image2_scale1.load() - image2_scale1_compare = Image.open(file2_compare).convert("RGB") - image2_scale1_compare.load() - self.assert_image_similar(image2_scale1, image2_scale1_compare, 10) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@skip_unless_feature("zlib") +def test_render_scale1(): + # We need png support for these render test - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_render_scale2(self): - # We need png support for these render test - codecs = dir(Image.core) - if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - self.skipTest("zip/deflate support not available") + # Zero bounding box + with Image.open(FILE1) as image1_scale1: + image1_scale1.load() + with Image.open(FILE1_COMPARE) as image1_scale1_compare: + image1_scale1_compare = image1_scale1_compare.convert("RGB") + image1_scale1_compare.load() + assert_image_similar(image1_scale1, image1_scale1_compare, 5) - # Zero bounding box - with Image.open(file1) as image1_scale2: - image1_scale2.load(scale=2) - image1_scale2_compare = Image.open(file1_compare_scale2).convert("RGB") - image1_scale2_compare.load() - self.assert_image_similar(image1_scale2, image1_scale2_compare, 5) + # Non-Zero bounding box + with Image.open(FILE2) as image2_scale1: + image2_scale1.load() + with Image.open(FILE2_COMPARE) as image2_scale1_compare: + image2_scale1_compare = image2_scale1_compare.convert("RGB") + image2_scale1_compare.load() + assert_image_similar(image2_scale1, image2_scale1_compare, 10) - # Non-Zero bounding box - with Image.open(file2) as image2_scale2: - image2_scale2.load(scale=2) - image2_scale2_compare = Image.open(file2_compare_scale2).convert("RGB") - image2_scale2_compare.load() - self.assert_image_similar(image2_scale2, image2_scale2_compare, 10) - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_resize(self): - files = [file1, file2, "Tests/images/illu10_preview.eps"] - for fn in files: - with Image.open(fn) as im: - new_size = (100, 100) - im = im.resize(new_size) - self.assertEqual(im.size, new_size) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@skip_unless_feature("zlib") +def test_render_scale2(): + # We need png support for these render test - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_thumbnail(self): - # Issue #619 - # Arrange - files = [file1, file2] - for fn in files: - with Image.open(file1) as im: - new_size = (100, 100) - im.thumbnail(new_size) - self.assertEqual(max(im.size), max(new_size)) + # Zero bounding box + with Image.open(FILE1) as image1_scale2: + image1_scale2.load(scale=2) + with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: + image1_scale2_compare = image1_scale2_compare.convert("RGB") + image1_scale2_compare.load() + assert_image_similar(image1_scale2, image1_scale2_compare, 5) - def test_read_binary_preview(self): - # Issue 302 - # open image with binary preview - with Image.open(file3): - pass + # 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: + image2_scale2_compare = image2_scale2_compare.convert("RGB") + image2_scale2_compare.load() + assert_image_similar(image2_scale2, image2_scale2_compare, 10) - def _test_readline(self, t, ending): + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_resize(): + files = [FILE1, FILE2, "Tests/images/illu10_preview.eps"] + for fn in files: + with Image.open(fn) as im: + new_size = (100, 100) + im = im.resize(new_size) + assert im.size == new_size + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_thumbnail(): + # Issue #619 + # Arrange + files = [FILE1, FILE2] + for fn in files: + with Image.open(FILE1) as im: + new_size = (100, 100) + im.thumbnail(new_size) + assert max(im.size) == max(new_size) + + +def test_read_binary_preview(): + # Issue 302 + # open image with binary preview + with Image.open(FILE3): + pass + + +def test_readline(tmp_path): + # 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): ending = "Failure with line ending: %s" % ( "".join("%s" % ord(s) for s in ending) ) - self.assertEqual(t.readline().strip("\r\n"), "something", ending) - self.assertEqual(t.readline().strip("\r\n"), "else", ending) - self.assertEqual(t.readline().strip("\r\n"), "baz", ending) - self.assertEqual(t.readline().strip("\r\n"), "bif", ending) + assert t.readline().strip("\r\n") == "something", ending + assert t.readline().strip("\r\n") == "else", ending + assert t.readline().strip("\r\n") == "baz", ending + assert t.readline().strip("\r\n") == "bif", ending - def _test_readline_io_psfile(self, test_string, ending): + def _test_readline_io_psfile(test_string, ending): f = io.BytesIO(test_string.encode("latin-1")) t = EpsImagePlugin.PSFile(f) - self._test_readline(t, ending) + _test_readline(t, ending) - def _test_readline_file_psfile(self, test_string, ending): - f = self.tempfile("temp.txt") + def _test_readline_file_psfile(test_string, ending): + f = str(tmp_path / "temp.txt") with open(f, "wb") as w: w.write(test_string.encode("latin-1")) with open(f, "rb") as r: t = EpsImagePlugin.PSFile(r) - self._test_readline(t, ending) + _test_readline(t, ending) - def test_readline(self): - # 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"] + for ending in line_endings: + s = ending.join(strings) + _test_readline_io_psfile(s, ending) + _test_readline_file_psfile(s, ending) - for ending in line_endings: - s = ending.join(strings) - self._test_readline_io_psfile(s, ending) - self._test_readline_file_psfile(s, ending) - def test_open_eps(self): - # https://github.com/python-pillow/Pillow/issues/1104 - # Arrange - FILES = [ - "Tests/images/illu10_no_preview.eps", - "Tests/images/illu10_preview.eps", - "Tests/images/illuCS6_no_preview.eps", - "Tests/images/illuCS6_preview.eps", - ] +def test_open_eps(): + # https://github.com/python-pillow/Pillow/issues/1104 + # Arrange + FILES = [ + "Tests/images/illu10_no_preview.eps", + "Tests/images/illu10_preview.eps", + "Tests/images/illuCS6_no_preview.eps", + "Tests/images/illuCS6_preview.eps", + ] - # Act / Assert - for filename in FILES: - with Image.open(filename) as img: - self.assertEqual(img.mode, "RGB") + # Act / Assert + for filename in FILES: + with Image.open(filename) as img: + assert img.mode == "RGB" - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_emptyline(self): - # Test file includes an empty line in the header data - emptyline_file = "Tests/images/zero_bb_emptyline.eps" - with Image.open(emptyline_file) as image: - image.load() - self.assertEqual(image.mode, "RGB") - self.assertEqual(image.size, (460, 352)) - self.assertEqual(image.format, "EPS") +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_emptyline(): + # Test file includes an empty line in the header data + emptyline_file = "Tests/images/zero_bb_emptyline.eps" + + with Image.open(emptyline_file) as image: + image.load() + assert image.mode == "RGB" + assert image.size == (460, 352) + assert image.format == "EPS" diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py index 933e0fd12..6dc7c4602 100644 --- a/Tests/test_file_fitsstub.py +++ b/Tests/test_file_fitsstub.py @@ -1,46 +1,48 @@ -from PIL import FitsStubImagePlugin, Image +import pytest -from .helper import PillowTestCase +from PIL import FitsStubImagePlugin, Image TEST_FILE = "Tests/images/hopper.fits" -class TestFileFitsStub(PillowTestCase): - def test_open(self): - # Act - with Image.open(TEST_FILE) as im: +def test_open(): + # Act + with Image.open(TEST_FILE) as im: - # Assert - self.assertEqual(im.format, "FITS") + # Assert + assert im.format == "FITS" - # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + # Dummy data from the stub + assert im.mode == "F" + assert im.size == (1, 1) - def test_invalid_file(self): - # Arrange - invalid_file = "Tests/images/flower.jpg" - # Act / Assert - self.assertRaises( - SyntaxError, FitsStubImagePlugin.FITSStubImageFile, invalid_file - ) +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" - def test_load(self): - # Arrange - with Image.open(TEST_FILE) as im: + # Act / Assert + with pytest.raises(SyntaxError): + FitsStubImagePlugin.FITSStubImageFile(invalid_file) - # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) - def test_save(self): - # Arrange - with Image.open(TEST_FILE) as im: - dummy_fp = None - dummy_filename = "dummy.filename" +def test_load(): + # Arrange + with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot save without an implemented handler - self.assertRaises(IOError, im.save, dummy_filename) - self.assertRaises( - IOError, FitsStubImagePlugin._save, im, dummy_fp, dummy_filename - ) + # Act / Assert: stub cannot load without an implemented handler + with pytest.raises(OSError): + im.load() + + +def test_save(): + # Arrange + with Image.open(TEST_FILE) as im: + dummy_fp = None + dummy_filename = "dummy.filename" + + # Act / Assert: stub cannot save without an implemented handler + with pytest.raises(OSError): + im.save(dummy_filename) + with pytest.raises(OSError): + FitsStubImagePlugin._save(im, dummy_fp, dummy_filename) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 895942d70..16b3dc59a 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,8 +1,8 @@ -import unittest +import pytest from PIL import FliImagePlugin, Image -from .helper import PillowTestCase, is_pypy +from .helper import assert_image_equal, is_pypy # created as an export of a palette image from Gimp2.6 # save as...-> hopper.fli, default options. @@ -12,105 +12,115 @@ static_test_file = "Tests/images/hopper.fli" animated_test_file = "Tests/images/a.fli" -class TestFileFli(PillowTestCase): - def test_sanity(self): +def test_sanity(): + with Image.open(static_test_file) as im: + im.load() + assert im.mode == "P" + assert im.size == (128, 128) + assert im.format == "FLI" + assert not im.is_animated + + with Image.open(animated_test_file) as im: + assert im.mode == "P" + assert im.size == (320, 200) + assert im.format == "FLI" + assert im.info["duration"] == 71 + assert im.is_animated + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + im = Image.open(static_test_file) + im.load() + + pytest.warns(ResourceWarning, open) + + +def test_closed_file(): + def open(): + im = Image.open(static_test_file) + im.load() + im.close() + + pytest.warns(None, open) + + +def test_context_manager(): + def open(): with Image.open(static_test_file) as im: im.load() - self.assertEqual(im.mode, "P") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "FLI") - self.assertFalse(im.is_animated) - with Image.open(animated_test_file) as im: - self.assertEqual(im.mode, "P") - self.assertEqual(im.size, (320, 200)) - self.assertEqual(im.format, "FLI") - self.assertEqual(im.info["duration"], 71) - self.assertTrue(im.is_animated) + pytest.warns(None, open) - @unittest.skipIf(is_pypy(), "Requires CPython") - def test_unclosed_file(self): - def open(): - im = Image.open(static_test_file) - im.load() - self.assert_warning(ResourceWarning, open) +def test_tell(): + # Arrange + with Image.open(static_test_file) as im: - def test_closed_file(self): - def open(): - im = Image.open(static_test_file) - im.load() - im.close() + # Act + frame = im.tell() - self.assert_warning(None, open) + # Assert + assert frame == 0 - def test_context_manager(self): - def open(): - with Image.open(static_test_file) as im: - im.load() - self.assert_warning(None, open) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - def test_tell(self): - # Arrange - with Image.open(static_test_file) as im: + with pytest.raises(SyntaxError): + FliImagePlugin.FliImageFile(invalid_file) - # Act - frame = im.tell() - # Assert - self.assertEqual(frame, 0) +def test_n_frames(): + with Image.open(static_test_file) as im: + assert im.n_frames == 1 + assert not im.is_animated - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" + with Image.open(animated_test_file) as im: + assert im.n_frames == 384 + assert im.is_animated - self.assertRaises(SyntaxError, FliImagePlugin.FliImageFile, invalid_file) - def test_n_frames(self): - with Image.open(static_test_file) as im: - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) +def test_eoferror(): + with Image.open(animated_test_file) as im: + n_frames = im.n_frames - with Image.open(animated_test_file) as im: - self.assertEqual(im.n_frames, 384) - self.assertTrue(im.is_animated) + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames - def test_eoferror(self): - with Image.open(animated_test_file) as im: - n_frames = im.n_frames + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) - # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) +def test_seek_tell(): + with Image.open(animated_test_file) as im: - def test_seek_tell(self): - with Image.open(animated_test_file) as im: + layer_number = im.tell() + assert layer_number == 0 - layer_number = im.tell() - self.assertEqual(layer_number, 0) + im.seek(0) + layer_number = im.tell() + assert layer_number == 0 - im.seek(0) - layer_number = im.tell() - self.assertEqual(layer_number, 0) + im.seek(1) + layer_number = im.tell() + assert layer_number == 1 - im.seek(1) - layer_number = im.tell() - self.assertEqual(layer_number, 1) + im.seek(2) + layer_number = im.tell() + assert layer_number == 2 - im.seek(2) - layer_number = im.tell() - self.assertEqual(layer_number, 2) + im.seek(1) + layer_number = im.tell() + assert layer_number == 1 - im.seek(1) - layer_number = im.tell() - self.assertEqual(layer_number, 1) - def test_seek(self): - with Image.open(animated_test_file) as im: - im.seek(50) +def test_seek(): + with Image.open(animated_test_file) as im: + im.seek(50) - with Image.open("Tests/images/a_fli.png") as expected: - self.assert_image_equal(im, expected) + with Image.open("Tests/images/a_fli.png") as expected: + assert_image_equal(im, expected) diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index 7c985be30..c3cc37ddf 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -1,22 +1,24 @@ -import unittest +import pytest -from .helper import PillowTestCase +from PIL import Image -try: - from PIL import FpxImagePlugin -except ImportError: - olefile_installed = False -else: - olefile_installed = True +FpxImagePlugin = pytest.importorskip( + "PIL.FpxImagePlugin", reason="olefile not installed" +) -@unittest.skipUnless(olefile_installed, "olefile package not installed") -class TestFileFpx(PillowTestCase): - def test_invalid_file(self): - # Test an invalid OLE file - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, FpxImagePlugin.FpxImageFile, invalid_file) +def test_invalid_file(): + # Test an invalid OLE file + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + FpxImagePlugin.FpxImageFile(invalid_file) - # Test a valid OLE file, but not an FPX file - ole_file = "Tests/images/test-ole-file.doc" - self.assertRaises(SyntaxError, FpxImagePlugin.FpxImageFile, ole_file) + # Test a valid OLE file, but not an FPX file + ole_file = "Tests/images/test-ole-file.doc" + with pytest.raises(SyntaxError): + FpxImagePlugin.FpxImageFile(ole_file) + + +def test_fpx_invalid_number_of_bands(): + with pytest.raises(OSError, match="Invalid number of bands"): + Image.open("Tests/images/input_bw_five_bands.fpx") diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index 7d30042ca..9b4375cd4 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,16 +1,15 @@ from PIL import Image -from .helper import PillowTestCase +from .helper import assert_image_equal, assert_image_similar -class TestFileFtex(PillowTestCase): - def test_load_raw(self): - im = Image.open("Tests/images/ftex_uncompressed.ftu") - target = Image.open("Tests/images/ftex_uncompressed.png") +def test_load_raw(): + with Image.open("Tests/images/ftex_uncompressed.ftu") as im: + with Image.open("Tests/images/ftex_uncompressed.png") as target: + assert_image_equal(im, target) - self.assert_image_equal(im, target) - def test_load_dxt1(self): - im = Image.open("Tests/images/ftex_dxt1.ftc") - target = Image.open("Tests/images/ftex_dxt1.png") - self.assert_image_similar(im, target.convert("RGBA"), 15) +def test_load_dxt1(): + with Image.open("Tests/images/ftex_dxt1.ftc") as im: + with Image.open("Tests/images/ftex_dxt1.png") as target: + assert_image_similar(im, target.convert("RGBA"), 15) diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index 4c26579a8..760f12e4d 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -1,15 +1,26 @@ +import pytest + from PIL import GbrImagePlugin, Image -from .helper import PillowTestCase +from .helper import assert_image_equal -class TestFileGbr(PillowTestCase): - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, GbrImagePlugin.GbrImageFile, invalid_file) + with pytest.raises(SyntaxError): + GbrImagePlugin.GbrImageFile(invalid_file) - def test_gbr_file(self): - with Image.open("Tests/images/gbr.gbr") as im: - with Image.open("Tests/images/gbr.png") as target: - self.assert_image_equal(target, im) + +def test_gbr_file(): + with Image.open("Tests/images/gbr.gbr") as im: + with Image.open("Tests/images/gbr.png") as target: + assert_image_equal(target, im) + + +def test_multiple_load_operations(): + with Image.open("Tests/images/gbr.gbr") as im: + im.load() + im.load() + with Image.open("Tests/images/gbr.png") as target: + assert_image_equal(target, im) diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index 0b7543a31..5594e5bbb 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -1,20 +1,23 @@ -from PIL import GdImageFile, UnidentifiedImageError +import pytest -from .helper import PillowTestCase +from PIL import GdImageFile, UnidentifiedImageError TEST_GD_FILE = "Tests/images/hopper.gd" -class TestFileGd(PillowTestCase): - def test_sanity(self): - with GdImageFile.open(TEST_GD_FILE) as im: - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "GD") +def test_sanity(): + with GdImageFile.open(TEST_GD_FILE) as im: + assert im.size == (128, 128) + assert im.format == "GD" - def test_bad_mode(self): - self.assertRaises(ValueError, GdImageFile.open, TEST_GD_FILE, "bad mode") - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" +def test_bad_mode(): + with pytest.raises(ValueError): + GdImageFile.open(TEST_GD_FILE, "bad mode") - self.assertRaises(UnidentifiedImageError, GdImageFile.open, invalid_file) + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(UnidentifiedImageError): + GdImageFile.open(invalid_file) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index bed25c890..90943ac8f 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,18 +1,16 @@ -import unittest from io import BytesIO -from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette +import pytest -from .helper import PillowTestCase, hopper, is_pypy, netpbm_available +from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, features -try: - from PIL import _webp - - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False - -codecs = dir(Image.core) +from .helper import ( + assert_image_equal, + assert_image_similar, + hopper, + is_pypy, + netpbm_available, +) # sample gif stream TEST_GIF = "Tests/images/hopper.gif" @@ -21,771 +19,811 @@ with open(TEST_GIF, "rb") as f: data = f.read() -class TestFileGif(PillowTestCase): - def setUp(self): - if "gif_encoder" not in codecs or "gif_decoder" not in codecs: - self.skipTest("gif support not available") # can this happen? +def test_sanity(): + with Image.open(TEST_GIF) as im: + im.load() + assert im.mode == "P" + assert im.size == (128, 128) + assert im.format == "GIF" + assert im.info["version"] == b"GIF89a" - def test_sanity(self): + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + im = Image.open(TEST_GIF) + im.load() + + pytest.warns(ResourceWarning, open) + + +def test_closed_file(): + def open(): + im = Image.open(TEST_GIF) + im.load() + im.close() + + pytest.warns(None, open) + + +def test_context_manager(): + def open(): with Image.open(TEST_GIF) as im: im.load() - self.assertEqual(im.mode, "P") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "GIF") - self.assertEqual(im.info["version"], b"GIF89a") - @unittest.skipIf(is_pypy(), "Requires CPython") - def test_unclosed_file(self): - def open(): - im = Image.open(TEST_GIF) - im.load() + pytest.warns(None, open) - self.assert_warning(ResourceWarning, open) - def test_closed_file(self): - def open(): - im = Image.open(TEST_GIF) - im.load() - im.close() +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - self.assert_warning(None, open) + with pytest.raises(SyntaxError): + GifImagePlugin.GifImageFile(invalid_file) - def test_context_manager(self): - def open(): - with Image.open(TEST_GIF) as im: - im.load() - self.assert_warning(None, open) +def test_optimize(): + def test_grayscale(optimize): + im = Image.new("L", (1, 1), 0) + filename = BytesIO() + im.save(filename, "GIF", optimize=optimize) + return len(filename.getvalue()) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" + def test_bilevel(optimize): + im = Image.new("1", (1, 1), 0) + test_file = BytesIO() + im.save(test_file, "GIF", optimize=optimize) + return len(test_file.getvalue()) - self.assertRaises(SyntaxError, GifImagePlugin.GifImageFile, invalid_file) + assert test_grayscale(0) == 800 + assert test_grayscale(1) == 44 + assert test_bilevel(0) == 800 + assert test_bilevel(1) == 800 - def test_optimize(self): - def test_grayscale(optimize): - im = Image.new("L", (1, 1), 0) - filename = BytesIO() - im.save(filename, "GIF", optimize=optimize) - return len(filename.getvalue()) - - def test_bilevel(optimize): - im = Image.new("1", (1, 1), 0) - test_file = BytesIO() - im.save(test_file, "GIF", optimize=optimize) - return len(test_file.getvalue()) - - self.assertEqual(test_grayscale(0), 800) - self.assertEqual(test_grayscale(1), 44) - self.assertEqual(test_bilevel(0), 800) - self.assertEqual(test_bilevel(1), 800) - - def test_optimize_correctness(self): - # 256 color Palette image, posterize to > 128 and < 128 levels - # Size bigger and smaller than 512x512 - # Check the palette for number of colors allocated. - # Check for correctness after conversion back to RGB - def check(colors, size, expected_palette_length): - # make an image with empty colors in the start of the palette range - im = Image.frombytes( - "P", (colors, colors), bytes(range(256 - colors, 256)) * colors - ) - im = im.resize((size, size)) - outfile = BytesIO() - im.save(outfile, "GIF") - outfile.seek(0) - reloaded = Image.open(outfile) +def test_optimize_correctness(): + # 256 color Palette image, posterize to > 128 and < 128 levels + # Size bigger and smaller than 512x512 + # Check the palette for number of colors allocated. + # Check for correctness after conversion back to RGB + def check(colors, size, expected_palette_length): + # make an image with empty colors in the start of the palette range + im = Image.frombytes( + "P", (colors, colors), bytes(range(256 - colors, 256)) * colors + ) + im = im.resize((size, size)) + outfile = BytesIO() + im.save(outfile, "GIF") + outfile.seek(0) + with Image.open(outfile) as reloaded: # check palette length palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v) - self.assertEqual(expected_palette_length, palette_length) + assert expected_palette_length == palette_length - self.assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) + assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) - # These do optimize the palette - check(128, 511, 128) - check(64, 511, 64) - check(4, 511, 4) + # These do optimize the palette + check(128, 511, 128) + check(64, 511, 64) + check(4, 511, 4) - # These don't optimize the palette - check(128, 513, 256) - check(64, 513, 256) - check(4, 513, 256) + # These don't optimize the palette + check(128, 513, 256) + check(64, 513, 256) + check(4, 513, 256) - # other limits that don't optimize the palette - check(129, 511, 256) - check(255, 511, 256) - check(256, 511, 256) + # Other limits that don't optimize the palette + check(129, 511, 256) + check(255, 511, 256) + check(256, 511, 256) - def test_optimize_full_l(self): - im = Image.frombytes("L", (16, 16), bytes(range(256))) - test_file = BytesIO() - im.save(test_file, "GIF", optimize=True) - self.assertEqual(im.mode, "L") - def test_roundtrip(self): - out = self.tempfile("temp.gif") - im = hopper() - im.save(out) - with Image.open(out) as reread: +def test_optimize_full_l(): + im = Image.frombytes("L", (16, 16), bytes(range(256))) + test_file = BytesIO() + im.save(test_file, "GIF", optimize=True) + assert im.mode == "L" - self.assert_image_similar(reread.convert("RGB"), im, 50) - def test_roundtrip2(self): - # see https://github.com/python-pillow/Pillow/issues/403 - out = self.tempfile("temp.gif") - with Image.open(TEST_GIF) as im: - im2 = im.copy() - im2.save(out) - with Image.open(out) as reread: +def test_roundtrip(tmp_path): + out = str(tmp_path / "temp.gif") + im = hopper() + im.save(out) + with Image.open(out) as reread: - self.assert_image_similar(reread.convert("RGB"), hopper(), 50) + assert_image_similar(reread.convert("RGB"), im, 50) - def test_roundtrip_save_all(self): - # Single frame image - out = self.tempfile("temp.gif") - im = hopper() + +def test_roundtrip2(tmp_path): + # see https://github.com/python-pillow/Pillow/issues/403 + out = str(tmp_path / "temp.gif") + with Image.open(TEST_GIF) as im: + im2 = im.copy() + im2.save(out) + with Image.open(out) as reread: + + assert_image_similar(reread.convert("RGB"), hopper(), 50) + + +def test_roundtrip_save_all(tmp_path): + # Single frame image + out = str(tmp_path / "temp.gif") + im = hopper() + im.save(out, save_all=True) + with Image.open(out) as reread: + + assert_image_similar(reread.convert("RGB"), im, 50) + + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + out = str(tmp_path / "temp.gif") im.save(out, save_all=True) - with Image.open(out) as reread: - self.assert_image_similar(reread.convert("RGB"), im, 50) + with Image.open(out) as reread: + assert reread.n_frames == 5 - # Multiframe image - with Image.open("Tests/images/dispose_bgnd.gif") as im: - out = self.tempfile("temp.gif") - im.save(out, save_all=True) - with Image.open(out) as reread: +def test_headers_saving_for_animated_gifs(tmp_path): + important_headers = ["background", "version", "duration", "loop"] + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: - self.assertEqual(reread.n_frames, 5) + info = im.info.copy() - def test_headers_saving_for_animated_gifs(self): - important_headers = ["background", "version", "duration", "loop"] - # Multiframe image - with Image.open("Tests/images/dispose_bgnd.gif") as im: + out = str(tmp_path / "temp.gif") + im.save(out, save_all=True) + with Image.open(out) as reread: - info = im.info.copy() + for header in important_headers: + assert info[header] == reread.info[header] - out = self.tempfile("temp.gif") - im.save(out, save_all=True) - with Image.open(out) as reread: - for header in important_headers: - self.assertEqual(info[header], reread.info[header]) +def test_palette_handling(tmp_path): + # see https://github.com/python-pillow/Pillow/issues/513 - def test_palette_handling(self): - # see https://github.com/python-pillow/Pillow/issues/513 + with Image.open(TEST_GIF) as im: + im = im.convert("RGB") - with Image.open(TEST_GIF) as im: - im = im.convert("RGB") + im = im.resize((100, 100), Image.LANCZOS) + im2 = im.convert("P", palette=Image.ADAPTIVE, colors=256) - im = im.resize((100, 100), Image.LANCZOS) - im2 = im.convert("P", palette=Image.ADAPTIVE, colors=256) + f = str(tmp_path / "temp.gif") + im2.save(f, optimize=True) - f = self.tempfile("temp.gif") - im2.save(f, optimize=True) + with Image.open(f) as reloaded: - with Image.open(f) as reloaded: + assert_image_similar(im, reloaded.convert("RGB"), 10) - self.assert_image_similar(im, reloaded.convert("RGB"), 10) - def test_palette_434(self): - # see https://github.com/python-pillow/Pillow/issues/434 +def test_palette_434(tmp_path): + # see https://github.com/python-pillow/Pillow/issues/434 - def roundtrip(im, *args, **kwargs): - out = self.tempfile("temp.gif") - im.copy().save(out, *args, **kwargs) - reloaded = Image.open(out) + def roundtrip(im, *args, **kwargs): + out = str(tmp_path / "temp.gif") + im.copy().save(out, *args, **kwargs) + reloaded = Image.open(out) - return reloaded + return reloaded - orig = "Tests/images/test.colors.gif" - with Image.open(orig) as im: + orig = "Tests/images/test.colors.gif" + with Image.open(orig) as im: - with roundtrip(im) as reloaded: - self.assert_image_similar(im, reloaded, 1) - with roundtrip(im, optimize=True) as reloaded: - self.assert_image_similar(im, reloaded, 1) + with roundtrip(im) as reloaded: + assert_image_similar(im, reloaded, 1) + with roundtrip(im, optimize=True) as reloaded: + assert_image_similar(im, reloaded, 1) - im = im.convert("RGB") - # check automatic P conversion - with roundtrip(im) as reloaded: - reloaded = reloaded.convert("RGB") - self.assert_image_equal(im, reloaded) + im = im.convert("RGB") + # check automatic P conversion + with roundtrip(im) as reloaded: + reloaded = reloaded.convert("RGB") + assert_image_equal(im, reloaded) - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_bmp_mode(self): - with Image.open(TEST_GIF) as img: - img = img.convert("RGB") - tempfile = self.tempfile("temp.gif") - GifImagePlugin._save_netpbm(img, 0, tempfile) - with Image.open(tempfile) as reloaded: - self.assert_image_similar(img, reloaded.convert("RGB"), 0) +@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") +def test_save_netpbm_bmp_mode(tmp_path): + with Image.open(TEST_GIF) as img: + img = img.convert("RGB") - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_l_mode(self): - with Image.open(TEST_GIF) as img: - img = img.convert("L") + tempfile = str(tmp_path / "temp.gif") + GifImagePlugin._save_netpbm(img, 0, tempfile) + with Image.open(tempfile) as reloaded: + assert_image_similar(img, reloaded.convert("RGB"), 0) - tempfile = self.tempfile("temp.gif") - GifImagePlugin._save_netpbm(img, 0, tempfile) - with Image.open(tempfile) as reloaded: - self.assert_image_similar(img, reloaded.convert("L"), 0) - def test_seek(self): - with Image.open("Tests/images/dispose_none.gif") as img: - framecount = 0 - try: - while True: - framecount += 1 - img.seek(img.tell() + 1) - except EOFError: - self.assertEqual(framecount, 5) +@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") +def test_save_netpbm_l_mode(tmp_path): + with Image.open(TEST_GIF) as img: + img = img.convert("L") - def test_seek_info(self): - with Image.open("Tests/images/iss634.gif") as im: - info = im.info.copy() + tempfile = str(tmp_path / "temp.gif") + GifImagePlugin._save_netpbm(img, 0, tempfile) + with Image.open(tempfile) as reloaded: + assert_image_similar(img, reloaded.convert("L"), 0) - im.seek(1) - im.seek(0) - self.assertEqual(im.info, info) - - def test_seek_rewind(self): - with Image.open("Tests/images/iss634.gif") as im: - im.seek(2) - im.seek(1) - - with Image.open("Tests/images/iss634.gif") as expected: - expected.seek(1) - self.assert_image_equal(im, expected) - - def test_n_frames(self): - for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]: - # Test is_animated before n_frames - with Image.open(path) as im: - self.assertEqual(im.is_animated, n_frames != 1) - - # Test is_animated after n_frames - with Image.open(path) as im: - self.assertEqual(im.n_frames, n_frames) - self.assertEqual(im.is_animated, n_frames != 1) - - def test_eoferror(self): - with Image.open(TEST_GIF) as im: - n_frames = im.n_frames - - # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) - - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) - - def test_dispose_none(self): - with Image.open("Tests/images/dispose_none.gif") as img: - try: - while True: - img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, 1) - except EOFError: - pass - - def test_dispose_background(self): - with Image.open("Tests/images/dispose_bgnd.gif") as img: - try: - while True: - img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, 2) - except EOFError: - pass - - def test_dispose_previous(self): - with Image.open("Tests/images/dispose_prev.gif") as img: - try: - while True: - img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, 3) - except EOFError: - pass - - def test_save_dispose(self): - out = self.tempfile("temp.gif") - im_list = [ - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#111"), - Image.new("L", (100, 100), "#222"), - ] - for method in range(0, 4): - im_list[0].save( - out, save_all=True, append_images=im_list[1:], disposal=method - ) - with Image.open(out) as img: - for _ in range(2): - img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, method) - - # check per frame disposal - im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - disposal=tuple(range(len(im_list))), - ) - - with Image.open(out) as img: - - for i in range(2): +def test_seek(): + with Image.open("Tests/images/dispose_none.gif") as img: + frame_count = 0 + try: + while True: + frame_count += 1 img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, i + 1) + except EOFError: + assert frame_count == 5 - def test_dispose2_palette(self): - out = self.tempfile("temp.gif") - # 4 backgrounds: White, Grey, Black, Red - circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] +def test_seek_info(): + with Image.open("Tests/images/iss634.gif") as im: + info = im.info.copy() - im_list = [] - for circle in circles: - img = Image.new("RGB", (100, 100), (255, 0, 0)) + im.seek(1) + im.seek(0) - # Red circle in center of each frame - d = ImageDraw.Draw(img) - d.ellipse([(40, 40), (60, 60)], fill=circle) + assert im.info == info - im_list.append(img) - im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) +def test_seek_rewind(): + with Image.open("Tests/images/iss634.gif") as im: + im.seek(2) + im.seek(1) + with Image.open("Tests/images/iss634.gif") as expected: + expected.seek(1) + assert_image_equal(im, expected) + + +def test_n_frames(): + for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]: + # Test is_animated before n_frames + with Image.open(path) as im: + assert im.is_animated == (n_frames != 1) + + # Test is_animated after n_frames + with Image.open(path) as im: + assert im.n_frames == n_frames + assert im.is_animated == (n_frames != 1) + + +def test_eoferror(): + with Image.open(TEST_GIF) as im: + n_frames = im.n_frames + + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames + + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) + + +def test_dispose_none(): + with Image.open("Tests/images/dispose_none.gif") as img: + try: + while True: + img.seek(img.tell() + 1) + assert img.disposal_method == 1 + except EOFError: + pass + + +def test_dispose_background(): + with Image.open("Tests/images/dispose_bgnd.gif") as img: + try: + while True: + img.seek(img.tell() + 1) + assert img.disposal_method == 2 + except EOFError: + pass + + +def test_dispose_previous(): + with Image.open("Tests/images/dispose_prev.gif") as img: + try: + while True: + img.seek(img.tell() + 1) + assert img.disposal_method == 3 + except EOFError: + pass + + +def test_save_dispose(tmp_path): + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + Image.new("L", (100, 100), "#222"), + ] + for method in range(0, 4): + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) with Image.open(out) as img: - for i, circle in enumerate(circles): - img.seek(i) - rgb_img = img.convert("RGB") + for _ in range(2): + img.seek(img.tell() + 1) + assert img.disposal_method == method - # Check top left pixel matches background - self.assertEqual(rgb_img.getpixel((0, 0)), (255, 0, 0)) + # Check per frame disposal + im_list[0].save( + out, + save_all=True, + append_images=im_list[1:], + disposal=tuple(range(len(im_list))), + ) - # Center remains red every frame - self.assertEqual(rgb_img.getpixel((50, 50)), circle) + with Image.open(out) as img: - def test_dispose2_diff(self): - out = self.tempfile("temp.gif") - - # 4 frames: red/blue, red/red, blue/blue, red/blue - circles = [ - ((255, 0, 0, 255), (0, 0, 255, 255)), - ((255, 0, 0, 255), (255, 0, 0, 255)), - ((0, 0, 255, 255), (0, 0, 255, 255)), - ((255, 0, 0, 255), (0, 0, 255, 255)), - ] - - im_list = [] - for i in range(len(circles)): - # Transparent BG - img = Image.new("RGBA", (100, 100), (255, 255, 255, 0)) - - # Two circles per frame - d = ImageDraw.Draw(img) - d.ellipse([(0, 30), (40, 70)], fill=circles[i][0]) - d.ellipse([(60, 30), (100, 70)], fill=circles[i][1]) - - im_list.append(img) - - im_list[0].save( - out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0 - ) - - with Image.open(out) as img: - for i, colours in enumerate(circles): - img.seek(i) - rgb_img = img.convert("RGBA") - - # Check left circle is correct colour - self.assertEqual(rgb_img.getpixel((20, 50)), colours[0]) - - # Check right circle is correct colour - self.assertEqual(rgb_img.getpixel((80, 50)), colours[1]) - - # Check BG is correct colour - self.assertEqual(rgb_img.getpixel((1, 1)), (255, 255, 255, 0)) - - def test_dispose2_background(self): - out = self.tempfile("temp.gif") - - im_list = [] - - im = Image.new("P", (100, 100)) - d = ImageDraw.Draw(im) - d.rectangle([(50, 0), (100, 100)], fill="#f00") - d.rectangle([(0, 0), (50, 100)], fill="#0f0") - im_list.append(im) - - im = Image.new("P", (100, 100)) - d = ImageDraw.Draw(im) - d.rectangle([(0, 0), (100, 50)], fill="#f00") - d.rectangle([(0, 50), (100, 100)], fill="#0f0") - im_list.append(im) - - im_list[0].save( - out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1 - ) - - with Image.open(out) as im: - im.seek(1) - self.assertEqual(im.getpixel((0, 0)), 0) - - def test_iss634(self): - with Image.open("Tests/images/iss634.gif") as img: - # seek to the second frame + for i in range(2): img.seek(img.tell() + 1) - # all transparent pixels should be replaced with the color from the - # first frame - self.assertEqual(img.histogram()[img.info["transparency"]], 0) + assert img.disposal_method == i + 1 - def test_duration(self): - duration = 1000 - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") +def test_dispose2_palette(tmp_path): + out = str(tmp_path / "temp.gif") - # Check that the argument has priority over the info settings - im.info["duration"] = 100 - im.save(out, duration=duration) + # 4 backgrounds: White, Grey, Black, Red + circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] - with Image.open(out) as reread: - self.assertEqual(reread.info["duration"], duration) + im_list = [] + for circle in circles: + img = Image.new("RGB", (100, 100), (255, 0, 0)) - def test_multiple_duration(self): - duration_list = [1000, 2000, 3000] + # Red circle in center of each frame + d = ImageDraw.Draw(img) + d.ellipse([(40, 40), (60, 60)], fill=circle) - out = self.tempfile("temp.gif") - im_list = [ - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#111"), - Image.new("L", (100, 100), "#222"), - ] + im_list.append(img) - # duration as list - im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=duration_list - ) - with Image.open(out) as reread: + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) - for duration in duration_list: - self.assertEqual(reread.info["duration"], duration) - try: - reread.seek(reread.tell() + 1) - except EOFError: - pass + with Image.open(out) as img: + for i, circle in enumerate(circles): + img.seek(i) + rgb_img = img.convert("RGB") - # duration as tuple - im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) - ) - with Image.open(out) as reread: + # Check top left pixel matches background + assert rgb_img.getpixel((0, 0)) == (255, 0, 0) - for duration in duration_list: - self.assertEqual(reread.info["duration"], duration) - try: - reread.seek(reread.tell() + 1) - except EOFError: - pass + # Center remains red every frame + assert rgb_img.getpixel((50, 50)) == circle - def test_identical_frames(self): - duration_list = [1000, 1500, 2000, 4000] - out = self.tempfile("temp.gif") +def test_dispose2_diff(tmp_path): + out = str(tmp_path / "temp.gif") + + # 4 frames: red/blue, red/red, blue/blue, red/blue + circles = [ + ((255, 0, 0, 255), (0, 0, 255, 255)), + ((255, 0, 0, 255), (255, 0, 0, 255)), + ((0, 0, 255, 255), (0, 0, 255, 255)), + ((255, 0, 0, 255), (0, 0, 255, 255)), + ] + + im_list = [] + for i in range(len(circles)): + # Transparent BG + img = Image.new("RGBA", (100, 100), (255, 255, 255, 0)) + + # Two circles per frame + d = ImageDraw.Draw(img) + d.ellipse([(0, 30), (40, 70)], fill=circles[i][0]) + d.ellipse([(60, 30), (100, 70)], fill=circles[i][1]) + + im_list.append(img) + + im_list[0].save( + out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0 + ) + + with Image.open(out) as img: + for i, colours in enumerate(circles): + img.seek(i) + rgb_img = img.convert("RGBA") + + # Check left circle is correct colour + assert rgb_img.getpixel((20, 50)) == colours[0] + + # Check right circle is correct colour + assert rgb_img.getpixel((80, 50)) == colours[1] + + # Check BG is correct colour + assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0) + + +def test_dispose2_background(tmp_path): + out = str(tmp_path / "temp.gif") + + im_list = [] + + im = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im) + d.rectangle([(50, 0), (100, 100)], fill="#f00") + d.rectangle([(0, 0), (50, 100)], fill="#0f0") + im_list.append(im) + + im = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im) + d.rectangle([(0, 0), (100, 50)], fill="#f00") + d.rectangle([(0, 50), (100, 100)], fill="#0f0") + im_list.append(im) + + im_list[0].save( + out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1 + ) + + with Image.open(out) as im: + im.seek(1) + assert im.getpixel((0, 0)) == 0 + + +def test_iss634(): + with Image.open("Tests/images/iss634.gif") as img: + # Seek to the second frame + img.seek(img.tell() + 1) + # All transparent pixels should be replaced with the color from the first frame + assert img.histogram()[img.info["transparency"]] == 0 + + +def test_duration(tmp_path): + duration = 1000 + + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + + # Check that the argument has priority over the info settings + im.info["duration"] = 100 + im.save(out, duration=duration) + + with Image.open(out) as reread: + assert reread.info["duration"] == duration + + +def test_multiple_duration(tmp_path): + duration_list = [1000, 2000, 3000] + + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + Image.new("L", (100, 100), "#222"), + ] + + # Duration as list + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=duration_list + ) + with Image.open(out) as reread: + + for duration in duration_list: + assert reread.info["duration"] == duration + try: + reread.seek(reread.tell() + 1) + except EOFError: + pass + + # Duration as tuple + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) + ) + with Image.open(out) as reread: + + for duration in duration_list: + assert reread.info["duration"] == duration + try: + reread.seek(reread.tell() + 1) + except EOFError: + pass + + +def test_identical_frames(tmp_path): + duration_list = [1000, 1500, 2000, 4000] + + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + ] + + # Duration as list + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=duration_list + ) + with Image.open(out) as reread: + + # Assert that the first three frames were combined + assert reread.n_frames == 2 + + # Assert that the new duration is the total of the identical frames + assert reread.info["duration"] == 4500 + + +def test_identical_frames_to_single_frame(tmp_path): + for duration in ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500): + out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#111"), ] - # duration as list im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=duration_list + out, save_all=True, append_images=im_list[1:], duration=duration ) with Image.open(out) as reread: - - # Assert that the first three frames were combined - self.assertEqual(reread.n_frames, 2) + # Assert that all frames were combined + assert reread.n_frames == 1 # Assert that the new duration is the total of the identical frames - self.assertEqual(reread.info["duration"], 4500) + assert reread.info["duration"] == 8500 - def test_identical_frames_to_single_frame(self): - for duration in ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500): - out = self.tempfile("temp.gif") - im_list = [ - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#000"), - ] - im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=duration - ) - with Image.open(out) as reread: - # Assert that all frames were combined - self.assertEqual(reread.n_frames, 1) +def test_number_of_loops(tmp_path): + number_of_loops = 2 - # Assert that the new duration is the total of the identical frames - self.assertEqual(reread.info["duration"], 8500) + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.save(out, loop=number_of_loops) + with Image.open(out) as reread: - def test_number_of_loops(self): - number_of_loops = 2 + assert reread.info["loop"] == number_of_loops - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") - im.save(out, loop=number_of_loops) - with Image.open(out) as reread: - self.assertEqual(reread.info["loop"], number_of_loops) +def test_background(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.info["background"] = 1 + im.save(out) + with Image.open(out) as reread: - def test_background(self): - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") - im.info["background"] = 1 + assert reread.info["background"] == im.info["background"] + + if features.check("webp") and features.check("webp_anim"): + with Image.open("Tests/images/hopper.webp") as im: + assert isinstance(im.info["background"], tuple) + im.save(out) + + +def test_comment(tmp_path): + with Image.open(TEST_GIF) as im: + assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" + + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.info["comment"] = b"Test comment text" + im.save(out) + with Image.open(out) as reread: + assert reread.info["comment"] == im.info["comment"] + + im.info["comment"] = "Test comment text" + im.save(out) + with Image.open(out) as reread: + assert reread.info["comment"] == im.info["comment"].encode() + + +def test_comment_over_255(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + comment = b"Test comment text" + while len(comment) < 256: + comment += comment + im.info["comment"] = comment + im.save(out) + with Image.open(out) as reread: + + assert reread.info["comment"] == comment + + +def test_zero_comment_subblocks(): + with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: + with Image.open(TEST_GIF) as expected: + assert_image_equal(im, expected) + + +def test_version(tmp_path): + out = str(tmp_path / "temp.gif") + + def assertVersionAfterSave(im, version): im.save(out) with Image.open(out) as reread: + assert reread.info["version"] == version - self.assertEqual(reread.info["background"], im.info["background"]) + # Test that GIF87a is used by default + im = Image.new("L", (100, 100), "#000") + assertVersionAfterSave(im, b"GIF87a") - if HAVE_WEBP and _webp.HAVE_WEBPANIM: - im = Image.open("Tests/images/hopper.webp") - self.assertIsInstance(im.info["background"], tuple) - im.save(out) + # Test setting the version to 89a + im = Image.new("L", (100, 100), "#000") + im.info["version"] = b"89a" + assertVersionAfterSave(im, b"GIF89a") - def test_comment(self): - with Image.open(TEST_GIF) as im: - self.assertEqual( - im.info["comment"], b"File written by Adobe Photoshop\xa8 4.0" - ) + # Test that adding a GIF89a feature changes the version + im.info["transparency"] = 1 + assertVersionAfterSave(im, b"GIF89a") - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") - im.info["comment"] = b"Test comment text" - im.save(out) - with Image.open(out) as reread: - - self.assertEqual(reread.info["comment"], im.info["comment"]) - - def test_comment_over_255(self): - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") - comment = b"Test comment text" - while len(comment) < 256: - comment += comment - im.info["comment"] = comment - im.save(out) - with Image.open(out) as reread: - - self.assertEqual(reread.info["comment"], comment) - - def test_zero_comment_subblocks(self): - with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: - with Image.open(TEST_GIF) as expected: - self.assert_image_equal(im, expected) - - def test_version(self): - out = self.tempfile("temp.gif") - - def assertVersionAfterSave(im, version): - im.save(out) - with Image.open(out) as reread: - self.assertEqual(reread.info["version"], version) - - # Test that GIF87a is used by default - im = Image.new("L", (100, 100), "#000") + # Test that a GIF87a image is also saved in that format + with Image.open("Tests/images/test.colors.gif") as im: assertVersionAfterSave(im, b"GIF87a") - # Test setting the version to 89a - im = Image.new("L", (100, 100), "#000") - im.info["version"] = b"89a" - assertVersionAfterSave(im, b"GIF89a") + # Test that a GIF89a image is also saved in that format + im.info["version"] = b"GIF89a" + assertVersionAfterSave(im, b"GIF87a") - # Test that adding a GIF89a feature changes the version - im.info["transparency"] = 1 - assertVersionAfterSave(im, b"GIF89a") - # Test that a GIF87a image is also saved in that format - with Image.open("Tests/images/test.colors.gif") as im: - assertVersionAfterSave(im, b"GIF87a") +def test_append_images(tmp_path): + out = str(tmp_path / "temp.gif") - # Test that a GIF89a image is also saved in that format - im.info["version"] = b"GIF89a" - assertVersionAfterSave(im, b"GIF87a") + # Test appending single frame images + im = Image.new("RGB", (100, 100), "#f00") + ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] + im.copy().save(out, save_all=True, append_images=ims) - def test_append_images(self): - out = self.tempfile("temp.gif") + with Image.open(out) as reread: + assert reread.n_frames == 3 - # Test appending single frame images - im = Image.new("RGB", (100, 100), "#f00") - ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] - im.copy().save(out, save_all=True, append_images=ims) + # Tests appending using a generator + def imGenerator(ims): + yield from ims - with Image.open(out) as reread: - self.assertEqual(reread.n_frames, 3) + im.save(out, save_all=True, append_images=imGenerator(ims)) - # Tests appending using a generator - def imGenerator(ims): - yield from ims + with Image.open(out) as reread: + assert reread.n_frames == 3 - im.save(out, save_all=True, append_images=imGenerator(ims)) + # Tests appending single and multiple frame images + with Image.open("Tests/images/dispose_none.gif") as im: + with Image.open("Tests/images/dispose_prev.gif") as im2: + im.save(out, save_all=True, append_images=[im2]) - with Image.open(out) as reread: - self.assertEqual(reread.n_frames, 3) + with Image.open(out) as reread: + assert reread.n_frames == 10 - # Tests appending single and multiple frame images - with Image.open("Tests/images/dispose_none.gif") as im: - with Image.open("Tests/images/dispose_prev.gif") as im2: - im.save(out, save_all=True, append_images=[im2]) - with Image.open(out) as reread: - self.assertEqual(reread.n_frames, 10) +def test_transparent_optimize(tmp_path): + # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses + # transparency. + # Need a palette that isn't using the 0 color, and one that's > 128 items where the + # transparent color is actually the top palette entry to trigger the bug. - def test_transparent_optimize(self): - # from issue #2195, if the transparent color is incorrectly - # optimized out, gif loses transparency - # Need a palette that isn't using the 0 color, and one - # that's > 128 items where the transparent color is actually - # the top palette entry to trigger the bug. + data = bytes(range(1, 254)) + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - data = bytes(range(1, 254)) - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + im = Image.new("L", (253, 1)) + im.frombytes(data) + im.putpalette(palette) - im = Image.new("L", (253, 1)) - im.frombytes(data) + out = str(tmp_path / "temp.gif") + im.save(out, transparency=253) + with Image.open(out) as reloaded: + + assert reloaded.info["transparency"] == 253 + + +def test_rgb_transparency(tmp_path): + out = str(tmp_path / "temp.gif") + + # Single frame + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = (255, 0, 0) + pytest.warns(UserWarning, im.save, out) + + with Image.open(out) as reloaded: + assert "transparency" not in reloaded.info + + # Multiple frames + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = b"" + ims = [Image.new("RGB", (1, 1))] + pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims) + + with Image.open(out) as reloaded: + assert "transparency" not in reloaded.info + + +def test_bbox(tmp_path): + out = str(tmp_path / "temp.gif") + + im = Image.new("RGB", (100, 100), "#fff") + ims = [Image.new("RGB", (100, 100), "#000")] + im.save(out, save_all=True, append_images=ims) + + with Image.open(out) as reread: + assert reread.n_frames == 2 + + +def test_palette_save_L(tmp_path): + # Generate an L mode image with a separate palette + + im = hopper("P") + im_l = Image.frombytes("L", im.size, im.tobytes()) + palette = bytes(im.getpalette()) + + out = str(tmp_path / "temp.gif") + im_l.save(out, palette=palette) + + with Image.open(out) as reloaded: + assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) + + +def test_palette_save_P(tmp_path): + # Pass in a different palette, then construct what the image would look like. + # Forcing a non-straight grayscale palette. + + im = hopper("P") + palette = bytes([255 - i // 3 for i in range(768)]) + + out = str(tmp_path / "temp.gif") + im.save(out, palette=palette) + + with Image.open(out) as reloaded: im.putpalette(palette) + assert_image_equal(reloaded, im) - out = self.tempfile("temp.gif") - im.save(out, transparency=253) - with Image.open(out) as reloaded: - self.assertEqual(reloaded.info["transparency"], 253) +def test_palette_save_ImagePalette(tmp_path): + # Pass in a different palette, as an ImagePalette.ImagePalette + # effectively the same as test_palette_save_P - def test_rgb_transparency(self): - out = self.tempfile("temp.gif") + im = hopper("P") + palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) - # Single frame - im = Image.new("RGB", (1, 1)) - im.info["transparency"] = (255, 0, 0) - self.assert_warning(UserWarning, im.save, out) + out = str(tmp_path / "temp.gif") + im.save(out, palette=palette) - with Image.open(out) as reloaded: - self.assertNotIn("transparency", reloaded.info) + with Image.open(out) as reloaded: + im.putpalette(palette) + assert_image_equal(reloaded, im) - # Multiple frames - im = Image.new("RGB", (1, 1)) - im.info["transparency"] = b"" - ims = [Image.new("RGB", (1, 1))] - self.assert_warning(UserWarning, im.save, out, save_all=True, append_images=ims) - with Image.open(out) as reloaded: - self.assertNotIn("transparency", reloaded.info) +def test_save_I(tmp_path): + # Test saving something that would trigger the auto-convert to 'L' - def test_bbox(self): - out = self.tempfile("temp.gif") + im = hopper("I") - im = Image.new("RGB", (100, 100), "#fff") - ims = [Image.new("RGB", (100, 100), "#000")] - im.save(out, save_all=True, append_images=ims) + out = str(tmp_path / "temp.gif") + im.save(out) - with Image.open(out) as reread: - self.assertEqual(reread.n_frames, 2) + with Image.open(out) as reloaded: + assert_image_equal(reloaded.convert("L"), im.convert("L")) - def test_palette_save_L(self): - # generate an L mode image with a separate palette - im = hopper("P") - im_l = Image.frombytes("L", im.size, im.tobytes()) - palette = bytes(im.getpalette()) +def test_getdata(): + # Test getheader/getdata against legacy values. + # Create a 'P' image with holes in the palette. + im = Image._wedge().resize((16, 16), Image.NEAREST) + im.putpalette(ImagePalette.ImagePalette("RGB")) + im.info = {"background": 0} - out = self.tempfile("temp.gif") - im_l.save(out, palette=palette) + passed_palette = bytes([255 - i // 3 for i in range(768)]) - with Image.open(out) as reloaded: + GifImagePlugin._FORCE_OPTIMIZE = True + try: + h = GifImagePlugin.getheader(im, passed_palette) + d = GifImagePlugin.getdata(im) - self.assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) + import pickle - def test_palette_save_P(self): - # pass in a different palette, then construct what the image - # would look like. - # Forcing a non-straight grayscale palette. + # Enable to get target values on pre-refactor version + # with open('Tests/images/gif_header_data.pkl', 'wb') as f: + # pickle.dump((h, d), f, 1) + with open("Tests/images/gif_header_data.pkl", "rb") as f: + (h_target, d_target) = pickle.load(f) - im = hopper("P") - palette = bytes([255 - i // 3 for i in range(768)]) + assert h == h_target + assert d == d_target + finally: + GifImagePlugin._FORCE_OPTIMIZE = False - out = self.tempfile("temp.gif") - im.save(out, palette=palette) - with Image.open(out) as reloaded: - im.putpalette(palette) - self.assert_image_equal(reloaded, im) +def test_lzw_bits(): + # 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 + # codec error prepatch + im.load() - def test_palette_save_ImagePalette(self): - # pass in a different palette, as an ImagePalette.ImagePalette - # effectively the same as test_palette_save_P - im = hopper("P") - palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) - - out = self.tempfile("temp.gif") - im.save(out, palette=palette) - - with Image.open(out) as reloaded: - im.putpalette(palette) - self.assert_image_equal(reloaded, im) - - def test_save_I(self): - # Test saving something that would trigger the auto-convert to 'L' - - im = hopper("I") - - out = self.tempfile("temp.gif") - im.save(out) - - with Image.open(out) as reloaded: - self.assert_image_equal(reloaded.convert("L"), im.convert("L")) - - def test_getdata(self): - # test getheader/getdata against legacy values - # Create a 'P' image with holes in the palette - im = Image._wedge().resize((16, 16)) - im.putpalette(ImagePalette.ImagePalette("RGB")) - im.info = {"background": 0} - - passed_palette = bytes([255 - i // 3 for i in range(768)]) - - GifImagePlugin._FORCE_OPTIMIZE = True - try: - h = GifImagePlugin.getheader(im, passed_palette) - d = GifImagePlugin.getdata(im) - - import pickle - - # Enable to get target values on pre-refactor version - # with open('Tests/images/gif_header_data.pkl', 'wb') as f: - # pickle.dump((h, d), f, 1) - with open("Tests/images/gif_header_data.pkl", "rb") as f: - (h_target, d_target) = pickle.load(f) - - self.assertEqual(h, h_target) - self.assertEqual(d, d_target) - finally: - GifImagePlugin._FORCE_OPTIMIZE = False - - def test_lzw_bits(self): - # see https://github.com/python-pillow/Pillow/issues/2811 - with Image.open("Tests/images/issue_2811.gif") as im: - self.assertEqual(im.tile[0][3][0], 11) # LZW bits - # codec error prepatch - im.load() - - def test_extents(self): - with Image.open("Tests/images/test_extents.gif") as im: - self.assertEqual(im.size, (100, 100)) - im.seek(1) - self.assertEqual(im.size, (150, 150)) +def test_extents(): + with Image.open("Tests/images/test_extents.gif") as im: + assert im.size == (100, 100) + im.seek(1) + assert im.size == (150, 150) diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index bafee79a3..3f056fdae 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -1,122 +1,124 @@ -from PIL import GimpGradientFile - -from .helper import PillowTestCase +from PIL import GimpGradientFile, ImagePalette -class TestImage(PillowTestCase): - def test_linear_pos_le_middle(self): - # Arrange - middle = 0.5 - pos = 0.25 +def test_linear_pos_le_middle(): + # Arrange + middle = 0.5 + pos = 0.25 - # Act - ret = GimpGradientFile.linear(middle, pos) + # Act + ret = GimpGradientFile.linear(middle, pos) - # Assert - self.assertEqual(ret, 0.25) + # Assert + assert ret == 0.25 - def test_linear_pos_le_small_middle(self): - # Arrange - middle = 1e-11 - pos = 1e-12 - # Act - ret = GimpGradientFile.linear(middle, pos) +def test_linear_pos_le_small_middle(): + # Arrange + middle = 1e-11 + pos = 1e-12 - # Assert - self.assertEqual(ret, 0.0) + # Act + ret = GimpGradientFile.linear(middle, pos) - def test_linear_pos_gt_middle(self): - # Arrange - middle = 0.5 - pos = 0.75 + # Assert + assert ret == 0.0 - # Act - ret = GimpGradientFile.linear(middle, pos) - # Assert - self.assertEqual(ret, 0.75) +def test_linear_pos_gt_middle(): + # Arrange + middle = 0.5 + pos = 0.75 - def test_linear_pos_gt_small_middle(self): - # Arrange - middle = 1 - 1e-11 - pos = 1 - 1e-12 + # Act + ret = GimpGradientFile.linear(middle, pos) - # Act - ret = GimpGradientFile.linear(middle, pos) + # Assert + assert ret == 0.75 - # Assert - self.assertEqual(ret, 1.0) - def test_curved(self): - # Arrange - middle = 0.5 - pos = 0.75 +def test_linear_pos_gt_small_middle(): + # Arrange + middle = 1 - 1e-11 + pos = 1 - 1e-12 - # Act - ret = GimpGradientFile.curved(middle, pos) + # Act + ret = GimpGradientFile.linear(middle, pos) - # Assert - self.assertEqual(ret, 0.75) + # Assert + assert ret == 1.0 - def test_sine(self): - # Arrange - middle = 0.5 - pos = 0.75 - # Act - ret = GimpGradientFile.sine(middle, pos) +def test_curved(): + # Arrange + middle = 0.5 + pos = 0.75 - # Assert - self.assertEqual(ret, 0.8535533905932737) + # Act + ret = GimpGradientFile.curved(middle, pos) - def test_sphere_increasing(self): - # Arrange - middle = 0.5 - pos = 0.75 + # Assert + assert ret == 0.75 - # Act - ret = GimpGradientFile.sphere_increasing(middle, pos) - # Assert - self.assertAlmostEqual(ret, 0.9682458365518543) +def test_sine(): + # Arrange + middle = 0.5 + pos = 0.75 - def test_sphere_decreasing(self): - # Arrange - middle = 0.5 - pos = 0.75 + # Act + ret = GimpGradientFile.sine(middle, pos) - # Act - ret = GimpGradientFile.sphere_decreasing(middle, pos) + # Assert + assert ret == 0.8535533905932737 - # Assert - self.assertEqual(ret, 0.3385621722338523) - def test_load_via_imagepalette(self): - # Arrange - from PIL import ImagePalette +def test_sphere_increasing(): + # Arrange + middle = 0.5 + pos = 0.75 - test_file = "Tests/images/gimp_gradient.ggr" + # Act + ret = GimpGradientFile.sphere_increasing(middle, pos) - # Act - palette = ImagePalette.load(test_file) + # Assert + assert round(abs(ret - 0.9682458365518543), 7) == 0 - # Assert - # load returns raw palette information - self.assertEqual(len(palette[0]), 1024) - self.assertEqual(palette[1], "RGBA") - def test_load_1_3_via_imagepalette(self): - # Arrange - from PIL import ImagePalette +def test_sphere_decreasing(): + # Arrange + middle = 0.5 + pos = 0.75 - # GIMP 1.3 gradient files contain a name field - test_file = "Tests/images/gimp_gradient_with_name.ggr" + # Act + ret = GimpGradientFile.sphere_decreasing(middle, pos) - # Act - palette = ImagePalette.load(test_file) + # Assert + assert ret == 0.3385621722338523 - # Assert - # load returns raw palette information - self.assertEqual(len(palette[0]), 1024) - self.assertEqual(palette[1], "RGBA") + +def test_load_via_imagepalette(): + # Arrange + test_file = "Tests/images/gimp_gradient.ggr" + + # Act + palette = ImagePalette.load(test_file) + + # Assert + # load returns raw palette information + assert len(palette[0]) == 1024 + assert palette[1] == "RGBA" + + +def test_load_1_3_via_imagepalette(): + # Arrange + # GIMP 1.3 gradient files contain a name field + test_file = "Tests/images/gimp_gradient_with_name.ggr" + + # Act + palette = ImagePalette.load(test_file) + + # Assert + # load returns raw palette information + assert len(palette[0]) == 1024 + assert palette[1] == "RGBA" diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index a1677f0cb..caec9cf21 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,29 +1,32 @@ +import pytest + from PIL.GimpPaletteFile import GimpPaletteFile -from .helper import PillowTestCase +def test_sanity(): + with open("Tests/images/test.gpl", "rb") as fp: + GimpPaletteFile(fp) -class TestImage(PillowTestCase): - def test_sanity(self): - with open("Tests/images/test.gpl", "rb") as fp: + with open("Tests/images/hopper.jpg", "rb") as fp: + with pytest.raises(SyntaxError): GimpPaletteFile(fp) - with open("Tests/images/hopper.jpg", "rb") as fp: - self.assertRaises(SyntaxError, GimpPaletteFile, fp) + with open("Tests/images/bad_palette_file.gpl", "rb") as fp: + with pytest.raises(SyntaxError): + GimpPaletteFile(fp) - with open("Tests/images/bad_palette_file.gpl", "rb") as fp: - self.assertRaises(SyntaxError, GimpPaletteFile, fp) + with open("Tests/images/bad_palette_entry.gpl", "rb") as fp: + with pytest.raises(ValueError): + GimpPaletteFile(fp) - with open("Tests/images/bad_palette_entry.gpl", "rb") as fp: - self.assertRaises(ValueError, GimpPaletteFile, fp) - def test_get_palette(self): - # Arrange - with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: - palette_file = GimpPaletteFile(fp) +def test_get_palette(): + # Arrange + with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: + palette_file = GimpPaletteFile(fp) - # Act - palette, mode = palette_file.getpalette() + # Act + palette, mode = palette_file.getpalette() - # Assert - self.assertEqual(mode, "RGB") + # Assert + assert mode == "RGB" diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 92d112ced..e4930d8dc 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,42 +1,47 @@ +import pytest + from PIL import GribStubImagePlugin, Image -from .helper import PillowTestCase, hopper +from .helper import hopper TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" -class TestFileGribStub(PillowTestCase): - def test_open(self): - # Act - with Image.open(TEST_FILE) as im: +def test_open(): + # Act + with Image.open(TEST_FILE) as im: - # Assert - self.assertEqual(im.format, "GRIB") + # Assert + assert im.format == "GRIB" - # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + # Dummy data from the stub + assert im.mode == "F" + assert im.size == (1, 1) - def test_invalid_file(self): - # Arrange - invalid_file = "Tests/images/flower.jpg" - # Act / Assert - self.assertRaises( - SyntaxError, GribStubImagePlugin.GribStubImageFile, invalid_file - ) +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" - def test_load(self): - # Arrange - with Image.open(TEST_FILE) as im: + # Act / Assert + with pytest.raises(SyntaxError): + GribStubImagePlugin.GribStubImageFile(invalid_file) - # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) - def test_save(self): - # Arrange - im = hopper() - tmpfile = self.tempfile("temp.grib") +def test_load(): + # Arrange + with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot save without an implemented handler - self.assertRaises(IOError, im.save, tmpfile) + # Act / Assert: stub cannot load without an implemented handler + with pytest.raises(OSError): + im.load() + + +def test_save(tmp_path): + # Arrange + im = hopper() + tmpfile = str(tmp_path / "temp.grib") + + # Act / Assert: stub cannot save without an implemented handler + with pytest.raises(OSError): + im.save(tmpfile) diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index cdaad0cf7..ff3397055 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,46 +1,48 @@ -from PIL import Hdf5StubImagePlugin, Image +import pytest -from .helper import PillowTestCase +from PIL import Hdf5StubImagePlugin, Image TEST_FILE = "Tests/images/hdf5.h5" -class TestFileHdf5Stub(PillowTestCase): - def test_open(self): - # Act - with Image.open(TEST_FILE) as im: +def test_open(): + # Act + with Image.open(TEST_FILE) as im: - # Assert - self.assertEqual(im.format, "HDF5") + # Assert + assert im.format == "HDF5" - # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + # Dummy data from the stub + assert im.mode == "F" + assert im.size == (1, 1) - def test_invalid_file(self): - # Arrange - invalid_file = "Tests/images/flower.jpg" - # Act / Assert - self.assertRaises( - SyntaxError, Hdf5StubImagePlugin.HDF5StubImageFile, invalid_file - ) +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" - def test_load(self): - # Arrange - with Image.open(TEST_FILE) as im: + # Act / Assert + with pytest.raises(SyntaxError): + Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file) - # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) - def test_save(self): - # Arrange - with Image.open(TEST_FILE) as im: - dummy_fp = None - dummy_filename = "dummy.filename" +def test_load(): + # Arrange + with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot save without an implemented handler - self.assertRaises(IOError, im.save, dummy_filename) - self.assertRaises( - IOError, Hdf5StubImagePlugin._save, im, dummy_fp, dummy_filename - ) + # Act / Assert: stub cannot load without an implemented handler + with pytest.raises(OSError): + im.load() + + +def test_save(): + # Arrange + with Image.open(TEST_FILE) as im: + dummy_fp = None + dummy_filename = "dummy.filename" + + # Act / Assert: stub cannot save without an implemented handler + with pytest.raises(OSError): + im.save(dummy_filename) + with pytest.raises(OSError): + Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index e258601ce..a3d502d42 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,121 +1,141 @@ import io import sys -import unittest -from PIL import IcnsImagePlugin, Image +import pytest -from .helper import PillowTestCase +from PIL import IcnsImagePlugin, Image, features + +from .helper import assert_image_equal, assert_image_similar # sample icon file TEST_FILE = "Tests/images/pillow.icns" -enable_jpeg2k = hasattr(Image.core, "jp2klib_version") +ENABLE_JPEG2K = features.check_codec("jpg_2000") -class TestFileIcns(PillowTestCase): - def test_sanity(self): - # Loading this icon by default should result in the largest size - # (512x512@2x) being loaded - with Image.open(TEST_FILE) as im: +def test_sanity(): + # Loading this icon by default should result in the largest size + # (512x512@2x) being loaded + with Image.open(TEST_FILE) as im: - # Assert that there is no unclosed file warning - self.assert_warning(None, im.load) + # Assert that there is no unclosed file warning + pytest.warns(None, im.load) - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (1024, 1024)) - self.assertEqual(im.format, "ICNS") + assert im.mode == "RGBA" + assert im.size == (1024, 1024) + assert im.format == "ICNS" - @unittest.skipIf(sys.platform != "darwin", "requires macOS") - def test_save(self): - im = Image.open(TEST_FILE) - temp_file = self.tempfile("temp.icns") +@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS") +def test_save(tmp_path): + temp_file = str(tmp_path / "temp.icns") + + with Image.open(TEST_FILE) as im: im.save(temp_file) - reread = Image.open(temp_file) + with Image.open(temp_file) as reread: + assert reread.mode == "RGBA" + assert reread.size == (1024, 1024) + assert reread.format == "ICNS" - self.assertEqual(reread.mode, "RGBA") - self.assertEqual(reread.size, (1024, 1024)) - self.assertEqual(reread.format, "ICNS") - @unittest.skipIf(sys.platform != "darwin", "requires macOS") - def test_save_append_images(self): - im = Image.open(TEST_FILE) +@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS") +def test_save_append_images(tmp_path): + temp_file = str(tmp_path / "temp.icns") + provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) - temp_file = self.tempfile("temp.icns") - provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) + with Image.open(TEST_FILE) as im: im.save(temp_file, append_images=[provided_im]) - reread = Image.open(temp_file) - self.assert_image_similar(reread, im, 1) + with Image.open(temp_file) as reread: + assert_image_similar(reread, im, 1) - reread = Image.open(temp_file) - reread.size = (16, 16, 2) - reread.load() - self.assert_image_equal(reread, provided_im) + with Image.open(temp_file) as reread: + reread.size = (16, 16, 2) + reread.load() + assert_image_equal(reread, provided_im) - def test_sizes(self): - # 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: - for w, h, r in im.info["sizes"]: - wr = w * r - hr = h * r - im.size = (w, h, r) - im.load() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (wr, hr)) - # Check that we cannot load an incorrect size - with self.assertRaises(ValueError): - im.size = (1, 1) +@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS") +def test_save_fp(): + fp = io.BytesIO() - def test_older_icon(self): - # 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: - for w, h, r in im.info["sizes"]: - wr = w * r - hr = h * r - with Image.open("Tests/images/pillow2.icns") as im2: - im2.size = (w, h, r) - im2.load() - self.assertEqual(im2.mode, "RGBA") - self.assertEqual(im2.size, (wr, hr)) + with Image.open(TEST_FILE) as im: + im.save(fp, format="ICNS") - def test_jp2_icon(self): - # This icon was made by using Uli Kusterer's oldiconutil to replace - # the PNG images with JPEG 2000 ones. The advantage of doing this is - # that OS X 10.5 supports JPEG 2000 but not PNG; some commercial - # software therefore does just this. + with Image.open(fp) as reread: + assert reread.mode == "RGBA" + assert reread.size == (1024, 1024) + assert reread.format == "ICNS" - # (oldiconutil is here: https://github.com/uliwitness/oldiconutil) - if not enable_jpeg2k: - return +def test_sizes(): + # 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: + for w, h, r in im.info["sizes"]: + wr = w * r + hr = h * r + im.size = (w, h, r) + im.load() + assert im.mode == "RGBA" + assert im.size == (wr, hr) - with Image.open("Tests/images/pillow3.icns") as im: - for w, h, r in im.info["sizes"]: - wr = w * r - hr = h * r - with Image.open("Tests/images/pillow3.icns") as im2: - im2.size = (w, h, r) - im2.load() - self.assertEqual(im2.mode, "RGBA") - self.assertEqual(im2.size, (wr, hr)) + # Check that we cannot load an incorrect size + with pytest.raises(ValueError): + im.size = (1, 1) - def test_getimage(self): - with open(TEST_FILE, "rb") as fp: - icns_file = IcnsImagePlugin.IcnsFile(fp) - im = icns_file.getimage() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (1024, 1024)) +def test_older_icon(): + # 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: + for w, h, r in im.info["sizes"]: + wr = w * r + hr = h * r + with Image.open("Tests/images/pillow2.icns") as im2: + im2.size = (w, h, r) + im2.load() + assert im2.mode == "RGBA" + assert im2.size == (wr, hr) - im = icns_file.getimage((512, 512)) - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (512, 512)) - def test_not_an_icns_file(self): - with io.BytesIO(b"invalid\n") as fp: - self.assertRaises(SyntaxError, IcnsImagePlugin.IcnsFile, fp) +def test_jp2_icon(): + # This icon was made by using Uli Kusterer's oldiconutil to replace + # the PNG images with JPEG 2000 ones. The advantage of doing this is + # that OS X 10.5 supports JPEG 2000 but not PNG; some commercial + # software therefore does just this. + + # (oldiconutil is here: https://github.com/uliwitness/oldiconutil) + + if not ENABLE_JPEG2K: + return + + with Image.open("Tests/images/pillow3.icns") as im: + for w, h, r in im.info["sizes"]: + wr = w * r + hr = h * r + with Image.open("Tests/images/pillow3.icns") as im2: + im2.size = (w, h, r) + im2.load() + assert im2.mode == "RGBA" + assert im2.size == (wr, hr) + + +def test_getimage(): + with open(TEST_FILE, "rb") as fp: + icns_file = IcnsImagePlugin.IcnsFile(fp) + + im = icns_file.getimage() + assert im.mode == "RGBA" + assert im.size == (1024, 1024) + + im = icns_file.getimage((512, 512)) + assert im.mode == "RGBA" + assert im.size == (512, 512) + + +def test_not_an_icns_file(): + with io.BytesIO(b"invalid\n") as fp: + with pytest.raises(SyntaxError): + IcnsImagePlugin.IcnsFile(fp) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index ac6b19041..f7a513509 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,103 +1,110 @@ import io +import pytest + from PIL import IcoImagePlugin, Image, ImageDraw -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper TEST_ICO_FILE = "Tests/images/hopper.ico" -class TestFileIco(PillowTestCase): - def test_sanity(self): - with Image.open(TEST_ICO_FILE) as im: - im.load() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (16, 16)) - self.assertEqual(im.format, "ICO") - self.assertEqual(im.get_format_mimetype(), "image/x-icon") +def test_sanity(): + with Image.open(TEST_ICO_FILE) as im: + im.load() + assert im.mode == "RGBA" + assert im.size == (16, 16) + assert im.format == "ICO" + assert im.get_format_mimetype() == "image/x-icon" - def test_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, IcoImagePlugin.IcoImageFile, fp) - def test_save_to_bytes(self): - output = io.BytesIO() - im = hopper() - im.save(output, "ico", sizes=[(32, 32), (64, 64)]) +def test_invalid_file(): + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + IcoImagePlugin.IcoImageFile(fp) - # the default image - output.seek(0) - reloaded = Image.open(output) - self.assertEqual(reloaded.info["sizes"], {(32, 32), (64, 64)}) - self.assertEqual(im.mode, reloaded.mode) - self.assertEqual((64, 64), reloaded.size) - self.assertEqual(reloaded.format, "ICO") - self.assert_image_equal(reloaded, hopper().resize((64, 64), Image.LANCZOS)) +def test_save_to_bytes(): + output = io.BytesIO() + im = hopper() + im.save(output, "ico", sizes=[(32, 32), (64, 64)]) - # the other one - output.seek(0) - reloaded = Image.open(output) + # The default image + output.seek(0) + with Image.open(output) as reloaded: + assert reloaded.info["sizes"] == {(32, 32), (64, 64)} + + assert im.mode == reloaded.mode + assert (64, 64) == reloaded.size + assert reloaded.format == "ICO" + assert_image_equal(reloaded, hopper().resize((64, 64), Image.LANCZOS)) + + # The other one + output.seek(0) + with Image.open(output) as reloaded: reloaded.size = (32, 32) - self.assertEqual(im.mode, reloaded.mode) - self.assertEqual((32, 32), reloaded.size) - self.assertEqual(reloaded.format, "ICO") - self.assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) + assert im.mode == reloaded.mode + assert (32, 32) == reloaded.size + assert reloaded.format == "ICO" + assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) - def test_incorrect_size(self): - with Image.open(TEST_ICO_FILE) as im: - with self.assertRaises(ValueError): - im.size = (1, 1) - def test_save_256x256(self): - """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" - # Arrange - with Image.open("Tests/images/hopper_256x256.ico") as im: - outfile = self.tempfile("temp_saved_hopper_256x256.ico") +def test_incorrect_size(): + with Image.open(TEST_ICO_FILE) as im: + with pytest.raises(ValueError): + im.size = (1, 1) - # Act - im.save(outfile) - with Image.open(outfile) as im_saved: - # Assert - self.assertEqual(im_saved.size, (256, 256)) +def test_save_256x256(tmp_path): + """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" + # Arrange + with Image.open("Tests/images/hopper_256x256.ico") as im: + outfile = str(tmp_path / "temp_saved_hopper_256x256.ico") - def test_only_save_relevant_sizes(self): - """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 - """ - # Arrange - with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 - outfile = self.tempfile("temp_saved_python.ico") - # Act - im.save(outfile) + # Act + im.save(outfile) + with Image.open(outfile) as im_saved: - with Image.open(outfile) as im_saved: - # Assert - self.assertEqual( - im_saved.info["sizes"], {(16, 16), (24, 24), (32, 32), (48, 48)} - ) + # Assert + assert im_saved.size == (256, 256) - def test_unexpected_size(self): - # This image has been manually hexedited to state that it is 16x32 - # while the image within is still 16x16 - def open(): - with Image.open("Tests/images/hopper_unexpected.ico") as im: - self.assertEqual(im.size, (16, 16)) - self.assert_warning(UserWarning, open) +def test_only_save_relevant_sizes(tmp_path): + """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 + """ + # Arrange + with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 + outfile = str(tmp_path / "temp_saved_python.ico") + # Act + im.save(outfile) - def test_draw_reloaded(self): - with Image.open(TEST_ICO_FILE) as im: - outfile = self.tempfile("temp_saved_hopper_draw.ico") + with Image.open(outfile) as im_saved: + # Assert + assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} - draw = ImageDraw.Draw(im) - draw.line((0, 0) + im.size, "#f00") - im.save(outfile) - with Image.open(outfile) as im: - im.save("Tests/images/hopper_draw.ico") - with Image.open("Tests/images/hopper_draw.ico") as reloaded: - self.assert_image_equal(im, reloaded) +def test_unexpected_size(): + # This image has been manually hexedited to state that it is 16x32 + # while the image within is still 16x16 + def open(): + with Image.open("Tests/images/hopper_unexpected.ico") as im: + assert im.size == (16, 16) + + pytest.warns(UserWarning, open) + + +def test_draw_reloaded(tmp_path): + with Image.open(TEST_ICO_FILE) as im: + outfile = str(tmp_path / "temp_saved_hopper_draw.ico") + + draw = ImageDraw.Draw(im) + draw.line((0, 0) + im.size, "#f00") + im.save(outfile) + + with Image.open(outfile) as im: + im.save("Tests/images/hopper_draw.ico") + with Image.open("Tests/images/hopper_draw.ico") as reloaded: + assert_image_equal(im, reloaded) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 1a5638523..afea82359 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,88 +1,111 @@ -import unittest +import filecmp + +import pytest from PIL import Image, ImImagePlugin -from .helper import PillowTestCase, hopper, is_pypy +from .helper import assert_image_equal, hopper, is_pypy # sample im TEST_IM = "Tests/images/hopper.im" -class TestFileIm(PillowTestCase): - def test_sanity(self): +def test_sanity(): + with Image.open(TEST_IM) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "IM" + + +def test_name_limit(tmp_path): + out = str(tmp_path / ("name_limit_test" * 7 + ".im")) + with Image.open(TEST_IM) as im: + im.save(out) + assert filecmp.cmp(out, "Tests/images/hopper_long_name.im") + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + im = Image.open(TEST_IM) + im.load() + + pytest.warns(ResourceWarning, open) + + +def test_closed_file(): + def open(): + im = Image.open(TEST_IM) + im.load() + im.close() + + pytest.warns(None, open) + + +def test_context_manager(): + def open(): with Image.open(TEST_IM) as im: im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "IM") - @unittest.skipIf(is_pypy(), "Requires CPython") - def test_unclosed_file(self): - def open(): - im = Image.open(TEST_IM) - im.load() + pytest.warns(None, open) - self.assert_warning(ResourceWarning, open) - def test_closed_file(self): - def open(): - im = Image.open(TEST_IM) - im.load() - im.close() +def test_tell(): + # Arrange + with Image.open(TEST_IM) as im: - self.assert_warning(None, open) + # Act + frame = im.tell() - def test_context_manager(self): - def open(): - with Image.open(TEST_IM) as im: - im.load() + # Assert + assert frame == 0 - self.assert_warning(None, open) - def test_tell(self): - # Arrange - with Image.open(TEST_IM) as im: +def test_n_frames(): + with Image.open(TEST_IM) as im: + assert im.n_frames == 1 + assert not im.is_animated - # Act - frame = im.tell() - # Assert - self.assertEqual(frame, 0) +def test_eoferror(): + with Image.open(TEST_IM) as im: + n_frames = im.n_frames - def test_n_frames(self): - with Image.open(TEST_IM) as im: - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames - def test_eoferror(self): - with Image.open(TEST_IM) as im: - n_frames = im.n_frames + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) - # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) +def test_roundtrip(tmp_path): + def roundtrip(mode): + out = str(tmp_path / "temp.im") + im = hopper(mode) + im.save(out) + with Image.open(out) as reread: + assert_image_equal(reread, im) - def test_roundtrip(self): - for mode in ["RGB", "P", "PA"]: - out = self.tempfile("temp.im") - im = hopper(mode) - im.save(out) - with Image.open(out) as reread: + for mode in ["RGB", "P", "PA"]: + roundtrip(mode) - self.assert_image_equal(reread, im) - def test_save_unsupported_mode(self): - out = self.tempfile("temp.im") - im = hopper("HSV") - self.assertRaises(ValueError, im.save, out) +def test_save_unsupported_mode(tmp_path): + out = str(tmp_path / "temp.im") + im = hopper("HSV") + with pytest.raises(ValueError): + im.save(out) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, ImImagePlugin.ImImageFile, invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - def test_number(self): - self.assertEqual(1.2, ImImagePlugin.number("1.2")) + with pytest.raises(SyntaxError): + ImImagePlugin.ImImageFile(invalid_file) + + +def test_number(): + assert ImImagePlugin.number("1.2") == 1.2 diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index b6cc4c225..2d0e6977a 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -3,66 +3,69 @@ from io import StringIO from PIL import Image, IptcImagePlugin -from .helper import PillowTestCase, hopper +from .helper import hopper TEST_FILE = "Tests/images/iptc.jpg" -class TestFileIptc(PillowTestCase): - def test_getiptcinfo_jpg_none(self): - # Arrange - with hopper() as im: - - # Act - iptc = IptcImagePlugin.getiptcinfo(im) - - # Assert - self.assertIsNone(iptc) - - def test_getiptcinfo_jpg_found(self): - # Arrange - with Image.open(TEST_FILE) as im: - - # Act - iptc = IptcImagePlugin.getiptcinfo(im) - - # Assert - self.assertIsInstance(iptc, dict) - self.assertEqual(iptc[(2, 90)], b"Budapest") - self.assertEqual(iptc[(2, 101)], b"Hungary") - - def test_getiptcinfo_tiff_none(self): - # Arrange - with Image.open("Tests/images/hopper.tif") as im: - - # Act - iptc = IptcImagePlugin.getiptcinfo(im) - - # Assert - self.assertIsNone(iptc) - - def test_i(self): - # Arrange - c = b"a" +def test_getiptcinfo_jpg_none(): + # Arrange + with hopper() as im: # Act - ret = IptcImagePlugin.i(c) + iptc = IptcImagePlugin.getiptcinfo(im) - # Assert - self.assertEqual(ret, 97) + # Assert + assert iptc is None - def test_dump(self): - # Arrange - c = b"abc" - # Temporarily redirect stdout - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() + +def test_getiptcinfo_jpg_found(): + # Arrange + with Image.open(TEST_FILE) as im: # Act - IptcImagePlugin.dump(c) + iptc = IptcImagePlugin.getiptcinfo(im) - # Reset stdout - sys.stdout = old_stdout + # Assert + assert isinstance(iptc, dict) + assert iptc[(2, 90)] == b"Budapest" + assert iptc[(2, 101)] == b"Hungary" - # Assert - self.assertEqual(mystdout.getvalue(), "61 62 63 \n") + +def test_getiptcinfo_tiff_none(): + # Arrange + with Image.open("Tests/images/hopper.tif") as im: + + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + assert iptc is None + + +def test_i(): + # Arrange + c = b"a" + + # Act + ret = IptcImagePlugin.i(c) + + # Assert + assert ret == 97 + + +def test_dump(): + # Arrange + c = b"abc" + # Temporarily redirect stdout + old_stdout = sys.stdout + sys.stdout = mystdout = StringIO() + + # Act + IptcImagePlugin.dump(c) + + # Reset stdout + sys.stdout = old_stdout + + # Assert + assert mystdout.getvalue() == "61 62 63 \n" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 35f2c0940..ff469d15c 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,27 +1,35 @@ import os +import re from io import BytesIO -from PIL import Image, ImageFile, JpegImagePlugin +import pytest + +from PIL import ( + ExifTags, + Image, + ImageFile, + ImageOps, + JpegImagePlugin, + UnidentifiedImageError, + features, +) from .helper import ( - PillowTestCase, + assert_image, + assert_image_equal, + assert_image_similar, cjpeg_available, djpeg_available, hopper, is_win32, - unittest, + skip_unless_feature, ) -codecs = dir(Image.core) - TEST_FILE = "Tests/images/hopper.jpg" -class TestFileJpeg(PillowTestCase): - def setUp(self): - if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: - self.skipTest("jpeg support not available") - +@skip_unless_feature("jpg") +class TestFileJpeg: def roundtrip(self, im, **options): out = BytesIO() im.save(out, "JPEG", **options) @@ -32,7 +40,7 @@ class TestFileJpeg(PillowTestCase): return im def gen_random_image(self, size, mode="RGB"): - """ Generates a very hard to compress file + """Generates a very hard to compress file :param size: tuple :param mode: optional image mode @@ -42,77 +50,86 @@ class TestFileJpeg(PillowTestCase): def test_sanity(self): # internal version number - self.assertRegex(Image.core.jpeglib_version, r"\d+\.\d+$") + assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) - im = Image.open(TEST_FILE) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "JPEG") - self.assertEqual(im.get_format_mimetype(), "image/jpeg") + with Image.open(TEST_FILE) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "JPEG" + assert im.get_format_mimetype() == "image/jpeg" def test_app(self): # Test APP/COM reader (@PIL135) with Image.open(TEST_FILE) as im: - self.assertEqual( - im.applist[0], ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") + assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") + assert im.applist[1] == ( + "COM", + b"File written by Adobe Photoshop\xa8 4.0\x00", ) - self.assertEqual( - im.applist[1], ("COM", b"File written by Adobe Photoshop\xa8 4.0\x00") - ) - self.assertEqual(len(im.applist), 2) + assert len(im.applist) == 2 + + assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" def test_cmyk(self): # 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" - im = Image.open(f) - # the source image has red pixels in the upper left corner. - c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] - self.assertEqual(c, 0.0) - self.assertGreater(m, 0.8) - self.assertGreater(y, 0.8) - self.assertEqual(k, 0.0) - # the opposite corner is black - c, m, y, k = [x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))] - self.assertGreater(k, 0.9) - # roundtrip, and check again - im = self.roundtrip(im) - c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] - self.assertEqual(c, 0.0) - self.assertGreater(m, 0.8) - self.assertGreater(y, 0.8) - self.assertEqual(k, 0.0) - c, m, y, k = [x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))] - self.assertGreater(k, 0.9) + with Image.open(f) as im: + # the source image has red pixels in the upper left corner. + c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] + assert c == 0.0 + assert m > 0.8 + assert y > 0.8 + assert k == 0.0 + # the opposite corner is black + c, m, y, k = [ + x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) + ] + assert k > 0.9 + # roundtrip, and check again + im = self.roundtrip(im) + c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] + assert c == 0.0 + assert m > 0.8 + assert y > 0.8 + assert k == 0.0 + c, m, y, k = [ + x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) + ] + assert k > 0.9 - def test_dpi(self): + @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): - im = Image.open(TEST_FILE) - im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) + with Image.open(test_image_path) as im: + im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) return im.info.get("dpi") - self.assertEqual(test(72), (72, 72)) - self.assertEqual(test(300), (300, 300)) - self.assertEqual(test(100, 200), (100, 200)) - self.assertIsNone(test(0)) # square pixels + assert test(72) == (72, 72) + assert test(300) == (300, 300) + assert test(100, 200) == (100, 200) + assert test(0) is None # square pixels - def test_icc(self): + def test_icc(self, tmp_path): # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: icc_profile = im1.info["icc_profile"] - self.assertEqual(len(icc_profile), 3144) + assert len(icc_profile) == 3144 # Roundtrip via physical file. - f = self.tempfile("temp.jpg") + f = str(tmp_path / "temp.jpg") im1.save(f, icc_profile=icc_profile) with Image.open(f) as im2: - self.assertEqual(im2.info.get("icc_profile"), icc_profile) + assert im2.info.get("icc_profile") == icc_profile # Roundtrip via memory buffer. im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), icc_profile=icc_profile) - self.assert_image_equal(im1, im2) - self.assertFalse(im1.info.get("icc_profile")) - self.assertTrue(im2.info.get("icc_profile")) + assert_image_equal(im1, im2) + assert not im1.info.get("icc_profile") + assert im2.info.get("icc_profile") def test_icc_big(self): # Make sure that the "extra" support handles large blocks @@ -121,9 +138,9 @@ class TestFileJpeg(PillowTestCase): # using a 4-byte test code should allow us to detect out of # order issues. icc_profile = (b"Test" * int(n / 4 + 1))[:n] - self.assertEqual(len(icc_profile), n) # sanity + assert len(icc_profile) == n # sanity im1 = self.roundtrip(hopper(), icc_profile=icc_profile) - self.assertEqual(im1.info.get("icc_profile"), icc_profile or None) + assert im1.info.get("icc_profile") == (icc_profile or None) test(0) test(1) @@ -136,35 +153,35 @@ class TestFileJpeg(PillowTestCase): test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte test(ImageFile.MAXBLOCK * 4 + 3) # large block - def test_large_icc_meta(self): + def test_large_icc_meta(self, tmp_path): # 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. - im = Image.open("Tests/images/icc_profile_big.jpg") - f = self.tempfile("temp.jpg") - icc_profile = im.info["icc_profile"] - # Should not raise IOError for image with icc larger than image size. - im.save( - f, - format="JPEG", - progressive=True, - quality=95, - icc_profile=icc_profile, - optimize=True, - ) + with Image.open("Tests/images/icc_profile_big.jpg") as im: + f = str(tmp_path / "temp.jpg") + icc_profile = im.info["icc_profile"] + # Should not raise OSError for image with icc larger than image size. + im.save( + f, + format="JPEG", + progressive=True, + quality=95, + icc_profile=icc_profile, + optimize=True, + ) def test_optimize(self): im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), optimize=0) im3 = self.roundtrip(hopper(), optimize=1) - self.assert_image_equal(im1, im2) - self.assert_image_equal(im1, im3) - self.assertGreaterEqual(im1.bytes, im2.bytes) - self.assertGreaterEqual(im1.bytes, im3.bytes) + assert_image_equal(im1, im2) + assert_image_equal(im1, im3) + assert im1.bytes >= im2.bytes + assert im1.bytes >= im3.bytes - def test_optimize_large_buffer(self): + def test_optimize_large_buffer(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/148 - f = self.tempfile("temp.jpg") + 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) @@ -173,21 +190,21 @@ class TestFileJpeg(PillowTestCase): im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), progressive=False) im3 = self.roundtrip(hopper(), progressive=True) - self.assertFalse(im1.info.get("progressive")) - self.assertFalse(im2.info.get("progressive")) - self.assertTrue(im3.info.get("progressive")) + assert not im1.info.get("progressive") + assert not im2.info.get("progressive") + assert im3.info.get("progressive") - self.assert_image_equal(im1, im3) - self.assertGreaterEqual(im1.bytes, im3.bytes) + assert_image_equal(im1, im3) + assert im1.bytes >= im3.bytes - def test_progressive_large_buffer(self): - f = self.tempfile("temp.jpg") + def test_progressive_large_buffer(self, tmp_path): + 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): - f = self.tempfile("temp.jpg") + def test_progressive_large_buffer_highest_quality(self, tmp_path): + 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) @@ -198,9 +215,9 @@ class TestFileJpeg(PillowTestCase): im = self.gen_random_image((256, 256), "CMYK") im.save(f, format="JPEG", progressive=True, quality=94) - def test_large_exif(self): + def test_large_exif(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/148 - f = self.tempfile("temp.jpg") + f = str(tmp_path / "temp.jpg") im = hopper() im.save(f, "JPEG", quality=90, exif=b"1" * 65532) @@ -209,23 +226,58 @@ class TestFileJpeg(PillowTestCase): # Should not raise a TypeError im._getexif() - def test_exif_gps(self): - # Arrange + def test_exif_gps(self, tmp_path): + expected_exif_gps = { + 0: b"\x00\x00\x00\x01", + 2: 4294967295, + 5: b"\x01", + 30: 65535, + 29: "1999:99:99 99:99:99", + } + gps_index = 34853 + + # Reading with Image.open("Tests/images/exif_gps.jpg") as im: - gps_index = 34853 - expected_exif_gps = { - 0: b"\x00\x00\x00\x01", - 2: (4294967295, 1), - 5: b"\x01", - 30: 65535, - 29: "1999:99:99 99:99:99", - } - - # Act exif = im._getexif() + assert exif[gps_index] == expected_exif_gps - # Assert - self.assertEqual(exif[gps_index], expected_exif_gps) + # Writing + f = str(tmp_path / "temp.jpg") + exif = Image.Exif() + exif[gps_index] = expected_exif_gps + hopper().save(f, exif=exif) + + with Image.open(f) as reloaded: + exif = reloaded._getexif() + assert exif[gps_index] == expected_exif_gps + + def test_empty_exif_gps(self): + with Image.open("Tests/images/empty_gps_ifd.jpg") as im: + exif = im.getexif() + del exif[0x8769] + + # Assert that it needs to be transposed + assert exif[0x0112] == Image.TRANSVERSE + + # Assert that the GPS IFD is present and empty + assert exif[0x8825] == {} + + transposed = ImageOps.exif_transpose(im) + exif = transposed.getexif() + assert exif[0x8825] == {} + + # Assert that it was transposed + assert 0x0112 not in exif + + def test_exif_equality(self): + # 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 + exifs = [] + for i in range(2): + with Image.open("Tests/images/exif-200dpcm.jpg") as im: + exifs.append(im._getexif()) + assert exifs[0] == exifs[1] def test_exif_rollback(self): # rolling back exif support in 3.1 to pre-3.0 formatting. @@ -237,7 +289,7 @@ class TestFileJpeg(PillowTestCase): 36867: "2099:09:29 10:10:10", 34853: { 0: b"\x00\x00\x00\x01", - 2: (4294967295, 1), + 2: 4294967295, 5: b"\x01", 30: 65535, 29: "1999:99:99 99:99:99", @@ -249,18 +301,18 @@ class TestFileJpeg(PillowTestCase): 271: "Make", 272: "XXX-XXX", 305: "PIL", - 42034: ((1, 1), (1, 1), (1, 1), (1, 1)), + 42034: (1, 1, 1, 1), 42035: "LensMake", 34856: b"\xaa\xaa\xaa\xaa\xaa\xaa", - 282: (4294967295, 1), - 33434: (4294967295, 1), + 282: 4294967295, + 33434: 4294967295, } with Image.open("Tests/images/exif_gps.jpg") as im: exif = im._getexif() for tag, value in expected_exif.items(): - self.assertEqual(value, exif[tag]) + assert value == exif[tag] def test_exif_gps_typeerror(self): with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: @@ -270,35 +322,39 @@ class TestFileJpeg(PillowTestCase): def test_progressive_compat(self): im1 = self.roundtrip(hopper()) - self.assertFalse(im1.info.get("progressive")) - self.assertFalse(im1.info.get("progression")) + assert not im1.info.get("progressive") + assert not im1.info.get("progression") im2 = self.roundtrip(hopper(), progressive=0) im3 = self.roundtrip(hopper(), progression=0) # compatibility - self.assertFalse(im2.info.get("progressive")) - self.assertFalse(im2.info.get("progression")) - self.assertFalse(im3.info.get("progressive")) - self.assertFalse(im3.info.get("progression")) + assert not im2.info.get("progressive") + assert not im2.info.get("progression") + assert not im3.info.get("progressive") + assert not im3.info.get("progression") im2 = self.roundtrip(hopper(), progressive=1) im3 = self.roundtrip(hopper(), progression=1) # compatibility - self.assert_image_equal(im1, im2) - self.assert_image_equal(im1, im3) - self.assertTrue(im2.info.get("progressive")) - self.assertTrue(im2.info.get("progression")) - self.assertTrue(im3.info.get("progressive")) - self.assertTrue(im3.info.get("progression")) + assert_image_equal(im1, im2) + assert_image_equal(im1, im3) + assert im2.info.get("progressive") + assert im2.info.get("progression") + assert im3.info.get("progressive") + assert im3.info.get("progression") def test_quality(self): im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), quality=50) - self.assert_image(im1, im2.mode, im2.size) - self.assertGreaterEqual(im1.bytes, im2.bytes) + assert_image(im1, im2.mode, im2.size) + assert im1.bytes >= im2.bytes + + im3 = self.roundtrip(hopper(), quality=0) + assert_image(im1, im3.mode, im3.size) + assert im2.bytes > im3.bytes def test_smooth(self): im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), smooth=100) - self.assert_image(im1, im2.mode, im2.size) + assert_image(im1, im2.mode, im2.size) def test_subsampling(self): def getsampling(im): @@ -307,49 +363,50 @@ class TestFileJpeg(PillowTestCase): # experimental API im = self.roundtrip(hopper(), subsampling=-1) # default - self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling=0) # 4:4:4 - self.assertEqual(getsampling(im), (1, 1, 1, 1, 1, 1)) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling=1) # 4:2:2 - self.assertEqual(getsampling(im), (2, 1, 1, 1, 1, 1)) + assert getsampling(im) == (2, 1, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling=2) # 4:2:0 - self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling=3) # default (undefined) - self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling="4:4:4") - self.assertEqual(getsampling(im), (1, 1, 1, 1, 1, 1)) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling="4:2:2") - self.assertEqual(getsampling(im), (2, 1, 1, 1, 1, 1)) + assert getsampling(im) == (2, 1, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling="4:2:0") - self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling="4:1:1") - self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) - self.assertRaises(TypeError, self.roundtrip, hopper(), subsampling="1:1:1") + with pytest.raises(TypeError): + self.roundtrip(hopper(), subsampling="1:1:1") def test_exif(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: info = im._getexif() - self.assertEqual(info[305], "Adobe Photoshop CS Macintosh") + assert info[305] == "Adobe Photoshop CS Macintosh" def test_mp(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: - self.assertIsNone(im._getmp()) + assert im._getmp() is None - def test_quality_keep(self): + def test_quality_keep(self, tmp_path): # RGB - im = Image.open("Tests/images/hopper.jpg") - f = self.tempfile("temp.jpg") - im.save(f, quality="keep") + with Image.open("Tests/images/hopper.jpg") as im: + f = str(tmp_path / "temp.jpg") + im.save(f, quality="keep") # Grayscale - im = Image.open("Tests/images/hopper_gray.jpg") - f = self.tempfile("temp.jpg") - im.save(f, quality="keep") + with Image.open("Tests/images/hopper_gray.jpg") as im: + f = str(tmp_path / "temp.jpg") + im.save(f, quality="keep") # CMYK - im = Image.open("Tests/images/pil_sample_cmyk.jpg") - f = self.tempfile("temp.jpg") - im.save(f, quality="keep") + with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: + f = str(tmp_path / "temp.jpg") + im.save(f, quality="keep") def test_junk_jpeg_header(self): # https://github.com/python-pillow/Pillow/issues/630 @@ -365,159 +422,185 @@ class TestFileJpeg(PillowTestCase): def test_truncated_jpeg_should_read_all_the_data(self): filename = "Tests/images/truncated_jpeg.jpg" ImageFile.LOAD_TRUNCATED_IMAGES = True - im = Image.open(filename) - im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False - self.assertIsNotNone(im.getbbox()) + with Image.open(filename) as im: + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False + assert im.getbbox() is not None - def test_truncated_jpeg_throws_IOError(self): + def test_truncated_jpeg_throws_oserror(self): filename = "Tests/images/truncated_jpeg.jpg" with Image.open(filename) as im: - with self.assertRaises(IOError): + with pytest.raises(OSError): im.load() # Test that the error is raised if loaded a second time - with self.assertRaises(IOError): + with pytest.raises(OSError): im.load() - def _n_qtables_helper(self, n, test_file): - im = Image.open(test_file) - f = self.tempfile("temp.jpg") - im.save(f, qtables=[[n] * 64] * n) - im = Image.open(f) - self.assertEqual(len(im.quantization), n) - reloaded = self.roundtrip(im, qtables="keep") - self.assertEqual(im.quantization, reloaded.quantization) + def test_qtables(self, tmp_path): + def _n_qtables_helper(n, test_file): + with Image.open(test_file) as im: + f = str(tmp_path / "temp.jpg") + im.save(f, qtables=[[n] * 64] * n) + with Image.open(f) as im: + assert len(im.quantization) == n + reloaded = self.roundtrip(im, qtables="keep") + assert im.quantization == reloaded.quantization + assert reloaded.quantization[0].typecode == "B" - def test_qtables(self): - im = Image.open("Tests/images/hopper.jpg") - qtables = im.quantization - reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) - self.assertEqual(im.quantization, reloaded.quantization) - self.assert_image_similar(im, self.roundtrip(im, qtables="web_low"), 30) - self.assert_image_similar(im, self.roundtrip(im, qtables="web_high"), 30) - self.assert_image_similar(im, self.roundtrip(im, qtables="keep"), 30) + with Image.open("Tests/images/hopper.jpg") as im: + qtables = im.quantization + reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) + assert im.quantization == reloaded.quantization + assert_image_similar(im, self.roundtrip(im, qtables="web_low"), 30) + assert_image_similar(im, self.roundtrip(im, qtables="web_high"), 30) + assert_image_similar(im, self.roundtrip(im, qtables="keep"), 30) - # valid bounds for baseline qtable - bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)] - self.roundtrip(im, qtables=[bounds_qtable]) + # valid bounds for baseline qtable + bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)] + self.roundtrip(im, qtables=[bounds_qtable]) - # values from wizard.txt in jpeg9-a src package. - standard_l_qtable = [ - int(s) - for s in """ - 16 11 10 16 24 40 51 61 - 12 12 14 19 26 58 60 55 - 14 13 16 24 40 57 69 56 - 14 17 22 29 51 87 80 62 - 18 22 37 56 68 109 103 77 - 24 35 55 64 81 104 113 92 - 49 64 78 87 103 121 120 101 - 72 92 95 98 112 100 103 99 - """.split( - None + # values from wizard.txt in jpeg9-a src package. + standard_l_qtable = [ + int(s) + for s in """ + 16 11 10 16 24 40 51 61 + 12 12 14 19 26 58 60 55 + 14 13 16 24 40 57 69 56 + 14 17 22 29 51 87 80 62 + 18 22 37 56 68 109 103 77 + 24 35 55 64 81 104 113 92 + 49 64 78 87 103 121 120 101 + 72 92 95 98 112 100 103 99 + """.split( + None + ) + ] + + standard_chrominance_qtable = [ + int(s) + for s in """ + 17 18 24 47 99 99 99 99 + 18 21 26 66 99 99 99 99 + 24 26 56 99 99 99 99 99 + 47 66 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + """.split( + None + ) + ] + # list of qtable lists + assert_image_similar( + im, + self.roundtrip( + im, qtables=[standard_l_qtable, standard_chrominance_qtable] + ), + 30, ) - ] - standard_chrominance_qtable = [ - int(s) - for s in """ - 17 18 24 47 99 99 99 99 - 18 21 26 66 99 99 99 99 - 24 26 56 99 99 99 99 99 - 47 66 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - """.split( - None + # tuple of qtable lists + assert_image_similar( + im, + self.roundtrip( + im, qtables=(standard_l_qtable, standard_chrominance_qtable) + ), + 30, ) - ] - # list of qtable lists - self.assert_image_similar( - im, - self.roundtrip( - im, qtables=[standard_l_qtable, standard_chrominance_qtable] - ), - 30, - ) - # tuple of qtable lists - self.assert_image_similar( - im, - self.roundtrip( - im, qtables=(standard_l_qtable, standard_chrominance_qtable) - ), - 30, - ) + # dict of qtable lists + assert_image_similar( + im, + self.roundtrip( + im, qtables={0: standard_l_qtable, 1: standard_chrominance_qtable} + ), + 30, + ) - # dict of qtable lists - self.assert_image_similar( - im, - self.roundtrip( - im, qtables={0: standard_l_qtable, 1: standard_chrominance_qtable} - ), - 30, - ) + _n_qtables_helper(1, "Tests/images/hopper_gray.jpg") + _n_qtables_helper(1, "Tests/images/pil_sample_rgb.jpg") + _n_qtables_helper(2, "Tests/images/pil_sample_rgb.jpg") + _n_qtables_helper(3, "Tests/images/pil_sample_rgb.jpg") + _n_qtables_helper(1, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(2, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(3, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(4, "Tests/images/pil_sample_cmyk.jpg") - self._n_qtables_helper(1, "Tests/images/hopper_gray.jpg") - self._n_qtables_helper(1, "Tests/images/pil_sample_rgb.jpg") - self._n_qtables_helper(2, "Tests/images/pil_sample_rgb.jpg") - self._n_qtables_helper(3, "Tests/images/pil_sample_rgb.jpg") - self._n_qtables_helper(1, "Tests/images/pil_sample_cmyk.jpg") - self._n_qtables_helper(2, "Tests/images/pil_sample_cmyk.jpg") - self._n_qtables_helper(3, "Tests/images/pil_sample_cmyk.jpg") - self._n_qtables_helper(4, "Tests/images/pil_sample_cmyk.jpg") + # not a sequence + with pytest.raises(ValueError): + self.roundtrip(im, qtables="a") + # sequence wrong length + with pytest.raises(ValueError): + self.roundtrip(im, qtables=[]) + # sequence wrong length + with pytest.raises(ValueError): + self.roundtrip(im, qtables=[1, 2, 3, 4, 5]) - # not a sequence - self.assertRaises(ValueError, self.roundtrip, im, qtables="a") - # sequence wrong length - self.assertRaises(ValueError, self.roundtrip, im, qtables=[]) - # sequence wrong length - self.assertRaises(ValueError, self.roundtrip, im, qtables=[1, 2, 3, 4, 5]) + # qtable entry not a sequence + with pytest.raises(ValueError): + self.roundtrip(im, qtables=[1]) + # qtable entry has wrong number of items + with pytest.raises(ValueError): + self.roundtrip(im, qtables=[[1, 2, 3, 4]]) - # qtable entry not a sequence - self.assertRaises(ValueError, self.roundtrip, im, qtables=[1]) - # qtable entry has wrong number of items - self.assertRaises(ValueError, self.roundtrip, im, qtables=[[1, 2, 3, 4]]) + def test_load_16bit_qtables(self): + 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 - @unittest.skipUnless(djpeg_available(), "djpeg not available") + def test_save_multiple_16bit_qtables(self): + 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): + 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): + with Image.open(TEST_FILE) as im: + im2 = self.roundtrip(im, quality=10) + assert len(im2.quantization) == 2 + assert max(im2.quantization[0]) <= 255 + assert max(im2.quantization[1]) <= 255 + + @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self): with Image.open(TEST_FILE) as img: img.load_djpeg() - self.assert_image_similar(img, Image.open(TEST_FILE), 0) + assert_image_similar(img, Image.open(TEST_FILE), 5) - @unittest.skipUnless(cjpeg_available(), "cjpeg not available") - def test_save_cjpeg(self): - img = Image.open(TEST_FILE) - - tempfile = self.tempfile("temp.jpg") - JpegImagePlugin._save_cjpeg(img, 0, tempfile) - # Default save quality is 75%, so a tiny bit of difference is alright - self.assert_image_similar(img, Image.open(tempfile), 17) + @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") + def test_save_cjpeg(self, tmp_path): + 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(img, Image.open(tempfile), 17) def test_no_duplicate_0x1001_tag(self): # Arrange - from PIL import ExifTags - tag_ids = {v: k for k, v in ExifTags.TAGS.items()} # Assert - self.assertEqual(tag_ids["RelatedImageWidth"], 0x1001) - self.assertEqual(tag_ids["RelatedImageLength"], 0x1002) + assert tag_ids["RelatedImageWidth"] == 0x1001 + assert tag_ids["RelatedImageLength"] == 0x1002 - def test_MAXBLOCK_scaling(self): + def test_MAXBLOCK_scaling(self, tmp_path): im = self.gen_random_image((512, 512)) - f = self.tempfile("temp.jpeg") + f = str(tmp_path / "temp.jpeg") im.save(f, quality=100, optimize=True) - reloaded = Image.open(f) - - # none of these should crash - reloaded.save(f, quality="keep") - reloaded.save(f, quality="keep", progressive=True) - reloaded.save(f, quality="keep", optimize=True) + with Image.open(f) as reloaded: + # none of these should crash + reloaded.save(f, quality="keep") + reloaded.save(f, quality="keep", progressive=True) + reloaded.save(f, quality="keep", optimize=True) def test_bad_mpo_header(self): """ Treat unknown MPO as JPEG """ @@ -526,10 +609,10 @@ class TestFileJpeg(PillowTestCase): # Act # Shouldn't raise error fn = "Tests/images/sugarshack_bad_mpo_header.jpg" - with self.assert_warning(UserWarning, Image.open, fn) as im: + with pytest.warns(UserWarning, Image.open, fn) as im: # Assert - self.assertEqual(im.format, "JPEG") + assert im.format == "JPEG" def test_save_correct_modes(self): out = BytesIO() @@ -542,41 +625,43 @@ class TestFileJpeg(PillowTestCase): out = BytesIO() for mode in ["LA", "La", "RGBA", "RGBa", "P"]: img = Image.new(mode, (20, 20)) - self.assertRaises(IOError, img.save, out, "JPEG") + with pytest.raises(OSError): + img.save(out, "JPEG") - def test_save_tiff_with_dpi(self): + def test_save_tiff_with_dpi(self, tmp_path): # Arrange - outfile = self.tempfile("temp.tif") - im = Image.open("Tests/images/hopper.tif") + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/hopper.tif") as im: - # Act - im.save(outfile, "JPEG", dpi=im.info["dpi"]) + # Act + im.save(outfile, "JPEG", dpi=im.info["dpi"]) - # Assert - reloaded = Image.open(outfile) - reloaded.load() - self.assertEqual(im.info["dpi"], reloaded.info["dpi"]) + # Assert + with Image.open(outfile) as reloaded: + reloaded.load() + assert im.info["dpi"] == reloaded.info["dpi"] def test_load_dpi_rounding(self): # Round up with Image.open("Tests/images/iptc_roundUp.jpg") as im: - self.assertEqual(im.info["dpi"], (44, 44)) + assert im.info["dpi"] == (44, 44) # Round down with Image.open("Tests/images/iptc_roundDown.jpg") as im: - self.assertEqual(im.info["dpi"], (2, 2)) + assert im.info["dpi"] == (2, 2) - def test_save_dpi_rounding(self): - outfile = self.tempfile("temp.jpg") - im = Image.open("Tests/images/hopper.jpg") + def test_save_dpi_rounding(self, tmp_path): + outfile = str(tmp_path / "temp.jpg") + with Image.open("Tests/images/hopper.jpg") as im: + im.save(outfile, dpi=(72.2, 72.2)) - im.save(outfile, dpi=(72.2, 72.2)) - with Image.open(outfile) as reloaded: - self.assertEqual(reloaded.info["dpi"], (72, 72)) + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == (72, 72) im.save(outfile, dpi=(72.8, 72.8)) + with Image.open(outfile) as reloaded: - self.assertEqual(reloaded.info["dpi"], (73, 73)) + assert reloaded.info["dpi"] == (73, 73) def test_dpi_tuple_from_exif(self): # Arrange @@ -585,7 +670,7 @@ class TestFileJpeg(PillowTestCase): with Image.open("Tests/images/photoshop-200dpi.jpg") as im: # Act / Assert - self.assertEqual(im.info.get("dpi"), (200, 200)) + assert im.info.get("dpi") == (200, 200) def test_dpi_int_from_exif(self): # Arrange @@ -594,7 +679,7 @@ class TestFileJpeg(PillowTestCase): with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Act / Assert - self.assertEqual(im.info.get("dpi"), (72, 72)) + assert im.info.get("dpi") == (72, 72) def test_dpi_from_dpcm_exif(self): # Arrange @@ -603,7 +688,7 @@ class TestFileJpeg(PillowTestCase): with Image.open("Tests/images/exif-200dpcm.jpg") as im: # Act / Assert - self.assertEqual(im.info.get("dpi"), (508, 508)) + assert im.info.get("dpi") == (508, 508) def test_dpi_exif_zero_division(self): # Arrange @@ -613,7 +698,7 @@ class TestFileJpeg(PillowTestCase): # Act / Assert # This should return the default, and not raise a ZeroDivisionError - self.assertEqual(im.info.get("dpi"), (72, 72)) + assert im.info.get("dpi") == (72, 72) def test_no_dpi_in_exif(self): # Arrange @@ -624,7 +709,7 @@ class TestFileJpeg(PillowTestCase): # Act / Assert # "When the image resolution is unknown, 72 [dpi] is designated." # http://www.exiv2.org/tags.html - self.assertEqual(im.info.get("dpi"), (72, 72)) + assert im.info.get("dpi") == (72, 72) def test_invalid_exif(self): # This is no-dpi-in-exif with the tiff header of the exif block @@ -633,7 +718,28 @@ class TestFileJpeg(PillowTestCase): # This should return the default, and not a SyntaxError or # OSError for unidentified image. - self.assertEqual(im.info.get("dpi"), (72, 72)) + assert im.info.get("dpi") == (72, 72) + + def test_exif_x_resolution(self, tmp_path): + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + assert exif[282] == 180 + + out = str(tmp_path / "out.jpg") + with pytest.warns(None) as record: + im.save(out, exif=exif) + assert len(record) == 0 + + with Image.open(out) as reloaded: + assert reloaded.getexif()[282] == 180 + + def test_invalid_exif_x_resolution(self): + # 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): # Arrange @@ -642,42 +748,71 @@ class TestFileJpeg(PillowTestCase): with Image.open("Tests/images/exif-ifd-offset.jpg") as im: # Act / Assert - self.assertEqual(im._getexif()[306], "2017:03:13 23:03:09") + assert im._getexif()[306] == "2017:03:13 23:03:09" def test_photoshop(self): with Image.open("Tests/images/photoshop-200dpi.jpg") as im: - self.assertEqual( - im.info["photoshop"][0x03ED], - { - "XResolution": 200.0, - "DisplayedUnitsX": 1, - "YResolution": 200.0, - "DisplayedUnitsY": 1, - }, - ) + assert im.info["photoshop"][0x03ED] == { + "XResolution": 200.0, + "DisplayedUnitsX": 1, + "YResolution": 200.0, + "DisplayedUnitsY": 1, + } + + # Test that the image can still load, even with broken Photoshop data + # This image had the APP13 length hexedited to be smaller + with Image.open("Tests/images/photoshop-200dpi-broken.jpg") as im_broken: + assert_image_equal(im_broken, im) # This image does not contain a Photoshop header string with Image.open("Tests/images/app13.jpg") as im: - self.assertNotIn("photoshop", im.info) + assert "photoshop" not in im.info + + def test_photoshop_malformed_and_multiple(self): + 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_icc_after_SOF(self): + with Image.open("Tests/images/icc-after-SOF.jpg") as im: + assert im.info["icc_profile"] == b"profile" + + def test_jpeg_magic_number(self): + size = 4097 + buffer = BytesIO(b"\xFF" * size) # Many xFF bytes + buffer.max_pos = 0 + orig_read = buffer.read + + def read(n=-1): + res = orig_read(n) + buffer.max_pos = max(buffer.max_pos, buffer.tell()) + return res + + buffer.read = read + with pytest.raises(UnidentifiedImageError): + Image.open(buffer) + + # Assert the entire file has not been read + assert 0 < buffer.max_pos < size -@unittest.skipUnless(is_win32(), "Windows only") -class TestFileCloseW32(PillowTestCase): - def setUp(self): - if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: - self.skipTest("jpeg support not available") - - def test_fd_leak(self): - tmpfile = self.tempfile("temp.jpg") +@pytest.mark.skipif(not is_win32(), reason="Windows only") +@skip_unless_feature("jpg") +class TestFileCloseW32: + def test_fd_leak(self, tmp_path): + tmpfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: im.save(tmpfile) im = Image.open(tmpfile) fp = im.fp - self.assertFalse(fp.closed) - self.assertRaises(WindowsError, os.remove, tmpfile) + assert not fp.closed + with pytest.raises(OSError): + os.remove(tmpfile) im.load() - self.assertTrue(fp.closed) + assert fp.closed # this should not fail, as load should have closed the file. os.remove(tmpfile) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index dac1d0ec0..c9e37f8b0 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,10 +1,18 @@ +import re from io import BytesIO -from PIL import Image, Jpeg2KImagePlugin +import pytest -from .helper import PillowTestCase +from PIL import Image, ImageFile, Jpeg2KImagePlugin, features -codecs = dir(Image.core) +from .helper import ( + assert_image_equal, + assert_image_similar, + is_big_endian, + skip_unless_feature, +) + +pytestmark = skip_unless_feature("jpg_2000") test_card = Image.open("Tests/images/test-card.png") test_card.load() @@ -14,199 +22,214 @@ test_card.load() # 'Not enough memory to handle tile data' -class TestFileJpeg2k(PillowTestCase): - def setUp(self): - if "jpeg2k_encoder" not in codecs or "jpeg2k_decoder" not in codecs: - self.skipTest("JPEG 2000 support not available") +def roundtrip(im, **options): + out = BytesIO() + im.save(out, "JPEG2000", **options) + test_bytes = out.tell() + out.seek(0) + im = Image.open(out) + im.bytes = test_bytes # for testing only + im.load() + return im - def roundtrip(self, im, **options): - out = BytesIO() - im.save(out, "JPEG2000", **options) - test_bytes = out.tell() - out.seek(0) - im = Image.open(out) - im.bytes = test_bytes # for testing only - im.load() - return im - def test_sanity(self): - # Internal version number - self.assertRegex(Image.core.jp2klib_version, r"\d+\.\d+\.\d+$") +def test_sanity(): + # Internal version number + assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000")) - im = Image.open("Tests/images/test-card-lossless.jp2") + with Image.open("Tests/images/test-card-lossless.jp2") as im: px = im.load() - self.assertEqual(px[0, 0], (0, 0, 0)) - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (640, 480)) - self.assertEqual(im.format, "JPEG2000") - self.assertEqual(im.get_format_mimetype(), "image/jp2") + assert px[0, 0] == (0, 0, 0) + assert im.mode == "RGB" + assert im.size == (640, 480) + assert im.format == "JPEG2000" + assert im.get_format_mimetype() == "image/jp2" - def test_jpf(self): - with Image.open("Tests/images/balloon.jpf") as im: - self.assertEqual(im.format, "JPEG2000") - self.assertEqual(im.get_format_mimetype(), "image/jpx") - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" +def test_jpf(): + with Image.open("Tests/images/balloon.jpf") as im: + assert im.format == "JPEG2000" + assert im.get_format_mimetype() == "image/jpx" - self.assertRaises(SyntaxError, Jpeg2KImagePlugin.Jpeg2KImageFile, invalid_file) - def test_bytesio(self): - with open("Tests/images/test-card-lossless.jp2", "rb") as f: - data = BytesIO(f.read()) - im = Image.open(data) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) + + +def test_bytesio(): + with open("Tests/images/test-card-lossless.jp2", "rb") as f: + data = BytesIO(f.read()) + with Image.open(data) as im: im.load() - self.assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, test_card, 1.0e-3) - # These two test pre-written JPEG 2000 files that were not written with - # PIL (they were made using Adobe Photoshop) - def test_lossless(self): - im = Image.open("Tests/images/test-card-lossless.jp2") +# These two test pre-written JPEG 2000 files that were not written with +# PIL (they were made using Adobe Photoshop) + + +def test_lossless(tmp_path): + with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() - outfile = self.tempfile("temp_test-card.png") + outfile = str(tmp_path / "temp_test-card.png") im.save(outfile) - self.assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, test_card, 1.0e-3) - def test_lossy_tiled(self): - im = Image.open("Tests/images/test-card-lossy-tiled.jp2") + +def test_lossy_tiled(): + with Image.open("Tests/images/test-card-lossy-tiled.jp2") as im: im.load() - self.assert_image_similar(im, test_card, 2.0) + assert_image_similar(im, test_card, 2.0) - def test_lossless_rt(self): - im = self.roundtrip(test_card) - self.assert_image_equal(im, test_card) - def test_lossy_rt(self): - im = self.roundtrip(test_card, quality_layers=[20]) - self.assert_image_similar(im, test_card, 2.0) +def test_lossless_rt(): + im = roundtrip(test_card) + assert_image_equal(im, test_card) - def test_tiled_rt(self): - im = self.roundtrip(test_card, tile_size=(128, 128)) - self.assert_image_equal(im, test_card) - def test_tiled_offset_rt(self): - im = self.roundtrip( - test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32) - ) - self.assert_image_equal(im, test_card) +def test_lossy_rt(): + im = roundtrip(test_card, quality_layers=[20]) + assert_image_similar(im, test_card, 2.0) - def test_tiled_offset_too_small(self): - with self.assertRaises(ValueError): - self.roundtrip( - test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32) - ) - def test_irreversible_rt(self): - im = self.roundtrip(test_card, irreversible=True, quality_layers=[20]) - self.assert_image_similar(im, test_card, 2.0) +def test_tiled_rt(): + im = roundtrip(test_card, tile_size=(128, 128)) + assert_image_equal(im, test_card) - def test_prog_qual_rt(self): - im = self.roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") - self.assert_image_similar(im, test_card, 2.0) - def test_prog_res_rt(self): - im = self.roundtrip(test_card, num_resolutions=8, progression="RLCP") - self.assert_image_equal(im, test_card) +def test_tiled_offset_rt(): + 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(): + with pytest.raises(ValueError): + roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) + + +def test_irreversible_rt(): + im = roundtrip(test_card, irreversible=True, quality_layers=[20]) + assert_image_similar(im, test_card, 2.0) + + +def test_prog_qual_rt(): + im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") + assert_image_similar(im, test_card, 2.0) + + +def test_prog_res_rt(): + im = roundtrip(test_card, num_resolutions=8, progression="RLCP") + assert_image_equal(im, test_card) + + +def test_reduce(): + with Image.open("Tests/images/test-card-lossless.jp2") as im: + assert callable(im.reduce) - def test_reduce(self): - im = Image.open("Tests/images/test-card-lossless.jp2") im.reduce = 2 - im.load() - self.assertEqual(im.size, (160, 120)) + assert im.reduce == 2 - def test_layers_type(self): - outfile = self.tempfile("temp_layers.jp2") - for quality_layers in [[100, 50, 10], (100, 50, 10), None]: + im.load() + assert im.size == (160, 120) + + im.thumbnail((40, 40)) + assert im.size == (40, 30) + + +def test_layers_type(tmp_path): + 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")]: + with pytest.raises(ValueError): test_card.save(outfile, quality_layers=quality_layers) - for quality_layers in ["quality_layers", ("100", "50", "10")]: - self.assertRaises( - ValueError, test_card.save, outfile, quality_layers=quality_layers - ) - def test_layers(self): - out = BytesIO() - test_card.save( - out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP" - ) - out.seek(0) +def test_layers(): + out = BytesIO() + test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") + out.seek(0) - im = Image.open(out) + with Image.open(out) as im: im.layers = 1 im.load() - self.assert_image_similar(im, test_card, 13) + assert_image_similar(im, test_card, 13) - out.seek(0) - im = Image.open(out) + out.seek(0) + with Image.open(out) as im: im.layers = 3 im.load() - self.assert_image_similar(im, test_card, 0.4) + assert_image_similar(im, test_card, 0.4) - def test_rgba(self): - # Arrange - j2k = Image.open("Tests/images/rgb_trns_ycbc.j2k") - jp2 = Image.open("Tests/images/rgb_trns_ycbc.jp2") - # Act +def test_rgba(): + # Arrange + with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: + with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: + + # Act + j2k.load() + jp2.load() + + # Assert + assert j2k.mode == "RGBA" + assert jp2.mode == "RGBA" + + +def test_16bit_monochrome_has_correct_mode(): + with Image.open("Tests/images/16bit.cropped.j2k") as j2k: j2k.load() + assert j2k.mode == "I;16" + + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: jp2.load() + assert jp2.mode == "I;16" - # Assert - self.assertEqual(j2k.mode, "RGBA") - self.assertEqual(jp2.mode, "RGBA") - def test_16bit_monochrome_has_correct_mode(self): +@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") +def test_16bit_monochrome_jp2_like_tiff(): + with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + assert_image_similar(jp2, tiff_16bit, 1e-3) - j2k = Image.open("Tests/images/16bit.cropped.j2k") - jp2 = Image.open("Tests/images/16bit.cropped.jp2") - j2k.load() - jp2.load() +@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") +def test_16bit_monochrome_j2k_like_tiff(): + with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: + with Image.open("Tests/images/16bit.cropped.j2k") as j2k: + assert_image_similar(j2k, tiff_16bit, 1e-3) - self.assertEqual(j2k.mode, "I;16") - self.assertEqual(jp2.mode, "I;16") - def test_16bit_monochrome_jp2_like_tiff(self): +def test_16bit_j2k_roundtrips(): + with Image.open("Tests/images/16bit.cropped.j2k") as j2k: + im = roundtrip(j2k) + assert_image_equal(im, j2k) - tiff_16bit = Image.open("Tests/images/16bit.cropped.tif") - jp2 = Image.open("Tests/images/16bit.cropped.jp2") - self.assert_image_similar(jp2, tiff_16bit, 1e-3) - def test_16bit_monochrome_j2k_like_tiff(self): +def test_16bit_jp2_roundtrips(): + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + im = roundtrip(jp2) + assert_image_equal(im, jp2) - tiff_16bit = Image.open("Tests/images/16bit.cropped.tif") - j2k = Image.open("Tests/images/16bit.cropped.j2k") - self.assert_image_similar(j2k, tiff_16bit, 1e-3) - def test_16bit_j2k_roundtrips(self): +def test_unbound_local(): + # prepatch, a malformed jp2 file could cause an UnboundLocalError exception. + with pytest.raises(OSError): + Image.open("Tests/images/unbound_variable.jp2") - j2k = Image.open("Tests/images/16bit.cropped.j2k") - im = self.roundtrip(j2k) - self.assert_image_equal(im, j2k) - def test_16bit_jp2_roundtrips(self): +def test_parser_feed(): + # Arrange + with open("Tests/images/test-card-lossless.jp2", "rb") as f: + data = f.read() - jp2 = Image.open("Tests/images/16bit.cropped.jp2") - im = self.roundtrip(jp2) - self.assert_image_equal(im, jp2) + # Act + p = ImageFile.Parser() + p.feed(data) - def test_unbound_local(self): - # prepatch, a malformed jp2 file could cause an UnboundLocalError - # exception. - with self.assertRaises(IOError): - Image.open("Tests/images/unbound_variable.jp2") - - def test_parser_feed(self): - # Arrange - from PIL import ImageFile - - with open("Tests/images/test-card-lossless.jp2", "rb") as f: - data = f.read() - - # Act - p = ImageFile.Parser() - p.feed(data) - - # Assert - self.assertEqual(p.image.size, (640, 480)) + # Assert + assert p.image.size == (640, 480) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index dba053e60..da955b3de 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,41 +1,44 @@ import base64 -import distutils.version import io import itertools -import logging import os +import re from collections import namedtuple from ctypes import c_float -from PIL import Image, TiffImagePlugin, TiffTags, features +import pytest -from .helper import PillowTestCase, hopper +from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features -logger = logging.getLogger(__name__) +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) -class LibTiffTestCase(PillowTestCase): - def setUp(self): - if not features.check("libtiff"): - self.skipTest("tiff support not available") - - def _assert_noerr(self, im): +@skip_unless_feature("libtiff") +class LibTiffTestCase: + def _assert_noerr(self, tmp_path, im): """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit - self.assertEqual(im.mode, "1") + assert im.mode == "1" # Does the data actually load im.load() im.getdata() try: - self.assertEqual(im._compression, "group4") + assert im._compression == "group4" except AttributeError: print("No _compression") print(dir(im)) # can we write it back out, in a different form. - out = self.tempfile("temp.png") + out = str(tmp_path / "temp.png") im.save(out) out_bytes = io.BytesIO() @@ -43,43 +46,43 @@ class LibTiffTestCase(PillowTestCase): class TestFileLibTiff(LibTiffTestCase): - def test_g4_tiff(self): + def test_version(self): + assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff")) + + def test_g4_tiff(self, tmp_path): """Test the ordinary file path load path""" test_file = "Tests/images/hopper_g4_500.tif" - im = Image.open(test_file) + with Image.open(test_file) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, im) - self.assertEqual(im.size, (500, 500)) - self._assert_noerr(im) - - def test_g4_large(self): + def test_g4_large(self, tmp_path): test_file = "Tests/images/pport_g4.tif" - im = Image.open(test_file) - self._assert_noerr(im) + with Image.open(test_file) as im: + self._assert_noerr(tmp_path, im) - def test_g4_tiff_file(self): + def test_g4_tiff_file(self, tmp_path): """Testing the string load path""" test_file = "Tests/images/hopper_g4_500.tif" with open(test_file, "rb") as f: - im = Image.open(f) + with Image.open(f) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, im) - self.assertEqual(im.size, (500, 500)) - self._assert_noerr(im) - - def test_g4_tiff_bytesio(self): + def test_g4_tiff_bytesio(self, tmp_path): """Testing the stringio loading code path""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() with open(test_file, "rb") as f: s.write(f.read()) s.seek(0) - im = Image.open(s) + with Image.open(s) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, im) - self.assertEqual(im.size, (500, 500)) - self._assert_noerr(im) - - def test_g4_non_disk_file_object(self): + def test_g4_non_disk_file_object(self, tmp_path): """Testing loading from non-disk non-BytesIO file object""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() @@ -87,61 +90,56 @@ class TestFileLibTiff(LibTiffTestCase): s.write(f.read()) s.seek(0) r = io.BufferedReader(s) - im = Image.open(r) - - self.assertEqual(im.size, (500, 500)) - self._assert_noerr(im) + with Image.open(r) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, im) def test_g4_eq_png(self): """ Checking that we're actually getting the data that we expect""" - png = Image.open("Tests/images/hopper_bw_500.png") - g4 = Image.open("Tests/images/hopper_g4_500.tif") - - self.assert_image_equal(g4, png) + with Image.open("Tests/images/hopper_bw_500.png") as png: + with Image.open("Tests/images/hopper_g4_500.tif") as g4: + assert_image_equal(g4, png) # see https://github.com/python-pillow/Pillow/issues/279 def test_g4_fillorder_eq_png(self): """ Checking that we're actually getting the data that we expect""" - png = Image.open("Tests/images/g4-fillorder-test.png") - g4 = Image.open("Tests/images/g4-fillorder-test.tif") + with Image.open("Tests/images/g4-fillorder-test.png") as png: + with Image.open("Tests/images/g4-fillorder-test.tif") as g4: + assert_image_equal(g4, png) - self.assert_image_equal(g4, png) - - def test_g4_write(self): + def test_g4_write(self, tmp_path): """Checking to see that the saved image is the same as what we wrote""" test_file = "Tests/images/hopper_g4_500.tif" - orig = Image.open(test_file) + with Image.open(test_file) as orig: + out = str(tmp_path / "temp.tif") + rot = orig.transpose(Image.ROTATE_90) + assert rot.size == (500, 500) + rot.save(out) - out = self.tempfile("temp.tif") - rot = orig.transpose(Image.ROTATE_90) - self.assertEqual(rot.size, (500, 500)) - rot.save(out) + with Image.open(out) as reread: + assert reread.size == (500, 500) + self._assert_noerr(tmp_path, reread) + assert_image_equal(reread, rot) + assert reread.info["compression"] == "group4" - reread = Image.open(out) - self.assertEqual(reread.size, (500, 500)) - self._assert_noerr(reread) - self.assert_image_equal(reread, rot) - self.assertEqual(reread.info["compression"], "group4") + assert reread.info["compression"] == orig.info["compression"] - self.assertEqual(reread.info["compression"], orig.info["compression"]) - - self.assertNotEqual(orig.tobytes(), reread.tobytes()) + assert orig.tobytes() != reread.tobytes() def test_adobe_deflate_tiff(self): test_file = "Tests/images/tiff_adobe_deflate.tif" - im = Image.open(test_file) + with Image.open(test_file) as im: + assert im.mode == "RGB" + assert im.size == (278, 374) + assert im.tile[0][:3] == ("libtiff", (0, 0, 278, 374), 0) + im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (278, 374)) - self.assertEqual(im.tile[0][:3], ("libtiff", (0, 0, 278, 374), 0)) - im.load() + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - - def test_write_metadata(self): + def test_write_metadata(self, tmp_path): """ Test metadata writing through libtiff """ for legacy_api in [False, True]: - f = self.tempfile("temp.tiff") + f = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper_g4.tif") as img: img.save(f, tiffinfo=img.tag) @@ -170,26 +168,23 @@ class TestFileLibTiff(LibTiffTestCase): val = original[tag] if tag.endswith("Resolution"): if legacy_api: - self.assertEqual( - c_float(val[0][0] / val[0][1]).value, - c_float(value[0][0] / value[0][1]).value, - msg="%s didn't roundtrip" % tag, - ) + assert ( + c_float(val[0][0] / val[0][1]).value + == c_float(value[0][0] / value[0][1]).value + ), f"{tag} didn't roundtrip" else: - self.assertEqual( - c_float(val).value, - c_float(value).value, - msg="%s didn't roundtrip" % tag, - ) + assert ( + c_float(val).value == c_float(value).value + ), f"{tag} didn't roundtrip" else: - self.assertEqual(val, value, msg="%s didn't roundtrip" % tag) + assert val == value, f"{tag} didn't roundtrip" # https://github.com/python-pillow/Pillow/issues/1561 requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"] for field in requested_fields: - self.assertIn(field, reloaded, "%s not in metadata" % field) + assert field in reloaded, f"{field} not in metadata" - def test_additional_metadata(self): + def test_additional_metadata(self, tmp_path): # 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. @@ -204,48 +199,49 @@ class TestFileLibTiff(LibTiffTestCase): # Exclude ones that have special meaning # that we're already testing them - im = Image.open("Tests/images/hopper_g4.tif") - for tag in im.tag_v2: - try: - del core_items[tag] - except KeyError: - pass + with Image.open("Tests/images/hopper_g4.tif") as im: + for tag in im.tag_v2: + try: + del core_items[tag] + except KeyError: + pass + del core_items[320] # colormap is special, tested below - # Type codes: - # 2: "ascii", - # 3: "short", - # 4: "long", - # 5: "rational", - # 12: "double", - # Type: dummy value - values = { - 2: "test", - 3: 1, - 4: 2 ** 20, - 5: TiffImagePlugin.IFDRational(100, 1), - 12: 1.05, - } + # Type codes: + # 2: "ascii", + # 3: "short", + # 4: "long", + # 5: "rational", + # 12: "double", + # Type: dummy value + values = { + 2: "test", + 3: 1, + 4: 2 ** 20, + 5: TiffImagePlugin.IFDRational(100, 1), + 12: 1.05, + } - new_ifd = TiffImagePlugin.ImageFileDirectory_v2() - for tag, info in core_items.items(): - if info.length == 1: - new_ifd[tag] = values[info.type] - if info.length == 0: - new_ifd[tag] = tuple(values[info.type] for _ in range(3)) - else: - new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) + new_ifd = TiffImagePlugin.ImageFileDirectory_v2() + for tag, info in core_items.items(): + if info.length == 1: + new_ifd[tag] = values[info.type] + if info.length == 0: + new_ifd[tag] = tuple(values[info.type] for _ in range(3)) + else: + new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) - # Extra samples really doesn't make sense in this application. - del new_ifd[338] + # Extra samples really doesn't make sense in this application. + del new_ifd[338] - out = self.tempfile("temp.tif") - TiffImagePlugin.WRITE_LIBTIFF = True + out = str(tmp_path / "temp.tif") + TiffImagePlugin.WRITE_LIBTIFF = True - im.save(out, tiffinfo=new_ifd) + im.save(out, tiffinfo=new_ifd) TiffImagePlugin.WRITE_LIBTIFF = False - def test_custom_metadata(self): + def test_custom_metadata(self, tmp_path): tc = namedtuple("test_case", "value,type,supported_by_default") custom = { 37000 + k: v @@ -280,12 +276,8 @@ class TestFileLibTiff(LibTiffTestCase): ) } - libtiff_version = TiffImagePlugin._libtiff_version() - libtiffs = [False] - if distutils.version.StrictVersion( - libtiff_version - ) >= distutils.version.StrictVersion("4.0"): + if Image.core.libtiff_support_custom_tags: libtiffs.append(True) for libtiff in libtiffs: @@ -294,7 +286,7 @@ class TestFileLibTiff(LibTiffTestCase): def check_tags(tiffinfo): im = hopper() - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") im.save(out, tiffinfo=tiffinfo) with Image.open(out) as reloaded: @@ -305,13 +297,12 @@ class TestFileLibTiff(LibTiffTestCase): and libtiff ): # libtiff does not support real RATIONALS - self.assertAlmostEqual(float(reloaded_value), float(value)) + assert ( + round(abs(float(reloaded_value) - float(value)), 7) == 0 + ) continue - if libtiff and isinstance(value, bytes): - value = value.decode() - - self.assertEqual(reloaded_value, value) + assert reloaded_value == value # Test with types ifd = TiffImagePlugin.ImageFileDirectory_v2() @@ -331,168 +322,200 @@ class TestFileLibTiff(LibTiffTestCase): ) TiffImagePlugin.WRITE_LIBTIFF = False - def test_int_dpi(self): + def test_xmlpacket_tag(self, tmp_path): + TiffImagePlugin.WRITE_LIBTIFF = True + + out = str(tmp_path / "temp.tif") + hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) + TiffImagePlugin.WRITE_LIBTIFF = False + + with Image.open(out) as reloaded: + if 700 in reloaded.tag_v2: + assert reloaded.tag_v2[700] == b"xmlpacket tag" + + def test_int_dpi(self, tmp_path): # issue #1765 im = hopper("RGB") - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True im.save(out, dpi=(72, 72)) TiffImagePlugin.WRITE_LIBTIFF = False with Image.open(out) as reloaded: - self.assertEqual(reloaded.info["dpi"], (72.0, 72.0)) + assert reloaded.info["dpi"] == (72.0, 72.0) - def test_g3_compression(self): - i = Image.open("Tests/images/hopper_g4_500.tif") - out = self.tempfile("temp.tif") - i.save(out, compression="group3") + def test_g3_compression(self, tmp_path): + with Image.open("Tests/images/hopper_g4_500.tif") as i: + out = str(tmp_path / "temp.tif") + i.save(out, compression="group3") - reread = Image.open(out) - self.assertEqual(reread.info["compression"], "group3") - self.assert_image_equal(reread, i) + with Image.open(out) as reread: + assert reread.info["compression"] == "group3" + assert_image_equal(reread, i) - def test_little_endian(self): - im = Image.open("Tests/images/16bit.deflate.tif") - self.assertEqual(im.getpixel((0, 0)), 480) - self.assertEqual(im.mode, "I;16") + def test_little_endian(self, tmp_path): + with Image.open("Tests/images/16bit.deflate.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16" - b = im.tobytes() - # Bytes are in image native order (little endian) - self.assertEqual(b[0], ord(b"\xe0")) - self.assertEqual(b[1], ord(b"\x01")) + b = im.tobytes() + # Bytes are in image native order (little endian) + assert b[0] == ord(b"\xe0") + assert b[1] == ord(b"\x01") - out = self.tempfile("temp.tif") - # out = "temp.le.tif" - im.save(out) - reread = Image.open(out) - - self.assertEqual(reread.info["compression"], im.info["compression"]) - self.assertEqual(reread.getpixel((0, 0)), 480) + out = str(tmp_path / "temp.tif") + # out = "temp.le.tif" + im.save(out) + with Image.open(out) as reread: + assert reread.info["compression"] == im.info["compression"] + assert reread.getpixel((0, 0)) == 480 # 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): - im = Image.open("Tests/images/16bit.MM.deflate.tif") + def test_big_endian(self, tmp_path): + with Image.open("Tests/images/16bit.MM.deflate.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16B" - self.assertEqual(im.getpixel((0, 0)), 480) - self.assertEqual(im.mode, "I;16B") + b = im.tobytes() - b = im.tobytes() + # Bytes are in image native order (big endian) + assert b[0] == ord(b"\x01") + assert b[1] == ord(b"\xe0") - # Bytes are in image native order (big endian) - self.assertEqual(b[0], ord(b"\x01")) - self.assertEqual(b[1], ord(b"\xe0")) + out = str(tmp_path / "temp.tif") + im.save(out) + with Image.open(out) as reread: + assert reread.info["compression"] == im.info["compression"] + assert reread.getpixel((0, 0)) == 480 - out = self.tempfile("temp.tif") - im.save(out) - reread = Image.open(out) - - self.assertEqual(reread.info["compression"], im.info["compression"]) - self.assertEqual(reread.getpixel((0, 0)), 480) - - def test_g4_string_info(self): + def test_g4_string_info(self, tmp_path): """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" - orig = Image.open(test_file) + with Image.open(test_file) as orig: + out = str(tmp_path / "temp.tif") - out = self.tempfile("temp.tif") - - orig.tag[269] = "temp.tif" - orig.save(out) + orig.tag[269] = "temp.tif" + orig.save(out) with Image.open(out) as reread: - self.assertEqual("temp.tif", reread.tag_v2[269]) - self.assertEqual("temp.tif", reread.tag[269][0]) + assert "temp.tif" == reread.tag_v2[269] + assert "temp.tif" == reread.tag[269][0] def test_12bit_rawmode(self): - """ Are we generating the same interpretation - of the image as Imagemagick is? """ + """Are we generating the same interpretation + of the image as Imagemagick is?""" TiffImagePlugin.READ_LIBTIFF = True - im = Image.open("Tests/images/12bit.cropped.tif") - im.load() - TiffImagePlugin.READ_LIBTIFF = False - # to make the target -- - # convert 12bit.cropped.tif -depth 16 tmp.tif - # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif - # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, - # so we need to unshift so that the integer values are the same. + with Image.open("Tests/images/12bit.cropped.tif") as im: + im.load() + TiffImagePlugin.READ_LIBTIFF = False + # to make the target -- + # convert 12bit.cropped.tif -depth 16 tmp.tif + # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif + # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, + # so we need to unshift so that the integer values are the same. - self.assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") + assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - def test_blur(self): + def test_blur(self, tmp_path): # test case from irc, how to do blur on b/w image # and save to compressed tif. - from PIL import ImageFilter - - out = self.tempfile("temp.tif") - im = Image.open("Tests/images/pport_g4.tif") - im = im.convert("L") + out = str(tmp_path / "temp.tif") + with Image.open("Tests/images/pport_g4.tif") as im: + im = im.convert("L") im = im.filter(ImageFilter.GaussianBlur(4)) im.save(out, compression="tiff_adobe_deflate") - im2 = Image.open(out) - im2.load() + with Image.open(out) as im2: + im2.load() - self.assert_image_equal(im, im2) + assert_image_equal(im, im2) - def test_compressions(self): + def test_compressions(self, tmp_path): # Test various tiff compressions and assert similar image content but reduced # file sizes. im = hopper("RGB") - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") im.save(out) size_raw = os.path.getsize(out) for compression in ("packbits", "tiff_lzw"): im.save(out, compression=compression) size_compressed = os.path.getsize(out) - im2 = Image.open(out) - self.assert_image_equal(im, im2) + with Image.open(out) as im2: + assert_image_equal(im, im2) im.save(out, compression="jpeg") size_jpeg = os.path.getsize(out) - im2 = Image.open(out) - self.assert_image_similar(im, im2, 30) + with Image.open(out) as im2: + assert_image_similar(im, im2, 30) im.save(out, compression="jpeg", quality=30) size_jpeg_30 = os.path.getsize(out) - im3 = Image.open(out) - self.assert_image_similar(im2, im3, 30) + with Image.open(out) as im3: + assert_image_similar(im2, im3, 30) - self.assertGreater(size_raw, size_compressed) - self.assertGreater(size_compressed, size_jpeg) - self.assertGreater(size_jpeg, size_jpeg_30) + assert size_raw > size_compressed + assert size_compressed > size_jpeg + assert size_jpeg > size_jpeg_30 - def test_quality(self): + def test_tiff_jpeg_compression(self, tmp_path): im = hopper("RGB") - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") + im.save(out, compression="tiff_jpeg") - self.assertRaises(ValueError, im.save, out, compression="tiff_lzw", quality=50) - self.assertRaises(ValueError, im.save, out, compression="jpeg", quality=-1) - self.assertRaises(ValueError, im.save, out, compression="jpeg", quality=101) - self.assertRaises(ValueError, im.save, out, compression="jpeg", quality="good") + with Image.open(out) as reloaded: + assert reloaded.info["compression"] == "jpeg" + + def test_quality(self, tmp_path): + im = hopper("RGB") + out = str(tmp_path / "temp.tif") + + with pytest.raises(ValueError): + im.save(out, compression="tiff_lzw", quality=50) + with pytest.raises(ValueError): + im.save(out, compression="jpeg", quality=-1) + with pytest.raises(ValueError): + im.save(out, compression="jpeg", quality=101) + with pytest.raises(ValueError): + im.save(out, compression="jpeg", quality="good") im.save(out, compression="jpeg", quality=0) im.save(out, compression="jpeg", quality=100) - def test_cmyk_save(self): + def test_cmyk_save(self, tmp_path): im = hopper("CMYK") - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") im.save(out, compression="tiff_adobe_deflate") - im2 = Image.open(out) - self.assert_image_equal(im, im2) + with Image.open(out) as im2: + assert_image_equal(im, im2) - def xtest_bw_compression_w_rgb(self): - """ This test passes, but when running all tests causes a failure due - to output on stderr from the error thrown by libtiff. We need to - capture that but not now""" + def test_palette_save(self, tmp_path): + im = hopper("P") + out = str(tmp_path / "temp.tif") + + TiffImagePlugin.WRITE_LIBTIFF = True + im.save(out) + TiffImagePlugin.WRITE_LIBTIFF = False + + with Image.open(out) as reloaded: + # colormap/palette tag + assert len(reloaded.tag_v2[320]) == 768 + + def xtest_bw_compression_w_rgb(self, tmp_path): + """This test passes, but when running all tests causes a failure due + to output on stderr from the error thrown by libtiff. We need to + capture that but not now""" im = hopper("RGB") - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") - self.assertRaises(IOError, im.save, out, compression="tiff_ccitt") - self.assertRaises(IOError, im.save, out, compression="group3") - self.assertRaises(IOError, im.save, out, compression="group4") + with pytest.raises(OSError): + im.save(out, compression="tiff_ccitt") + with pytest.raises(OSError): + im.save(out, compression="group3") + with pytest.raises(OSError): + im.save(out, compression="group4") def test_fp_leak(self): im = Image.open("Tests/images/hopper_g4_500.tif") @@ -500,10 +523,13 @@ class TestFileLibTiff(LibTiffTestCase): os.fstat(fn) im.load() # this should close it. - self.assertRaises(OSError, os.fstat, fn) + with pytest.raises(OSError): + os.fstat(fn) im = None # this should force even more closed. - self.assertRaises(OSError, os.fstat, fn) - self.assertRaises(OSError, os.close, fn) + with pytest.raises(OSError): + os.fstat(fn) + with pytest.raises(OSError): + os.close(fn) def test_multipage(self): # issue #862 @@ -512,19 +538,19 @@ class TestFileLibTiff(LibTiffTestCase): # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue im.seek(0) - self.assertEqual(im.size, (10, 10)) - self.assertEqual(im.convert("RGB").getpixel((0, 0)), (0, 128, 0)) - self.assertTrue(im.tag.next) + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) + assert im.tag.next im.seek(1) - self.assertEqual(im.size, (10, 10)) - self.assertEqual(im.convert("RGB").getpixel((0, 0)), (255, 0, 0)) - self.assertTrue(im.tag.next) + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) + assert im.tag.next im.seek(2) - self.assertFalse(im.tag.next) - self.assertEqual(im.size, (20, 20)) - self.assertEqual(im.convert("RGB").getpixel((0, 0)), (0, 0, 255)) + assert not im.tag.next + assert im.size == (20, 20) + assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) TiffImagePlugin.READ_LIBTIFF = False @@ -533,7 +559,7 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: frames = im.n_frames - self.assertEqual(frames, 3) + assert frames == 3 for _ in range(frames): im.seek(0) # Should not raise ValueError: I/O operation on closed file @@ -543,10 +569,10 @@ class TestFileLibTiff(LibTiffTestCase): def test__next(self): TiffImagePlugin.READ_LIBTIFF = True - im = Image.open("Tests/images/hopper.tif") - self.assertFalse(im.tag.next) - im.load() - self.assertFalse(im.tag.next) + 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): # Arrange @@ -555,13 +581,13 @@ class TestFileLibTiff(LibTiffTestCase): # Act TiffImagePlugin.READ_LIBTIFF = True - im = Image.open(test_file) - TiffImagePlugin.READ_LIBTIFF = False + with Image.open(test_file) as im: + TiffImagePlugin.READ_LIBTIFF = False - # Assert - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.mode, "L") - self.assert_image_similar(im, original, 7.3) + # Assert + assert im.size == (128, 128) + assert im.mode == "L" + assert_image_similar(im, original, 7.3) def test_gray_semibyte_per_pixel(self): test_files = ( @@ -586,15 +612,15 @@ class TestFileLibTiff(LibTiffTestCase): ) original = hopper("L") for epsilon, group in test_files: - im = Image.open(group[0]) - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.mode, "L") - self.assert_image_similar(im, original, epsilon) + with Image.open(group[0]) as im: + assert im.size == (128, 128) + assert im.mode == "L" + assert_image_similar(im, original, epsilon) for file in group[1:]: - im2 = Image.open(file) - self.assertEqual(im2.size, (128, 128)) - self.assertEqual(im2.mode, "L") - self.assert_image_equal(im, im2) + with Image.open(file) as im2: + assert im2.size == (128, 128) + assert im2.mode == "L" + assert_image_equal(im, im2) def test_save_bytesio(self): # PR 1011 @@ -612,8 +638,8 @@ class TestFileLibTiff(LibTiffTestCase): pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) - pilim_load = Image.open(buffer_io) - self.assert_image_similar(pilim, pilim_load, 0) + with Image.open(buffer_io) as pilim_load: + assert_image_similar(pilim, pilim_load, 0) save_bytesio() save_bytesio("raw") @@ -623,22 +649,22 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False - def test_crashing_metadata(self): + def test_crashing_metadata(self, tmp_path): # issue 1597 - im = Image.open("Tests/images/rdf.tif") - out = self.tempfile("temp.tif") + with Image.open("Tests/images/rdf.tif") as im: + out = str(tmp_path / "temp.tif") - TiffImagePlugin.WRITE_LIBTIFF = True - # this shouldn't crash - im.save(out, format="TIFF") + TiffImagePlugin.WRITE_LIBTIFF = True + # this shouldn't crash + im.save(out, format="TIFF") TiffImagePlugin.WRITE_LIBTIFF = False - def test_page_number_x_0(self): + def test_page_number_x_0(self, tmp_path): # Issue 973 # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. # The second is the total number of pages, zero means not available. - outfile = self.tempfile("temp.tif") + outfile = str(tmp_path / "temp.tif") # Created by printing a page in Chrome to PDF, then: # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif # -dNOPAUSE /tmp/test.pdf -c quit @@ -647,10 +673,10 @@ class TestFileLibTiff(LibTiffTestCase): # Should not divide by zero im.save(outfile) - def test_fd_duplication(self): + def test_fd_duplication(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/1651 - tmpfile = self.tempfile("temp.tif") + tmpfile = str(tmp_path / "temp.tif") with open(tmpfile, "wb") as f: with open("Tests/images/g4-multi.tiff", "rb") as src: f.write(src.read()) @@ -664,177 +690,173 @@ class TestFileLibTiff(LibTiffTestCase): def test_read_icc(self): with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc = img.info.get("icc_profile") - self.assertIsNotNone(icc) + assert icc is not None TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc_libtiff = img.info.get("icc_profile") - self.assertIsNotNone(icc_libtiff) + assert icc_libtiff is not None TiffImagePlugin.READ_LIBTIFF = False - self.assertEqual(icc, icc_libtiff) + assert icc == icc_libtiff + + def test_write_icc(self, tmp_path): + def check_write(libtiff): + TiffImagePlugin.WRITE_LIBTIFF = libtiff + + with Image.open("Tests/images/hopper.iccprofile.tif") as img: + icc_profile = img.info["icc_profile"] + + out = str(tmp_path / "temp.tif") + img.save(out, icc_profile=icc_profile) + with Image.open(out) as reloaded: + assert icc_profile == reloaded.info["icc_profile"] + + libtiffs = [] + if Image.core.libtiff_support_custom_tags: + libtiffs.append(True) + libtiffs.append(False) + + for libtiff in libtiffs: + check_write(libtiff) def test_multipage_compression(self): with Image.open("Tests/images/compression.tif") as im: im.seek(0) - self.assertEqual(im._compression, "tiff_ccitt") - self.assertEqual(im.size, (10, 10)) + assert im._compression == "tiff_ccitt" + assert im.size == (10, 10) im.seek(1) - self.assertEqual(im._compression, "packbits") - self.assertEqual(im.size, (10, 10)) + assert im._compression == "packbits" + assert im.size == (10, 10) im.load() im.seek(0) - self.assertEqual(im._compression, "tiff_ccitt") - self.assertEqual(im.size, (10, 10)) + assert im._compression == "tiff_ccitt" + assert im.size == (10, 10) im.load() - def test_save_tiff_with_jpegtables(self): + def test_save_tiff_with_jpegtables(self, tmp_path): # Arrange - outfile = self.tempfile("temp.tif") + outfile = str(tmp_path / "temp.tif") # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif # Contains JPEGTables (347) tag infile = "Tests/images/hopper_jpg.tif" - im = Image.open(infile) - - # Act / Assert - # Should not raise UnicodeDecodeError or anything else - im.save(outfile) + with Image.open(infile) as im: + # Act / Assert + # Should not raise UnicodeDecodeError or anything else + im.save(outfile) def test_16bit_RGB_tiff(self): - im = Image.open("Tests/images/tiff_16bit_RGB.tiff") - - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (100, 40)) - self.assertEqual( - im.tile, - [ + with Image.open("Tests/images/tiff_16bit_RGB.tiff") as im: + assert im.mode == "RGB" + assert im.size == (100, 40) + assert im.tile, [ ( "libtiff", (0, 0, 100, 40), 0, ("RGB;16N", "tiff_adobe_deflate", False, 8), ) - ], - ) - im.load() + ] + im.load() - self.assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") def test_16bit_RGBa_tiff(self): - im = Image.open("Tests/images/tiff_16bit_RGBa.tiff") + with Image.open("Tests/images/tiff_16bit_RGBa.tiff") as im: + assert im.mode == "RGBA" + assert im.size == (100, 40) + assert im.tile, [ + ("libtiff", (0, 0, 100, 40), 0, ("RGBa;16N", "tiff_lzw", False, 38236)) + ] + im.load() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (100, 40)) - self.assertEqual( - im.tile, - [("libtiff", (0, 0, 100, 40), 0, ("RGBa;16N", "tiff_lzw", False, 38236))], - ) - im.load() - - self.assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + @skip_unless_feature("jpg") def test_gimp_tiff(self): # Read TIFF JPEG images from GIMP [@PIL168] - - codecs = dir(Image.core) - if "jpeg_decoder" not in codecs: - self.skipTest("jpeg support not available") - filename = "Tests/images/pil168.tif" - im = Image.open(filename) + with Image.open(filename) as im: + assert im.mode == "RGB" + assert im.size == (256, 256) + assert im.tile == [ + ("libtiff", (0, 0, 256, 256), 0, ("RGB", "jpeg", False, 5122)) + ] + im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (256, 256)) - self.assertEqual( - im.tile, [("libtiff", (0, 0, 256, 256), 0, ("RGB", "jpeg", False, 5122))] - ) - im.load() - - self.assert_image_equal_tofile(im, "Tests/images/pil168.png") + assert_image_equal_tofile(im, "Tests/images/pil168.png") def test_sampleformat(self): # https://github.com/python-pillow/Pillow/issues/1466 - im = Image.open("Tests/images/copyleft.tiff") - self.assertEqual(im.mode, "RGB") + with Image.open("Tests/images/copyleft.tiff") as im: + assert im.mode == "RGB" - self.assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") + assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") def test_lzw(self): - im = Image.open("Tests/images/hopper_lzw.tif") - - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "TIFF") - im2 = hopper() - self.assert_image_similar(im, im2, 5) + with Image.open("Tests/images/hopper_lzw.tif") as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "TIFF" + im2 = hopper() + assert_image_similar(im, im2, 5) def test_strip_cmyk_jpeg(self): infile = "Tests/images/tiff_strip_cmyk_jpeg.tif" - im = Image.open(infile) - - self.assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) + 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): infile = "Tests/images/tiff_strip_cmyk_16l_jpeg.tif" - im = Image.open(infile) - - self.assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) def test_strip_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" - im = Image.open(infile) - - self.assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) def test_strip_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" - im = Image.open(infile) - - self.assert_image_equal_tofile(im, "Tests/images/flower2.jpg") + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/flower2.jpg") def test_tiled_cmyk_jpeg(self): infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" - im = Image.open(infile) - - self.assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) def test_tiled_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" - im = Image.open(infile) - - self.assert_image_equal_tofile(im, "Tests/images/flower2.jpg") + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/flower2.jpg") def test_tiled_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" - im = Image.open(infile) - - self.assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) def test_old_style_jpeg(self): infile = "Tests/images/old-style-jpeg-compression.tif" - im = Image.open(infile) - - self.assert_image_equal_tofile( - im, "Tests/images/old-style-jpeg-compression.png" - ) + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") def test_no_rows_per_strip(self): # 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() - self.assertEqual(im.size, (950, 975)) + assert im.size == (950, 975) def test_orientation(self): - base_im = Image.open("Tests/images/g4_orientation_1.tif") + 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: + im.load() - for i in range(2, 9): - im = Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") - im.load() - - self.assert_image_similar(base_im, im, 0.7) + assert_image_similar(base_im, im, 0.7) def test_sampleformat_not_corrupted(self): # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted @@ -856,3 +878,13 @@ class TestFileLibTiff(LibTiffTestCase): out.seek(0) with Image.open(out) as im: im.load() + + def test_realloc_overflow(self): + TiffImagePlugin.READ_LIBTIFF = True + with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: + with pytest.raises(OSError) as e: + im.load() + + # Assert that the error code is IMAGING_CODEC_MEMORY + assert str(e.value) == "-9" + TiffImagePlugin.READ_LIBTIFF = False diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 2eabc60fd..03137c8b6 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -7,41 +7,38 @@ 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. + """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. - It was fixed by forcing an lseek to the beginning of the - file just before reading in libtiff. These tests remain - to ensure that it stays fixed. """ + It was fixed by forcing an lseek to the beginning of the + file just before reading in libtiff. These tests remain + to ensure that it stays fixed.""" - def test_g4_hopper_file(self): + def test_g4_hopper_file(self, tmp_path): """Testing the open file load path""" test_file = "Tests/images/hopper_g4.tif" with open(test_file, "rb") as f: - im = Image.open(f) + with Image.open(f) as im: + assert im.size == (128, 128) + self._assert_noerr(tmp_path, im) - self.assertEqual(im.size, (128, 128)) - self._assert_noerr(im) - - def test_g4_hopper_bytesio(self): + def test_g4_hopper_bytesio(self, tmp_path): """Testing the bytesio loading code path""" test_file = "Tests/images/hopper_g4.tif" s = BytesIO() with open(test_file, "rb") as f: s.write(f.read()) s.seek(0) - im = Image.open(s) + with Image.open(s) as im: + assert im.size == (128, 128) + self._assert_noerr(tmp_path, im) - self.assertEqual(im.size, (128, 128)) - self._assert_noerr(im) - - def test_g4_hopper(self): + def test_g4_hopper(self, tmp_path): """The 128x128 lena image failed for some reason.""" test_file = "Tests/images/hopper_g4.tif" - im = Image.open(test_file) - - self.assertEqual(im.size, (128, 128)) - self._assert_noerr(im) + with Image.open(test_file) as im: + assert im.size == (128, 128) + self._assert_noerr(tmp_path, im) diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index acc4ddb91..88c8f8f4f 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -1,28 +1,31 @@ +import pytest + from PIL import Image, McIdasImagePlugin -from .helper import PillowTestCase +from .helper import assert_image_equal -class TestFileMcIdas(PillowTestCase): - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, McIdasImagePlugin.McIdasImageFile, invalid_file) + with pytest.raises(SyntaxError): + McIdasImagePlugin.McIdasImageFile(invalid_file) - def test_valid_file(self): - # Arrange - # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 - # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ - test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara" - saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png" - # Act - im = Image.open(test_file) +def test_valid_file(): + # Arrange + # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 + # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ + test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara" + saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png" + + # Act + with Image.open(test_file) as im: im.load() # Assert - self.assertEqual(im.format, "MCIDAS") - self.assertEqual(im.mode, "I") - self.assertEqual(im.size, (1800, 400)) - im2 = Image.open(saved_file) - self.assert_image_equal(im, im2) + assert im.format == "MCIDAS" + assert im.mode == "I" + assert im.size == (1800, 400) + with Image.open(saved_file) as im2: + assert_image_equal(im, im2) diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 00f42fa4a..464d138e2 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -1,65 +1,63 @@ -import unittest +import pytest -from PIL import Image, ImagePalette, features +from PIL import Image, ImagePalette -from .helper import PillowTestCase, hopper - -try: - from PIL import MicImagePlugin -except ImportError: - olefile_installed = False -else: - olefile_installed = True +from .helper import assert_image_similar, hopper, skip_unless_feature +MicImagePlugin = pytest.importorskip( + "PIL.MicImagePlugin", reason="olefile not installed" +) +pytestmark = skip_unless_feature("libtiff") TEST_FILE = "Tests/images/hopper.mic" -@unittest.skipUnless(olefile_installed, "olefile package not installed") -@unittest.skipUnless(features.check("libtiff"), "libtiff not installed") -class TestFileMic(PillowTestCase): - def test_sanity(self): - with Image.open(TEST_FILE) as im: - im.load() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "MIC") +def test_sanity(): + with Image.open(TEST_FILE) as im: + im.load() + assert im.mode == "RGBA" + assert im.size == (128, 128) + assert im.format == "MIC" - # Adjust for the gamma of 2.2 encoded into the file - lut = ImagePalette.make_gamma_lut(1 / 2.2) - im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) + # Adjust for the gamma of 2.2 encoded into the file + lut = ImagePalette.make_gamma_lut(1 / 2.2) + im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) - im2 = hopper("RGBA") - self.assert_image_similar(im, im2, 10) + im2 = hopper("RGBA") + assert_image_similar(im, im2, 10) - def test_n_frames(self): - with Image.open(TEST_FILE) as im: - self.assertEqual(im.n_frames, 1) +def test_n_frames(): + with Image.open(TEST_FILE) as im: + assert im.n_frames == 1 - def test_is_animated(self): - with Image.open(TEST_FILE) as im: - self.assertFalse(im.is_animated) +def test_is_animated(): + with Image.open(TEST_FILE) as im: + assert not im.is_animated - def test_tell(self): - with Image.open(TEST_FILE) as im: - self.assertEqual(im.tell(), 0) +def test_tell(): + with Image.open(TEST_FILE) as im: + assert im.tell() == 0 - def test_seek(self): - with Image.open(TEST_FILE) as im: - im.seek(0) - self.assertEqual(im.tell(), 0) +def test_seek(): + with Image.open(TEST_FILE) as im: + im.seek(0) + assert im.tell() == 0 - self.assertRaises(EOFError, im.seek, 99) - self.assertEqual(im.tell(), 0) + with pytest.raises(EOFError): + im.seek(99) + assert im.tell() == 0 - def test_invalid_file(self): - # Test an invalid OLE file - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, MicImagePlugin.MicImageFile, invalid_file) - # Test a valid OLE file, but not a MIC file - ole_file = "Tests/images/test-ole-file.doc" - self.assertRaises(SyntaxError, MicImagePlugin.MicImageFile, ole_file) +def test_invalid_file(): + # Test an invalid OLE file + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + MicImagePlugin.MicImageFile(invalid_file) + + # Test a valid OLE file, but not a MIC file + ole_file = "Tests/images/test-ole-file.doc" + with pytest.raises(SyntaxError): + MicImagePlugin.MicImageFile(ole_file) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 9c8a2b468..791efcc3f 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,194 +1,218 @@ -import unittest from io import BytesIO +import pytest + from PIL import Image -from .helper import PillowTestCase, is_pypy +from .helper import assert_image_similar, is_pypy, skip_unless_feature test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] +pytestmark = skip_unless_feature("jpg") -class TestFileMpo(PillowTestCase): - def setUp(self): - codecs = dir(Image.core) - if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: - self.skipTest("jpeg support not available") - def frame_roundtrip(self, im, **options): - # Note that for now, there is no MPO saving functionality - out = BytesIO() - im.save(out, "MPO", **options) - test_bytes = out.tell() - out.seek(0) - im = Image.open(out) - im.bytes = test_bytes # for testing only - return im +def frame_roundtrip(im, **options): + # Note that for now, there is no MPO saving functionality + out = BytesIO() + im.save(out, "MPO", **options) + test_bytes = out.tell() + out.seek(0) + im = Image.open(out) + im.bytes = test_bytes # for testing only + return im - def test_sanity(self): - for test_file in test_files: - with Image.open(test_file) as im: - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (640, 480)) - self.assertEqual(im.format, "MPO") - @unittest.skipIf(is_pypy(), "Requires CPython") - def test_unclosed_file(self): - def open(): - im = Image.open(test_files[0]) +def test_sanity(): + for test_file in test_files: + with Image.open(test_file) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (640, 480) + assert im.format == "MPO" + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + im = Image.open(test_files[0]) + im.load() + + pytest.warns(ResourceWarning, open) + + +def test_closed_file(): + def open(): + im = Image.open(test_files[0]) + im.load() + im.close() + + pytest.warns(None, open) + + +def test_context_manager(): + def open(): + with Image.open(test_files[0]) as im: im.load() - self.assert_warning(ResourceWarning, open) + pytest.warns(None, open) - def test_closed_file(self): - def open(): - im = Image.open(test_files[0]) - im.load() - im.close() - self.assert_warning(None, open) - - def test_context_manager(self): - def open(): - with Image.open(test_files[0]) as im: - im.load() - - self.assert_warning(None, open) - - def test_app(self): - for test_file in test_files: - # Test APP/COM reader (@PIL135) - with Image.open(test_file) as im: - self.assertEqual(im.applist[0][0], "APP1") - self.assertEqual(im.applist[1][0], "APP2") - self.assertEqual( - im.applist[1][1][:16], - b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00", - ) - self.assertEqual(len(im.applist), 2) - - def test_exif(self): - for test_file in test_files: - with Image.open(test_file) as im: - info = im._getexif() - self.assertEqual(info[272], "Nintendo 3DS") - self.assertEqual(info[296], 2) - self.assertEqual(info[34665], 188) - - def test_frame_size(self): - # 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: - self.assertEqual(im.size, (640, 480)) - - im.seek(1) - self.assertEqual(im.size, (680, 480)) - - def test_parallax(self): - # Nintendo - with Image.open("Tests/images/sugarshack.mpo") as im: - exif = im.getexif() - self.assertEqual( - exif.get_ifd(0x927C)[0x1101]["Parallax"], -44.798187255859375 +def test_app(): + for test_file in test_files: + # Test APP/COM reader (@PIL135) + with Image.open(test_file) as im: + assert im.applist[0][0] == "APP1" + assert im.applist[1][0] == "APP2" + assert ( + im.applist[1][1][:16] + == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" ) + assert len(im.applist) == 2 - # Fujifilm - with Image.open("Tests/images/fujifilm.mpo") as im: - im.seek(1) - exif = im.getexif() - self.assertEqual(exif.get_ifd(0x927C)[0xB211], -3.125) - def test_mp(self): - for test_file in test_files: - with Image.open(test_file) as im: - mpinfo = im._getmp() - self.assertEqual(mpinfo[45056], b"0100") - self.assertEqual(mpinfo[45057], 2) +def test_exif(): + for test_file in test_files: + with Image.open(test_file) as im: + info = im._getexif() + assert info[272] == "Nintendo 3DS" + assert info[296] == 2 + assert info[34665] == 188 - def test_mp_offset(self): - # 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: + +def test_frame_size(): + # 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: + assert im.size == (640, 480) + + im.seek(1) + assert im.size == (680, 480) + + +def test_parallax(): + # Nintendo + with Image.open("Tests/images/sugarshack.mpo") as im: + exif = im.getexif() + assert exif.get_ifd(0x927C)[0x1101]["Parallax"] == -44.798187255859375 + + # Fujifilm + with Image.open("Tests/images/fujifilm.mpo") as im: + im.seek(1) + exif = im.getexif() + assert exif.get_ifd(0x927C)[0xB211] == -3.125 + + +def test_mp(): + for test_file in test_files: + with Image.open(test_file) as im: mpinfo = im._getmp() - self.assertEqual(mpinfo[45056], b"0100") - self.assertEqual(mpinfo[45057], 2) + assert mpinfo[45056] == b"0100" + assert mpinfo[45057] == 2 - def test_mp_attribute(self): - for test_file in test_files: - with Image.open(test_file) as im: - mpinfo = im._getmp() - frameNumber = 0 - for mpentry in mpinfo[45058]: - mpattr = mpentry["Attribute"] - if frameNumber: - self.assertFalse(mpattr["RepresentativeImageFlag"]) - else: - self.assertTrue(mpattr["RepresentativeImageFlag"]) - self.assertFalse(mpattr["DependentParentImageFlag"]) - self.assertFalse(mpattr["DependentChildImageFlag"]) - self.assertEqual(mpattr["ImageDataFormat"], "JPEG") - self.assertEqual(mpattr["MPType"], "Multi-Frame Image: (Disparity)") - self.assertEqual(mpattr["Reserved"], 0) - frameNumber += 1 - def test_seek(self): - for test_file in test_files: - with Image.open(test_file) as im: - self.assertEqual(im.tell(), 0) - # prior to first image raises an error, both blatant and borderline - self.assertRaises(EOFError, im.seek, -1) - self.assertRaises(EOFError, im.seek, -523) - # after the final image raises an error, - # both blatant and borderline - self.assertRaises(EOFError, im.seek, 2) - self.assertRaises(EOFError, im.seek, 523) - # bad calls shouldn't change the frame - self.assertEqual(im.tell(), 0) - # this one will work - im.seek(1) - self.assertEqual(im.tell(), 1) - # and this one, too - im.seek(0) - self.assertEqual(im.tell(), 0) +def test_mp_offset(): + # 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: + mpinfo = im._getmp() + assert mpinfo[45056] == b"0100" + assert mpinfo[45057] == 2 - def test_n_frames(self): - with Image.open("Tests/images/sugarshack.mpo") as im: - self.assertEqual(im.n_frames, 2) - self.assertTrue(im.is_animated) - def test_eoferror(self): - with Image.open("Tests/images/sugarshack.mpo") as im: - n_frames = im.n_frames +def test_mp_no_data(): + # 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: + with pytest.raises(ValueError): + im.seek(1) - # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) +def test_mp_attribute(): + for test_file in test_files: + with Image.open(test_file) as im: + mpinfo = im._getmp() + frameNumber = 0 + for mpentry in mpinfo[45058]: + mpattr = mpentry["Attribute"] + if frameNumber: + assert not mpattr["RepresentativeImageFlag"] + else: + assert mpattr["RepresentativeImageFlag"] + assert not mpattr["DependentParentImageFlag"] + assert not mpattr["DependentChildImageFlag"] + assert mpattr["ImageDataFormat"] == "JPEG" + assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" + assert mpattr["Reserved"] == 0 + frameNumber += 1 - def test_image_grab(self): - for test_file in test_files: - with Image.open(test_file) as im: - self.assertEqual(im.tell(), 0) - im0 = im.tobytes() - im.seek(1) - self.assertEqual(im.tell(), 1) - im1 = im.tobytes() - im.seek(0) - self.assertEqual(im.tell(), 0) - im02 = im.tobytes() - self.assertEqual(im0, im02) - self.assertNotEqual(im0, im1) - def test_save(self): - # Note that only individual frames can be saved at present - for test_file in test_files: - with Image.open(test_file) as im: - self.assertEqual(im.tell(), 0) - jpg0 = self.frame_roundtrip(im) - self.assert_image_similar(im, jpg0, 30) - im.seek(1) - self.assertEqual(im.tell(), 1) - jpg1 = self.frame_roundtrip(im) - self.assert_image_similar(im, jpg1, 30) +def test_seek(): + for test_file in test_files: + with Image.open(test_file) as im: + assert im.tell() == 0 + # prior to first image raises an error, both blatant and borderline + with pytest.raises(EOFError): + im.seek(-1) + with pytest.raises(EOFError): + im.seek(-523) + # after the final image raises an error, + # both blatant and borderline + with pytest.raises(EOFError): + im.seek(2) + with pytest.raises(EOFError): + im.seek(523) + # bad calls shouldn't change the frame + assert im.tell() == 0 + # this one will work + im.seek(1) + assert im.tell() == 1 + # and this one, too + im.seek(0) + assert im.tell() == 0 + + +def test_n_frames(): + with Image.open("Tests/images/sugarshack.mpo") as im: + assert im.n_frames == 2 + assert im.is_animated + + +def test_eoferror(): + with Image.open("Tests/images/sugarshack.mpo") as im: + n_frames = im.n_frames + + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames + + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) + + +def test_image_grab(): + for test_file in test_files: + with Image.open(test_file) as im: + assert im.tell() == 0 + im0 = im.tobytes() + im.seek(1) + assert im.tell() == 1 + im1 = im.tobytes() + im.seek(0) + assert im.tell() == 0 + im02 = im.tobytes() + assert im0 == im02 + assert im0 != im1 + + +def test_save(): + # Note that only individual frames can be saved at present + for test_file in test_files: + with Image.open(test_file) as im: + assert im.tell() == 0 + jpg0 = frame_roundtrip(im) + assert_image_similar(im, jpg0, 30) + im.seek(1) + assert im.tell() == 1 + jpg1 = frame_roundtrip(im) + assert_image_similar(im, jpg1, 30) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index d717ade16..293b856b0 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,79 +1,91 @@ import os -import unittest + +import pytest from PIL import Image, MspImagePlugin -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper TEST_FILE = "Tests/images/hopper.msp" EXTRA_DIR = "Tests/images/picins" YA_EXTRA_DIR = "Tests/images/msp" -class TestFileMsp(PillowTestCase): - def test_sanity(self): - test_file = self.tempfile("temp.msp") +def test_sanity(tmp_path): + test_file = str(tmp_path / "temp.msp") - hopper("1").save(test_file) + hopper("1").save(test_file) - im = Image.open(test_file) + with Image.open(test_file) as im: im.load() - self.assertEqual(im.mode, "1") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "MSP") + assert im.mode == "1" + assert im.size == (128, 128) + assert im.format == "MSP" - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, MspImagePlugin.MspImageFile, invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - def test_bad_checksum(self): - # Arrange - # This was created by forcing Pillow to save with checksum=0 - bad_checksum = "Tests/images/hopper_bad_checksum.msp" + with pytest.raises(SyntaxError): + MspImagePlugin.MspImageFile(invalid_file) - # Act / Assert - self.assertRaises(SyntaxError, MspImagePlugin.MspImageFile, bad_checksum) - def test_open_windows_v1(self): - # Arrange - # Act - im = Image.open(TEST_FILE) +def test_bad_checksum(): + # Arrange + # This was created by forcing Pillow to save with checksum=0 + bad_checksum = "Tests/images/hopper_bad_checksum.msp" + + # Act / Assert + with pytest.raises(SyntaxError): + MspImagePlugin.MspImageFile(bad_checksum) + + +def test_open_windows_v1(): + # Arrange + # Act + with Image.open(TEST_FILE) as im: # Assert - self.assert_image_equal(im, hopper("1")) - self.assertIsInstance(im, MspImagePlugin.MspImageFile) + assert_image_equal(im, hopper("1")) + assert isinstance(im, MspImagePlugin.MspImageFile) - def _assert_file_image_equal(self, source_path, target_path): - with Image.open(source_path) as im: - target = Image.open(target_path) - self.assert_image_equal(im, target) - @unittest.skipIf(not os.path.exists(EXTRA_DIR), "Extra image files not installed") - def test_open_windows_v2(self): +def _assert_file_image_equal(source_path, target_path): + with Image.open(source_path) as im: + with Image.open(target_path) as target: + assert_image_equal(im, target) - files = ( - os.path.join(EXTRA_DIR, f) - for f in os.listdir(EXTRA_DIR) - if os.path.splitext(f)[1] == ".msp" - ) - for path in files: - self._assert_file_image_equal(path, path.replace(".msp", ".png")) - @unittest.skipIf( - not os.path.exists(YA_EXTRA_DIR), "Even More Extra image files not installed" +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_open_windows_v2(): + + files = ( + os.path.join(EXTRA_DIR, f) + for f in os.listdir(EXTRA_DIR) + if os.path.splitext(f)[1] == ".msp" ) - def test_msp_v2(self): - for f in os.listdir(YA_EXTRA_DIR): - if ".MSP" not in f: - continue - path = os.path.join(YA_EXTRA_DIR, f) - self._assert_file_image_equal(path, path.replace(".MSP", ".png")) + for path in files: + _assert_file_image_equal(path, path.replace(".msp", ".png")) - def test_cannot_save_wrong_mode(self): - # Arrange - im = hopper() - filename = self.tempfile("temp.msp") - # Act/Assert - self.assertRaises(IOError, im.save, filename) +@pytest.mark.skipif( + not os.path.exists(YA_EXTRA_DIR), reason="Even More Extra image files not installed" +) +def test_msp_v2(): + for f in os.listdir(YA_EXTRA_DIR): + if ".MSP" not in f: + continue + path = os.path.join(YA_EXTRA_DIR, f) + _assert_file_image_equal(path, path.replace(".MSP", ".png")) + + +def test_cannot_save_wrong_mode(tmp_path): + # Arrange + im = hopper() + filename = str(tmp_path / "temp.msp") + + # Act/Assert + with pytest.raises(OSError): + im.save(filename) diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index fbfd89661..25d194b62 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,61 +1,85 @@ import os.path +import subprocess -from .helper import PillowTestCase, hopper, imagemagick_available +import pytest + +from PIL import Image + +from .helper import IMCONVERT, assert_image_equal, hopper, imagemagick_available + +_roundtrip = imagemagick_available() -class TestFilePalm(PillowTestCase): - _roundtrip = imagemagick_available() +def helper_save_as_palm(tmp_path, mode): + # Arrange + im = hopper(mode) + outfile = str(tmp_path / ("temp_" + mode + ".palm")) - def helper_save_as_palm(self, mode): - # Arrange - im = hopper(mode) - outfile = self.tempfile("temp_" + mode + ".palm") + # Act + im.save(outfile) - # Act - im.save(outfile) + # Assert + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 - # Assert - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) - def roundtrip(self, mode): - if not self._roundtrip: - return +def open_with_imagemagick(tmp_path, f): + if not imagemagick_available(): + raise OSError() - im = hopper(mode) - outfile = self.tempfile("temp.palm") + outfile = str(tmp_path / "temp.png") + rc = subprocess.call( + [IMCONVERT, f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + if rc: + raise OSError + return Image.open(outfile) - im.save(outfile) - converted = self.open_withImagemagick(outfile) - self.assert_image_equal(converted, im) - def test_monochrome(self): - # Arrange - mode = "1" +def roundtrip(tmp_path, mode): + if not _roundtrip: + return - # Act / Assert - self.helper_save_as_palm(mode) - self.roundtrip(mode) + im = hopper(mode) + outfile = str(tmp_path / "temp.palm") - def test_p_mode(self): - # Arrange - mode = "P" + im.save(outfile) + converted = open_with_imagemagick(tmp_path, outfile) + assert_image_equal(converted, im) - # Act / Assert - self.helper_save_as_palm(mode) - self.skipKnownBadTest("Palm P image is wrong") - self.roundtrip(mode) - def test_l_ioerror(self): - # Arrange - mode = "L" +def test_monochrome(tmp_path): + # Arrange + mode = "1" - # Act / Assert - self.assertRaises(IOError, self.helper_save_as_palm, mode) + # Act / Assert + helper_save_as_palm(tmp_path, mode) + roundtrip(tmp_path, mode) - def test_rgb_ioerror(self): - # Arrange - mode = "RGB" - # Act / Assert - self.assertRaises(IOError, self.helper_save_as_palm, mode) +@pytest.mark.xfail(reason="Palm P image is wrong") +def test_p_mode(tmp_path): + # Arrange + mode = "P" + + # Act / Assert + helper_save_as_palm(tmp_path, mode) + roundtrip(tmp_path, mode) + + +def test_l_oserror(tmp_path): + # Arrange + mode = "L" + + # Act / Assert + with pytest.raises(OSError): + helper_save_as_palm(tmp_path, mode) + + +def test_rgb_oserror(tmp_path): + # Arrange + mode = "RGB" + + # Act / Assert + 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 b23328ba5..dc45a48c1 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,18 +1,15 @@ from PIL import Image -from .helper import PillowTestCase - -class TestFilePcd(PillowTestCase): - def test_load_raw(self): - im = Image.open("Tests/images/hopper.pcd") +def test_load_raw(): + with Image.open("Tests/images/hopper.pcd") as im: im.load() # should not segfault. - # Note that this image was created with a resized hopper - # image, which was then converted to pcd with imagemagick - # and the colors are wonky in Pillow. It's unclear if this - # is a pillow or a convert issue, as other images not generated - # from convert look find on pillow and not imagemagick. + # Note that this image was created with a resized hopper + # image, which was then converted to pcd with imagemagick + # and the colors are wonky in Pillow. It's unclear if this + # is a pillow or a convert issue, as other images not generated + # from convert look find on pillow and not imagemagick. - # target = hopper().resize((768,512)) - # self.assert_image_similar(im, target, 10) + # target = hopper().resize((768,512)) + # assert_image_similar(im, target, 10) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index eb2c7d611..670c03b95 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,129 +1,143 @@ +import pytest + from PIL import Image, ImageFile, PcxImagePlugin -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper -class TestFilePcx(PillowTestCase): - def _roundtrip(self, im): - f = self.tempfile("temp.pcx") +def _roundtrip(tmp_path, im): + f = str(tmp_path / "temp.pcx") + im.save(f) + with Image.open(f) as im2: + assert im2.mode == im.mode + assert im2.size == im.size + assert im2.format == "PCX" + assert im2.get_format_mimetype() == "image/x-pcx" + assert_image_equal(im2, im) + + +def test_sanity(tmp_path): + for mode in ("1", "L", "P", "RGB"): + _roundtrip(tmp_path, hopper(mode)) + + # Test an unsupported mode + f = str(tmp_path / "temp.pcx") + im = hopper("RGBA") + with pytest.raises(ValueError): im.save(f) - im2 = Image.open(f) - self.assertEqual(im2.mode, im.mode) - self.assertEqual(im2.size, im.size) - self.assertEqual(im2.format, "PCX") - self.assertEqual(im2.get_format_mimetype(), "image/x-pcx") - self.assert_image_equal(im2, im) - def test_sanity(self): - for mode in ("1", "L", "P", "RGB"): - self._roundtrip(hopper(mode)) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - # Test an unsupported mode - f = self.tempfile("temp.pcx") - im = hopper("RGBA") - self.assertRaises(ValueError, im.save, f) + with pytest.raises(SyntaxError): + PcxImagePlugin.PcxImageFile(invalid_file) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, PcxImagePlugin.PcxImageFile, invalid_file) +def test_odd(tmp_path): + # See issue #523, odd sized images should have a stride that's even. + # Not that ImageMagick or GIMP write PCX that way. + # We were not handling properly. + for mode in ("1", "L", "P", "RGB"): + # larger, odd sized images are better here to ensure that + # we handle interrupted scan lines properly. + _roundtrip(tmp_path, hopper(mode).resize((511, 511))) - def test_odd(self): - # see issue #523, odd sized images should have a stride that's even. - # not that imagemagick or gimp write pcx that way. - # we were not handling properly. - for mode in ("1", "L", "P", "RGB"): - # larger, odd sized images are better here to ensure that - # we handle interrupted scan lines properly. - self._roundtrip(hopper(mode).resize((511, 511))) - def test_pil184(self): - # Check reading of files where xmin/xmax is not zero. +def test_pil184(): + # Check reading of files where xmin/xmax is not zero. - test_file = "Tests/images/pil184.pcx" - im = Image.open(test_file) - - self.assertEqual(im.size, (447, 144)) - self.assertEqual(im.tile[0][1], (0, 0, 447, 144)) + test_file = "Tests/images/pil184.pcx" + with Image.open(test_file) as im: + assert im.size == (447, 144) + assert im.tile[0][1] == (0, 0, 447, 144) # Make sure all pixels are either 0 or 255. - self.assertEqual(im.histogram()[0] + im.histogram()[255], 447 * 144) + assert im.histogram()[0] + im.histogram()[255] == 447 * 144 - def test_1px_width(self): - im = Image.new("L", (1, 256)) - px = im.load() - for y in range(256): - px[0, y] = y - self._roundtrip(im) - def test_large_count(self): - im = Image.new("L", (256, 1)) - px = im.load() +def test_1px_width(tmp_path): + im = Image.new("L", (1, 256)) + px = im.load() + for y in range(256): + px[0, y] = y + _roundtrip(tmp_path, im) + + +def test_large_count(tmp_path): + im = Image.new("L", (256, 1)) + px = im.load() + for x in range(256): + px[x, 0] = x // 67 * 67 + _roundtrip(tmp_path, im) + + +def _test_buffer_overflow(tmp_path, im, size=1024): + _last = ImageFile.MAXBLOCK + ImageFile.MAXBLOCK = size + try: + _roundtrip(tmp_path, im) + finally: + ImageFile.MAXBLOCK = _last + + +def test_break_in_count_overflow(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(4): for x in range(256): - px[x, 0] = x // 67 * 67 - self._roundtrip(im) + px[x, y] = x % 128 + _test_buffer_overflow(tmp_path, im) - def _test_buffer_overflow(self, im, size=1024): - _last = ImageFile.MAXBLOCK - ImageFile.MAXBLOCK = size - try: - self._roundtrip(im) - finally: - ImageFile.MAXBLOCK = _last - def test_break_in_count_overflow(self): - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(4): - for x in range(256): - px[x, y] = x % 128 - self._test_buffer_overflow(im) +def test_break_one_in_loop(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(5): + for x in range(256): + px[x, y] = x % 128 + _test_buffer_overflow(tmp_path, im) - def test_break_one_in_loop(self): - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(5): - for x in range(256): - px[x, y] = x % 128 - self._test_buffer_overflow(im) - def test_break_many_in_loop(self): - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(4): - for x in range(256): - px[x, y] = x % 128 - for x in range(8): - px[x, 4] = 16 - self._test_buffer_overflow(im) +def test_break_many_in_loop(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(4): + for x in range(256): + px[x, y] = x % 128 + for x in range(8): + px[x, 4] = 16 + _test_buffer_overflow(tmp_path, im) - def test_break_one_at_end(self): - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(5): - for x in range(256): - px[x, y] = x % 128 - px[0, 3] = 128 + 64 - self._test_buffer_overflow(im) - def test_break_many_at_end(self): - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(5): - for x in range(256): - px[x, y] = x % 128 - for x in range(4): - px[x * 2, 3] = 128 + 64 - px[x + 256 - 4, 3] = 0 - self._test_buffer_overflow(im) +def test_break_one_at_end(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(5): + for x in range(256): + px[x, y] = x % 128 + px[0, 3] = 128 + 64 + _test_buffer_overflow(tmp_path, im) - def test_break_padding(self): - im = Image.new("L", (257, 5)) - px = im.load() - for y in range(5): - for x in range(257): - px[x, y] = x % 128 - for x in range(5): - px[x, 3] = 0 - self._test_buffer_overflow(im) + +def test_break_many_at_end(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(5): + for x in range(256): + px[x, y] = x % 128 + for x in range(4): + px[x * 2, 3] = 128 + 64 + px[x + 256 - 4, 3] = 0 + _test_buffer_overflow(tmp_path, im) + + +def test_break_padding(tmp_path): + im = Image.new("L", (257, 5)) + px = im.load() + for y in range(5): + for x in range(257): + px[x, y] = x % 128 + for x in range(5): + px[x, 3] = 0 + _test_buffer_overflow(tmp_path, im) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 4b5897589..3e23beae7 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -4,272 +4,284 @@ import os.path import tempfile import time +import pytest + from PIL import Image, PdfParser -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestFilePdf(PillowTestCase): - def helper_save_as_pdf(self, mode, **kwargs): - # Arrange - im = hopper(mode) - outfile = self.tempfile("temp_" + mode + ".pdf") +def helper_save_as_pdf(tmp_path, mode, **kwargs): + # Arrange + im = hopper(mode) + outfile = str(tmp_path / ("temp_" + mode + ".pdf")) - # Act - im.save(outfile, **kwargs) + # Act + im.save(outfile, **kwargs) - # Assert - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) - with PdfParser.PdfParser(outfile) as pdf: - if kwargs.get("append_images", False) or kwargs.get("append", False): - self.assertGreater(len(pdf.pages), 1) - else: - self.assertGreater(len(pdf.pages), 0) - with open(outfile, "rb") as fp: - contents = fp.read() - size = tuple( - int(d) - for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() - ) - self.assertEqual(im.size, size) + # Assert + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + with PdfParser.PdfParser(outfile) as pdf: + if kwargs.get("append_images", False) or kwargs.get("append", False): + assert len(pdf.pages) > 1 + else: + assert len(pdf.pages) > 0 + with open(outfile, "rb") as fp: + contents = fp.read() + size = tuple( + int(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert im.size == size - return outfile + return outfile - def test_monochrome(self): - # Arrange - mode = "1" - # Act / Assert - self.helper_save_as_pdf(mode) +def test_monochrome(tmp_path): + # Arrange + mode = "1" - def test_greyscale(self): - # Arrange - mode = "L" + # Act / Assert + helper_save_as_pdf(tmp_path, mode) - # Act / Assert - self.helper_save_as_pdf(mode) - def test_rgb(self): - # Arrange - mode = "RGB" +def test_greyscale(tmp_path): + # Arrange + mode = "L" - # Act / Assert - self.helper_save_as_pdf(mode) + # Act / Assert + helper_save_as_pdf(tmp_path, mode) - def test_p_mode(self): - # Arrange - mode = "P" - # Act / Assert - self.helper_save_as_pdf(mode) +def test_rgb(tmp_path): + # Arrange + mode = "RGB" - def test_cmyk_mode(self): - # Arrange - mode = "CMYK" + # Act / Assert + helper_save_as_pdf(tmp_path, mode) - # Act / Assert - self.helper_save_as_pdf(mode) - def test_unsupported_mode(self): - im = hopper("LA") - outfile = self.tempfile("temp_LA.pdf") +def test_p_mode(tmp_path): + # Arrange + mode = "P" - self.assertRaises(ValueError, im.save, outfile) + # Act / Assert + helper_save_as_pdf(tmp_path, mode) - def test_save_all(self): - # Single frame image - self.helper_save_as_pdf("RGB", save_all=True) - # Multiframe image - with Image.open("Tests/images/dispose_bgnd.gif") as im: +def test_cmyk_mode(tmp_path): + # Arrange + mode = "CMYK" - outfile = self.tempfile("temp.pdf") - im.save(outfile, save_all=True) + # Act / Assert + helper_save_as_pdf(tmp_path, mode) - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) - # Append images - ims = [hopper()] - im.copy().save(outfile, save_all=True, append_images=ims) +def test_unsupported_mode(tmp_path): + im = hopper("LA") + outfile = str(tmp_path / "temp_LA.pdf") - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + with pytest.raises(ValueError): + im.save(outfile) - # Test appending using a generator - def imGenerator(ims): - yield from ims - im.save(outfile, save_all=True, append_images=imGenerator(ims)) +def test_save_all(tmp_path): + # Single frame image + helper_save_as_pdf(tmp_path, "RGB", save_all=True) - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: - # Append JPEG images - jpeg = Image.open("Tests/images/flower.jpg") + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, save_all=True) + + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + # Append images + ims = [hopper()] + im.copy().save(outfile, save_all=True, append_images=ims) + + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + # Test appending using a generator + def imGenerator(ims): + yield from ims + + im.save(outfile, save_all=True, append_images=imGenerator(ims)) + + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + # Append JPEG images + with Image.open("Tests/images/flower.jpg") as jpeg: jpeg.save(outfile, save_all=True, append_images=[jpeg.copy()]) - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 - def test_multiframe_normal_save(self): - # Test saving a multiframe image without save_all - with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = self.tempfile("temp.pdf") - im.save(outfile) +def test_multiframe_normal_save(tmp_path): + # Test saving a multiframe image without save_all + with Image.open("Tests/images/dispose_bgnd.gif") as im: - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + outfile = str(tmp_path / "temp.pdf") + im.save(outfile) - def test_pdf_open(self): - # fail on a buffer full of null bytes - self.assertRaises( - PdfParser.PdfFormatError, PdfParser.PdfParser, buf=bytearray(65536) - ) + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 - # make an empty PDF object - with PdfParser.PdfParser() as empty_pdf: - self.assertEqual(len(empty_pdf.pages), 0) - self.assertEqual(len(empty_pdf.info), 0) - self.assertFalse(empty_pdf.should_close_buf) - self.assertFalse(empty_pdf.should_close_file) - # make a PDF file - pdf_filename = self.helper_save_as_pdf("RGB") +def test_pdf_open(tmp_path): + # fail on a buffer full of null bytes + with pytest.raises(PdfParser.PdfFormatError): + PdfParser.PdfParser(buf=bytearray(65536)) - # open the PDF file - with PdfParser.PdfParser(filename=pdf_filename) as hopper_pdf: - self.assertEqual(len(hopper_pdf.pages), 1) - self.assertTrue(hopper_pdf.should_close_buf) - self.assertTrue(hopper_pdf.should_close_file) + # make an empty PDF object + with PdfParser.PdfParser() as empty_pdf: + assert len(empty_pdf.pages) == 0 + assert len(empty_pdf.info) == 0 + assert not empty_pdf.should_close_buf + assert not empty_pdf.should_close_file - # read a PDF file from a buffer with a non-zero offset - with open(pdf_filename, "rb") as f: - content = b"xyzzy" + f.read() - with PdfParser.PdfParser(buf=content, start_offset=5) as hopper_pdf: - self.assertEqual(len(hopper_pdf.pages), 1) - self.assertFalse(hopper_pdf.should_close_buf) - self.assertFalse(hopper_pdf.should_close_file) + # make a PDF file + pdf_filename = helper_save_as_pdf(tmp_path, "RGB") - # read a PDF file from an already open file - with open(pdf_filename, "rb") as f: - with PdfParser.PdfParser(f=f) as hopper_pdf: - self.assertEqual(len(hopper_pdf.pages), 1) - self.assertTrue(hopper_pdf.should_close_buf) - self.assertFalse(hopper_pdf.should_close_file) + # open the PDF file + with PdfParser.PdfParser(filename=pdf_filename) as hopper_pdf: + assert len(hopper_pdf.pages) == 1 + assert hopper_pdf.should_close_buf + assert hopper_pdf.should_close_file - def test_pdf_append_fails_on_nonexistent_file(self): - im = hopper("RGB") - with tempfile.TemporaryDirectory() as temp_dir: - self.assertRaises( - IOError, im.save, os.path.join(temp_dir, "nonexistent.pdf"), append=True - ) + # read a PDF file from a buffer with a non-zero offset + with open(pdf_filename, "rb") as f: + content = b"xyzzy" + f.read() + with PdfParser.PdfParser(buf=content, start_offset=5) as hopper_pdf: + assert len(hopper_pdf.pages) == 1 + assert not hopper_pdf.should_close_buf + assert not hopper_pdf.should_close_file - def check_pdf_pages_consistency(self, pdf): - pages_info = pdf.read_indirect(pdf.pages_ref) - self.assertNotIn(b"Parent", pages_info) - self.assertIn(b"Kids", pages_info) - kids_not_used = pages_info[b"Kids"] - for page_ref in pdf.pages: - while True: - if page_ref in kids_not_used: - kids_not_used.remove(page_ref) - page_info = pdf.read_indirect(page_ref) - self.assertIn(b"Parent", page_info) - page_ref = page_info[b"Parent"] - if page_ref == pdf.pages_ref: - break - self.assertEqual(pdf.pages_ref, page_info[b"Parent"]) - self.assertEqual(kids_not_used, []) + # read a PDF file from an already open file + with open(pdf_filename, "rb") as f: + with PdfParser.PdfParser(f=f) as hopper_pdf: + assert len(hopper_pdf.pages) == 1 + assert hopper_pdf.should_close_buf + assert not hopper_pdf.should_close_file - def test_pdf_append(self): - # make a PDF file - pdf_filename = self.helper_save_as_pdf("RGB", producer="PdfParser") - # open it, check pages and info - with PdfParser.PdfParser(pdf_filename, mode="r+b") as pdf: - self.assertEqual(len(pdf.pages), 1) - self.assertEqual(len(pdf.info), 4) - self.assertEqual( - pdf.info.Title, os.path.splitext(os.path.basename(pdf_filename))[0] - ) - self.assertEqual(pdf.info.Producer, "PdfParser") - self.assertIn(b"CreationDate", pdf.info) - self.assertIn(b"ModDate", pdf.info) - self.check_pdf_pages_consistency(pdf) +def test_pdf_append_fails_on_nonexistent_file(): + im = hopper("RGB") + with tempfile.TemporaryDirectory() as temp_dir: + with pytest.raises(OSError): + im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) - # append some info - pdf.info.Title = "abc" - pdf.info.Author = "def" - pdf.info.Subject = "ghi\uABCD" - pdf.info.Keywords = "qw)e\\r(ty" - pdf.info.Creator = "hopper()" - pdf.start_writing() - pdf.write_xref_and_trailer() - # open it again, check pages and info again - with PdfParser.PdfParser(pdf_filename) as pdf: - self.assertEqual(len(pdf.pages), 1) - self.assertEqual(len(pdf.info), 8) - self.assertEqual(pdf.info.Title, "abc") - self.assertIn(b"CreationDate", pdf.info) - self.assertIn(b"ModDate", pdf.info) - self.check_pdf_pages_consistency(pdf) +def check_pdf_pages_consistency(pdf): + pages_info = pdf.read_indirect(pdf.pages_ref) + assert b"Parent" not in pages_info + assert b"Kids" in pages_info + kids_not_used = pages_info[b"Kids"] + for page_ref in pdf.pages: + while True: + if page_ref in kids_not_used: + kids_not_used.remove(page_ref) + page_info = pdf.read_indirect(page_ref) + assert b"Parent" in page_info + page_ref = page_info[b"Parent"] + if page_ref == pdf.pages_ref: + break + assert pdf.pages_ref == page_info[b"Parent"] + assert kids_not_used == [] - # append two images - mode_CMYK = hopper("CMYK") - mode_P = hopper("P") - mode_CMYK.save(pdf_filename, append=True, save_all=True, append_images=[mode_P]) - # open the PDF again, check pages and info again - with PdfParser.PdfParser(pdf_filename) as pdf: - self.assertEqual(len(pdf.pages), 3) - self.assertEqual(len(pdf.info), 8) - self.assertEqual(PdfParser.decode_text(pdf.info[b"Title"]), "abc") - self.assertEqual(pdf.info.Title, "abc") - self.assertEqual(pdf.info.Producer, "PdfParser") - self.assertEqual(pdf.info.Keywords, "qw)e\\r(ty") - self.assertEqual(pdf.info.Subject, "ghi\uABCD") - self.assertIn(b"CreationDate", pdf.info) - self.assertIn(b"ModDate", pdf.info) - self.check_pdf_pages_consistency(pdf) +def test_pdf_append(tmp_path): + # make a PDF file + pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser") - def test_pdf_info(self): - # make a PDF file - pdf_filename = self.helper_save_as_pdf( - "RGB", - title="title", - author="author", - subject="subject", - keywords="keywords", - creator="creator", - producer="producer", - creationDate=time.strptime("2000", "%Y"), - modDate=time.strptime("2001", "%Y"), - ) + # open it, check pages and info + with PdfParser.PdfParser(pdf_filename, mode="r+b") as pdf: + assert len(pdf.pages) == 1 + assert len(pdf.info) == 4 + assert pdf.info.Title == os.path.splitext(os.path.basename(pdf_filename))[0] + assert pdf.info.Producer == "PdfParser" + assert b"CreationDate" in pdf.info + assert b"ModDate" in pdf.info + check_pdf_pages_consistency(pdf) - # open it, check pages and info - with PdfParser.PdfParser(pdf_filename) as pdf: - self.assertEqual(len(pdf.info), 8) - self.assertEqual(pdf.info.Title, "title") - self.assertEqual(pdf.info.Author, "author") - self.assertEqual(pdf.info.Subject, "subject") - self.assertEqual(pdf.info.Keywords, "keywords") - self.assertEqual(pdf.info.Creator, "creator") - self.assertEqual(pdf.info.Producer, "producer") - self.assertEqual(pdf.info.CreationDate, time.strptime("2000", "%Y")) - self.assertEqual(pdf.info.ModDate, time.strptime("2001", "%Y")) - self.check_pdf_pages_consistency(pdf) + # append some info + pdf.info.Title = "abc" + pdf.info.Author = "def" + pdf.info.Subject = "ghi\uABCD" + pdf.info.Keywords = "qw)e\\r(ty" + pdf.info.Creator = "hopper()" + pdf.start_writing() + pdf.write_xref_and_trailer() - def test_pdf_append_to_bytesio(self): - im = hopper("RGB") - f = io.BytesIO() - im.save(f, format="PDF") - initial_size = len(f.getvalue()) - self.assertGreater(initial_size, 0) - im = hopper("P") - f = io.BytesIO(f.getvalue()) - im.save(f, format="PDF", append=True) - self.assertGreater(len(f.getvalue()), initial_size) + # open it again, check pages and info again + with PdfParser.PdfParser(pdf_filename) as pdf: + assert len(pdf.pages) == 1 + assert len(pdf.info) == 8 + assert pdf.info.Title == "abc" + assert b"CreationDate" in pdf.info + assert b"ModDate" in pdf.info + check_pdf_pages_consistency(pdf) + + # append two images + mode_CMYK = hopper("CMYK") + mode_P = hopper("P") + mode_CMYK.save(pdf_filename, append=True, save_all=True, append_images=[mode_P]) + + # open the PDF again, check pages and info again + with PdfParser.PdfParser(pdf_filename) as pdf: + assert len(pdf.pages) == 3 + assert len(pdf.info) == 8 + assert PdfParser.decode_text(pdf.info[b"Title"]) == "abc" + assert pdf.info.Title == "abc" + assert pdf.info.Producer == "PdfParser" + assert pdf.info.Keywords == "qw)e\\r(ty" + assert pdf.info.Subject == "ghi\uABCD" + assert b"CreationDate" in pdf.info + assert b"ModDate" in pdf.info + check_pdf_pages_consistency(pdf) + + +def test_pdf_info(tmp_path): + # make a PDF file + pdf_filename = helper_save_as_pdf( + tmp_path, + "RGB", + title="title", + author="author", + subject="subject", + keywords="keywords", + creator="creator", + producer="producer", + creationDate=time.strptime("2000", "%Y"), + modDate=time.strptime("2001", "%Y"), + ) + + # open it, check pages and info + with PdfParser.PdfParser(pdf_filename) as pdf: + assert len(pdf.info) == 8 + assert pdf.info.Title == "title" + assert pdf.info.Author == "author" + assert pdf.info.Subject == "subject" + assert pdf.info.Keywords == "keywords" + assert pdf.info.Creator == "creator" + assert pdf.info.Producer == "producer" + assert pdf.info.CreationDate == time.strptime("2000", "%Y") + assert pdf.info.ModDate == time.strptime("2001", "%Y") + check_pdf_pages_consistency(pdf) + + +def test_pdf_append_to_bytesio(): + im = hopper("RGB") + f = io.BytesIO() + im.save(f, format="PDF") + initial_size = len(f.getvalue()) + assert initial_size > 0 + im = hopper("P") + f = io.BytesIO(f.getvalue()) + im.save(f, format="PDF", append=True) + assert len(f.getvalue()) > initial_size diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index c744932d4..315ea4676 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -1,23 +1,26 @@ +import pytest + from PIL import Image, PixarImagePlugin -from .helper import PillowTestCase, hopper +from .helper import assert_image_similar, hopper TEST_FILE = "Tests/images/hopper.pxr" -class TestFilePixar(PillowTestCase): - def test_sanity(self): - im = Image.open(TEST_FILE) +def test_sanity(): + with Image.open(TEST_FILE) as im: im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PIXAR") - self.assertIsNone(im.get_format_mimetype()) + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "PIXAR" + assert im.get_format_mimetype() is None im2 = hopper() - self.assert_image_similar(im, im2, 4.8) + assert_image_similar(im, im2, 4.8) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, PixarImagePlugin.PixarImageFile, invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + PixarImagePlugin.PixarImageFile(invalid_file) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index c9c337e64..80305c7b4 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,20 +1,20 @@ -import unittest +import re import zlib from io import BytesIO -from PIL import Image, ImageFile, PngImagePlugin +import pytest -from .helper import PillowLeakTestCase, PillowTestCase, hopper, is_win32 - -try: - from PIL import _webp - - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False - -codecs = dir(Image.core) +from PIL import Image, ImageFile, PngImagePlugin, features +from .helper import ( + PillowLeakTestCase, + assert_image, + assert_image_equal, + hopper, + is_big_endian, + is_win32, + skip_unless_feature, +) # sample png stream @@ -52,11 +52,8 @@ def roundtrip(im, **options): return Image.open(out) -class TestFilePng(PillowTestCase): - def setUp(self): - if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - self.skipTest("zip/deflate support not available") - +@skip_unless_feature("zlib") +class TestFilePng: def get_chunks(self, filename): chunks = [] with open(filename, "rb") as fp: @@ -72,277 +69,272 @@ class TestFilePng(PillowTestCase): png.crc(cid, s) return chunks - def test_sanity(self): + @pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") + def test_sanity(self, tmp_path): # internal version number - self.assertRegex(Image.core.zlib_version, r"\d+\.\d+\.\d+(\.\d+)?$") + assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") hopper("RGB").save(test_file) with Image.open(test_file) as im: im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PNG") - self.assertEqual(im.get_format_mimetype(), "image/png") + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "PNG" + assert im.get_format_mimetype() == "image/png" for mode in ["1", "L", "P", "RGB", "I", "I;16"]: im = hopper(mode) im.save(test_file) - reloaded = Image.open(test_file) - if mode == "I;16": - reloaded = reloaded.convert(mode) - self.assert_image_equal(reloaded, im) + with Image.open(test_file) as reloaded: + if mode == "I;16": + reloaded = reloaded.convert(mode) + assert_image_equal(reloaded, im) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, invalid_file) + with pytest.raises(SyntaxError): + PngImagePlugin.PngImageFile(invalid_file) def test_broken(self): # Check reading of totally broken files. In this case, the test # file was checked into Subversion as a text file. test_file = "Tests/images/broken.png" - self.assertRaises(IOError, Image.open, test_file) + with pytest.raises(OSError): + Image.open(test_file) def test_bad_text(self): # Make sure PIL can read malformed tEXt chunks (@PIL152) im = load(HEAD + chunk(b"tEXt") + TAIL) - self.assertEqual(im.info, {}) + assert im.info == {} im = load(HEAD + chunk(b"tEXt", b"spam") + TAIL) - self.assertEqual(im.info, {"spam": ""}) + assert im.info == {"spam": ""} im = load(HEAD + chunk(b"tEXt", b"spam\0") + TAIL) - self.assertEqual(im.info, {"spam": ""}) + assert im.info == {"spam": ""} im = load(HEAD + chunk(b"tEXt", b"spam\0egg") + TAIL) - self.assertEqual(im.info, {"spam": "egg"}) + assert im.info == {"spam": "egg"} im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL) - self.assertEqual(im.info, {"spam": "egg\x00"}) + assert im.info == {"spam": "egg\x00"} def test_bad_ztxt(self): # Test reading malformed zTXt chunks (python-pillow/Pillow#318) im = load(HEAD + chunk(b"zTXt") + TAIL) - self.assertEqual(im.info, {}) + assert im.info == {} im = load(HEAD + chunk(b"zTXt", b"spam") + TAIL) - self.assertEqual(im.info, {"spam": ""}) + assert im.info == {"spam": ""} im = load(HEAD + chunk(b"zTXt", b"spam\0") + TAIL) - self.assertEqual(im.info, {"spam": ""}) + assert im.info == {"spam": ""} im = load(HEAD + chunk(b"zTXt", b"spam\0\0") + TAIL) - self.assertEqual(im.info, {"spam": ""}) + assert im.info == {"spam": ""} im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")[:1]) + TAIL) - self.assertEqual(im.info, {"spam": ""}) + assert im.info == {"spam": ""} im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL) - self.assertEqual(im.info, {"spam": "egg"}) + assert im.info == {"spam": "egg"} def test_bad_itxt(self): im = load(HEAD + chunk(b"iTXt") + TAIL) - self.assertEqual(im.info, {}) + assert im.info == {} im = load(HEAD + chunk(b"iTXt", b"spam") + TAIL) - self.assertEqual(im.info, {}) + assert im.info == {} im = load(HEAD + chunk(b"iTXt", b"spam\0") + TAIL) - self.assertEqual(im.info, {}) + assert im.info == {} im = load(HEAD + chunk(b"iTXt", b"spam\0\x02") + TAIL) - self.assertEqual(im.info, {}) + assert im.info == {} im = load(HEAD + chunk(b"iTXt", b"spam\0\0\0foo\0") + TAIL) - self.assertEqual(im.info, {}) + assert im.info == {} im = load(HEAD + chunk(b"iTXt", b"spam\0\0\0en\0Spam\0egg") + TAIL) - self.assertEqual(im.info, {"spam": "egg"}) - self.assertEqual(im.info["spam"].lang, "en") - self.assertEqual(im.info["spam"].tkey, "Spam") + assert im.info == {"spam": "egg"} + assert im.info["spam"].lang == "en" + assert im.info["spam"].tkey == "Spam" im = load( HEAD + chunk(b"iTXt", b"spam\0\1\0en\0Spam\0" + zlib.compress(b"egg")[:1]) + TAIL ) - self.assertEqual(im.info, {"spam": ""}) + assert im.info == {"spam": ""} im = load( HEAD + chunk(b"iTXt", b"spam\0\1\1en\0Spam\0" + zlib.compress(b"egg")) + TAIL ) - self.assertEqual(im.info, {}) + assert im.info == {} im = load( HEAD + chunk(b"iTXt", b"spam\0\1\0en\0Spam\0" + zlib.compress(b"egg")) + TAIL ) - self.assertEqual(im.info, {"spam": "egg"}) - self.assertEqual(im.info["spam"].lang, "en") - self.assertEqual(im.info["spam"].tkey, "Spam") + assert im.info == {"spam": "egg"} + assert im.info["spam"].lang == "en" + assert im.info["spam"].tkey == "Spam" def test_interlace(self): test_file = "Tests/images/pil123p.png" - im = Image.open(test_file) + with Image.open(test_file) as im: + assert_image(im, "P", (162, 150)) + assert im.info.get("interlace") - self.assert_image(im, "P", (162, 150)) - self.assertTrue(im.info.get("interlace")) - - im.load() + im.load() test_file = "Tests/images/pil123rgba.png" - im = Image.open(test_file) + with Image.open(test_file) as im: + assert_image(im, "RGBA", (162, 150)) + assert im.info.get("interlace") - self.assert_image(im, "RGBA", (162, 150)) - self.assertTrue(im.info.get("interlace")) - - im.load() + im.load() def test_load_transparent_p(self): test_file = "Tests/images/pil123p.png" - im = Image.open(test_file) - - self.assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - self.assert_image(im, "RGBA", (162, 150)) + with Image.open(test_file) as im: + assert_image(im, "P", (162, 150)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (162, 150)) # image has 124 unique alpha values - self.assertEqual(len(im.getchannel("A").getcolors()), 124) + assert len(im.getchannel("A").getcolors()) == 124 def test_load_transparent_rgb(self): test_file = "Tests/images/rgb_trns.png" - im = Image.open(test_file) - self.assertEqual(im.info["transparency"], (0, 255, 52)) + with Image.open(test_file) as im: + assert im.info["transparency"] == (0, 255, 52) - self.assert_image(im, "RGB", (64, 64)) - im = im.convert("RGBA") - self.assert_image(im, "RGBA", (64, 64)) + assert_image(im, "RGB", (64, 64)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (64, 64)) # image has 876 transparent pixels - self.assertEqual(im.getchannel("A").getcolors()[0][0], 876) + assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_palette(self): + def test_save_p_transparent_palette(self, tmp_path): in_file = "Tests/images/pil123p.png" - im = Image.open(in_file) + with Image.open(in_file) as im: + # 'transparency' contains a byte string with the opacity for + # each palette entry + assert len(im.info["transparency"]) == 256 - # 'transparency' contains a byte string with the opacity for - # each palette entry - self.assertEqual(len(im.info["transparency"]), 256) - - test_file = self.tempfile("temp.png") - im.save(test_file) + test_file = str(tmp_path / "temp.png") + im.save(test_file) # check if saved image contains same transparency - im = Image.open(test_file) - self.assertEqual(len(im.info["transparency"]), 256) + with Image.open(test_file) as im: + assert len(im.info["transparency"]) == 256 - self.assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - self.assert_image(im, "RGBA", (162, 150)) + assert_image(im, "P", (162, 150)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (162, 150)) # image has 124 unique alpha values - self.assertEqual(len(im.getchannel("A").getcolors()), 124) + assert len(im.getchannel("A").getcolors()) == 124 - def test_save_p_single_transparency(self): + def test_save_p_single_transparency(self, tmp_path): in_file = "Tests/images/p_trns_single.png" - im = Image.open(in_file) + with Image.open(in_file) as im: + # pixel value 164 is full transparent + assert im.info["transparency"] == 164 + assert im.getpixel((31, 31)) == 164 - # pixel value 164 is full transparent - self.assertEqual(im.info["transparency"], 164) - self.assertEqual(im.getpixel((31, 31)), 164) - - test_file = self.tempfile("temp.png") - im.save(test_file) + test_file = str(tmp_path / "temp.png") + im.save(test_file) # check if saved image contains same transparency - im = Image.open(test_file) - self.assertEqual(im.info["transparency"], 164) - self.assertEqual(im.getpixel((31, 31)), 164) - self.assert_image(im, "P", (64, 64)) - im = im.convert("RGBA") - self.assert_image(im, "RGBA", (64, 64)) + with Image.open(test_file) as im: + assert im.info["transparency"] == 164 + assert im.getpixel((31, 31)) == 164 + assert_image(im, "P", (64, 64)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (64, 64)) - self.assertEqual(im.getpixel((31, 31)), (0, 255, 52, 0)) + assert im.getpixel((31, 31)) == (0, 255, 52, 0) # image has 876 transparent pixels - self.assertEqual(im.getchannel("A").getcolors()[0][0], 876) + assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_black(self): + def test_save_p_transparent_black(self, tmp_path): # check if solid black image with full transparency # is supported (check for #1838) im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) - self.assertEqual(im.getcolors(), [(100, (0, 0, 0, 0))]) + assert im.getcolors() == [(100, (0, 0, 0, 0))] im = im.convert("P") - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") im.save(test_file) # check if saved image contains same transparency - im = Image.open(test_file) - self.assertEqual(len(im.info["transparency"]), 256) - self.assert_image(im, "P", (10, 10)) - im = im.convert("RGBA") - self.assert_image(im, "RGBA", (10, 10)) - self.assertEqual(im.getcolors(), [(100, (0, 0, 0, 0))]) + with Image.open(test_file) as im: + assert len(im.info["transparency"]) == 256 + assert_image(im, "P", (10, 10)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (10, 10)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] - def test_save_greyscale_transparency(self): + def test_save_greyscale_transparency(self, tmp_path): for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items(): in_file = "Tests/images/" + mode.lower() + "_trns.png" - im = Image.open(in_file) - self.assertEqual(im.mode, mode) - self.assertEqual(im.info["transparency"], 255) + with Image.open(in_file) as im: + assert im.mode == mode + assert im.info["transparency"] == 255 - im_rgba = im.convert("RGBA") - self.assertEqual(im_rgba.getchannel("A").getcolors()[0][0], num_transparent) + im_rgba = im.convert("RGBA") + assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") im.save(test_file) - test_im = Image.open(test_file) - self.assertEqual(test_im.mode, mode) - self.assertEqual(test_im.info["transparency"], 255) - self.assert_image_equal(im, test_im) + with Image.open(test_file) as test_im: + assert test_im.mode == mode + assert test_im.info["transparency"] == 255 + assert_image_equal(im, test_im) test_im_rgba = test_im.convert("RGBA") - self.assertEqual( - test_im_rgba.getchannel("A").getcolors()[0][0], num_transparent - ) + assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent - def test_save_rgb_single_transparency(self): + def test_save_rgb_single_transparency(self, tmp_path): in_file = "Tests/images/caption_6_33_22.png" - im = Image.open(in_file) - - test_file = self.tempfile("temp.png") - im.save(test_file) + with Image.open(in_file) as im: + test_file = str(tmp_path / "temp.png") + im.save(test_file) def test_load_verify(self): # Check open/load/verify exception (@PIL150) - im = Image.open(TEST_PNG_FILE) + with Image.open(TEST_PNG_FILE) as im: + # Assert that there is no unclosed file warning + pytest.warns(None, im.verify) - # Assert that there is no unclosed file warning - self.assert_warning(None, im.verify) - - im = Image.open(TEST_PNG_FILE) - im.load() - self.assertRaises(RuntimeError, im.verify) + with Image.open(TEST_PNG_FILE) as im: + im.load() + with pytest.raises(RuntimeError): + im.verify() def test_verify_struct_error(self): # Check open/load/verify exception (#1755) - # offsets to test, -10: breaks in i32() in read. (IOError) + # offsets to test, -10: breaks in i32() in read. (OSError) # -13: breaks in crc, txt chunk. # -14: malformed chunk @@ -350,9 +342,10 @@ class TestFilePng(PillowTestCase): with open(TEST_PNG_FILE, "rb") as f: test_file = f.read()[:offset] - im = Image.open(BytesIO(test_file)) - self.assertIsNotNone(im.fp) - self.assertRaises((IOError, SyntaxError), im.verify) + with Image.open(BytesIO(test_file)) as im: + assert im.fp is not None + with pytest.raises((OSError, SyntaxError)): + im.verify() def test_verify_ignores_crc_error(self): # check ignores crc errors in ancillary chunks @@ -361,12 +354,13 @@ class TestFilePng(PillowTestCase): broken_crc_chunk_data = chunk_data[:-1] + b"q" # break CRC image_data = HEAD + broken_crc_chunk_data + TAIL - self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, BytesIO(image_data)) + with pytest.raises(SyntaxError): + PngImagePlugin.PngImageFile(BytesIO(image_data)) ImageFile.LOAD_TRUNCATED_IMAGES = True try: im = load(image_data) - self.assertIsNotNone(im) + assert im is not None finally: ImageFile.LOAD_TRUNCATED_IMAGES = False @@ -377,50 +371,46 @@ class TestFilePng(PillowTestCase): ImageFile.LOAD_TRUNCATED_IMAGES = True try: - self.assertRaises( - SyntaxError, PngImagePlugin.PngImageFile, BytesIO(image_data) - ) + with pytest.raises(SyntaxError): + PngImagePlugin.PngImageFile(BytesIO(image_data)) finally: ImageFile.LOAD_TRUNCATED_IMAGES = False def test_roundtrip_dpi(self): # Check dpi roundtripping - im = Image.open(TEST_PNG_FILE) - - im = roundtrip(im, dpi=(100, 100)) - self.assertEqual(im.info["dpi"], (100, 100)) + with Image.open(TEST_PNG_FILE) as im: + im = roundtrip(im, dpi=(100, 100)) + assert im.info["dpi"] == (100, 100) def test_load_dpi_rounding(self): # Round up with Image.open(TEST_PNG_FILE) as im: - self.assertEqual(im.info["dpi"], (96, 96)) + assert im.info["dpi"] == (96, 96) # Round down with Image.open("Tests/images/icc_profile_none.png") as im: - self.assertEqual(im.info["dpi"], (72, 72)) + assert im.info["dpi"] == (72, 72) def test_save_dpi_rounding(self): - im = Image.open(TEST_PNG_FILE) - - im = roundtrip(im, dpi=(72.2, 72.2)) - self.assertEqual(im.info["dpi"], (72, 72)) + with Image.open(TEST_PNG_FILE) as im: + im = roundtrip(im, dpi=(72.2, 72.2)) + assert im.info["dpi"] == (72, 72) im = roundtrip(im, dpi=(72.8, 72.8)) - self.assertEqual(im.info["dpi"], (73, 73)) + assert im.info["dpi"] == (73, 73) def test_roundtrip_text(self): # Check text roundtripping - im = Image.open(TEST_PNG_FILE) + with Image.open(TEST_PNG_FILE) as im: + info = PngImagePlugin.PngInfo() + info.add_text("TXT", "VALUE") + info.add_text("ZIP", "VALUE", zip=True) - info = PngImagePlugin.PngInfo() - info.add_text("TXT", "VALUE") - info.add_text("ZIP", "VALUE", zip=True) - - im = roundtrip(im, pnginfo=info) - self.assertEqual(im.info, {"TXT": "VALUE", "ZIP": "VALUE"}) - self.assertEqual(im.text, {"TXT": "VALUE", "ZIP": "VALUE"}) + im = roundtrip(im, pnginfo=info) + assert im.info == {"TXT": "VALUE", "ZIP": "VALUE"} + assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} def test_roundtrip_itxt(self): # Check iTXt roundtripping @@ -431,12 +421,12 @@ class TestFilePng(PillowTestCase): info.add_text("eggs", PngImagePlugin.iTXt("Spam", "en", "Eggs"), zip=True) im = roundtrip(im, pnginfo=info) - self.assertEqual(im.info, {"spam": "Eggs", "eggs": "Spam"}) - self.assertEqual(im.text, {"spam": "Eggs", "eggs": "Spam"}) - self.assertEqual(im.text["spam"].lang, "en") - self.assertEqual(im.text["spam"].tkey, "Spam") - self.assertEqual(im.text["eggs"].lang, "en") - self.assertEqual(im.text["eggs"].tkey, "Eggs") + assert im.info == {"spam": "Eggs", "eggs": "Spam"} + assert im.text == {"spam": "Eggs", "eggs": "Spam"} + assert im.text["spam"].lang == "en" + assert im.text["spam"].tkey == "Spam" + assert im.text["eggs"].lang == "en" + assert im.text["eggs"].tkey == "Eggs" def test_nonunicode_text(self): # Check so that non-Unicode text is saved as a tEXt rather than iTXt @@ -445,7 +435,7 @@ class TestFilePng(PillowTestCase): info = PngImagePlugin.PngInfo() info.add_text("Text", "Ascii") im = roundtrip(im, pnginfo=info) - self.assertIsInstance(im.info["Text"], str) + assert isinstance(im.info["Text"], str) def test_unicode_text(self): # Check preservation of non-ASCII characters @@ -455,7 +445,7 @@ class TestFilePng(PillowTestCase): info = PngImagePlugin.PngInfo() info.add_text("Text", value) im = roundtrip(im, pnginfo=info) - self.assertEqual(im.info, {"Text": value}) + assert im.info == {"Text": value} rt_text(" Aa" + chr(0xA0) + chr(0xC4) + chr(0xFF)) # Latin1 rt_text(chr(0x400) + chr(0x472) + chr(0x4FF)) # Cyrillic @@ -473,188 +463,226 @@ class TestFilePng(PillowTestCase): data = b"\x89" + fd.read() pngfile = BytesIO(data) - self.assertRaises(IOError, Image.open, pngfile) + with pytest.raises(OSError): + Image.open(pngfile) def test_trns_rgb(self): # Check writing and reading of tRNS chunks for RGB images. # Independent file sample provided by Sebastian Spaeth. test_file = "Tests/images/caption_6_33_22.png" - im = Image.open(test_file) - self.assertEqual(im.info["transparency"], (248, 248, 248)) + with Image.open(test_file) as im: + assert im.info["transparency"] == (248, 248, 248) - # check saving transparency by default - im = roundtrip(im) - self.assertEqual(im.info["transparency"], (248, 248, 248)) + # check saving transparency by default + im = roundtrip(im) + assert im.info["transparency"] == (248, 248, 248) im = roundtrip(im, transparency=(0, 1, 2)) - self.assertEqual(im.info["transparency"], (0, 1, 2)) + assert im.info["transparency"] == (0, 1, 2) - def test_trns_p(self): + def test_trns_p(self, tmp_path): # Check writing a transparency of 0, issue #528 im = hopper("P") im.info["transparency"] = 0 - f = self.tempfile("temp.png") + f = str(tmp_path / "temp.png") im.save(f) - im2 = Image.open(f) - self.assertIn("transparency", im2.info) + with Image.open(f) as im2: + assert "transparency" in im2.info - self.assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) + assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) def test_trns_null(self): # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" with Image.open(test_file) as im: - self.assertEqual(im.info["transparency"], 0) + assert im.info["transparency"] == 0 def test_save_icc_profile(self): with Image.open("Tests/images/icc_profile_none.png") as im: - self.assertIsNone(im.info["icc_profile"]) + assert im.info["icc_profile"] is None with Image.open("Tests/images/icc_profile.png") as with_icc: expected_icc = with_icc.info["icc_profile"] im = roundtrip(im, icc_profile=expected_icc) - self.assertEqual(im.info["icc_profile"], expected_icc) + assert im.info["icc_profile"] == expected_icc def test_discard_icc_profile(self): - im = Image.open("Tests/images/icc_profile.png") - - im = roundtrip(im, icc_profile=None) - self.assertNotIn("icc_profile", im.info) + with Image.open("Tests/images/icc_profile.png") as im: + im = roundtrip(im, icc_profile=None) + assert "icc_profile" not in im.info def test_roundtrip_icc_profile(self): - im = Image.open("Tests/images/icc_profile.png") - expected_icc = im.info["icc_profile"] + with Image.open("Tests/images/icc_profile.png") as im: + expected_icc = im.info["icc_profile"] - im = roundtrip(im) - self.assertEqual(im.info["icc_profile"], expected_icc) + im = roundtrip(im) + assert im.info["icc_profile"] == expected_icc def test_roundtrip_no_icc_profile(self): - im = Image.open("Tests/images/icc_profile_none.png") - self.assertIsNone(im.info["icc_profile"]) + with Image.open("Tests/images/icc_profile_none.png") as im: + assert im.info["icc_profile"] is None - im = roundtrip(im) - self.assertNotIn("icc_profile", im.info) + im = roundtrip(im) + assert "icc_profile" not in im.info def test_repr_png(self): im = hopper() - repr_png = Image.open(BytesIO(im._repr_png_())) - self.assertEqual(repr_png.format, "PNG") - self.assert_image_equal(im, repr_png) + with Image.open(BytesIO(im._repr_png_())) as repr_png: + assert repr_png.format == "PNG" + assert_image_equal(im, repr_png) - def test_chunk_order(self): - im = Image.open("Tests/images/icc_profile.png") - test_file = self.tempfile("temp.png") - im.convert("P").save(test_file, dpi=(100, 100)) + def test_chunk_order(self, tmp_path): + 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)) chunks = self.get_chunks(test_file) # https://www.w3.org/TR/PNG/#5ChunkOrdering # IHDR - shall be first - self.assertEqual(chunks.index(b"IHDR"), 0) + assert chunks.index(b"IHDR") == 0 # PLTE - before first IDAT - self.assertLess(chunks.index(b"PLTE"), chunks.index(b"IDAT")) + assert chunks.index(b"PLTE") < chunks.index(b"IDAT") # iCCP - before PLTE and IDAT - self.assertLess(chunks.index(b"iCCP"), chunks.index(b"PLTE")) - self.assertLess(chunks.index(b"iCCP"), chunks.index(b"IDAT")) + assert chunks.index(b"iCCP") < chunks.index(b"PLTE") + assert chunks.index(b"iCCP") < chunks.index(b"IDAT") # tRNS - after PLTE, before IDAT - self.assertGreater(chunks.index(b"tRNS"), chunks.index(b"PLTE")) - self.assertLess(chunks.index(b"tRNS"), chunks.index(b"IDAT")) + assert chunks.index(b"tRNS") > chunks.index(b"PLTE") + assert chunks.index(b"tRNS") < chunks.index(b"IDAT") # pHYs - before IDAT - self.assertLess(chunks.index(b"pHYs"), chunks.index(b"IDAT")) + assert chunks.index(b"pHYs") < chunks.index(b"IDAT") def test_getchunks(self): im = hopper() chunks = PngImagePlugin.getchunks(im) - self.assertEqual(len(chunks), 3) + assert len(chunks) == 3 + + def test_read_private_chunks(self): + im = Image.open("Tests/images/exif.png") + assert im.private_chunks == [(b"orNT", b"\x01")] + + def test_roundtrip_private_chunk(self): + # Check private chunk roundtripping + + with Image.open(TEST_PNG_FILE) as im: + info = PngImagePlugin.PngInfo() + info.add(b"prIV", b"VALUE") + info.add(b"atEC", b"VALUE2") + info.add(b"prIV", b"VALUE3", True) + + im = roundtrip(im, pnginfo=info) + assert im.private_chunks == [(b"prIV", b"VALUE"), (b"atEC", b"VALUE2")] + im.load() + assert im.private_chunks == [ + (b"prIV", b"VALUE"), + (b"atEC", b"VALUE2"), + (b"prIV", b"VALUE3", True), + ] def test_textual_chunks_after_idat(self): - im = Image.open("Tests/images/hopper.png") - self.assertIn("comment", im.text.keys()) - for k, v in { - "date:create": "2014-09-04T09:37:08+03:00", - "date:modify": "2014-09-04T09:37:08+03:00", - }.items(): - self.assertEqual(im.text[k], v) + with Image.open("Tests/images/hopper.png") as im: + assert "comment" in im.text.keys() + for k, v in { + "date:create": "2014-09-04T09:37:08+03:00", + "date:modify": "2014-09-04T09:37:08+03:00", + }.items(): + assert im.text[k] == v # Raises a SyntaxError in load_end - im = Image.open("Tests/images/broken_data_stream.png") - with self.assertRaises(IOError): - self.assertIsInstance(im.text, dict) + with Image.open("Tests/images/broken_data_stream.png") as im: + with pytest.raises(OSError): + assert isinstance(im.text, dict) # Raises a UnicodeDecodeError in load_end - im = Image.open("Tests/images/truncated_image.png") - # The file is truncated - self.assertRaises(IOError, lambda: im.text) - ImageFile.LOAD_TRUNCATED_IMAGES = True - self.assertIsInstance(im.text, dict) - ImageFile.LOAD_TRUNCATED_IMAGES = False + with Image.open("Tests/images/truncated_image.png") as im: + # The file is truncated + with pytest.raises(OSError): + im.text() + ImageFile.LOAD_TRUNCATED_IMAGES = True + assert isinstance(im.text, dict) + ImageFile.LOAD_TRUNCATED_IMAGES = False # Raises an EOFError in load_end - im = Image.open("Tests/images/hopper_idat_after_image_end.png") - self.assertEqual(im.text, {"TXT": "VALUE", "ZIP": "VALUE"}) + with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: + assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} def test_exif(self): - im = Image.open("Tests/images/exif.png") - exif = im._getexif() - self.assertEqual(exif[274], 1) + # With an EXIF chunk + with Image.open("Tests/images/exif.png") as im: + exif = im._getexif() + assert exif[274] == 1 - def test_exif_save(self): - im = Image.open("Tests/images/exif.png") + # With an ImageMagick zTXt chunk + with Image.open("Tests/images/exif_imagemagick.png") as im: + exif = im._getexif() + assert exif[274] == 1 - test_file = self.tempfile("temp.png") - im.save(test_file) + # Assert that info still can be extracted + # when the image is no longer a PngImageFile instance + exif = im.copy().getexif() + assert exif[274] == 1 + + # With a tEXt chunk + with Image.open("Tests/images/exif_text.png") as im: + exif = im._getexif() + assert exif[274] == 1 + + # With XMP tags + with Image.open("Tests/images/xmp_tags_orientation.png") as im: + exif = im.getexif() + assert exif[274] == 3 + + def test_exif_save(self, tmp_path): + with Image.open("Tests/images/exif.png") as im: + test_file = str(tmp_path / "temp.png") + im.save(test_file) with Image.open(test_file) as reloaded: exif = reloaded._getexif() - self.assertEqual(exif[274], 1) + assert exif[274] == 1 - def test_exif_from_jpg(self): - im = Image.open("Tests/images/pil_sample_rgb.jpg") - - test_file = self.tempfile("temp.png") - im.save(test_file) + def test_exif_from_jpg(self, tmp_path): + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + test_file = str(tmp_path / "temp.png") + im.save(test_file) with Image.open(test_file) as reloaded: exif = reloaded._getexif() - self.assertEqual(exif[305], "Adobe Photoshop CS Macintosh") + assert exif[305] == "Adobe Photoshop CS Macintosh" - def test_exif_argument(self): - im = Image.open(TEST_PNG_FILE) - - test_file = self.tempfile("temp.png") - im.save(test_file, exif=b"exifstring") + def test_exif_argument(self, tmp_path): + with Image.open(TEST_PNG_FILE) as im: + test_file = str(tmp_path / "temp.png") + im.save(test_file, exif=b"exifstring") with Image.open(test_file) as reloaded: - self.assertEqual(reloaded.info["exif"], b"Exif\x00\x00exifstring") + assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" - @unittest.skipUnless( - HAVE_WEBP and _webp.HAVE_WEBPANIM, "WebP support not installed with animation" - ) - def test_apng(self): - im = Image.open("Tests/images/iss634.apng") - self.assertEqual(im.get_format_mimetype(), "image/apng") + def test_tell(self): + with Image.open(TEST_PNG_FILE) as im: + assert im.tell() == 0 - # This also tests reading unknown PNG chunks (fcTL and fdAT) in load_end - expected = Image.open("Tests/images/iss634.webp") - self.assert_image_similar(im, expected, 0.23) + def test_seek(self): + with Image.open(TEST_PNG_FILE) as im: + im.seek(0) + + with pytest.raises(EOFError): + im.seek(1) -@unittest.skipIf(is_win32(), "requires Unix or macOS") +@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") +@skip_unless_feature("zlib") class TestTruncatedPngPLeaks(PillowLeakTestCase): mem_limit = 2 * 1024 # max increase in K iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs - def setUp(self): - if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - self.skipTest("zip/deflate support not available") - def test_leak_load(self): with open("Tests/images/hopper.png", "rb") as f: DATA = BytesIO(f.read(16 * 1024)) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 226687fa3..e7c3fb06f 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,75 +1,83 @@ +import pytest + from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, assert_image_similar, hopper # sample ppm stream -test_file = "Tests/images/hopper.ppm" +TEST_FILE = "Tests/images/hopper.ppm" -class TestFilePpm(PillowTestCase): - def test_sanity(self): - im = Image.open(test_file) +def test_sanity(): + with Image.open(TEST_FILE) as im: im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PPM") - self.assertEqual(im.get_format_mimetype(), "image/x-portable-pixmap") + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format, "PPM" + assert im.get_format_mimetype() == "image/x-portable-pixmap" - def test_16bit_pgm(self): - im = Image.open("Tests/images/16_bit_binary.pgm") + +def test_16bit_pgm(): + with Image.open("Tests/images/16_bit_binary.pgm") as im: im.load() - self.assertEqual(im.mode, "I") - self.assertEqual(im.size, (20, 100)) - self.assertEqual(im.get_format_mimetype(), "image/x-portable-graymap") + assert im.mode == "I" + assert im.size == (20, 100) + assert im.get_format_mimetype() == "image/x-portable-graymap" - tgt = Image.open("Tests/images/16_bit_binary_pgm.png") - self.assert_image_equal(im, tgt) + with Image.open("Tests/images/16_bit_binary_pgm.png") as tgt: + assert_image_equal(im, tgt) - def test_16bit_pgm_write(self): - im = Image.open("Tests/images/16_bit_binary.pgm") + +def test_16bit_pgm_write(tmp_path): + with Image.open("Tests/images/16_bit_binary.pgm") as im: im.load() - f = self.tempfile("temp.pgm") + f = str(tmp_path / "temp.pgm") im.save(f, "PPM") - reloaded = Image.open(f) - self.assert_image_equal(im, reloaded) + with Image.open(f) as reloaded: + assert_image_equal(im, reloaded) - def test_pnm(self): - im = Image.open("Tests/images/hopper.pnm") - self.assert_image_similar(im, hopper(), 0.0001) - f = self.tempfile("temp.pnm") +def test_pnm(tmp_path): + 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) - reloaded = Image.open(f) - self.assert_image_equal(im, reloaded) + with Image.open(f) as reloaded: + assert_image_equal(im, reloaded) - def test_truncated_file(self): - path = self.tempfile("temp.pgm") - with open(path, "w") as f: - f.write("P6") - self.assertRaises(ValueError, Image.open, path) +def test_truncated_file(tmp_path): + path = str(tmp_path / "temp.pgm") + with open(path, "w") as f: + f.write("P6") - def test_neg_ppm(self): - # 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 - # sizes. + with pytest.raises(ValueError): + Image.open(path) - with self.assertRaises(IOError): - Image.open("Tests/images/negative_size.ppm") - def test_mimetypes(self): - path = self.tempfile("temp.pgm") +def test_neg_ppm(): + # 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 + # sizes. - with open(path, "w") as f: - f.write("P4\n128 128\n255") - with Image.open(path) as im: - self.assertEqual(im.get_format_mimetype(), "image/x-portable-bitmap") + with pytest.raises(OSError): + Image.open("Tests/images/negative_size.ppm") - with open(path, "w") as f: - f.write("PyCMYK\n128 128\n255") - with Image.open(path) as im: - self.assertEqual(im.get_format_mimetype(), "image/x-portable-anymap") + +def test_mimetypes(tmp_path): + path = str(tmp_path / "temp.pgm") + + with open(path, "w") as f: + f.write("P4\n128 128\n255") + with Image.open(path) as im: + assert im.get_format_mimetype() == "image/x-portable-bitmap" + + with open(path, "w") as f: + f.write("PyCMYK\n128 128\n255") + with Image.open(path) as im: + assert im.get_format_mimetype() == "image/x-portable-anymap" diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 939485f67..8bb45630e 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,115 +1,131 @@ -import unittest +import pytest from PIL import Image, PsdImagePlugin -from .helper import PillowTestCase, hopper, is_pypy +from .helper import assert_image_similar, hopper, is_pypy test_file = "Tests/images/hopper.psd" -class TestImagePsd(PillowTestCase): - def test_sanity(self): +def test_sanity(): + with Image.open(test_file) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "PSD" + assert im.get_format_mimetype() == "image/vnd.adobe.photoshop" + + im2 = hopper() + assert_image_similar(im, im2, 4.8) + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + im = Image.open(test_file) + im.load() + + pytest.warns(ResourceWarning, open) + + +def test_closed_file(): + def open(): + im = Image.open(test_file) + im.load() + im.close() + + pytest.warns(None, open) + + +def test_context_manager(): + def open(): with Image.open(test_file) as im: im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PSD") - im2 = hopper() - self.assert_image_similar(im, im2, 4.8) + pytest.warns(None, open) - @unittest.skipIf(is_pypy(), "Requires CPython") - def test_unclosed_file(self): - def open(): - im = Image.open(test_file) - im.load() - self.assert_warning(ResourceWarning, open) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - def test_closed_file(self): - def open(): - im = Image.open(test_file) - im.load() - im.close() + with pytest.raises(SyntaxError): + PsdImagePlugin.PsdImageFile(invalid_file) - self.assert_warning(None, open) - def test_context_manager(self): - def open(): - with Image.open(test_file) as im: - im.load() +def test_n_frames(): + with Image.open("Tests/images/hopper_merged.psd") as im: + assert im.n_frames == 1 + assert not im.is_animated - self.assert_warning(None, open) + with Image.open(test_file) as im: + assert im.n_frames == 2 + assert im.is_animated - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, PsdImagePlugin.PsdImageFile, invalid_file) +def test_eoferror(): + with Image.open(test_file) as im: + # PSD seek index starts at 1 rather than 0 + n_frames = im.n_frames + 1 - def test_n_frames(self): - with Image.open("Tests/images/hopper_merged.psd") as im: - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames - with Image.open(test_file) as im: - self.assertEqual(im.n_frames, 2) - self.assertTrue(im.is_animated) + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) - def test_eoferror(self): - with Image.open(test_file) as im: - # PSD seek index starts at 1 rather than 0 - n_frames = im.n_frames + 1 - # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) +def test_seek_tell(): + with Image.open(test_file) as im: - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) + layer_number = im.tell() + assert layer_number == 1 - def test_seek_tell(self): - with Image.open(test_file) as im: + with pytest.raises(EOFError): + im.seek(0) - layer_number = im.tell() - self.assertEqual(layer_number, 1) + im.seek(1) + layer_number = im.tell() + assert layer_number == 1 - self.assertRaises(EOFError, im.seek, 0) + im.seek(2) + layer_number = im.tell() + assert layer_number == 2 - im.seek(1) - layer_number = im.tell() - self.assertEqual(layer_number, 1) - im.seek(2) - layer_number = im.tell() - self.assertEqual(layer_number, 2) +def test_seek_eoferror(): + with Image.open(test_file) as im: - def test_seek_eoferror(self): - with Image.open(test_file) as im: + with pytest.raises(EOFError): + im.seek(-1) - self.assertRaises(EOFError, im.seek, -1) - def test_open_after_exclusive_load(self): - with Image.open(test_file) as im: - im.load() - im.seek(im.tell() + 1) - im.load() +def test_open_after_exclusive_load(): + with Image.open(test_file) as im: + im.load() + im.seek(im.tell() + 1) + im.load() - def test_icc_profile(self): - with Image.open(test_file) as im: - self.assertIn("icc_profile", im.info) - icc_profile = im.info["icc_profile"] - self.assertEqual(len(icc_profile), 3144) +def test_icc_profile(): + with Image.open(test_file) as im: + assert "icc_profile" in im.info - def test_no_icc_profile(self): - with Image.open("Tests/images/hopper_merged.psd") as im: - self.assertNotIn("icc_profile", im.info) + icc_profile = im.info["icc_profile"] + assert len(icc_profile) == 3144 - def test_combined_larger_than_size(self): - # The 'combined' sizes of the individual parts is larger than the - # declared 'size' of the extra data field, resulting in a backwards seek. - # If we instead take the 'size' of the extra data field as the source of truth, - # then the seek can't be negative - with self.assertRaises(IOError): - Image.open("Tests/images/combined_larger_than_size.psd") +def test_no_icc_profile(): + with Image.open("Tests/images/hopper_merged.psd") as im: + assert "icc_profile" not in im.info + + +def test_combined_larger_than_size(): + # The 'combined' sizes of the individual parts is larger than the + # declared 'size' of the extra data field, resulting in a backwards seek. + + # If we instead take the 'size' of the extra data field as the source of truth, + # then the seek can't be negative + with pytest.raises(OSError): + Image.open("Tests/images/combined_larger_than_size.psd") diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index ff3aea1d5..a197fa775 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,89 +1,101 @@ +import pytest + from PIL import Image, SgiImagePlugin -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, assert_image_similar, hopper -class TestFileSgi(PillowTestCase): - def test_rgb(self): - # Created with ImageMagick then renamed: - # convert hopper.ppm -compress None sgi:hopper.rgb - test_file = "Tests/images/hopper.rgb" +def test_rgb(): + # Created with ImageMagick then renamed: + # convert hopper.ppm -compress None sgi:hopper.rgb + test_file = "Tests/images/hopper.rgb" - im = Image.open(test_file) - self.assert_image_equal(im, hopper()) - self.assertEqual(im.get_format_mimetype(), "image/rgb") + with Image.open(test_file) as im: + assert_image_equal(im, hopper()) + assert im.get_format_mimetype() == "image/rgb" - def test_rgb16(self): - test_file = "Tests/images/hopper16.rgb" - im = Image.open(test_file) - self.assert_image_equal(im, hopper()) +def test_rgb16(): + test_file = "Tests/images/hopper16.rgb" - def test_l(self): - # Created with ImageMagick - # convert hopper.ppm -monochrome -compress None sgi:hopper.bw - test_file = "Tests/images/hopper.bw" + with Image.open(test_file) as im: + assert_image_equal(im, hopper()) - im = Image.open(test_file) - self.assert_image_similar(im, hopper("L"), 2) - self.assertEqual(im.get_format_mimetype(), "image/sgi") - def test_rgba(self): - # Created with ImageMagick: - # convert transparent.png -compress None transparent.sgi - test_file = "Tests/images/transparent.sgi" +def test_l(): + # Created with ImageMagick + # convert hopper.ppm -monochrome -compress None sgi:hopper.bw + test_file = "Tests/images/hopper.bw" - im = Image.open(test_file) - target = Image.open("Tests/images/transparent.png") - self.assert_image_equal(im, target) - self.assertEqual(im.get_format_mimetype(), "image/sgi") + with Image.open(test_file) as im: + assert_image_similar(im, hopper("L"), 2) + assert im.get_format_mimetype() == "image/sgi" - def test_rle(self): - # Created with ImageMagick: - # convert hopper.ppm hopper.sgi - test_file = "Tests/images/hopper.sgi" - im = Image.open(test_file) - target = Image.open("Tests/images/hopper.rgb") - self.assert_image_equal(im, target) +def test_rgba(): + # Created with ImageMagick: + # convert transparent.png -compress None transparent.sgi + test_file = "Tests/images/transparent.sgi" - def test_rle16(self): - test_file = "Tests/images/tv16.sgi" + with Image.open(test_file) as im: + with Image.open("Tests/images/transparent.png") as target: + assert_image_equal(im, target) + assert im.get_format_mimetype() == "image/sgi" - im = Image.open(test_file) - target = Image.open("Tests/images/tv.rgb") - self.assert_image_equal(im, target) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" +def test_rle(): + # Created with ImageMagick: + # convert hopper.ppm hopper.sgi + test_file = "Tests/images/hopper.sgi" - self.assertRaises(ValueError, SgiImagePlugin.SgiImageFile, invalid_file) + with Image.open(test_file) as im: + with Image.open("Tests/images/hopper.rgb") as target: + assert_image_equal(im, target) - def test_write(self): - def roundtrip(img): - out = self.tempfile("temp.sgi") - img.save(out, format="sgi") - reloaded = Image.open(out) - self.assert_image_equal(img, reloaded) - for mode in ("L", "RGB", "RGBA"): - roundtrip(hopper(mode)) +def test_rle16(): + test_file = "Tests/images/tv16.sgi" - # Test 1 dimension for an L mode image - roundtrip(Image.new("L", (10, 1))) + with Image.open(test_file) as im: + with Image.open("Tests/images/tv.rgb") as target: + assert_image_equal(im, target) - def test_write16(self): - test_file = "Tests/images/hopper16.rgb" - im = Image.open(test_file) - out = self.tempfile("temp.sgi") +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(ValueError): + SgiImagePlugin.SgiImageFile(invalid_file) + + +def test_write(tmp_path): + def roundtrip(img): + out = str(tmp_path / "temp.sgi") + img.save(out, format="sgi") + with Image.open(out) as reloaded: + assert_image_equal(img, reloaded) + + for mode in ("L", "RGB", "RGBA"): + roundtrip(hopper(mode)) + + # Test 1 dimension for an L mode image + roundtrip(Image.new("L", (10, 1))) + + +def test_write16(tmp_path): + test_file = "Tests/images/hopper16.rgb" + + with Image.open(test_file) as im: + out = str(tmp_path / "temp.sgi") im.save(out, format="sgi", bpc=2) - reloaded = Image.open(out) - self.assert_image_equal(im, reloaded) + with Image.open(out) as reloaded: + assert_image_equal(im, reloaded) - def test_unsupported_mode(self): - im = hopper("LA") - out = self.tempfile("temp.sgi") - self.assertRaises(ValueError, im.save, out, format="sgi") +def test_unsupported_mode(tmp_path): + im = hopper("LA") + out = str(tmp_path / "temp.sgi") + + with pytest.raises(ValueError): + im.save(out, format="sgi") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 5940c2ff2..9cdb451c9 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,147 +1,163 @@ import tempfile -import unittest from io import BytesIO +import pytest + from PIL import Image, ImageSequence, SpiderImagePlugin -from .helper import PillowTestCase, hopper, is_pypy +from .helper import assert_image_equal, hopper, is_pypy TEST_FILE = "Tests/images/hopper.spider" -class TestImageSpider(PillowTestCase): - def test_sanity(self): +def test_sanity(): + with Image.open(TEST_FILE) as im: + im.load() + assert im.mode == "F" + assert im.size == (128, 128) + assert im.format == "SPIDER" + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + im = Image.open(TEST_FILE) + im.load() + + pytest.warns(ResourceWarning, open) + + +def test_closed_file(): + def open(): + im = Image.open(TEST_FILE) + im.load() + im.close() + + pytest.warns(None, open) + + +def test_context_manager(): + def open(): with Image.open(TEST_FILE) as im: im.load() - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "SPIDER") - @unittest.skipIf(is_pypy(), "Requires CPython") - def test_unclosed_file(self): - def open(): - im = Image.open(TEST_FILE) - im.load() + pytest.warns(None, open) - self.assert_warning(ResourceWarning, open) - def test_closed_file(self): - def open(): - im = Image.open(TEST_FILE) - im.load() - im.close() +def test_save(tmp_path): + # Arrange + temp = str(tmp_path / "temp.spider") + im = hopper() - self.assert_warning(None, open) + # Act + im.save(temp, "SPIDER") - def test_context_manager(self): - def open(): - with Image.open(TEST_FILE) as im: - im.load() + # Assert + with Image.open(temp) as im2: + assert im2.mode == "F" + assert im2.size == (128, 128) + assert im2.format == "SPIDER" - self.assert_warning(None, open) - def test_save(self): - # Arrange - temp = self.tempfile("temp.spider") - im = hopper() +def test_tempfile(): + # Arrange + im = hopper() - # Act - im.save(temp, "SPIDER") + # Act + with tempfile.TemporaryFile() as fp: + im.save(fp, "SPIDER") # Assert - with Image.open(temp) as im2: - self.assertEqual(im2.mode, "F") - self.assertEqual(im2.size, (128, 128)) - self.assertEqual(im2.format, "SPIDER") + fp.seek(0) + with Image.open(fp) as reloaded: + assert reloaded.mode == "F" + assert reloaded.size == (128, 128) + assert reloaded.format == "SPIDER" - def test_tempfile(self): - # Arrange - im = hopper() + +def test_is_spider_image(): + assert SpiderImagePlugin.isSpiderImage(TEST_FILE) + + +def test_tell(): + # Arrange + with Image.open(TEST_FILE) as im: # Act - with tempfile.TemporaryFile() as fp: - im.save(fp, "SPIDER") - - # Assert - fp.seek(0) - reloaded = Image.open(fp) - self.assertEqual(reloaded.mode, "F") - self.assertEqual(reloaded.size, (128, 128)) - self.assertEqual(reloaded.format, "SPIDER") - - def test_isSpiderImage(self): - self.assertTrue(SpiderImagePlugin.isSpiderImage(TEST_FILE)) - - def test_tell(self): - # Arrange - with Image.open(TEST_FILE) as im: - - # Act - index = im.tell() - - # Assert - self.assertEqual(index, 0) - - def test_n_frames(self): - with Image.open(TEST_FILE) as im: - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) - - def test_loadImageSeries(self): - # Arrange - not_spider_file = "Tests/images/hopper.ppm" - file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] - - # Act - img_list = SpiderImagePlugin.loadImageSeries(file_list) + index = im.tell() # Assert - self.assertEqual(len(img_list), 1) - self.assertIsInstance(img_list[0], Image.Image) - self.assertEqual(img_list[0].size, (128, 128)) + assert index == 0 - def test_loadImageSeries_no_input(self): - # Arrange - file_list = None - # Act - img_list = SpiderImagePlugin.loadImageSeries(file_list) +def test_n_frames(): + with Image.open(TEST_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated - # Assert - self.assertIsNone(img_list) - def test_isInt_not_a_number(self): - # Arrange - not_a_number = "a" +def test_load_image_series(): + # Arrange + not_spider_file = "Tests/images/hopper.ppm" + file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] - # Act - ret = SpiderImagePlugin.isInt(not_a_number) + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) - # Assert - self.assertEqual(ret, 0) + # Assert + assert len(img_list) == 1 + assert isinstance(img_list[0], Image.Image) + assert img_list[0].size == (128, 128) - def test_invalid_file(self): - invalid_file = "Tests/images/invalid.spider" - self.assertRaises(IOError, Image.open, invalid_file) +def test_load_image_series_no_input(): + # Arrange + file_list = None - def test_nonstack_file(self): - with Image.open(TEST_FILE) as im: - self.assertRaises(EOFError, im.seek, 0) + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) - def test_nonstack_dos(self): - with Image.open(TEST_FILE) as im: - for i, frame in enumerate(ImageSequence.Iterator(im)): - if i > 1: - self.fail("Non-stack DOS file test failed") + # Assert + assert img_list is None - # for issue #4093 - def test_odd_size(self): - data = BytesIO() - width = 100 - im = Image.new("F", (width, 64)) - im.save(data, format="SPIDER") - data.seek(0) - im2 = Image.open(data) - self.assert_image_equal(im, im2) +def test_is_int_not_a_number(): + # Arrange + not_a_number = "a" + + # Act + ret = SpiderImagePlugin.isInt(not_a_number) + + # Assert + assert ret == 0 + + +def test_invalid_file(): + invalid_file = "Tests/images/invalid.spider" + + with pytest.raises(OSError): + Image.open(invalid_file) + + +def test_nonstack_file(): + with Image.open(TEST_FILE) as im: + with pytest.raises(EOFError): + im.seek(0) + + +def test_nonstack_dos(): + 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(): + data = BytesIO() + width = 100 + im = Image.new("F", (width, 64)) + im.save(data, format="SPIDER") + + data.seek(0) + with Image.open(data) as im2: + assert_image_equal(im, im2) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 5fc171054..8421106a2 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,47 +1,52 @@ import os -import unittest + +import pytest from PIL import Image, SunImagePlugin -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, assert_image_similar, hopper EXTRA_DIR = "Tests/images/sunraster" -class TestFileSun(PillowTestCase): - def test_sanity(self): - # Arrange - # Created with ImageMagick: convert hopper.jpg hopper.ras - test_file = "Tests/images/hopper.ras" +def test_sanity(): + # Arrange + # Created with ImageMagick: convert hopper.jpg hopper.ras + test_file = "Tests/images/hopper.ras" - # Act - im = Image.open(test_file) + # Act + with Image.open(test_file) as im: # Assert - self.assertEqual(im.size, (128, 128)) + assert im.size == (128, 128) - self.assert_image_similar(im, hopper(), 5) # visually verified + assert_image_similar(im, hopper(), 5) # visually verified - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, SunImagePlugin.SunImageFile, invalid_file) + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + SunImagePlugin.SunImageFile(invalid_file) - def test_im1(self): - im = Image.open("Tests/images/sunraster.im1") - target = Image.open("Tests/images/sunraster.im1.png") - self.assert_image_equal(im, target) - @unittest.skipIf(not os.path.exists(EXTRA_DIR), "Extra image files not installed") - def test_others(self): - files = ( - os.path.join(EXTRA_DIR, f) - for f in os.listdir(EXTRA_DIR) - if os.path.splitext(f)[1] in (".sun", ".SUN", ".ras") - ) - for path in files: - with Image.open(path) as im: - im.load() - self.assertIsInstance(im, SunImagePlugin.SunImageFile) - target_path = "%s.png" % os.path.splitext(path)[0] - # im.save(target_file) - with Image.open(target_path) as target: - self.assert_image_equal(im, target) +def test_im1(): + with Image.open("Tests/images/sunraster.im1") as im: + with Image.open("Tests/images/sunraster.im1.png") as target: + assert_image_equal(im, target) + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_others(): + files = ( + os.path.join(EXTRA_DIR, f) + for f in os.listdir(EXTRA_DIR) + if os.path.splitext(f)[1] in (".sun", ".SUN", ".ras") + ) + for path in files: + with Image.open(path) as im: + im.load() + assert isinstance(im, SunImagePlugin.SunImageFile) + target_path = f"{os.path.splitext(path)[0]}.png" + # im.save(target_file) + with Image.open(target_path) as target: + assert_image_equal(im, target) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index f381eef7e..02001e5b1 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,50 +1,46 @@ -import unittest +import pytest -from PIL import Image, TarIO +from PIL import Image, TarIO, features -from .helper import PillowTestCase, is_pypy - -codecs = dir(Image.core) +from .helper import is_pypy # Sample tar archive TEST_TAR_FILE = "Tests/images/hopper.tar" -class TestFileTar(PillowTestCase): - def setUp(self): - if "zip_decoder" not in codecs and "jpeg_decoder" not in codecs: - self.skipTest("neither jpeg nor zip support available") - - def test_sanity(self): - for codec, test_path, format in [ - ["zip_decoder", "hopper.png", "PNG"], - ["jpeg_decoder", "hopper.jpg", "JPEG"], - ]: - if codec in codecs: - with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: - im = Image.open(tar) +def test_sanity(): + for codec, test_path, format in [ + ["zlib", "hopper.png", "PNG"], + ["jpg", "hopper.jpg", "JPEG"], + ]: + if features.check(codec): + with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: + with Image.open(tar) as im: im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, format) + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == format - @unittest.skipIf(is_pypy(), "Requires CPython") - def test_unclosed_file(self): - def open(): - TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") - self.assert_warning(ResourceWarning, open) +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") - def test_close(self): - def open(): - tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") - tar.close() + pytest.warns(ResourceWarning, open) - self.assert_warning(None, open) - def test_contextmanager(self): - def open(): - with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): - pass +def test_close(): + def open(): + tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") + tar.close() - self.assert_warning(None, open) + pytest.warns(None, open) + + +def test_contextmanager(): + def open(): + with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): + pass + + pytest.warns(None, open) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bd5c32c5b..465e13316 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -2,199 +2,204 @@ import os from glob import glob from itertools import product +import pytest + from PIL import Image -from .helper import PillowTestCase +from .helper import assert_image_equal, hopper _TGA_DIR = os.path.join("Tests", "images", "tga") _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") -class TestFileTga(PillowTestCase): +_MODES = ("L", "LA", "P", "RGB", "RGBA") +_ORIGINS = ("tl", "bl") - _MODES = ("L", "LA", "P", "RGB", "RGBA") - _ORIGINS = ("tl", "bl") +_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} - _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} - def test_sanity(self): - for mode in self._MODES: - png_paths = glob( - os.path.join(_TGA_DIR_COMMON, "*x*_{}.png".format(mode.lower())) - ) +def test_sanity(tmp_path): + for mode in _MODES: - for png_path in png_paths: - reference_im = Image.open(png_path) - self.assertEqual(reference_im.mode, mode) + def roundtrip(original_im): + out = str(tmp_path / "temp.tga") + + original_im.save(out, rle=rle) + with Image.open(out) as saved_im: + if rle: + assert ( + saved_im.info["compression"] == original_im.info["compression"] + ) + assert saved_im.info["orientation"] == original_im.info["orientation"] + if mode == "P": + assert saved_im.getpalette() == original_im.getpalette() + + assert_image_equal(saved_im, original_im) + + png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) + + for png_path in png_paths: + with Image.open(png_path) as reference_im: + assert reference_im.mode == mode path_no_ext = os.path.splitext(png_path)[0] - for origin, rle in product(self._ORIGINS, (True, False)): + for origin, rle in product(_ORIGINS, (True, False)): tga_path = "{}_{}_{}.tga".format( path_no_ext, origin, "rle" if rle else "raw" ) - original_im = Image.open(tga_path) - self.assertEqual(original_im.format, "TGA") - self.assertEqual(original_im.get_format_mimetype(), "image/x-tga") - if rle: - self.assertEqual(original_im.info["compression"], "tga_rle") - self.assertEqual( - original_im.info["orientation"], - self._ORIGIN_TO_ORIENTATION[origin], - ) - if mode == "P": - self.assertEqual( - original_im.getpalette(), reference_im.getpalette() + with Image.open(tga_path) as original_im: + assert original_im.format == "TGA" + assert original_im.get_format_mimetype() == "image/x-tga" + if rle: + assert original_im.info["compression"] == "tga_rle" + assert ( + original_im.info["orientation"] + == _ORIGIN_TO_ORIENTATION[origin] ) + if mode == "P": + assert original_im.getpalette() == reference_im.getpalette() - self.assert_image_equal(original_im, reference_im) + assert_image_equal(original_im, reference_im) - # Generate a new test name every time so the - # test will not fail with permission error - # on Windows. - out = self.tempfile("temp.tga") + roundtrip(original_im) - original_im.save(out, rle=rle) - saved_im = Image.open(out) - if rle: - self.assertEqual( - saved_im.info["compression"], - original_im.info["compression"], - ) - self.assertEqual( - saved_im.info["orientation"], original_im.info["orientation"] - ) - if mode == "P": - self.assertEqual( - saved_im.getpalette(), original_im.getpalette() - ) - self.assert_image_equal(saved_im, original_im) +def test_id_field(): + # tga file with id field + test_file = "Tests/images/tga_id_field.tga" - def test_id_field(self): - # tga file with id field - test_file = "Tests/images/tga_id_field.tga" + # Act + with Image.open(test_file) as im: - # Act - with Image.open(test_file) as im: + # Assert + assert im.size == (100, 100) - # Assert - self.assertEqual(im.size, (100, 100)) - def test_id_field_rle(self): - # tga file with id field - test_file = "Tests/images/rgb32rle.tga" +def test_id_field_rle(): + # tga file with id field + test_file = "Tests/images/rgb32rle.tga" - # Act - with Image.open(test_file) as im: + # Act + with Image.open(test_file) as im: - # Assert - self.assertEqual(im.size, (199, 199)) + # Assert + assert im.size == (199, 199) - def test_save(self): - test_file = "Tests/images/tga_id_field.tga" - im = Image.open(test_file) - out = self.tempfile("temp.tga") +def test_save(tmp_path): + test_file = "Tests/images/tga_id_field.tga" + with Image.open(test_file) as im: + out = str(tmp_path / "temp.tga") # Save im.save(out) with Image.open(out) as test_im: - self.assertEqual(test_im.size, (100, 100)) - self.assertEqual(test_im.info["id_section"], im.info["id_section"]) + assert test_im.size == (100, 100) + assert test_im.info["id_section"] == im.info["id_section"] # RGBA save im.convert("RGBA").save(out) - with Image.open(out) as test_im: - self.assertEqual(test_im.size, (100, 100)) + with Image.open(out) as test_im: + assert test_im.size == (100, 100) - def test_save_id_section(self): - test_file = "Tests/images/rgb32rle.tga" - im = Image.open(test_file) - out = self.tempfile("temp.tga") +def test_save_wrong_mode(tmp_path): + im = hopper("PA") + out = str(tmp_path / "temp.tga") + + with pytest.raises(OSError): + im.save(out) + + +def test_save_id_section(tmp_path): + test_file = "Tests/images/rgb32rle.tga" + with Image.open(test_file) as im: + out = str(tmp_path / "temp.tga") # Check there is no id section im.save(out) - with Image.open(out) as test_im: - self.assertNotIn("id_section", test_im.info) + with Image.open(out) as test_im: + assert "id_section" not in test_im.info - # Save with custom id section - im.save(out, id_section=b"Test content") - with Image.open(out) as test_im: - self.assertEqual(test_im.info["id_section"], b"Test content") + # Save with custom id section + im.save(out, id_section=b"Test content") + with Image.open(out) as test_im: + assert test_im.info["id_section"] == b"Test content" - # Save with custom id section greater than 255 characters - id_section = b"Test content" * 25 - self.assert_warning(UserWarning, lambda: im.save(out, id_section=id_section)) - with Image.open(out) as test_im: - self.assertEqual(test_im.info["id_section"], id_section[:255]) + # Save with custom id section greater than 255 characters + id_section = b"Test content" * 25 + pytest.warns(UserWarning, lambda: im.save(out, id_section=id_section)) + with Image.open(out) as test_im: + assert test_im.info["id_section"] == id_section[:255] - test_file = "Tests/images/tga_id_field.tga" - with Image.open(test_file) as im: + test_file = "Tests/images/tga_id_field.tga" + with Image.open(test_file) as im: - # Save with no id section - im.save(out, id_section="") - with Image.open(out) as test_im: - self.assertNotIn("id_section", test_im.info) + # Save with no id section + im.save(out, id_section="") + with Image.open(out) as test_im: + assert "id_section" not in test_im.info - def test_save_orientation(self): - test_file = "Tests/images/rgb32rle.tga" - im = Image.open(test_file) - self.assertEqual(im.info["orientation"], -1) - out = self.tempfile("temp.tga") +def test_save_orientation(tmp_path): + test_file = "Tests/images/rgb32rle.tga" + out = str(tmp_path / "temp.tga") + with Image.open(test_file) as im: + assert im.info["orientation"] == -1 im.save(out, orientation=1) - with Image.open(out) as test_im: - self.assertEqual(test_im.info["orientation"], 1) + with Image.open(out) as test_im: + assert test_im.info["orientation"] == 1 - def test_save_rle(self): - test_file = "Tests/images/rgb32rle.tga" - im = Image.open(test_file) - self.assertEqual(im.info["compression"], "tga_rle") - out = self.tempfile("temp.tga") +def test_save_rle(tmp_path): + test_file = "Tests/images/rgb32rle.tga" + with Image.open(test_file) as im: + assert im.info["compression"] == "tga_rle" + + out = str(tmp_path / "temp.tga") # Save im.save(out) - with Image.open(out) as test_im: - self.assertEqual(test_im.size, (199, 199)) - self.assertEqual(test_im.info["compression"], "tga_rle") + with Image.open(out) as test_im: + assert test_im.size == (199, 199) + assert test_im.info["compression"] == "tga_rle" - # Save without compression - im.save(out, compression=None) - with Image.open(out) as test_im: - self.assertNotIn("compression", test_im.info) + # Save without compression + im.save(out, compression=None) + with Image.open(out) as test_im: + assert "compression" not in test_im.info - # RGBA save - im.convert("RGBA").save(out) - with Image.open(out) as test_im: - self.assertEqual(test_im.size, (199, 199)) + # RGBA save + im.convert("RGBA").save(out) + with Image.open(out) as test_im: + assert test_im.size == (199, 199) - test_file = "Tests/images/tga_id_field.tga" - im = Image.open(test_file) - self.assertNotIn("compression", im.info) + test_file = "Tests/images/tga_id_field.tga" + with Image.open(test_file) as im: + assert "compression" not in im.info # Save with compression im.save(out, compression="tga_rle") - with Image.open(out) as test_im: - self.assertEqual(test_im.info["compression"], "tga_rle") + with Image.open(out) as test_im: + assert test_im.info["compression"] == "tga_rle" - def test_save_l_transparency(self): - # There are 559 transparent pixels in la.tga. - num_transparent = 559 - in_file = "Tests/images/la.tga" - im = Image.open(in_file) - self.assertEqual(im.mode, "LA") - self.assertEqual(im.getchannel("A").getcolors()[0][0], num_transparent) +def test_save_l_transparency(tmp_path): + # There are 559 transparent pixels in la.tga. + num_transparent = 559 - out = self.tempfile("temp.tga") + in_file = "Tests/images/la.tga" + with Image.open(in_file) as im: + assert im.mode == "LA" + assert im.getchannel("A").getcolors()[0][0] == num_transparent + + out = str(tmp_path / "temp.tga") im.save(out) - test_im = Image.open(out) - self.assertEqual(test_im.mode, "LA") - self.assertEqual(test_im.getchannel("A").getcolors()[0][0], num_transparent) + with Image.open(out) as test_im: + assert test_im.mode == "LA" + assert test_im.getchannel("A").getcolors()[0][0] == num_transparent - self.assert_image_equal(im, test_im) + assert_image_equal(im, test_im) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a4346a7b5..f644ef887 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,29 +1,34 @@ -import logging import os -import unittest from io import BytesIO import pytest + from PIL import Image, TiffImagePlugin from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION -from .helper import PillowTestCase, hopper, is_pypy, is_win32 - -logger = logging.getLogger(__name__) +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_tofile, + hopper, + is_pypy, + is_win32, +) -class TestFileTiff(PillowTestCase): - def test_sanity(self): +class TestFileTiff: + def test_sanity(self, tmp_path): - filename = self.tempfile("temp.tif") + filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) with Image.open(filename) as im: im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "TIFF") + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "TIFF" hopper("1").save(filename) with Image.open(filename): @@ -45,13 +50,13 @@ class TestFileTiff(PillowTestCase): with Image.open(filename): pass - @unittest.skipIf(is_pypy(), "Requires CPython") + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(self): def open(): im = Image.open("Tests/images/multipage.tiff") im.load() - self.assert_warning(ResourceWarning, open) + pytest.warns(ResourceWarning, open) def test_closed_file(self): def open(): @@ -59,69 +64,68 @@ class TestFileTiff(PillowTestCase): im.load() im.close() - self.assert_warning(None, open) + pytest.warns(None, open) def test_context_manager(self): def open(): with Image.open("Tests/images/multipage.tiff") as im: im.load() - self.assert_warning(None, open) + pytest.warns(None, open) def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] filename = "Tests/images/pil136.tiff" - im = Image.open(filename) + with Image.open(filename) as im: + assert im.mode == "RGBA" + assert im.size == (55, 43) + assert im.tile == [("raw", (0, 0, 55, 43), 8, ("RGBa", 0, 1))] + im.load() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (55, 43)) - self.assertEqual(im.tile, [("raw", (0, 0, 55, 43), 8, ("RGBa", 0, 1))]) - im.load() - - self.assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) + assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) def test_wrong_bits_per_sample(self): - im = Image.open("Tests/images/tiff_wrong_bits_per_sample.tiff") - - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (52, 53)) - self.assertEqual(im.tile, [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))]) - im.load() + with Image.open("Tests/images/tiff_wrong_bits_per_sample.tiff") as im: + assert im.mode == "RGBA" + assert im.size == (52, 53) + assert im.tile == [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))] + im.load() def test_set_legacy_api(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() - with self.assertRaises(Exception) as e: + with pytest.raises(Exception) as e: ifd.legacy_api = None - self.assertEqual(str(e.exception), "Not allowing setting of legacy api") + assert str(e.value) == "Not allowing setting of legacy api" def test_xyres_tiff(self): filename = "Tests/images/pil168.tif" with Image.open(filename) as im: # legacy api - self.assertIsInstance(im.tag[X_RESOLUTION][0], tuple) - self.assertIsInstance(im.tag[Y_RESOLUTION][0], tuple) + assert isinstance(im.tag[X_RESOLUTION][0], tuple) + assert isinstance(im.tag[Y_RESOLUTION][0], tuple) # v2 api - self.assertIsInstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) - self.assertIsInstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) + assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) + assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) - self.assertEqual(im.info["dpi"], (72.0, 72.0)) + assert im.info["dpi"] == (72.0, 72.0) def test_xyres_fallback_tiff(self): filename = "Tests/images/compression.tif" with Image.open(filename) as im: # v2 api - self.assertIsInstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) - self.assertIsInstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) - self.assertRaises(KeyError, lambda: im.tag_v2[RESOLUTION_UNIT]) + assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) + assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) + with pytest.raises(KeyError): + im.tag_v2[RESOLUTION_UNIT] # Legacy. - self.assertEqual(im.info["resolution"], (100.0, 100.0)) + assert im.info["resolution"] == (100.0, 100.0) # Fallback "inch". - self.assertEqual(im.info["dpi"], (100.0, 100.0)) + assert im.info["dpi"] == (100.0, 100.0) def test_int_resolution(self): filename = "Tests/images/pil168.tif" @@ -131,121 +135,120 @@ class TestFileTiff(PillowTestCase): im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71 im._setup() - self.assertEqual(im.info["dpi"], (71.0, 71.0)) + assert im.info["dpi"] == (71.0, 71.0) def test_load_dpi_rounding(self): for resolutionUnit, dpi in ((None, (72, 73)), (2, (72, 73)), (3, (183, 185))): with Image.open( "Tests/images/hopper_roundDown_" + str(resolutionUnit) + ".tif" ) as im: - self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit) - self.assertEqual(im.info["dpi"], (dpi[0], dpi[0])) + assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit + assert im.info["dpi"] == (dpi[0], dpi[0]) with Image.open( "Tests/images/hopper_roundUp_" + str(resolutionUnit) + ".tif" ) as im: - self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit) - self.assertEqual(im.info["dpi"], (dpi[1], dpi[1])) + assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit + assert im.info["dpi"] == (dpi[1], dpi[1]) - def test_save_dpi_rounding(self): - outfile = self.tempfile("temp.tif") - im = Image.open("Tests/images/hopper.tif") + def test_save_dpi_rounding(self, tmp_path): + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/hopper.tif") as im: + for dpi in (72.2, 72.8): + im.save(outfile, dpi=(dpi, dpi)) - for dpi in (72.2, 72.8): - im.save(outfile, dpi=(dpi, dpi)) - - reloaded = Image.open(outfile) - reloaded.load() - self.assertEqual((round(dpi), round(dpi)), reloaded.info["dpi"]) + with Image.open(outfile) as reloaded: + reloaded.load() + assert (round(dpi), round(dpi)) == reloaded.info["dpi"] def test_save_setting_missing_resolution(self): b = BytesIO() Image.open("Tests/images/10ct_32bit_128.tiff").save( b, format="tiff", resolution=123.45 ) - im = Image.open(b) - self.assertEqual(float(im.tag_v2[X_RESOLUTION]), 123.45) - self.assertEqual(float(im.tag_v2[Y_RESOLUTION]), 123.45) + with Image.open(b) as im: + assert float(im.tag_v2[X_RESOLUTION]) == 123.45 + assert float(im.tag_v2[Y_RESOLUTION]) == 123.45 def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, TiffImagePlugin.TiffImageFile, invalid_file) + with pytest.raises(SyntaxError): + TiffImagePlugin.TiffImageFile(invalid_file) TiffImagePlugin.PREFIXES.append(b"\xff\xd8\xff\xe0") - self.assertRaises(SyntaxError, TiffImagePlugin.TiffImageFile, invalid_file) + with pytest.raises(SyntaxError): + TiffImagePlugin.TiffImageFile(invalid_file) TiffImagePlugin.PREFIXES.pop() def test_bad_exif(self): with Image.open("Tests/images/hopper_bad_exif.jpg") as i: # Should not raise struct.error. - self.assert_warning(UserWarning, i._getexif) + pytest.warns(UserWarning, i._getexif) - def test_save_rgba(self): + def test_save_rgba(self, tmp_path): im = hopper("RGBA") - outfile = self.tempfile("temp.tif") + outfile = str(tmp_path / "temp.tif") im.save(outfile) - def test_save_unsupported_mode(self): + def test_save_unsupported_mode(self, tmp_path): im = hopper("HSV") - outfile = self.tempfile("temp.tif") - self.assertRaises(IOError, im.save, outfile) + outfile = str(tmp_path / "temp.tif") + with pytest.raises(OSError): + im.save(outfile) def test_little_endian(self): - im = Image.open("Tests/images/16bit.cropped.tif") - self.assertEqual(im.getpixel((0, 0)), 480) - self.assertEqual(im.mode, "I;16") + with Image.open("Tests/images/16bit.cropped.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16" - b = im.tobytes() + b = im.tobytes() # Bytes are in image native order (little endian) - self.assertEqual(b[0], ord(b"\xe0")) - self.assertEqual(b[1], ord(b"\x01")) + assert b[0] == ord(b"\xe0") + assert b[1] == ord(b"\x01") def test_big_endian(self): - im = Image.open("Tests/images/16bit.MM.cropped.tif") - self.assertEqual(im.getpixel((0, 0)), 480) - self.assertEqual(im.mode, "I;16B") - - b = im.tobytes() + with Image.open("Tests/images/16bit.MM.cropped.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16B" + b = im.tobytes() # Bytes are in image native order (big endian) - self.assertEqual(b[0], ord(b"\x01")) - self.assertEqual(b[1], ord(b"\xe0")) + assert b[0] == ord(b"\x01") + assert b[1] == ord(b"\xe0") def test_16bit_s(self): - im = Image.open("Tests/images/16bit.s.tif") - im.load() - self.assertEqual(im.mode, "I") - self.assertEqual(im.getpixel((0, 0)), 32767) - self.assertEqual(im.getpixel((0, 1)), 0) + 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): - """ Are we generating the same interpretation - of the image as Imagemagick is? """ + """Are we generating the same interpretation + of the image as Imagemagick is?""" - im = Image.open("Tests/images/12bit.cropped.tif") + with Image.open("Tests/images/12bit.cropped.tif") as im: + # to make the target -- + # convert 12bit.cropped.tif -depth 16 tmp.tif + # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif + # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, + # so we need to unshift so that the integer values are the same. - # to make the target -- - # convert 12bit.cropped.tif -depth 16 tmp.tif - # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif - # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, - # so we need to unshift so that the integer values are the same. - - self.assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") + assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") def test_32bit_float(self): # Issue 614, specific 32-bit float format path = "Tests/images/10ct_32bit_128.tiff" - im = Image.open(path) - im.load() + with Image.open(path) as im: + im.load() - self.assertEqual(im.getpixel((0, 0)), -0.4526388943195343) - self.assertEqual(im.getextrema(), (-3.140936851501465, 3.140684127807617)) + assert im.getpixel((0, 0)) == -0.4526388943195343 + assert im.getextrema() == (-3.140936851501465, 3.140684127807617) def test_unknown_pixel_mode(self): - self.assertRaises( - IOError, Image.open, "Tests/images/hopper_unknown_pixel_mode.tif" - ) + with pytest.raises(OSError): + Image.open("Tests/images/hopper_unknown_pixel_mode.tif") def test_n_frames(self): for path, n_frames in [ @@ -253,16 +256,17 @@ class TestFileTiff(PillowTestCase): ["Tests/images/multipage.tiff", 3], ]: with Image.open(path) as im: - self.assertEqual(im.n_frames, n_frames) - self.assertEqual(im.is_animated, n_frames != 1) + assert im.n_frames == n_frames + assert im.is_animated == (n_frames != 1) def test_eoferror(self): with Image.open("Tests/images/multipage-lastframe.tif") as im: n_frames = im.n_frames # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames # Test that seeking to the last frame does not raise an error im.seek(n_frames - 1) @@ -273,29 +277,29 @@ class TestFileTiff(PillowTestCase): # file is a multipage tiff: 10x10 green, 10x10 red, 20x20 blue im.seek(0) - self.assertEqual(im.size, (10, 10)) - self.assertEqual(im.convert("RGB").getpixel((0, 0)), (0, 128, 0)) + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) im.seek(1) im.load() - self.assertEqual(im.size, (10, 10)) - self.assertEqual(im.convert("RGB").getpixel((0, 0)), (255, 0, 0)) + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) im.seek(0) im.load() - self.assertEqual(im.size, (10, 10)) - self.assertEqual(im.convert("RGB").getpixel((0, 0)), (0, 128, 0)) + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) im.seek(2) im.load() - self.assertEqual(im.size, (20, 20)) - self.assertEqual(im.convert("RGB").getpixel((0, 0)), (0, 0, 255)) + assert im.size == (20, 20) + assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) def test_multipage_last_frame(self): - im = Image.open("Tests/images/multipage-lastframe.tif") - im.load() - self.assertEqual(im.size, (20, 20)) - self.assertEqual(im.convert("RGB").getpixel((0, 0)), (0, 0, 255)) + 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___str__(self): filename = "Tests/images/pil136.tiff" @@ -305,7 +309,7 @@ class TestFileTiff(PillowTestCase): ret = str(im.ifd) # Assert - self.assertIsInstance(ret, str) + assert isinstance(ret, str) def test_dict(self): # Arrange @@ -328,7 +332,7 @@ class TestFileTiff(PillowTestCase): 283: 72.0, 284: 1, } - self.assertEqual(dict(im.tag_v2), v2_tags) + assert dict(im.tag_v2) == v2_tags # legacy interface legacy_tags = { @@ -346,7 +350,7 @@ class TestFileTiff(PillowTestCase): 283: ((720000, 10000),), 284: (1,), } - self.assertEqual(dict(im.tag), legacy_tags) + assert dict(im.tag) == legacy_tags def test__delitem__(self): filename = "Tests/images/pil136.tiff" @@ -354,67 +358,73 @@ class TestFileTiff(PillowTestCase): len_before = len(dict(im.ifd)) del im.ifd[256] len_after = len(dict(im.ifd)) - self.assertEqual(len_before, len_after + 1) + assert len_before == len_after + 1 def test_load_byte(self): for legacy_api in [False, True]: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc" ret = ifd.load_byte(data, legacy_api) - self.assertEqual(ret, b"abc") + assert ret == b"abc" def test_load_string(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc\0" ret = ifd.load_string(data, False) - self.assertEqual(ret, "abc") + assert ret == "abc" def test_load_float(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" ret = ifd.load_float(data, False) - self.assertEqual(ret, (1.6777999408082104e22, 1.6777999408082104e22)) + assert ret == (1.6777999408082104e22, 1.6777999408082104e22) def test_load_double(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" ret = ifd.load_double(data, False) - self.assertEqual(ret, (8.540883223036124e194, 8.540883223036124e194)) + assert ret == (8.540883223036124e194, 8.540883223036124e194) + + def test_ifd_tag_type(self): + with Image.open("Tests/images/ifd_tag_type.tiff") as im: + assert 0x8825 in im.tag_v2 def test_seek(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: im.seek(0) - self.assertEqual(im.tell(), 0) + assert im.tell() == 0 def test_seek_eof(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - self.assertEqual(im.tell(), 0) - self.assertRaises(EOFError, im.seek, -1) - self.assertRaises(EOFError, im.seek, 1) + assert im.tell() == 0 + with pytest.raises(EOFError): + im.seek(-1) + with pytest.raises(EOFError): + im.seek(1) def test__limit_rational_int(self): from PIL.TiffImagePlugin import _limit_rational value = 34 ret = _limit_rational(value, 65536) - self.assertEqual(ret, (34, 1)) + assert ret == (34, 1) def test__limit_rational_float(self): from PIL.TiffImagePlugin import _limit_rational value = 22.3 ret = _limit_rational(value, 65536) - self.assertEqual(ret, (223, 10)) + assert ret == (223, 10) def test_4bit(self): test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") - im = Image.open(test_file) - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.mode, "L") - self.assert_image_similar(im, original, 7.3) + with Image.open(test_file) as im: + assert im.size == (128, 128) + assert im.mode == "L" + assert_image_similar(im, original, 7.3) def test_gray_semibyte_per_pixel(self): test_files = ( @@ -439,82 +449,81 @@ class TestFileTiff(PillowTestCase): ) original = hopper("L") for epsilon, group in test_files: - im = Image.open(group[0]) - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.mode, "L") - self.assert_image_similar(im, original, epsilon) - for file in group[1:]: - im2 = Image.open(file) - self.assertEqual(im2.size, (128, 128)) - self.assertEqual(im2.mode, "L") - self.assert_image_equal(im, im2) + with Image.open(group[0]) as im: + assert im.size == (128, 128) + assert im.mode == "L" + assert_image_similar(im, original, epsilon) + for file in group[1:]: + with Image.open(file) as im2: + assert im2.size == (128, 128) + assert im2.mode == "L" + assert_image_equal(im, im2) - def test_with_underscores(self): + def test_with_underscores(self, tmp_path): kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} - filename = self.tempfile("temp.tif") + filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, **kwargs) with Image.open(filename) as im: # legacy interface - self.assertEqual(im.tag[X_RESOLUTION][0][0], 72) - self.assertEqual(im.tag[Y_RESOLUTION][0][0], 36) + assert im.tag[X_RESOLUTION][0][0] == 72 + assert im.tag[Y_RESOLUTION][0][0] == 36 # v2 interface - self.assertEqual(im.tag_v2[X_RESOLUTION], 72) - self.assertEqual(im.tag_v2[Y_RESOLUTION], 36) + assert im.tag_v2[X_RESOLUTION] == 72 + assert im.tag_v2[Y_RESOLUTION] == 36 - def test_roundtrip_tiff_uint16(self): + def test_roundtrip_tiff_uint16(self, tmp_path): # Test an image of all '0' values pixel_value = 0x1234 infile = "Tests/images/uint16_1_4660.tif" - im = Image.open(infile) - self.assertEqual(im.getpixel((0, 0)), pixel_value) + with Image.open(infile) as im: + assert im.getpixel((0, 0)) == pixel_value - tmpfile = self.tempfile("temp.tif") - im.save(tmpfile) + tmpfile = str(tmp_path / "temp.tif") + im.save(tmpfile) - reloaded = Image.open(tmpfile) - - self.assert_image_equal(im, reloaded) + with Image.open(tmpfile) as reloaded: + assert_image_equal(im, reloaded) def test_strip_raw(self): infile = "Tests/images/tiff_strip_raw.tif" - im = Image.open(infile) - - self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") def test_strip_planar_raw(self): # gdal_translate -of GTiff -co INTERLEAVE=BAND \ # tiff_strip_raw.tif tiff_strip_planar_raw.tiff infile = "Tests/images/tiff_strip_planar_raw.tif" - im = Image.open(infile) - - self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + 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): # 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: - self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") def test_tiled_planar_raw(self): # 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 infile = "Tests/images/tiff_tiled_planar_raw.tif" - im = Image.open(infile) + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - - def test_palette(self): - for mode in ["P", "PA"]: - outfile = self.tempfile("temp.tif") + def test_palette(self, tmp_path): + def roundtrip(mode): + outfile = str(tmp_path / "temp.tif") im = hopper(mode) im.save(outfile) - reloaded = Image.open(outfile) - self.assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) + with Image.open(outfile) as reloaded: + assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) + + for mode in ["P", "PA"]: + roundtrip(mode) def test_tiff_save_all(self): mp = BytesIO() @@ -523,7 +532,7 @@ class TestFileTiff(PillowTestCase): mp.seek(0, os.SEEK_SET) with Image.open(mp) as im: - self.assertEqual(im.n_frames, 3) + assert im.n_frames == 3 # Test appending images mp = BytesIO() @@ -532,8 +541,8 @@ class TestFileTiff(PillowTestCase): im.copy().save(mp, format="TIFF", save_all=True, append_images=ims) mp.seek(0, os.SEEK_SET) - reread = Image.open(mp) - self.assertEqual(reread.n_frames, 3) + with Image.open(mp) as reread: + assert reread.n_frames == 3 # Test appending using a generator def imGenerator(ims): @@ -543,10 +552,10 @@ class TestFileTiff(PillowTestCase): im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims)) mp.seek(0, os.SEEK_SET) - reread = Image.open(mp) - self.assertEqual(reread.n_frames, 3) + with Image.open(mp) as reread: + assert reread.n_frames == 3 - def test_saving_icc_profile(self): + def test_saving_icc_profile(self, tmp_path): # 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, @@ -555,26 +564,26 @@ class TestFileTiff(PillowTestCase): im.info["icc_profile"] = "Dummy value" # Try save-load round trip to make sure both handle icc_profile. - tmpfile = self.tempfile("temp.tif") + tmpfile = str(tmp_path / "temp.tif") im.save(tmpfile, "TIFF", compression="raw") with Image.open(tmpfile) as reloaded: - self.assertEqual(b"Dummy value", reloaded.info["icc_profile"]) + assert b"Dummy value" == reloaded.info["icc_profile"] - def test_close_on_load_exclusive(self): + def test_close_on_load_exclusive(self, tmp_path): # similar to test_fd_leak, but runs on unixlike os - tmpfile = self.tempfile("temp.tif") + tmpfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) im = Image.open(tmpfile) fp = im.fp - self.assertFalse(fp.closed) + assert not fp.closed im.load() - self.assertTrue(fp.closed) + assert fp.closed - def test_close_on_load_nonexclusive(self): - tmpfile = self.tempfile("temp.tif") + def test_close_on_load_nonexclusive(self, tmp_path): + tmpfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) @@ -582,23 +591,27 @@ class TestFileTiff(PillowTestCase): with open(tmpfile, "rb") as f: im = Image.open(f) fp = im.fp - self.assertFalse(fp.closed) + assert not fp.closed im.load() - self.assertFalse(fp.closed) + assert not fp.closed # Ignore this UserWarning which triggers for four tags: # "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..." @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") + @pytest.mark.skipif( + not os.path.exists("Tests/images/string_dimension.tiff"), + reason="Extra image files not installed", + ) def test_string_dimension(self): # Assert that an error is raised if one of the dimensions is a string - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Image.open("Tests/images/string_dimension.tiff") -@unittest.skipUnless(is_win32(), "Windows only") -class TestFileTiffW32(PillowTestCase): - def test_fd_leak(self): - tmpfile = self.tempfile("temp.tif") +@pytest.mark.skipif(not is_win32(), reason="Windows only") +class TestFileTiffW32: + def test_fd_leak(self, tmp_path): + tmpfile = str(tmp_path / "temp.tif") # this is an mmaped file. with Image.open("Tests/images/uint16_1_4660.tif") as im: @@ -606,10 +619,11 @@ class TestFileTiffW32(PillowTestCase): im = Image.open(tmpfile) fp = im.fp - self.assertFalse(fp.closed) - self.assertRaises(WindowsError, os.remove, tmpfile) + assert not fp.closed + with pytest.raises(OSError): + os.remove(tmpfile) im.load() - self.assertTrue(fp.closed) + assert fp.closed # this closes the mmap im.close() diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 7393a147c..0f7f8adf1 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,262 +1,357 @@ import io import struct +import pytest + from PIL import Image, TiffImagePlugin, TiffTags -from PIL.TiffImagePlugin import IFDRational, _limit_rational +from PIL.TiffImagePlugin import IFDRational -from .helper import PillowTestCase, hopper +from .helper import assert_deep_equal, hopper -tag_ids = {info.name: info.value for info in TiffTags.TAGS_V2.values()} +TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()} -class TestFileTiffMetadata(PillowTestCase): - def test_rt_metadata(self): - """ 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 - """ +def test_rt_metadata(tmp_path): + """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 + """ - img = hopper() + img = hopper() - # Behaviour change: re #1416 - # Pre ifd rewrite, ImageJMetaData was being written as a string(2), - # Post ifd rewrite, it's defined as arbitrary bytes(7). It should - # roundtrip with the actual bytes, rather than stripped text - # of the premerge tests. - # - # For text items, we still have to decode('ascii','replace') because - # the tiff file format can't take 8 bit bytes in that field. + # Behaviour change: re #1416 + # Pre ifd rewrite, ImageJMetaData was being written as a string(2), + # Post ifd rewrite, it's defined as arbitrary bytes(7). It should + # roundtrip with the actual bytes, rather than stripped text + # of the premerge tests. + # + # For text items, we still have to decode('ascii','replace') because + # the tiff file format can't take 8 bit bytes in that field. - basetextdata = "This is some arbitrary metadata for a text field" - bindata = basetextdata.encode("ascii") + b" \xff" - textdata = basetextdata + " " + chr(255) - reloaded_textdata = basetextdata + " ?" - floatdata = 12.345 - doubledata = 67.89 - info = TiffImagePlugin.ImageFileDirectory() + basetextdata = "This is some arbitrary metadata for a text field" + bindata = basetextdata.encode("ascii") + b" \xff" + textdata = basetextdata + " " + chr(255) + reloaded_textdata = basetextdata + " ?" + floatdata = 12.345 + doubledata = 67.89 + info = TiffImagePlugin.ImageFileDirectory() - ImageJMetaData = tag_ids["ImageJMetaData"] - ImageJMetaDataByteCounts = tag_ids["ImageJMetaDataByteCounts"] - ImageDescription = tag_ids["ImageDescription"] + ImageJMetaData = TAG_IDS["ImageJMetaData"] + ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"] + ImageDescription = TAG_IDS["ImageDescription"] - info[ImageJMetaDataByteCounts] = len(bindata) - info[ImageJMetaData] = bindata - info[tag_ids["RollAngle"]] = floatdata - info.tagtype[tag_ids["RollAngle"]] = 11 - info[tag_ids["YawAngle"]] = doubledata - info.tagtype[tag_ids["YawAngle"]] = 12 + info[ImageJMetaDataByteCounts] = len(bindata) + info[ImageJMetaData] = bindata + info[TAG_IDS["RollAngle"]] = floatdata + info.tagtype[TAG_IDS["RollAngle"]] = 11 + info[TAG_IDS["YawAngle"]] = doubledata + info.tagtype[TAG_IDS["YawAngle"]] = 12 - info[ImageDescription] = textdata + info[ImageDescription] = textdata - f = self.tempfile("temp.tif") + f = str(tmp_path / "temp.tif") - img.save(f, tiffinfo=info) + img.save(f, tiffinfo=info) - with Image.open(f) as loaded: + with Image.open(f) as loaded: - self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (len(bindata),)) + assert loaded.tag[ImageJMetaDataByteCounts] == (len(bindata),) + assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bindata),) - self.assertEqual(loaded.tag[ImageJMetaData], bindata) - self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata) + assert loaded.tag[ImageJMetaData] == bindata + assert loaded.tag_v2[ImageJMetaData] == bindata - self.assertEqual(loaded.tag[ImageDescription], (reloaded_textdata,)) - self.assertEqual(loaded.tag_v2[ImageDescription], reloaded_textdata) + assert loaded.tag[ImageDescription] == (reloaded_textdata,) + assert loaded.tag_v2[ImageDescription] == reloaded_textdata - loaded_float = loaded.tag[tag_ids["RollAngle"]][0] - self.assertAlmostEqual(loaded_float, floatdata, places=5) - loaded_double = loaded.tag[tag_ids["YawAngle"]][0] - self.assertAlmostEqual(loaded_double, doubledata) + loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0] + assert round(abs(loaded_float - floatdata), 5) == 0 + loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0] + assert round(abs(loaded_double - doubledata), 7) == 0 - # check with 2 element ImageJMetaDataByteCounts, issue #2006 + # check with 2 element ImageJMetaDataByteCounts, issue #2006 - info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8) - img.save(f, tiffinfo=info) - with Image.open(f) as loaded: + info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8) + img.save(f, tiffinfo=info) + with Image.open(f) as loaded: - self.assertEqual( - loaded.tag[ImageJMetaDataByteCounts], (8, len(bindata) - 8) - ) - self.assertEqual( - loaded.tag_v2[ImageJMetaDataByteCounts], (8, len(bindata) - 8) - ) + assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) + assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) - def test_read_metadata(self): - with Image.open("Tests/images/hopper_g4.tif") as img: - self.assertEqual( - { - "YResolution": IFDRational(4294967295, 113653537), - "PlanarConfiguration": 1, - "BitsPerSample": (1,), - "ImageLength": 128, - "Compression": 4, - "FillOrder": 1, - "RowsPerStrip": 128, - "ResolutionUnit": 3, - "PhotometricInterpretation": 0, - "PageNumber": (0, 1), - "XResolution": IFDRational(4294967295, 113653537), - "ImageWidth": 128, - "Orientation": 1, - "StripByteCounts": (1968,), - "SamplesPerPixel": 1, - "StripOffsets": (8,), - }, - img.tag_v2.named(), - ) +def test_read_metadata(): + with Image.open("Tests/images/hopper_g4.tif") as img: - self.assertEqual( - { - "YResolution": ((4294967295, 113653537),), - "PlanarConfiguration": (1,), - "BitsPerSample": (1,), - "ImageLength": (128,), - "Compression": (4,), - "FillOrder": (1,), - "RowsPerStrip": (128,), - "ResolutionUnit": (3,), - "PhotometricInterpretation": (0,), - "PageNumber": (0, 1), - "XResolution": ((4294967295, 113653537),), - "ImageWidth": (128,), - "Orientation": (1,), - "StripByteCounts": (1968,), - "SamplesPerPixel": (1,), - "StripOffsets": (8,), - }, - img.tag.named(), - ) + assert { + "YResolution": IFDRational(4294967295, 113653537), + "PlanarConfiguration": 1, + "BitsPerSample": (1,), + "ImageLength": 128, + "Compression": 4, + "FillOrder": 1, + "RowsPerStrip": 128, + "ResolutionUnit": 3, + "PhotometricInterpretation": 0, + "PageNumber": (0, 1), + "XResolution": IFDRational(4294967295, 113653537), + "ImageWidth": 128, + "Orientation": 1, + "StripByteCounts": (1968,), + "SamplesPerPixel": 1, + "StripOffsets": (8,), + } == img.tag_v2.named() - def test_write_metadata(self): - """ Test metadata writing through the python code """ - img = Image.open("Tests/images/hopper.tif") + assert { + "YResolution": ((4294967295, 113653537),), + "PlanarConfiguration": (1,), + "BitsPerSample": (1,), + "ImageLength": (128,), + "Compression": (4,), + "FillOrder": (1,), + "RowsPerStrip": (128,), + "ResolutionUnit": (3,), + "PhotometricInterpretation": (0,), + "PageNumber": (0, 1), + "XResolution": ((4294967295, 113653537),), + "ImageWidth": (128,), + "Orientation": (1,), + "StripByteCounts": (1968,), + "SamplesPerPixel": (1,), + "StripOffsets": (8,), + } == img.tag.named() - f = self.tempfile("temp.tiff") + +def test_write_metadata(tmp_path): + """ Test metadata writing through the python code """ + with Image.open("Tests/images/hopper.tif") as img: + f = str(tmp_path / "temp.tiff") img.save(f, tiffinfo=img.tag) - with Image.open(f) as loaded: + original = img.tag_v2.named() - original = img.tag_v2.named() - reloaded = loaded.tag_v2.named() + with Image.open(f) as loaded: + reloaded = loaded.tag_v2.named() - for k, v in original.items(): - if isinstance(v, IFDRational): - original[k] = IFDRational(*_limit_rational(v, 2 ** 31)) - elif isinstance(v, tuple) and isinstance(v[0], IFDRational): - original[k] = tuple( - IFDRational(*_limit_rational(elt, 2 ** 31)) for elt in v - ) + ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] - ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] + for tag, value in reloaded.items(): + if tag in ignored: + continue + if isinstance(original[tag], tuple) and isinstance( + original[tag][0], IFDRational + ): + # Need to compare element by element in the tuple, + # not comparing tuples of object references + assert_deep_equal( + original[tag], + value, + f"{tag} didn't roundtrip, {original[tag]}, {value}", + ) + else: + assert ( + original[tag] == value + ), f"{tag} didn't roundtrip, {original[tag]}, {value}" - for tag, value in reloaded.items(): - if tag in ignored: - continue - if isinstance(original[tag], tuple) and isinstance( - original[tag][0], IFDRational - ): - # Need to compare element by element in the tuple, - # not comparing tuples of object references - self.assert_deep_equal( - original[tag], - value, - "{} didn't roundtrip, {}, {}".format(tag, original[tag], value), - ) - else: - self.assertEqual( - original[tag], - value, - "{} didn't roundtrip, {}, {}".format(tag, original[tag], value), - ) + for tag, value in original.items(): + if tag not in ignored: + assert value == reloaded[tag], f"{tag} didn't roundtrip" - for tag, value in original.items(): - if tag not in ignored: - self.assertEqual(value, reloaded[tag], "%s didn't roundtrip" % tag) - def test_no_duplicate_50741_tag(self): - self.assertEqual(tag_ids["MakerNoteSafety"], 50741) - self.assertEqual(tag_ids["BestQualityScale"], 50780) +def test_change_stripbytecounts_tag_type(tmp_path): + out = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper.tif") as im: + info = im.tag_v2 - def test_empty_metadata(self): - f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") - head = f.read(8) - info = TiffImagePlugin.ImageFileDirectory(head) - # Should not raise struct.error. - self.assert_warning(UserWarning, info.load, f) + # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT + im = im.resize((500, 500)) - def test_iccprofile(self): - # https://github.com/python-pillow/Pillow/issues/1462 - im = Image.open("Tests/images/hopper.iccprofile.tif") - out = self.tempfile("temp.tiff") + # STRIPBYTECOUNTS can be a SHORT or a LONG + info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG + + +def test_no_duplicate_50741_tag(): + assert TAG_IDS["MakerNoteSafety"] == 50741 + assert TAG_IDS["BestQualityScale"] == 50780 + + +def test_empty_metadata(): + f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") + head = f.read(8) + info = TiffImagePlugin.ImageFileDirectory(head) + # Should not raise struct.error. + pytest.warns(UserWarning, info.load, f) + + +def test_iccprofile(tmp_path): + # https://github.com/python-pillow/Pillow/issues/1462 + out = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper.iccprofile.tif") as im: im.save(out) - with Image.open(out) as reloaded: - self.assertNotIsInstance(im.info["icc_profile"], tuple) - self.assertEqual(im.info["icc_profile"], reloaded.info["icc_profile"]) - def test_iccprofile_binary(self): - # https://github.com/python-pillow/Pillow/issues/1526 - # We should be able to load this, - # but probably won't be able to save it. + with Image.open(out) as reloaded: + assert not isinstance(im.info["icc_profile"], tuple) + assert im.info["icc_profile"] == reloaded.info["icc_profile"] - with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: - self.assertEqual(im.tag_v2.tagtype[34675], 1) - self.assertTrue(im.info["icc_profile"]) - def test_iccprofile_save_png(self): - im = Image.open("Tests/images/hopper.iccprofile.tif") - outfile = self.tempfile("temp.png") +def test_iccprofile_binary(): + # https://github.com/python-pillow/Pillow/issues/1526 + # We should be able to load this, + # but probably won't be able to save it. + + with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + assert im.tag_v2.tagtype[34675] == 1 + assert im.info["icc_profile"] + + +def test_iccprofile_save_png(tmp_path): + 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(self): - im = Image.open("Tests/images/hopper.iccprofile_binary.tif") - outfile = self.tempfile("temp.png") + +def test_iccprofile_binary_save_png(tmp_path): + 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(self): - im = hopper() - info = TiffImagePlugin.ImageFileDirectory_v2() - info[41988] = TiffImagePlugin.IFDRational(0, 0) - out = self.tempfile("temp.tiff") - im.save(out, tiffinfo=info, compression="raw") +def test_exif_div_zero(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + info[41988] = TiffImagePlugin.IFDRational(0, 0) - with Image.open(out) as reloaded: - self.assertEqual(0, reloaded.tag_v2[41988].numerator) - self.assertEqual(0, reloaded.tag_v2[41988].denominator) + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") - def test_empty_values(self): - 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" - b"\x98\x82\x02\x00\x07\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00a " - b"text\x00\x00" - ) - head = data.read(8) - info = TiffImagePlugin.ImageFileDirectory_v2(head) - info.load(data) - # Should not raise ValueError. - info = dict(info) - self.assertIn(33432, info) + with Image.open(out) as reloaded: + assert 0 == reloaded.tag_v2[41988].numerator + assert 0 == reloaded.tag_v2[41988].denominator - def test_PhotoshopInfo(self): - im = Image.open("Tests/images/issue_2278.tif") - self.assertEqual(len(im.tag_v2[34377]), 1) - self.assertIsInstance(im.tag_v2[34377][0], bytes) - out = self.tempfile("temp.tiff") +def test_ifd_unsigned_rational(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + max_long = 2 ** 32 - 1 + + # 4 bytes unsigned long + numerator = max_long + + info[41493] = TiffImagePlugin.IFDRational(numerator, 1) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert max_long == reloaded.tag_v2[41493].numerator + assert 1 == reloaded.tag_v2[41493].denominator + + # out of bounds of 4 byte unsigned long + numerator = max_long + 1 + + info[41493] = TiffImagePlugin.IFDRational(numerator, 1) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert max_long == reloaded.tag_v2[41493].numerator + assert 1 == reloaded.tag_v2[41493].denominator + + +def test_ifd_signed_rational(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + # pair of 4 byte signed longs + numerator = 2 ** 31 - 1 + denominator = -(2 ** 31) + + info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert numerator == reloaded.tag_v2[37380].numerator + assert denominator == reloaded.tag_v2[37380].denominator + + numerator = -(2 ** 31) + denominator = 2 ** 31 - 1 + + info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert numerator == reloaded.tag_v2[37380].numerator + assert denominator == reloaded.tag_v2[37380].denominator + + # out of bounds of 4 byte signed long + numerator = -(2 ** 31) - 1 + denominator = 1 + + info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert 2 ** 31 - 1 == reloaded.tag_v2[37380].numerator + assert -1 == reloaded.tag_v2[37380].denominator + + +def test_ifd_signed_long(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + info[37000] = -60000 + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[37000] == -60000 + + +def test_empty_values(): + 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" + b"\x98\x82\x02\x00\x07\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00a " + b"text\x00\x00" + ) + head = data.read(8) + info = TiffImagePlugin.ImageFileDirectory_v2(head) + info.load(data) + # Should not raise ValueError. + info = dict(info) + assert 33432 in info + + +def test_PhotoshopInfo(tmp_path): + with Image.open("Tests/images/issue_2278.tif") as im: + assert len(im.tag_v2[34377]) == 70 + assert isinstance(im.tag_v2[34377], bytes) + out = str(tmp_path / "temp.tiff") im.save(out) - with Image.open(out) as reloaded: - self.assertEqual(len(reloaded.tag_v2[34377]), 1) - self.assertIsInstance(reloaded.tag_v2[34377][0], bytes) + with Image.open(out) as reloaded: + assert len(reloaded.tag_v2[34377]) == 70 + assert isinstance(reloaded.tag_v2[34377], bytes) - def test_too_many_entries(self): - ifd = TiffImagePlugin.ImageFileDirectory_v2() - # 277: ("SamplesPerPixel", SHORT, 1), - ifd._tagdata[277] = struct.pack("hh", 4, 4) - ifd.tagtype[277] = TiffTags.SHORT +def test_too_many_entries(): + ifd = TiffImagePlugin.ImageFileDirectory_v2() - # Should not raise ValueError. - self.assert_warning(UserWarning, lambda: ifd[277]) + # 277: ("SamplesPerPixel", SHORT, 1), + ifd._tagdata[277] = struct.pack("hh", 4, 4) + ifd.tagtype[277] = TiffTags.SHORT + + # Should not raise ValueError. + pytest.warns(UserWarning, lambda: ifd[277]) diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index 74c238fc1..60be1d5bc 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -1,18 +1,15 @@ from PIL import WalImageFile -from .helper import PillowTestCase +def test_open(): + # Arrange + TEST_FILE = "Tests/images/hopper.wal" -class TestFileWal(PillowTestCase): - def test_open(self): - # Arrange - TEST_FILE = "Tests/images/hopper.wal" + # Act + im = WalImageFile.open(TEST_FILE) - # Act - im = WalImageFile.open(TEST_FILE) - - # Assert - self.assertEqual(im.format, "WAL") - self.assertEqual(im.format_description, "Quake2 Texture") - self.assertEqual(im.mode, "P") - self.assertEqual(im.size, (128, 128)) + # Assert + assert im.format == "WAL" + assert im.format_description == "Quake2 Texture" + assert im.mode == "P" + assert im.size == (128, 128) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 685f876e2..11fbd9fd5 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,8 +1,16 @@ -import unittest +import io +import re -from PIL import Image, WebPImagePlugin +import pytest -from .helper import PillowTestCase, hopper +from PIL import Image, WebPImagePlugin, features + +from .helper import ( + assert_image_similar, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) try: from PIL import _webp @@ -12,28 +20,27 @@ except ImportError: HAVE_WEBP = False -class TestUnsupportedWebp(PillowTestCase): +class TestUnsupportedWebp: def test_unsupported(self): if HAVE_WEBP: WebPImagePlugin.SUPPORTED = False file_path = "Tests/images/hopper.webp" - self.assert_warning( - UserWarning, lambda: self.assertRaises(IOError, Image.open, file_path) - ) + pytest.warns(UserWarning, lambda: pytest.raises(OSError, Image.open, file_path)) if HAVE_WEBP: WebPImagePlugin.SUPPORTED = True -@unittest.skipIf(not HAVE_WEBP, "WebP support not installed") -class TestFileWebp(PillowTestCase): - def setUp(self): +@skip_unless_feature("webp") +class TestFileWebp: + def setup_method(self): self.rgb_mode = "RGB" def test_version(self): _webp.WebPDecoderVersion() _webp.WebPDecoderBuggyAlpha() + assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp")) def test_read_rgb(self): """ @@ -41,89 +48,76 @@ class TestFileWebp(PillowTestCase): Does it have the bits we expect? """ - image = Image.open("Tests/images/hopper.webp") + with Image.open("Tests/images/hopper.webp") as image: + assert image.mode == self.rgb_mode + assert image.size == (128, 128) + assert image.format == "WEBP" + image.load() + image.getdata() - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") - image.load() - image.getdata() + # generated with: + # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm + assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0) - # generated with: - # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm - self.assert_image_similar_tofile( - image, "Tests/images/hopper_webp_bits.ppm", 1.0 - ) + def _roundtrip(self, tmp_path, mode, epsilon, args={}): + temp_file = str(tmp_path / "temp.webp") - def test_write_rgb(self): + hopper(mode).save(temp_file, **args) + with Image.open(temp_file) as image: + assert image.mode == self.rgb_mode + assert image.size == (128, 128) + assert image.format == "WEBP" + image.load() + image.getdata() + + if mode == self.rgb_mode: + # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm + assert_image_similar_tofile( + image, "Tests/images/hopper_webp_write.ppm", 12.0 + ) + + # This test asserts that the images are similar. If the average pixel + # difference between the two images is less than the epsilon value, + # then we're going to accept that it's a reasonable lossy version of + # the image. + target = hopper(mode) + if mode != self.rgb_mode: + target = target.convert(self.rgb_mode) + assert_image_similar(image, target, epsilon) + + def test_write_rgb(self, tmp_path): """ - Can we write a RGB mode file to webp without error. + Can we write a RGB mode file to webp without error? Does it have the bits we expect? """ - temp_file = self.tempfile("temp.webp") + self._roundtrip(tmp_path, self.rgb_mode, 12.5) - hopper(self.rgb_mode).save(temp_file) - image = Image.open(temp_file) + def test_write_method(self, tmp_path): + self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6}) - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") - image.load() - image.getdata() + buffer_no_args = io.BytesIO() + hopper().save(buffer_no_args, format="WEBP") - # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm - self.assert_image_similar_tofile( - image, "Tests/images/hopper_webp_write.ppm", 12.0 - ) + buffer_method = io.BytesIO() + hopper().save(buffer_method, format="WEBP", method=6) + assert buffer_no_args.getbuffer() != buffer_method.getbuffer() - # This test asserts that the images are similar. If the average pixel - # difference between the two images is less than the epsilon value, - # then we're going to accept that it's a reasonable lossy version of - # the image. The old lena images for WebP are showing ~16 on - # Ubuntu, the jpegs are showing ~18. - target = hopper(self.rgb_mode) - self.assert_image_similar(image, target, 12.0) - - def test_write_unsupported_mode_L(self): + def test_write_unsupported_mode_L(self, tmp_path): """ Saving a black-and-white file to WebP format should work, and be similar to the original file. """ - temp_file = self.tempfile("temp.webp") - hopper("L").save(temp_file) - image = Image.open(temp_file) + self._roundtrip(tmp_path, "L", 10.0) - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") - - image.load() - image.getdata() - target = hopper("L").convert(self.rgb_mode) - - self.assert_image_similar(image, target, 10.0) - - def test_write_unsupported_mode_P(self): + def test_write_unsupported_mode_P(self, tmp_path): """ Saving a palette-based file to WebP format should work, and be similar to the original file. """ - temp_file = self.tempfile("temp.webp") - hopper("P").save(temp_file) - image = Image.open(temp_file) - - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") - - image.load() - image.getdata() - target = hopper("P").convert(self.rgb_mode) - - self.assert_image_similar(image, target, 50.0) + self._roundtrip(tmp_path, "P", 50.0) def test_WebPEncode_with_invalid_args(self): """ @@ -131,8 +125,10 @@ class TestFileWebp(PillowTestCase): """ if _webp.HAVE_WEBPANIM: - self.assertRaises(TypeError, _webp.WebPAnimEncoder) - self.assertRaises(TypeError, _webp.WebPEncode) + with pytest.raises(TypeError): + _webp.WebPAnimEncoder() + with pytest.raises(TypeError): + _webp.WebPEncode() def test_WebPDecode_with_invalid_args(self): """ @@ -140,15 +136,16 @@ class TestFileWebp(PillowTestCase): """ if _webp.HAVE_WEBPANIM: - self.assertRaises(TypeError, _webp.WebPAnimDecoder) - self.assertRaises(TypeError, _webp.WebPDecode) + with pytest.raises(TypeError): + _webp.WebPAnimDecoder() + with pytest.raises(TypeError): + _webp.WebPDecode() - def test_no_resource_warning(self): + def test_no_resource_warning(self, tmp_path): file_path = "Tests/images/hopper.webp" - image = Image.open(file_path) - - temp_file = self.tempfile("temp.webp") - self.assert_warning(None, image.save, temp_file) + with Image.open(file_path) as image: + temp_file = str(tmp_path / "temp.webp") + pytest.warns(None, image.save, temp_file) def test_file_pointer_could_be_reused(self): file_path = "Tests/images/hopper.webp" @@ -156,19 +153,18 @@ class TestFileWebp(PillowTestCase): Image.open(blob).load() Image.open(blob).load() - @unittest.skipUnless( - HAVE_WEBP and _webp.HAVE_WEBPANIM, "WebP save all not available" - ) - def test_background_from_gif(self): + @skip_unless_feature("webp") + @skip_unless_feature("webp_anim") + def test_background_from_gif(self, tmp_path): with Image.open("Tests/images/chi.gif") as im: original_value = im.convert("RGB").getpixel((1, 1)) # Save as WEBP - out_webp = self.tempfile("temp.webp") + out_webp = str(tmp_path / "temp.webp") im.save(out_webp, save_all=True) # Save as GIF - out_gif = self.tempfile("temp.gif") + out_gif = str(tmp_path / "temp.gif") Image.open(out_webp).save(out_gif) with Image.open(out_gif) as reread: @@ -176,4 +172,4 @@ class TestFileWebp(PillowTestCase): difference = sum( [abs(original_value[i] - reread_value[i]) for i in range(0, 3)] ) - self.assertLess(difference, 5) + assert difference < 5 diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index e3c2b98b9..362edac1a 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,121 +1,116 @@ -import unittest +import pytest from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, assert_image_similar, hopper -try: - from PIL import _webp -except ImportError: - _webp = None +_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") -@unittest.skipIf(_webp is None, "WebP support not installed") -class TestFileWebpAlpha(PillowTestCase): - def setUp(self): - if _webp.WebPDecoderBuggyAlpha(self): - self.skipTest( - "Buggy early version of WebP installed, not testing transparency" - ) +def setup_module(): + if _webp.WebPDecoderBuggyAlpha(): + pytest.skip("Buggy early version of WebP installed, not testing transparency") - def test_read_rgba(self): - """ - Can we read an RGBA mode file without error? - Does it have the bits we expect? - """ - # Generated with `cwebp transparent.png -o transparent.webp` - file_path = "Tests/images/transparent.webp" - image = Image.open(file_path) +def test_read_rgba(): + """ + Can we read an RGBA mode file without error? + Does it have the bits we expect? + """ - self.assertEqual(image.mode, "RGBA") - self.assertEqual(image.size, (200, 150)) - self.assertEqual(image.format, "WEBP") + # Generated with `cwebp transparent.png -o transparent.webp` + file_path = "Tests/images/transparent.webp" + with Image.open(file_path) as image: + assert image.mode == "RGBA" + assert image.size == (200, 150) + assert image.format == "WEBP" image.load() image.getdata() image.tobytes() - target = Image.open("Tests/images/transparent.png") - self.assert_image_similar(image, target, 20.0) + with Image.open("Tests/images/transparent.png") as target: + assert_image_similar(image, target, 20.0) - def test_write_lossless_rgb(self): - """ - Can we write an RGBA mode file with lossless compression without - error? Does it have the bits we expect? - """ - temp_file = self.tempfile("temp.webp") - # temp_file = "temp.webp" +def test_write_lossless_rgb(tmp_path): + """ + Can we write an RGBA mode file with lossless compression without error? + Does it have the bits we expect? + """ - pil_image = hopper("RGBA") + temp_file = str(tmp_path / "temp.webp") + # temp_file = "temp.webp" - mask = Image.new("RGBA", (64, 64), (128, 128, 128, 128)) - # Add some partially transparent bits: - pil_image.paste(mask, (0, 0), mask) + pil_image = hopper("RGBA") - pil_image.save(temp_file, lossless=True) + mask = Image.new("RGBA", (64, 64), (128, 128, 128, 128)) + # Add some partially transparent bits: + pil_image.paste(mask, (0, 0), mask) - image = Image.open(temp_file) + pil_image.save(temp_file, lossless=True) + + with Image.open(temp_file) as image: image.load() - self.assertEqual(image.mode, "RGBA") - self.assertEqual(image.size, pil_image.size) - self.assertEqual(image.format, "WEBP") + assert image.mode == "RGBA" + assert image.size == pil_image.size + assert image.format == "WEBP" image.load() image.getdata() - self.assert_image_equal(image, pil_image) + assert_image_equal(image, pil_image) - def test_write_rgba(self): - """ - Can we write a RGBA mode file to webp without error. - Does it have the bits we expect? - """ - temp_file = self.tempfile("temp.webp") +def test_write_rgba(tmp_path): + """ + Can we write a RGBA mode file to WebP without error. + Does it have the bits we expect? + """ - pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) - pil_image.save(temp_file) + temp_file = str(tmp_path / "temp.webp") - if _webp.WebPDecoderBuggyAlpha(self): - return + pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) + pil_image.save(temp_file) - image = Image.open(temp_file) + if _webp.WebPDecoderBuggyAlpha(): + return + + with Image.open(temp_file) as image: image.load() - self.assertEqual(image.mode, "RGBA") - self.assertEqual(image.size, (10, 10)) - self.assertEqual(image.format, "WEBP") + assert image.mode == "RGBA" + assert image.size == (10, 10) + assert image.format == "WEBP" image.load() image.getdata() - # early versions of webp are known to produce higher deviations: + # Early versions of WebP are known to produce higher deviations: # deal with it - if _webp.WebPDecoderVersion(self) <= 0x201: - self.assert_image_similar(image, pil_image, 3.0) + if _webp.WebPDecoderVersion() <= 0x201: + assert_image_similar(image, pil_image, 3.0) else: - self.assert_image_similar(image, pil_image, 1.0) + assert_image_similar(image, pil_image, 1.0) - def test_write_unsupported_mode_PA(self): - """ - Saving a palette-based file with transparency to WebP format - should work, and be similar to the original file. - """ - temp_file = self.tempfile("temp.webp") - file_path = "Tests/images/transparent.gif" - with Image.open(file_path) as im: - im.save(temp_file) - image = Image.open(temp_file) +def test_write_unsupported_mode_PA(tmp_path): + """ + Saving a palette-based file with transparency to WebP format + should work, and be similar to the original file. + """ - self.assertEqual(image.mode, "RGBA") - self.assertEqual(image.size, (200, 150)) - self.assertEqual(image.format, "WEBP") + temp_file = str(tmp_path / "temp.webp") + file_path = "Tests/images/transparent.gif" + with Image.open(file_path) as im: + im.save(temp_file) + with Image.open(temp_file) as image: + assert image.mode == "RGBA" + assert image.size == (200, 150) + assert image.format == "WEBP" image.load() image.getdata() with Image.open(file_path) as im: target = im.convert("RGBA") - self.assert_image_similar(image, target, 25.0) + assert_image_similar(image, target, 25.0) diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 834080db8..26e903488 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,164 +1,166 @@ +import pytest + from PIL import Image -from .helper import PillowTestCase +from .helper import ( + assert_image_equal, + assert_image_similar, + is_big_endian, + skip_unless_feature, +) -try: - from PIL import _webp - - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False +pytestmark = [ + skip_unless_feature("webp"), + skip_unless_feature("webp_anim"), +] -class TestFileWebpAnimation(PillowTestCase): - def setUp(self): - if not HAVE_WEBP: - self.skipTest("WebP support not installed") - return +def test_n_frames(): + """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" - if not _webp.HAVE_WEBPANIM: - self.skipTest( - "WebP library does not contain animation support, " - "not testing animation" - ) + with Image.open("Tests/images/hopper.webp") as im: + assert im.n_frames == 1 + assert not im.is_animated - def test_n_frames(self): - """ - Ensure that WebP format sets n_frames and is_animated - attributes correctly. - """ + with Image.open("Tests/images/iss634.webp") as im: + assert im.n_frames == 42 + assert im.is_animated - with Image.open("Tests/images/hopper.webp") as im: - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) - with Image.open("Tests/images/iss634.webp") as im: - self.assertEqual(im.n_frames, 42) - self.assertTrue(im.is_animated) +@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") +def test_write_animation_L(tmp_path): + """ + Convert an animated GIF to animated WebP, then compare the frame count, and first + and last frames to ensure they're visually similar. + """ - def test_write_animation_L(self): - """ - Convert an animated GIF to animated WebP, then compare the - frame count, and first and last frames to ensure they're - visually similar. - """ + with Image.open("Tests/images/iss634.gif") as orig: + assert orig.n_frames > 1 - with Image.open("Tests/images/iss634.gif") as orig: - self.assertGreater(orig.n_frames, 1) - - temp_file = self.tempfile("temp.webp") - orig.save(temp_file, save_all=True) - im = Image.open(temp_file) - self.assertEqual(im.n_frames, orig.n_frames) + temp_file = str(tmp_path / "temp.webp") + orig.save(temp_file, save_all=True) + with Image.open(temp_file) as im: + assert im.n_frames == orig.n_frames # Compare first and last frames to the original animated GIF orig.load() im.load() - self.assert_image_similar(im, orig.convert("RGBA"), 25.0) + assert_image_similar(im, orig.convert("RGBA"), 25.0) orig.seek(orig.n_frames - 1) im.seek(im.n_frames - 1) orig.load() im.load() - self.assert_image_similar(im, orig.convert("RGBA"), 25.0) + assert_image_similar(im, orig.convert("RGBA"), 25.0) - def test_write_animation_RGB(self): - """ - Write an animated WebP from RGB frames, and ensure the frames - are visually similar to the originals. - """ - def check(temp_file): - im = Image.open(temp_file) - self.assertEqual(im.n_frames, 2) +@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") +def test_write_animation_RGB(tmp_path): + """ + Write an animated WebP from RGB frames, and ensure the frames + are visually similar to the originals. + """ + + def check(temp_file): + with Image.open(temp_file) as im: + assert im.n_frames == 2 # Compare first frame to original im.load() - self.assert_image_equal(im, frame1.convert("RGBA")) + assert_image_equal(im, frame1.convert("RGBA")) # Compare second frame to original im.seek(1) im.load() - self.assert_image_equal(im, frame2.convert("RGBA")) + assert_image_equal(im, frame2.convert("RGBA")) - frame1 = Image.open("Tests/images/anim_frame1.webp") - frame2 = Image.open("Tests/images/anim_frame2.webp") + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + temp_file1 = str(tmp_path / "temp.webp") + frame1.copy().save( + temp_file1, save_all=True, append_images=[frame2], lossless=True + ) + check(temp_file1) - temp_file1 = self.tempfile("temp.webp") - frame1.copy().save( - temp_file1, save_all=True, append_images=[frame2], lossless=True - ) - check(temp_file1) + # Tests appending using a generator + def imGenerator(ims): + yield from ims - # Tests appending using a generator - def imGenerator(ims): - yield from ims + temp_file2 = str(tmp_path / "temp_generator.webp") + frame1.copy().save( + temp_file2, + save_all=True, + append_images=imGenerator([frame2]), + lossless=True, + ) + check(temp_file2) - temp_file2 = self.tempfile("temp_generator.webp") - frame1.copy().save( - temp_file2, - save_all=True, - append_images=imGenerator([frame2]), - lossless=True, - ) - check(temp_file2) - def test_timestamp_and_duration(self): - """ - Try passing a list of durations, and make sure the encoded - timestamps and durations are correct. - """ +def test_timestamp_and_duration(tmp_path): + """ + Try passing a list of durations, and make sure the encoded + timestamps and durations are correct. + """ - durations = [0, 10, 20, 30, 40] - temp_file = self.tempfile("temp.webp") - frame1 = Image.open("Tests/images/anim_frame1.webp") - frame2 = Image.open("Tests/images/anim_frame2.webp") - frame1.save( - temp_file, - save_all=True, - append_images=[frame2, frame1, frame2, frame1], - duration=durations, - ) + durations = [0, 10, 20, 30, 40] + temp_file = str(tmp_path / "temp.webp") + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2, frame1], + duration=durations, + ) - im = Image.open(temp_file) - self.assertEqual(im.n_frames, 5) - self.assertTrue(im.is_animated) + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated # Check that timestamps and durations match original values specified ts = 0 for frame in range(im.n_frames): im.seek(frame) im.load() - self.assertEqual(im.info["duration"], durations[frame]) - self.assertEqual(im.info["timestamp"], ts) + assert im.info["duration"] == durations[frame] + assert im.info["timestamp"] == ts ts += durations[frame] - def test_seeking(self): - """ - Create an animated WebP file, and then try seeking through - frames in reverse-order, verifying the timestamps and durations - are correct. - """ - dur = 33 - temp_file = self.tempfile("temp.webp") - frame1 = Image.open("Tests/images/anim_frame1.webp") - frame2 = Image.open("Tests/images/anim_frame2.webp") - frame1.save( - temp_file, - save_all=True, - append_images=[frame2, frame1, frame2, frame1], - duration=dur, - ) +def test_seeking(tmp_path): + """ + Create an animated WebP file, and then try seeking through frames in reverse-order, + verifying the timestamps and durations are correct. + """ - im = Image.open(temp_file) - self.assertEqual(im.n_frames, 5) - self.assertTrue(im.is_animated) + dur = 33 + temp_file = str(tmp_path / "temp.webp") + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2, frame1], + duration=dur, + ) + + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated # Traverse frames in reverse, checking timestamps and durations ts = dur * (im.n_frames - 1) for frame in reversed(range(im.n_frames)): im.seek(frame) im.load() - self.assertEqual(im.info["duration"], dur) - self.assertEqual(im.info["timestamp"], ts) + assert im.info["duration"] == dur + assert im.info["timestamp"] == ts ts -= dur + + +def test_seek_errors(): + with Image.open("Tests/images/iss634.webp") as im: + with pytest.raises(EOFError): + im.seek(-1) + + with pytest.raises(EOFError): + im.seek(42) diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 2eff41529..2da443628 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,38 +1,28 @@ +import pytest + from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper -try: - from PIL import _webp - - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False +_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") +RGB_MODE = "RGB" -class TestFileWebpLossless(PillowTestCase): - def setUp(self): - if not HAVE_WEBP: - self.skipTest("WebP support not installed") - return +def test_write_lossless_rgb(tmp_path): + if _webp.WebPDecoderVersion() < 0x0200: + pytest.skip("lossless not included") - if _webp.WebPDecoderVersion() < 0x0200: - self.skipTest("lossless not included") + temp_file = str(tmp_path / "temp.webp") - self.rgb_mode = "RGB" + hopper(RGB_MODE).save(temp_file, lossless=True) - def test_write_lossless_rgb(self): - temp_file = self.tempfile("temp.webp") - - hopper(self.rgb_mode).save(temp_file, lossless=True) - - image = Image.open(temp_file) + with Image.open(temp_file) as image: image.load() - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") + assert image.mode == RGB_MODE + assert image.size == (128, 128) + assert image.format == "WEBP" image.load() image.getdata() - self.assert_image_equal(image, hopper(self.rgb_mode)) + assert_image_equal(image, hopper(RGB_MODE)) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index e3acf745d..a2a05f96b 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -2,134 +2,127 @@ from io import BytesIO from PIL import Image -from .helper import PillowTestCase +from .helper import skip_unless_feature -try: - from PIL import _webp - - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False +pytestmark = [ + skip_unless_feature("webp"), + skip_unless_feature("webp_mux"), +] -class TestFileWebpMetadata(PillowTestCase): - def setUp(self): - if not HAVE_WEBP: - self.skipTest("WebP support not installed") - return +def test_read_exif_metadata(): - if not _webp.HAVE_WEBPMUX: - self.skipTest("WebPMux support not installed") + file_path = "Tests/images/flower.webp" + with Image.open(file_path) as image: - def test_read_exif_metadata(self): + assert image.format == "WEBP" + exif_data = image.info.get("exif", None) + assert exif_data - file_path = "Tests/images/flower.webp" - with Image.open(file_path) as image: + exif = image._getexif() - self.assertEqual(image.format, "WEBP") - exif_data = image.info.get("exif", None) - self.assertTrue(exif_data) + # Camera make + assert exif[271] == "Canon" - exif = image._getexif() + with Image.open("Tests/images/flower.jpg") as jpeg_image: + expected_exif = jpeg_image.info["exif"] - # camera make - self.assertEqual(exif[271], "Canon") + assert exif_data == expected_exif - with Image.open("Tests/images/flower.jpg") as jpeg_image: - expected_exif = jpeg_image.info["exif"] - self.assertEqual(exif_data, expected_exif) +def test_read_exif_metadata_without_prefix(): + with Image.open("Tests/images/flower2.webp") as im: + # Assert prefix is not present + assert im.info["exif"][:6] != b"Exif\x00\x00" - def test_write_exif_metadata(self): - file_path = "Tests/images/flower.jpg" - image = Image.open(file_path) + exif = im.getexif() + assert exif[305] == "Adobe Photoshop CS6 (Macintosh)" + + +def test_write_exif_metadata(): + file_path = "Tests/images/flower.jpg" + test_buffer = BytesIO() + with Image.open(file_path) as image: expected_exif = image.info["exif"] - test_buffer = BytesIO() - image.save(test_buffer, "webp", exif=expected_exif) - test_buffer.seek(0) - webp_image = Image.open(test_buffer) - + test_buffer.seek(0) + with Image.open(test_buffer) as webp_image: webp_exif = webp_image.info.get("exif", None) - self.assertTrue(webp_exif) - if webp_exif: - self.assertEqual(webp_exif, expected_exif, "WebP EXIF didn't match") + assert webp_exif + if webp_exif: + assert webp_exif == expected_exif, "WebP EXIF didn't match" - def test_read_icc_profile(self): - file_path = "Tests/images/flower2.webp" - with Image.open(file_path) as image: +def test_read_icc_profile(): - self.assertEqual(image.format, "WEBP") - self.assertTrue(image.info.get("icc_profile", None)) + file_path = "Tests/images/flower2.webp" + with Image.open(file_path) as image: - icc = image.info["icc_profile"] + assert image.format == "WEBP" + assert image.info.get("icc_profile", None) - with Image.open("Tests/images/flower2.jpg") as jpeg_image: - expected_icc = jpeg_image.info["icc_profile"] + icc = image.info["icc_profile"] - self.assertEqual(icc, expected_icc) + with Image.open("Tests/images/flower2.jpg") as jpeg_image: + expected_icc = jpeg_image.info["icc_profile"] - def test_write_icc_metadata(self): - file_path = "Tests/images/flower2.jpg" - image = Image.open(file_path) + assert icc == expected_icc + + +def test_write_icc_metadata(): + file_path = "Tests/images/flower2.jpg" + test_buffer = BytesIO() + with Image.open(file_path) as image: expected_icc_profile = image.info["icc_profile"] - test_buffer = BytesIO() - image.save(test_buffer, "webp", icc_profile=expected_icc_profile) - test_buffer.seek(0) - webp_image = Image.open(test_buffer) - + test_buffer.seek(0) + with Image.open(test_buffer) as webp_image: webp_icc_profile = webp_image.info.get("icc_profile", None) - self.assertTrue(webp_icc_profile) - if webp_icc_profile: - self.assertEqual( - webp_icc_profile, expected_icc_profile, "Webp ICC didn't match" - ) + assert webp_icc_profile + if webp_icc_profile: + assert webp_icc_profile == expected_icc_profile, "Webp ICC didn't match" - def test_read_no_exif(self): - file_path = "Tests/images/flower.jpg" - image = Image.open(file_path) - self.assertIn("exif", image.info) - test_buffer = BytesIO() +def test_read_no_exif(): + file_path = "Tests/images/flower.jpg" + test_buffer = BytesIO() + with Image.open(file_path) as image: + assert "exif" in image.info image.save(test_buffer, "webp") - test_buffer.seek(0) - webp_image = Image.open(test_buffer) + test_buffer.seek(0) + with Image.open(test_buffer) as webp_image: + assert not webp_image._getexif() - self.assertFalse(webp_image._getexif()) - def test_write_animated_metadata(self): - if not _webp.HAVE_WEBPANIM: - self.skipTest("WebP animation support not available") +@skip_unless_feature("webp_anim") +def test_write_animated_metadata(tmp_path): + iccp_data = b"" + exif_data = b"" + xmp_data = b"" - iccp_data = b"" - exif_data = b"" - xmp_data = b"" + temp_file = str(tmp_path / "temp.webp") + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2], + icc_profile=iccp_data, + exif=exif_data, + xmp=xmp_data, + ) - temp_file = self.tempfile("temp.webp") - frame1 = Image.open("Tests/images/anim_frame1.webp") - frame2 = Image.open("Tests/images/anim_frame2.webp") - frame1.save( - temp_file, - save_all=True, - append_images=[frame2, frame1, frame2], - icc_profile=iccp_data, - exif=exif_data, - xmp=xmp_data, - ) - - with Image.open(temp_file) as image: - self.assertIn("icc_profile", image.info) - self.assertIn("exif", image.info) - self.assertIn("xmp", image.info) - self.assertEqual(iccp_data, image.info.get("icc_profile", None)) - self.assertEqual(exif_data, image.info.get("exif", None)) - self.assertEqual(xmp_data, image.info.get("xmp", None)) + with Image.open(temp_file) as image: + assert "icc_profile" in image.info + assert "exif" in image.info + assert "xmp" in image.info + assert iccp_data == image.info.get("icc_profile", None) + assert exif_data == image.info.get("exif", None) + assert xmp_data == image.info.get("xmp", None) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index c07bbf60a..d18225680 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,61 +1,79 @@ +import pytest + from PIL import Image, WmfImagePlugin -from .helper import PillowTestCase, hopper +from .helper import assert_image_similar, hopper -class TestFileWmf(PillowTestCase): - def test_load_raw(self): +def test_load_raw(): - # Test basic EMF open and rendering - with Image.open("Tests/images/drawing.emf") as im: - if hasattr(Image.core, "drawwmf"): - # Currently, support for WMF/EMF is Windows-only - im.load() - # Compare to reference rendering - imref = Image.open("Tests/images/drawing_emf_ref.png") + # Test basic EMF open and rendering + with Image.open("Tests/images/drawing.emf") as im: + if hasattr(Image.core, "drawwmf"): + # Currently, support for WMF/EMF is Windows-only + im.load() + # Compare to reference rendering + with Image.open("Tests/images/drawing_emf_ref.png") as imref: imref.load() - self.assert_image_similar(im, imref, 0) + assert_image_similar(im, imref, 0) - # Test basic WMF open and rendering - with Image.open("Tests/images/drawing.wmf") as im: - if hasattr(Image.core, "drawwmf"): - # Currently, support for WMF/EMF is Windows-only - im.load() - # Compare to reference rendering - imref = Image.open("Tests/images/drawing_wmf_ref.png") + # Test basic WMF open and rendering + with Image.open("Tests/images/drawing.wmf") as im: + if hasattr(Image.core, "drawwmf"): + # Currently, support for WMF/EMF is Windows-only + im.load() + # Compare to reference rendering + with Image.open("Tests/images/drawing_wmf_ref.png") as imref: imref.load() - self.assert_image_similar(im, imref, 2.0) + assert_image_similar(im, imref, 2.0) - def test_register_handler(self): - class TestHandler: - methodCalled = False - def save(self, im, fp, filename): - self.methodCalled = True +def test_register_handler(tmp_path): + class TestHandler: + methodCalled = False - handler = TestHandler() - WmfImagePlugin.register_handler(handler) + def save(self, im, fp, filename): + self.methodCalled = True - im = hopper() - tmpfile = self.tempfile("temp.wmf") - im.save(tmpfile) - self.assertTrue(handler.methodCalled) + handler = TestHandler() + original_handler = WmfImagePlugin._handler + WmfImagePlugin.register_handler(handler) - # Restore the state before this test - WmfImagePlugin.register_handler(None) + im = hopper() + tmpfile = str(tmp_path / "temp.wmf") + im.save(tmpfile) + assert handler.methodCalled - def test_load_dpi_rounding(self): - # Round up - with Image.open("Tests/images/drawing.emf") as im: - self.assertEqual(im.info["dpi"], 1424) + # Restore the state before this test + WmfImagePlugin.register_handler(original_handler) - # Round down - with Image.open("Tests/images/drawing_roundDown.emf") as im: - self.assertEqual(im.info["dpi"], 1426) - def test_save(self): - im = hopper() +def test_load_dpi_rounding(): + # Round up + with Image.open("Tests/images/drawing.emf") as im: + assert im.info["dpi"] == 1424 - for ext in [".wmf", ".emf"]: - tmpfile = self.tempfile("temp" + ext) - self.assertRaises(IOError, im.save, tmpfile) + # Round down + with Image.open("Tests/images/drawing_roundDown.emf") as im: + assert im.info["dpi"] == 1426 + + +def test_load_set_dpi(): + with Image.open("Tests/images/drawing.wmf") as im: + assert im.size == (82, 82) + + if hasattr(Image.core, "drawwmf"): + im.load(144) + assert im.size == (164, 164) + + with Image.open("Tests/images/drawing_wmf_ref_144.png") as expected: + assert_image_similar(im, expected, 2.1) + + +def test_save(tmp_path): + im = hopper() + + for ext in [".wmf", ".emf"]: + tmpfile = str(tmp_path / ("temp" + ext)) + with pytest.raises(OSError): + im.save(tmpfile) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 5a1eb54bc..487920a92 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,8 +1,10 @@ from io import BytesIO +import pytest + from PIL import Image -from .helper import PillowTestCase +from .helper import hopper PIL151 = b""" #define basic_width 32 @@ -28,34 +30,53 @@ static char basic_bits[] = { """ -class TestFileXbm(PillowTestCase): - def test_pil151(self): - im = Image.open(BytesIO(PIL151)) - +def test_pil151(): + with Image.open(BytesIO(PIL151)) as im: im.load() - self.assertEqual(im.mode, "1") - self.assertEqual(im.size, (32, 32)) + assert im.mode == "1" + assert im.size == (32, 32) - def test_open(self): - # Arrange - # Created with `convert hopper.png hopper.xbm` - filename = "Tests/images/hopper.xbm" - # Act - with Image.open(filename) as im: +def test_open(): + # Arrange + # Created with `convert hopper.png hopper.xbm` + filename = "Tests/images/hopper.xbm" - # Assert - self.assertEqual(im.mode, "1") - self.assertEqual(im.size, (128, 128)) + # Act + with Image.open(filename) as im: - def test_open_filename_with_underscore(self): - # Arrange - # Created with `convert hopper.png hopper_underscore.xbm` - filename = "Tests/images/hopper_underscore.xbm" + # Assert + assert im.mode == "1" + assert im.size == (128, 128) - # Act - with Image.open(filename) as im: - # Assert - self.assertEqual(im.mode, "1") - self.assertEqual(im.size, (128, 128)) +def test_open_filename_with_underscore(): + # Arrange + # Created with `convert hopper.png hopper_underscore.xbm` + filename = "Tests/images/hopper_underscore.xbm" + + # Act + with Image.open(filename) as im: + + # Assert + assert im.mode == "1" + assert im.size == (128, 128) + + +def test_save_wrong_mode(tmp_path): + im = hopper() + out = str(tmp_path / "temp.xbm") + + with pytest.raises(OSError): + im.save(out) + + +def test_hotspot(tmp_path): + im = hopper("1") + out = str(tmp_path / "temp.xbm") + + hotspot = (0, 7) + im.save(out, hotspot=hotspot) + + with Image.open(out) as reloaded: + assert reloaded.info["hotspot"] == hotspot diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index a4ea8c491..8595b07eb 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,33 +1,37 @@ +import pytest + from PIL import Image, XpmImagePlugin -from .helper import PillowTestCase, hopper +from .helper import assert_image_similar, hopper TEST_FILE = "Tests/images/hopper.xpm" -class TestFileXpm(PillowTestCase): - def test_sanity(self): - im = Image.open(TEST_FILE) +def test_sanity(): + with Image.open(TEST_FILE) as im: im.load() - self.assertEqual(im.mode, "P") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "XPM") + assert im.mode == "P" + assert im.size == (128, 128) + assert im.format == "XPM" # large error due to quantization->44 colors. - self.assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) + assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, XpmImagePlugin.XpmImageFile, invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - def test_load_read(self): - # Arrange - with Image.open(TEST_FILE) as im: - dummy_bytes = 1 + with pytest.raises(SyntaxError): + XpmImagePlugin.XpmImageFile(invalid_file) - # Act - data = im.load_read(dummy_bytes) - # Assert - self.assertEqual(len(data), 16384) +def test_load_read(): + # Arrange + with Image.open(TEST_FILE) as im: + dummy_bytes = 1 + + # Act + data = im.load_read(dummy_bytes) + + # Assert + assert len(data) == 16384 diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index f8b6d3531..ae53d2b63 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -1,35 +1,38 @@ +import pytest + from PIL import Image, XVThumbImagePlugin -from .helper import PillowTestCase, hopper +from .helper import assert_image_similar, hopper TEST_FILE = "Tests/images/hopper.p7" -class TestFileXVThumb(PillowTestCase): - def test_open(self): - # Act - im = Image.open(TEST_FILE) +def test_open(): + # Act + with Image.open(TEST_FILE) as im: # Assert - self.assertEqual(im.format, "XVThumb") + assert im.format == "XVThumb" # Create a Hopper image with a similar XV palette im_hopper = hopper().quantize(palette=im) - self.assert_image_similar(im, im_hopper, 9) + assert_image_similar(im, im_hopper, 9) - def test_unexpected_eof(self): - # Test unexpected EOF reading XV thumbnail file - # Arrange - bad_file = "Tests/images/hopper_bad.p7" - # Act / Assert - self.assertRaises(SyntaxError, XVThumbImagePlugin.XVThumbImageFile, bad_file) +def test_unexpected_eof(): + # Test unexpected EOF reading XV thumbnail file + # Arrange + bad_file = "Tests/images/hopper_bad.p7" - def test_invalid_file(self): - # Arrange - invalid_file = "Tests/images/flower.jpg" + # Act / Assert + with pytest.raises(SyntaxError): + XVThumbImagePlugin.XVThumbImageFile(bad_file) - # Act / Assert - self.assertRaises( - SyntaxError, XVThumbImagePlugin.XVThumbImageFile, invalid_file - ) + +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert + with pytest.raises(SyntaxError): + XVThumbImagePlugin.XVThumbImageFile(invalid_file) diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 9b3a342d0..1e7caee32 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,19 +1,19 @@ -from PIL import BdfFontFile, FontFile +import pytest -from .helper import PillowTestCase +from PIL import BdfFontFile, FontFile filename = "Tests/images/courB08.bdf" -class TestFontBdf(PillowTestCase): - def test_sanity(self): +def test_sanity(): + with open(filename, "rb") as test_file: + font = BdfFontFile.BdfFontFile(test_file) - with open(filename, "rb") as test_file: - font = BdfFontFile.BdfFontFile(test_file) + assert isinstance(font, FontFile.FontFile) + assert len([_f for _f in font.glyph if _f]) == 190 - self.assertIsInstance(font, FontFile.FontFile) - self.assertEqual(len([_f for _f in font.glyph if _f]), 190) - def test_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, BdfFontFile.BdfFontFile, fp) +def test_invalid_file(): + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + BdfFontFile.BdfFontFile(fp) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 393ddb0fb..015210b4d 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,11 +1,8 @@ -import unittest +from PIL import Image, ImageDraw, ImageFont -from PIL import Image, ImageDraw, ImageFont, features - -from .helper import PillowLeakTestCase, is_win32 +from .helper import PillowLeakTestCase, skip_unless_feature -@unittest.skipIf(is_win32(), "requires Unix or macOS") class TestTTypeFontLeak(PillowLeakTestCase): # fails at iteration 3 in master iterations = 10 @@ -20,7 +17,7 @@ class TestTTypeFontLeak(PillowLeakTestCase): ) ) - @unittest.skipIf(not features.check("freetype2"), "Test requires freetype2") + @skip_unless_feature("freetype2") def test_leak(self): ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) self._test_font(ttype) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index e37f43207..4db73e56e 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,77 +1,91 @@ +import os + +import pytest + from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile -from .helper import PillowTestCase - -codecs = dir(Image.core) +from .helper import assert_image_equal, assert_image_similar, skip_unless_feature fontname = "Tests/fonts/10x20-ISO8859-1.pcf" message = "hello, world" -class TestFontPcf(PillowTestCase): - def setUp(self): - if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - self.skipTest("zlib support not available") +pytestmark = skip_unless_feature("zlib") - def save_font(self): - with open(fontname, "rb") as test_file: - font = PcfFontFile.PcfFontFile(test_file) - self.assertIsInstance(font, FontFile.FontFile) - # check the number of characters in the font - self.assertEqual(len([_f for _f in font.glyph if _f]), 223) - tempname = self.tempfile("temp.pil") - self.addCleanup(self.delete_tempfile, tempname[:-4] + ".pbm") - font.save(tempname) +def save_font(request, tmp_path): + with open(fontname, "rb") as test_file: + font = PcfFontFile.PcfFontFile(test_file) + assert isinstance(font, FontFile.FontFile) + # check the number of characters in the font + assert len([_f for _f in font.glyph if _f]) == 223 - with Image.open(tempname.replace(".pil", ".pbm")) as loaded: - with Image.open("Tests/fonts/10x20.pbm") as target: - self.assert_image_equal(loaded, target) + tempname = str(tmp_path / "temp.pil") - with open(tempname, "rb") as f_loaded: - with open("Tests/fonts/10x20.pil", "rb") as f_target: - self.assertEqual(f_loaded.read(), f_target.read()) - return tempname + def delete_tempfile(): + try: + os.remove(tempname[:-4] + ".pbm") + except OSError: + pass # report? - def test_sanity(self): - self.save_font() + request.addfinalizer(delete_tempfile) + font.save(tempname) - def test_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, PcfFontFile.PcfFontFile, fp) + with Image.open(tempname.replace(".pil", ".pbm")) as loaded: + with Image.open("Tests/fonts/10x20.pbm") as target: + assert_image_equal(loaded, target) - def test_draw(self): - tempname = self.save_font() - font = ImageFont.load(tempname) - im = Image.new("L", (130, 30), "white") - draw = ImageDraw.Draw(im) - draw.text((0, 0), message, "black", font=font) - with Image.open("Tests/images/test_draw_pbm_target.png") as target: - self.assert_image_similar(im, target, 0) + with open(tempname, "rb") as f_loaded: + with open("Tests/fonts/10x20.pil", "rb") as f_target: + assert f_loaded.read() == f_target.read() + return tempname - def test_textsize(self): - tempname = self.save_font() - font = ImageFont.load(tempname) - for i in range(255): - (dx, dy) = font.getsize(chr(i)) - self.assertEqual(dy, 20) - self.assertIn(dx, (0, 10)) - for l in range(len(message)): - msg = message[: l + 1] - self.assertEqual(font.getsize(msg), (len(msg) * 10, 20)) - def _test_high_characters(self, message): - tempname = self.save_font() - font = ImageFont.load(tempname) - im = Image.new("L", (750, 30), "white") - draw = ImageDraw.Draw(im) - draw.text((0, 0), message, "black", font=font) - with Image.open("Tests/images/high_ascii_chars.png") as target: - self.assert_image_similar(im, target, 0) +def test_sanity(request, tmp_path): + save_font(request, tmp_path) - def test_high_characters(self): - message = "".join(chr(i + 1) for i in range(140, 232)) - self._test_high_characters(message) - # accept bytes instances. - self._test_high_characters(message.encode("latin1")) + +def test_invalid_file(): + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + PcfFontFile.PcfFontFile(fp) + + +def test_draw(request, tmp_path): + tempname = save_font(request, tmp_path) + font = ImageFont.load(tempname) + im = Image.new("L", (130, 30), "white") + draw = ImageDraw.Draw(im) + draw.text((0, 0), message, "black", font=font) + with Image.open("Tests/images/test_draw_pbm_target.png") as target: + assert_image_similar(im, target, 0) + + +def test_textsize(request, tmp_path): + tempname = save_font(request, tmp_path) + font = ImageFont.load(tempname) + for i in range(255): + (dx, dy) = font.getsize(chr(i)) + assert dy == 20 + assert dx in (0, 10) + for i in range(len(message)): + msg = message[: i + 1] + assert font.getsize(msg) == (len(msg) * 10, 20) + + +def _test_high_characters(request, tmp_path, message): + tempname = save_font(request, tmp_path) + font = ImageFont.load(tempname) + im = Image.new("L", (750, 30), "white") + draw = ImageDraw.Draw(im) + draw.text((0, 0), message, "black", font=font) + with Image.open("Tests/images/high_ascii_chars.png") as target: + assert_image_similar(im, target, 0) + + +def test_high_characters(request, tmp_path): + message = "".join(chr(i + 1) for i in range(140, 232)) + _test_high_characters(request, tmp_path, message) + # accept bytes instances. + _test_high_characters(request, tmp_path, message.encode("latin1")) diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py new file mode 100644 index 000000000..d7d1bf200 --- /dev/null +++ b/Tests/test_font_pcf_charsets.py @@ -0,0 +1,120 @@ +import os + +from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile + +from .helper import assert_image_equal, assert_image_similar, skip_unless_feature + +fontname = "Tests/fonts/ter-x20b.pcf" + +charsets = { + "iso8859-1": { + "glyph_count": 223, + "message": "hello, world", + "image1": "Tests/images/test_draw_pbm_ter_en_target.png", + }, + "iso8859-2": { + "glyph_count": 223, + "message": "witaj świecie", + "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", + }, + "cp1250": { + "glyph_count": 250, + "message": "witaj świecie", + "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", + }, +} + + +pytestmark = skip_unless_feature("zlib") + + +def save_font(request, tmp_path, encoding): + with open(fontname, "rb") as test_file: + font = PcfFontFile.PcfFontFile(test_file, encoding) + assert isinstance(font, FontFile.FontFile) + # check the number of characters in the font + assert len([_f for _f in font.glyph if _f]) == charsets[encoding]["glyph_count"] + + tempname = str(tmp_path / "temp.pil") + + def delete_tempfile(): + try: + os.remove(tempname[:-4] + ".pbm") + except OSError: + pass # report? + + request.addfinalizer(delete_tempfile) + font.save(tempname) + + with Image.open(tempname.replace(".pil", ".pbm")) as loaded: + with Image.open(f"Tests/fonts/ter-x20b-{encoding}.pbm") as target: + assert_image_equal(loaded, target) + + with open(tempname, "rb") as f_loaded: + with open(f"Tests/fonts/ter-x20b-{encoding}.pil", "rb") as f_target: + assert f_loaded.read() == f_target.read() + return tempname + + +def _test_sanity(request, tmp_path, encoding): + save_font(request, tmp_path, encoding) + + +def test_sanity_iso8859_1(request, tmp_path): + _test_sanity(request, tmp_path, "iso8859-1") + + +def test_sanity_iso8859_2(request, tmp_path): + _test_sanity(request, tmp_path, "iso8859-2") + + +def test_sanity_cp1250(request, tmp_path): + _test_sanity(request, tmp_path, "cp1250") + + +def _test_draw(request, tmp_path, encoding): + tempname = save_font(request, tmp_path, encoding) + font = ImageFont.load(tempname) + im = Image.new("L", (150, 30), "white") + draw = ImageDraw.Draw(im) + message = charsets[encoding]["message"].encode(encoding) + draw.text((0, 0), message, "black", font=font) + with Image.open(charsets[encoding]["image1"]) as target: + assert_image_similar(im, target, 0) + + +def test_draw_iso8859_1(request, tmp_path): + _test_draw(request, tmp_path, "iso8859-1") + + +def test_draw_iso8859_2(request, tmp_path): + _test_draw(request, tmp_path, "iso8859-2") + + +def test_draw_cp1250(request, tmp_path): + _test_draw(request, tmp_path, "cp1250") + + +def _test_textsize(request, tmp_path, encoding): + tempname = save_font(request, tmp_path, encoding) + font = ImageFont.load(tempname) + for i in range(255): + (dx, dy) = font.getsize(bytearray([i])) + assert dy == 20 + assert dx in (0, 10) + message = charsets[encoding]["message"].encode(encoding) + for i in range(len(message)): + msg = message[: i + 1] + assert font.getsize(msg) == (len(msg) * 10, 20) + + +def test_textsize_iso8859_1(request, tmp_path): + _test_textsize(request, tmp_path, "iso8859-1") + + +def test_textsize_iso8859_2(request, tmp_path): + _test_textsize(request, tmp_path, "iso8859-2") + + +def test_textsize_cp1250(request, tmp_path): + _test_textsize(request, tmp_path, "cp1250") diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index e80e16ffe..3b9c8b071 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -3,142 +3,149 @@ import itertools from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image_similar, hopper -class TestFormatHSV(PillowTestCase): - def int_to_float(self, i): - return float(i) / 255.0 +def int_to_float(i): + return i / 255 - def str_to_float(self, i): - return float(ord(i)) / 255.0 - def tuple_to_ints(self, tp): - x, y, z = tp - return int(x * 255.0), int(y * 255.0), int(z * 255.0) +def str_to_float(i): + return ord(i) / 255 - def test_sanity(self): - Image.new("HSV", (100, 100)) - def wedge(self): - w = Image._wedge() - w90 = w.rotate(90) +def tuple_to_ints(tp): + x, y, z = tp + return int(x * 255.0), int(y * 255.0), int(z * 255.0) - (px, h) = w.size - r = Image.new("L", (px * 3, h)) - g = r.copy() - b = r.copy() +def test_sanity(): + Image.new("HSV", (100, 100)) - r.paste(w, (0, 0)) - r.paste(w90, (px, 0)) - g.paste(w90, (0, 0)) - g.paste(w, (2 * px, 0)) +def wedge(): + w = Image._wedge() + w90 = w.rotate(90) - b.paste(w, (px, 0)) - b.paste(w90, (2 * px, 0)) + (px, h) = w.size - img = Image.merge("RGB", (r, g, b)) + r = Image.new("L", (px * 3, h)) + g = r.copy() + b = r.copy() - return img + r.paste(w, (0, 0)) + r.paste(w90, (px, 0)) - def to_xxx_colorsys(self, im, func, mode): - # convert the hard way using the library colorsys routines. + g.paste(w90, (0, 0)) + g.paste(w, (2 * px, 0)) - (r, g, b) = im.split() + b.paste(w, (px, 0)) + b.paste(w90, (2 * px, 0)) - conv_func = self.int_to_float + img = Image.merge("RGB", (r, g, b)) - converted = [ - self.tuple_to_ints(func(conv_func(_r), conv_func(_g), conv_func(_b))) - for (_r, _g, _b) in itertools.zip_longest( - r.tobytes(), g.tobytes(), b.tobytes() - ) - ] + return img - new_bytes = b"".join( - bytes(chr(h) + chr(s) + chr(v), "latin-1") for (h, s, v) in converted - ) - hsv = Image.frombytes(mode, r.size, new_bytes) +def to_xxx_colorsys(im, func, mode): + # convert the hard way using the library colorsys routines. - return hsv + (r, g, b) = im.split() - def to_hsv_colorsys(self, im): - return self.to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") + conv_func = int_to_float - def to_rgb_colorsys(self, im): - return self.to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") + converted = [ + tuple_to_ints(func(conv_func(_r), conv_func(_g), conv_func(_b))) + for (_r, _g, _b) in itertools.zip_longest(r.tobytes(), g.tobytes(), b.tobytes()) + ] - def test_wedge(self): - src = self.wedge().resize((3 * 32, 32), Image.BILINEAR) - im = src.convert("HSV") - comparable = self.to_hsv_colorsys(src) + new_bytes = b"".join( + bytes(chr(h) + chr(s) + chr(v), "latin-1") for (h, s, v) in converted + ) - self.assert_image_similar( - im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" - ) - self.assert_image_similar( - im.getchannel(1), - comparable.getchannel(1), - 1, - "Saturation conversion is wrong", - ) - self.assert_image_similar( - im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" - ) + hsv = Image.frombytes(mode, r.size, new_bytes) - comparable = src - im = im.convert("RGB") + return hsv - self.assert_image_similar( - im.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong" - ) - self.assert_image_similar( - im.getchannel(1), comparable.getchannel(1), 3, "G conversion is wrong" - ) - self.assert_image_similar( - im.getchannel(2), comparable.getchannel(2), 3, "B conversion is wrong" - ) - def test_convert(self): - im = hopper("RGB").convert("HSV") - comparable = self.to_hsv_colorsys(hopper("RGB")) +def to_hsv_colorsys(im): + return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") - self.assert_image_similar( - im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" - ) - self.assert_image_similar( - im.getchannel(1), - comparable.getchannel(1), - 1, - "Saturation conversion is wrong", - ) - self.assert_image_similar( - im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" - ) - def test_hsv_to_rgb(self): - comparable = self.to_hsv_colorsys(hopper("RGB")) - converted = comparable.convert("RGB") - comparable = self.to_rgb_colorsys(comparable) +def to_rgb_colorsys(im): + return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") - self.assert_image_similar( - converted.getchannel(0), - comparable.getchannel(0), - 3, - "R conversion is wrong", - ) - self.assert_image_similar( - converted.getchannel(1), - comparable.getchannel(1), - 3, - "G conversion is wrong", - ) - self.assert_image_similar( - converted.getchannel(2), - comparable.getchannel(2), - 3, - "B conversion is wrong", - ) + +def test_wedge(): + src = wedge().resize((3 * 32, 32), Image.BILINEAR) + im = src.convert("HSV") + comparable = to_hsv_colorsys(src) + + assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" + ) + assert_image_similar( + im.getchannel(1), + comparable.getchannel(1), + 1, + "Saturation conversion is wrong", + ) + assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" + ) + + comparable = src + im = im.convert("RGB") + + assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong" + ) + assert_image_similar( + im.getchannel(1), comparable.getchannel(1), 3, "G conversion is wrong" + ) + assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 3, "B conversion is wrong" + ) + + +def test_convert(): + im = hopper("RGB").convert("HSV") + comparable = to_hsv_colorsys(hopper("RGB")) + + assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" + ) + assert_image_similar( + im.getchannel(1), + comparable.getchannel(1), + 1, + "Saturation conversion is wrong", + ) + assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" + ) + + +def test_hsv_to_rgb(): + comparable = to_hsv_colorsys(hopper("RGB")) + converted = comparable.convert("RGB") + comparable = to_rgb_colorsys(comparable) + + assert_image_similar( + converted.getchannel(0), + comparable.getchannel(0), + 3, + "R conversion is wrong", + ) + assert_image_similar( + converted.getchannel(1), + comparable.getchannel(1), + 3, + "G conversion is wrong", + ) + assert_image_similar( + converted.getchannel(2), + comparable.getchannel(2), + 3, + "B conversion is wrong", + ) diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index a98c20579..41c8efdf3 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -1,41 +1,38 @@ from PIL import Image -from .helper import PillowTestCase - - -class TestFormatLab(PillowTestCase): - def test_white(self): - i = Image.open("Tests/images/lab.tif") +def test_white(): + with Image.open("Tests/images/lab.tif") as i: i.load() - self.assertEqual(i.mode, "LAB") + assert i.mode == "LAB" - self.assertEqual(i.getbands(), ("L", "A", "B")) + assert i.getbands() == ("L", "A", "B") k = i.getpixel((0, 0)) - self.assertEqual(k, (255, 128, 128)) L = i.getdata(0) a = i.getdata(1) b = i.getdata(2) - self.assertEqual(list(L), [255] * 100) - self.assertEqual(list(a), [128] * 100) - self.assertEqual(list(b), [128] * 100) + assert k == (255, 128, 128) - def test_green(self): - # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS - # == RGB: 0, 152, 117 - i = Image.open("Tests/images/lab-green.tif") + assert list(L) == [255] * 100 + assert list(a) == [128] * 100 + assert list(b) == [128] * 100 + +def test_green(): + # 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: k = i.getpixel((0, 0)) - self.assertEqual(k, (128, 28, 128)) + assert k == (128, 28, 128) - def test_red(self): - # l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS - # == RGB: 255, 0, 124 - i = Image.open("Tests/images/lab-red.tif") +def test_red(): + # 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: k = i.getpixel((0, 0)) - self.assertEqual(k, (128, 228, 128)) + assert k == (128, 228, 128) diff --git a/Tests/test_image.py b/Tests/test_image.py index 15cf435b0..84c098cc8 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,14 +1,24 @@ +import io import os import shutil import tempfile -import unittest -from PIL import Image, UnidentifiedImageError +import pytest -from .helper import PillowTestCase, hopper, is_win32 +import PIL +from PIL import Image, ImageDraw, ImagePalette, ImageShow, UnidentifiedImageError + +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_not_all_same, + hopper, + is_win32, + skip_unless_feature, +) -class TestImage(PillowTestCase): +class TestImage: def test_image_modes_success(self): for mode in [ "1", @@ -44,71 +54,100 @@ class TestImage(PillowTestCase): "BGR;24", "BGR;32", ]: - with self.assertRaises(ValueError) as e: + with pytest.raises(ValueError) as e: Image.new(mode, (1, 1)) - self.assertEqual(str(e.exception), "unrecognized image mode") + assert str(e.value) == "unrecognized image mode" def test_exception_inheritance(self): - self.assertTrue(issubclass(UnidentifiedImageError, IOError)) + assert issubclass(UnidentifiedImageError, OSError) def test_sanity(self): im = Image.new("L", (100, 100)) - self.assertEqual(repr(im)[:45], "= 7 + + with pytest.warns(DeprecationWarning): + assert test_module.PILLOW_VERSION < "9.9.0" + + with pytest.warns(DeprecationWarning): + assert test_module.PILLOW_VERSION <= "9.9.0" + + with pytest.warns(DeprecationWarning): + assert test_module.PILLOW_VERSION != "7.0.0" + + with pytest.warns(DeprecationWarning): + assert test_module.PILLOW_VERSION >= "7.0.0" + + with pytest.warns(DeprecationWarning): + assert test_module.PILLOW_VERSION > "7.0.0" def test_overrun(self): - for file in ["fli_overrun.bin", "sgi_overrun.bin", "pcx_overrun.bin"]: + """For overrun completeness, test as: + valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c + """ + for file in [ + "fli_overrun.bin", + "sgi_overrun.bin", + "sgi_overrun_expandrow.bin", + "sgi_overrun_expandrow2.bin", + "pcx_overrun.bin", + "pcx_overrun2.bin", + "01r_00.pcx", + ]: with Image.open(os.path.join("Tests/images", file)) as im: try: im.load() - self.assertFail() + assert False except OSError as e: - self.assertEqual(str(e), "buffer overrun when reading image file") + assert str(e) == "buffer overrun when reading image file" + + with Image.open("Tests/images/fli_overrun2.bin") as im: + try: + im.seek(1) + assert False + except OSError as e: + assert str(e) == "buffer overrun when reading image file" + + def test_show_deprecation(self, monkeypatch): + monkeypatch.setattr(Image, "_show", lambda *args, **kwargs: None) + + im = Image.new("RGB", (50, 50), "white") + + with pytest.warns(None) as raised: + im.show() + assert not raised + + with pytest.warns(DeprecationWarning): + im.show(command="mock") class MockEncoder: @@ -603,23 +825,17 @@ def mock_encode(*args): return encoder -class TestRegistry(PillowTestCase): +class TestRegistry: def test_encode_registry(self): Image.register_encoder("MOCK", mock_encode) - self.assertIn("MOCK", Image.ENCODERS) + assert "MOCK" in Image.ENCODERS enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",)) - self.assertIsInstance(enc, MockEncoder) - self.assertEqual(enc.args, ("RGB", "args", "extra")) + assert isinstance(enc, MockEncoder) + assert enc.args == ("RGB", "args", "extra") def test_encode_registry_fail(self): - self.assertRaises( - IOError, - Image._getencoder, - "RGB", - "DoesNotExist", - ("args",), - extra=("extra",), - ) + 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 57bac753d..e86dc8530 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -2,12 +2,14 @@ import ctypes import os import subprocess import sys -import unittest -from distutils import ccompiler, sysconfig +import sysconfig + +import pytest +from setuptools.command.build_ext import new_compiler from PIL import Image -from .helper import PillowTestCase, hopper, is_win32, on_ci +from .helper import assert_image_equal, hopper, is_win32, on_ci # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -15,23 +17,24 @@ if os.environ.get("PYTHONOPTIMIZE") == "2": cffi = None else: try: - from PIL import PyAccess import cffi + + from PIL import PyAccess except ImportError: cffi = None -class AccessTest(PillowTestCase): +class AccessTest: # initial value _init_cffi_access = Image.USE_CFFI_ACCESS _need_cffi_access = False @classmethod - def setUpClass(cls): + def setup_class(cls): Image.USE_CFFI_ACCESS = cls._need_cffi_access @classmethod - def tearDownClass(cls): + def teardown_class(cls): Image.USE_CFFI_ACCESS = cls._init_cffi_access @@ -45,7 +48,7 @@ class TestImagePutPixel(AccessTest): pos = x, y im2.putpixel(pos, im1.getpixel(pos)) - self.assert_image_equal(im1, im2) + assert_image_equal(im1, im2) im2 = Image.new(im1.mode, im1.size, 0) im2.readonly = 1 @@ -55,8 +58,8 @@ class TestImagePutPixel(AccessTest): pos = x, y im2.putpixel(pos, im1.getpixel(pos)) - self.assertFalse(im2.readonly) - self.assert_image_equal(im1, im2) + assert not im2.readonly + assert_image_equal(im1, im2) im2 = Image.new(im1.mode, im1.size, 0) @@ -67,22 +70,22 @@ class TestImagePutPixel(AccessTest): for x in range(im1.size[0]): pix2[x, y] = pix1[x, y] - self.assert_image_equal(im1, im2) + assert_image_equal(im1, im2) def test_sanity_negative_index(self): im1 = hopper() im2 = Image.new(im1.mode, im1.size, 0) width, height = im1.size - self.assertEqual(im1.getpixel((0, 0)), im1.getpixel((-width, -height))) - self.assertEqual(im1.getpixel((-1, -1)), im1.getpixel((width - 1, height - 1))) + assert im1.getpixel((0, 0)) == im1.getpixel((-width, -height)) + assert im1.getpixel((-1, -1)) == im1.getpixel((width - 1, height - 1)) for y in range(-1, -im1.size[1] - 1, -1): for x in range(-1, -im1.size[0] - 1, -1): pos = x, y im2.putpixel(pos, im1.getpixel(pos)) - self.assert_image_equal(im1, im2) + assert_image_equal(im1, im2) im2 = Image.new(im1.mode, im1.size, 0) im2.readonly = 1 @@ -92,8 +95,8 @@ class TestImagePutPixel(AccessTest): pos = x, y im2.putpixel(pos, im1.getpixel(pos)) - self.assertFalse(im2.readonly) - self.assert_image_equal(im1, im2) + assert not im2.readonly + assert_image_equal(im1, im2) im2 = Image.new(im1.mode, im1.size, 0) @@ -104,7 +107,7 @@ class TestImagePutPixel(AccessTest): for x in range(-1, -im1.size[0] - 1, -1): pix2[x, y] = pix1[x, y] - self.assert_image_equal(im1, im2) + assert_image_equal(im1, im2) class TestImageGetPixel(AccessTest): @@ -123,54 +126,44 @@ class TestImageGetPixel(AccessTest): # check putpixel im = Image.new(mode, (1, 1), None) im.putpixel((0, 0), c) - self.assertEqual( - im.getpixel((0, 0)), - c, - "put/getpixel roundtrip failed for mode {}, color {}".format(mode, c), - ) + assert ( + im.getpixel((0, 0)) == c + ), f"put/getpixel roundtrip failed for mode {mode}, color {c}" # check putpixel negative index im.putpixel((-1, -1), c) - self.assertEqual( - im.getpixel((-1, -1)), - c, - "put/getpixel roundtrip negative index failed" - " for mode %s, color %s" % (mode, c), - ) + assert ( + im.getpixel((-1, -1)) == c + ), f"put/getpixel roundtrip negative index failed for mode {mode}, color {c}" # Check 0 im = Image.new(mode, (0, 0), None) - with self.assertRaises(IndexError): + with pytest.raises(IndexError): im.putpixel((0, 0), c) - with self.assertRaises(IndexError): + with pytest.raises(IndexError): im.getpixel((0, 0)) # Check 0 negative index - with self.assertRaises(IndexError): + with pytest.raises(IndexError): im.putpixel((-1, -1), c) - with self.assertRaises(IndexError): + with pytest.raises(IndexError): im.getpixel((-1, -1)) # check initial color im = Image.new(mode, (1, 1), c) - self.assertEqual( - im.getpixel((0, 0)), - c, - "initial color failed for mode {}, color {} ".format(mode, c), - ) + assert ( + im.getpixel((0, 0)) == c + ), f"initial color failed for mode {mode}, color {c} " # check initial color negative index - self.assertEqual( - im.getpixel((-1, -1)), - c, - "initial color failed with negative index" - "for mode %s, color %s " % (mode, c), - ) + assert ( + im.getpixel((-1, -1)) == c + ), f"initial color failed with negative index for mode {mode}, color {c} " # Check 0 im = Image.new(mode, (0, 0), c) - with self.assertRaises(IndexError): + with pytest.raises(IndexError): im.getpixel((0, 0)) # Check 0 negative index - with self.assertRaises(IndexError): + with pytest.raises(IndexError): im.getpixel((-1, -1)) def test_basic(self): @@ -205,20 +198,20 @@ class TestImageGetPixel(AccessTest): for color in [(255, 0, 0), (255, 0, 0, 255)]: im = Image.new("P", (1, 1), 0) im.putpixel((0, 0), color) - self.assertEqual(im.convert("RGB").getpixel((0, 0)), (255, 0, 0)) + assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) -@unittest.skipIf(cffi is None, "No cffi") +@pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffiPutPixel(TestImagePutPixel): _need_cffi_access = True -@unittest.skipIf(cffi is None, "No cffi") +@pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffiGetPixel(TestImageGetPixel): _need_cffi_access = True -@unittest.skipIf(cffi is None, "No cffi") +@pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffi(AccessTest): _need_cffi_access = True @@ -233,12 +226,11 @@ class TestCffi(AccessTest): w, h = im.size for x in range(0, w, 10): for y in range(0, h, 10): - self.assertEqual(access[(x, y)], caccess[(x, y)]) + assert access[(x, y)] == caccess[(x, y)] # Access an out-of-range pixel - self.assertRaises( - ValueError, lambda: access[(access.xsize + 1, access.ysize + 1)] - ) + with pytest.raises(ValueError): + access[(access.xsize + 1, access.ysize + 1)] def test_get_vs_c(self): rgb = hopper("RGB") @@ -280,11 +272,11 @@ class TestCffi(AccessTest): for x in range(0, w, 10): for y in range(0, h, 10): access[(x, y)] = color - self.assertEqual(color, caccess[(x, y)]) + assert color == caccess[(x, y)] # Attempt to set the value on a read-only image access = PyAccess.new(im, True) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): access[(0, 0)] = color def test_set_vs_c(self): @@ -314,7 +306,7 @@ class TestCffi(AccessTest): # self._test_set_access(im, 2**13-1) def test_not_implemented(self): - self.assertIsNone(PyAccess.new(hopper("BGR;15"))) + assert PyAccess.new(hopper("BGR;15")) is None # ref https://github.com/python-pillow/Pillow/pull/2009 def test_reference_counting(self): @@ -325,20 +317,54 @@ class TestCffi(AccessTest): px = Image.new("L", (size, 1), 0).load() for i in range(size): # pixels can contain garbage if image is released - self.assertEqual(px[i, 0], 0) + assert px[i, 0] == 0 def test_p_putpixel_rgb_rgba(self): for color in [(255, 0, 0), (255, 0, 0, 255)]: im = Image.new("P", (1, 1), 0) access = PyAccess.new(im, False) access.putpixel((0, 0), color) - self.assertEqual(im.convert("RGB").getpixel((0, 0)), (255, 0, 0)) + assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) -class TestEmbeddable(unittest.TestCase): - @unittest.skipIf( +class TestImagePutPixelError(AccessTest): + IMAGE_MODES1 = ["L", "LA", "RGB", "RGBA"] + IMAGE_MODES2 = ["I", "I;16", "BGR;15"] + INVALID_TYPES = ["foo", 1.0, None] + + @pytest.mark.parametrize("mode", IMAGE_MODES1) + def test_putpixel_type_error1(self, mode): + im = hopper(mode) + for v in self.INVALID_TYPES: + with pytest.raises(TypeError, match="color must be int or tuple"): + im.putpixel((0, 0), v) + + @pytest.mark.parametrize("mode", IMAGE_MODES2) + def test_putpixel_type_error2(self, mode): + im = hopper(mode) + for v in self.INVALID_TYPES: + with pytest.raises( + TypeError, match="color must be int or single-element tuple" + ): + im.putpixel((0, 0), v) + + @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) + def test_putpixel_overflow_error(self, mode): + im = hopper(mode) + with pytest.raises(OverflowError): + im.putpixel((0, 0), 2 ** 80) + + def test_putpixel_unrecognized_mode(self): + im = hopper("BGR;15") + with pytest.raises(ValueError, match="unrecognized image mode"): + im.putpixel((0, 0), 0) + + +class TestEmbeddable: + @pytest.mark.skipif( not is_win32() or on_ci(), - "Failing on AppVeyor / GitHub Actions when run from subprocess, not from shell", + reason="Failing on AppVeyor / GitHub Actions when run from subprocess, " + "not from shell", ) def test_embeddable(self): with open("embed_pil.c", "w") as fh: @@ -368,13 +394,12 @@ int main(int argc, char* argv[]) % sys.prefix.replace("\\", "\\\\") ) - compiler = ccompiler.new_compiler() - compiler.add_include_dir(sysconfig.get_python_inc()) + compiler = new_compiler() + compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) - libdir = sysconfig.get_config_var( - "LIBDIR" - ) or sysconfig.get_python_inc().replace("include", "libs") - print(libdir) + libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( + "INCLUDEPY" + ).replace("include", "libs") compiler.add_library_dir(libdir) objects = compiler.compile(["embed_pil.c"]) compiler.link_executable(objects, "embed_pil") @@ -387,4 +412,4 @@ int main(int argc, char* argv[]) process = subprocess.Popen(["embed_pil.exe"], env=env) process.communicate() - self.assertEqual(process.returncode, 0) + assert process.returncode == 0 diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 8277e42af..980458407 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,54 +1,61 @@ +import pytest + from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import hopper im = hopper().resize((128, 100)) -class TestImageArray(PillowTestCase): - def test_toarray(self): - def test(mode): - ai = im.convert(mode).__array_interface__ - return ai["version"], ai["shape"], ai["typestr"], len(ai["data"]) +def test_toarray(): + def test(mode): + ai = im.convert(mode).__array_interface__ + return ai["version"], ai["shape"], ai["typestr"], len(ai["data"]) - # self.assertEqual(test("1"), (3, (100, 128), '|b1', 1600)) - self.assertEqual(test("L"), (3, (100, 128), "|u1", 12800)) + # assert test("1") == (3, (100, 128), '|b1', 1600)) + assert test("L") == (3, (100, 128), "|u1", 12800) - # FIXME: wrong? - self.assertEqual(test("I"), (3, (100, 128), Image._ENDIAN + "i4", 51200)) - # FIXME: wrong? - self.assertEqual(test("F"), (3, (100, 128), Image._ENDIAN + "f4", 51200)) + # FIXME: wrong? + assert test("I") == (3, (100, 128), Image._ENDIAN + "i4", 51200) + # FIXME: wrong? + assert test("F") == (3, (100, 128), Image._ENDIAN + "f4", 51200) - self.assertEqual(test("LA"), (3, (100, 128, 2), "|u1", 25600)) - self.assertEqual(test("RGB"), (3, (100, 128, 3), "|u1", 38400)) - self.assertEqual(test("RGBA"), (3, (100, 128, 4), "|u1", 51200)) - self.assertEqual(test("RGBX"), (3, (100, 128, 4), "|u1", 51200)) + assert test("LA") == (3, (100, 128, 2), "|u1", 25600) + assert test("RGB") == (3, (100, 128, 3), "|u1", 38400) + assert test("RGBA") == (3, (100, 128, 4), "|u1", 51200) + assert test("RGBX") == (3, (100, 128, 4), "|u1", 51200) - def test_fromarray(self): - class Wrapper: - """ Class with API matching Image.fromarray """ - def __init__(self, img, arr_params): - self.img = img - self.__array_interface__ = arr_params +def test_fromarray(): + class Wrapper: + """ Class with API matching Image.fromarray """ - def tobytes(self): - return self.img.tobytes() + def __init__(self, img, arr_params): + self.img = img + self.__array_interface__ = arr_params - def test(mode): - i = im.convert(mode) - a = i.__array_interface__ - a["strides"] = 1 # pretend it's non-contiguous - # Make wrapper instance for image, new array interface - wrapped = Wrapper(i, a) - out = Image.fromarray(wrapped) - return out.mode, out.size, list(i.getdata()) == list(out.getdata()) + def tobytes(self): + return self.img.tobytes() - # self.assertEqual(test("1"), ("1", (128, 100), True)) - self.assertEqual(test("L"), ("L", (128, 100), True)) - self.assertEqual(test("I"), ("I", (128, 100), True)) - self.assertEqual(test("F"), ("F", (128, 100), True)) - self.assertEqual(test("LA"), ("LA", (128, 100), True)) - self.assertEqual(test("RGB"), ("RGB", (128, 100), True)) - self.assertEqual(test("RGBA"), ("RGBA", (128, 100), True)) - self.assertEqual(test("RGBX"), ("RGBA", (128, 100), True)) + def test(mode): + i = im.convert(mode) + a = i.__array_interface__ + a["strides"] = 1 # pretend it's non-contiguous + # Make wrapper instance for image, new array interface + wrapped = Wrapper(i, a) + out = Image.fromarray(wrapped) + return out.mode, out.size, list(i.getdata()) == list(out.getdata()) + + # assert test("1") == ("1", (128, 100), True) + assert test("L") == ("L", (128, 100), True) + assert test("I") == ("I", (128, 100), True) + assert test("F") == ("F", (128, 100), True) + assert test("LA") == ("LA", (128, 100), True) + assert test("RGB") == ("RGB", (128, 100), True) + assert test("RGBA") == ("RGBA", (128, 100), True) + assert test("RGBX") == ("RGBA", (128, 100), True) + + # Test mode is None with no "typestr" in the array interface + with pytest.raises(TypeError): + wrapped = Wrapper(test("L"), {"shape": (100, 128)}) + Image.fromarray(wrapped) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 80fa9f513..6fe1bd962 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,241 +1,262 @@ +import pytest + from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image, assert_image_equal, assert_image_similar, hopper -class TestImageConvert(PillowTestCase): - def test_sanity(self): - def convert(im, mode): - out = im.convert(mode) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size) +def test_sanity(): + def convert(im, mode): + out = im.convert(mode) + assert out.mode == mode + assert out.size == im.size - modes = ( - "1", - "L", - "LA", - "P", - "PA", - "I", - "F", - "RGB", - "RGBA", - "RGBX", - "CMYK", - "YCbCr", - "HSV", - ) + modes = ( + "1", + "L", + "LA", + "P", + "PA", + "I", + "F", + "RGB", + "RGBA", + "RGBX", + "CMYK", + "YCbCr", + "HSV", + ) + for mode in modes: + im = hopper(mode) for mode in modes: - im = hopper(mode) - for mode in modes: - convert(im, mode) + convert(im, mode) - # Check 0 - im = Image.new(mode, (0, 0)) - for mode in modes: - convert(im, mode) + # Check 0 + im = Image.new(mode, (0, 0)) + for mode in modes: + convert(im, mode) - def test_default(self): - im = hopper("P") - self.assert_image(im, "P", im.size) - im = im.convert() - self.assert_image(im, "RGB", im.size) - im = im.convert() - self.assert_image(im, "RGB", im.size) +def test_default(): - # ref https://github.com/python-pillow/Pillow/issues/274 + im = hopper("P") + assert_image(im, "P", im.size) + im = im.convert() + assert_image(im, "RGB", im.size) + im = im.convert() + assert_image(im, "RGB", im.size) - def _test_float_conversion(self, im): - orig = im.getpixel((5, 5)) - converted = im.convert("F").getpixel((5, 5)) - self.assertEqual(orig, converted) - def test_8bit(self): - im = Image.open("Tests/images/hopper.jpg") - self._test_float_conversion(im.convert("L")) +# ref https://github.com/python-pillow/Pillow/issues/274 - def test_16bit(self): - im = Image.open("Tests/images/16bit.cropped.tif") - self._test_float_conversion(im) - def test_16bit_workaround(self): - im = Image.open("Tests/images/16bit.cropped.tif") - self._test_float_conversion(im.convert("I")) +def _test_float_conversion(im): + orig = im.getpixel((5, 5)) + converted = im.convert("F").getpixel((5, 5)) + assert orig == converted - def test_rgba_p(self): - im = hopper("RGBA") - im.putalpha(hopper("L")) - converted = im.convert("P") - comparable = converted.convert("RGBA") +def test_8bit(): + with Image.open("Tests/images/hopper.jpg") as im: + _test_float_conversion(im.convert("L")) - self.assert_image_similar(im, comparable, 20) - def test_trns_p(self): - im = hopper("P") - im.info["transparency"] = 0 +def test_16bit(): + with Image.open("Tests/images/16bit.cropped.tif") as im: + _test_float_conversion(im) - f = self.tempfile("temp.png") - im_l = im.convert("L") - self.assertEqual(im_l.info["transparency"], 0) # undone - im_l.save(f) +def test_16bit_workaround(): + with Image.open("Tests/images/16bit.cropped.tif") as im: + _test_float_conversion(im.convert("I")) - im_rgb = im.convert("RGB") - self.assertEqual(im_rgb.info["transparency"], (0, 0, 0)) # undone - im_rgb.save(f) - # ref https://github.com/python-pillow/Pillow/issues/664 +def test_rgba_p(): + im = hopper("RGBA") + im.putalpha(hopper("L")) - def test_trns_p_rgba(self): + converted = im.convert("P") + comparable = converted.convert("RGBA") + + assert_image_similar(im, comparable, 20) + + +def test_trns_p(tmp_path): + im = hopper("P") + im.info["transparency"] = 0 + + f = str(tmp_path / "temp.png") + + im_l = im.convert("L") + assert im_l.info["transparency"] == 0 # undone + im_l.save(f) + + im_rgb = im.convert("RGB") + assert im_rgb.info["transparency"] == (0, 0, 0) # undone + im_rgb.save(f) + + +# ref https://github.com/python-pillow/Pillow/issues/664 + + +def test_trns_p_rgba(): + # Arrange + im = hopper("P") + im.info["transparency"] = 128 + + # Act + im_rgba = im.convert("RGBA") + + # Assert + assert "transparency" not in im_rgba.info + # https://github.com/python-pillow/Pillow/issues/2702 + assert im_rgba.palette is None + + +def test_trns_l(tmp_path): + im = hopper("L") + im.info["transparency"] = 128 + + f = str(tmp_path / "temp.png") + + im_rgb = im.convert("RGB") + assert im_rgb.info["transparency"] == (128, 128, 128) # undone + im_rgb.save(f) + + im_p = im.convert("P") + assert "transparency" in im_p.info + im_p.save(f) + + im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) + assert "transparency" not in im_p.info + im_p.save(f) + + +def test_trns_RGB(tmp_path): + im = hopper("RGB") + im.info["transparency"] = im.getpixel((0, 0)) + + f = str(tmp_path / "temp.png") + + im_l = im.convert("L") + assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone + im_l.save(f) + + im_p = im.convert("P") + assert "transparency" in im_p.info + im_p.save(f) + + im_rgba = im.convert("RGBA") + assert "transparency" not in im_rgba.info + im_rgba.save(f) + + im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) + assert "transparency" not in im_p.info + im_p.save(f) + + +def test_gif_with_rgba_palette_to_p(): + # See https://github.com/python-pillow/Pillow/issues/2433 + with Image.open("Tests/images/hopper.gif") as im: + im.info["transparency"] = 255 + im.load() + assert im.palette.mode == "RGBA" + im_p = im.convert("P") + + # Should not raise ValueError: unrecognized raw mode + im_p.load() + + +def test_p_la(): + im = hopper("RGBA") + alpha = hopper("L") + im.putalpha(alpha) + + comparable = im.convert("P").convert("LA").getchannel("A") + + assert_image_similar(alpha, comparable, 5) + + +def test_matrix_illegal_conversion(): + # Arrange + im = hopper("CMYK") + # fmt: off + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + # fmt: on + assert im.mode != "RGB" + + # Act / Assert + with pytest.raises(ValueError): + im.convert(mode="CMYK", matrix=matrix) + + +def test_matrix_wrong_mode(): + # Arrange + im = hopper("L") + # fmt: off + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + # fmt: on + assert im.mode == "L" + + # Act / Assert + with pytest.raises(ValueError): + im.convert(mode="L", matrix=matrix) + + +def test_matrix_xyz(): + def matrix_convert(mode): # Arrange - im = hopper("P") - im.info["transparency"] = 128 + im = hopper("RGB") + im.info["transparency"] = (255, 0, 0) + # fmt: off + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + # fmt: on + assert im.mode == "RGB" # Act - im_rgba = im.convert("RGBA") + # Convert an RGB image to the CIE XYZ colour space + converted_im = im.convert(mode=mode, matrix=matrix) # Assert - self.assertNotIn("transparency", im_rgba.info) - # https://github.com/python-pillow/Pillow/issues/2702 - self.assertIsNone(im_rgba.palette) - - def test_trns_l(self): - im = hopper("L") - im.info["transparency"] = 128 - - f = self.tempfile("temp.png") - - im_rgb = im.convert("RGB") - self.assertEqual(im_rgb.info["transparency"], (128, 128, 128)) # undone - im_rgb.save(f) - - im_p = im.convert("P") - self.assertIn("transparency", im_p.info) - im_p.save(f) - - im_p = self.assert_warning(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) - self.assertNotIn("transparency", im_p.info) - im_p.save(f) - - def test_trns_RGB(self): - im = hopper("RGB") - im.info["transparency"] = im.getpixel((0, 0)) - - f = self.tempfile("temp.png") - - im_l = im.convert("L") - self.assertEqual(im_l.info["transparency"], im_l.getpixel((0, 0))) # undone - im_l.save(f) - - im_p = im.convert("P") - self.assertIn("transparency", im_p.info) - im_p.save(f) - - im_rgba = im.convert("RGBA") - self.assertNotIn("transparency", im_rgba.info) - im_rgba.save(f) - - im_p = self.assert_warning(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) - self.assertNotIn("transparency", im_p.info) - im_p.save(f) - - def test_gif_with_rgba_palette_to_p(self): - # See https://github.com/python-pillow/Pillow/issues/2433 - with Image.open("Tests/images/hopper.gif") as im: - im.info["transparency"] = 255 - im.load() - self.assertEqual(im.palette.mode, "RGBA") - im_p = im.convert("P") - - # Should not raise ValueError: unrecognized raw mode - im_p.load() - - def test_p_la(self): - im = hopper("RGBA") - alpha = hopper("L") - im.putalpha(alpha) - - comparable = im.convert("P").convert("LA").getchannel("A") - - self.assert_image_similar(alpha, comparable, 5) - - def test_matrix_illegal_conversion(self): - # Arrange - im = hopper("CMYK") - # fmt: off - matrix = ( - 0.412453, 0.357580, 0.180423, 0, - 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0) - # fmt: on - self.assertNotEqual(im.mode, "RGB") - - # Act / Assert - self.assertRaises(ValueError, im.convert, mode="CMYK", matrix=matrix) - - def test_matrix_wrong_mode(self): - # Arrange - im = hopper("L") - # fmt: off - matrix = ( - 0.412453, 0.357580, 0.180423, 0, - 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0) - # fmt: on - self.assertEqual(im.mode, "L") - - # Act / Assert - self.assertRaises(ValueError, im.convert, mode="L", matrix=matrix) - - def test_matrix_xyz(self): - def matrix_convert(mode): - # Arrange - im = hopper("RGB") - im.info["transparency"] = (255, 0, 0) - # fmt: off - matrix = ( - 0.412453, 0.357580, 0.180423, 0, - 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0) - # fmt: on - self.assertEqual(im.mode, "RGB") - - # Act - # Convert an RGB image to the CIE XYZ colour space - converted_im = im.convert(mode=mode, matrix=matrix) - - # Assert - self.assertEqual(converted_im.mode, mode) - self.assertEqual(converted_im.size, im.size) - target = Image.open("Tests/images/hopper-XYZ.png") + assert converted_im.mode == mode + assert converted_im.size == im.size + with Image.open("Tests/images/hopper-XYZ.png") as target: if converted_im.mode == "RGB": - self.assert_image_similar(converted_im, target, 3) - self.assertEqual(converted_im.info["transparency"], (105, 54, 4)) + assert_image_similar(converted_im, target, 3) + assert converted_im.info["transparency"] == (105, 54, 4) else: - self.assert_image_similar(converted_im, target.getchannel(0), 1) - self.assertEqual(converted_im.info["transparency"], 105) + assert_image_similar(converted_im, target.getchannel(0), 1) + assert converted_im.info["transparency"] == 105 - matrix_convert("RGB") - matrix_convert("L") + matrix_convert("RGB") + matrix_convert("L") - def test_matrix_identity(self): - # Arrange - im = hopper("RGB") - # fmt: off - identity_matrix = ( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0) - # fmt: on - self.assertEqual(im.mode, "RGB") - # Act - # Convert with an identity matrix - converted_im = im.convert(mode="RGB", matrix=identity_matrix) +def test_matrix_identity(): + # Arrange + im = hopper("RGB") + # fmt: off + identity_matrix = ( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0) + # fmt: on + assert im.mode == "RGB" - # Assert - # No change - self.assert_image_equal(converted_im, im) + # Act + # Convert with an identity matrix + converted_im = im.convert(mode="RGB", matrix=identity_matrix) + + # Assert + # No change + assert_image_equal(converted_im, im) diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 653159e14..ad0391dbe 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -2,40 +2,40 @@ import copy from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageCopy(PillowTestCase): - def test_copy(self): - croppedCoordinates = (10, 10, 20, 20) - croppedSize = (10, 10) - for mode in "1", "P", "L", "RGB", "I", "F": - # Internal copy method - im = hopper(mode) - out = im.copy() - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, im.size) - - # Python's copy method - im = hopper(mode) - out = copy.copy(im) - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, im.size) - - # Internal copy method on a cropped image - im = hopper(mode) - out = im.crop(croppedCoordinates).copy() - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, croppedSize) - - # Python's copy method on a cropped image - im = hopper(mode) - out = copy.copy(im.crop(croppedCoordinates)) - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, croppedSize) - - def test_copy_zero(self): - im = Image.new("RGB", (0, 0)) +def test_copy(): + croppedCoordinates = (10, 10, 20, 20) + croppedSize = (10, 10) + for mode in "1", "P", "L", "RGB", "I", "F": + # Internal copy method + im = hopper(mode) out = im.copy() - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, im.size) + assert out.mode == im.mode + assert out.size == im.size + + # Python's copy method + im = hopper(mode) + out = copy.copy(im) + assert out.mode == im.mode + assert out.size == im.size + + # Internal copy method on a cropped image + im = hopper(mode) + out = im.crop(croppedCoordinates).copy() + assert out.mode == im.mode + assert out.size == croppedSize + + # Python's copy method on a cropped image + im = hopper(mode) + out = copy.copy(im.crop(croppedCoordinates)) + assert out.mode == im.mode + assert out.size == croppedSize + + +def test_copy_zero(): + im = Image.new("RGB", (0, 0)) + out = im.copy() + assert out.mode == im.mode + assert out.size == im.size diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 6a604f494..e2228758c 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -1,103 +1,110 @@ +import pytest + from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper -class TestImageCrop(PillowTestCase): - def test_crop(self): - def crop(mode): - im = hopper(mode) - self.assert_image_equal(im.crop(), im) +def test_crop(): + def crop(mode): + im = hopper(mode) + assert_image_equal(im.crop(), im) - cropped = im.crop((50, 50, 100, 100)) - self.assertEqual(cropped.mode, mode) - self.assertEqual(cropped.size, (50, 50)) + cropped = im.crop((50, 50, 100, 100)) + assert cropped.mode == mode + assert cropped.size == (50, 50) - for mode in "1", "P", "L", "RGB", "I", "F": - crop(mode) + for mode in "1", "P", "L", "RGB", "I", "F": + crop(mode) - def test_wide_crop(self): - def crop(*bbox): - i = im.crop(bbox) - h = i.histogram() - while h and not h[-1]: - del h[-1] - return tuple(h) - im = Image.new("L", (100, 100), 1) +def test_wide_crop(): + def crop(*bbox): + i = im.crop(bbox) + h = i.histogram() + while h and not h[-1]: + del h[-1] + return tuple(h) - self.assertEqual(crop(0, 0, 100, 100), (0, 10000)) - self.assertEqual(crop(25, 25, 75, 75), (0, 2500)) + im = Image.new("L", (100, 100), 1) - # sides - self.assertEqual(crop(-25, 0, 25, 50), (1250, 1250)) - self.assertEqual(crop(0, -25, 50, 25), (1250, 1250)) - self.assertEqual(crop(75, 0, 125, 50), (1250, 1250)) - self.assertEqual(crop(0, 75, 50, 125), (1250, 1250)) + assert crop(0, 0, 100, 100) == (0, 10000) + assert crop(25, 25, 75, 75) == (0, 2500) - self.assertEqual(crop(-25, 25, 125, 75), (2500, 5000)) - self.assertEqual(crop(25, -25, 75, 125), (2500, 5000)) + # sides + assert crop(-25, 0, 25, 50) == (1250, 1250) + assert crop(0, -25, 50, 25) == (1250, 1250) + assert crop(75, 0, 125, 50) == (1250, 1250) + assert crop(0, 75, 50, 125) == (1250, 1250) - # corners - self.assertEqual(crop(-25, -25, 25, 25), (1875, 625)) - self.assertEqual(crop(75, -25, 125, 25), (1875, 625)) - self.assertEqual(crop(75, 75, 125, 125), (1875, 625)) - self.assertEqual(crop(-25, 75, 25, 125), (1875, 625)) + assert crop(-25, 25, 125, 75) == (2500, 5000) + assert crop(25, -25, 75, 125) == (2500, 5000) - def test_negative_crop(self): - # Check negative crop size (@PIL171) + # corners + assert crop(-25, -25, 25, 25) == (1875, 625) + assert crop(75, -25, 125, 25) == (1875, 625) + assert crop(75, 75, 125, 125) == (1875, 625) + assert crop(-25, 75, 25, 125) == (1875, 625) - im = Image.new("L", (512, 512)) - im = im.crop((400, 400, 200, 200)) - self.assertEqual(im.size, (0, 0)) - self.assertEqual(len(im.getdata()), 0) - self.assertRaises(IndexError, lambda: im.getdata()[0]) +def test_negative_crop(): + # Check negative crop size (@PIL171) - def test_crop_float(self): - # Check cropping floats are rounded to nearest integer - # https://github.com/python-pillow/Pillow/issues/1744 + im = Image.new("L", (512, 512)) + im = im.crop((400, 400, 200, 200)) - # Arrange - im = Image.new("RGB", (10, 10)) - self.assertEqual(im.size, (10, 10)) + assert im.size == (0, 0) + assert len(im.getdata()) == 0 + with pytest.raises(IndexError): + im.getdata()[0] - # Act - cropped = im.crop((0.9, 1.1, 4.2, 5.8)) - # Assert - self.assertEqual(cropped.size, (3, 5)) +def test_crop_float(): + # Check cropping floats are rounded to nearest integer + # https://github.com/python-pillow/Pillow/issues/1744 - def test_crop_crash(self): - # Image.crop crashes prepatch with an access violation - # apparently a use after free on windows, see - # https://github.com/python-pillow/Pillow/issues/1077 + # Arrange + im = Image.new("RGB", (10, 10)) + assert im.size == (10, 10) - test_img = "Tests/images/bmp/g/pal8-0.bmp" - extents = (1, 1, 10, 10) - # works prepatch - img = Image.open(test_img) + # Act + cropped = im.crop((0.9, 1.1, 4.2, 5.8)) + + # Assert + assert cropped.size == (3, 5) + + +def test_crop_crash(): + # Image.crop crashes prepatch with an access violation + # apparently a use after free on Windows, see + # https://github.com/python-pillow/Pillow/issues/1077 + + test_img = "Tests/images/bmp/g/pal8-0.bmp" + extents = (1, 1, 10, 10) + # works prepatch + with Image.open(test_img) as img: img2 = img.crop(extents) - img2.load() + img2.load() - # fail prepatch - img = Image.open(test_img) + # fail prepatch + with Image.open(test_img) as img: img = img.crop(extents) - img.load() + img.load() - def test_crop_zero(self): - im = Image.new("RGB", (0, 0), "white") +def test_crop_zero(): - cropped = im.crop((0, 0, 0, 0)) - self.assertEqual(cropped.size, (0, 0)) + im = Image.new("RGB", (0, 0), "white") - cropped = im.crop((10, 10, 20, 20)) - self.assertEqual(cropped.size, (10, 10)) - self.assertEqual(cropped.getdata()[0], (0, 0, 0)) + cropped = im.crop((0, 0, 0, 0)) + assert cropped.size == (0, 0) - im = Image.new("RGB", (0, 0)) + cropped = im.crop((10, 10, 20, 20)) + assert cropped.size == (10, 10) + assert cropped.getdata()[0] == (0, 0, 0) - cropped = im.crop((10, 10, 20, 20)) - self.assertEqual(cropped.size, (10, 10)) - self.assertEqual(cropped.getdata()[2], (0, 0, 0)) + im = Image.new("RGB", (0, 0)) + + cropped = im.crop((10, 10, 20, 20)) + assert cropped.size == (10, 10) + assert cropped.getdata()[2] == (0, 0, 0) diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 5d92ee797..8b4b44768 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -1,69 +1,72 @@ from PIL import Image -from .helper import PillowTestCase, fromstring, tostring +from .helper import fromstring, skip_unless_feature, tostring + +pytestmark = skip_unless_feature("jpg") -class TestImageDraft(PillowTestCase): - def setUp(self): - codecs = dir(Image.core) - if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: - self.skipTest("jpeg support not available") +def draft_roundtrip(in_mode, in_size, req_mode, req_size): + im = Image.new(in_mode, in_size) + data = tostring(im, "JPEG") + im = fromstring(data) + mode, box = im.draft(req_mode, req_size) + scale, _ = im.decoderconfig + assert box[:2] == (0, 0) + assert (im.width - scale) < box[2] <= im.width + assert (im.height - scale) < box[3] <= im.height + return im - def draft_roundtrip(self, in_mode, in_size, req_mode, req_size): - im = Image.new(in_mode, in_size) - data = tostring(im, "JPEG") - im = fromstring(data) - im.draft(req_mode, req_size) - return im - def test_size(self): - for in_size, req_size, out_size in [ - ((435, 361), (2048, 2048), (435, 361)), # bigger - ((435, 361), (435, 361), (435, 361)), # same - ((128, 128), (64, 64), (64, 64)), - ((128, 128), (32, 32), (32, 32)), - ((128, 128), (16, 16), (16, 16)), - # large requested width - ((435, 361), (218, 128), (435, 361)), # almost 2x - ((435, 361), (217, 128), (218, 181)), # more than 2x - ((435, 361), (109, 64), (218, 181)), # almost 4x - ((435, 361), (108, 64), (109, 91)), # more than 4x - ((435, 361), (55, 32), (109, 91)), # almost 8x - ((435, 361), (54, 32), (55, 46)), # more than 8x - ((435, 361), (27, 16), (55, 46)), # more than 16x - # and vice versa - ((435, 361), (128, 181), (435, 361)), # almost 2x - ((435, 361), (128, 180), (218, 181)), # more than 2x - ((435, 361), (64, 91), (218, 181)), # almost 4x - ((435, 361), (64, 90), (109, 91)), # more than 4x - ((435, 361), (32, 46), (109, 91)), # almost 8x - ((435, 361), (32, 45), (55, 46)), # more than 8x - ((435, 361), (16, 22), (55, 46)), # more than 16x - ]: - im = self.draft_roundtrip("L", in_size, None, req_size) - im.load() - self.assertEqual(im.size, out_size) - - def test_mode(self): - for in_mode, req_mode, out_mode in [ - ("RGB", "1", "RGB"), - ("RGB", "L", "L"), - ("RGB", "RGB", "RGB"), - ("RGB", "YCbCr", "YCbCr"), - ("L", "1", "L"), - ("L", "L", "L"), - ("L", "RGB", "L"), - ("L", "YCbCr", "L"), - ("CMYK", "1", "CMYK"), - ("CMYK", "L", "CMYK"), - ("CMYK", "RGB", "CMYK"), - ("CMYK", "YCbCr", "CMYK"), - ]: - im = self.draft_roundtrip(in_mode, (64, 64), req_mode, None) - im.load() - self.assertEqual(im.mode, out_mode) - - def test_several_drafts(self): - im = self.draft_roundtrip("L", (128, 128), None, (64, 64)) - im.draft(None, (64, 64)) +def test_size(): + for in_size, req_size, out_size in [ + ((435, 361), (2048, 2048), (435, 361)), # bigger + ((435, 361), (435, 361), (435, 361)), # same + ((128, 128), (64, 64), (64, 64)), + ((128, 128), (32, 32), (32, 32)), + ((128, 128), (16, 16), (16, 16)), + # large requested width + ((435, 361), (218, 128), (435, 361)), # almost 2x + ((435, 361), (217, 128), (218, 181)), # more than 2x + ((435, 361), (109, 64), (218, 181)), # almost 4x + ((435, 361), (108, 64), (109, 91)), # more than 4x + ((435, 361), (55, 32), (109, 91)), # almost 8x + ((435, 361), (54, 32), (55, 46)), # more than 8x + ((435, 361), (27, 16), (55, 46)), # more than 16x + # and vice versa + ((435, 361), (128, 181), (435, 361)), # almost 2x + ((435, 361), (128, 180), (218, 181)), # more than 2x + ((435, 361), (64, 91), (218, 181)), # almost 4x + ((435, 361), (64, 90), (109, 91)), # more than 4x + ((435, 361), (32, 46), (109, 91)), # almost 8x + ((435, 361), (32, 45), (55, 46)), # more than 8x + ((435, 361), (16, 22), (55, 46)), # more than 16x + ]: + im = draft_roundtrip("L", in_size, None, req_size) im.load() + assert im.size == out_size + + +def test_mode(): + for in_mode, req_mode, out_mode in [ + ("RGB", "1", "RGB"), + ("RGB", "L", "L"), + ("RGB", "RGB", "RGB"), + ("RGB", "YCbCr", "YCbCr"), + ("L", "1", "L"), + ("L", "L", "L"), + ("L", "RGB", "L"), + ("L", "YCbCr", "L"), + ("CMYK", "1", "CMYK"), + ("CMYK", "L", "CMYK"), + ("CMYK", "RGB", "CMYK"), + ("CMYK", "YCbCr", "CMYK"), + ]: + im = draft_roundtrip(in_mode, (64, 64), req_mode, None) + im.load() + assert im.mode == out_mode + + +def test_several_drafts(): + 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 bc792bca6..876d676fe 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -1,17 +1,16 @@ -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageEntropy(PillowTestCase): - def test_entropy(self): - def entropy(mode): - return hopper(mode).entropy() +def test_entropy(): + def entropy(mode): + return hopper(mode).entropy() - self.assertAlmostEqual(entropy("1"), 0.9138803254693582) - self.assertAlmostEqual(entropy("L"), 7.06650513081286) - self.assertAlmostEqual(entropy("I"), 7.06650513081286) - self.assertAlmostEqual(entropy("F"), 7.06650513081286) - self.assertAlmostEqual(entropy("P"), 5.0530452472519745) - self.assertAlmostEqual(entropy("RGB"), 8.821286587714319) - self.assertAlmostEqual(entropy("RGBA"), 7.42724306524488) - self.assertAlmostEqual(entropy("CMYK"), 7.4272430652448795) - self.assertAlmostEqual(entropy("YCbCr"), 7.698360534903628) + assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0 + assert round(abs(entropy("L") - 7.063008716585465), 7) == 0 + assert round(abs(entropy("I") - 7.063008716585465), 7) == 0 + assert round(abs(entropy("F") - 7.063008716585465), 7) == 0 + assert round(abs(entropy("P") - 5.0530452472519745), 7) == 0 + assert round(abs(entropy("RGB") - 8.821286587714319), 7) == 0 + assert round(abs(entropy("RGBA") - 7.42724306524488), 7) == 0 + assert round(abs(entropy("CMYK") - 7.4272430652448795), 7) == 0 + assert round(abs(entropy("YCbCr") - 7.698360534903628), 7) == 0 diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index bbd10e6e5..df8c353f3 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,143 +1,156 @@ +import pytest + from PIL import Image, ImageFilter -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper -class TestImageFilter(PillowTestCase): - def test_sanity(self): - def filter(filter): - for mode in ["L", "RGB", "CMYK"]: - im = hopper(mode) - out = im.filter(filter) - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, im.size) +def test_sanity(): + def apply_filter(filter_to_apply): + for mode in ["L", "RGB", "CMYK"]: + im = hopper(mode) + out = im.filter(filter_to_apply) + assert out.mode == im.mode + assert out.size == im.size - filter(ImageFilter.BLUR) - filter(ImageFilter.CONTOUR) - filter(ImageFilter.DETAIL) - filter(ImageFilter.EDGE_ENHANCE) - filter(ImageFilter.EDGE_ENHANCE_MORE) - filter(ImageFilter.EMBOSS) - filter(ImageFilter.FIND_EDGES) - filter(ImageFilter.SMOOTH) - filter(ImageFilter.SMOOTH_MORE) - filter(ImageFilter.SHARPEN) - filter(ImageFilter.MaxFilter) - filter(ImageFilter.MedianFilter) - filter(ImageFilter.MinFilter) - filter(ImageFilter.ModeFilter) - filter(ImageFilter.GaussianBlur) - filter(ImageFilter.GaussianBlur(5)) - filter(ImageFilter.BoxBlur(5)) - filter(ImageFilter.UnsharpMask) - filter(ImageFilter.UnsharpMask(10)) + apply_filter(ImageFilter.BLUR) + apply_filter(ImageFilter.CONTOUR) + apply_filter(ImageFilter.DETAIL) + apply_filter(ImageFilter.EDGE_ENHANCE) + apply_filter(ImageFilter.EDGE_ENHANCE_MORE) + apply_filter(ImageFilter.EMBOSS) + apply_filter(ImageFilter.FIND_EDGES) + apply_filter(ImageFilter.SMOOTH) + apply_filter(ImageFilter.SMOOTH_MORE) + apply_filter(ImageFilter.SHARPEN) + apply_filter(ImageFilter.MaxFilter) + apply_filter(ImageFilter.MedianFilter) + apply_filter(ImageFilter.MinFilter) + apply_filter(ImageFilter.ModeFilter) + apply_filter(ImageFilter.GaussianBlur) + apply_filter(ImageFilter.GaussianBlur(5)) + apply_filter(ImageFilter.BoxBlur(5)) + apply_filter(ImageFilter.UnsharpMask) + apply_filter(ImageFilter.UnsharpMask(10)) - self.assertRaises(TypeError, filter, "hello") + with pytest.raises(TypeError): + apply_filter("hello") - def test_crash(self): - # crashes on small images - im = Image.new("RGB", (1, 1)) - im.filter(ImageFilter.SMOOTH) +def test_crash(): - im = Image.new("RGB", (2, 2)) - im.filter(ImageFilter.SMOOTH) + # crashes on small images + im = Image.new("RGB", (1, 1)) + im.filter(ImageFilter.SMOOTH) - im = Image.new("RGB", (3, 3)) - im.filter(ImageFilter.SMOOTH) + im = Image.new("RGB", (2, 2)) + im.filter(ImageFilter.SMOOTH) - def test_modefilter(self): - def modefilter(mode): - im = Image.new(mode, (3, 3), None) - im.putdata(list(range(9))) - # image is: - # 0 1 2 - # 3 4 5 - # 6 7 8 - mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) - im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 - mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) - return mod, mod2 + im = Image.new("RGB", (3, 3)) + im.filter(ImageFilter.SMOOTH) - self.assertEqual(modefilter("1"), (4, 0)) - self.assertEqual(modefilter("L"), (4, 0)) - self.assertEqual(modefilter("P"), (4, 0)) - self.assertEqual(modefilter("RGB"), ((4, 0, 0), (0, 0, 0))) - def test_rankfilter(self): - def rankfilter(mode): - im = Image.new(mode, (3, 3), None) - im.putdata(list(range(9))) - # image is: - # 0 1 2 - # 3 4 5 - # 6 7 8 - minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) - med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) - maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) - return minimum, med, maximum +def test_modefilter(): + def modefilter(mode): + im = Image.new(mode, (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) + im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 + mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) + return mod, mod2 - self.assertEqual(rankfilter("1"), (0, 4, 8)) - self.assertEqual(rankfilter("L"), (0, 4, 8)) - self.assertRaises(ValueError, rankfilter, "P") - self.assertEqual(rankfilter("RGB"), ((0, 0, 0), (4, 0, 0), (8, 0, 0))) - self.assertEqual(rankfilter("I"), (0, 4, 8)) - self.assertEqual(rankfilter("F"), (0.0, 4.0, 8.0)) + assert modefilter("1") == (4, 0) + assert modefilter("L") == (4, 0) + assert modefilter("P") == (4, 0) + assert modefilter("RGB") == ((4, 0, 0), (0, 0, 0)) - def test_rankfilter_properties(self): - rankfilter = ImageFilter.RankFilter(1, 2) - self.assertEqual(rankfilter.size, 1) - self.assertEqual(rankfilter.rank, 2) +def test_rankfilter(): + def rankfilter(mode): + im = Image.new(mode, (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) + med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) + maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) + return minimum, med, maximum - def test_builtinfilter_p(self): - builtinFilter = ImageFilter.BuiltinFilter() + assert rankfilter("1") == (0, 4, 8) + assert rankfilter("L") == (0, 4, 8) + with pytest.raises(ValueError): + rankfilter("P") + assert rankfilter("RGB") == ((0, 0, 0), (4, 0, 0), (8, 0, 0)) + assert rankfilter("I") == (0, 4, 8) + assert rankfilter("F") == (0.0, 4.0, 8.0) - self.assertRaises(ValueError, builtinFilter.filter, hopper("P")) - def test_kernel_not_enough_coefficients(self): - self.assertRaises(ValueError, lambda: ImageFilter.Kernel((3, 3), (0, 0))) +def test_rankfilter_properties(): + rankfilter = ImageFilter.RankFilter(1, 2) - def test_consistency_3x3(self): - source = Image.open("Tests/images/hopper.bmp") - reference = Image.open("Tests/images/hopper_emboss.bmp") - kernel = ImageFilter.Kernel( # noqa: E127 - (3, 3), - # fmt: off - (-1, -1, 0, - -1, 0, 1, - 0, 1, 1), - # fmt: on - 0.3, - ) - source = source.split() * 2 - reference = reference.split() * 2 + assert rankfilter.size == 1 + assert rankfilter.rank == 2 - for mode in ["L", "LA", "RGB", "CMYK"]: - self.assert_image_equal( - Image.merge(mode, source[: len(mode)]).filter(kernel), - Image.merge(mode, reference[: len(mode)]), + +def test_builtinfilter_p(): + builtinFilter = ImageFilter.BuiltinFilter() + + with pytest.raises(ValueError): + builtinFilter.filter(hopper("P")) + + +def test_kernel_not_enough_coefficients(): + with pytest.raises(ValueError): + ImageFilter.Kernel((3, 3), (0, 0)) + + +def test_consistency_3x3(): + with Image.open("Tests/images/hopper.bmp") as source: + with Image.open("Tests/images/hopper_emboss.bmp") as reference: + kernel = ImageFilter.Kernel( + (3, 3), + # fmt: off + (-1, -1, 0, + -1, 0, 1, + 0, 1, 1), + # fmt: on + 0.3, ) + source = source.split() * 2 + reference = reference.split() * 2 - def test_consistency_5x5(self): - source = Image.open("Tests/images/hopper.bmp") - reference = Image.open("Tests/images/hopper_emboss_more.bmp") - kernel = ImageFilter.Kernel( # noqa: E127 - (5, 5), - # fmt: off - (-1, -1, -1, -1, 0, - -1, -1, -1, 0, 1, - -1, -1, 0, 1, 1, - -1, 0, 1, 1, 1, - 0, 1, 1, 1, 1), - # fmt: on - 0.3, - ) - source = source.split() * 2 - reference = reference.split() * 2 + for mode in ["L", "LA", "RGB", "CMYK"]: + assert_image_equal( + Image.merge(mode, source[: len(mode)]).filter(kernel), + Image.merge(mode, reference[: len(mode)]), + ) - for mode in ["L", "LA", "RGB", "CMYK"]: - self.assert_image_equal( - Image.merge(mode, source[: len(mode)]).filter(kernel), - Image.merge(mode, reference[: len(mode)]), + +def test_consistency_5x5(): + with Image.open("Tests/images/hopper.bmp") as source: + with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: + kernel = ImageFilter.Kernel( + (5, 5), + # fmt: off + (-1, -1, -1, -1, 0, + -1, -1, -1, 0, 1, + -1, -1, 0, 1, 1, + -1, 0, 1, 1, 1, + 0, 1, 1, 1, 1), + # fmt: on + 0.3, ) + source = source.split() * 2 + reference = reference.split() * 2 + + for mode in ["L", "LA", "RGB", "CMYK"]: + assert_image_equal( + Image.merge(mode, source[: len(mode)]).filter(kernel), + Image.merge(mode, reference[: len(mode)]), + ) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 21d466d02..7fb05cda7 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,14 +1,10 @@ from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper -class TestImageFromBytes(PillowTestCase): - def test_sanity(self): - im1 = hopper() - im2 = Image.frombytes(im1.mode, im1.size, im1.tobytes()) +def test_sanity(): + im1 = hopper() + im2 = Image.frombytes(im1.mode, im1.size, im1.tobytes()) - self.assert_image_equal(im1, im2) - - def test_not_implemented(self): - self.assertRaises(NotImplementedError, Image.fromstring) + assert_image_equal(im1, im2) diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index 0d961572c..5ad5b5c3c 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -1,47 +1,60 @@ +import pytest + from PIL import Image, ImageQt -from .helper import PillowTestCase, hopper -from .test_imageqt import PillowQtTestCase +from .helper import assert_image_equal, hopper + +pytestmark = pytest.mark.skipif( + not ImageQt.qt_is_installed, reason="Qt bindings are not installed" +) -class TestFromQImage(PillowQtTestCase, PillowTestCase): - def setUp(self): - super().setUp() - self.files_to_test = [ - hopper(), - Image.open("Tests/images/transparent.png"), - Image.open("Tests/images/7x13.png"), - ] - for im in self.files_to_test: - self.addCleanup(im.close) +@pytest.fixture +def test_images(): + ims = [ + hopper(), + Image.open("Tests/images/transparent.png"), + Image.open("Tests/images/7x13.png"), + ] + try: + yield ims + finally: + for im in ims: + im.close() - def roundtrip(self, expected): - # PIL -> Qt - intermediate = expected.toqimage() - # Qt -> PIL - result = ImageQt.fromqimage(intermediate) - if intermediate.hasAlphaChannel(): - self.assert_image_equal(result, expected.convert("RGBA")) - else: - self.assert_image_equal(result, expected.convert("RGB")) +def roundtrip(expected): + # PIL -> Qt + intermediate = expected.toqimage() + # Qt -> PIL + result = ImageQt.fromqimage(intermediate) - def test_sanity_1(self): - for im in self.files_to_test: - self.roundtrip(im.convert("1")) + if intermediate.hasAlphaChannel(): + assert_image_equal(result, expected.convert("RGBA")) + else: + assert_image_equal(result, expected.convert("RGB")) - def test_sanity_rgb(self): - for im in self.files_to_test: - self.roundtrip(im.convert("RGB")) - def test_sanity_rgba(self): - for im in self.files_to_test: - self.roundtrip(im.convert("RGBA")) +def test_sanity_1(test_images): + for im in test_images: + roundtrip(im.convert("1")) - def test_sanity_l(self): - for im in self.files_to_test: - self.roundtrip(im.convert("L")) - def test_sanity_p(self): - for im in self.files_to_test: - self.roundtrip(im.convert("P")) +def test_sanity_rgb(test_images): + for im in test_images: + roundtrip(im.convert("RGB")) + + +def test_sanity_rgba(test_images): + for im in test_images: + roundtrip(im.convert("RGBA")) + + +def test_sanity_l(test_images): + for im in test_images: + roundtrip(im.convert("L")) + + +def test_sanity_p(test_images): + for im in test_images: + roundtrip(im.convert("P")) diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index 785b2ae42..08fc12c1c 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -1,16 +1,13 @@ from PIL import Image -from .helper import PillowTestCase - -class TestImageGetBands(PillowTestCase): - def test_getbands(self): - self.assertEqual(Image.new("1", (1, 1)).getbands(), ("1",)) - self.assertEqual(Image.new("L", (1, 1)).getbands(), ("L",)) - self.assertEqual(Image.new("I", (1, 1)).getbands(), ("I",)) - self.assertEqual(Image.new("F", (1, 1)).getbands(), ("F",)) - self.assertEqual(Image.new("P", (1, 1)).getbands(), ("P",)) - self.assertEqual(Image.new("RGB", (1, 1)).getbands(), ("R", "G", "B")) - self.assertEqual(Image.new("RGBA", (1, 1)).getbands(), ("R", "G", "B", "A")) - self.assertEqual(Image.new("CMYK", (1, 1)).getbands(), ("C", "M", "Y", "K")) - self.assertEqual(Image.new("YCbCr", (1, 1)).getbands(), ("Y", "Cb", "Cr")) +def test_getbands(): + assert Image.new("1", (1, 1)).getbands() == ("1",) + assert Image.new("L", (1, 1)).getbands() == ("L",) + assert Image.new("I", (1, 1)).getbands() == ("I",) + assert Image.new("F", (1, 1)).getbands() == ("F",) + assert Image.new("P", (1, 1)).getbands() == ("P",) + assert Image.new("RGB", (1, 1)).getbands() == ("R", "G", "B") + assert Image.new("RGBA", (1, 1)).getbands() == ("R", "G", "B", "A") + assert Image.new("CMYK", (1, 1)).getbands() == ("C", "M", "Y", "K") + assert Image.new("YCbCr", (1, 1)).getbands() == ("Y", "Cb", "Cr") diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index 2df9f20f1..c86e33eb2 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,38 +1,41 @@ from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageGetBbox(PillowTestCase): - def test_sanity(self): +def test_sanity(): - bbox = hopper().getbbox() - self.assertIsInstance(bbox, tuple) + bbox = hopper().getbbox() + assert isinstance(bbox, tuple) - def test_bbox(self): - # 8-bit mode - im = Image.new("L", (100, 100), 0) - self.assertIsNone(im.getbbox()) +def test_bbox(): + def check(im, fill_color): + assert im.getbbox() is None - im.paste(255, (10, 25, 90, 75)) - self.assertEqual(im.getbbox(), (10, 25, 90, 75)) + im.paste(fill_color, (10, 25, 90, 75)) + assert im.getbbox() == (10, 25, 90, 75) - im.paste(255, (25, 10, 75, 90)) - self.assertEqual(im.getbbox(), (10, 10, 90, 90)) + im.paste(fill_color, (25, 10, 75, 90)) + assert im.getbbox() == (10, 10, 90, 90) - im.paste(255, (-10, -10, 110, 110)) - self.assertEqual(im.getbbox(), (0, 0, 100, 100)) + im.paste(fill_color, (-10, -10, 110, 110)) + assert im.getbbox() == (0, 0, 100, 100) - # 32-bit mode - im = Image.new("RGB", (100, 100), 0) - self.assertIsNone(im.getbbox()) + # 8-bit mode + im = Image.new("L", (100, 100), 0) + check(im, 255) - im.paste(255, (10, 25, 90, 75)) - self.assertEqual(im.getbbox(), (10, 25, 90, 75)) + # 32-bit mode + im = Image.new("RGB", (100, 100), 0) + check(im, 255) - im.paste(255, (25, 10, 75, 90)) - self.assertEqual(im.getbbox(), (10, 10, 90, 90)) + 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) + check(im, (255, 255, 255, 255)) - im.paste(255, (-10, -10, 110, 110)) - self.assertEqual(im.getbbox(), (0, 0, 100, 100)) + for mode in ("La", "LA", "PA"): + for color in ((0, 0), (127, 0), (255, 0)): + im = Image.new(mode, (100, 100), color) + check(im, (255, 255)) diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index f1abf0287..e5b6a7724 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -1,67 +1,68 @@ -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageGetColors(PillowTestCase): - def test_getcolors(self): - def getcolors(mode, limit=None): - im = hopper(mode) - if limit: - colors = im.getcolors(limit) - else: - colors = im.getcolors() - if colors: - return len(colors) - return None +def test_getcolors(): + def getcolors(mode, limit=None): + im = hopper(mode) + if limit: + colors = im.getcolors(limit) + else: + colors = im.getcolors() + if colors: + return len(colors) + return None - self.assertEqual(getcolors("1"), 2) - self.assertEqual(getcolors("L"), 255) - self.assertEqual(getcolors("I"), 255) - self.assertEqual(getcolors("F"), 255) - self.assertEqual(getcolors("P"), 90) # fixed palette - self.assertIsNone(getcolors("RGB")) - self.assertIsNone(getcolors("RGBA")) - self.assertIsNone(getcolors("CMYK")) - self.assertIsNone(getcolors("YCbCr")) + assert getcolors("1") == 2 + assert getcolors("L") == 255 + assert getcolors("I") == 255 + assert getcolors("F") == 255 + assert getcolors("P") == 90 # fixed palette + assert getcolors("RGB") is None + assert getcolors("RGBA") is None + assert getcolors("CMYK") is None + assert getcolors("YCbCr") is None - self.assertIsNone(getcolors("L", 128)) - self.assertEqual(getcolors("L", 1024), 255) + assert getcolors("L", 128) is None + assert getcolors("L", 1024) == 255 - self.assertIsNone(getcolors("RGB", 8192)) - self.assertEqual(getcolors("RGB", 16384), 10100) - self.assertEqual(getcolors("RGB", 100000), 10100) + assert getcolors("RGB", 8192) is None + assert getcolors("RGB", 16384) == 10100 + assert getcolors("RGB", 100000) == 10100 - self.assertEqual(getcolors("RGBA", 16384), 10100) - self.assertEqual(getcolors("CMYK", 16384), 10100) - self.assertEqual(getcolors("YCbCr", 16384), 9329) + assert getcolors("RGBA", 16384) == 10100 + assert getcolors("CMYK", 16384) == 10100 + assert getcolors("YCbCr", 16384) == 9329 - # -------------------------------------------------------------------- - def test_pack(self): - # Pack problems for small tables (@PIL209) +# -------------------------------------------------------------------- - im = hopper().quantize(3).convert("RGB") - expected = [ - (4039, (172, 166, 181)), - (4385, (124, 113, 134)), - (7960, (31, 20, 33)), - ] +def test_pack(): + # Pack problems for small tables (@PIL209) - A = im.getcolors(maxcolors=2) - self.assertIsNone(A) + im = hopper().quantize(3).convert("RGB") - A = im.getcolors(maxcolors=3) - A.sort() - self.assertEqual(A, expected) + expected = [ + (4039, (172, 166, 181)), + (4385, (124, 113, 134)), + (7960, (31, 20, 33)), + ] - A = im.getcolors(maxcolors=4) - A.sort() - self.assertEqual(A, expected) + A = im.getcolors(maxcolors=2) + assert A is None - A = im.getcolors(maxcolors=8) - A.sort() - self.assertEqual(A, expected) + A = im.getcolors(maxcolors=3) + A.sort() + assert A == expected - A = im.getcolors(maxcolors=16) - A.sort() - self.assertEqual(A, expected) + A = im.getcolors(maxcolors=4) + A.sort() + assert A == expected + + A = im.getcolors(maxcolors=8) + A.sort() + assert A == expected + + A = im.getcolors(maxcolors=16) + A.sort() + assert A == expected diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index d9bfcc7dd..159efd78a 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,27 +1,28 @@ -from .helper import PillowTestCase, hopper +from PIL import Image + +from .helper import hopper -class TestImageGetData(PillowTestCase): - def test_sanity(self): +def test_sanity(): + data = hopper().getdata() - data = hopper().getdata() + len(data) + list(data) - len(data) - list(data) + assert data[0] == (20, 20, 70) - self.assertEqual(data[0], (20, 20, 70)) - def test_roundtrip(self): - def getdata(mode): - im = hopper(mode).resize((32, 30)) - data = im.getdata() - return data[0], len(data), len(list(data)) +def test_roundtrip(): + def getdata(mode): + im = hopper(mode).resize((32, 30), Image.NEAREST) + data = im.getdata() + return data[0], len(data), len(list(data)) - self.assertEqual(getdata("1"), (0, 960, 960)) - self.assertEqual(getdata("L"), (16, 960, 960)) - self.assertEqual(getdata("I"), (16, 960, 960)) - self.assertEqual(getdata("F"), (16.0, 960, 960)) - self.assertEqual(getdata("RGB"), ((11, 13, 52), 960, 960)) - self.assertEqual(getdata("RGBA"), ((11, 13, 52, 255), 960, 960)) - self.assertEqual(getdata("CMYK"), ((244, 242, 203, 0), 960, 960)) - self.assertEqual(getdata("YCbCr"), ((16, 147, 123), 960, 960)) + assert getdata("1") == (0, 960, 960) + assert getdata("L") == (17, 960, 960) + assert getdata("I") == (17, 960, 960) + assert getdata("F") == (17.0, 960, 960) + assert getdata("RGB") == ((11, 13, 52), 960, 960) + assert getdata("RGBA") == ((11, 13, 52, 255), 960, 960) + assert getdata("CMYK") == ((244, 242, 203, 0), 960, 960) + assert getdata("YCbCr") == ((16, 147, 123), 960, 960) diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 1944b041c..710794da4 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -1,25 +1,25 @@ from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageGetExtrema(PillowTestCase): - def test_extrema(self): - def extrema(mode): - return hopper(mode).getextrema() +def test_extrema(): + def extrema(mode): + return hopper(mode).getextrema() - self.assertEqual(extrema("1"), (0, 255)) - self.assertEqual(extrema("L"), (0, 255)) - self.assertEqual(extrema("I"), (0, 255)) - self.assertEqual(extrema("F"), (0, 255)) - self.assertEqual(extrema("P"), (0, 225)) # fixed palette - self.assertEqual(extrema("RGB"), ((0, 255), (0, 255), (0, 255))) - self.assertEqual(extrema("RGBA"), ((0, 255), (0, 255), (0, 255), (255, 255))) - self.assertEqual(extrema("CMYK"), ((0, 255), (0, 255), (0, 255), (0, 0))) - self.assertEqual(extrema("I;16"), (0, 255)) + assert extrema("1") == (0, 255) + assert extrema("L") == (1, 255) + assert extrema("I") == (1, 255) + assert extrema("F") == (1, 255) + assert extrema("P") == (0, 225) # fixed palette + assert extrema("RGB") == ((0, 255), (0, 255), (0, 255)) + assert extrema("RGBA") == ((0, 255), (0, 255), (0, 255), (255, 255)) + assert extrema("CMYK") == ((0, 255), (0, 255), (0, 255), (0, 0)) + assert extrema("I;16") == (1, 255) - def test_true_16(self): - im = Image.open("Tests/images/16_bit_noise.tif") - self.assertEqual(im.mode, "I;16") + +def test_true_16(): + with Image.open("Tests/images/16_bit_noise.tif") as im: + assert im.mode == "I;16" extrema = im.getextrema() - self.assertEqual(extrema, (106, 285)) + assert extrema == (106, 285) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index f6908af4b..746e63b15 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,10 +1,9 @@ -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageGetIm(PillowTestCase): - def test_sanity(self): - im = hopper() - type_repr = repr(type(im.getim())) +def test_sanity(): + im = hopper() + type_repr = repr(type(im.getim())) - self.assertIn("PyCapsule", type_repr) - self.assertIsInstance(im.im.id, int) + assert "PyCapsule" in type_repr + assert isinstance(im.im.id, int) diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 7beeeff58..1818adca2 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -1,20 +1,19 @@ -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageGetPalette(PillowTestCase): - def test_palette(self): - def palette(mode): - p = hopper(mode).getpalette() - if p: - return p[:10] - return None +def test_palette(): + def palette(mode): + p = hopper(mode).getpalette() + if p: + return p[:10] + return None - self.assertIsNone(palette("1")) - self.assertIsNone(palette("L")) - self.assertIsNone(palette("I")) - self.assertIsNone(palette("F")) - self.assertEqual(palette("P"), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - self.assertIsNone(palette("RGB")) - self.assertIsNone(palette("RGBA")) - self.assertIsNone(palette("CMYK")) - self.assertIsNone(palette("YCbCr")) + assert palette("1") is None + assert palette("L") is None + assert palette("I") is None + assert palette("F") is None + assert palette("P") == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + assert palette("RGB") is None + assert palette("RGBA") is None + assert palette("CMYK") is None + assert palette("YCbCr") is None diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index 3b8bca64f..f65d40708 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -1,31 +1,29 @@ from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageGetProjection(PillowTestCase): - def test_sanity(self): +def test_sanity(): + im = hopper() - im = hopper() + projection = im.getprojection() - projection = im.getprojection() + assert len(projection) == 2 + assert len(projection[0]) == im.size[0] + assert len(projection[1]) == im.size[1] - self.assertEqual(len(projection), 2) - self.assertEqual(len(projection[0]), im.size[0]) - self.assertEqual(len(projection[1]), im.size[1]) + # 8-bit image + im = Image.new("L", (10, 10)) + assert im.getprojection()[0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + assert im.getprojection()[1] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + im.paste(255, (2, 4, 8, 6)) + assert im.getprojection()[0] == [0, 0, 1, 1, 1, 1, 1, 1, 0, 0] + assert im.getprojection()[1] == [0, 0, 0, 0, 1, 1, 0, 0, 0, 0] - # 8-bit image - im = Image.new("L", (10, 10)) - self.assertEqual(im.getprojection()[0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - self.assertEqual(im.getprojection()[1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - im.paste(255, (2, 4, 8, 6)) - self.assertEqual(im.getprojection()[0], [0, 0, 1, 1, 1, 1, 1, 1, 0, 0]) - self.assertEqual(im.getprojection()[1], [0, 0, 0, 0, 1, 1, 0, 0, 0, 0]) - - # 32-bit image - im = Image.new("RGB", (10, 10)) - self.assertEqual(im.getprojection()[0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - self.assertEqual(im.getprojection()[1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - im.paste(255, (2, 4, 8, 6)) - self.assertEqual(im.getprojection()[0], [0, 0, 1, 1, 1, 1, 1, 1, 0, 0]) - self.assertEqual(im.getprojection()[1], [0, 0, 0, 0, 1, 1, 0, 0, 0, 0]) + # 32-bit image + im = Image.new("RGB", (10, 10)) + assert im.getprojection()[0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + assert im.getprojection()[1] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + im.paste(255, (2, 4, 8, 6)) + assert im.getprojection()[0] == [0, 0, 1, 1, 1, 1, 1, 1, 0, 0] + assert im.getprojection()[1] == [0, 0, 0, 0, 1, 1, 0, 0, 0, 0] diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 8d34658b8..91e02973d 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -1,18 +1,17 @@ -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageHistogram(PillowTestCase): - def test_histogram(self): - def histogram(mode): - h = hopper(mode).histogram() - return len(h), min(h), max(h) +def test_histogram(): + def histogram(mode): + h = hopper(mode).histogram() + return len(h), min(h), max(h) - self.assertEqual(histogram("1"), (256, 0, 10994)) - self.assertEqual(histogram("L"), (256, 0, 638)) - self.assertEqual(histogram("I"), (256, 0, 638)) - self.assertEqual(histogram("F"), (256, 0, 638)) - self.assertEqual(histogram("P"), (256, 0, 1871)) - self.assertEqual(histogram("RGB"), (768, 4, 675)) - self.assertEqual(histogram("RGBA"), (1024, 0, 16384)) - self.assertEqual(histogram("CMYK"), (1024, 0, 16384)) - self.assertEqual(histogram("YCbCr"), (768, 0, 1908)) + assert histogram("1") == (256, 0, 10994) + assert histogram("L") == (256, 0, 662) + assert histogram("I") == (256, 0, 662) + assert histogram("F") == (256, 0, 662) + assert histogram("P") == (256, 0, 1871) + assert histogram("RGB") == (768, 4, 675) + assert histogram("RGBA") == (1024, 0, 16384) + assert histogram("CMYK") == (1024, 0, 16384) + assert histogram("YCbCr") == (768, 0, 1908) diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 2770126c4..f7fe99bb4 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,36 +1,50 @@ +import logging import os +import pytest + from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageLoad(PillowTestCase): - def test_sanity(self): +def test_sanity(): + im = hopper() + pix = im.load() - im = hopper() + assert pix[0, 0] == (20, 20, 70) - pix = im.load() - self.assertEqual(pix[0, 0], (20, 20, 70)) +def test_close(): + im = Image.open("Tests/images/hopper.gif") + im.close() + with pytest.raises(ValueError): + im.load() + with pytest.raises(ValueError): + im.getpixel((0, 0)) - def test_close(self): - im = Image.open("Tests/images/hopper.gif") + +def test_close_after_load(caplog): + im = Image.open("Tests/images/hopper.gif") + im.load() + with caplog.at_level(logging.DEBUG): im.close() - self.assertRaises(ValueError, im.load) - self.assertRaises(ValueError, im.getpixel, (0, 0)) + assert len(caplog.records) == 0 - def test_contextmanager(self): - fn = None - with Image.open("Tests/images/hopper.gif") as im: - fn = im.fp.fileno() - os.fstat(fn) - self.assertRaises(OSError, os.fstat, fn) +def test_contextmanager(): + fn = None + with Image.open("Tests/images/hopper.gif") as im: + fn = im.fp.fileno() + os.fstat(fn) - def test_contextmanager_non_exclusive_fp(self): - with open("Tests/images/hopper.gif", "rb") as fp: - with Image.open(fp): - pass + with pytest.raises(OSError): + os.fstat(fn) - self.assertFalse(fp.closed) + +def test_contextmanager_non_exclusive_fp(): + with open("Tests/images/hopper.gif", "rb") as fp: + with Image.open(fp): + pass + + assert not fp.closed diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 1d41d8609..7f92c2264 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,72 +1,70 @@ -from PIL import Image +from PIL import Image, ImageMode -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageMode(PillowTestCase): - def test_sanity(self): +def test_sanity(): - with hopper() as im: - im.mode + with hopper() as im: + im.mode - from PIL import ImageMode + ImageMode.getmode("1") + ImageMode.getmode("L") + ImageMode.getmode("P") + ImageMode.getmode("RGB") + ImageMode.getmode("I") + ImageMode.getmode("F") - ImageMode.getmode("1") - ImageMode.getmode("L") - ImageMode.getmode("P") - ImageMode.getmode("RGB") - ImageMode.getmode("I") - ImageMode.getmode("F") + m = ImageMode.getmode("1") + assert m.mode == "1" + assert str(m) == "1" + assert m.bands == ("1",) + assert m.basemode == "L" + assert m.basetype == "L" - m = ImageMode.getmode("1") - self.assertEqual(m.mode, "1") - self.assertEqual(str(m), "1") - self.assertEqual(m.bands, ("1",)) - self.assertEqual(m.basemode, "L") - self.assertEqual(m.basetype, "L") + for mode in ( + "I;16", + "I;16S", + "I;16L", + "I;16LS", + "I;16B", + "I;16BS", + "I;16N", + "I;16NS", + ): + m = ImageMode.getmode(mode) + assert m.mode == mode + assert str(m) == mode + assert m.bands == ("I",) + assert m.basemode == "L" + assert m.basetype == "L" - for mode in ( - "I;16", - "I;16S", - "I;16L", - "I;16LS", - "I;16B", - "I;16BS", - "I;16N", - "I;16NS", - ): - m = ImageMode.getmode(mode) - self.assertEqual(m.mode, mode) - self.assertEqual(str(m), mode) - self.assertEqual(m.bands, ("I",)) - self.assertEqual(m.basemode, "L") - self.assertEqual(m.basetype, "L") + m = ImageMode.getmode("RGB") + assert m.mode == "RGB" + assert str(m) == "RGB" + assert m.bands == ("R", "G", "B") + assert m.basemode == "RGB" + assert m.basetype == "L" - m = ImageMode.getmode("RGB") - self.assertEqual(m.mode, "RGB") - self.assertEqual(str(m), "RGB") - self.assertEqual(m.bands, ("R", "G", "B")) - self.assertEqual(m.basemode, "RGB") - self.assertEqual(m.basetype, "L") - def test_properties(self): - def check(mode, *result): - signature = ( - Image.getmodebase(mode), - Image.getmodetype(mode), - Image.getmodebands(mode), - Image.getmodebandnames(mode), - ) - self.assertEqual(signature, result) +def test_properties(): + def check(mode, *result): + signature = ( + Image.getmodebase(mode), + Image.getmodetype(mode), + Image.getmodebands(mode), + Image.getmodebandnames(mode), + ) + assert signature == result - check("1", "L", "L", 1, ("1",)) - check("L", "L", "L", 1, ("L",)) - check("P", "P", "L", 1, ("P",)) - check("I", "L", "I", 1, ("I",)) - check("F", "L", "F", 1, ("F",)) - check("RGB", "RGB", "L", 3, ("R", "G", "B")) - check("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")) - check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) - check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) - check("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")) - check("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")) + check("1", "L", "L", 1, ("1",)) + check("L", "L", "L", 1, ("L",)) + check("P", "P", "L", 1, ("P",)) + check("I", "L", "I", 1, ("I",)) + check("F", "L", "F", 1, ("F",)) + check("RGB", "RGB", "L", 3, ("R", "G", "B")) + check("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")) + check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) + check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) + check("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")) + check("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 3139db664..3740fbcdc 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,9 +1,9 @@ from PIL import Image -from .helper import PillowTestCase, cached_property +from .helper import assert_image_equal, cached_property -class TestImagingPaste(PillowTestCase): +class TestImagingPaste: masks = {} size = 128 @@ -23,7 +23,7 @@ class TestImagingPaste(PillowTestCase): px[self.size // 2, self.size - 1], px[self.size - 1, self.size - 1], ] - self.assertEqual(actual, expected) + assert actual == expected def assert_9points_paste(self, im, im2, mask, expected): im3 = im.copy() @@ -99,7 +99,7 @@ class TestImagingPaste(PillowTestCase): im.paste(im2, (12, 23)) im = im.crop((12, 23, im2.width + 12, im2.height + 23)) - self.assert_image_equal(im, im2) + assert_image_equal(im, im2) def test_image_mask_1(self): for mode in ("RGBA", "RGB", "L"): @@ -199,8 +199,8 @@ class TestImagingPaste(PillowTestCase): hist = im.crop(rect).histogram() while hist: head, hist = hist[:256], hist[256:] - self.assertEqual(head[255], 128 * 128) - self.assertEqual(sum(head[:255]), 0) + assert head[255] == 128 * 128 + assert sum(head[:255]) == 0 def test_color_mask_1(self): for mode in ("RGBA", "RGB", "L"): @@ -236,7 +236,7 @@ class TestImagingPaste(PillowTestCase): [ (127, 191, 254, 191), (111, 207, 206, 110), - (127, 254, 127, 0), + (255, 255, 255, 0) if mode == "RGBA" else (127, 254, 127, 0), (207, 207, 239, 239), (191, 191, 190, 191), (207, 206, 111, 112), diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 56ed46488..51108ead2 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,39 +1,48 @@ -from .helper import PillowTestCase, hopper +import pytest + +from .helper import assert_image_equal, hopper -class TestImagePoint(PillowTestCase): - def test_sanity(self): - im = hopper() +def test_sanity(): + im = hopper() - self.assertRaises(ValueError, im.point, list(range(256))) - im.point(list(range(256)) * 3) - im.point(lambda x: x) + with pytest.raises(ValueError): + im.point(list(range(256))) + im.point(list(range(256)) * 3) + im.point(lambda x: x) - im = im.convert("I") - self.assertRaises(ValueError, im.point, list(range(256))) - im.point(lambda x: x * 1) - im.point(lambda x: x + 1) - im.point(lambda x: x * 1 + 1) - self.assertRaises(TypeError, im.point, lambda x: x - 1) - self.assertRaises(TypeError, im.point, lambda x: x / 1) + im = im.convert("I") + with pytest.raises(ValueError): + im.point(list(range(256))) + im.point(lambda x: x * 1) + im.point(lambda x: x + 1) + im.point(lambda x: x * 1 + 1) + with pytest.raises(TypeError): + im.point(lambda x: x - 1) + with pytest.raises(TypeError): + im.point(lambda x: x / 1) - def test_16bit_lut(self): - """ Tests for 16 bit -> 8 bit lut for converting I->L images - see https://github.com/python-pillow/Pillow/issues/440 - """ - im = hopper("I") - im.point(list(range(256)) * 256, "L") - def test_f_lut(self): - """ Tests for floating point lut of 8bit gray image """ - im = hopper("L") - lut = [0.5 * float(x) for x in range(256)] +def test_16bit_lut(): + """Tests for 16 bit -> 8 bit lut for converting I->L images + see https://github.com/python-pillow/Pillow/issues/440 + """ + im = hopper("I") + im.point(list(range(256)) * 256, "L") - out = im.point(lut, "F") - int_lut = [x // 2 for x in range(256)] - self.assert_image_equal(out.convert("L"), im.point(int_lut, "L")) +def test_f_lut(): + """ Tests for floating point lut of 8bit gray image """ + im = hopper("L") + lut = [0.5 * float(x) for x in range(256)] - def test_f_mode(self): - im = hopper("F") - self.assertRaises(ValueError, im.point, None) + out = im.point(lut, "F") + + int_lut = [x // 2 for x in range(256)] + assert_image_equal(out.convert("L"), im.point(int_lut, "L")) + + +def test_f_mode(): + 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 6dc802598..e2dcead34 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -1,52 +1,48 @@ from PIL import Image -from .helper import PillowTestCase + +def test_interface(): + im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) + assert im.getpixel((0, 0)) == (1, 2, 3, 0) + + im = Image.new("RGBA", (1, 1), (1, 2, 3)) + assert im.getpixel((0, 0)) == (1, 2, 3, 255) + + im.putalpha(Image.new("L", im.size, 4)) + assert im.getpixel((0, 0)) == (1, 2, 3, 4) + + im.putalpha(5) + assert im.getpixel((0, 0)) == (1, 2, 3, 5) -class TestImagePutAlpha(PillowTestCase): - def test_interface(self): +def test_promote(): + im = Image.new("L", (1, 1), 1) + assert im.getpixel((0, 0)) == 1 - im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 0)) + im.putalpha(2) + assert im.mode == "LA" + assert im.getpixel((0, 0)) == (1, 2) - im = Image.new("RGBA", (1, 1), (1, 2, 3)) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 255)) + im = Image.new("P", (1, 1), 1) + assert im.getpixel((0, 0)) == 1 - im.putalpha(Image.new("L", im.size, 4)) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) + im.putalpha(2) + assert im.mode == "PA" + assert im.getpixel((0, 0)) == (1, 2) - im.putalpha(5) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 5)) + im = Image.new("RGB", (1, 1), (1, 2, 3)) + assert im.getpixel((0, 0)) == (1, 2, 3) - def test_promote(self): + im.putalpha(4) + assert im.mode == "RGBA" + assert im.getpixel((0, 0)) == (1, 2, 3, 4) - im = Image.new("L", (1, 1), 1) - self.assertEqual(im.getpixel((0, 0)), 1) - im.putalpha(2) - self.assertEqual(im.mode, "LA") - self.assertEqual(im.getpixel((0, 0)), (1, 2)) +def test_readonly(): + im = Image.new("RGB", (1, 1), (1, 2, 3)) + im.readonly = 1 - im = Image.new("P", (1, 1), 1) - self.assertEqual(im.getpixel((0, 0)), 1) - - im.putalpha(2) - self.assertEqual(im.mode, "PA") - self.assertEqual(im.getpixel((0, 0)), (1, 2)) - - im = Image.new("RGB", (1, 1), (1, 2, 3)) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3)) - - im.putalpha(4) - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) - - def test_readonly(self): - - im = Image.new("RGB", (1, 1), (1, 2, 3)) - im.readonly = 1 - - im.putalpha(4) - self.assertFalse(im.readonly) - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) + im.putalpha(4) + assert not im.readonly + assert im.mode == "RGBA" + assert im.getpixel((0, 0)) == (1, 2, 3, 4) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index a213fbf88..54712fd6c 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -3,83 +3,87 @@ from array import array from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper -class TestImagePutData(PillowTestCase): - def test_sanity(self): +def test_sanity(): + im1 = hopper() - im1 = hopper() + data = list(im1.getdata()) - data = list(im1.getdata()) + im2 = Image.new(im1.mode, im1.size, 0) + im2.putdata(data) - im2 = Image.new(im1.mode, im1.size, 0) - im2.putdata(data) + assert_image_equal(im1, im2) - self.assert_image_equal(im1, im2) + # readonly + im2 = Image.new(im1.mode, im2.size, 0) + im2.readonly = 1 + im2.putdata(data) - # readonly - im2 = Image.new(im1.mode, im2.size, 0) - im2.readonly = 1 - im2.putdata(data) + assert not im2.readonly + assert_image_equal(im1, im2) - self.assertFalse(im2.readonly) - self.assert_image_equal(im1, im2) - def test_long_integers(self): - # see bug-200802-systemerror - def put(value): - im = Image.new("RGBA", (1, 1)) - im.putdata([value]) - return im.getpixel((0, 0)) +def test_long_integers(): + # see bug-200802-systemerror + def put(value): + im = Image.new("RGBA", (1, 1)) + im.putdata([value]) + return im.getpixel((0, 0)) - self.assertEqual(put(0xFFFFFFFF), (255, 255, 255, 255)) - self.assertEqual(put(0xFFFFFFFF), (255, 255, 255, 255)) - self.assertEqual(put(-1), (255, 255, 255, 255)) - self.assertEqual(put(-1), (255, 255, 255, 255)) - if sys.maxsize > 2 ** 32: - self.assertEqual(put(sys.maxsize), (255, 255, 255, 255)) - else: - self.assertEqual(put(sys.maxsize), (255, 255, 255, 127)) + assert put(0xFFFFFFFF) == (255, 255, 255, 255) + assert put(0xFFFFFFFF) == (255, 255, 255, 255) + assert put(-1) == (255, 255, 255, 255) + assert put(-1) == (255, 255, 255, 255) + if sys.maxsize > 2 ** 32: + assert put(sys.maxsize) == (255, 255, 255, 255) + else: + assert put(sys.maxsize) == (255, 255, 255, 127) - def test_pypy_performance(self): - im = Image.new("L", (256, 256)) - im.putdata(list(range(256)) * 256) - def test_mode_i(self): - src = hopper("L") - data = list(src.getdata()) - im = Image.new("I", src.size, 0) - im.putdata(data, 2, 256) +def test_pypy_performance(): + im = Image.new("L", (256, 256)) + im.putdata(list(range(256)) * 256) - target = [2 * elt + 256 for elt in data] - self.assertEqual(list(im.getdata()), target) - def test_mode_F(self): - src = hopper("L") - data = list(src.getdata()) - im = Image.new("F", src.size, 0) - im.putdata(data, 2.0, 256.0) +def test_mode_i(): + src = hopper("L") + data = list(src.getdata()) + im = Image.new("I", src.size, 0) + im.putdata(data, 2, 256) - target = [2.0 * float(elt) + 256.0 for elt in data] - self.assertEqual(list(im.getdata()), target) + target = [2 * elt + 256 for elt in data] + assert list(im.getdata()) == target - def test_array_B(self): - # shouldn't segfault - # see https://github.com/python-pillow/Pillow/issues/1008 - arr = array("B", [0]) * 15000 - im = Image.new("L", (150, 100)) - im.putdata(arr) +def test_mode_F(): + src = hopper("L") + data = list(src.getdata()) + im = Image.new("F", src.size, 0) + im.putdata(data, 2.0, 256.0) - self.assertEqual(len(im.getdata()), len(arr)) + target = [2.0 * float(elt) + 256.0 for elt in data] + assert list(im.getdata()) == target - def test_array_F(self): - # shouldn't segfault - # see https://github.com/python-pillow/Pillow/issues/1008 - im = Image.new("F", (150, 100)) - arr = array("f", [0.0]) * 15000 - im.putdata(arr) +def test_array_B(): + # shouldn't segfault + # see https://github.com/python-pillow/Pillow/issues/1008 - self.assertEqual(len(im.getdata()), len(arr)) + arr = array("B", [0]) * 15000 + im = Image.new("L", (150, 100)) + im.putdata(arr) + + assert len(im.getdata()) == len(arr) + + +def test_array_F(): + # shouldn't segfault + # see https://github.com/python-pillow/Pillow/issues/1008 + + im = Image.new("F", (150, 100)) + arr = array("f", [0.0]) * 15000 + im.putdata(arr) + + assert len(im.getdata()) == len(arr) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 68cfc4efe..32f8de2c0 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,33 +1,41 @@ +import pytest + from PIL import ImagePalette -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImagePutPalette(PillowTestCase): - def test_putpalette(self): - def palette(mode): - im = hopper(mode).copy() - im.putpalette(list(range(256)) * 3) - p = im.getpalette() - if p: - return im.mode, p[:10] - return im.mode +def test_putpalette(): + def palette(mode): + im = hopper(mode).copy() + im.putpalette(list(range(256)) * 3) + p = im.getpalette() + if p: + return im.mode, p[:10] + return im.mode - self.assertRaises(ValueError, palette, "1") - for mode in ["L", "LA", "P", "PA"]: - self.assertEqual( - palette(mode), - ("PA" if "A" in mode else "P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), - ) - self.assertRaises(ValueError, palette, "I") - self.assertRaises(ValueError, palette, "F") - self.assertRaises(ValueError, palette, "RGB") - self.assertRaises(ValueError, palette, "RGBA") - self.assertRaises(ValueError, palette, "YCbCr") + with pytest.raises(ValueError): + palette("1") + for mode in ["L", "LA", "P", "PA"]: + assert palette(mode) == ( + "PA" if "A" in mode else "P", + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + ) + with pytest.raises(ValueError): + palette("I") + with pytest.raises(ValueError): + palette("F") + with pytest.raises(ValueError): + palette("RGB") + with pytest.raises(ValueError): + palette("RGBA") + with pytest.raises(ValueError): + palette("YCbCr") - def test_imagepalette(self): - im = hopper("P") - im.putpalette(ImagePalette.negative()) - im.putpalette(ImagePalette.random()) - im.putpalette(ImagePalette.sepia()) - im.putpalette(ImagePalette.wedge()) + +def test_imagepalette(): + im = hopper("P") + im.putpalette(ImagePalette.negative()) + im.putpalette(ImagePalette.random()) + im.putpalette(ImagePalette.sepia()) + im.putpalette(ImagePalette.wedge()) diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 2be5b74fc..192617a52 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,64 +1,75 @@ +import pytest + from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image, assert_image_similar, hopper -class TestImageQuantize(PillowTestCase): - def test_sanity(self): - image = hopper() - converted = image.quantize() - self.assert_image(converted, "P", converted.size) - self.assert_image_similar(converted.convert("RGB"), image, 10) +def test_sanity(): + image = hopper() + converted = image.quantize() + assert_image(converted, "P", converted.size) + assert_image_similar(converted.convert("RGB"), image, 10) - image = hopper() - converted = image.quantize(palette=hopper("P")) - self.assert_image(converted, "P", converted.size) - self.assert_image_similar(converted.convert("RGB"), image, 60) + image = hopper() + converted = image.quantize(palette=hopper("P")) + assert_image(converted, "P", converted.size) + assert_image_similar(converted.convert("RGB"), image, 60) - def test_libimagequant_quantize(self): - image = hopper() - try: - converted = image.quantize(100, Image.LIBIMAGEQUANT) - except ValueError as ex: - if "dependency" in str(ex).lower(): - self.skipTest("libimagequant support not available") - else: - raise - self.assert_image(converted, "P", converted.size) - self.assert_image_similar(converted.convert("RGB"), image, 15) - self.assertEqual(len(converted.getcolors()), 100) - def test_octree_quantize(self): - image = hopper() - converted = image.quantize(100, Image.FASTOCTREE) - self.assert_image(converted, "P", converted.size) - self.assert_image_similar(converted.convert("RGB"), image, 20) - self.assertEqual(len(converted.getcolors()), 100) +def test_libimagequant_quantize(): + image = hopper() + try: + converted = image.quantize(100, Image.LIBIMAGEQUANT) + except ValueError as ex: + if "dependency" in str(ex).lower(): + pytest.skip("libimagequant support not available") + else: + raise + assert_image(converted, "P", converted.size) + assert_image_similar(converted.convert("RGB"), image, 15) + assert len(converted.getcolors()) == 100 - def test_rgba_quantize(self): - image = hopper("RGBA") - self.assertRaises(ValueError, image.quantize, method=0) - self.assertEqual(image.quantize().convert().mode, "RGBA") +def test_octree_quantize(): + image = hopper() + converted = image.quantize(100, Image.FASTOCTREE) + assert_image(converted, "P", converted.size) + assert_image_similar(converted.convert("RGB"), image, 20) + assert len(converted.getcolors()) == 100 - def test_quantize(self): - image = Image.open("Tests/images/caption_6_33_22.png").convert("RGB") - converted = image.quantize() - self.assert_image(converted, "P", converted.size) - self.assert_image_similar(converted.convert("RGB"), image, 1) - def test_quantize_no_dither(self): - image = hopper() - palette = Image.open("Tests/images/caption_6_33_22.png").convert("P") +def test_rgba_quantize(): + image = hopper("RGBA") + with pytest.raises(ValueError): + image.quantize(method=0) - converted = image.quantize(dither=0, palette=palette) - self.assert_image(converted, "P", converted.size) + assert image.quantize().convert().mode == "RGBA" - def test_quantize_dither_diff(self): - image = hopper() - palette = Image.open("Tests/images/caption_6_33_22.png").convert("P") - dither = image.quantize(dither=1, palette=palette) - nodither = image.quantize(dither=0, palette=palette) +def test_quantize(): + with Image.open("Tests/images/caption_6_33_22.png") as image: + image = image.convert("RGB") + converted = image.quantize() + assert_image(converted, "P", converted.size) + assert_image_similar(converted.convert("RGB"), image, 1) - self.assertNotEqual(dither.tobytes(), nodither.tobytes()) + +def test_quantize_no_dither(): + image = hopper() + with Image.open("Tests/images/caption_6_33_22.png") as palette: + palette = palette.convert("P") + + converted = image.quantize(dither=0, palette=palette) + assert_image(converted, "P", converted.size) + + +def test_quantize_dither_diff(): + image = hopper() + with Image.open("Tests/images/caption_6_33_22.png") as palette: + palette = palette.convert("P") + + dither = image.quantize(dither=1, palette=palette) + nodither = image.quantize(dither=0, palette=palette) + + assert dither.tobytes() != nodither.tobytes() diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py new file mode 100644 index 000000000..b4eebc142 --- /dev/null +++ b/Tests/test_image_reduce.py @@ -0,0 +1,260 @@ +import pytest + +from PIL import Image, ImageMath, ImageMode + +from .helper import convert_to_comparable, skip_unless_feature + +codecs = dir(Image.core) + + +# There are several internal implementations +remarkable_factors = [ + # special implementations + 1, + 2, + 3, + 4, + 5, + 6, + # 1xN implementation + (1, 2), + (1, 3), + (1, 4), + (1, 7), + # Nx1 implementation + (2, 1), + (3, 1), + (4, 1), + (7, 1), + # general implementation with different paths + (4, 6), + (5, 6), + (4, 7), + (5, 7), + (19, 17), +] + +gradients_image = Image.open("Tests/images/radial_gradients.png") +gradients_image.load() + + +def test_args_factor(): + im = Image.new("L", (10, 10)) + + assert (4, 4) == im.reduce(3).size + assert (4, 10) == im.reduce((3, 1)).size + assert (10, 4) == im.reduce((1, 3)).size + + with pytest.raises(ValueError): + im.reduce(0) + with pytest.raises(TypeError): + im.reduce(2.0) + with pytest.raises(ValueError): + im.reduce((0, 10)) + + +def test_args_box(): + im = Image.new("L", (10, 10)) + + assert (5, 5) == im.reduce(2, (0, 0, 10, 10)).size + assert (1, 1) == im.reduce(2, (5, 5, 6, 6)).size + + with pytest.raises(TypeError): + im.reduce(2, "stri") + with pytest.raises(TypeError): + im.reduce(2, 2) + with pytest.raises(ValueError): + im.reduce(2, (0, 0, 11, 10)) + with pytest.raises(ValueError): + im.reduce(2, (0, 0, 10, 11)) + with pytest.raises(ValueError): + im.reduce(2, (-1, 0, 10, 10)) + with pytest.raises(ValueError): + im.reduce(2, (0, -1, 10, 10)) + with pytest.raises(ValueError): + im.reduce(2, (0, 5, 10, 5)) + with pytest.raises(ValueError): + im.reduce(2, (5, 0, 5, 10)) + + +def test_unsupported_modes(): + im = Image.new("P", (10, 10)) + with pytest.raises(ValueError): + im.reduce(3) + + im = Image.new("1", (10, 10)) + with pytest.raises(ValueError): + im.reduce(3) + + im = Image.new("I;16", (10, 10)) + with pytest.raises(ValueError): + im.reduce(3) + + +def get_image(mode): + mode_info = ImageMode.getmode(mode) + if mode_info.basetype == "L": + bands = [gradients_image] + for _ in mode_info.bands[1:]: + # rotate previous image + band = bands[-1].transpose(Image.ROTATE_90) + bands.append(band) + # Correct alpha channel by transforming completely transparent pixels. + # Low alpha values also emphasize error after alpha multiplication. + if mode.endswith("A"): + bands[-1] = bands[-1].point(lambda x: int(85 + x / 1.5)) + im = Image.merge(mode, bands) + else: + assert len(mode_info.bands) == 1 + im = gradients_image.convert(mode) + # change the height to make a not-square image + return im.crop((0, 0, im.width, im.height - 5)) + + +def compare_reduce_with_box(im, factor): + 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): + """Image.reduce() should look very similar to Image.resize(BOX). + + A reference image is compiled from a large source area + and possible last column and last row. + +-----------+ + |..........c| + |..........c| + |..........c| + |rrrrrrrrrrp| + +-----------+ + """ + reduced = im.reduce(factor) + + if not isinstance(factor, (list, tuple)): + factor = (factor, factor) + + reference = Image.new(im.mode, reduced.size) + area_size = (im.size[0] // factor[0], im.size[1] // factor[1]) + area_box = (0, 0, area_size[0] * factor[0], area_size[1] * factor[1]) + area = im.resize(area_size, Image.BOX, area_box) + reference.paste(area, (0, 0)) + + if area_size[0] < reduced.size[0]: + assert reduced.size[0] - area_size[0] == 1 + last_column_box = (area_box[2], 0, im.size[0], area_box[3]) + last_column = im.resize((1, area_size[1]), Image.BOX, last_column_box) + reference.paste(last_column, (area_size[0], 0)) + + if area_size[1] < reduced.size[1]: + assert reduced.size[1] - area_size[1] == 1 + last_row_box = (0, area_box[3], area_box[2], im.size[1]) + last_row = im.resize((area_size[0], 1), Image.BOX, last_row_box) + reference.paste(last_row, (0, area_size[1])) + + if area_size[0] < reduced.size[0] and area_size[1] < reduced.size[1]: + last_pixel_box = (area_box[2], area_box[3], im.size[0], im.size[1]) + last_pixel = im.resize((1, 1), Image.BOX, last_pixel_box) + reference.paste(last_pixel, area_size) + + assert_compare_images(reduced, reference, average_diff, max_diff) + + +def assert_compare_images(a, b, max_average_diff, max_diff=255): + 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)}" + + a, b = convert_to_comparable(a, b) + + bands = ImageMode.getmode(a.mode).bands + for band, ach, bch in zip(bands, a.split(), b.split()): + ch_diff = ImageMath.eval("convert(abs(a - b), 'L')", a=ach, b=bch) + ch_hist = ch_diff.histogram() + + average_diff = sum(i * num for i, num in enumerate(ch_hist)) / ( + a.size[0] * a.size[1] + ) + msg = ( + f"average pixel value difference {average_diff:.4f} > " + f"expected {max_average_diff:.4f} for '{band}' band" + ) + assert max_average_diff >= average_diff, msg + + last_diff = [i for i, num in enumerate(ch_hist) if num > 0][-1] + assert max_diff >= last_diff, ( + f"max pixel value difference {last_diff} > expected {max_diff} " + f"for '{band}' band" + ) + + +def test_mode_L(): + im = get_image("L") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_LA(): + im = get_image("LA") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor, 0.8, 5) + + # With opaque alpha, an error should be way smaller. + im.putalpha(Image.new("L", im.size, 255)) + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_La(): + im = get_image("La") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_RGB(): + im = get_image("RGB") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_RGBA(): + im = get_image("RGBA") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor, 0.8, 5) + + # With opaque alpha, an error should be way smaller. + im.putalpha(Image.new("L", im.size, 255)) + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_RGBa(): + im = get_image("RGBa") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_I(): + im = get_image("I") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_F(): + im = get_image("F") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor, 0, 0) + compare_reduce_with_box(im, factor) + + +@skip_unless_feature("jpg_2000") +def test_jpeg2k(): + 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 00c8fa5f2..ef4ca4101 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,12 +1,13 @@ -import unittest from contextlib import contextmanager +import pytest + from PIL import Image, ImageDraw -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, assert_image_similar, hopper -class TestImagingResampleVulnerability(PillowTestCase): +class TestImagingResampleVulnerability: # see https://github.com/python-pillow/Pillow/issues/1710 def test_overflow(self): im = hopper("L") @@ -16,7 +17,7 @@ class TestImagingResampleVulnerability(PillowTestCase): (size_too_large, size_normal), (size_normal, size_too_large), ): - with self.assertRaises(MemoryError): + with pytest.raises(MemoryError): # any resampling filter will do here im.im.resize((xsize, ysize), Image.BILINEAR) @@ -26,10 +27,10 @@ class TestImagingResampleVulnerability(PillowTestCase): # Should not crash im.resize((100, 100)) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): im.resize((-100, 100)) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): im.resize((100, -100)) def test_modify_after_resizing(self): @@ -39,10 +40,10 @@ class TestImagingResampleVulnerability(PillowTestCase): # some in-place operation copy.paste("black", (0, 0, im.width // 2, im.height // 2)) # image should be different - self.assertNotEqual(im.tobytes(), copy.tobytes()) + assert im.tobytes() != copy.tobytes() -class TestImagingCoreResampleAccuracy(PillowTestCase): +class TestImagingCoreResampleAccuracy: def make_case(self, mode, size, color): """Makes a sample image with two dark and two bright squares. For example: @@ -81,15 +82,16 @@ class TestImagingCoreResampleAccuracy(PillowTestCase): for y in range(case.size[1]): for x in range(case.size[0]): if c_px[x, y] != s_px[x, y]: - message = "\nHave: \n{}\n\nExpected: \n{}".format( - self.serialize_image(case), self.serialize_image(sample) + message = ( + f"\nHave: \n{self.serialize_image(case)}\n" + f"\nExpected: \n{self.serialize_image(sample)}" ) - self.assertEqual(s_px[x, y], c_px[x, y], message) + assert s_px[x, y] == c_px[x, y], message def serialize_image(self, image): s_px = image.load() return "\n".join( - " ".join("{:02x}".format(s_px[x, y]) for x in range(image.size[0])) + " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) for y in range(image.size[1]) ) @@ -212,8 +214,13 @@ class TestImagingCoreResampleAccuracy(PillowTestCase): for channel in case.split(): self.check_case(channel, self.make_sample(data, (12, 12))) + def test_box_filter_correct_range(self): + im = Image.new("RGB", (8, 8), "#1688ff").resize((100, 100), Image.BOX) + ref = Image.new("RGB", (100, 100), "#1688ff") + assert_image_equal(im, ref) -class CoreResampleConsistencyTest(PillowTestCase): + +class TestCoreResampleConsistency: def make_case(self, mode, fill): im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.LANCZOS), im.load()[0, 0] @@ -224,8 +231,8 @@ class CoreResampleConsistencyTest(PillowTestCase): for x in range(channel.size[0]): for y in range(channel.size[1]): if px[x, y] != color: - message = "{} != {} for pixel {}".format(px[x, y], color, (x, y)) - self.assertEqual(px[x, y], color, message) + message = f"{px[x, y]} != {color} for pixel {(x, y)}" + assert px[x, y] == color, message def test_8u(self): im, color = self.make_case("RGB", (0, 64, 255)) @@ -248,7 +255,7 @@ class CoreResampleConsistencyTest(PillowTestCase): self.run_case(self.make_case("F", 1.192093e-07)) -class CoreResampleAlphaCorrectTest(PillowTestCase): +class TestCoreResampleAlphaCorrect: def make_levels_case(self, mode): i = Image.new(mode, (256, 16)) px = i.load() @@ -263,14 +270,12 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): px = i.load() for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} - self.assertEqual( - 256, - len(used_colors), - "All colors should present in resized image. " - "Only {} on {} line.".format(len(used_colors), y), + assert 256 == len(used_colors), ( + "All colors should be present in resized image. " + f"Only {len(used_colors)} on {y} line." ) - @unittest.skip("current implementation isn't precise enough") + @pytest.mark.xfail(reason="Current implementation isn't precise enough") def test_levels_rgba(self): case = self.make_levels_case("RGBA") self.run_levels_case(case.resize((512, 32), Image.BOX)) @@ -279,7 +284,7 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) - @unittest.skip("current implementation isn't precise enough") + @pytest.mark.xfail(reason="Current implementation isn't precise enough") def test_levels_la(self): case = self.make_levels_case("LA") self.run_levels_case(case.resize((512, 32), Image.BOX)) @@ -303,10 +308,11 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): for y in range(i.size[1]): for x in range(i.size[0]): if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel: - message = "pixel at ({}, {}) is differ:\n{}\n{}".format( - x, y, px[x, y], clean_pixel + message = ( + f"pixel at ({x}, {y}) is different:\n" + f"{px[x, y]}\n{clean_pixel}" ) - self.assertEqual(px[x, y][:3], clean_pixel, message) + assert px[x, y][:3] == clean_pixel, message def test_dirty_pixels_rgba(self): case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) @@ -325,12 +331,12 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255,)) -class CoreResamplePassesTest(PillowTestCase): +class TestCoreResamplePasses: @contextmanager def count(self, diff): count = Image.core.get_stats()["new_count"] yield - self.assertEqual(Image.core.get_stats()["new_count"] - count, diff) + assert Image.core.get_stats()["new_count"] - count == diff def test_horizontal(self): im = hopper("L") @@ -355,7 +361,7 @@ class CoreResamplePassesTest(PillowTestCase): with_box = im.resize(im.size, Image.BILINEAR, box) with self.count(2): cropped = im.crop(box).resize(im.size, Image.BILINEAR) - self.assert_image_similar(with_box, cropped, 0.1) + assert_image_similar(with_box, cropped, 0.1) def test_box_vertical(self): im = hopper("L") @@ -365,10 +371,10 @@ class CoreResamplePassesTest(PillowTestCase): with_box = im.resize(im.size, Image.BILINEAR, box) with self.count(2): cropped = im.crop(box).resize(im.size, Image.BILINEAR) - self.assert_image_similar(with_box, cropped, 0.1) + assert_image_similar(with_box, cropped, 0.1) -class CoreResampleCoefficientsTest(PillowTestCase): +class TestCoreResampleCoefficients: def test_reduce(self): test_color = 254 @@ -379,7 +385,7 @@ class CoreResampleCoefficientsTest(PillowTestCase): px = i.resize((5, i.size[1]), Image.BICUBIC).load() if px[2, 0] != test_color // 2: - self.assertEqual(test_color // 2, px[2, 0]) + assert test_color // 2 == px[2, 0] def test_nonzero_coefficients(self): # regression test for the wrong coefficients calculation @@ -388,16 +394,16 @@ class CoreResampleCoefficientsTest(PillowTestCase): histogram = im.resize((256, 256), Image.BICUBIC).histogram() # first channel - self.assertEqual(histogram[0x100 * 0 + 0x20], 0x10000) + assert histogram[0x100 * 0 + 0x20] == 0x10000 # second channel - self.assertEqual(histogram[0x100 * 1 + 0x40], 0x10000) + assert histogram[0x100 * 1 + 0x40] == 0x10000 # third channel - self.assertEqual(histogram[0x100 * 2 + 0x60], 0x10000) + assert histogram[0x100 * 2 + 0x60] == 0x10000 # fourth channel - self.assertEqual(histogram[0x100 * 3 + 0xFF], 0x10000) + assert histogram[0x100 * 3 + 0xFF] == 0x10000 -class CoreResampleBoxTest(PillowTestCase): +class TestCoreResampleBox: def test_wrong_arguments(self): im = hopper() for resample in ( @@ -413,24 +419,24 @@ class CoreResampleBoxTest(PillowTestCase): im.resize((32, 32), resample, (20, 20, 20, 100)) im.resize((32, 32), resample, (20, 20, 100, 20)) - with self.assertRaisesRegex(TypeError, "must be sequence of length 4"): + with pytest.raises(TypeError, match="must be sequence of length 4"): im.resize((32, 32), resample, (im.width, im.height)) - with self.assertRaisesRegex(ValueError, "can't be negative"): + with pytest.raises(ValueError, match="can't be negative"): im.resize((32, 32), resample, (-20, 20, 100, 100)) - with self.assertRaisesRegex(ValueError, "can't be negative"): + with pytest.raises(ValueError, match="can't be negative"): im.resize((32, 32), resample, (20, -20, 100, 100)) - with self.assertRaisesRegex(ValueError, "can't be empty"): + with pytest.raises(ValueError, match="can't be empty"): im.resize((32, 32), resample, (20.1, 20, 20, 100)) - with self.assertRaisesRegex(ValueError, "can't be empty"): + with pytest.raises(ValueError, match="can't be empty"): im.resize((32, 32), resample, (20, 20.1, 100, 20)) - with self.assertRaisesRegex(ValueError, "can't be empty"): + with pytest.raises(ValueError, match="can't be empty"): im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) - with self.assertRaisesRegex(ValueError, "can't exceed"): + with pytest.raises(ValueError, match="can't exceed"): im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) - with self.assertRaisesRegex(ValueError, "can't exceed"): + 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): @@ -450,33 +456,33 @@ class CoreResampleBoxTest(PillowTestCase): return tiled def test_tiles(self): - im = Image.open("Tests/images/flower.jpg") - self.assertEqual(im.size, (480, 360)) - dst_size = (251, 188) - reference = im.resize(dst_size, Image.BICUBIC) + with Image.open("Tests/images/flower.jpg") as im: + assert im.size == (480, 360) + dst_size = (251, 188) + reference = im.resize(dst_size, Image.BICUBIC) - for tiles in [(1, 1), (3, 3), (9, 7), (100, 100)]: - tiled = self.resize_tiled(im, dst_size, *tiles) - self.assert_image_similar(reference, tiled, 0.01) + for tiles in [(1, 1), (3, 3), (9, 7), (100, 100)]: + tiled = self.resize_tiled(im, dst_size, *tiles) + assert_image_similar(reference, tiled, 0.01) def test_subsample(self): # This test shows advantages of the subpixel resizing # after supersampling (e.g. during JPEG decoding). - im = Image.open("Tests/images/flower.jpg") - self.assertEqual(im.size, (480, 360)) - dst_size = (48, 36) - # Reference is cropped image resized to destination - reference = im.crop((0, 0, 473, 353)).resize(dst_size, Image.BICUBIC) - # Image.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45) - supersampled = im.resize((60, 45), Image.BOX) + with Image.open("Tests/images/flower.jpg") as im: + assert im.size == (480, 360) + dst_size = (48, 36) + # Reference is cropped image resized to destination + reference = im.crop((0, 0, 473, 353)).resize(dst_size, Image.BICUBIC) + # Image.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45) + supersampled = im.resize((60, 45), Image.BOX) with_box = supersampled.resize(dst_size, Image.BICUBIC, (0, 0, 59.125, 44.125)) without_box = supersampled.resize(dst_size, Image.BICUBIC) # error with box should be much smaller than without - self.assert_image_similar(reference, with_box, 6) - with self.assertRaisesRegex(AssertionError, r"difference 29\."): - self.assert_image_similar(reference, without_box, 5) + assert_image_similar(reference, with_box, 6) + with pytest.raises(AssertionError, match=r"difference 29\."): + assert_image_similar(reference, without_box, 5) def test_formats(self): for resample in [Image.NEAREST, Image.BILINEAR]: @@ -485,7 +491,7 @@ class CoreResampleBoxTest(PillowTestCase): 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) - self.assert_image_similar(cropped, with_box, 0.4) + assert_image_similar(cropped, with_box, 0.4) def test_passthrough(self): # When no resize is required @@ -497,13 +503,9 @@ class CoreResampleBoxTest(PillowTestCase): ((40, 50), (10, 0, 50, 50)), ((40, 50), (10, 20, 50, 70)), ]: - try: - res = im.resize(size, Image.LANCZOS, box) - self.assertEqual(res.size, size) - self.assert_image_equal(res, im.crop(box)) - except AssertionError: - print(">>>", size, box) - raise + res = im.resize(size, Image.LANCZOS, box) + assert res.size == size + assert_image_equal(res, im.crop(box), f">>> {size} {box}") def test_no_passthrough(self): # When resize is required @@ -515,15 +517,11 @@ class CoreResampleBoxTest(PillowTestCase): ((40, 50), (10.4, 0.4, 50.4, 50.4)), ((40, 50), (10.4, 20.4, 50.4, 70.4)), ]: - try: - res = im.resize(size, Image.LANCZOS, box) - self.assertEqual(res.size, size) - with self.assertRaisesRegex(AssertionError, r"difference \d"): - # check that the difference at least that much - self.assert_image_similar(res, im.crop(box), 20) - except AssertionError: - print(">>>", size, box) - raise + res = im.resize(size, Image.LANCZOS, box) + assert res.size == size + with pytest.raises(AssertionError, match=r"difference \d"): + # check that the difference at least that much + assert_image_similar(res, im.crop(box), 20, f">>> {size} {box}") def test_skip_horizontal(self): # Can skip resize for one dimension @@ -536,14 +534,15 @@ class CoreResampleBoxTest(PillowTestCase): ((40, 50), (10, 0, 50, 90)), ((40, 50), (10, 20, 50, 90)), ]: - try: - res = im.resize(size, flt, box) - self.assertEqual(res.size, size) - # Borders should be slightly different - self.assert_image_similar(res, im.crop(box).resize(size, flt), 0.4) - except AssertionError: - print(">>>", size, box, flt) - raise + res = im.resize(size, flt, box) + assert res.size == size + # Borders should be slightly different + assert_image_similar( + res, + im.crop(box).resize(size, flt), + 0.4, + f">>> {size} {box} {flt}", + ) def test_skip_vertical(self): # Can skip resize for one dimension @@ -556,11 +555,12 @@ class CoreResampleBoxTest(PillowTestCase): ((40, 50), (0, 10, 90, 60)), ((40, 50), (20, 10, 90, 60)), ]: - try: - res = im.resize(size, flt, box) - self.assertEqual(res.size, size) - # Borders should be slightly different - self.assert_image_similar(res, im.crop(box).resize(size, flt), 0.4) - except AssertionError: - print(">>>", size, box, flt) - raise + res = im.resize(size, flt, box) + assert res.size == size + # Borders should be slightly different + assert_image_similar( + res, + im.crop(box).resize(size, flt), + 0.4, + f">>> {size} {box} {flt}", + ) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 2538dd378..a49abe1b9 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -3,12 +3,14 @@ Tests for resize functionality. """ from itertools import permutations +import pytest + from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, assert_image_similar, hopper -class TestImagingCoreResize(PillowTestCase): +class TestImagingCoreResize: def resize(self, im, size, f): # Image class independent version of resize. im.load() @@ -29,26 +31,23 @@ class TestImagingCoreResize(PillowTestCase): ]: # exotic mode im = hopper(mode) r = self.resize(im, (15, 12), Image.NEAREST) - self.assertEqual(r.mode, mode) - self.assertEqual(r.size, (15, 12)) - self.assertEqual(r.im.bands, im.im.bands) + assert r.mode == mode + assert r.size == (15, 12) + assert r.im.bands == im.im.bands def test_convolution_modes(self): - self.assertRaises( - ValueError, self.resize, hopper("1"), (15, 12), Image.BILINEAR - ) - self.assertRaises( - ValueError, self.resize, hopper("P"), (15, 12), Image.BILINEAR - ) - self.assertRaises( - ValueError, self.resize, hopper("I;16"), (15, 12), Image.BILINEAR - ) + with pytest.raises(ValueError): + self.resize(hopper("1"), (15, 12), Image.BILINEAR) + with pytest.raises(ValueError): + self.resize(hopper("P"), (15, 12), Image.BILINEAR) + with pytest.raises(ValueError): + self.resize(hopper("I;16"), (15, 12), Image.BILINEAR) for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]: im = hopper(mode) r = self.resize(im, (15, 12), Image.BILINEAR) - self.assertEqual(r.mode, mode) - self.assertEqual(r.size, (15, 12)) - self.assertEqual(r.im.bands, im.im.bands) + assert r.mode == mode + assert r.size == (15, 12) + assert r.im.bands == im.im.bands def test_reduce_filters(self): for f in [ @@ -60,8 +59,8 @@ class TestImagingCoreResize(PillowTestCase): Image.LANCZOS, ]: r = self.resize(hopper("RGB"), (15, 12), f) - self.assertEqual(r.mode, "RGB") - self.assertEqual(r.size, (15, 12)) + assert r.mode == "RGB" + assert r.size == (15, 12) def test_enlarge_filters(self): for f in [ @@ -73,8 +72,8 @@ class TestImagingCoreResize(PillowTestCase): Image.LANCZOS, ]: r = self.resize(hopper("RGB"), (212, 195), f) - self.assertEqual(r.mode, "RGB") - self.assertEqual(r.size, (212, 195)) + assert r.mode == "RGB" + assert r.size == (212, 195) def test_endianness(self): # Make an image with one colored pixel, in one channel. @@ -116,7 +115,7 @@ class TestImagingCoreResize(PillowTestCase): for i, ch in enumerate(resized.split()): # check what resized channel in image is the same # as separately resized channel - self.assert_image_equal(ch, references[channels[i]]) + assert_image_equal(ch, references[channels[i]]) def test_enlarge_zero(self): for f in [ @@ -128,20 +127,111 @@ class TestImagingCoreResize(PillowTestCase): Image.LANCZOS, ]: r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), f) - self.assertEqual(r.mode, "RGB") - self.assertEqual(r.size, (212, 195)) - self.assertEqual(r.getdata()[0], (0, 0, 0)) + assert r.mode == "RGB" + assert r.size == (212, 195) + assert r.getdata()[0] == (0, 0, 0) def test_unknown_filter(self): - self.assertRaises(ValueError, self.resize, hopper(), (10, 10), 9) + with pytest.raises(ValueError): + self.resize(hopper(), (10, 10), 9) -class TestImageResize(PillowTestCase): +@pytest.fixture +def gradients_image(): + im = Image.open("Tests/images/radial_gradients.png") + im.load() + try: + yield im + finally: + im.close() + + +class TestReducingGapResize: + def test_reducing_gap_values(self, gradients_image): + ref = gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=None) + im = gradients_image.resize((52, 34), Image.BICUBIC) + assert_image_equal(ref, im) + + with pytest.raises(ValueError): + gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0) + + with pytest.raises(ValueError): + gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0.99) + + def test_reducing_gap_1(self, gradients_image): + for box, epsilon in [ + (None, 4), + ((1.1, 2.2, 510.8, 510.9), 4), + ((3, 10, 410, 256), 10), + ]: + ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.BICUBIC, box=box, reducing_gap=1.0 + ) + + with pytest.raises(AssertionError): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, epsilon) + + def test_reducing_gap_2(self, gradients_image): + for box, epsilon in [ + (None, 1.5), + ((1.1, 2.2, 510.8, 510.9), 1.5), + ((3, 10, 410, 256), 1), + ]: + ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.BICUBIC, box=box, reducing_gap=2.0 + ) + + with pytest.raises(AssertionError): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, epsilon) + + def test_reducing_gap_3(self, gradients_image): + for box, epsilon in [ + (None, 1), + ((1.1, 2.2, 510.8, 510.9), 1), + ((3, 10, 410, 256), 0.5), + ]: + ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.BICUBIC, box=box, reducing_gap=3.0 + ) + + with pytest.raises(AssertionError): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, epsilon) + + def test_reducing_gap_8(self, gradients_image): + for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]: + ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.BICUBIC, box=box, reducing_gap=8.0 + ) + + assert_image_equal(ref, im) + + def test_box_filter(self, gradients_image): + for box, epsilon in [ + ((0, 0, 512, 512), 5.5), + ((0.9, 1.7, 128, 128), 9.5), + ]: + ref = gradients_image.resize((52, 34), Image.BOX, box=box) + im = gradients_image.resize((52, 34), Image.BOX, box=box, reducing_gap=1.0) + + assert_image_similar(ref, im, epsilon) + + +class TestImageResize: def test_resize(self): def resize(mode, size): out = hopper(mode).resize(size) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, size) + assert out.mode == mode + assert out.size == size for mode in "1", "P", "L", "RGB", "I", "F": resize(mode, (112, 103)) @@ -149,4 +239,14 @@ class TestImageResize(PillowTestCase): # Test unknown resampling filter with hopper() as im: - self.assertRaises(ValueError, im.resize, (10, 10), "unknown") + with pytest.raises(ValueError): + im.resize((10, 10), "unknown") + + def test_default_filter(self): + for mode in "L", "RGB", "I", "F": + im = hopper(mode) + assert im.resize((20, 20), Image.BICUBIC) == im.resize((20, 20)) + + for mode in "1", "P": + im = hopper(mode) + assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20)) diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 9c62e7362..a41d850bb 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,44 +1,47 @@ from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, assert_image_similar, hopper -class TestImageRotate(PillowTestCase): - def rotate(self, im, mode, angle, center=None, translate=None): - out = im.rotate(angle, center=center, translate=translate) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size) # default rotate clips output - out = im.rotate(angle, center=center, translate=translate, expand=1) - self.assertEqual(out.mode, mode) - if angle % 180 == 0: - self.assertEqual(out.size, im.size) - elif im.size == (0, 0): - self.assertEqual(out.size, im.size) - else: - self.assertNotEqual(out.size, im.size) +def rotate(im, mode, angle, center=None, translate=None): + out = im.rotate(angle, center=center, translate=translate) + assert out.mode == mode + assert out.size == im.size # default rotate clips output + out = im.rotate(angle, center=center, translate=translate, expand=1) + assert out.mode == mode + if angle % 180 == 0: + assert out.size == im.size + elif im.size == (0, 0): + assert out.size == im.size + else: + assert out.size != im.size - def test_mode(self): - for mode in ("1", "P", "L", "RGB", "I", "F"): - im = hopper(mode) - self.rotate(im, mode, 45) - def test_angle(self): - for angle in (0, 90, 180, 270): - im = Image.open("Tests/images/test-card.png") - self.rotate(im, im.mode, angle) +def test_mode(): + for mode in ("1", "P", "L", "RGB", "I", "F"): + im = hopper(mode) + rotate(im, mode, 45) - def test_zero(self): - for angle in (0, 45, 90, 180, 270): - im = Image.new("RGB", (0, 0)) - self.rotate(im, im.mode, angle) - def test_resample(self): - # Target image creation, inspected by eye. - # >>> im = Image.open('Tests/images/hopper.ppm') - # >>> im = im.rotate(45, resample=Image.BICUBIC, expand=True) - # >>> im.save('Tests/images/hopper_45.png') +def test_angle(): + for angle in (0, 90, 180, 270): + with Image.open("Tests/images/test-card.png") as im: + rotate(im, im.mode, angle) - target = Image.open("Tests/images/hopper_45.png") + +def test_zero(): + for angle in (0, 45, 90, 180, 270): + im = Image.new("RGB", (0, 0)) + rotate(im, im.mode, angle) + + +def test_resample(): + # Target image creation, inspected by eye. + # >>> im = Image.open('Tests/images/hopper.ppm') + # >>> im = im.rotate(45, resample=Image.BICUBIC, expand=True) + # >>> im.save('Tests/images/hopper_45.png') + + with Image.open("Tests/images/hopper_45.png") as target: for (resample, epsilon) in ( (Image.NEAREST, 10), (Image.BILINEAR, 5), @@ -46,82 +49,92 @@ class TestImageRotate(PillowTestCase): ): im = hopper() im = im.rotate(45, resample=resample, expand=True) - self.assert_image_similar(im, target, epsilon) + assert_image_similar(im, target, epsilon) - def test_center_0(self): - im = hopper() - target = Image.open("Tests/images/hopper_45.png") + +def test_center_0(): + im = hopper() + im = im.rotate(45, center=(0, 0), resample=Image.BICUBIC) + + with Image.open("Tests/images/hopper_45.png") as target: target_origin = target.size[1] / 2 target = target.crop((0, target_origin, 128, target_origin + 128)) - im = im.rotate(45, center=(0, 0), resample=Image.BICUBIC) + assert_image_similar(im, target, 15) - self.assert_image_similar(im, target, 15) - def test_center_14(self): - im = hopper() - target = Image.open("Tests/images/hopper_45.png") +def test_center_14(): + im = hopper() + im = im.rotate(45, center=(14, 14), resample=Image.BICUBIC) + + with Image.open("Tests/images/hopper_45.png") as target: target_origin = target.size[1] / 2 - 14 target = target.crop((6, target_origin, 128 + 6, target_origin + 128)) - im = im.rotate(45, center=(14, 14), resample=Image.BICUBIC) + assert_image_similar(im, target, 10) - self.assert_image_similar(im, target, 10) - def test_translate(self): - im = hopper() - target = Image.open("Tests/images/hopper_45.png") +def test_translate(): + im = hopper() + with Image.open("Tests/images/hopper_45.png") as target: target_origin = (target.size[1] / 2 - 64) - 5 target = target.crop( (target_origin, target_origin, target_origin + 128, target_origin + 128) ) - im = im.rotate(45, translate=(5, 5), resample=Image.BICUBIC) + im = im.rotate(45, translate=(5, 5), resample=Image.BICUBIC) - self.assert_image_similar(im, target, 1) + assert_image_similar(im, target, 1) - def test_fastpath_center(self): - # 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): - im = hopper().rotate(angle, center=(-1, -1)) - self.assert_image_equal(im, Image.new("RGB", im.size, "black")) - def test_fastpath_translate(self): - # if we post-translate by -128 - # resulting image should be black - for angle in (0, 90, 180, 270): - im = hopper().rotate(angle, translate=(-128, -128)) - self.assert_image_equal(im, Image.new("RGB", im.size, "black")) +def test_fastpath_center(): + # 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): + im = hopper().rotate(angle, center=(-1, -1)) + assert_image_equal(im, Image.new("RGB", im.size, "black")) - def test_center(self): - im = hopper() - self.rotate(im, im.mode, 45, center=(0, 0)) - self.rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0)) - self.rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0)) - def test_rotate_no_fill(self): - im = Image.new("RGB", (100, 100), "green") - target = Image.open("Tests/images/rotate_45_no_fill.png") - im = im.rotate(45) - self.assert_image_equal(im, target) +def test_fastpath_translate(): + # if we post-translate by -128 + # resulting image should be black + for angle in (0, 90, 180, 270): + im = hopper().rotate(angle, translate=(-128, -128)) + assert_image_equal(im, Image.new("RGB", im.size, "black")) - def test_rotate_with_fill(self): - im = Image.new("RGB", (100, 100), "green") - target = Image.open("Tests/images/rotate_45_with_fill.png") - im = im.rotate(45, fillcolor="white") - self.assert_image_equal(im, target) - def test_alpha_rotate_no_fill(self): - # Alpha images are handled differently internally - im = Image.new("RGBA", (10, 10), "green") - im = im.rotate(45, expand=1) - corner = im.getpixel((0, 0)) - self.assertEqual(corner, (0, 0, 0, 0)) +def test_center(): + 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_alpha_rotate_with_fill(self): - # Alpha images are handled differently internally - im = Image.new("RGBA", (10, 10), "green") - im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) - corner = im.getpixel((0, 0)) - self.assertEqual(corner, (255, 0, 0, 255)) + +def test_rotate_no_fill(): + im = Image.new("RGB", (100, 100), "green") + im = im.rotate(45) + with Image.open("Tests/images/rotate_45_no_fill.png") as target: + assert_image_equal(im, target) + + +def test_rotate_with_fill(): + im = Image.new("RGB", (100, 100), "green") + im = im.rotate(45, fillcolor="white") + with Image.open("Tests/images/rotate_45_with_fill.png") as target: + assert_image_equal(im, target) + + +def test_alpha_rotate_no_fill(): + # Alpha images are handled differently internally + im = Image.new("RGBA", (10, 10), "green") + im = im.rotate(45, expand=1) + corner = im.getpixel((0, 0)) + assert corner == (0, 0, 0, 0) + + +def test_alpha_rotate_with_fill(): + # Alpha images are handled differently internally + im = Image.new("RGBA", (10, 10), "green") + im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) + corner = im.getpixel((0, 0)) + assert corner == (255, 0, 0, 255) diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index a19878aae..fbed276b8 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,64 +1,63 @@ -from PIL import Image +from PIL import Image, features -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper -class TestImageSplit(PillowTestCase): - def test_split(self): - def split(mode): - layers = hopper(mode).split() - return [(i.mode, i.size[0], i.size[1]) for i in layers] +def test_split(): + def split(mode): + layers = hopper(mode).split() + return [(i.mode, i.size[0], i.size[1]) for i in layers] - self.assertEqual(split("1"), [("1", 128, 128)]) - self.assertEqual(split("L"), [("L", 128, 128)]) - self.assertEqual(split("I"), [("I", 128, 128)]) - self.assertEqual(split("F"), [("F", 128, 128)]) - self.assertEqual(split("P"), [("P", 128, 128)]) - self.assertEqual( - split("RGB"), [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] - ) - self.assertEqual( - split("RGBA"), - [("L", 128, 128), ("L", 128, 128), ("L", 128, 128), ("L", 128, 128)], - ) - self.assertEqual( - split("CMYK"), - [("L", 128, 128), ("L", 128, 128), ("L", 128, 128), ("L", 128, 128)], - ) - self.assertEqual( - split("YCbCr"), [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] - ) + assert split("1") == [("1", 128, 128)] + assert split("L") == [("L", 128, 128)] + assert split("I") == [("I", 128, 128)] + assert split("F") == [("F", 128, 128)] + assert split("P") == [("P", 128, 128)] + assert split("RGB") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] + assert split("RGBA") == [ + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ] + assert split("CMYK") == [ + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ] + assert split("YCbCr") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] - def test_split_merge(self): - def split_merge(mode): - return Image.merge(mode, hopper(mode).split()) - self.assert_image_equal(hopper("1"), split_merge("1")) - self.assert_image_equal(hopper("L"), split_merge("L")) - self.assert_image_equal(hopper("I"), split_merge("I")) - self.assert_image_equal(hopper("F"), split_merge("F")) - self.assert_image_equal(hopper("P"), split_merge("P")) - self.assert_image_equal(hopper("RGB"), split_merge("RGB")) - self.assert_image_equal(hopper("RGBA"), split_merge("RGBA")) - self.assert_image_equal(hopper("CMYK"), split_merge("CMYK")) - self.assert_image_equal(hopper("YCbCr"), split_merge("YCbCr")) +def test_split_merge(): + def split_merge(mode): + return Image.merge(mode, hopper(mode).split()) - def test_split_open(self): - codecs = dir(Image.core) + assert_image_equal(hopper("1"), split_merge("1")) + assert_image_equal(hopper("L"), split_merge("L")) + assert_image_equal(hopper("I"), split_merge("I")) + assert_image_equal(hopper("F"), split_merge("F")) + assert_image_equal(hopper("P"), split_merge("P")) + assert_image_equal(hopper("RGB"), split_merge("RGB")) + assert_image_equal(hopper("RGBA"), split_merge("RGBA")) + assert_image_equal(hopper("CMYK"), split_merge("CMYK")) + assert_image_equal(hopper("YCbCr"), split_merge("YCbCr")) - if "zip_encoder" in codecs: - test_file = self.tempfile("temp.png") - else: - test_file = self.tempfile("temp.pcx") - def split_open(mode): - hopper(mode).save(test_file) - im = Image.open(test_file) +def test_split_open(tmp_path): + if features.check("zlib"): + test_file = str(tmp_path / "temp.png") + else: + test_file = str(tmp_path / "temp.pcx") + + def split_open(mode): + hopper(mode).save(test_file) + with Image.open(test_file) as im: return len(im.split()) - self.assertEqual(split_open("1"), 1) - self.assertEqual(split_open("L"), 1) - self.assertEqual(split_open("P"), 1) - self.assertEqual(split_open("RGB"), 3) - if "zip_encoder" in codecs: - self.assertEqual(split_open("RGBA"), 4) + assert split_open("1") == 1 + assert split_open("L") == 1 + assert split_open("P") == 1 + assert split_open("RGB") == 3 + if features.check("zlib"): + assert split_open("RGBA") == 4 diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index d1224f075..c42310c32 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,49 +1,131 @@ +import pytest + from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import ( + assert_image_equal, + assert_image_similar, + fromstring, + hopper, + tostring, +) -class TestImageThumbnail(PillowTestCase): - def test_sanity(self): +def test_sanity(): + im = hopper() + assert im.thumbnail((100, 100)) is None - im = hopper() - im.thumbnail((100, 100)) + assert im.size == (100, 100) - self.assert_image(im, im.mode, (100, 100)) - def test_aspect(self): +def test_aspect(): + im = Image.new("L", (128, 128)) + im.thumbnail((100, 100)) + assert im.size == (100, 100) - im = hopper() - im.thumbnail((100, 100)) - self.assert_image(im, im.mode, (100, 100)) + im = Image.new("L", (128, 256)) + im.thumbnail((100, 100)) + assert im.size == (50, 100) - im = hopper().resize((128, 256)) - im.thumbnail((100, 100)) - self.assert_image(im, im.mode, (50, 100)) + im = Image.new("L", (128, 256)) + im.thumbnail((50, 100)) + assert im.size == (50, 100) - im = hopper().resize((128, 256)) - im.thumbnail((50, 100)) - self.assert_image(im, im.mode, (50, 100)) + im = Image.new("L", (256, 128)) + im.thumbnail((100, 100)) + assert im.size == (100, 50) - im = hopper().resize((256, 128)) - im.thumbnail((100, 100)) - self.assert_image(im, im.mode, (100, 50)) + im = Image.new("L", (256, 128)) + im.thumbnail((100, 50)) + assert im.size == (100, 50) - im = hopper().resize((256, 128)) - im.thumbnail((100, 50)) - self.assert_image(im, im.mode, (100, 50)) + im = Image.new("L", (64, 64)) + im.thumbnail((100, 100)) + assert im.size == (64, 64) - im = hopper().resize((128, 128)) - im.thumbnail((100, 100)) - self.assert_image(im, im.mode, (100, 100)) + im = Image.new("L", (256, 162)) # ratio is 1.5802469136 + im.thumbnail((33, 33)) + assert im.size == (33, 21) # ratio is 1.5714285714 + + im = Image.new("L", (162, 256)) # ratio is 0.6328125 + im.thumbnail((33, 33)) + assert im.size == (21, 33) # ratio is 0.6363636364 + + im = Image.new("L", (145, 100)) # ratio is 1.45 + im.thumbnail((50, 50)) + assert im.size == (50, 34) # ratio is 1.47058823529 + + im = Image.new("L", (100, 145)) # ratio is 0.689655172414 + im.thumbnail((50, 50)) + assert im.size == (34, 50) # ratio is 0.68 + + im = Image.new("L", (100, 30)) # ratio is 3.333333333333 + im.thumbnail((75, 75)) + assert im.size == (75, 23) # ratio is 3.260869565217 + + +def test_division_by_zero(): + im = Image.new("L", (200, 2)) + im.thumbnail((75, 75)) + assert im.size == (75, 1) + + +def test_float(): + im = Image.new("L", (128, 128)) + im.thumbnail((99.9, 99.9)) + assert im.size == (99, 99) + + +def test_no_resize(): + # 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)) + assert im.size == (64, 64) + + # Test thumbnail(), where only draft() is necessary to resize the image + with Image.open("Tests/images/hopper.jpg") as im: + im.thumbnail((64, 64)) + assert im.size == (64, 64) + + +def test_DCT_scaling_edges(): + # 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)) + + thumb = fromstring(tostring(im, "JPEG", quality=99, subsampling=0)) + # small reducing_gap to amplify the effect + thumb.thumbnail((32, 32), Image.BICUBIC, reducing_gap=1.0) + + ref = im.resize((32, 32), Image.BICUBIC) + # This is still JPEG, some error is present. Without the fix it is 11.5 + assert_image_similar(thumb, ref, 1.5) + + +def test_reducing_gap_values(): + im = hopper() + im.thumbnail((18, 18), Image.BICUBIC) + + ref = hopper() + ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=2.0) + # reducing_gap=2.0 should be the default + assert_image_equal(ref, im) + + ref = hopper() + ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=None) + with pytest.raises(AssertionError): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, 3.5) + + +def test_reducing_gap_for_DCT_scaling(): + with Image.open("Tests/images/hopper.jpg") as ref: + # thumbnail should call draft with reducing_gap scale + ref.draft(None, (18 * 3, 18 * 3)) + ref = ref.resize((18, 18), Image.BICUBIC) - def test_no_resize(self): - # 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)) - self.assertEqual(im.size, (64, 64)) + im.thumbnail((18, 18), Image.BICUBIC, reducing_gap=3.0) - # Test thumbnail(), where only draft() is necessary to resize the image - with Image.open("Tests/images/hopper.jpg") as im: - im.thumbnail((64, 64)) - self.assert_image(im, im.mode, (64, 64)) + assert_image_equal(ref, im) diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index d7c879a25..178cfcef3 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -1,14 +1,16 @@ -from .helper import PillowTestCase, fromstring, hopper +import pytest + +from .helper import assert_image_equal, fromstring, hopper -class TestImageToBitmap(PillowTestCase): - def test_sanity(self): +def test_sanity(): - self.assertRaises(ValueError, lambda: hopper().tobitmap()) + with pytest.raises(ValueError): + hopper().tobitmap() - im1 = hopper().convert("1") + im1 = hopper().convert("1") - bitmap = im1.tobitmap() + bitmap = im1.tobitmap() - self.assertIsInstance(bitmap, bytes) - self.assert_image_equal(im1, fromstring(bitmap)) + assert isinstance(bitmap, bytes) + assert_image_equal(im1, fromstring(bitmap)) diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index d21ef7f6f..31e1c0080 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -1,7 +1,6 @@ -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageToBytes(PillowTestCase): - def test_sanity(self): - data = hopper().tobytes() - self.assertIsInstance(data, bytes) +def test_sanity(): + data = hopper().tobytes() + assert isinstance(data, bytes) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 35f64bcc9..3ee51178d 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,11 +1,13 @@ import math +import pytest + from PIL import Image, ImageTransform -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, assert_image_similar, hopper -class TestImageTransform(PillowTestCase): +class TestImageTransform: def test_sanity(self): im = Image.new("L", (100, 100)) @@ -23,12 +25,12 @@ class TestImageTransform(PillowTestCase): def test_info(self): comment = b"File written by Adobe Photoshop\xa8 4.0" - im = Image.open("Tests/images/hopper.gif") - self.assertEqual(im.info["comment"], comment) + with Image.open("Tests/images/hopper.gif") as im: + assert im.info["comment"] == comment - transform = ImageTransform.ExtentTransform((0, 0, 0, 0)) - new_im = im.transform((100, 100), transform) - self.assertEqual(new_im.info["comment"], comment) + transform = ImageTransform.ExtentTransform((0, 0, 0, 0)) + new_im = im.transform((100, 100), transform) + assert new_im.info["comment"] == comment def test_extent(self): im = hopper("RGB") @@ -43,7 +45,7 @@ class TestImageTransform(PillowTestCase): scaled = im.resize((w * 2, h * 2), Image.BILINEAR).crop((0, 0, w, h)) # undone -- precision? - self.assert_image_similar(transformed, scaled, 23) + assert_image_similar(transformed, scaled, 23) def test_quad(self): # one simple quad transform, equivalent to scale & crop upper left quad @@ -61,7 +63,7 @@ class TestImageTransform(PillowTestCase): (w, h), Image.AFFINE, (0.5, 0, 0, 0, 0.5, 0), Image.BILINEAR ) - self.assert_image_equal(transformed, scaled) + assert_image_equal(transformed, scaled) def test_fill(self): for mode, pixel in [ @@ -79,7 +81,7 @@ class TestImageTransform(PillowTestCase): fillcolor="red", ) - self.assertEqual(transformed.getpixel((w - 1, h - 1)), pixel) + assert transformed.getpixel((w - 1, h - 1)) == pixel def test_mesh(self): # this should be a checkerboard of halfsized hoppers in ul, lr @@ -104,13 +106,13 @@ class TestImageTransform(PillowTestCase): checker.paste(scaled, (0, 0)) checker.paste(scaled, (w // 2, h // 2)) - self.assert_image_equal(transformed, checker) + assert_image_equal(transformed, checker) # now, check to see that the extra area is (0, 0, 0, 0) blank = Image.new("RGBA", (w // 2, h // 2), (0, 0, 0, 0)) - self.assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) - self.assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) + 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): # create image with half white, half black, @@ -126,7 +128,7 @@ class TestImageTransform(PillowTestCase): im_background.paste(im, (0, 0), im) hist = im_background.histogram() - self.assertEqual(40 * 10, hist[-1]) + assert 40 * 10 == hist[-1] def test_alpha_premult_resize(self): def op(im, sz): @@ -165,23 +167,18 @@ class TestImageTransform(PillowTestCase): def test_missing_method_data(self): with hopper() as im: - self.assertRaises(ValueError, im.transform, (100, 100), None) + with pytest.raises(ValueError): + im.transform((100, 100), None) def test_unknown_resampling_filter(self): with hopper() as im: (w, h) = im.size for resample in (Image.BOX, "unknown"): - self.assertRaises( - ValueError, - im.transform, - (100, 100), - Image.EXTENT, - (0, 0, w, h), - resample, - ) + with pytest.raises(ValueError): + im.transform((100, 100), Image.EXTENT, (0, 0, w, h), resample) -class TestImageTransformAffine(PillowTestCase): +class TestImageTransformAffine: transform = Image.AFFINE def _test_image(self): @@ -214,7 +211,7 @@ class TestImageTransformAffine(PillowTestCase): transformed = im.transform( transposed.size, self.transform, matrix, resample ) - self.assert_image_equal(transposed, transformed) + assert_image_equal(transposed, transformed) def test_rotate_0_deg(self): self._test_rotate(0, None) @@ -244,7 +241,7 @@ class TestImageTransformAffine(PillowTestCase): transformed = transformed.transform( im.size, self.transform, matrix_down, resample ) - self.assert_image_similar(transformed, im, epsilon * epsilonscale) + assert_image_similar(transformed, im, epsilon * epsilonscale) def test_resize_1_1x(self): self._test_resize(1.1, 6.9) @@ -277,7 +274,7 @@ class TestImageTransformAffine(PillowTestCase): transformed = transformed.transform( im.size, self.transform, matrix_down, resample ) - self.assert_image_similar(transformed, im, epsilon * epsilonscale) + assert_image_similar(transformed, im, epsilon * epsilonscale) def test_translate_0_1(self): self._test_translate(0.1, 0, 3.7) diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index f5e8746ee..a004434da 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -9,151 +9,154 @@ from PIL.Image import ( ) from . import helper -from .helper import PillowTestCase +from .helper import assert_image_equal + +HOPPER = { + mode: helper.hopper(mode).crop((0, 0, 121, 127)).copy() + for mode in ["L", "RGB", "I;16", "I;16L", "I;16B"] +} -class TestImageTranspose(PillowTestCase): +def test_flip_left_right(): + def transpose(mode): + im = HOPPER[mode] + out = im.transpose(FLIP_LEFT_RIGHT) + assert out.mode == mode + assert out.size == im.size - hopper = { - mode: helper.hopper(mode).crop((0, 0, 121, 127)).copy() - for mode in ["L", "RGB", "I;16", "I;16L", "I;16B"] - } + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2)) - def test_flip_left_right(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(FLIP_LEFT_RIGHT) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size) + for mode in HOPPER: + transpose(mode) - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((x - 2, 1))) - self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((x - 2, y - 2))) - self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((1, y - 2))) - for mode in self.hopper: - transpose(mode) +def test_flip_top_bottom(): + def transpose(mode): + im = HOPPER[mode] + out = im.transpose(FLIP_TOP_BOTTOM) + assert out.mode == mode + assert out.size == im.size - def test_flip_top_bottom(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(FLIP_TOP_BOTTOM) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, y - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1)) - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, y - 2))) - self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((x - 2, y - 2))) - self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((x - 2, 1))) + for mode in HOPPER: + transpose(mode) - for mode in self.hopper: - transpose(mode) - def test_rotate_90(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(ROTATE_90) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size[::-1]) +def test_rotate_90(): + def transpose(mode): + im = HOPPER[mode] + out = im.transpose(ROTATE_90) + assert out.mode == mode + assert out.size == im.size[::-1] - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, x - 2))) - self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((y - 2, x - 2))) - self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((y - 2, 1))) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, x - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1)) - for mode in self.hopper: - transpose(mode) + for mode in HOPPER: + transpose(mode) - def test_rotate_180(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(ROTATE_180) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size) - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((x - 2, y - 2))) - self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((1, y - 2))) - self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((x - 2, 1))) - self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((1, 1))) +def test_rotate_180(): + def transpose(mode): + im = HOPPER[mode] + out = im.transpose(ROTATE_180) + assert out.mode == mode + assert out.size == im.size - for mode in self.hopper: - transpose(mode) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) - def test_rotate_270(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(ROTATE_270) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size[::-1]) + for mode in HOPPER: + transpose(mode) - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((y - 2, 1))) - self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((y - 2, x - 2))) - self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((1, x - 2))) - for mode in self.hopper: - transpose(mode) +def test_rotate_270(): + def transpose(mode): + im = HOPPER[mode] + out = im.transpose(ROTATE_270) + assert out.mode == mode + assert out.size == im.size[::-1] - def test_transpose(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(TRANSPOSE) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size[::-1]) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2)) - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((1, x - 2))) - self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((y - 2, 1))) - self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((y - 2, x - 2))) + for mode in HOPPER: + transpose(mode) - for mode in self.hopper: - transpose(mode) - def test_tranverse(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(TRANSVERSE) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size[::-1]) +def test_transpose(): + def transpose(mode): + im = HOPPER[mode] + out = im.transpose(TRANSPOSE) + assert out.mode == mode + assert out.size == im.size[::-1] - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((y - 2, x - 2))) - self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((y - 2, 1))) - self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((1, x - 2))) - self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((1, 1))) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2)) - for mode in self.hopper: - transpose(mode) + for mode in HOPPER: + transpose(mode) - def test_roundtrip(self): - for mode in self.hopper: - im = self.hopper[mode] - def transpose(first, second): - return im.transpose(first).transpose(second) +def test_tranverse(): + def transpose(mode): + im = HOPPER[mode] + out = im.transpose(TRANSVERSE) + assert out.mode == mode + assert out.size == im.size[::-1] - self.assert_image_equal(im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT)) - self.assert_image_equal(im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM)) - self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270)) - self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180)) - self.assert_image_equal( - im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM) - ) - self.assert_image_equal( - im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT) - ) - self.assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT) - ) - self.assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM) - ) - self.assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE) - ) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) + + for mode in HOPPER: + transpose(mode) + + +def test_roundtrip(): + for mode in HOPPER: + im = HOPPER[mode] + + def transpose(first, second): + return im.transpose(first).transpose(second) + + assert_image_equal(im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT)) + assert_image_equal(im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM)) + assert_image_equal(im, transpose(ROTATE_90, ROTATE_270)) + assert_image_equal(im, transpose(ROTATE_180, ROTATE_180)) + assert_image_equal( + im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM) + ) + assert_image_equal( + im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT) + ) + assert_image_equal( + im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT) + ) + assert_image_equal( + im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM) + ) + assert_image_equal(im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE)) diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 6f42a28df..a19fbf239 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,6 +1,6 @@ from PIL import Image, ImageChops -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper BLACK = (0, 0, 0) BROWN = (127, 64, 0) @@ -13,352 +13,416 @@ WHITE = (255, 255, 255) GREY = 128 -class TestImageChops(PillowTestCase): - def test_sanity(self): +def test_sanity(): + im = hopper("L") - im = hopper("L") + ImageChops.constant(im, 128) + ImageChops.duplicate(im) + ImageChops.invert(im) + ImageChops.lighter(im, im) + ImageChops.darker(im, im) + ImageChops.difference(im, im) + ImageChops.multiply(im, im) + ImageChops.screen(im, im) - ImageChops.constant(im, 128) - ImageChops.duplicate(im) - ImageChops.invert(im) - ImageChops.lighter(im, im) - ImageChops.darker(im, im) - ImageChops.difference(im, im) - ImageChops.multiply(im, im) - ImageChops.screen(im, im) + ImageChops.add(im, im) + ImageChops.add(im, im, 2.0) + ImageChops.add(im, im, 2.0, 128) + ImageChops.subtract(im, im) + ImageChops.subtract(im, im, 2.0) + ImageChops.subtract(im, im, 2.0, 128) - ImageChops.add(im, im) - ImageChops.add(im, im, 2.0) - ImageChops.add(im, im, 2.0, 128) - ImageChops.subtract(im, im) - ImageChops.subtract(im, im, 2.0) - ImageChops.subtract(im, im, 2.0, 128) + ImageChops.add_modulo(im, im) + ImageChops.subtract_modulo(im, im) - ImageChops.add_modulo(im, im) - ImageChops.subtract_modulo(im, im) + ImageChops.blend(im, im, 0.5) + ImageChops.composite(im, im, im) - ImageChops.blend(im, im, 0.5) - ImageChops.composite(im, im, im) + ImageChops.soft_light(im, im) + ImageChops.hard_light(im, im) + ImageChops.overlay(im, im) - ImageChops.offset(im, 10) - ImageChops.offset(im, 10, 20) + ImageChops.offset(im, 10) + ImageChops.offset(im, 10, 20) - def test_add(self): - # Arrange - im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") - im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") - # Act - new = ImageChops.add(im1, im2) +def test_add(): + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Assert - self.assertEqual(new.getbbox(), (25, 25, 76, 76)) - self.assertEqual(new.getpixel((50, 50)), ORANGE) + # Act + new = ImageChops.add(im1, im2) - def test_add_scale_offset(self): - # Arrange - im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") - im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((50, 50)) == ORANGE - # Act - new = ImageChops.add(im1, im2, scale=2.5, offset=100) - # Assert - self.assertEqual(new.getbbox(), (0, 0, 100, 100)) - self.assertEqual(new.getpixel((50, 50)), (202, 151, 100)) +def test_add_scale_offset(): + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - def test_add_clip(self): - # Arrange - im = hopper() + # Act + new = ImageChops.add(im1, im2, scale=2.5, offset=100) - # Act - new = ImageChops.add(im, im) + # Assert + assert new.getbbox() == (0, 0, 100, 100) + assert new.getpixel((50, 50)) == (202, 151, 100) - # Assert - self.assertEqual(new.getpixel((50, 50)), (255, 255, 254)) - def test_add_modulo(self): - # Arrange - im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") - im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") +def test_add_clip(): + # Arrange + im = hopper() - # Act - new = ImageChops.add_modulo(im1, im2) + # Act + new = ImageChops.add(im, im) - # Assert - self.assertEqual(new.getbbox(), (25, 25, 76, 76)) - self.assertEqual(new.getpixel((50, 50)), ORANGE) + # Assert + assert new.getpixel((50, 50)) == (255, 255, 254) - def test_add_modulo_no_clip(self): - # Arrange - im = hopper() - # Act - new = ImageChops.add_modulo(im, im) +def test_add_modulo(): + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Assert - self.assertEqual(new.getpixel((50, 50)), (224, 76, 254)) + # Act + new = ImageChops.add_modulo(im1, im2) - def test_blend(self): - # Arrange - im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") - im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((50, 50)) == ORANGE - # Act - new = ImageChops.blend(im1, im2, 0.5) - # Assert - self.assertEqual(new.getbbox(), (25, 25, 76, 76)) - self.assertEqual(new.getpixel((50, 50)), BROWN) +def test_add_modulo_no_clip(): + # Arrange + im = hopper() - def test_constant(self): - # Arrange - im = Image.new("RGB", (20, 10)) + # Act + new = ImageChops.add_modulo(im, im) - # Act - new = ImageChops.constant(im, GREY) + # Assert + assert new.getpixel((50, 50)) == (224, 76, 254) - # Assert - self.assertEqual(new.size, im.size) - self.assertEqual(new.getpixel((0, 0)), GREY) - self.assertEqual(new.getpixel((19, 9)), GREY) - def test_darker_image(self): - # Arrange - im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") - im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") +def test_blend(): + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: + + # Act + new = ImageChops.blend(im1, im2, 0.5) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((50, 50)) == BROWN + + +def test_constant(): + # Arrange + im = Image.new("RGB", (20, 10)) + + # Act + new = ImageChops.constant(im, GREY) + + # Assert + assert new.size == im.size + assert new.getpixel((0, 0)) == GREY + assert new.getpixel((19, 9)) == GREY + + +def test_darker_image(): + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + + # Act + new = ImageChops.darker(im1, im2) + + # Assert + assert_image_equal(new, im2) + + +def test_darker_pixel(): + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: # Act new = ImageChops.darker(im1, im2) - # Assert - self.assert_image_equal(new, im2) + # Assert + assert new.getpixel((50, 50)) == (240, 166, 0) - def test_darker_pixel(self): - # Arrange - im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") - # Act - new = ImageChops.darker(im1, im2) +def test_difference(): + # 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: - # Assert - self.assertEqual(new.getpixel((50, 50)), (240, 166, 0)) + # Act + new = ImageChops.difference(im1, im2) - def test_difference(self): - # Arrange - im1 = Image.open("Tests/images/imagedraw_arc_end_le_start.png") - im2 = Image.open("Tests/images/imagedraw_arc_no_loops.png") + # Assert + assert new.getbbox() == (25, 25, 76, 76) + + +def test_difference_pixel(): + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: # Act new = ImageChops.difference(im1, im2) - # Assert - self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + # Assert + assert new.getpixel((50, 50)) == (240, 166, 128) - def test_difference_pixel(self): - # Arrange - im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") - # Act - new = ImageChops.difference(im1, im2) +def test_duplicate(): + # Arrange + im = hopper() - # Assert - self.assertEqual(new.getpixel((50, 50)), (240, 166, 128)) + # Act + new = ImageChops.duplicate(im) - def test_duplicate(self): - # Arrange - im = hopper() + # Assert + assert_image_equal(new, im) - # Act - new = ImageChops.duplicate(im) - # Assert - self.assert_image_equal(new, im) - - def test_invert(self): - # Arrange - im = Image.open("Tests/images/imagedraw_floodfill_RGB.png") +def test_invert(): + # Arrange + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: # Act new = ImageChops.invert(im) - # Assert - self.assertEqual(new.getbbox(), (0, 0, 100, 100)) - self.assertEqual(new.getpixel((0, 0)), WHITE) - self.assertEqual(new.getpixel((50, 50)), CYAN) + # Assert + assert new.getbbox() == (0, 0, 100, 100) + assert new.getpixel((0, 0)) == WHITE + assert new.getpixel((50, 50)) == CYAN - def test_lighter_image(self): - # Arrange - im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") - im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + +def test_lighter_image(): + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + + # Act + new = ImageChops.lighter(im1, im2) + + # Assert + assert_image_equal(new, im1) + + +def test_lighter_pixel(): + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: # Act new = ImageChops.lighter(im1, im2) - # Assert - self.assert_image_equal(new, im1) + # Assert + assert new.getpixel((50, 50)) == (255, 255, 127) - def test_lighter_pixel(self): - # Arrange - im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") - # Act - new = ImageChops.lighter(im1, im2) +def test_multiply_black(): + """If you multiply an image with a solid black image, + the result is black.""" + # Arrange + im1 = hopper() + black = Image.new("RGB", im1.size, "black") - # Assert - self.assertEqual(new.getpixel((50, 50)), (255, 255, 127)) + # Act + new = ImageChops.multiply(im1, black) - def test_multiply_black(self): - """If you multiply an image with a solid black image, - the result is black.""" - # Arrange - im1 = hopper() - black = Image.new("RGB", im1.size, "black") + # Assert + assert_image_equal(new, black) - # Act - new = ImageChops.multiply(im1, black) - # Assert - self.assert_image_equal(new, black) - - def test_multiply_green(self): - # Arrange - im = Image.open("Tests/images/imagedraw_floodfill_RGB.png") +def test_multiply_green(): + # Arrange + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: green = Image.new("RGB", im.size, "green") # Act new = ImageChops.multiply(im, green) - # Assert - self.assertEqual(new.getbbox(), (25, 25, 76, 76)) - self.assertEqual(new.getpixel((25, 25)), DARK_GREEN) - self.assertEqual(new.getpixel((50, 50)), BLACK) + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((25, 25)) == DARK_GREEN + assert new.getpixel((50, 50)) == BLACK - def test_multiply_white(self): - """If you multiply with a solid white image, - the image is unaffected.""" - # Arrange - im1 = hopper() - white = Image.new("RGB", im1.size, "white") - # Act - new = ImageChops.multiply(im1, white) +def test_multiply_white(): + """If you multiply with a solid white image, the image is unaffected.""" + # Arrange + im1 = hopper() + white = Image.new("RGB", im1.size, "white") - # Assert - self.assert_image_equal(new, im1) + # Act + new = ImageChops.multiply(im1, white) - def test_offset(self): - # Arrange - im = Image.open("Tests/images/imagedraw_ellipse_RGB.png") - xoffset = 45 - yoffset = 20 + # Assert + assert_image_equal(new, im1) + + +def test_offset(): + # Arrange + xoffset = 45 + yoffset = 20 + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im: # Act new = ImageChops.offset(im, xoffset, yoffset) # Assert - self.assertEqual(new.getbbox(), (0, 45, 100, 96)) - self.assertEqual(new.getpixel((50, 50)), BLACK) - self.assertEqual(new.getpixel((50 + xoffset, 50 + yoffset)), DARK_GREEN) + assert new.getbbox() == (0, 45, 100, 96) + assert new.getpixel((50, 50)) == BLACK + assert new.getpixel((50 + xoffset, 50 + yoffset)) == DARK_GREEN # Test no yoffset - self.assertEqual( - ImageChops.offset(im, xoffset), ImageChops.offset(im, xoffset, xoffset) - ) + assert ImageChops.offset(im, xoffset) == ImageChops.offset(im, xoffset, xoffset) - def test_screen(self): - # Arrange - im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") - im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") - # Act - new = ImageChops.screen(im1, im2) +def test_screen(): + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Assert - self.assertEqual(new.getbbox(), (25, 25, 76, 76)) - self.assertEqual(new.getpixel((50, 50)), ORANGE) + # Act + new = ImageChops.screen(im1, im2) - def test_subtract(self): - # Arrange - im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") - im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((50, 50)) == ORANGE + + +def test_subtract(): + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + + # Act + new = ImageChops.subtract(im1, im2) + + # Assert + assert new.getbbox() == (25, 50, 76, 76) + assert new.getpixel((50, 51)) == GREEN + assert new.getpixel((50, 52)) == BLACK + + +def test_subtract_scale_offset(): + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + + # Act + new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) + + # Assert + assert new.getbbox() == (0, 0, 100, 100) + assert new.getpixel((50, 50)) == (100, 202, 100) + + +def test_subtract_clip(): + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: # Act new = ImageChops.subtract(im1, im2) - # Assert - self.assertEqual(new.getbbox(), (25, 50, 76, 76)) - self.assertEqual(new.getpixel((50, 50)), GREEN) - self.assertEqual(new.getpixel((50, 51)), BLACK) + # Assert + assert new.getpixel((50, 50)) == (0, 0, 127) - def test_subtract_scale_offset(self): - # Arrange - im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") - im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") - # Act - new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) +def test_subtract_modulo(): + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Assert - self.assertEqual(new.getbbox(), (0, 0, 100, 100)) - self.assertEqual(new.getpixel((50, 50)), (100, 202, 100)) + # Act + new = ImageChops.subtract_modulo(im1, im2) - def test_subtract_clip(self): - # Arrange - im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + # Assert + assert new.getbbox() == (25, 50, 76, 76) + assert new.getpixel((50, 51)) == GREEN + assert new.getpixel((50, 52)) == BLACK - # Act - new = ImageChops.subtract(im1, im2) - # Assert - self.assertEqual(new.getpixel((50, 50)), (0, 0, 127)) - - def test_subtract_modulo(self): - # Arrange - im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") - im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") +def test_subtract_modulo_no_clip(): + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: # Act new = ImageChops.subtract_modulo(im1, im2) - # Assert - self.assertEqual(new.getbbox(), (25, 50, 76, 76)) - self.assertEqual(new.getpixel((50, 50)), GREEN) - self.assertEqual(new.getpixel((50, 51)), BLACK) + # Assert + assert new.getpixel((50, 50)) == (241, 167, 127) - def test_subtract_modulo_no_clip(self): - # Arrange - im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") - # Act - new = ImageChops.subtract_modulo(im1, im2) +def test_soft_light(): + # Arrange + im1 = Image.open("Tests/images/hopper.png") + im2 = Image.open("Tests/images/hopper-XYZ.png") - # Assert - self.assertEqual(new.getpixel((50, 50)), (241, 167, 127)) + # Act + new = ImageChops.soft_light(im1, im2) - def test_logical(self): - def table(op, a, b): - out = [] - for x in (a, b): - imx = Image.new("1", (1, 1), x) - for y in (a, b): - imy = Image.new("1", (1, 1), y) - out.append(op(imx, imy).getpixel((0, 0))) - return tuple(out) + # Assert + assert new.getpixel((64, 64)) == (163, 54, 32) + assert new.getpixel((15, 100)) == (1, 1, 3) - self.assertEqual(table(ImageChops.logical_and, 0, 1), (0, 0, 0, 255)) - self.assertEqual(table(ImageChops.logical_or, 0, 1), (0, 255, 255, 255)) - self.assertEqual(table(ImageChops.logical_xor, 0, 1), (0, 255, 255, 0)) - self.assertEqual(table(ImageChops.logical_and, 0, 128), (0, 0, 0, 255)) - self.assertEqual(table(ImageChops.logical_or, 0, 128), (0, 255, 255, 255)) - self.assertEqual(table(ImageChops.logical_xor, 0, 128), (0, 255, 255, 0)) +def test_hard_light(): + # Arrange + im1 = Image.open("Tests/images/hopper.png") + im2 = Image.open("Tests/images/hopper-XYZ.png") - self.assertEqual(table(ImageChops.logical_and, 0, 255), (0, 0, 0, 255)) - self.assertEqual(table(ImageChops.logical_or, 0, 255), (0, 255, 255, 255)) - self.assertEqual(table(ImageChops.logical_xor, 0, 255), (0, 255, 255, 0)) + # Act + new = ImageChops.hard_light(im1, im2) + + # Assert + assert new.getpixel((64, 64)) == (144, 50, 27) + assert new.getpixel((15, 100)) == (1, 1, 2) + + +def test_overlay(): + # Arrange + im1 = Image.open("Tests/images/hopper.png") + im2 = Image.open("Tests/images/hopper-XYZ.png") + + # Act + new = ImageChops.overlay(im1, im2) + + # Assert + assert new.getpixel((64, 64)) == (159, 50, 27) + assert new.getpixel((15, 100)) == (1, 1, 2) + + +def test_logical(): + def table(op, a, b): + out = [] + for x in (a, b): + imx = Image.new("1", (1, 1), x) + for y in (a, b): + imy = Image.new("1", (1, 1), y) + out.append(op(imx, imy).getpixel((0, 0))) + return tuple(out) + + assert table(ImageChops.logical_and, 0, 1) == (0, 0, 0, 255) + assert table(ImageChops.logical_or, 0, 1) == (0, 255, 255, 255) + assert table(ImageChops.logical_xor, 0, 1) == (0, 255, 255, 0) + + assert table(ImageChops.logical_and, 0, 128) == (0, 0, 0, 255) + assert table(ImageChops.logical_or, 0, 128) == (0, 255, 255, 255) + assert table(ImageChops.logical_xor, 0, 128) == (0, 255, 255, 0) + + assert table(ImageChops.logical_and, 0, 255) == (0, 0, 0, 255) + assert table(ImageChops.logical_or, 0, 255) == (0, 255, 255, 255) + assert table(ImageChops.logical_xor, 0, 255) == (0, 255, 255, 0) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 97f81fb3f..9fab41746 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,10 +1,14 @@ import datetime import os +import re +import shutil from io import BytesIO -from PIL import Image, ImageMode +import pytest -from .helper import PillowTestCase, hopper +from PIL import Image, ImageMode, features + +from .helper import assert_image, assert_image_equal, assert_image_similar, hopper try: from PIL import ImageCms @@ -12,7 +16,7 @@ try: ImageCms.core.profile_open except ImportError: - # Skipped via setUp() + # Skipped via setup_module() pass @@ -20,590 +24,567 @@ SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc" HAVE_PROFILE = os.path.exists(SRGB) -class TestImageCms(PillowTestCase): - def setUp(self): - try: - from PIL import ImageCms +def setup_module(): + try: + from PIL import ImageCms - # need to hit getattr to trigger the delayed import error - ImageCms.core.profile_open - except ImportError as v: - self.skipTest(v) + # need to hit getattr to trigger the delayed import error + ImageCms.core.profile_open + except ImportError as v: + pytest.skip(str(v)) - def skip_missing(self): - if not HAVE_PROFILE: - self.skipTest("SRGB profile not available") - def test_sanity(self): +def skip_missing(): + if not HAVE_PROFILE: + pytest.skip("SRGB profile not available") - # basic smoke test. - # this mostly follows the cms_test outline. - v = ImageCms.versions() # should return four strings - self.assertEqual(v[0], "1.0.0 pil") - self.assertEqual(list(map(type, v)), [str, str, str, str]) +def test_sanity(): + # basic smoke test. + # this mostly follows the cms_test outline. - # internal version number - self.assertRegex(ImageCms.core.littlecms_version, r"\d+\.\d+$") + v = ImageCms.versions() # should return four strings + assert v[0] == "1.0.0 pil" + assert list(map(type, v)) == [str, str, str, str] - self.skip_missing() - i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) - self.assert_image(i, "RGB", (128, 128)) + # internal version number + assert re.search(r"\d+\.\d+(\.\d+)?$", features.version_module("littlecms2")) - i = hopper() - ImageCms.profileToProfile(i, SRGB, SRGB, inPlace=True) - self.assert_image(i, "RGB", (128, 128)) + skip_missing() + i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) + assert_image(i, "RGB", (128, 128)) + i = hopper() + ImageCms.profileToProfile(i, SRGB, SRGB, inPlace=True) + assert_image(i, "RGB", (128, 128)) + + t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") + i = ImageCms.applyTransform(hopper(), t) + assert_image(i, "RGB", (128, 128)) + + with hopper() as i: t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") - i = ImageCms.applyTransform(hopper(), t) - self.assert_image(i, "RGB", (128, 128)) + ImageCms.applyTransform(hopper(), t, inPlace=True) + assert_image(i, "RGB", (128, 128)) - with hopper() as i: - t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") - ImageCms.applyTransform(hopper(), t, inPlace=True) - self.assert_image(i, "RGB", (128, 128)) + p = ImageCms.createProfile("sRGB") + o = ImageCms.getOpenProfile(SRGB) + t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB") + i = ImageCms.applyTransform(hopper(), t) + assert_image(i, "RGB", (128, 128)) - p = ImageCms.createProfile("sRGB") - o = ImageCms.getOpenProfile(SRGB) - t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB") - i = ImageCms.applyTransform(hopper(), t) - self.assert_image(i, "RGB", (128, 128)) + t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB") + assert t.inputMode == "RGB" + assert t.outputMode == "RGB" + i = ImageCms.applyTransform(hopper(), t) + assert_image(i, "RGB", (128, 128)) - t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB") - self.assertEqual(t.inputMode, "RGB") - self.assertEqual(t.outputMode, "RGB") - i = ImageCms.applyTransform(hopper(), t) - self.assert_image(i, "RGB", (128, 128)) + # test PointTransform convenience API + hopper().point(t) - # test PointTransform convenience API - hopper().point(t) - def test_name(self): - self.skip_missing() - # get profile information for file - self.assertEqual( - ImageCms.getProfileName(SRGB).strip(), - "IEC 61966-2-1 Default RGB Colour Space - sRGB", - ) +def test_name(): + skip_missing() + # get profile information for file + assert ( + ImageCms.getProfileName(SRGB).strip() + == "IEC 61966-2-1 Default RGB Colour Space - sRGB" + ) - def test_info(self): - self.skip_missing() - self.assertEqual( - ImageCms.getProfileInfo(SRGB).splitlines(), - [ - "sRGB IEC61966-2-1 black scaled", - "", - "Copyright International Color Consortium, 2009", - "", - ], - ) - def test_copyright(self): - self.skip_missing() - self.assertEqual( - ImageCms.getProfileCopyright(SRGB).strip(), - "Copyright International Color Consortium, 2009", - ) +def test_info(): + skip_missing() + assert ImageCms.getProfileInfo(SRGB).splitlines() == [ + "sRGB IEC61966-2-1 black scaled", + "", + "Copyright International Color Consortium, 2009", + "", + ] - def test_manufacturer(self): - self.skip_missing() - self.assertEqual(ImageCms.getProfileManufacturer(SRGB).strip(), "") - def test_model(self): - self.skip_missing() - self.assertEqual( - ImageCms.getProfileModel(SRGB).strip(), - "IEC 61966-2-1 Default RGB Colour Space - sRGB", - ) +def test_copyright(): + skip_missing() + assert ( + ImageCms.getProfileCopyright(SRGB).strip() + == "Copyright International Color Consortium, 2009" + ) - def test_description(self): - self.skip_missing() - self.assertEqual( - ImageCms.getProfileDescription(SRGB).strip(), - "sRGB IEC61966-2-1 black scaled", - ) - def test_intent(self): - self.skip_missing() - self.assertEqual(ImageCms.getDefaultIntent(SRGB), 0) - self.assertEqual( - ImageCms.isIntentSupported( - SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT - ), - 1, - ) +def test_manufacturer(): + skip_missing() + assert ImageCms.getProfileManufacturer(SRGB).strip() == "" - def test_profile_object(self): - # same, using profile object - p = ImageCms.createProfile("sRGB") - # self.assertEqual(ImageCms.getProfileName(p).strip(), - # 'sRGB built-in - (lcms internal)') - # self.assertEqual(ImageCms.getProfileInfo(p).splitlines(), - # ['sRGB built-in', '', 'WhitePoint : D65 (daylight)', '', '']) - self.assertEqual(ImageCms.getDefaultIntent(p), 0) - self.assertEqual( - ImageCms.isIntentSupported( - p, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT - ), - 1, - ) - def test_extensions(self): - # extensions +def test_model(): + skip_missing() + assert ( + ImageCms.getProfileModel(SRGB).strip() + == "IEC 61966-2-1 Default RGB Colour Space - sRGB" + ) - with Image.open("Tests/images/rgb.jpg") as i: - p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) - self.assertEqual( - ImageCms.getProfileName(p).strip(), - "IEC 61966-2.1 Default RGB colour space - sRGB", - ) - def test_exceptions(self): - # Test mode mismatch - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - self.assertRaises(ValueError, t.apply_in_place, hopper("RGBA")) +def test_description(): + skip_missing() + assert ( + ImageCms.getProfileDescription(SRGB).strip() == "sRGB IEC61966-2-1 black scaled" + ) - # the procedural pyCMS API uses PyCMSError for all sorts of errors - with hopper() as im: - self.assertRaises( - ImageCms.PyCMSError, ImageCms.profileToProfile, im, "foo", "bar" - ) - self.assertRaises( - ImageCms.PyCMSError, ImageCms.buildTransform, "foo", "bar", "RGB", "RGB" - ) - self.assertRaises(ImageCms.PyCMSError, ImageCms.getProfileName, None) - self.skip_missing() - self.assertRaises( - ImageCms.PyCMSError, ImageCms.isIntentSupported, SRGB, None, None - ) - def test_display_profile(self): - # try fetching the profile for the current display device - ImageCms.get_display_profile() +def test_intent(): + skip_missing() + assert ImageCms.getDefaultIntent(SRGB) == 0 + support = ImageCms.isIntentSupported( + SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT + ) + assert support == 1 - def test_lab_color_profile(self): - ImageCms.createProfile("LAB", 5000) - ImageCms.createProfile("LAB", 6500) - def test_unsupported_color_space(self): - self.assertRaises(ImageCms.PyCMSError, ImageCms.createProfile, "unsupported") +def test_profile_object(): + # same, using profile object + p = ImageCms.createProfile("sRGB") + # assert ImageCms.getProfileName(p).strip() == "sRGB built-in - (lcms internal)" + # assert ImageCms.getProfileInfo(p).splitlines() == + # ["sRGB built-in", "", "WhitePoint : D65 (daylight)", "", ""] + assert ImageCms.getDefaultIntent(p) == 0 + support = ImageCms.isIntentSupported( + p, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT + ) + assert support == 1 - def test_invalid_color_temperature(self): - self.assertRaises(ImageCms.PyCMSError, ImageCms.createProfile, "LAB", "invalid") - def test_simple_lab(self): - i = Image.new("RGB", (10, 10), (128, 128, 128)) +def test_extensions(): + # extensions - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") + with Image.open("Tests/images/rgb.jpg") as i: + p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) + assert ( + ImageCms.getProfileName(p).strip() + == "IEC 61966-2.1 Default RGB colour space - sRGB" + ) - i_lab = ImageCms.applyTransform(i, t) - self.assertEqual(i_lab.mode, "LAB") +def test_exceptions(): + # Test mode mismatch + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") + with pytest.raises(ValueError): + t.apply_in_place(hopper("RGBA")) - k = i_lab.getpixel((0, 0)) - # not a linear luminance map. so L != 128: - self.assertEqual(k, (137, 128, 128)) + # the procedural pyCMS API uses PyCMSError for all sorts of errors + with hopper() as im: + with pytest.raises(ImageCms.PyCMSError): + ImageCms.profileToProfile(im, "foo", "bar") + with pytest.raises(ImageCms.PyCMSError): + ImageCms.buildTransform("foo", "bar", "RGB", "RGB") + with pytest.raises(ImageCms.PyCMSError): + ImageCms.getProfileName(None) + skip_missing() + with pytest.raises(ImageCms.PyCMSError): + ImageCms.isIntentSupported(SRGB, None, None) - l_data = i_lab.getdata(0) - a_data = i_lab.getdata(1) - b_data = i_lab.getdata(2) - self.assertEqual(list(l_data), [137] * 100) - self.assertEqual(list(a_data), [128] * 100) - self.assertEqual(list(b_data), [128] * 100) +def test_display_profile(): + # try fetching the profile for the current display device + ImageCms.get_display_profile() - def test_lab_color(self): - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") - # Need to add a type mapping for some PIL type to TYPE_Lab_8 in - # findLCMSType, and have that mapping work back to a PIL mode - # (likely RGB). - i = ImageCms.applyTransform(hopper(), t) - self.assert_image(i, "LAB", (128, 128)) +def test_lab_color_profile(): + ImageCms.createProfile("LAB", 5000) + ImageCms.createProfile("LAB", 6500) - # i.save('temp.lab.tif') # visually verified vs PS. - target = Image.open("Tests/images/hopper.Lab.tif") +def test_unsupported_color_space(): + with pytest.raises(ImageCms.PyCMSError): + ImageCms.createProfile("unsupported") - self.assert_image_similar(i, target, 3.5) - def test_lab_srgb(self): - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") +def test_invalid_color_temperature(): + with pytest.raises(ImageCms.PyCMSError): + ImageCms.createProfile("LAB", "invalid") - img = Image.open("Tests/images/hopper.Lab.tif") +def test_simple_lab(): + i = Image.new("RGB", (10, 10), (128, 128, 128)) + + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") + + i_lab = ImageCms.applyTransform(i, t) + + assert i_lab.mode == "LAB" + + k = i_lab.getpixel((0, 0)) + # not a linear luminance map. so L != 128: + assert k == (137, 128, 128) + + l_data = i_lab.getdata(0) + a_data = i_lab.getdata(1) + b_data = i_lab.getdata(2) + + assert list(l_data) == [137] * 100 + assert list(a_data) == [128] * 100 + assert list(b_data) == [128] * 100 + + +def test_lab_color(): + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") + + # Need to add a type mapping for some PIL type to TYPE_Lab_8 in findLCMSType, and + # have that mapping work back to a PIL mode (likely RGB). + i = ImageCms.applyTransform(hopper(), t) + assert_image(i, "LAB", (128, 128)) + + # i.save('temp.lab.tif') # visually verified vs PS. + + with Image.open("Tests/images/hopper.Lab.tif") as target: + assert_image_similar(i, target, 3.5) + + +def test_lab_srgb(): + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") + + with Image.open("Tests/images/hopper.Lab.tif") as img: img_srgb = ImageCms.applyTransform(img, t) - # img_srgb.save('temp.srgb.tif') # visually verified vs ps. + # img_srgb.save('temp.srgb.tif') # visually verified vs ps. - self.assert_image_similar(hopper(), img_srgb, 30) - self.assertTrue(img_srgb.info["icc_profile"]) + assert_image_similar(hopper(), img_srgb, 30) + assert img_srgb.info["icc_profile"] - profile = ImageCmsProfile(BytesIO(img_srgb.info["icc_profile"])) - self.assertIn("sRGB", ImageCms.getProfileDescription(profile)) + profile = ImageCmsProfile(BytesIO(img_srgb.info["icc_profile"])) + assert "sRGB" in ImageCms.getProfileDescription(profile) - def test_lab_roundtrip(self): - # check to see if we're at least internally consistent. - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") - t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") +def test_lab_roundtrip(): + # check to see if we're at least internally consistent. + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") - i = ImageCms.applyTransform(hopper(), t) + t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - self.assertEqual(i.info["icc_profile"], ImageCmsProfile(pLab).tobytes()) + i = ImageCms.applyTransform(hopper(), t) - out = ImageCms.applyTransform(i, t2) + assert i.info["icc_profile"] == ImageCmsProfile(pLab).tobytes() - self.assert_image_similar(hopper(), out, 2) + out = ImageCms.applyTransform(i, t2) - def test_profile_tobytes(self): - with Image.open("Tests/images/rgb.jpg") as i: - p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) + assert_image_similar(hopper(), out, 2) - p2 = ImageCms.getOpenProfile(BytesIO(p.tobytes())) - # not the same bytes as the original icc_profile, - # but it does roundtrip - self.assertEqual(p.tobytes(), p2.tobytes()) - self.assertEqual(ImageCms.getProfileName(p), ImageCms.getProfileName(p2)) - self.assertEqual( - ImageCms.getProfileDescription(p), ImageCms.getProfileDescription(p2) - ) +def test_profile_tobytes(): + with Image.open("Tests/images/rgb.jpg") as i: + p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) - def test_extended_information(self): - self.skip_missing() - o = ImageCms.getOpenProfile(SRGB) - p = o.profile + p2 = ImageCms.getOpenProfile(BytesIO(p.tobytes())) - def assert_truncated_tuple_equal(tup1, tup2, digits=10): - # Helper function to reduce precision of tuples of floats - # recursively and then check equality. - power = 10 ** digits + # not the same bytes as the original icc_profile, but it does roundtrip + assert p.tobytes() == p2.tobytes() + assert ImageCms.getProfileName(p) == ImageCms.getProfileName(p2) + assert ImageCms.getProfileDescription(p) == ImageCms.getProfileDescription(p2) - def truncate_tuple(tuple_or_float): - return tuple( - truncate_tuple(val) - if isinstance(val, tuple) - else int(val * power) / power - for val in tuple_or_float - ) - self.assertEqual(truncate_tuple(tup1), truncate_tuple(tup2)) +def test_extended_information(): + skip_missing() + o = ImageCms.getOpenProfile(SRGB) + p = o.profile - self.assertEqual(p.attributes, 4294967296) - assert_truncated_tuple_equal( - p.blue_colorant, + def assert_truncated_tuple_equal(tup1, tup2, digits=10): + # Helper function to reduce precision of tuples of floats + # recursively and then check equality. + power = 10 ** digits + + def truncate_tuple(tuple_or_float): + return tuple( + truncate_tuple(val) + if isinstance(val, tuple) + else int(val * power) / power + for val in tuple_or_float + ) + + assert truncate_tuple(tup1) == truncate_tuple(tup2) + + assert p.attributes == 4294967296 + assert_truncated_tuple_equal( + p.blue_colorant, + ( + (0.14306640625, 0.06060791015625, 0.7140960693359375), + (0.1558847490315394, 0.06603820639433387, 0.06060791015625), + ), + ) + assert_truncated_tuple_equal( + p.blue_primary, + ( + (0.14306641366715667, 0.06060790921083026, 0.7140960805782015), + (0.15588475410450106, 0.06603820408959558, 0.06060790921083026), + ), + ) + assert_truncated_tuple_equal( + p.chromatic_adaptation, + ( ( - (0.14306640625, 0.06060791015625, 0.7140960693359375), - (0.1558847490315394, 0.06603820639433387, 0.06060791015625), + (1.04791259765625, 0.0229339599609375, -0.050201416015625), + (0.02960205078125, 0.9904632568359375, -0.0170745849609375), + (-0.009246826171875, 0.0150604248046875, 0.7517852783203125), ), - ) - assert_truncated_tuple_equal( - p.blue_primary, ( - (0.14306641366715667, 0.06060790921083026, 0.7140960805782015), - (0.15588475410450106, 0.06603820408959558, 0.06060790921083026), + (1.0267159024652783, 0.022470062342089134, 0.0229339599609375), + (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), + (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875), ), - ) - assert_truncated_tuple_equal( - p.chromatic_adaptation, - ( - ( - (1.04791259765625, 0.0229339599609375, -0.050201416015625), - (0.02960205078125, 0.9904632568359375, -0.0170745849609375), - (-0.009246826171875, 0.0150604248046875, 0.7517852783203125), - ), - ( - (1.0267159024652783, 0.022470062342089134, 0.0229339599609375), - (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), - (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875), - ), - ), - ) - self.assertIsNone(p.chromaticity) - self.assertEqual( - p.clut, - { - 0: (False, False, True), - 1: (False, False, True), - 2: (False, False, True), - 3: (False, False, True), - }, - ) + ), + ) + assert p.chromaticity is None + assert p.clut == { + 0: (False, False, True), + 1: (False, False, True), + 2: (False, False, True), + 3: (False, False, True), + } - self.assertIsNone(p.colorant_table) - self.assertIsNone(p.colorant_table_out) - self.assertIsNone(p.colorimetric_intent) - self.assertEqual(p.connection_space, "XYZ ") - self.assertEqual(p.copyright, "Copyright International Color Consortium, 2009") - self.assertEqual(p.creation_date, datetime.datetime(2009, 2, 27, 21, 36, 31)) - self.assertEqual(p.device_class, "mntr") - assert_truncated_tuple_equal( - p.green_colorant, - ( - (0.3851470947265625, 0.7168731689453125, 0.097076416015625), - (0.32119769927720654, 0.5978443449048152, 0.7168731689453125), - ), - ) - assert_truncated_tuple_equal( - p.green_primary, - ( - (0.3851470888162112, 0.7168731974161346, 0.09707641738998518), - (0.32119768793686687, 0.5978443567149709, 0.7168731974161346), - ), - ) - self.assertEqual(p.header_flags, 0) - self.assertEqual(p.header_manufacturer, "\x00\x00\x00\x00") - self.assertEqual(p.header_model, "\x00\x00\x00\x00") - self.assertEqual( - p.icc_measurement_condition, - { - "backing": (0.0, 0.0, 0.0), - "flare": 0.0, - "geo": "unknown", - "observer": 1, - "illuminant_type": "D65", - }, - ) - self.assertEqual(p.icc_version, 33554432) - self.assertIsNone(p.icc_viewing_condition) - self.assertEqual( - p.intent_supported, - { - 0: (True, True, True), - 1: (True, True, True), - 2: (True, True, True), - 3: (True, True, True), - }, - ) - self.assertTrue(p.is_matrix_shaper) - self.assertEqual(p.luminance, ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0))) - self.assertIsNone(p.manufacturer) - assert_truncated_tuple_equal( - p.media_black_point, - ( - (0.012054443359375, 0.0124969482421875, 0.01031494140625), - (0.34573304157549234, 0.35842450765864337, 0.0124969482421875), - ), - ) - assert_truncated_tuple_equal( - p.media_white_point, - ( - (0.964202880859375, 1.0, 0.8249053955078125), - (0.3457029219802284, 0.3585375327567059, 1.0), - ), - ) - assert_truncated_tuple_equal( - (p.media_white_point_temperature,), (5000.722328847392,) - ) - self.assertEqual(p.model, "IEC 61966-2-1 Default RGB Colour Space - sRGB") + assert p.colorant_table is None + assert p.colorant_table_out is None + assert p.colorimetric_intent is None + assert p.connection_space == "XYZ " + assert p.copyright == "Copyright International Color Consortium, 2009" + assert p.creation_date == datetime.datetime(2009, 2, 27, 21, 36, 31) + assert p.device_class == "mntr" + assert_truncated_tuple_equal( + p.green_colorant, + ( + (0.3851470947265625, 0.7168731689453125, 0.097076416015625), + (0.32119769927720654, 0.5978443449048152, 0.7168731689453125), + ), + ) + assert_truncated_tuple_equal( + p.green_primary, + ( + (0.3851470888162112, 0.7168731974161346, 0.09707641738998518), + (0.32119768793686687, 0.5978443567149709, 0.7168731974161346), + ), + ) + assert p.header_flags == 0 + assert p.header_manufacturer == "\x00\x00\x00\x00" + assert p.header_model == "\x00\x00\x00\x00" + assert p.icc_measurement_condition == { + "backing": (0.0, 0.0, 0.0), + "flare": 0.0, + "geo": "unknown", + "observer": 1, + "illuminant_type": "D65", + } + assert p.icc_version == 33554432 + assert p.icc_viewing_condition is None + assert p.intent_supported == { + 0: (True, True, True), + 1: (True, True, True), + 2: (True, True, True), + 3: (True, True, True), + } + assert p.is_matrix_shaper + assert p.luminance == ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0)) + assert p.manufacturer is None + assert_truncated_tuple_equal( + p.media_black_point, + ( + (0.012054443359375, 0.0124969482421875, 0.01031494140625), + (0.34573304157549234, 0.35842450765864337, 0.0124969482421875), + ), + ) + assert_truncated_tuple_equal( + p.media_white_point, + ( + (0.964202880859375, 1.0, 0.8249053955078125), + (0.3457029219802284, 0.3585375327567059, 1.0), + ), + ) + assert_truncated_tuple_equal( + (p.media_white_point_temperature,), (5000.722328847392,) + ) + assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB" - self.assertIsNone(p.perceptual_rendering_intent_gamut) + assert p.perceptual_rendering_intent_gamut is None - self.assertEqual(p.profile_description, "sRGB IEC61966-2-1 black scaled") - self.assertEqual(p.profile_id, b")\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r") - assert_truncated_tuple_equal( - p.red_colorant, - ( - (0.436065673828125, 0.2224884033203125, 0.013916015625), - (0.6484536316398539, 0.3308524880306778, 0.2224884033203125), - ), - ) - assert_truncated_tuple_equal( - p.red_primary, - ( - (0.43606566581047446, 0.22248840582960838, 0.013916015621759925), - (0.6484536250319214, 0.3308524944738204, 0.22248840582960838), - ), - ) - self.assertEqual(p.rendering_intent, 0) - self.assertIsNone(p.saturation_rendering_intent_gamut) - self.assertIsNone(p.screening_description) - self.assertIsNone(p.target) - self.assertEqual(p.technology, "CRT ") - self.assertEqual(p.version, 2.0) - self.assertEqual( - p.viewing_condition, "Reference Viewing Condition in IEC 61966-2-1" - ) - self.assertEqual(p.xcolor_space, "RGB ") + assert p.profile_description == "sRGB IEC61966-2-1 black scaled" + assert p.profile_id == b")\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r" + assert_truncated_tuple_equal( + p.red_colorant, + ( + (0.436065673828125, 0.2224884033203125, 0.013916015625), + (0.6484536316398539, 0.3308524880306778, 0.2224884033203125), + ), + ) + assert_truncated_tuple_equal( + p.red_primary, + ( + (0.43606566581047446, 0.22248840582960838, 0.013916015621759925), + (0.6484536250319214, 0.3308524944738204, 0.22248840582960838), + ), + ) + assert p.rendering_intent == 0 + assert p.saturation_rendering_intent_gamut is None + assert p.screening_description is None + assert p.target is None + assert p.technology == "CRT " + assert p.version == 2.0 + assert p.viewing_condition == "Reference Viewing Condition in IEC 61966-2-1" + assert p.xcolor_space == "RGB " - def test_deprecations(self): - self.skip_missing() - o = ImageCms.getOpenProfile(SRGB) - p = o.profile - def helper_deprecated(attr, expected): - result = self.assert_warning(DeprecationWarning, getattr, p, attr) - self.assertEqual(result, expected) +def test_non_ascii_path(tmp_path): + skip_missing() + tempfile = str(tmp_path / ("temp_" + chr(128) + ".icc")) + try: + shutil.copy(SRGB, tempfile) + except UnicodeEncodeError: + pytest.skip("Non-ASCII path could not be created") - # p.color_space - helper_deprecated("color_space", "RGB") + o = ImageCms.getOpenProfile(tempfile) + p = o.profile + assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB" - # p.pcs - helper_deprecated("pcs", "XYZ") - # p.product_copyright - helper_deprecated( - "product_copyright", "Copyright International Color Consortium, 2009" - ) +def test_profile_typesafety(): + """Profile init type safety - # p.product_desc - helper_deprecated("product_desc", "sRGB IEC61966-2-1 black scaled") + prepatch, these would segfault, postpatch they should emit a typeerror + """ - # p.product_description - helper_deprecated("product_description", "sRGB IEC61966-2-1 black scaled") + with pytest.raises(TypeError): + ImageCms.ImageCmsProfile(0).tobytes() + with pytest.raises(TypeError): + ImageCms.ImageCmsProfile(1).tobytes() - # p.product_manufacturer - helper_deprecated("product_manufacturer", "") - # p.product_model - helper_deprecated( - "product_model", "IEC 61966-2-1 Default RGB Colour Space - sRGB" - ) - - def test_profile_typesafety(self): - """ Profile init type safety - - prepatch, these would segfault, postpatch they should emit a typeerror - """ - - with self.assertRaises(TypeError): - ImageCms.ImageCmsProfile(0).tobytes() - with self.assertRaises(TypeError): - ImageCms.ImageCmsProfile(1).tobytes() - - def assert_aux_channel_preserved(self, mode, transform_in_place, preserved_channel): - def create_test_image(): - # set up test image with something interesting in the tested aux channel. - # fmt: off - nine_grid_deltas = [ # noqa: E131 - (-1, -1), (-1, 0), (-1, 1), - (0, -1), (0, 0), (0, 1), - (1, -1), (1, 0), (1, 1), - ] - # fmt: on - chans = [] - bands = ImageMode.getmode(mode).bands - for band_ndx in range(len(bands)): - channel_type = "L" # 8-bit unorm - channel_pattern = hopper(channel_type) - - # paste pattern with varying offsets to avoid correlation - # potentially hiding some bugs (like channels getting mixed). - paste_offset = ( - int(band_ndx / float(len(bands)) * channel_pattern.size[0]), - int(band_ndx / float(len(bands) * 2) * channel_pattern.size[1]), - ) - channel_data = Image.new(channel_type, channel_pattern.size) - for delta in nine_grid_deltas: - channel_data.paste( - channel_pattern, - tuple( - paste_offset[c] + delta[c] * channel_pattern.size[c] - for c in range(2) - ), - ) - chans.append(channel_data) - return Image.merge(mode, chans) - - source_image = create_test_image() - source_image_aux = source_image.getchannel(preserved_channel) - - # create some transform, it doesn't matter which one - source_profile = ImageCms.createProfile("sRGB") - destination_profile = ImageCms.createProfile("sRGB") - t = ImageCms.buildTransform( - source_profile, destination_profile, inMode=mode, outMode=mode - ) - - # apply transform - if transform_in_place: - ImageCms.applyTransform(source_image, t, inPlace=True) - result_image = source_image - else: - result_image = ImageCms.applyTransform(source_image, t, inPlace=False) - result_image_aux = result_image.getchannel(preserved_channel) - - self.assert_image_equal(source_image_aux, result_image_aux) - - def test_preserve_auxiliary_channels_rgba(self): - self.assert_aux_channel_preserved( - mode="RGBA", transform_in_place=False, preserved_channel="A" - ) - - def test_preserve_auxiliary_channels_rgba_in_place(self): - self.assert_aux_channel_preserved( - mode="RGBA", transform_in_place=True, preserved_channel="A" - ) - - def test_preserve_auxiliary_channels_rgbx(self): - self.assert_aux_channel_preserved( - mode="RGBX", transform_in_place=False, preserved_channel="X" - ) - - def test_preserve_auxiliary_channels_rgbx_in_place(self): - self.assert_aux_channel_preserved( - mode="RGBX", transform_in_place=True, preserved_channel="X" - ) - - def test_auxiliary_channels_isolated(self): - # test data in aux channels does not affect non-aux channels - aux_channel_formats = [ - # format, profile, color-only format, source test image - ("RGBA", "sRGB", "RGB", hopper("RGBA")), - ("RGBX", "sRGB", "RGB", hopper("RGBX")), - ("LAB", "LAB", "LAB", Image.open("Tests/images/hopper.Lab.tif")), +def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel): + def create_test_image(): + # set up test image with something interesting in the tested aux channel. + # fmt: off + nine_grid_deltas = [ + (-1, -1), (-1, 0), (-1, 1), + (0, -1), (0, 0), (0, 1), + (1, -1), (1, 0), (1, 1), ] - for src_format in aux_channel_formats: - for dst_format in aux_channel_formats: - for transform_in_place in [True, False]: - # inplace only if format doesn't change - if transform_in_place and src_format[0] != dst_format[0]: - continue + # fmt: on + chans = [] + bands = ImageMode.getmode(mode).bands + for band_ndx in range(len(bands)): + channel_type = "L" # 8-bit unorm + channel_pattern = hopper(channel_type) - # convert with and without AUX data, test colors are equal - source_profile = ImageCms.createProfile(src_format[1]) - destination_profile = ImageCms.createProfile(dst_format[1]) - source_image = src_format[3] - test_transform = ImageCms.buildTransform( - source_profile, - destination_profile, - inMode=src_format[0], - outMode=dst_format[0], + # paste pattern with varying offsets to avoid correlation + # potentially hiding some bugs (like channels getting mixed). + paste_offset = ( + int(band_ndx / len(bands) * channel_pattern.size[0]), + int(band_ndx / (len(bands) * 2) * channel_pattern.size[1]), + ) + channel_data = Image.new(channel_type, channel_pattern.size) + for delta in nine_grid_deltas: + channel_data.paste( + channel_pattern, + tuple( + paste_offset[c] + delta[c] * channel_pattern.size[c] + for c in range(2) + ), + ) + chans.append(channel_data) + return Image.merge(mode, chans) + + source_image = create_test_image() + source_image_aux = source_image.getchannel(preserved_channel) + + # create some transform, it doesn't matter which one + source_profile = ImageCms.createProfile("sRGB") + destination_profile = ImageCms.createProfile("sRGB") + t = ImageCms.buildTransform( + source_profile, destination_profile, inMode=mode, outMode=mode + ) + + # apply transform + if transform_in_place: + ImageCms.applyTransform(source_image, t, inPlace=True) + result_image = source_image + else: + result_image = ImageCms.applyTransform(source_image, t, inPlace=False) + result_image_aux = result_image.getchannel(preserved_channel) + + assert_image_equal(source_image_aux, result_image_aux) + + +def test_preserve_auxiliary_channels_rgba(): + assert_aux_channel_preserved( + mode="RGBA", transform_in_place=False, preserved_channel="A" + ) + + +def test_preserve_auxiliary_channels_rgba_in_place(): + assert_aux_channel_preserved( + mode="RGBA", transform_in_place=True, preserved_channel="A" + ) + + +def test_preserve_auxiliary_channels_rgbx(): + assert_aux_channel_preserved( + mode="RGBX", transform_in_place=False, preserved_channel="X" + ) + + +def test_preserve_auxiliary_channels_rgbx_in_place(): + assert_aux_channel_preserved( + mode="RGBX", transform_in_place=True, preserved_channel="X" + ) + + +def test_auxiliary_channels_isolated(): + # test data in aux channels does not affect non-aux channels + aux_channel_formats = [ + # format, profile, color-only format, source test image + ("RGBA", "sRGB", "RGB", hopper("RGBA")), + ("RGBX", "sRGB", "RGB", hopper("RGBX")), + ("LAB", "LAB", "LAB", Image.open("Tests/images/hopper.Lab.tif")), + ] + for src_format in aux_channel_formats: + for dst_format in aux_channel_formats: + for transform_in_place in [True, False]: + # inplace only if format doesn't change + if transform_in_place and src_format[0] != dst_format[0]: + continue + + # convert with and without AUX data, test colors are equal + source_profile = ImageCms.createProfile(src_format[1]) + destination_profile = ImageCms.createProfile(dst_format[1]) + source_image = src_format[3] + test_transform = ImageCms.buildTransform( + source_profile, + destination_profile, + inMode=src_format[0], + outMode=dst_format[0], + ) + + # test conversion from aux-ful source + if transform_in_place: + test_image = source_image.copy() + ImageCms.applyTransform(test_image, test_transform, inPlace=True) + else: + test_image = ImageCms.applyTransform( + source_image, test_transform, inPlace=False ) - # test conversion from aux-ful source - if transform_in_place: - test_image = source_image.copy() - ImageCms.applyTransform( - test_image, test_transform, inPlace=True - ) - else: - test_image = ImageCms.applyTransform( - source_image, test_transform, inPlace=False - ) + # reference conversion from aux-less source + reference_transform = ImageCms.buildTransform( + source_profile, + destination_profile, + inMode=src_format[2], + outMode=dst_format[2], + ) + reference_image = ImageCms.applyTransform( + source_image.convert(src_format[2]), reference_transform + ) - # reference conversion from aux-less source - reference_transform = ImageCms.buildTransform( - source_profile, - destination_profile, - inMode=src_format[2], - outMode=dst_format[2], - ) - reference_image = ImageCms.applyTransform( - source_image.convert(src_format[2]), reference_transform - ) - - self.assert_image_equal( - test_image.convert(dst_format[2]), reference_image - ) + assert_image_equal(test_image.convert(dst_format[2]), reference_image) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index e4a7b7dfe..b5d693796 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -1,184 +1,193 @@ +import pytest + from PIL import Image, ImageColor -from .helper import PillowTestCase + +def test_hash(): + # short 3 components + assert (255, 0, 0) == ImageColor.getrgb("#f00") + assert (0, 255, 0) == ImageColor.getrgb("#0f0") + assert (0, 0, 255) == ImageColor.getrgb("#00f") + + # short 4 components + assert (255, 0, 0, 0) == ImageColor.getrgb("#f000") + assert (0, 255, 0, 0) == ImageColor.getrgb("#0f00") + assert (0, 0, 255, 0) == ImageColor.getrgb("#00f0") + assert (0, 0, 0, 255) == ImageColor.getrgb("#000f") + + # long 3 components + assert (222, 0, 0) == ImageColor.getrgb("#de0000") + assert (0, 222, 0) == ImageColor.getrgb("#00de00") + assert (0, 0, 222) == ImageColor.getrgb("#0000de") + + # long 4 components + assert (222, 0, 0, 0) == ImageColor.getrgb("#de000000") + assert (0, 222, 0, 0) == ImageColor.getrgb("#00de0000") + assert (0, 0, 222, 0) == ImageColor.getrgb("#0000de00") + assert (0, 0, 0, 222) == ImageColor.getrgb("#000000de") + + # case insensitivity + assert ImageColor.getrgb("#DEF") == ImageColor.getrgb("#def") + assert ImageColor.getrgb("#CDEF") == ImageColor.getrgb("#cdef") + assert ImageColor.getrgb("#DEFDEF") == ImageColor.getrgb("#defdef") + assert ImageColor.getrgb("#CDEFCDEF") == ImageColor.getrgb("#cdefcdef") + + # not a number + with pytest.raises(ValueError): + ImageColor.getrgb("#fo0") + with pytest.raises(ValueError): + ImageColor.getrgb("#fo00") + with pytest.raises(ValueError): + ImageColor.getrgb("#fo0000") + with pytest.raises(ValueError): + ImageColor.getrgb("#fo000000") + + # wrong number of components + with pytest.raises(ValueError): + ImageColor.getrgb("#f0000") + with pytest.raises(ValueError): + ImageColor.getrgb("#f000000") + with pytest.raises(ValueError): + ImageColor.getrgb("#f00000000") + with pytest.raises(ValueError): + ImageColor.getrgb("#f000000000") + with pytest.raises(ValueError): + ImageColor.getrgb("#f00000 ") -class TestImageColor(PillowTestCase): - def test_hash(self): - # short 3 components - self.assertEqual((255, 0, 0), ImageColor.getrgb("#f00")) - self.assertEqual((0, 255, 0), ImageColor.getrgb("#0f0")) - self.assertEqual((0, 0, 255), ImageColor.getrgb("#00f")) +def test_colormap(): + assert (0, 0, 0) == ImageColor.getrgb("black") + assert (255, 255, 255) == ImageColor.getrgb("white") + assert (255, 255, 255) == ImageColor.getrgb("WHITE") - # short 4 components - self.assertEqual((255, 0, 0, 0), ImageColor.getrgb("#f000")) - self.assertEqual((0, 255, 0, 0), ImageColor.getrgb("#0f00")) - self.assertEqual((0, 0, 255, 0), ImageColor.getrgb("#00f0")) - self.assertEqual((0, 0, 0, 255), ImageColor.getrgb("#000f")) + with pytest.raises(ValueError): + ImageColor.getrgb("black ") - # long 3 components - self.assertEqual((222, 0, 0), ImageColor.getrgb("#de0000")) - self.assertEqual((0, 222, 0), ImageColor.getrgb("#00de00")) - self.assertEqual((0, 0, 222), ImageColor.getrgb("#0000de")) - # long 4 components - self.assertEqual((222, 0, 0, 0), ImageColor.getrgb("#de000000")) - self.assertEqual((0, 222, 0, 0), ImageColor.getrgb("#00de0000")) - self.assertEqual((0, 0, 222, 0), ImageColor.getrgb("#0000de00")) - self.assertEqual((0, 0, 0, 222), ImageColor.getrgb("#000000de")) +def test_functions(): + # rgb numbers + assert (255, 0, 0) == ImageColor.getrgb("rgb(255,0,0)") + assert (0, 255, 0) == ImageColor.getrgb("rgb(0,255,0)") + assert (0, 0, 255) == ImageColor.getrgb("rgb(0,0,255)") - # case insensitivity - self.assertEqual(ImageColor.getrgb("#DEF"), ImageColor.getrgb("#def")) - self.assertEqual(ImageColor.getrgb("#CDEF"), ImageColor.getrgb("#cdef")) - self.assertEqual(ImageColor.getrgb("#DEFDEF"), ImageColor.getrgb("#defdef")) - self.assertEqual(ImageColor.getrgb("#CDEFCDEF"), ImageColor.getrgb("#cdefcdef")) + # percents + assert (255, 0, 0) == ImageColor.getrgb("rgb(100%,0%,0%)") + assert (0, 255, 0) == ImageColor.getrgb("rgb(0%,100%,0%)") + assert (0, 0, 255) == ImageColor.getrgb("rgb(0%,0%,100%)") - # not a number - self.assertRaises(ValueError, ImageColor.getrgb, "#fo0") - self.assertRaises(ValueError, ImageColor.getrgb, "#fo00") - self.assertRaises(ValueError, ImageColor.getrgb, "#fo0000") - self.assertRaises(ValueError, ImageColor.getrgb, "#fo000000") + # rgba numbers + assert (255, 0, 0, 0) == ImageColor.getrgb("rgba(255,0,0,0)") + assert (0, 255, 0, 0) == ImageColor.getrgb("rgba(0,255,0,0)") + assert (0, 0, 255, 0) == ImageColor.getrgb("rgba(0,0,255,0)") + assert (0, 0, 0, 255) == ImageColor.getrgb("rgba(0,0,0,255)") - # wrong number of components - self.assertRaises(ValueError, ImageColor.getrgb, "#f0000") - self.assertRaises(ValueError, ImageColor.getrgb, "#f000000") - self.assertRaises(ValueError, ImageColor.getrgb, "#f00000000") - self.assertRaises(ValueError, ImageColor.getrgb, "#f000000000") - self.assertRaises(ValueError, ImageColor.getrgb, "#f00000 ") + assert (255, 0, 0) == ImageColor.getrgb("hsl(0,100%,50%)") + assert (255, 0, 0) == ImageColor.getrgb("hsl(360,100%,50%)") + assert (0, 255, 255) == ImageColor.getrgb("hsl(180,100%,50%)") - def test_colormap(self): - self.assertEqual((0, 0, 0), ImageColor.getrgb("black")) - self.assertEqual((255, 255, 255), ImageColor.getrgb("white")) - self.assertEqual((255, 255, 255), ImageColor.getrgb("WHITE")) + assert (255, 0, 0) == ImageColor.getrgb("hsv(0,100%,100%)") + assert (255, 0, 0) == ImageColor.getrgb("hsv(360,100%,100%)") + assert (0, 255, 255) == ImageColor.getrgb("hsv(180,100%,100%)") - self.assertRaises(ValueError, ImageColor.getrgb, "black ") + # alternate format + assert ImageColor.getrgb("hsb(0,100%,50%)") == ImageColor.getrgb("hsv(0,100%,50%)") - def test_functions(self): - # rgb numbers - self.assertEqual((255, 0, 0), ImageColor.getrgb("rgb(255,0,0)")) - self.assertEqual((0, 255, 0), ImageColor.getrgb("rgb(0,255,0)")) - self.assertEqual((0, 0, 255), ImageColor.getrgb("rgb(0,0,255)")) + # floats + assert (254, 3, 3) == ImageColor.getrgb("hsl(0.1,99.2%,50.3%)") + assert (255, 0, 0) == ImageColor.getrgb("hsl(360.,100.0%,50%)") - # percents - self.assertEqual((255, 0, 0), ImageColor.getrgb("rgb(100%,0%,0%)")) - self.assertEqual((0, 255, 0), ImageColor.getrgb("rgb(0%,100%,0%)")) - self.assertEqual((0, 0, 255), ImageColor.getrgb("rgb(0%,0%,100%)")) + assert (253, 2, 2) == ImageColor.getrgb("hsv(0.1,99.2%,99.3%)") + assert (255, 0, 0) == ImageColor.getrgb("hsv(360.,100.0%,100%)") - # rgba numbers - self.assertEqual((255, 0, 0, 0), ImageColor.getrgb("rgba(255,0,0,0)")) - self.assertEqual((0, 255, 0, 0), ImageColor.getrgb("rgba(0,255,0,0)")) - self.assertEqual((0, 0, 255, 0), ImageColor.getrgb("rgba(0,0,255,0)")) - self.assertEqual((0, 0, 0, 255), ImageColor.getrgb("rgba(0,0,0,255)")) + # case insensitivity + assert ImageColor.getrgb("RGB(255,0,0)") == ImageColor.getrgb("rgb(255,0,0)") + assert ImageColor.getrgb("RGB(100%,0%,0%)") == ImageColor.getrgb("rgb(100%,0%,0%)") + assert ImageColor.getrgb("RGBA(255,0,0,0)") == ImageColor.getrgb("rgba(255,0,0,0)") + assert ImageColor.getrgb("HSL(0,100%,50%)") == ImageColor.getrgb("hsl(0,100%,50%)") + assert ImageColor.getrgb("HSV(0,100%,50%)") == ImageColor.getrgb("hsv(0,100%,50%)") + assert ImageColor.getrgb("HSB(0,100%,50%)") == ImageColor.getrgb("hsb(0,100%,50%)") - self.assertEqual((255, 0, 0), ImageColor.getrgb("hsl(0,100%,50%)")) - self.assertEqual((255, 0, 0), ImageColor.getrgb("hsl(360,100%,50%)")) - self.assertEqual((0, 255, 255), ImageColor.getrgb("hsl(180,100%,50%)")) + # space agnosticism + assert (255, 0, 0) == ImageColor.getrgb("rgb( 255 , 0 , 0 )") + assert (255, 0, 0) == ImageColor.getrgb("rgb( 100% , 0% , 0% )") + assert (255, 0, 0, 0) == ImageColor.getrgb("rgba( 255 , 0 , 0 , 0 )") + assert (255, 0, 0) == ImageColor.getrgb("hsl( 0 , 100% , 50% )") + assert (255, 0, 0) == ImageColor.getrgb("hsv( 0 , 100% , 100% )") - self.assertEqual((255, 0, 0), ImageColor.getrgb("hsv(0,100%,100%)")) - self.assertEqual((255, 0, 0), ImageColor.getrgb("hsv(360,100%,100%)")) - self.assertEqual((0, 255, 255), ImageColor.getrgb("hsv(180,100%,100%)")) + # wrong number of components + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(255,0)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(255,0,0,0)") - # alternate format - self.assertEqual( - ImageColor.getrgb("hsb(0,100%,50%)"), ImageColor.getrgb("hsv(0,100%,50%)") - ) + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(100%,0%)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(100%,0%,0)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(100%,0%,0 %)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(100%,0%,0%,0%)") - # floats - self.assertEqual((254, 3, 3), ImageColor.getrgb("hsl(0.1,99.2%,50.3%)")) - self.assertEqual((255, 0, 0), ImageColor.getrgb("hsl(360.,100.0%,50%)")) + with pytest.raises(ValueError): + ImageColor.getrgb("rgba(255,0,0)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgba(255,0,0,0,0)") - self.assertEqual((253, 2, 2), ImageColor.getrgb("hsv(0.1,99.2%,99.3%)")) - self.assertEqual((255, 0, 0), ImageColor.getrgb("hsv(360.,100.0%,100%)")) + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0,100%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0,100%,0%,0%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0%,100%,50%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0,100,50%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0,100%,50)") - # case insensitivity - self.assertEqual( - ImageColor.getrgb("RGB(255,0,0)"), ImageColor.getrgb("rgb(255,0,0)") - ) - self.assertEqual( - ImageColor.getrgb("RGB(100%,0%,0%)"), ImageColor.getrgb("rgb(100%,0%,0%)") - ) - self.assertEqual( - ImageColor.getrgb("RGBA(255,0,0,0)"), ImageColor.getrgb("rgba(255,0,0,0)") - ) - self.assertEqual( - ImageColor.getrgb("HSL(0,100%,50%)"), ImageColor.getrgb("hsl(0,100%,50%)") - ) - self.assertEqual( - ImageColor.getrgb("HSV(0,100%,50%)"), ImageColor.getrgb("hsv(0,100%,50%)") - ) - self.assertEqual( - ImageColor.getrgb("HSB(0,100%,50%)"), ImageColor.getrgb("hsb(0,100%,50%)") - ) + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0,100%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0,100%,0%,0%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0%,100%,50%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0,100,50%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0,100%,50)") - # space agnosticism - self.assertEqual((255, 0, 0), ImageColor.getrgb("rgb( 255 , 0 , 0 )")) - self.assertEqual((255, 0, 0), ImageColor.getrgb("rgb( 100% , 0% , 0% )")) - self.assertEqual( - (255, 0, 0, 0), ImageColor.getrgb("rgba( 255 , 0 , 0 , 0 )") - ) - self.assertEqual((255, 0, 0), ImageColor.getrgb("hsl( 0 , 100% , 50% )")) - self.assertEqual((255, 0, 0), ImageColor.getrgb("hsv( 0 , 100% , 100% )")) - # wrong number of components - self.assertRaises(ValueError, ImageColor.getrgb, "rgb(255,0)") - self.assertRaises(ValueError, ImageColor.getrgb, "rgb(255,0,0,0)") +# look for rounding errors (based on code by Tim Hatch) +def test_rounding_errors(): + for color in ImageColor.colormap: + expected = Image.new("RGB", (1, 1), color).convert("L").getpixel((0, 0)) + actual = ImageColor.getcolor(color, "L") + assert expected == actual - self.assertRaises(ValueError, ImageColor.getrgb, "rgb(100%,0%)") - self.assertRaises(ValueError, ImageColor.getrgb, "rgb(100%,0%,0)") - self.assertRaises(ValueError, ImageColor.getrgb, "rgb(100%,0%,0 %)") - self.assertRaises(ValueError, ImageColor.getrgb, "rgb(100%,0%,0%,0%)") + assert (0, 255, 115) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGB") + Image.new("RGB", (1, 1), "white") - self.assertRaises(ValueError, ImageColor.getrgb, "rgba(255,0,0)") - self.assertRaises(ValueError, ImageColor.getrgb, "rgba(255,0,0,0,0)") + assert (0, 0, 0, 255) == ImageColor.getcolor("black", "RGBA") + assert (255, 255, 255, 255) == ImageColor.getcolor("white", "RGBA") + assert (0, 255, 115, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGBA") + Image.new("RGBA", (1, 1), "white") - self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0,100%)") - self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0,100%,0%,0%)") - self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0%,100%,50%)") - self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0,100,50%)") - self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0,100%,50)") + assert 0 == ImageColor.getcolor("black", "L") + assert 255 == ImageColor.getcolor("white", "L") + assert 163 == ImageColor.getcolor("rgba(0, 255, 115, 33)", "L") + Image.new("L", (1, 1), "white") - self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0,100%)") - self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0,100%,0%,0%)") - self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0%,100%,50%)") - self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0,100,50%)") - self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0,100%,50)") + assert 0 == ImageColor.getcolor("black", "1") + assert 255 == ImageColor.getcolor("white", "1") + # The following test is wrong, but is current behavior + # The correct result should be 255 due to the mode 1 + assert 163 == ImageColor.getcolor("rgba(0, 255, 115, 33)", "1") + # Correct behavior + # assert + # 255, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) + Image.new("1", (1, 1), "white") - # look for rounding errors (based on code by Tim Hatch) - def test_rounding_errors(self): - - for color in ImageColor.colormap: - expected = Image.new("RGB", (1, 1), color).convert("L").getpixel((0, 0)) - actual = ImageColor.getcolor(color, "L") - self.assertEqual(expected, actual) - - self.assertEqual( - (0, 255, 115), ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGB") - ) - Image.new("RGB", (1, 1), "white") - - self.assertEqual((0, 0, 0, 255), ImageColor.getcolor("black", "RGBA")) - self.assertEqual((255, 255, 255, 255), ImageColor.getcolor("white", "RGBA")) - self.assertEqual( - (0, 255, 115, 33), ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGBA") - ) - Image.new("RGBA", (1, 1), "white") - - self.assertEqual(0, ImageColor.getcolor("black", "L")) - self.assertEqual(255, ImageColor.getcolor("white", "L")) - self.assertEqual(162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "L")) - Image.new("L", (1, 1), "white") - - self.assertEqual(0, ImageColor.getcolor("black", "1")) - self.assertEqual(255, ImageColor.getcolor("white", "1")) - # The following test is wrong, but is current behavior - # The correct result should be 255 due to the mode 1 - self.assertEqual(162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) - # Correct behavior - # self.assertEqual( - # 255, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) - Image.new("1", (1, 1), "white") - - self.assertEqual((0, 255), ImageColor.getcolor("black", "LA")) - self.assertEqual((255, 255), ImageColor.getcolor("white", "LA")) - self.assertEqual((162, 33), ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA")) - Image.new("LA", (1, 1), "white") + assert (0, 255) == ImageColor.getcolor("black", "LA") + assert (255, 255) == ImageColor.getcolor("white", "LA") + assert (163, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA") + Image.new("LA", (1, 1), "white") diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 50a774df2..a87c1f2be 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,9 +1,15 @@ import os.path -import unittest -from PIL import Image, ImageColor, ImageDraw, ImageFont, features +import pytest -from .helper import PillowTestCase, hopper +from PIL import Image, ImageColor, ImageDraw, ImageFont + +from .helper import ( + assert_image_equal, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) BLACK = (0, 0, 0) WHITE = (255, 255, 255) @@ -30,747 +36,942 @@ POINTS2 = [10, 10, 20, 40, 30, 30] KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] -HAS_FREETYPE = features.check("freetype2") + +def test_sanity(): + im = hopper("RGB").copy() + + draw = ImageDraw.ImageDraw(im) + draw = ImageDraw.Draw(im) + + draw.ellipse(list(range(4))) + draw.line(list(range(10))) + draw.polygon(list(range(100))) + draw.rectangle(list(range(4))) -class TestImageDraw(PillowTestCase): - def test_sanity(self): - im = hopper("RGB").copy() +def test_valueerror(): + with Image.open("Tests/images/chi.gif") as im: - draw = ImageDraw.ImageDraw(im) draw = ImageDraw.Draw(im) + draw.line((0, 0), fill=(0, 0, 0)) - draw.ellipse(list(range(4))) - draw.line(list(range(10))) - draw.polygon(list(range(100))) - draw.rectangle(list(range(4))) - def test_valueerror(self): - with Image.open("Tests/images/chi.gif") as im: +def test_mode_mismatch(): + im = hopper("RGB").copy() - draw = ImageDraw.Draw(im) - draw.line((0, 0), fill=(0, 0, 0)) + with pytest.raises(ValueError): + ImageDraw.ImageDraw(im, mode="L") - def test_mode_mismatch(self): - im = hopper("RGB").copy() - self.assertRaises(ValueError, ImageDraw.ImageDraw, im, mode="L") +def helper_arc(bbox, start, end): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - def helper_arc(self, bbox, start, end): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) + # Act + draw.arc(bbox, start, end) - # Act - draw.arc(bbox, start, end) + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) - # Assert - self.assert_image_similar(im, Image.open("Tests/images/imagedraw_arc.png"), 1) - def test_arc1(self): - self.helper_arc(BBOX1, 0, 180) - self.helper_arc(BBOX1, 0.5, 180.4) +def test_arc1(): + helper_arc(BBOX1, 0, 180) + helper_arc(BBOX1, 0.5, 180.4) - def test_arc2(self): - self.helper_arc(BBOX2, 0, 180) - self.helper_arc(BBOX2, 0.5, 180.4) - def test_arc_end_le_start(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - start = 270.5 - end = 0 +def test_arc2(): + helper_arc(BBOX2, 0, 180) + helper_arc(BBOX2, 0.5, 180.4) - # Act - draw.arc(BBOX1, start=start, end=end) - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_arc_end_le_start.png") - ) +def test_arc_end_le_start(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + start = 270.5 + end = 0 - def test_arc_no_loops(self): - # No need to go in loops - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - start = 5 - end = 370 + # Act + draw.arc(BBOX1, start=start, end=end) - # Act - draw.arc(BBOX1, start=start, end=end) + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_arc_end_le_start.png")) - # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_arc_no_loops.png"), 1 - ) - def test_arc_width(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_arc_width.png" +def test_arc_no_loops(): + # No need to go in loops + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + start = 5 + end = 370 - # Act - draw.arc(BBOX1, 10, 260, width=5) + # Act + draw.arc(BBOX1, start=start, end=end) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1) - def test_arc_width_pieslice_large(self): - # Tests an arc with a large enough width that it is a pieslice - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_arc_width_pieslice.png" - # Act - draw.arc(BBOX1, 10, 260, fill="yellow", width=100) +def test_arc_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + # Act + draw.arc(BBOX1, 10, 260, width=5) - def test_arc_width_fill(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_arc_width_fill.png" + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1) - # Act - draw.arc(BBOX1, 10, 260, fill="yellow", width=5) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) +def test_arc_width_pieslice_large(): + # Tests an arc with a large enough width that it is a pieslice + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - def test_arc_width_non_whole_angle(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png" + # Act + draw.arc(BBOX1, 10, 260, fill="yellow", width=100) - # Act - draw.arc(BBOX1, 10, 259.5, width=5) + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - def test_bitmap(self): - # Arrange - small = Image.open("Tests/images/pil123rgba.png").resize((50, 50)) - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) +def test_arc_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.arc(BBOX1, 10, 260, fill="yellow", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1) + + +def test_arc_width_non_whole_angle(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png" + + # Act + draw.arc(BBOX1, 10, 259.5, width=5) + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +def test_arc_high(): + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.arc([10, 10, 89, 189], 20, 330, width=20, fill="white") + draw.arc([110, 10, 189, 189], 20, 150, width=20, fill="white") + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_arc_high.png")) + + +def test_bitmap(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + with Image.open("Tests/images/pil123rgba.png") as small: + small = small.resize((50, 50), Image.NEAREST) # Act draw.bitmap((10, 10), small) - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_bitmap.png")) + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_bitmap.png")) - def helper_chord(self, mode, bbox, start, end): - # Arrange - im = Image.new(mode, (W, H)) + +def helper_chord(mode, bbox, start, end): + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_chord_{mode}.png" + + # Act + draw.chord(bbox, start, end, fill="red", outline="yellow") + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +def test_chord1(): + for mode in ["RGB", "L"]: + helper_chord(mode, BBOX1, 0, 180) + + +def test_chord2(): + for mode in ["RGB", "L"]: + helper_chord(mode, BBOX2, 0, 180) + + +def test_chord_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.chord(BBOX1, 10, 260, outline="yellow", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1) + + +def test_chord_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1) + + +def test_chord_zero_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=0) + + # Assert + with Image.open("Tests/images/imagedraw_chord_zero_width.png") as expected: + assert_image_equal(im, expected) + + +def test_chord_too_fat(): + # Arrange + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + + # Act + draw.chord([-150, -150, 99, 99], 15, 60, width=10, fill="white", outline="red") + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_chord_too_fat.png")) + + +def helper_ellipse(mode, bbox): + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_ellipse_{mode}.png" + + # Act + draw.ellipse(bbox, fill="green", outline="blue") + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +def test_ellipse1(): + for mode in ["RGB", "L"]: + helper_ellipse(mode, BBOX1) + + +def test_ellipse2(): + for mode in ["RGB", "L"]: + helper_ellipse(mode, BBOX2) + + +def test_ellipse_translucent(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.ellipse(BBOX1, fill=(0, 255, 0, 127)) + + # Assert + expected = "Tests/images/imagedraw_ellipse_translucent.png" + assert_image_similar_tofile(im, expected, 1) + + +def test_ellipse_edge(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.ellipse(((0, 0), (W - 1, H - 1)), fill="white") + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) + + +def test_ellipse_symmetric(): + for width, bbox in ( + (100, (24, 24, 75, 75)), + (101, (25, 25, 75, 75)), + ): + im = Image.new("RGB", (width, 100)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_chord_{}.png".format(mode) - - # Act - draw.chord(bbox, start, end, fill="red", outline="yellow") - - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - - def test_chord1(self): - for mode in ["RGB", "L"]: - self.helper_chord(mode, BBOX1, 0, 180) - self.helper_chord(mode, BBOX1, 0.5, 180.4) - - def test_chord2(self): - for mode in ["RGB", "L"]: - self.helper_chord(mode, BBOX2, 0, 180) - self.helper_chord(mode, BBOX2, 0.5, 180.4) - - def test_chord_width(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_chord_width.png" - - # Act - draw.chord(BBOX1, 10, 260, outline="yellow", width=5) - - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - - def test_chord_width_fill(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_chord_width_fill.png" - - # Act - draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5) - - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - - def helper_ellipse(self, mode, bbox): - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_ellipse_{}.png".format(mode) - - # Act draw.ellipse(bbox, fill="green", outline="blue") + assert_image_equal(im, im.transpose(Image.FLIP_LEFT_RIGHT)) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - def test_ellipse1(self): - for mode in ["RGB", "L"]: - self.helper_ellipse(mode, BBOX1) +def test_ellipse_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - def test_ellipse2(self): - for mode in ["RGB", "L"]: - self.helper_ellipse(mode, BBOX2) + # Act + draw.ellipse(BBOX1, outline="blue", width=5) - def test_ellipse_edge(self): + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) + + +def test_ellipse_width_large(): + # Arrange + im = Image.new("RGB", (500, 500)) + draw = ImageDraw.Draw(im) + + # Act + draw.ellipse((25, 25, 475, 475), outline="blue", width=75) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_large.png", 1) + + +def test_ellipse_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.ellipse(BBOX1, fill="green", outline="blue", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1) + + +def test_ellipse_zero_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.ellipse(BBOX1, fill="green", outline="blue", width=0) + + # Assert + with Image.open("Tests/images/imagedraw_ellipse_zero_width.png") as expected: + assert_image_equal(im, expected) + + +def ellipse_various_sizes_helper(filled): + ellipse_sizes = range(32) + image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1 + im = Image.new("RGB", (image_size, image_size)) + draw = ImageDraw.Draw(im) + + x = 1 + for w in ellipse_sizes: + y = 1 + for h in ellipse_sizes: + border = [x, y, x + w - 1, y + h - 1] + if filled: + draw.ellipse(border, fill="white") + else: + draw.ellipse(border, outline="white") + y += h + 1 + x += w + 1 + + return im + + +def test_ellipse_various_sizes(): + im = ellipse_various_sizes_helper(False) + + with Image.open("Tests/images/imagedraw_ellipse_various_sizes.png") as expected: + assert_image_equal(im, expected) + + +def test_ellipse_various_sizes_filled(): + im = ellipse_various_sizes_helper(True) + + with Image.open( + "Tests/images/imagedraw_ellipse_various_sizes_filled.png" + ) as expected: + assert_image_equal(im, expected) + + +def helper_line(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.line(points, fill="yellow", width=2) + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_line.png")) + + +def test_line1(): + helper_line(POINTS1) + + +def test_line2(): + helper_line(POINTS2) + + +def test_shape1(): + # Arrange + im = Image.new("RGB", (100, 100), "white") + draw = ImageDraw.Draw(im) + x0, y0 = 5, 5 + x1, y1 = 5, 50 + x2, y2 = 95, 50 + x3, y3 = 95, 5 + + # Act + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + draw.shape(s, fill=1) + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_shape1.png")) + + +def test_shape2(): + # Arrange + im = Image.new("RGB", (100, 100), "white") + draw = ImageDraw.Draw(im) + x0, y0 = 95, 95 + x1, y1 = 95, 50 + x2, y2 = 5, 50 + x3, y3 = 5, 95 + + # Act + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + draw.shape(s, outline="blue") + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_shape2.png")) + + +def helper_pieslice(bbox, start, end): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice(bbox, start, end, fill="white", outline="blue") + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) + + +def test_pieslice1(): + helper_pieslice(BBOX1, -92, 46) + helper_pieslice(BBOX1, -92.2, 46.2) + + +def test_pieslice2(): + helper_pieslice(BBOX2, -92, 46) + helper_pieslice(BBOX2, -92.2, 46.2) + + +def test_pieslice_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice(BBOX1, 10, 260, outline="blue", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1) + + +def test_pieslice_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_pieslice_width_fill.png" + + # Act + draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5) + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +def test_pieslice_zero_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=0) + + # Assert + with Image.open("Tests/images/imagedraw_pieslice_zero_width.png") as expected: + assert_image_equal(im, expected) + + +def test_pieslice_wide(): + # Arrange + im = Image.new("RGB", (200, 100)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice([0, 0, 199, 99], 190, 170, width=10, fill="white", outline="red") + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_pieslice_wide.png")) + + +def helper_point(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.point(points, fill="yellow") + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_point.png")) + + +def test_point1(): + helper_point(POINTS1) + + +def test_point2(): + helper_point(POINTS2) + + +def helper_polygon(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.polygon(points, fill="red", outline="blue") + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_polygon.png")) + + +def test_polygon1(): + helper_polygon(POINTS1) + + +def test_polygon2(): + helper_polygon(POINTS2) + + +def test_polygon_kite(): + # Test drawing lines of different gradients (dx>dy, dy>dx) and + # vertical (dx==0) and horizontal (dy==0) lines + for mode in ["RGB", "L"]: # Arrange - im = Image.new("RGB", (W, H)) + im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" # Act - draw.ellipse(((0, 0), (W - 1, H)), fill="white") + draw.polygon(KITE_POINTS, fill="blue", outline="yellow") # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1 - ) + assert_image_equal(im, Image.open(expected)) - def test_ellipse_symmetric(self): - for bbox in [(25, 25, 76, 76), (25, 25, 75, 75)]: - im = Image.new("RGB", (101, 101)) - draw = ImageDraw.Draw(im) - draw.ellipse(bbox, fill="green", outline="blue") - self.assert_image_equal(im, im.transpose(Image.FLIP_LEFT_RIGHT)) - def test_ellipse_width(self): +def test_polygon_1px_high(): + # Test drawing a 1px high polygon + # Arrange + im = Image.new("RGB", (3, 3)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_polygon_1px_high.png" + + # Act + draw.polygon([(0, 1), (0, 1), (2, 1), (2, 1)], "#f00") + + # Assert + assert_image_equal(im, Image.open(expected)) + + +def helper_rectangle(bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(bbox, fill="black", outline="green") + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_rectangle.png")) + + +def test_rectangle1(): + helper_rectangle(BBOX1) + + +def test_rectangle2(): + helper_rectangle(BBOX2) + + +def test_big_rectangle(): + # Test drawing a rectangle bigger than the image + # Arrange + im = Image.new("RGB", (W, H)) + bbox = [(-1, -1), (W + 1, H + 1)] + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(bbox, fill="orange") + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1) + + +def test_rectangle_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_rectangle_width.png" + + # Act + draw.rectangle(BBOX1, outline="green", width=5) + + # Assert + assert_image_equal(im, Image.open(expected)) + + +def test_rectangle_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_rectangle_width_fill.png" + + # Act + draw.rectangle(BBOX1, fill="blue", outline="green", width=5) + + # Assert + assert_image_equal(im, Image.open(expected)) + + +def test_rectangle_zero_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(BBOX1, fill="blue", outline="green", width=0) + + # Assert + with Image.open("Tests/images/imagedraw_rectangle_zero_width.png") as expected: + assert_image_equal(im, expected) + + +def test_rectangle_I16(): + # Arrange + im = Image.new("I;16", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(BBOX1, fill="black", outline="green") + + # Assert + assert_image_equal( + im.convert("I"), Image.open("Tests/images/imagedraw_rectangle_I.png") + ) + + +def test_floodfill(): + red = ImageColor.getrgb("red") + + for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_ellipse_width.png" - - # Act - draw.ellipse(BBOX1, outline="blue", width=5) - - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - - def test_ellipse_width_large(self): - # Arrange - im = Image.new("RGB", (500, 500)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_ellipse_width_large.png" - - # Act - draw.ellipse((25, 25, 475, 475), outline="blue", width=75) - - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - - def test_ellipse_width_fill(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_ellipse_width_fill.png" - - # Act - draw.ellipse(BBOX1, fill="green", outline="blue", width=5) - - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - - def helper_line(self, points): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.line(points, fill="yellow", width=2) - - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_line.png")) - - def test_line1(self): - self.helper_line(POINTS1) - - def test_line2(self): - self.helper_line(POINTS2) - - def test_shape1(self): - # Arrange - im = Image.new("RGB", (100, 100), "white") - draw = ImageDraw.Draw(im) - x0, y0 = 5, 5 - x1, y1 = 5, 50 - x2, y2 = 95, 50 - x3, y3 = 95, 5 - - # Act - s = ImageDraw.Outline() - s.move(x0, y0) - s.curve(x1, y1, x2, y2, x3, y3) - s.line(x0, y0) - - draw.shape(s, fill=1) - - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_shape1.png")) - - def test_shape2(self): - # Arrange - im = Image.new("RGB", (100, 100), "white") - draw = ImageDraw.Draw(im) - x0, y0 = 95, 95 - x1, y1 = 95, 50 - x2, y2 = 5, 50 - x3, y3 = 5, 95 - - # Act - s = ImageDraw.Outline() - s.move(x0, y0) - s.curve(x1, y1, x2, y2, x3, y3) - s.line(x0, y0) - - draw.shape(s, outline="blue") - - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_shape2.png")) - - def helper_pieslice(self, bbox, start, end): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.pieslice(bbox, start, end, fill="white", outline="blue") - - # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_pieslice.png"), 1 - ) - - def test_pieslice1(self): - self.helper_pieslice(BBOX1, -90, 45) - self.helper_pieslice(BBOX1, -90.5, 45.4) - - def test_pieslice2(self): - self.helper_pieslice(BBOX2, -90, 45) - self.helper_pieslice(BBOX2, -90.5, 45.4) - - def test_pieslice_width(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_pieslice_width.png" - - # Act - draw.pieslice(BBOX1, 10, 260, outline="blue", width=5) - - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - - def test_pieslice_width_fill(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_pieslice_width_fill.png" - - # Act - draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5) - - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - - def helper_point(self, points): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.point(points, fill="yellow") - - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_point.png")) - - def test_point1(self): - self.helper_point(POINTS1) - - def test_point2(self): - self.helper_point(POINTS2) - - def helper_polygon(self, points): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.polygon(points, fill="red", outline="blue") - - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_polygon.png")) - - def test_polygon1(self): - self.helper_polygon(POINTS1) - - def test_polygon2(self): - self.helper_polygon(POINTS2) - - def test_polygon_kite(self): - # Test drawing lines of different gradients (dx>dy, dy>dx) and - # vertical (dx==0) and horizontal (dy==0) lines - for mode in ["RGB", "L"]: - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_polygon_kite_{}.png".format(mode) - - # Act - draw.polygon(KITE_POINTS, fill="blue", outline="yellow") - - # Assert - self.assert_image_equal(im, Image.open(expected)) - - def helper_rectangle(self, bbox): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.rectangle(bbox, fill="black", outline="green") - - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_rectangle.png")) - - def test_rectangle1(self): - self.helper_rectangle(BBOX1) - - def test_rectangle2(self): - self.helper_rectangle(BBOX2) - - def test_big_rectangle(self): - # Test drawing a rectangle bigger than the image - # Arrange - im = Image.new("RGB", (W, H)) - bbox = [(-1, -1), (W + 1, H + 1)] - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_big_rectangle.png" - - # Act - draw.rectangle(bbox, fill="orange") - - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - - def test_rectangle_width(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_rectangle_width.png" - - # Act - draw.rectangle(BBOX1, outline="green", width=5) - - # Assert - self.assert_image_equal(im, Image.open(expected)) - - def test_rectangle_width_fill(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_rectangle_width_fill.png" - - # Act - draw.rectangle(BBOX1, fill="blue", outline="green", width=5) - - # Assert - self.assert_image_equal(im, Image.open(expected)) - - def test_rectangle_I16(self): - # Arrange - im = Image.new("I;16", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.rectangle(BBOX1, fill="black", outline="green") - - # Assert - self.assert_image_equal( - im.convert("I"), Image.open("Tests/images/imagedraw_rectangle_I.png") - ) - - def test_floodfill(self): - red = ImageColor.getrgb("red") - - for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") - centre_point = (int(W / 2), int(H / 2)) - - # Act - ImageDraw.floodfill(im, centre_point, value) - - # Assert - expected = "Tests/images/imagedraw_floodfill_" + mode + ".png" - im_floodfill = Image.open(expected) - self.assert_image_equal(im, im_floodfill) - - # Test that using the same colour does not change the image - ImageDraw.floodfill(im, centre_point, red) - self.assert_image_equal(im, im_floodfill) - - # Test that filling outside the image does not change the image - ImageDraw.floodfill(im, (W, H), red) - self.assert_image_equal(im, im_floodfill) - - # Test filling at the edge of an image - im = Image.new("RGB", (1, 1)) - ImageDraw.floodfill(im, (0, 0), red) - self.assert_image_equal(im, Image.new("RGB", (1, 1), red)) - - def test_floodfill_border(self): - # floodfill() is experimental - - # Arrange - im = Image.new("RGB", (W, H)) + im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) draw.rectangle(BBOX2, outline="yellow", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act - ImageDraw.floodfill( - im, - centre_point, - ImageColor.getrgb("red"), - border=ImageColor.getrgb("black"), - ) + ImageDraw.floodfill(im, centre_point, value) # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_floodfill2.png")) + expected = "Tests/images/imagedraw_floodfill_" + mode + ".png" + with Image.open(expected) as im_floodfill: + assert_image_equal(im, im_floodfill) - def test_floodfill_thresh(self): - # floodfill() is experimental + # Test that using the same colour does not change the image + ImageDraw.floodfill(im, centre_point, red) + assert_image_equal(im, im_floodfill) - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="darkgreen", fill="green") - centre_point = (int(W / 2), int(H / 2)) + # Test that filling outside the image does not change the image + ImageDraw.floodfill(im, (W, H), red) + assert_image_equal(im, im_floodfill) - # Act - ImageDraw.floodfill(im, centre_point, ImageColor.getrgb("red"), thresh=30) + # Test filling at the edge of an image + im = Image.new("RGB", (1, 1)) + ImageDraw.floodfill(im, (0, 0), red) + assert_image_equal(im, Image.new("RGB", (1, 1), red)) - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_floodfill2.png")) - def test_floodfill_not_negative(self): - # floodfill() is experimental - # Test that floodfill does not extend into negative coordinates +def test_floodfill_border(): + # floodfill() is experimental - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - draw.line((W / 2, 0, W / 2, H / 2), fill="green") - draw.line((0, H / 2, W / 2, H / 2), fill="green") + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.rectangle(BBOX2, outline="yellow", fill="green") + centre_point = (int(W / 2), int(H / 2)) - # Act - ImageDraw.floodfill(im, (int(W / 4), int(H / 4)), ImageColor.getrgb("red")) + # Act + ImageDraw.floodfill( + im, + centre_point, + ImageColor.getrgb("red"), + border=ImageColor.getrgb("black"), + ) - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_floodfill_not_negative.png") - ) + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_floodfill2.png")) - def create_base_image_draw( - self, size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY - ): - img = Image.new(mode, size, background1) - for x in range(0, size[0]): - for y in range(0, size[1]): - if (x + y) % 2 == 0: - img.putpixel((x, y), background2) - return img, ImageDraw.Draw(img) - def test_square(self): - expected = Image.open(os.path.join(IMAGES_PATH, "square.png")) +def test_floodfill_thresh(): + # floodfill() is experimental + + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.rectangle(BBOX2, outline="darkgreen", fill="green") + centre_point = (int(W / 2), int(H / 2)) + + # Act + ImageDraw.floodfill(im, centre_point, ImageColor.getrgb("red"), thresh=30) + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_floodfill2.png")) + + +def test_floodfill_not_negative(): + # floodfill() is experimental + # Test that floodfill does not extend into negative coordinates + + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.line((W / 2, 0, W / 2, H / 2), fill="green") + draw.line((0, H / 2, W / 2, H / 2), fill="green") + + # Act + ImageDraw.floodfill(im, (int(W / 4), int(H / 4)), ImageColor.getrgb("red")) + + # Assert + assert_image_equal( + im, Image.open("Tests/images/imagedraw_floodfill_not_negative.png") + ) + + +def create_base_image_draw( + size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY +): + img = Image.new(mode, size, background1) + for x in range(0, size[0]): + for y in range(0, size[1]): + if (x + y) % 2 == 0: + img.putpixel((x, y), background2) + return img, ImageDraw.Draw(img) + + +def test_square(): + with Image.open(os.path.join(IMAGES_PATH, "square.png")) as expected: expected.load() - img, draw = self.create_base_image_draw((10, 10)) + img, draw = create_base_image_draw((10, 10)) draw.polygon([(2, 2), (2, 7), (7, 7), (7, 2)], BLACK) - self.assert_image_equal(img, expected, "square as normal polygon failed") - img, draw = self.create_base_image_draw((10, 10)) + assert_image_equal(img, expected, "square as normal polygon failed") + img, draw = create_base_image_draw((10, 10)) draw.polygon([(7, 7), (7, 2), (2, 2), (2, 7)], BLACK) - self.assert_image_equal(img, expected, "square as inverted polygon failed") - img, draw = self.create_base_image_draw((10, 10)) + assert_image_equal(img, expected, "square as inverted polygon failed") + img, draw = create_base_image_draw((10, 10)) draw.rectangle((2, 2, 7, 7), BLACK) - self.assert_image_equal(img, expected, "square as normal rectangle failed") - img, draw = self.create_base_image_draw((10, 10)) + assert_image_equal(img, expected, "square as normal rectangle failed") + img, draw = create_base_image_draw((10, 10)) draw.rectangle((7, 7, 2, 2), BLACK) - self.assert_image_equal(img, expected, "square as inverted rectangle failed") + assert_image_equal(img, expected, "square as inverted rectangle failed") - def test_triangle_right(self): - expected = Image.open(os.path.join(IMAGES_PATH, "triangle_right.png")) + +def test_triangle_right(): + with Image.open(os.path.join(IMAGES_PATH, "triangle_right.png")) as expected: expected.load() - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK) - self.assert_image_equal(img, expected, "triangle right failed") + assert_image_equal(img, expected, "triangle right failed") - def test_line_horizontal(self): - expected = Image.open( - os.path.join(IMAGES_PATH, "line_horizontal_w2px_normal.png") - ) + +def test_line_horizontal(): + with Image.open( + os.path.join(IMAGES_PATH, "line_horizontal_w2px_normal.png") + ) as expected: expected.load() - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 5), BLACK, 2) - self.assert_image_equal( + assert_image_equal( img, expected, "line straight horizontal normal 2px wide failed" ) - expected = Image.open( - os.path.join(IMAGES_PATH, "line_horizontal_w2px_inverted.png") - ) + with Image.open( + os.path.join(IMAGES_PATH, "line_horizontal_w2px_inverted.png") + ) as expected: expected.load() - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.line((14, 5, 5, 5), BLACK, 2) - self.assert_image_equal( + assert_image_equal( img, expected, "line straight horizontal inverted 2px wide failed" ) - expected = Image.open(os.path.join(IMAGES_PATH, "line_horizontal_w3px.png")) + with Image.open(os.path.join(IMAGES_PATH, "line_horizontal_w3px.png")) as expected: expected.load() - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 5), BLACK, 3) - self.assert_image_equal( + assert_image_equal( img, expected, "line straight horizontal normal 3px wide failed" ) - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.line((14, 5, 5, 5), BLACK, 3) - self.assert_image_equal( + assert_image_equal( img, expected, "line straight horizontal inverted 3px wide failed" ) - expected = Image.open(os.path.join(IMAGES_PATH, "line_horizontal_w101px.png")) + with Image.open( + os.path.join(IMAGES_PATH, "line_horizontal_w101px.png") + ) as expected: expected.load() - img, draw = self.create_base_image_draw((200, 110)) + img, draw = create_base_image_draw((200, 110)) draw.line((5, 55, 195, 55), BLACK, 101) - self.assert_image_equal( - img, expected, "line straight horizontal 101px wide failed" - ) + assert_image_equal(img, expected, "line straight horizontal 101px wide failed") - def test_line_h_s1_w2(self): - self.skipTest("failing") - expected = Image.open( - os.path.join(IMAGES_PATH, "line_horizontal_slope1px_w2px.png") - ) + +def test_line_h_s1_w2(): + pytest.skip("failing") + with Image.open( + os.path.join(IMAGES_PATH, "line_horizontal_slope1px_w2px.png") + ) as expected: expected.load() - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 6), BLACK, 2) - self.assert_image_equal( - img, expected, "line horizontal 1px slope 2px wide failed" - ) + assert_image_equal(img, expected, "line horizontal 1px slope 2px wide failed") - def test_line_vertical(self): - expected = Image.open( - os.path.join(IMAGES_PATH, "line_vertical_w2px_normal.png") - ) + +def test_line_vertical(): + with Image.open( + os.path.join(IMAGES_PATH, "line_vertical_w2px_normal.png") + ) as expected: expected.load() - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 5, 14), BLACK, 2) - self.assert_image_equal( + assert_image_equal( img, expected, "line straight vertical normal 2px wide failed" ) - expected = Image.open( - os.path.join(IMAGES_PATH, "line_vertical_w2px_inverted.png") - ) + with Image.open( + os.path.join(IMAGES_PATH, "line_vertical_w2px_inverted.png") + ) as expected: expected.load() - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.line((5, 14, 5, 5), BLACK, 2) - self.assert_image_equal( + assert_image_equal( img, expected, "line straight vertical inverted 2px wide failed" ) - expected = Image.open(os.path.join(IMAGES_PATH, "line_vertical_w3px.png")) + with Image.open(os.path.join(IMAGES_PATH, "line_vertical_w3px.png")) as expected: expected.load() - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 5, 14), BLACK, 3) - self.assert_image_equal( + assert_image_equal( img, expected, "line straight vertical normal 3px wide failed" ) - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.line((5, 14, 5, 5), BLACK, 3) - self.assert_image_equal( + assert_image_equal( img, expected, "line straight vertical inverted 3px wide failed" ) - expected = Image.open(os.path.join(IMAGES_PATH, "line_vertical_w101px.png")) + with Image.open(os.path.join(IMAGES_PATH, "line_vertical_w101px.png")) as expected: expected.load() - img, draw = self.create_base_image_draw((110, 200)) + img, draw = create_base_image_draw((110, 200)) draw.line((55, 5, 55, 195), BLACK, 101) - self.assert_image_equal( - img, expected, "line straight vertical 101px wide failed" - ) - expected = Image.open( - os.path.join(IMAGES_PATH, "line_vertical_slope1px_w2px.png") - ) + assert_image_equal(img, expected, "line straight vertical 101px wide failed") + with Image.open( + os.path.join(IMAGES_PATH, "line_vertical_slope1px_w2px.png") + ) as expected: expected.load() - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 6, 14), BLACK, 2) - self.assert_image_equal( - img, expected, "line vertical 1px slope 2px wide failed" - ) + assert_image_equal(img, expected, "line vertical 1px slope 2px wide failed") - def test_line_oblique_45(self): - expected = Image.open(os.path.join(IMAGES_PATH, "line_oblique_45_w3px_a.png")) + +def test_line_oblique_45(): + with Image.open( + os.path.join(IMAGES_PATH, "line_oblique_45_w3px_a.png") + ) as expected: expected.load() - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 14), BLACK, 3) - self.assert_image_equal( - img, expected, "line oblique 45 normal 3px wide A failed" - ) - img, draw = self.create_base_image_draw((20, 20)) + assert_image_equal(img, expected, "line oblique 45 normal 3px wide A failed") + img, draw = create_base_image_draw((20, 20)) draw.line((14, 14, 5, 5), BLACK, 3) - self.assert_image_equal( - img, expected, "line oblique 45 inverted 3px wide A failed" - ) - expected = Image.open(os.path.join(IMAGES_PATH, "line_oblique_45_w3px_b.png")) + assert_image_equal(img, expected, "line oblique 45 inverted 3px wide A failed") + with Image.open( + os.path.join(IMAGES_PATH, "line_oblique_45_w3px_b.png") + ) as expected: expected.load() - img, draw = self.create_base_image_draw((20, 20)) + img, draw = create_base_image_draw((20, 20)) draw.line((14, 5, 5, 14), BLACK, 3) - self.assert_image_equal( - img, expected, "line oblique 45 normal 3px wide B failed" - ) - img, draw = self.create_base_image_draw((20, 20)) + assert_image_equal(img, expected, "line oblique 45 normal 3px wide B failed") + img, draw = create_base_image_draw((20, 20)) draw.line((5, 14, 14, 5), BLACK, 3) - self.assert_image_equal( - img, expected, "line oblique 45 inverted 3px wide B failed" - ) + assert_image_equal(img, expected, "line oblique 45 inverted 3px wide B failed") - def test_wide_line_dot(self): - # Test drawing a wide "line" from one point to another just draws - # a single point - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_wide_line_dot.png" - # Act - draw.line([(50, 50), (50, 50)], width=3) +def test_wide_line_dot(): + # Test drawing a wide "line" from one point to another just draws a single point + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + # Act + draw.line([(50, 50), (50, 50)], width=3) - def test_line_joint(self): - im = Image.new("RGB", (500, 325)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_line_joint_curve.png" + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_wide_line_dot.png", 1) - # Act - xy = [ + +def test_wide_line_larger_than_int(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_wide_line_larger_than_int.png" + + # Act + draw.line([(0, 0), (32768, 32768)], width=3) + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize( + "xy", + [ + [ (400, 280), (380, 280), (450, 280), @@ -785,107 +986,289 @@ class TestImageDraw(PillowTestCase): (50, 200), (150, 50), (250, 100), - ] - draw.line(xy, GRAY, 50, "curve") + ], + ( + 400, + 280, + 380, + 280, + 450, + 280, + 440, + 120, + 350, + 200, + 310, + 280, + 300, + 280, + 250, + 280, + 250, + 200, + 150, + 200, + 150, + 260, + 50, + 200, + 150, + 50, + 250, + 100, + ), + [ + 400, + 280, + 380, + 280, + 450, + 280, + 440, + 120, + 350, + 200, + 310, + 280, + 300, + 280, + 250, + 280, + 250, + 200, + 150, + 200, + 150, + 260, + 50, + 200, + 150, + 50, + 250, + 100, + ], + ], +) +def test_line_joint(xy): + im = Image.new("RGB", (500, 325)) + draw = ImageDraw.Draw(im) - # Assert - self.assert_image_similar(im, Image.open(expected), 3) + # Act + draw.line(xy, GRAY, 50, "curve") - def test_textsize_empty_string(self): - # https://github.com/python-pillow/Pillow/issues/2783 + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_line_joint_curve.png", 3) + + +def test_textsize_empty_string(): + # https://github.com/python-pillow/Pillow/issues/2783 + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + # Should not cause 'SystemError: returned NULL without setting an error' + draw.textsize("") + draw.textsize("\n") + draw.textsize("test\n") + + +@skip_unless_feature("freetype2") +def test_textsize_stroke(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) + + # Act / Assert + assert draw.textsize("A", font, stroke_width=2) == (16, 20) + assert draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) == (52, 44) + + +@skip_unless_feature("freetype2") +def test_stroke(): + for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - # Should not cause 'SystemError: returned NULL without setting an error' - draw.textsize("") - draw.textsize("\n") - draw.textsize("test\n") - - @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") - def test_textsize_stroke(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) - - # Act / Assert - self.assertEqual(draw.textsize("A", font, stroke_width=2), (16, 20)) - self.assertEqual( - draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2), (52, 44) - ) - - @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") - def test_stroke(self): - for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): - # Arrange - im = Image.new("RGB", (120, 130)) - draw = ImageDraw.Draw(im) - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) - - # Act - draw.text( - (10, 10), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill - ) - - # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_stroke_" + suffix + ".png"), 3.1 - ) - - @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") - def test_stroke_multiline(self): - # Arrange - im = Image.new("RGB", (100, 250)) + im = Image.new("RGB", (120, 130)) draw = ImageDraw.Draw(im) font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) # Act - draw.multiline_text( - (10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0" - ) + draw.text((12, 12), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill) # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_stroke_multiline.png"), 3.3 + assert_image_similar_tofile( + im, "Tests/images/imagedraw_stroke_" + suffix + ".png", 3.1 ) - def test_same_color_outline(self): - # Prepare shape - x0, y0 = 5, 5 - x1, y1 = 5, 50 - x2, y2 = 95, 50 - x3, y3 = 95, 5 - s = ImageDraw.Outline() - s.move(x0, y0) - s.curve(x1, y1, x2, y2, x3, y3) - s.line(x0, y0) +@skip_unless_feature("freetype2") +def test_stroke_descender(): + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) - # Begin - for mode in ["RGB", "L"]: - for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: - for operation, args in { - "chord": [BBOX1, 0, 180], - "ellipse": [BBOX1], - "shape": [s], - "pieslice": [BBOX1, -90, 45], - "polygon": [[(18, 30), (85, 30), (60, 72)]], - "rectangle": [BBOX1], - }.items(): - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) + # Act + draw.text((12, 2), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0") - # Act - draw_method = getattr(draw, operation) - args += [fill, outline] - draw_method(*args) + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) - # Assert - expected = "Tests/images/imagedraw_outline_{}_{}.png".format( - operation, mode - ) - self.assert_image_similar(im, Image.open(expected), 1) + +@skip_unless_feature("freetype2") +def test_stroke_multiline(): + # Arrange + im = Image.new("RGB", (100, 250)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.multiline_text( + (12, 12), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0" + ) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) + + +def test_same_color_outline(): + # Prepare shape + x0, y0 = 5, 5 + x1, y1 = 5, 50 + x2, y2 = 95, 50 + x3, y3 = 95, 5 + + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + # Begin + for mode in ["RGB", "L"]: + for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: + for operation, args in { + "chord": [BBOX1, 0, 180], + "ellipse": [BBOX1], + "shape": [s], + "pieslice": [BBOX1, -90, 45], + "polygon": [[(18, 30), (85, 30), (60, 72)]], + "rectangle": [BBOX1], + }.items(): + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw_method = getattr(draw, operation) + args += [fill, outline] + draw_method(*args) + + # Assert + expected = f"Tests/images/imagedraw_outline_{operation}_{mode}.png" + assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize( + "n_sides, rotation, polygon_name", + [(4, 0, "square"), (8, 0, "regular_octagon"), (4, 45, "square")], +) +def test_draw_regular_polygon(n_sides, rotation, polygon_name): + im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) + filename_base = f"Tests/images/imagedraw_{polygon_name}" + filename = ( + f"{filename_base}.png" + if rotation == 0 + else f"{filename_base}_rotate_{rotation}.png" + ) + draw = ImageDraw.Draw(im) + bounding_circle = ((W // 2, H // 2), 25) + draw.regular_polygon(bounding_circle, n_sides, rotation=rotation, fill="red") + assert_image_equal(im, Image.open(filename)) + + +@pytest.mark.parametrize( + "n_sides, expected_vertices", + [ + (3, [(28.35, 62.5), (71.65, 62.5), (50.0, 25.0)]), + (4, [(32.32, 67.68), (67.68, 67.68), (67.68, 32.32), (32.32, 32.32)]), + ( + 5, + [ + (35.31, 70.23), + (64.69, 70.23), + (73.78, 42.27), + (50.0, 25.0), + (26.22, 42.27), + ], + ), + ( + 6, + [ + (37.5, 71.65), + (62.5, 71.65), + (75.0, 50.0), + (62.5, 28.35), + (37.5, 28.35), + (25.0, 50.0), + ], + ), + ], +) +def test_compute_regular_polygon_vertices(n_sides, expected_vertices): + bounding_circle = (W // 2, H // 2, 25) + vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) + assert vertices == expected_vertices + + +@pytest.mark.parametrize( + "n_sides, bounding_circle, rotation, expected_error, error_message", + [ + (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, 50, 100, 100), + 0, + ValueError, + "bounding_circle should contain 2D coordinates " + "and a radius (e.g. (x, y, r) or ((x, y), r) )", + ), + ( + 3, + (50, 50, "25"), + 0, + ValueError, + "bounding_circle should only contain numeric data", + ), + ( + 3, + ((50, 50, 50), 25), + 0, + ValueError, + "bounding_circle centre should contain 2D coordinates (e.g. (x, y))", + ), + ( + 3, + (50, 50, 0), + 0, + ValueError, + "bounding_circle radius should be > 0", + ), + ( + 3, + (50, 50, 25), + "0", + ValueError, + "rotation should be an int or float", + ), + ], +) +def test_compute_regular_polygon_vertices_input_error_handling( + n_sides, bounding_circle, rotation, expected_error, error_message +): + with pytest.raises(expected_error) as e: + ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) + assert str(e.value) == error_message diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 577727664..b78dfe85b 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -1,9 +1,13 @@ import os.path -import unittest -from PIL import Image, ImageDraw2, features +from PIL import Image, ImageDraw, ImageDraw2 -from .helper import PillowTestCase, hopper +from .helper import ( + assert_image_equal, + assert_image_similar, + hopper, + skip_unless_feature, +) BLACK = (0, 0, 0) WHITE = (255, 255, 255) @@ -30,194 +34,207 @@ POINTS2 = [10, 10, 20, 40, 30, 30] KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] -HAS_FREETYPE = features.check("freetype2") FONT_PATH = "Tests/fonts/FreeMono.ttf" -class TestImageDraw(PillowTestCase): - def test_sanity(self): - im = hopper("RGB").copy() +def test_sanity(): + im = hopper("RGB").copy() - draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("blue", width=7) - draw.line(list(range(10)), pen) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=7) + draw.line(list(range(10)), pen) - from PIL import ImageDraw + draw, handler = ImageDraw.getdraw(im) + pen = ImageDraw2.Pen("blue", width=7) + draw.line(list(range(10)), pen) - draw, handler = ImageDraw.getdraw(im) - pen = ImageDraw2.Pen("blue", width=7) - draw.line(list(range(10)), pen) - def helper_ellipse(self, mode, bbox): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("blue", width=2) - brush = ImageDraw2.Brush("green") - expected = "Tests/images/imagedraw_ellipse_{}.png".format(mode) +def helper_ellipse(mode, bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=2) + brush = ImageDraw2.Brush("green") + expected = f"Tests/images/imagedraw_ellipse_{mode}.png" - # Act - draw.ellipse(bbox, pen, brush) + # Act + draw.ellipse(bbox, pen, brush) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + # Assert + assert_image_similar(im, Image.open(expected), 1) - def test_ellipse1(self): - self.helper_ellipse("RGB", BBOX1) - def test_ellipse2(self): - self.helper_ellipse("RGB", BBOX2) +def test_ellipse1(): + helper_ellipse("RGB", BBOX1) - def test_ellipse_edge(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - brush = ImageDraw2.Brush("white") - # Act - draw.ellipse(((0, 0), (W - 1, H)), brush) +def test_ellipse2(): + helper_ellipse("RGB", BBOX2) - # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1 - ) - def helper_line(self, points): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("yellow", width=2) +def test_ellipse_edge(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + brush = ImageDraw2.Brush("white") - # Act - draw.line(points, pen) + # Act + draw.ellipse(((0, 0), (W - 1, H - 1)), brush) - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_line.png")) + # Assert + assert_image_similar(im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) - def test_line1_pen(self): - self.helper_line(POINTS1) - def test_line2_pen(self): - self.helper_line(POINTS2) +def helper_line(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("yellow", width=2) - def test_line_pen_as_brush(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - pen = None - brush = ImageDraw2.Pen("yellow", width=2) + # Act + draw.line(points, pen) - # Act - # Pass in the pen as the brush parameter - draw.line(POINTS1, pen, brush) + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_line.png")) - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_line.png")) - def helper_polygon(self, points): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("blue", width=2) - brush = ImageDraw2.Brush("red") +def test_line1_pen(): + helper_line(POINTS1) - # Act - draw.polygon(points, pen, brush) - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_polygon.png")) +def test_line2_pen(): + helper_line(POINTS2) - def test_polygon1(self): - self.helper_polygon(POINTS1) - def test_polygon2(self): - self.helper_polygon(POINTS2) +def test_line_pen_as_brush(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = None + brush = ImageDraw2.Pen("yellow", width=2) - def helper_rectangle(self, bbox): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("green", width=2) - brush = ImageDraw2.Brush("black") + # Act + # Pass in the pen as the brush parameter + draw.line(POINTS1, pen, brush) - # Act - draw.rectangle(bbox, pen, brush) + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_line.png")) - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_rectangle.png")) - def test_rectangle1(self): - self.helper_rectangle(BBOX1) +def helper_polygon(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=2) + brush = ImageDraw2.Brush("red") - def test_rectangle2(self): - self.helper_rectangle(BBOX2) + # Act + draw.polygon(points, pen, brush) - def test_big_rectangle(self): - # Test drawing a rectangle bigger than the image - # Arrange - im = Image.new("RGB", (W, H)) - bbox = [(-1, -1), (W + 1, H + 1)] - brush = ImageDraw2.Brush("orange") - draw = ImageDraw2.Draw(im) - expected = "Tests/images/imagedraw_big_rectangle.png" + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_polygon.png")) - # Act - draw.rectangle(bbox, brush) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) +def test_polygon1(): + helper_polygon(POINTS1) - @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") - def test_text(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - font = ImageDraw2.Font("white", FONT_PATH) - expected = "Tests/images/imagedraw2_text.png" - # Act - draw.text((5, 5), "ImageDraw2", font) +def test_polygon2(): + helper_polygon(POINTS2) - # Assert - self.assert_image_similar(im, Image.open(expected), 13) - @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") - def test_textsize(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - font = ImageDraw2.Font("white", FONT_PATH) +def helper_rectangle(bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("green", width=2) + brush = ImageDraw2.Brush("black") - # Act - size = draw.textsize("ImageDraw2", font) + # Act + draw.rectangle(bbox, pen, brush) - # Assert - self.assertEqual(size[1], 12) + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_rectangle.png")) - @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") - def test_textsize_empty_string(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - font = ImageDraw2.Font("white", FONT_PATH) - # Act - # Should not cause 'SystemError: returned NULL without setting an error' - draw.textsize("", font) - draw.textsize("\n", font) - draw.textsize("test\n", font) +def test_rectangle1(): + helper_rectangle(BBOX1) - @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") - def test_flush(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - font = ImageDraw2.Font("white", FONT_PATH) - # Act - draw.text((5, 5), "ImageDraw2", font) - im2 = draw.flush() +def test_rectangle2(): + helper_rectangle(BBOX2) - # Assert - self.assert_image_equal(im, im2) + +def test_big_rectangle(): + # Test drawing a rectangle bigger than the image + # Arrange + im = Image.new("RGB", (W, H)) + bbox = [(-1, -1), (W + 1, H + 1)] + brush = ImageDraw2.Brush("orange") + draw = ImageDraw2.Draw(im) + expected = "Tests/images/imagedraw_big_rectangle.png" + + # Act + draw.rectangle(bbox, brush) + + # Assert + assert_image_similar(im, Image.open(expected), 1) + + +@skip_unless_feature("freetype2") +def test_text(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + expected = "Tests/images/imagedraw2_text.png" + + # Act + draw.text((5, 5), "ImageDraw2", font) + + # Assert + assert_image_similar(im, Image.open(expected), 13) + + +@skip_unless_feature("freetype2") +def test_textsize(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + size = draw.textsize("ImageDraw2", font) + + # Assert + assert size[1] == 12 + + +@skip_unless_feature("freetype2") +def test_textsize_empty_string(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + # Should not cause 'SystemError: returned NULL without setting an error' + draw.textsize("", font) + draw.textsize("\n", font) + draw.textsize("test\n", font) + + +@skip_unless_feature("freetype2") +def test_flush(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + draw.text((5, 5), "ImageDraw2", font) + im2 = draw.flush() + + # Assert + assert_image_equal(im, im2) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index d0d994eee..8bc94401e 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,54 +1,55 @@ from PIL import Image, ImageEnhance -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper -class TestImageEnhance(PillowTestCase): - def test_sanity(self): +def test_sanity(): + # FIXME: assert_image + # Implicit asserts no exception: + ImageEnhance.Color(hopper()).enhance(0.5) + ImageEnhance.Contrast(hopper()).enhance(0.5) + ImageEnhance.Brightness(hopper()).enhance(0.5) + ImageEnhance.Sharpness(hopper()).enhance(0.5) - # FIXME: assert_image - # Implicit asserts no exception: - ImageEnhance.Color(hopper()).enhance(0.5) - ImageEnhance.Contrast(hopper()).enhance(0.5) - ImageEnhance.Brightness(hopper()).enhance(0.5) - ImageEnhance.Sharpness(hopper()).enhance(0.5) - def test_crash(self): +def test_crash(): + # crashes on small images + im = Image.new("RGB", (1, 1)) + ImageEnhance.Sharpness(im).enhance(0.5) - # crashes on small images - im = Image.new("RGB", (1, 1)) - ImageEnhance.Sharpness(im).enhance(0.5) - def _half_transparent_image(self): - # returns an image, half transparent, half solid - im = hopper("RGB") +def _half_transparent_image(): + # returns an image, half transparent, half solid + im = hopper("RGB") - transparent = Image.new("L", im.size, 0) - solid = Image.new("L", (im.size[0] // 2, im.size[1]), 255) - transparent.paste(solid, (0, 0)) - im.putalpha(transparent) + transparent = Image.new("L", im.size, 0) + solid = Image.new("L", (im.size[0] // 2, im.size[1]), 255) + transparent.paste(solid, (0, 0)) + im.putalpha(transparent) - return im + return im - def _check_alpha(self, im, original, op, amount): - self.assertEqual(im.getbands(), original.getbands()) - self.assert_image_equal( - im.getchannel("A"), - original.getchannel("A"), - "Diff on {}: {}".format(op, amount), - ) - def test_alpha(self): - # Issue https://github.com/python-pillow/Pillow/issues/899 - # Is alpha preserved through image enhancement? +def _check_alpha(im, original, op, amount): + assert im.getbands() == original.getbands() + assert_image_equal( + im.getchannel("A"), + original.getchannel("A"), + f"Diff on {op}: {amount}", + ) - original = self._half_transparent_image() - for op in ["Color", "Brightness", "Contrast", "Sharpness"]: - for amount in [0, 0.5, 1.0]: - self._check_alpha( - getattr(ImageEnhance, op)(original).enhance(amount), - original, - op, - amount, - ) +def test_alpha(): + # Issue https://github.com/python-pillow/Pillow/issues/899 + # Is alpha preserved through image enhancement? + + original = _half_transparent_image() + + for op in ["Color", "Brightness", "Contrast", "Sharpness"]: + for amount in [0, 0.5, 1.0]: + _check_alpha( + getattr(ImageEnhance, op)(original).enhance(amount), + original, + op, + amount, + ) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 228bbf929..b4107e8e3 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,30 +1,29 @@ -import unittest from io import BytesIO -from PIL import EpsImagePlugin, Image, ImageFile +import pytest -from .helper import PillowTestCase, fromstring, hopper, tostring +from PIL import EpsImagePlugin, Image, ImageFile, features -try: - from PIL import _webp - - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False - - -codecs = dir(Image.core) +from .helper import ( + assert_image, + assert_image_equal, + assert_image_similar, + fromstring, + hopper, + skip_unless_feature, + tostring, +) # save original block sizes MAXBLOCK = ImageFile.MAXBLOCK SAFEBLOCK = ImageFile.SAFEBLOCK -class TestImageFile(PillowTestCase): +class TestImageFile: def test_parser(self): def roundtrip(format): - im = hopper("L").resize((1000, 1000)) + im = hopper("L").resize((1000, 1000), Image.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") @@ -40,23 +39,23 @@ class TestImageFile(PillowTestCase): return im, imOut - self.assert_image_equal(*roundtrip("BMP")) + assert_image_equal(*roundtrip("BMP")) im1, im2 = roundtrip("GIF") - self.assert_image_similar(im1.convert("P"), im2, 1) - self.assert_image_equal(*roundtrip("IM")) - self.assert_image_equal(*roundtrip("MSP")) - if "zip_encoder" in codecs: + assert_image_similar(im1.convert("P"), im2, 1) + assert_image_equal(*roundtrip("IM")) + assert_image_equal(*roundtrip("MSP")) + if features.check("zlib"): try: # force multiple blocks in PNG driver ImageFile.MAXBLOCK = 8192 - self.assert_image_equal(*roundtrip("PNG")) + assert_image_equal(*roundtrip("PNG")) finally: ImageFile.MAXBLOCK = MAXBLOCK - self.assert_image_equal(*roundtrip("PPM")) - self.assert_image_equal(*roundtrip("TIFF")) - self.assert_image_equal(*roundtrip("XBM")) - self.assert_image_equal(*roundtrip("TGA")) - self.assert_image_equal(*roundtrip("PCX")) + assert_image_equal(*roundtrip("PPM")) + assert_image_equal(*roundtrip("TIFF")) + assert_image_equal(*roundtrip("XBM")) + assert_image_equal(*roundtrip("TGA")) + assert_image_equal(*roundtrip("PCX")) if EpsImagePlugin.has_ghostscript(): im1, im2 = roundtrip("EPS") @@ -67,25 +66,24 @@ class TestImageFile(PillowTestCase): # md5sum: ba974835ff2d6f3f2fd0053a23521d4a # EPS comes back in RGB: - self.assert_image_similar(im1, im2.convert("L"), 20) + assert_image_similar(im1, im2.convert("L"), 20) - if "jpeg_encoder" in codecs: + if features.check("jpg"): im1, im2 = roundtrip("JPEG") # lossy compression - self.assert_image(im1, im2.mode, im2.size) + assert_image(im1, im2.mode, im2.size) - self.assertRaises(IOError, roundtrip, "PDF") + with pytest.raises(OSError): + roundtrip("PDF") def test_ico(self): with open("Tests/images/python.ico", "rb") as f: data = f.read() with ImageFile.Parser() as p: p.feed(data) - self.assertEqual((48, 48), p.image.size) + assert (48, 48) == p.image.size + @skip_unless_feature("zlib") def test_safeblock(self): - if "zip_encoder" not in codecs: - self.skipTest("PNG (zlib) encoder not available") - im1 = hopper() try: @@ -94,13 +92,20 @@ class TestImageFile(PillowTestCase): finally: ImageFile.SAFEBLOCK = SAFEBLOCK - self.assert_image_equal(im1, im2) + assert_image_equal(im1, im2) def test_raise_ioerror(self): - self.assertRaises(IOError, ImageFile.raise_ioerror, 1) + with pytest.raises(IOError): + with pytest.warns(DeprecationWarning) as record: + ImageFile.raise_ioerror(1) + assert len(record) == 1 + + def test_raise_oserror(self): + with pytest.raises(OSError): + ImageFile.raise_oserror(1) def test_raise_typeerror(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): parser = ImageFile.Parser() parser.feed(1) @@ -109,52 +114,42 @@ class TestImageFile(PillowTestCase): input = f.read() p = ImageFile.Parser() p.feed(input) - with self.assertRaises(IOError): + with pytest.raises(OSError): p.close() + @skip_unless_feature("zlib") def test_truncated_with_errors(self): - if "zip_encoder" not in codecs: - self.skipTest("PNG (zlib) encoder not available") - with Image.open("Tests/images/truncated_image.png") as im: - with self.assertRaises(IOError): + with pytest.raises(OSError): im.load() # Test that the error is raised if loaded a second time - with self.assertRaises(IOError): + with pytest.raises(OSError): im.load() + @skip_unless_feature("zlib") def test_truncated_without_errors(self): - if "zip_encoder" not in codecs: - self.skipTest("PNG (zlib) encoder not available") - - im = Image.open("Tests/images/truncated_image.png") - - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + with Image.open("Tests/images/truncated_image.png") as im: + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + im.load() + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + @skip_unless_feature("zlib") def test_broken_datastream_with_errors(self): - if "zip_encoder" not in codecs: - self.skipTest("PNG (zlib) encoder not available") - - im = Image.open("Tests/images/broken_data_stream.png") - with self.assertRaises(IOError): - im.load() + 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): - if "zip_encoder" not in codecs: - self.skipTest("PNG (zlib) encoder not available") - - im = Image.open("Tests/images/broken_data_stream.png") - - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + with Image.open("Tests/images/broken_data_stream.png") as im: + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + im.load() + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False class MockPyDecoder(ImageFile.PyDecoder): @@ -174,7 +169,7 @@ class MockImageFile(ImageFile.ImageFile): self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] -class TestPyDecoder(PillowTestCase): +class TestPyDecoder: def get_decoder(self): decoder = MockPyDecoder(None) @@ -193,12 +188,13 @@ class TestPyDecoder(PillowTestCase): im.load() - self.assertEqual(d.state.xoff, xoff) - self.assertEqual(d.state.yoff, yoff) - self.assertEqual(d.state.xsize, xsize) - self.assertEqual(d.state.ysize, ysize) + assert d.state.xoff == xoff + assert d.state.yoff == yoff + assert d.state.xsize == xsize + assert d.state.ysize == ysize - self.assertRaises(ValueError, d.set_as_raw, b"\x00") + with pytest.raises(ValueError): + d.set_as_raw(b"\x00") def test_extents_none(self): buf = BytesIO(b"\x00" * 255) @@ -209,10 +205,10 @@ class TestPyDecoder(PillowTestCase): im.load() - self.assertEqual(d.state.xoff, 0) - self.assertEqual(d.state.yoff, 0) - self.assertEqual(d.state.xsize, 200) - self.assertEqual(d.state.ysize, 200) + assert d.state.xoff == 0 + assert d.state.yoff == 0 + assert d.state.xsize == 200 + assert d.state.ysize == 200 def test_negsize(self): buf = BytesIO(b"\x00" * 255) @@ -221,10 +217,12 @@ class TestPyDecoder(PillowTestCase): im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] self.get_decoder() - self.assertRaises(ValueError, im.load) + with pytest.raises(ValueError): + im.load() im.tile = [("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)] - self.assertRaises(ValueError, im.load) + with pytest.raises(ValueError): + im.load() def test_oversize(self): buf = BytesIO(b"\x00" * 255) @@ -233,104 +231,21 @@ class TestPyDecoder(PillowTestCase): im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] self.get_decoder() - self.assertRaises(ValueError, im.load) + with pytest.raises(ValueError): + im.load() im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)] - self.assertRaises(ValueError, im.load) + with pytest.raises(ValueError): + im.load() def test_no_format(self): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - self.assertIsNone(im.format) - self.assertIsNone(im.get_format_mimetype()) + assert im.format is None + assert im.get_format_mimetype() is None - def test_exif_jpeg(self): - im = Image.open("Tests/images/exif-72dpi-int.jpg") # Little endian - exif = im.getexif() - self.assertNotIn(258, exif) - self.assertIn(40960, exif) - self.assertEqual(exif[40963], 450) - self.assertEqual(exif[11], "gThumb 3.0.1") - - out = self.tempfile("temp.jpg") - exif[258] = 8 - del exif[40960] - exif[40963] = 455 - exif[11] = "Pillow test" - im.save(out, exif=exif) - with Image.open(out) as reloaded: - reloaded_exif = reloaded.getexif() - self.assertEqual(reloaded_exif[258], 8) - self.assertNotIn(40960, exif) - self.assertEqual(reloaded_exif[40963], 455) - self.assertEqual(exif[11], "Pillow test") - - im = Image.open("Tests/images/no-dpi-in-exif.jpg") # Big endian - exif = im.getexif() - self.assertNotIn(258, exif) - self.assertIn(40962, exif) - self.assertEqual(exif[40963], 200) - self.assertEqual(exif[305], "Adobe Photoshop CC 2017 (Macintosh)") - - out = self.tempfile("temp.jpg") - exif[258] = 8 - del exif[34665] - exif[40963] = 455 - exif[305] = "Pillow test" - im.save(out, exif=exif) - with Image.open(out) as reloaded: - reloaded_exif = reloaded.getexif() - self.assertEqual(reloaded_exif[258], 8) - self.assertNotIn(40960, exif) - self.assertEqual(reloaded_exif[40963], 455) - self.assertEqual(exif[305], "Pillow test") - - @unittest.skipIf( - not HAVE_WEBP or not _webp.HAVE_WEBPANIM, - "WebP support not installed with animation", - ) - def test_exif_webp(self): - im = Image.open("Tests/images/hopper.webp") - exif = im.getexif() - self.assertEqual(exif, {}) - - out = self.tempfile("temp.webp") - exif[258] = 8 - exif[40963] = 455 - exif[305] = "Pillow test" - - def check_exif(): - with Image.open(out) as reloaded: - reloaded_exif = reloaded.getexif() - self.assertEqual(reloaded_exif[258], 8) - self.assertEqual(reloaded_exif[40963], 455) - self.assertEqual(exif[305], "Pillow test") - - im.save(out, exif=exif) - check_exif() - im.save(out, exif=exif, save_all=True) - check_exif() - - def test_exif_png(self): - im = Image.open("Tests/images/exif.png") - exif = im.getexif() - self.assertEqual(exif, {274: 1}) - - out = self.tempfile("temp.png") - exif[258] = 8 - del exif[274] - exif[40963] = 455 - exif[305] = "Pillow test" - im.save(out, exif=exif) - - with Image.open(out) as reloaded: - reloaded_exif = reloaded.getexif() - self.assertEqual(reloaded_exif, {258: 8, 40963: 455, 305: "Pillow test"}) - - def test_exif_interop(self): - with Image.open("Tests/images/flower.jpg") as im: - exif = im.getexif() - self.assertEqual( - exif.get_ifd(0xA005), {1: "R98", 2: b"0100", 4097: 2272, 4098: 1704} - ) + def test_oserror(self): + im = Image.new("RGB", (1, 1)) + with pytest.raises(OSError): + im.save(BytesIO(), "JPEG2000") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 5a15eb535..7824b7f53 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,65 +1,69 @@ import copy -import distutils.version import os import re import shutil import sys -import unittest from io import BytesIO +import pytest +from packaging.version import parse as parse_version + from PIL import Image, ImageDraw, ImageFont, features -from .helper import PillowTestCase, is_pypy, is_win32 +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_tofile, + is_win32, + skip_unless_feature, + skip_unless_feature_version, +) FONT_PATH = "Tests/fonts/FreeMono.ttf" FONT_SIZE = 20 TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" -HAS_FREETYPE = features.check("freetype2") -HAS_RAQM = features.check("raqm") + +pytestmark = skip_unless_feature("freetype2") -class SimplePatcher: - def __init__(self, parent_obj, attr_name, value): - self._parent_obj = parent_obj - self._attr_name = attr_name - self._saved = None - self._is_saved = False - self._value = value - - def __enter__(self): - # Patch the attr on the object - if hasattr(self._parent_obj, self._attr_name): - self._saved = getattr(self._parent_obj, self._attr_name) - setattr(self._parent_obj, self._attr_name, self._value) - self._is_saved = True - else: - setattr(self._parent_obj, self._attr_name, self._value) - self._is_saved = False - - def __exit__(self, type, value, traceback): - # Restore the original value - if self._is_saved: - setattr(self._parent_obj, self._attr_name, self._saved) - else: - delattr(self._parent_obj, self._attr_name) - - -@unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") -class TestImageFont(PillowTestCase): +class TestImageFont: LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC # Freetype has different metrics depending on the version. # (and, other things, but first things first) METRICS = { - (">=2.3", "<2.4"): {"multiline": 30, "textsize": 12, "getters": (13, 16)}, - (">=2.7",): {"multiline": 6.2, "textsize": 2.5, "getters": (12, 16)}, - "Default": {"multiline": 0.5, "textsize": 0.5, "getters": (12, 16)}, + (">=2.3", "<2.4"): { + "multiline": 30, + "textsize": 12, + "getters": (13, 16), + "mask": (107, 13), + "multiline-anchor": 6, + "getlength": (36, 27, 27, 33), + }, + (">=2.7",): { + "multiline": 6.2, + "textsize": 2.5, + "getters": (12, 16), + "mask": (108, 13), + "multiline-anchor": 4, + "getlength": (36, 21, 24, 33), + }, + "Default": { + "multiline": 0.5, + "textsize": 0.5, + "getters": (12, 16), + "mask": (108, 13), + "multiline-anchor": 4, + "getlength": (36, 24, 24, 33), + }, } - def setUp(self): - freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) + @classmethod + def setup_class(self): + freetype = parse_version(features.version_module("freetype2")) self.metrics = self.METRICS["Default"] for conditions, metrics in self.METRICS.items(): @@ -67,7 +71,7 @@ class TestImageFont(PillowTestCase): continue for condition in conditions: - version = re.sub("[<=>]", "", condition) + version = parse_version(re.sub("[<=>]", "", condition)) if (condition.startswith(">=") and freetype >= version) or ( condition.startswith("<") and freetype < version ): @@ -86,23 +90,23 @@ class TestImageFont(PillowTestCase): ) def test_sanity(self): - self.assertRegex(ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") + assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) def test_font_properties(self): ttf = self.get_font() - self.assertEqual(ttf.path, FONT_PATH) - self.assertEqual(ttf.size, FONT_SIZE) + assert ttf.path == FONT_PATH + assert ttf.size == FONT_SIZE ttf_copy = ttf.font_variant() - self.assertEqual(ttf_copy.path, FONT_PATH) - self.assertEqual(ttf_copy.size, FONT_SIZE) + assert ttf_copy.path == FONT_PATH + assert ttf_copy.size == FONT_SIZE ttf_copy = ttf.font_variant(size=FONT_SIZE + 1) - self.assertEqual(ttf_copy.size, FONT_SIZE + 1) + assert ttf_copy.size == FONT_SIZE + 1 second_font_path = "Tests/fonts/DejaVuSans.ttf" ttf_copy = ttf.font_variant(font=second_font_path) - self.assertEqual(ttf_copy.path, second_font_path) + assert ttf_copy.path == second_font_path def test_font_with_name(self): self.get_font() @@ -121,18 +125,19 @@ class TestImageFont(PillowTestCase): # Usage note: making two fonts from the same buffer fails. # shared_bytes = self._font_as_bytes() # self._render(shared_bytes) - # self.assertRaises(Exception, _render, shared_bytes) + # with pytest.raises(Exception): + # _render(shared_bytes) def test_font_with_open_file(self): with open(FONT_PATH, "rb") as f: self._render(f) - def test_non_unicode_path(self): + def test_non_ascii_path(self, tmp_path): + tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) try: - tempfile = self.tempfile("temp_" + chr(128) + ".ttf") + shutil.copy(FONT_PATH, tempfile) except UnicodeEncodeError: - self.skipTest("Unicode path could not be created") - shutil.copy(FONT_PATH, tempfile) + pytest.skip("Non-ASCII path could not be created") ImageFont.truetype(tempfile, FONT_SIZE) @@ -147,7 +152,7 @@ class TestImageFont(PillowTestCase): finally: ImageFont.core.HAVE_RAQM = have_raqm - self.assertEqual(ttf.layout_engine, ImageFont.LAYOUT_BASIC) + assert ttf.layout_engine == ImageFont.LAYOUT_BASIC def _render(self, font): txt = "Hello World!" @@ -166,7 +171,19 @@ class TestImageFont(PillowTestCase): font_filelike = BytesIO(f.read()) img_filelike = self._render(font_filelike) - self.assert_image_equal(img_path, img_filelike) + assert_image_equal(img_path, img_filelike) + + def test_transparent_background(self): + im = Image.new(mode="RGBA", size=(300, 100)) + draw = ImageDraw.Draw(im) + ttf = self.get_font() + + txt = "Hello World!" + draw.text((10, 10), txt, font=ttf) + + target = "Tests/images/transparent_background_text.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 4.09) def test_textsize_equal(self): im = Image.new(mode="RGB", size=(300, 100)) @@ -179,10 +196,41 @@ class TestImageFont(PillowTestCase): draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) target = "Tests/images/rectangle_surrounding_text.png" - target_img = Image.open(target) + with Image.open(target) as target_img: - # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics["textsize"]) + # Epsilon ~.5 fails with FreeType 2.7 + assert_image_similar(im, target_img, self.metrics["textsize"]) + + @pytest.mark.parametrize( + "text, mode, font, size, length_basic_index, length_raqm", + ( + # basic test + ("text", "L", "FreeMono.ttf", 15, 0, 36), + ("text", "1", "FreeMono.ttf", 15, 0, 36), + # issue 4177 + ("rrr", "L", "DejaVuSans.ttf", 18, 1, 22.21875), + ("rrr", "1", "DejaVuSans.ttf", 18, 2, 22.21875), + # test 'l' not including extra margin + # using exact value 2047 / 64 for raqm, checked with debugger + ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 3, 31.984375), + ("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 3, 31.984375), + ), + ) + def test_getlength(self, text, mode, font, size, length_basic_index, length_raqm): + f = ImageFont.truetype( + "Tests/fonts/" + font, size, layout_engine=self.LAYOUT_ENGINE + ) + + im = Image.new(mode, (1, 1), 0) + d = ImageDraw.Draw(im) + + if self.LAYOUT_ENGINE == ImageFont.LAYOUT_BASIC: + length = d.textlength(text, f) + assert length == self.metrics["getlength"][length_basic_index] + else: + # disable kerning, kerning metrics changed + length = d.textlength(text, f, features=["-kern"]) + assert length == length_raqm def test_render_multiline(self): im = Image.new(mode="RGB", size=(300, 100)) @@ -196,12 +244,12 @@ class TestImageFont(PillowTestCase): y += line_spacing target = "Tests/images/multiline_text.png" - target_img = Image.open(target) + with Image.open(target) as target_img: - # some versions of freetype have different horizontal spacing. - # setting a tight epsilon, I'm showing the original test failure - # at epsilon = ~38. - self.assert_image_similar(im, target_img, self.metrics["multiline"]) + # some versions of freetype have different horizontal spacing. + # setting a tight epsilon, I'm showing the original test failure + # at epsilon = ~38. + assert_image_similar(im, target_img, self.metrics["multiline"]) def test_render_multiline_text(self): ttf = self.get_font() @@ -213,10 +261,10 @@ class TestImageFont(PillowTestCase): draw.text((0, 0), TEST_TEXT, font=ttf) target = "Tests/images/multiline_text.png" - target_img = Image.open(target) + with Image.open(target) as target_img: - # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics["multiline"]) + # Epsilon ~.5 fails with FreeType 2.7 + assert_image_similar(im, target_img, self.metrics["multiline"]) # Test that text() can pass on additional arguments # to multiline_text() @@ -232,10 +280,10 @@ class TestImageFont(PillowTestCase): draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align) target = "Tests/images/multiline_text" + ext + ".png" - target_img = Image.open(target) + with Image.open(target) as target_img: - # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics["multiline"]) + # Epsilon ~.5 fails with FreeType 2.7 + assert_image_similar(im, target_img, self.metrics["multiline"]) def test_unknown_align(self): im = Image.new(mode="RGB", size=(300, 100)) @@ -243,14 +291,8 @@ class TestImageFont(PillowTestCase): ttf = self.get_font() # Act/Assert - self.assertRaises( - ValueError, - draw.multiline_text, - (0, 0), - TEST_TEXT, - font=ttf, - align="unknown", - ) + with pytest.raises(ValueError): + draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align="unknown") def test_draw_align(self): im = Image.new("RGB", (300, 100), "white") @@ -265,14 +307,13 @@ class TestImageFont(PillowTestCase): draw = ImageDraw.Draw(im) # Test that textsize() correctly connects to multiline_textsize() - self.assertEqual( - draw.textsize(TEST_TEXT, font=ttf), - draw.multiline_textsize(TEST_TEXT, font=ttf), + assert draw.textsize(TEST_TEXT, font=ttf) == draw.multiline_textsize( + TEST_TEXT, font=ttf ) # Test that multiline_textsize corresponds to ImageFont.textsize() # for single line text - self.assertEqual(ttf.getsize("A"), draw.multiline_textsize("A", font=ttf)) + assert ttf.getsize("A") == draw.multiline_textsize("A", font=ttf) # Test that textsize() can pass on additional arguments # to multiline_textsize() @@ -284,9 +325,9 @@ class TestImageFont(PillowTestCase): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - self.assertEqual( - draw.textsize("longest line", font=ttf)[0], - draw.multiline_textsize("longest line\nline", font=ttf)[0], + assert ( + draw.textsize("longest line", font=ttf)[0] + == draw.multiline_textsize("longest line\nline", font=ttf)[0] ) def test_multiline_spacing(self): @@ -297,10 +338,10 @@ class TestImageFont(PillowTestCase): draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) target = "Tests/images/multiline_text_spacing.png" - target_img = Image.open(target) + with Image.open(target) as target_img: - # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics["multiline"]) + # Epsilon ~.5 fails with FreeType 2.7 + assert_image_similar(im, target_img, self.metrics["multiline"]) def test_rotated_transposed_font(self): img_grey = Image.new("L", (100, 100)) @@ -320,8 +361,8 @@ class TestImageFont(PillowTestCase): box_size_b = draw.textsize(word) # Check (w,h) of box a is (h,w) of box b - self.assertEqual(box_size_a[0], box_size_b[1]) - self.assertEqual(box_size_a[1], box_size_b[0]) + assert box_size_a[0] == box_size_b[1] + assert box_size_a[1] == box_size_b[0] def test_unrotated_transposed_font(self): img_grey = Image.new("L", (100, 100)) @@ -341,7 +382,7 @@ class TestImageFont(PillowTestCase): box_size_b = draw.textsize(word) # Check boxes a and b are same size - self.assertEqual(box_size_a, box_size_b) + assert box_size_a == box_size_b def test_rotated_transposed_font_get_mask(self): # Arrange @@ -354,7 +395,7 @@ class TestImageFont(PillowTestCase): mask = transposed_font.getmask(text) # Assert - self.assertEqual(mask.size, (13, 108)) + assert mask.size == self.metrics["mask"][::-1] def test_unrotated_transposed_font_get_mask(self): # Arrange @@ -367,7 +408,7 @@ class TestImageFont(PillowTestCase): mask = transposed_font.getmask(text) # Assert - self.assertEqual(mask.size, (108, 13)) + assert mask.size == self.metrics["mask"] def test_free_type_font_get_name(self): # Arrange @@ -377,7 +418,7 @@ class TestImageFont(PillowTestCase): name = font.getname() # Assert - self.assertEqual(("FreeMono", "Regular"), name) + assert ("FreeMono", "Regular") == name def test_free_type_font_get_metrics(self): # Arrange @@ -387,9 +428,9 @@ class TestImageFont(PillowTestCase): ascent, descent = font.getmetrics() # Assert - self.assertIsInstance(ascent, int) - self.assertIsInstance(descent, int) - self.assertEqual((ascent, descent), (16, 4)) # too exact check? + assert isinstance(ascent, int) + assert isinstance(descent, int) + assert (ascent, descent) == (16, 4) # too exact check? def test_free_type_font_get_offset(self): # Arrange @@ -400,7 +441,7 @@ class TestImageFont(PillowTestCase): offset = font.getoffset(text) # Assert - self.assertEqual(offset, (0, 3)) + assert offset == (0, 3) def test_free_type_font_get_mask(self): # Arrange @@ -411,19 +452,22 @@ class TestImageFont(PillowTestCase): mask = font.getmask(text) # Assert - self.assertEqual(mask.size, (108, 13)) + assert mask.size == self.metrics["mask"] def test_load_path_not_found(self): # Arrange filename = "somefilenamethatdoesntexist.ttf" # Act/Assert - self.assertRaises(IOError, ImageFont.load_path, filename) - self.assertRaises(IOError, ImageFont.truetype, filename) + with pytest.raises(OSError): + ImageFont.load_path(filename) + with pytest.raises(OSError): + ImageFont.truetype(filename) def test_load_non_font_bytes(self): with open("Tests/images/hopper.jpg", "rb") as f: - self.assertRaises(IOError, ImageFont.truetype, f) + with pytest.raises(OSError): + ImageFont.truetype(f) def test_default_font(self): # Arrange @@ -432,20 +476,20 @@ class TestImageFont(PillowTestCase): draw = ImageDraw.Draw(im) target = "Tests/images/default_font.png" - target_img = Image.open(target) + with Image.open(target) as target_img: - # Act - default_font = ImageFont.load_default() - draw.text((10, 10), txt, font=default_font) + # Act + default_font = ImageFont.load_default() + draw.text((10, 10), txt, font=default_font) - # Assert - self.assert_image_equal(im, target_img) + # Assert + assert_image_equal(im, target_img) def test_getsize_empty(self): # issue #2614 font = self.get_font() # should not crash. - self.assertEqual((0, 0), font.getsize("")) + assert (0, 0) == font.getsize("") def test_render_empty(self): # issue 2666 @@ -455,16 +499,15 @@ class TestImageFont(PillowTestCase): draw = ImageDraw.Draw(im) # should not crash here. draw.text((10, 10), "", font=font) - self.assert_image_equal(im, target) + assert_image_equal(im, target) def test_unicode_pilfont(self): # should not segfault, should return UnicodeDecodeError # issue #2826 font = ImageFont.load_default() - with self.assertRaises(UnicodeEncodeError): + with pytest.raises(UnicodeEncodeError): font.getsize("’") - @unittest.skipIf(is_pypy(), "failing on PyPy") def test_unicode_extended(self): # issue #3777 text = "A\u278A\U0001F12B" @@ -479,12 +522,14 @@ class TestImageFont(PillowTestCase): d = ImageDraw.Draw(img) d.text((10, 10), text, font=ttf) - self.assert_image_similar_tofile(img, target, self.metrics["multiline"]) + # fails with 14.7 + assert_image_similar_tofile(img, target, 6.2) - def _test_fake_loading_font(self, path_to_fake, fontname): + def _test_fake_loading_font(self, monkeypatch, path_to_fake, fontname): # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) - with SimplePatcher(ImageFont, "_FreeTypeFont", free_type_font): + with monkeypatch.context() as m: + m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) def loadable_font(filepath, size, index, encoding, *args, **kwargs): if filepath == path_to_fake: @@ -495,112 +540,108 @@ class TestImageFont(PillowTestCase): filepath, size, index, encoding, *args, **kwargs ) - with SimplePatcher(ImageFont, "FreeTypeFont", loadable_font): - font = ImageFont.truetype(fontname) - # Make sure it's loaded - name = font.getname() - self.assertEqual(("FreeMono", "Regular"), name) + m.setattr(ImageFont, "FreeTypeFont", loadable_font) + font = ImageFont.truetype(fontname) + # Make sure it's loaded + name = font.getname() + assert ("FreeMono", "Regular") == name - @unittest.skipIf(is_win32(), "requires Unix or macOS") - def test_find_linux_font(self): + @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") + def test_find_linux_font(self, monkeypatch): # A lot of mocking here - this is more for hitting code and # catching syntax like errors font_directory = "/usr/local/share/fonts" - with SimplePatcher(sys, "platform", "linux"): - patched_env = copy.deepcopy(os.environ) - patched_env["XDG_DATA_DIRS"] = "/usr/share/:/usr/local/share/" - with SimplePatcher(os, "environ", patched_env): + monkeypatch.setattr(sys, "platform", "linux") + monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") - def fake_walker(path): - if path == font_directory: - return [ - ( - path, - [], - [ - "Arial.ttf", - "Single.otf", - "Duplicate.otf", - "Duplicate.ttf", - ], - ) - ] - return [(path, [], ["some_random_font.ttf"])] - - with SimplePatcher(os, "walk", fake_walker): - # Test that the font loads both with and without the - # extension - self._test_fake_loading_font( - font_directory + "/Arial.ttf", "Arial.ttf" + def fake_walker(path): + if path == font_directory: + return [ + ( + path, + [], + ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"], ) - self._test_fake_loading_font(font_directory + "/Arial.ttf", "Arial") + ] + return [(path, [], ["some_random_font.ttf"])] - # Test that non-ttf fonts can be found without the - # extension - self._test_fake_loading_font( - font_directory + "/Single.otf", "Single" - ) + monkeypatch.setattr(os, "walk", fake_walker) + # Test that the font loads both with and without the + # extension + self._test_fake_loading_font( + monkeypatch, font_directory + "/Arial.ttf", "Arial.ttf" + ) + self._test_fake_loading_font( + monkeypatch, font_directory + "/Arial.ttf", "Arial" + ) - # Test that ttf fonts are preferred if the extension is - # not specified - self._test_fake_loading_font( - font_directory + "/Duplicate.ttf", "Duplicate" - ) + # Test that non-ttf fonts can be found without the + # extension + self._test_fake_loading_font( + monkeypatch, font_directory + "/Single.otf", "Single" + ) - @unittest.skipIf(is_win32(), "requires Unix or macOS") - def test_find_macos_font(self): + # Test that ttf fonts are preferred if the extension is + # not specified + self._test_fake_loading_font( + monkeypatch, font_directory + "/Duplicate.ttf", "Duplicate" + ) + + @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") + def test_find_macos_font(self, monkeypatch): # Like the linux test, more cover hitting code rather than testing # correctness. font_directory = "/System/Library/Fonts" - with SimplePatcher(sys, "platform", "darwin"): + monkeypatch.setattr(sys, "platform", "darwin") - def fake_walker(path): - if path == font_directory: - return [ - ( - path, - [], - [ - "Arial.ttf", - "Single.otf", - "Duplicate.otf", - "Duplicate.ttf", - ], - ) - ] - return [(path, [], ["some_random_font.ttf"])] + def fake_walker(path): + if path == font_directory: + return [ + ( + path, + [], + ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"], + ) + ] + return [(path, [], ["some_random_font.ttf"])] - with SimplePatcher(os, "walk", fake_walker): - self._test_fake_loading_font(font_directory + "/Arial.ttf", "Arial.ttf") - self._test_fake_loading_font(font_directory + "/Arial.ttf", "Arial") - self._test_fake_loading_font(font_directory + "/Single.otf", "Single") - self._test_fake_loading_font( - font_directory + "/Duplicate.ttf", "Duplicate" - ) + monkeypatch.setattr(os, "walk", fake_walker) + self._test_fake_loading_font( + monkeypatch, font_directory + "/Arial.ttf", "Arial.ttf" + ) + self._test_fake_loading_font( + monkeypatch, font_directory + "/Arial.ttf", "Arial" + ) + self._test_fake_loading_font( + monkeypatch, font_directory + "/Single.otf", "Single" + ) + self._test_fake_loading_font( + monkeypatch, font_directory + "/Duplicate.ttf", "Duplicate" + ) def test_imagefont_getters(self): # Arrange t = self.get_font() # Act / Assert - self.assertEqual(t.getmetrics(), (16, 4)) - self.assertEqual(t.font.ascent, 16) - self.assertEqual(t.font.descent, 4) - self.assertEqual(t.font.height, 20) - self.assertEqual(t.font.x_ppem, 20) - self.assertEqual(t.font.y_ppem, 20) - self.assertEqual(t.font.glyphs, 4177) - self.assertEqual(t.getsize("A"), (12, 16)) - self.assertEqual(t.getsize("AB"), (24, 16)) - self.assertEqual(t.getsize("M"), self.metrics["getters"]) - self.assertEqual(t.getsize("y"), (12, 20)) - self.assertEqual(t.getsize("a"), (12, 16)) - self.assertEqual(t.getsize_multiline("A"), (12, 16)) - self.assertEqual(t.getsize_multiline("AB"), (24, 16)) - self.assertEqual(t.getsize_multiline("a"), (12, 16)) - self.assertEqual(t.getsize_multiline("ABC\n"), (36, 36)) - self.assertEqual(t.getsize_multiline("ABC\nA"), (36, 36)) - self.assertEqual(t.getsize_multiline("ABC\nAaaa"), (48, 36)) + assert t.getmetrics() == (16, 4) + assert t.font.ascent == 16 + assert t.font.descent == 4 + assert t.font.height == 20 + assert t.font.x_ppem == 20 + assert t.font.y_ppem == 20 + assert t.font.glyphs == 4177 + assert t.getsize("A") == (12, 16) + assert t.getsize("AB") == (24, 16) + assert t.getsize("M") == self.metrics["getters"] + assert t.getsize("y") == (12, 20) + assert t.getsize("a") == (12, 16) + assert t.getsize_multiline("A") == (12, 16) + assert t.getsize_multiline("AB") == (24, 16) + assert t.getsize_multiline("a") == (12, 16) + assert t.getsize_multiline("ABC\n") == (36, 36) + assert t.getsize_multiline("ABC\nA") == (36, 36) + assert t.getsize_multiline("ABC\nAaaa") == (48, 36) def test_getsize_stroke(self): # Arrange @@ -608,13 +649,13 @@ class TestImageFont(PillowTestCase): # Act / Assert for stroke_width in [0, 2]: - self.assertEqual( - t.getsize("A", stroke_width=stroke_width), - (12 + stroke_width * 2, 16 + stroke_width * 2), + assert t.getsize("A", stroke_width=stroke_width) == ( + 12 + stroke_width * 2, + 16 + stroke_width * 2, ) - self.assertEqual( - t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width), - (48 + stroke_width * 2, 36 + stroke_width * 4), + assert t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( + 48 + stroke_width * 2, + 36 + stroke_width * 4, ) def test_complex_font_settings(self): @@ -622,129 +663,360 @@ class TestImageFont(PillowTestCase): t = self.get_font() # Act / Assert if t.layout_engine == ImageFont.LAYOUT_BASIC: - self.assertRaises(KeyError, t.getmask, "абвг", direction="rtl") - self.assertRaises(KeyError, t.getmask, "абвг", features=["-kern"]) - self.assertRaises(KeyError, t.getmask, "абвг", language="sr") + with pytest.raises(KeyError): + t.getmask("абвг", direction="rtl") + with pytest.raises(KeyError): + t.getmask("абвг", features=["-kern"]) + with pytest.raises(KeyError): + t.getmask("абвг", language="sr") def test_variation_get(self): font = self.get_font() - freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) - if freetype < "2.9.1": - self.assertRaises(NotImplementedError, font.get_variation_names) - self.assertRaises(NotImplementedError, font.get_variation_axes) + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): + font.get_variation_names() + with pytest.raises(NotImplementedError): + font.get_variation_axes() return - self.assertRaises(IOError, font.get_variation_names) - self.assertRaises(IOError, font.get_variation_axes) + with pytest.raises(OSError): + font.get_variation_names() + with pytest.raises(OSError): + font.get_variation_axes() font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf") - self.assertEqual( - font.get_variation_names(), - [ - b"ExtraLight", - b"Light", - b"Regular", - b"Semibold", - b"Bold", - b"Black", - b"Black Medium Contrast", - b"Black High Contrast", - b"Default", - ], - ) - self.assertEqual( - font.get_variation_axes(), - [ - {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389}, - {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0}, - ], - ) + assert font.get_variation_names(), [ + b"ExtraLight", + b"Light", + b"Regular", + b"Semibold", + b"Bold", + b"Black", + b"Black Medium Contrast", + b"Black High Contrast", + b"Default", + ] + assert font.get_variation_axes() == [ + {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389}, + {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0}, + ] font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf") - self.assertEqual( - font.get_variation_names(), - [ - b"20", - b"40", - b"60", - b"80", - b"100", - b"120", - b"140", - b"160", - b"180", - b"200", - b"220", - b"240", - b"260", - b"280", - b"300", - b"Regular", - ], - ) - self.assertEqual( - font.get_variation_axes(), - [{"name": b"Size", "minimum": 0, "maximum": 300, "default": 0}], - ) + assert font.get_variation_names() == [ + b"20", + b"40", + b"60", + b"80", + b"100", + b"120", + b"140", + b"160", + b"180", + b"200", + b"220", + b"240", + b"260", + b"280", + b"300", + b"Regular", + ] + assert font.get_variation_axes() == [ + {"name": b"Size", "minimum": 0, "maximum": 300, "default": 0} + ] + + def _check_text(self, font, path, epsilon): + im = Image.new("RGB", (100, 75), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), "Text", font=font, fill="black") + + try: + with Image.open(path) as expected: + assert_image_similar(im, expected, epsilon) + except AssertionError: + if "_adobe" in path: + path = path.replace("_adobe", "_adobe_older_harfbuzz") + with Image.open(path) as expected: + assert_image_similar(im, expected, epsilon) + else: + raise def test_variation_set_by_name(self): font = self.get_font() - freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) - if freetype < "2.9.1": - self.assertRaises(NotImplementedError, font.set_variation_by_name, "Bold") + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): + font.set_variation_by_name("Bold") return - self.assertRaises(IOError, font.set_variation_by_name, "Bold") - - def _check_text(font, path, epsilon): - im = Image.new("RGB", (100, 75), "white") - d = ImageDraw.Draw(im) - d.text((10, 10), "Text", font=font, fill="black") - - expected = Image.open(path) - self.assert_image_similar(im, expected, epsilon) + with pytest.raises(OSError): + font.set_variation_by_name("Bold") font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) - _check_text(font, "Tests/images/variation_adobe.png", 11) + self._check_text(font, "Tests/images/variation_adobe.png", 11) for name in ["Bold", b"Bold"]: font.set_variation_by_name(name) - _check_text(font, "Tests/images/variation_adobe_name.png", 11) + self._check_text(font, "Tests/images/variation_adobe_name.png", 11) font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) - _check_text(font, "Tests/images/variation_tiny.png", 40) + self._check_text(font, "Tests/images/variation_tiny.png", 40) for name in ["200", b"200"]: font.set_variation_by_name(name) - _check_text(font, "Tests/images/variation_tiny_name.png", 40) + self._check_text(font, "Tests/images/variation_tiny_name.png", 40) def test_variation_set_by_axes(self): font = self.get_font() - freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) - if freetype < "2.9.1": - self.assertRaises(NotImplementedError, font.set_variation_by_axes, [100]) + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): + font.set_variation_by_axes([100]) return - self.assertRaises(IOError, font.set_variation_by_axes, [500, 50]) - - def _check_text(font, path, epsilon): - im = Image.new("RGB", (100, 75), "white") - d = ImageDraw.Draw(im) - d.text((10, 10), "Text", font=font, fill="black") - - expected = Image.open(path) - self.assert_image_similar(im, expected, epsilon) + with pytest.raises(OSError): + font.set_variation_by_axes([500, 50]) font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) font.set_variation_by_axes([500, 50]) - _check_text(font, "Tests/images/variation_adobe_axes.png", 5.1) + self._check_text(font, "Tests/images/variation_adobe_axes.png", 11.05) font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) font.set_variation_by_axes([100]) - _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) + self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) + + @pytest.mark.parametrize( + "anchor, left, left_old, top", + ( + # test horizontal anchors + ("ls", 0, 0, -36), + ("ms", -64, -65, -36), + ("rs", -128, -129, -36), + # test vertical anchors + ("ma", -64, -65, 16), + ("mt", -64, -65, 0), + ("mm", -64, -65, -17), + ("mb", -64, -65, -44), + ("md", -64, -65, -51), + ), + ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), + ) + def test_anchor(self, anchor, left, left_old, top): + name, text = "quick", "Quick" + path = f"Tests/images/test_anchor_{name}_{anchor}.png" + + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.4"): + width, height = (129, 44) + left = left_old + elif self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM: + width, height = (129, 44) + else: + width, height = (128, 44) + + bbox_expected = (left, top, left + width, top + height) + + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE + ) + + im = Image.new("RGB", (200, 200), "white") + d = ImageDraw.Draw(im) + d.line(((0, 100), (200, 100)), "gray") + d.line(((100, 0), (100, 200)), "gray") + d.text((100, 100), text, fill="black", anchor=anchor, font=f) + + assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected + + with Image.open(path) as expected: + assert_image_similar(im, expected, 7) + + @pytest.mark.parametrize( + "anchor, align", + ( + # test horizontal anchors + ("lm", "left"), + ("lm", "center"), + ("lm", "right"), + ("mm", "left"), + ("mm", "center"), + ("mm", "right"), + ("rm", "left"), + ("rm", "center"), + ("rm", "right"), + # test vertical anchors + ("ma", "center"), + # ("mm", "center"), # duplicate + ("md", "center"), + ), + ) + def test_anchor_multiline(self, anchor, align): + target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" + text = "a\nlong\ntext sample" + + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE + ) + + # test render + im = Image.new("RGB", (600, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (600, 200)), "gray") + d.line(((300, 0), (300, 400)), "gray") + d.multiline_text( + (300, 200), text, fill="black", anchor=anchor, font=f, align=align + ) + + with Image.open(target) as expected: + assert_image_similar(im, expected, self.metrics["multiline-anchor"]) + + def test_anchor_invalid(self): + font = self.get_font() + im = Image.new("RGB", (100, 100), "white") + d = ImageDraw.Draw(im) + d.font = font + + for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: + pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor)) + pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor)) + pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor)) + pytest.raises( + ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor) + ) + pytest.raises( + ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), + ) + for anchor in ["lt", "lb"]: + pytest.raises( + ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), + ) + + @skip_unless_feature("freetype2") + @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) + def test_bitmap_font(self, bpp): + text = "Bitmap Font" + layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE] + target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" + font = ImageFont.truetype( + f"Tests/fonts/DejaVuSans-24-{bpp}-stripped.ttf", + 24, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (160, 35), "white") + draw = ImageDraw.Draw(im) + draw.text((2, 2), text, "black", font) + + assert_image_equal_tofile(im, target) + + def test_standard_embedded_color(self): + txt = "Hello World!" + ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE) + ttf.getsize(txt) + + im = Image.new("RGB", (300, 64), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True) + + with Image.open("Tests/images/standard_embedded.png") as expected: + assert_image_similar(im, expected, max(self.metrics["multiline"], 3)) + + @skip_unless_feature_version("freetype2", "2.5.0") + def test_cbdt(self): + try: + font = ImageFont.truetype( + "Tests/fonts/NotoColorEmoji.ttf", + size=109, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (150, 150), "white") + d = ImageDraw.Draw(im) + + d.text((10, 10), "\U0001f469", embedded_color=True, font=font) + + with Image.open("Tests/images/cbdt_notocoloremoji.png") as expected: + assert_image_similar(im, expected, self.metrics["multiline"]) + except IOError as e: + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or unsupported") + + @skip_unless_feature_version("freetype2", "2.5.0") + def test_cbdt_mask(self): + try: + font = ImageFont.truetype( + "Tests/fonts/NotoColorEmoji.ttf", + size=109, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (150, 150), "white") + d = ImageDraw.Draw(im) + + d.text((10, 10), "\U0001f469", "black", font=font) + + with Image.open("Tests/images/cbdt_notocoloremoji_mask.png") as expected: + assert_image_similar(im, expected, self.metrics["multiline"]) + except IOError as e: + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or unsupported") + + @skip_unless_feature_version("freetype2", "2.10.0") + def test_colr(self): + font = ImageFont.truetype( + "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", + size=64, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (300, 75), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "Bungee", embedded_color=True, font=font) + + with Image.open("Tests/images/colr_bungee.png") as expected: + assert_image_similar(im, expected, 21) + + @skip_unless_feature_version("freetype2", "2.10.0") + def test_colr_mask(self): + font = ImageFont.truetype( + "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", + size=64, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (300, 75), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "Bungee", "black", font=font) + + with Image.open("Tests/images/colr_bungee_mask.png") as expected: + assert_image_similar(im, expected, 22) -@unittest.skipUnless(HAS_RAQM, "Raqm not Available") +@skip_unless_feature("raqm") class TestImageFont_RaqmLayout(TestImageFont): LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM + + +@skip_unless_feature_version("freetype2", "2.4", "Different metrics") +def test_render_mono_size(): + # issue 4177 + + im = Image.new("P", (100, 30), "white") + draw = ImageDraw.Draw(im) + ttf = ImageFont.truetype( + "Tests/fonts/DejaVuSans.ttf", 18, layout_engine=ImageFont.LAYOUT_BASIC + ) + + draw.text((10, 10), "r" * 10, "black", ttf) + assert_image_equal_tofile(im, "Tests/images/text_mono.gif") diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py deleted file mode 100644 index eed0c70b6..000000000 --- a/Tests/test_imagefont_bitmap.py +++ /dev/null @@ -1,45 +0,0 @@ -import unittest - -from PIL import Image, ImageDraw, ImageFont - -from .helper import PillowTestCase - -image_font_installed = True -try: - ImageFont.core.getfont -except ImportError: - image_font_installed = False - - -@unittest.skipIf(not image_font_installed, "image font not installed") -class TestImageFontBitmap(PillowTestCase): - def test_similar(self): - text = "EmbeddedBitmap" - font_outline = ImageFont.truetype(font="Tests/fonts/DejaVuSans.ttf", size=24) - font_bitmap = ImageFont.truetype( - font="Tests/fonts/DejaVuSans-bitmap.ttf", size=24 - ) - size_outline = font_outline.getsize(text) - size_bitmap = font_bitmap.getsize(text) - size_final = ( - max(size_outline[0], size_bitmap[0]), - max(size_outline[1], size_bitmap[1]), - ) - im_bitmap = Image.new("RGB", size_final, (255, 255, 255)) - im_outline = im_bitmap.copy() - draw_bitmap = ImageDraw.Draw(im_bitmap) - draw_outline = ImageDraw.Draw(im_outline) - - # Metrics are different on the bitmap and ttf fonts, - # more so on some platforms and versions of freetype than others. - # Mac has a 1px difference, linux doesn't. - draw_bitmap.text( - (0, size_final[1] - size_bitmap[1]), text, fill=(0, 0, 0), font=font_bitmap - ) - draw_outline.text( - (0, size_final[1] - size_outline[1]), - text, - fill=(0, 0, 0), - font=font_outline, - ) - self.assert_image_similar(im_bitmap, im_outline, 20) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 71efb897f..82e2b4ebc 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,209 +1,431 @@ -import unittest +import pytest +from packaging.version import parse as parse_version from PIL import Image, ImageDraw, ImageFont, features -from .helper import PillowTestCase +from .helper import ( + assert_image_similar, + skip_unless_feature, + skip_unless_feature_version, +) FONT_SIZE = 20 FONT_PATH = "Tests/fonts/DejaVuSans.ttf" +pytestmark = skip_unless_feature("raqm") -@unittest.skipUnless(features.check("raqm"), "Raqm Library is not installed.") -class TestImagecomplextext(PillowTestCase): - def test_english(self): - # smoke test, this should not fail - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr") - def test_complex_text(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) +def test_english(): + # smoke test, this should not fail + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr") - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "اهلا عمان", font=ttf, fill=500) - target = "Tests/images/test_text.png" - target_img = Image.open(target) +def test_complex_text(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - self.assert_image_similar(im, target_img, 0.5) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "اهلا عمان", font=ttf, fill=500) - def test_y_offset(self): - ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) + target = "Tests/images/test_text.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "العالم العربي", font=ttf, fill=500) - target = "Tests/images/test_y_offset.png" - target_img = Image.open(target) +def test_y_offset(): + ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) - self.assert_image_similar(im, target_img, 1.7) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "العالم العربي", font=ttf, fill=500) - def test_complex_unicode_text(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + target = "Tests/images/test_y_offset.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 1.7) - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "السلام عليكم", font=ttf, fill=500) - target = "Tests/images/test_complex_unicode_text.png" - target_img = Image.open(target) +def test_complex_unicode_text(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - self.assert_image_similar(im, target_img, 0.5) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "السلام عليكم", font=ttf, fill=500) - ttf = ImageFont.truetype("Tests/fonts/KhmerOSBattambang-Regular.ttf", FONT_SIZE) + target = "Tests/images/test_complex_unicode_text.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "លោកុប្បត្តិ", font=ttf, fill=500) + ttf = ImageFont.truetype("Tests/fonts/KhmerOSBattambang-Regular.ttf", FONT_SIZE) - target = "Tests/images/test_complex_unicode_text2.png" - target_img = Image.open(target) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "លោកុប្បត្តិ", font=ttf, fill=500) - self.assert_image_similar(im, target_img, 2.3) + target = "Tests/images/test_complex_unicode_text2.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 2.33) - def test_text_direction_rtl(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "English عربي", font=ttf, fill=500, direction="rtl") +def test_text_direction_rtl(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - target = "Tests/images/test_direction_rtl.png" - target_img = Image.open(target) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "English عربي", font=ttf, fill=500, direction="rtl") - self.assert_image_similar(im, target_img, 0.5) + target = "Tests/images/test_direction_rtl.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) - def test_text_direction_ltr(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "سلطنة عمان Oman", font=ttf, fill=500, direction="ltr") +def test_text_direction_ltr(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - target = "Tests/images/test_direction_ltr.png" - target_img = Image.open(target) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "سلطنة عمان Oman", font=ttf, fill=500, direction="ltr") - self.assert_image_similar(im, target_img, 0.5) + target = "Tests/images/test_direction_ltr.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) - def test_text_direction_rtl2(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "Oman سلطنة عمان", font=ttf, fill=500, direction="rtl") +def test_text_direction_rtl2(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - target = "Tests/images/test_direction_ltr.png" - target_img = Image.open(target) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "Oman سلطنة عمان", font=ttf, fill=500, direction="rtl") - self.assert_image_similar(im, target_img, 0.5) + target = "Tests/images/test_direction_ltr.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) - def test_text_direction_ttb(self): - ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE) - im = Image.new(mode="RGB", size=(100, 300)) - draw = ImageDraw.Draw(im) - try: - draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - self.skipTest("libraqm 0.7 or greater not available") +def test_text_direction_ttb(): + ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE) - target = "Tests/images/test_direction_ttb.png" - target_img = Image.open(target) + im = Image.new(mode="RGB", size=(100, 300)) + draw = ImageDraw.Draw(im) + try: + draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") - self.assert_image_similar(im, target_img, 1.15) + target = "Tests/images/test_direction_ttb.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 2.8) - def test_text_direction_ttb_stroke(self): - ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) - im = Image.new(mode="RGB", size=(100, 300)) - draw = ImageDraw.Draw(im) - try: - draw.text( - (25, 25), - "あい", - font=ttf, - fill=500, - direction="ttb", - stroke_width=2, - stroke_fill="#0f0", - ) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - self.skipTest("libraqm 0.7 or greater not available") +def test_text_direction_ttb_stroke(): + ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) - target = "Tests/images/test_direction_ttb_stroke.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 12.4) - - def test_ligature_features(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "filling", font=ttf, fill=500, features=["-liga"]) - target = "Tests/images/test_ligature_features.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 0.5) - - liga_size = ttf.getsize("fi", features=["-liga"]) - self.assertEqual(liga_size, (13, 19)) - - def test_kerning_features(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "TeToAV", font=ttf, fill=500, features=["-kern"]) - - target = "Tests/images/test_kerning_features.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 0.5) - - def test_arabictext_features(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) + im = Image.new(mode="RGB", size=(100, 300)) + draw = ImageDraw.Draw(im) + try: draw.text( - (0, 0), - "اللغة العربية", + (27, 27), + "あい", font=ttf, fill=500, - features=["-fina", "-init", "-medi"], + direction="ttb", + stroke_width=2, + stroke_fill="#0f0", ) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") - target = "Tests/images/test_arabictext_features.png" - target_img = Image.open(target) + target = "Tests/images/test_direction_ttb_stroke.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 19.4) - self.assert_image_similar(im, target_img, 0.5) - def test_x_max_and_y_offset(self): - ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) +def test_ligature_features(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode="RGB", size=(50, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "لح", font=ttf, fill=500) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "filling", font=ttf, fill=500, features=["-liga"]) + target = "Tests/images/test_ligature_features.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) - target = "Tests/images/test_x_max_and_y_offset.png" - target_img = Image.open(target) + liga_size = ttf.getsize("fi", features=["-liga"]) + assert liga_size == (13, 19) - self.assert_image_similar(im, target_img, 0.5) - def test_language(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) +def test_kerning_features(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "абвг", font=ttf, fill=500, language="sr") + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "TeToAV", font=ttf, fill=500, features=["-kern"]) - target = "Tests/images/test_language.png" - target_img = Image.open(target) + target = "Tests/images/test_kerning_features.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) - self.assert_image_similar(im, target_img, 0.5) + +def test_arabictext_features(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text( + (0, 0), + "اللغة العربية", + font=ttf, + fill=500, + features=["-fina", "-init", "-medi"], + ) + + target = "Tests/images/test_arabictext_features.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) + + +def test_x_max_and_y_offset(): + ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) + + im = Image.new(mode="RGB", size=(50, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "لح", font=ttf, fill=500) + + target = "Tests/images/test_x_max_and_y_offset.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) + + +def test_language(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "абвг", font=ttf, fill=500, language="sr") + + target = "Tests/images/test_language.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) + + +@pytest.mark.parametrize("mode", ("L", "1")) +@pytest.mark.parametrize( + "text, direction, expected", + ( + ("سلطنة عمان Oman", None, 173.703125), + ("سلطنة عمان Oman", "ltr", 173.703125), + ("Oman سلطنة عمان", "rtl", 173.703125), + ("English عربي", "rtl", 123.796875), + ("test", "ttb", 80.0), + ), + ids=("None", "ltr", "rtl2", "rtl", "ttb"), +) +def test_getlength(mode, text, direction, expected): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode, (1, 1), 0) + d = ImageDraw.Draw(im) + + try: + assert d.textlength(text, ttf, direction) == expected + except ValueError as ex: + if ( + direction == "ttb" + and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" + ): + pytest.skip("libraqm 0.7 or greater not available") + + +@pytest.mark.parametrize("mode", ("L", "1")) +@pytest.mark.parametrize("direction", ("ltr", "ttb")) +@pytest.mark.parametrize( + "text", + ("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): + if text == "i\u0305i" and direction == "ttb": + pytest.skip("fails with this font") + + ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + + try: + target = ttf.getlength("ii", mode, direction) + actual = ttf.getlength(text, mode, direction) + + assert actual == target + except ValueError as ex: + if ( + direction == "ttb" + and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" + ): + pytest.skip("libraqm 0.7 or greater not available") + + +# FreeType 2.5.1 README: Miscellaneous Changes: +# Improved computation of emulated vertical metrics for TrueType fonts. +@skip_unless_feature_version( + "freetype2", "2.5.1", "FreeType <2.5.1 has incompatible ttb metrics" +) +@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) +def test_anchor_ttb(anchor): + text = "f" + path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) + + im = Image.new("RGB", (200, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (200, 200)), "gray") + d.line(((100, 0), (100, 400)), "gray") + try: + d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") + + with Image.open(path) as expected: + assert_image_similar(im, expected, 1) # fails at 5 + + +combine_tests = ( + # extends above (e.g. issue #4553) + ("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08), + ("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08), + ("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08), + ("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08), + ("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3), + ("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3), + # extends below + ("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02), + ("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02), + ("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02), + ("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02), + ("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03), + ("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03), + # extends to the right (e.g. issue #3745) + ("double_breve_below", "a\u035Ci", None, None, 0.02), + ("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02), + ("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02), + ("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02), + ("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02), + ("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02), + ("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02), + # extends to the left (fail=0.064) + ("overline", "i\u0305", None, None, 0.02), + ("overline_la", "i\u0305", "la", None, 0.02), + ("overline_ra", "i\u0305", "ra", None, 0.02), + ("overline_ttb", "i\u0305", None, "ttb", 0.02), + ("overline_ttb_rt", "i\u0305", "rt", "ttb", 0.02), + ("overline_ttb_mt", "i\u0305", "mt", "ttb", 0.02), + ("overline_ttb_st", "i\u0305", "st", "ttb", 0.02), +) + + +# this tests various combining characters for anchor alignment and clipping +@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): + if ( + parse_version(features.version_module("freetype2")) < parse_version("2.5.1") + and dir == "ttb" + ): + # FreeType 2.5.1 README: Miscellaneous Changes: + # Improved computation of emulated vertical metrics for TrueType fonts. + pytest.skip("FreeType <2.5.1 has incompatible ttb metrics") + + path = f"Tests/images/test_combine_{name}.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (400, 200)), "gray") + d.line(((200, 0), (200, 400)), "gray") + try: + d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") + + with Image.open(path) as expected: + assert_image_similar(im, expected, epsilon) + + +@pytest.mark.parametrize( + "anchor, align", + ( + ("lm", "left"), # pass with getsize + ("lm", "center"), # fail at 2.12 + ("lm", "right"), # fail at 2.57 + ("mm", "left"), # fail at 2.12 + ("mm", "center"), # pass with getsize + ("mm", "right"), # fail at 2.12 + ("rm", "left"), # fail at 2.57 + ("rm", "center"), # fail at 2.12 + ("rm", "right"), # pass with getsize + ), +) +def test_combine_multiline(anchor, align): + # test that multiline text uses getlength, not getsize or getbbox + + path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + text = "i\u0305\u035C\ntext" # i with overline and double breve, and a word + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (400, 200)), "gray") + d.line(((200, 0), (200, 400)), "gray") + bbox = d.multiline_textbbox((200, 200), text, anchor=anchor, font=f, align=align) + d.rectangle(bbox, outline="red") + d.multiline_text((200, 200), text, fill="black", anchor=anchor, font=f, align=align) + + with Image.open(path) as expected: + assert_image_similar(im, expected, 0.015) + + +def test_anchor_invalid_ttb(): + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new("RGB", (100, 100), "white") + d = ImageDraw.Draw(im) + d.font = font + + for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]: + pytest.raises( + ValueError, lambda: font.getmask2("hello", anchor=anchor, direction="ttb") + ) + pytest.raises( + ValueError, lambda: font.getbbox("hello", anchor=anchor, direction="ttb") + ) + pytest.raises( + ValueError, lambda: d.text((0, 0), "hello", anchor=anchor, direction="ttb") + ) + pytest.raises( + ValueError, + lambda: d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb"), + ) + pytest.raises( + ValueError, + lambda: d.multiline_text( + (0, 0), "foo\nbar", anchor=anchor, direction="ttb" + ), + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox( + (0, 0), "foo\nbar", anchor=anchor, direction="ttb" + ), + ) + # ttb multiline text does not support anchors at all + pytest.raises( + ValueError, + lambda: d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb"), + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb"), + ) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index bea7f68b3..c36285451 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,62 +1,99 @@ +import os import subprocess import sys -from .helper import PillowTestCase +import pytest -try: - from PIL import ImageGrab +from PIL import Image, ImageGrab - class TestImageGrab(PillowTestCase): - def test_grab(self): - for im in [ - ImageGrab.grab(), - ImageGrab.grab(include_layered_windows=True), - ImageGrab.grab(all_screens=True), - ]: - self.assert_image(im, im.mode, im.size) +from .helper import assert_image, assert_image_equal_tofile, skip_unless_feature - def test_grabclipboard(self): - if sys.platform == "darwin": - subprocess.call(["screencapture", "-cx"]) - else: - p = subprocess.Popen( - ["powershell", "-command", "-"], stdin=subprocess.PIPE - ) - p.stdin.write( - b"""[Reflection.Assembly]::LoadWithPartialName("System.Drawing") + +class TestImageGrab: + @pytest.mark.skipif( + sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" + ) + def test_grab(self): + for im in [ + ImageGrab.grab(), + ImageGrab.grab(include_layered_windows=True), + ImageGrab.grab(all_screens=True), + ]: + assert_image(im, im.mode, im.size) + + im = ImageGrab.grab(bbox=(10, 20, 50, 80)) + assert_image(im, im.mode, (40, 60)) + + @skip_unless_feature("xcb") + def test_grab_x11(self): + try: + if sys.platform not in ("win32", "darwin"): + im = ImageGrab.grab() + assert_image(im, im.mode, im.size) + + im2 = ImageGrab.grab(xdisplay="") + assert_image(im2, im2.mode, im2.size) + except OSError as e: + pytest.skip(str(e)) + + @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") + def test_grab_no_xcb(self): + if sys.platform not in ("win32", "darwin"): + with pytest.raises(OSError) as e: + ImageGrab.grab() + assert str(e.value).startswith("Pillow was built without XCB support") + + with pytest.raises(OSError) as e: + ImageGrab.grab(xdisplay="") + assert str(e.value).startswith("Pillow was built without XCB support") + + @skip_unless_feature("xcb") + def test_grab_invalid_xdisplay(self): + 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): + if sys.platform == "darwin": + subprocess.call(["screencapture", "-cx"]) + elif sys.platform == "win32": + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + p.stdin.write( + b"""[Reflection.Assembly]::LoadWithPartialName("System.Drawing") [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") $bmp = New-Object Drawing.Bitmap 200, 200 [Windows.Forms.Clipboard]::SetImage($bmp)""" - ) - p.communicate() - - im = ImageGrab.grabclipboard() - self.assert_image(im, im.mode, im.size) - - -except ImportError: - - class TestImageGrab(PillowTestCase): - def test_skip(self): - self.skipTest("ImportError") - - -class TestImageGrabImport(PillowTestCase): - def test_import(self): - # Arrange - exception = None - - # Act - try: - from PIL import ImageGrab - - ImageGrab.__name__ # dummy to prevent Pyflakes warning - except Exception as e: - exception = e - - # Assert - if sys.platform in ["win32", "darwin"]: - self.assertIsNone(exception) + ) + p.communicate() else: - self.assertIsInstance(exception, ImportError) - self.assertEqual(str(exception), "ImageGrab is macOS and Windows only") + with pytest.raises(NotImplementedError) as e: + ImageGrab.grabclipboard() + assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only" + return + + im = ImageGrab.grabclipboard() + assert_image(im, im.mode, im.size) + + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grabclipboard_file(self): + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') + p.communicate() + + im = ImageGrab.grabclipboard() + assert len(im) == 1 + assert os.path.samefile(im[0], "Tests/images/hopper.gif") + + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grabclipboard_png(self): + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + p.stdin.write( + rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") +$ms = new-object System.IO.MemoryStream(, $bytes) +[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") +[Windows.Forms.Clipboard]::SetData("PNG", $ms)""" + ) + p.communicate() + + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, "Tests/images/hopper.png") diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 8d2b94226..239806796 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,11 +1,9 @@ from PIL import Image, ImageMath -from .helper import PillowTestCase - def pixel(im): if hasattr(im, "im"): - return "{} {!r}".format(im.mode, im.getpixel((0, 0))) + return "{} {}".format(im.mode, repr(im.getpixel((0, 0)))) else: if isinstance(im, int): return int(im) # hack to deal with booleans @@ -24,153 +22,168 @@ B2 = B.resize((2, 2)) images = {"A": A, "B": B, "F": F, "I": I} -class TestImageMath(PillowTestCase): - def test_sanity(self): - self.assertEqual(ImageMath.eval("1"), 1) - self.assertEqual(ImageMath.eval("1+A", A=2), 3) - self.assertEqual(pixel(ImageMath.eval("A+B", A=A, B=B)), "I 3") - self.assertEqual(pixel(ImageMath.eval("A+B", images)), "I 3") - self.assertEqual(pixel(ImageMath.eval("float(A)+B", images)), "F 3.0") - self.assertEqual(pixel(ImageMath.eval("int(float(A)+B)", images)), "I 3") +def test_sanity(): + 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" + assert pixel(ImageMath.eval("A+B", images)) == "I 3" + assert pixel(ImageMath.eval("float(A)+B", images)) == "F 3.0" + assert pixel(ImageMath.eval("int(float(A)+B)", images)) == "I 3" - def test_ops(self): - self.assertEqual(pixel(ImageMath.eval("-A", images)), "I -1") - self.assertEqual(pixel(ImageMath.eval("+B", images)), "L 2") +def test_ops(): + assert pixel(ImageMath.eval("-A", images)) == "I -1" + assert pixel(ImageMath.eval("+B", images)) == "L 2" - self.assertEqual(pixel(ImageMath.eval("A+B", images)), "I 3") - self.assertEqual(pixel(ImageMath.eval("A-B", images)), "I -1") - self.assertEqual(pixel(ImageMath.eval("A*B", images)), "I 2") - self.assertEqual(pixel(ImageMath.eval("A/B", images)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B**2", images)), "I 4") - self.assertEqual(pixel(ImageMath.eval("B**33", images)), "I 2147483647") + assert pixel(ImageMath.eval("A+B", images)) == "I 3" + assert pixel(ImageMath.eval("A-B", images)) == "I -1" + assert pixel(ImageMath.eval("A*B", images)) == "I 2" + assert pixel(ImageMath.eval("A/B", images)) == "I 0" + assert pixel(ImageMath.eval("B**2", images)) == "I 4" + assert pixel(ImageMath.eval("B**33", images)) == "I 2147483647" - self.assertEqual(pixel(ImageMath.eval("float(A)+B", images)), "F 3.0") - self.assertEqual(pixel(ImageMath.eval("float(A)-B", images)), "F -1.0") - self.assertEqual(pixel(ImageMath.eval("float(A)*B", images)), "F 2.0") - self.assertEqual(pixel(ImageMath.eval("float(A)/B", images)), "F 0.5") - self.assertEqual(pixel(ImageMath.eval("float(B)**2", images)), "F 4.0") - self.assertEqual( - pixel(ImageMath.eval("float(B)**33", images)), "F 8589934592.0" - ) + assert pixel(ImageMath.eval("float(A)+B", images)) == "F 3.0" + assert pixel(ImageMath.eval("float(A)-B", images)) == "F -1.0" + assert pixel(ImageMath.eval("float(A)*B", images)) == "F 2.0" + assert pixel(ImageMath.eval("float(A)/B", images)) == "F 0.5" + assert pixel(ImageMath.eval("float(B)**2", images)) == "F 4.0" + assert pixel(ImageMath.eval("float(B)**33", images)) == "F 8589934592.0" - def test_logical(self): - self.assertEqual(pixel(ImageMath.eval("not A", images)), 0) - self.assertEqual(pixel(ImageMath.eval("A and B", images)), "L 2") - self.assertEqual(pixel(ImageMath.eval("A or B", images)), "L 1") - def test_convert(self): - self.assertEqual(pixel(ImageMath.eval("convert(A+B, 'L')", images)), "L 3") - self.assertEqual(pixel(ImageMath.eval("convert(A+B, '1')", images)), "1 0") - self.assertEqual( - pixel(ImageMath.eval("convert(A+B, 'RGB')", images)), "RGB (3, 3, 3)" - ) +def test_logical(): + 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_compare(self): - self.assertEqual(pixel(ImageMath.eval("min(A, B)", images)), "I 1") - self.assertEqual(pixel(ImageMath.eval("max(A, B)", images)), "I 2") - self.assertEqual(pixel(ImageMath.eval("A == 1", images)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A == 2", images)), "I 0") - def test_one_image_larger(self): - self.assertEqual(pixel(ImageMath.eval("A+B", A=A2, B=B)), "I 3") - self.assertEqual(pixel(ImageMath.eval("A+B", A=A, B=B2)), "I 3") +def test_convert(): + 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_abs(self): - self.assertEqual(pixel(ImageMath.eval("abs(A)", A=A)), "I 1") - self.assertEqual(pixel(ImageMath.eval("abs(B)", B=B)), "I 2") - def test_binary_mod(self): - self.assertEqual(pixel(ImageMath.eval("A%A", A=A)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B%B", B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A%B", A=A, B=B)), "I 1") - self.assertEqual(pixel(ImageMath.eval("B%A", A=A, B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z%A", A=A, Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z%B", B=B, Z=Z)), "I 0") +def test_compare(): + 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_bitwise_invert(self): - self.assertEqual(pixel(ImageMath.eval("~Z", Z=Z)), "I -1") - self.assertEqual(pixel(ImageMath.eval("~A", A=A)), "I -2") - self.assertEqual(pixel(ImageMath.eval("~B", B=B)), "I -3") - def test_bitwise_and(self): - self.assertEqual(pixel(ImageMath.eval("Z&Z", A=A, Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z&A", A=A, Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A&Z", A=A, Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A&A", A=A, Z=Z)), "I 1") +def test_one_image_larger(): + 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_bitwise_or(self): - self.assertEqual(pixel(ImageMath.eval("Z|Z", A=A, Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z|A", A=A, Z=Z)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A|Z", A=A, Z=Z)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A|A", A=A, Z=Z)), "I 1") - def test_bitwise_xor(self): - self.assertEqual(pixel(ImageMath.eval("Z^Z", A=A, Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z^A", A=A, Z=Z)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A^Z", A=A, Z=Z)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A^A", A=A, Z=Z)), "I 0") +def test_abs(): + assert pixel(ImageMath.eval("abs(A)", A=A)) == "I 1" + assert pixel(ImageMath.eval("abs(B)", B=B)) == "I 2" - def test_bitwise_leftshift(self): - self.assertEqual(pixel(ImageMath.eval("Z<<0", Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z<<1", Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A<<0", A=A)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A<<1", A=A)), "I 2") - def test_bitwise_rightshift(self): - self.assertEqual(pixel(ImageMath.eval("Z>>0", Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z>>1", Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A>>0", A=A)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A>>1", A=A)), "I 0") +def test_binary_mod(): + 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 0" + assert pixel(ImageMath.eval("Z%A", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.eval("Z%B", B=B, Z=Z)) == "I 0" - def test_logical_eq(self): - self.assertEqual(pixel(ImageMath.eval("A==A", A=A)), "I 1") - self.assertEqual(pixel(ImageMath.eval("B==B", B=B)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A==B", A=A, B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B==A", A=A, B=B)), "I 0") - def test_logical_ne(self): - self.assertEqual(pixel(ImageMath.eval("A!=A", A=A)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B!=B", B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A!=B", A=A, B=B)), "I 1") - self.assertEqual(pixel(ImageMath.eval("B!=A", A=A, B=B)), "I 1") +def test_bitwise_invert(): + 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_logical_lt(self): - self.assertEqual(pixel(ImageMath.eval("AA", A=A)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B>B", B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A>B", A=A, B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B>A", A=A, B=B)), "I 1") - def test_logical_ge(self): - self.assertEqual(pixel(ImageMath.eval("A>=A", A=A)), "I 1") - self.assertEqual(pixel(ImageMath.eval("B>=B", B=B)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A>=B", A=A, B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B>=A", A=A, B=B)), "I 1") +def test_bitwise_or(): + 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_logical_equal(self): - self.assertEqual(pixel(ImageMath.eval("equal(A, A)", A=A)), "I 1") - self.assertEqual(pixel(ImageMath.eval("equal(B, B)", B=B)), "I 1") - self.assertEqual(pixel(ImageMath.eval("equal(Z, Z)", Z=Z)), "I 1") - self.assertEqual(pixel(ImageMath.eval("equal(A, B)", A=A, B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("equal(B, A)", A=A, B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)), "I 0") - def test_logical_not_equal(self): - self.assertEqual(pixel(ImageMath.eval("notequal(A, A)", A=A)), "I 0") - self.assertEqual(pixel(ImageMath.eval("notequal(B, B)", B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("notequal(A, B)", A=A, B=B)), "I 1") - self.assertEqual(pixel(ImageMath.eval("notequal(B, A)", A=A, B=B)), "I 1") - self.assertEqual(pixel(ImageMath.eval("notequal(A, Z)", A=A, Z=Z)), "I 1") +def test_bitwise_xor(): + 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(): + 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(): + 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(): + 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(): + 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(): + assert pixel(ImageMath.eval("AA", 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(): + 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(): + 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" + assert pixel(ImageMath.eval("equal(A, B)", A=A, B=B)) == "I 0" + assert pixel(ImageMath.eval("equal(B, A)", A=A, B=B)) == "I 0" + assert pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)) == "I 0" + + +def test_logical_not_equal(): + 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" + assert pixel(ImageMath.eval("notequal(A, B)", A=A, B=B)) == "I 1" + assert pixel(ImageMath.eval("notequal(B, A)", A=A, B=B)) == "I 1" + assert pixel(ImageMath.eval("notequal(A, Z)", A=A, Z=Z)) == "I 1" diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 1492872b6..087c39e01 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,328 +1,342 @@ # Test the ImageMorphology functionality +import pytest + from PIL import Image, ImageMorph, _imagingmorph -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper -class MorphTests(PillowTestCase): - def setUp(self): - self.A = self.string_to_img( - """ - ....... - ....... - ..111.. - ..111.. - ..111.. - ....... - ....... - """ - ) +def string_to_img(image_string): + """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) + width = len(rows[0]) + im = Image.new("L", (width, height)) + for i in range(width): + for j in range(height): + c = rows[j][i] + v = c in "X1" + im.putpixel((i, j), v) - def img_to_string(self, im): - """Turn a (small) binary image into a string representation""" - chars = ".1" - width, height = im.size - return "\n".join( - "".join(chars[im.getpixel((c, r)) > 0] for c in range(width)) - for r in range(height) - ) + return im - def string_to_img(self, image_string): - """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) - width = len(rows[0]) - im = Image.new("L", (width, height)) - for i in range(width): - for j in range(height): - c = rows[j][i] - v = c in "X1" - im.putpixel((i, j), v) - return im +A = string_to_img( + """ + ....... + ....... + ..111.. + ..111.. + ..111.. + ....... + ....... + """ +) - def img_string_normalize(self, im): - return self.img_to_string(self.string_to_img(im)) - def assert_img_equal(self, A, B): - self.assertEqual(self.img_to_string(A), self.img_to_string(B)) +def img_to_string(im): + """Turn a (small) binary image into a string representation""" + chars = ".1" + width, height = im.size + return "\n".join( + "".join(chars[im.getpixel((c, r)) > 0] for c in range(width)) + for r in range(height) + ) - def assert_img_equal_img_string(self, A, Bstring): - self.assertEqual(self.img_to_string(A), self.img_string_normalize(Bstring)) - def test_str_to_img(self): - im = Image.open("Tests/images/morph_a.png") - self.assert_image_equal(self.A, im) +def img_string_normalize(im): + return img_to_string(string_to_img(im)) - def create_lut(self): - for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): - lb = ImageMorph.LutBuilder(op_name=op) - lut = lb.build_lut() - with open("Tests/images/%s.lut" % op, "wb") as f: - f.write(lut) - # create_lut() - def test_lut(self): - for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): - lb = ImageMorph.LutBuilder(op_name=op) - self.assertIsNone(lb.get_lut()) +def assert_img_equal(A, B): + assert img_to_string(A) == img_to_string(B) - lut = lb.build_lut() - with open("Tests/images/%s.lut" % op, "rb") as f: - self.assertEqual(lut, bytearray(f.read())) - def test_no_operator_loaded(self): - mop = ImageMorph.MorphOp() - with self.assertRaises(Exception) as e: - mop.apply(None) - self.assertEqual(str(e.exception), "No operator loaded") - with self.assertRaises(Exception) as e: - mop.match(None) - self.assertEqual(str(e.exception), "No operator loaded") - with self.assertRaises(Exception) as e: - mop.save_lut(None) - self.assertEqual(str(e.exception), "No operator loaded") +def assert_img_equal_img_string(A, Bstring): + assert img_to_string(A) == img_string_normalize(Bstring) - # Test the named patterns - def test_erosion8(self): - # erosion8 - mop = ImageMorph.MorphOp(op_name="erosion8") - count, Aout = mop.apply(self.A) - self.assertEqual(count, 8) - self.assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ....... - ...1... - ....... - ....... - ....... - """, - ) - def test_dialation8(self): - # dialation8 - mop = ImageMorph.MorphOp(op_name="dilation8") - count, Aout = mop.apply(self.A) - self.assertEqual(count, 16) - self.assert_img_equal_img_string( - Aout, - """ - ....... - .11111. - .11111. - .11111. - .11111. - .11111. - ....... - """, - ) +def test_str_to_img(): + with Image.open("Tests/images/morph_a.png") as im: + assert_image_equal(A, im) - def test_erosion4(self): - # erosion4 - mop = ImageMorph.MorphOp(op_name="dilation4") - count, Aout = mop.apply(self.A) - self.assertEqual(count, 12) - self.assert_img_equal_img_string( - Aout, - """ - ....... - ..111.. - .11111. - .11111. - .11111. - ..111.. - ....... - """, - ) - def test_edge(self): - # edge - mop = ImageMorph.MorphOp(op_name="edge") - count, Aout = mop.apply(self.A) - self.assertEqual(count, 1) - self.assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ..111.. - ..1.1.. - ..111.. - ....... - ....... - """, - ) - - def test_corner(self): - # Create a corner detector pattern - mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) - count, Aout = mop.apply(self.A) - self.assertEqual(count, 5) - self.assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ..1.1.. - ....... - ..1.1.. - ....... - ....... - """, - ) - - # Test the coordinate counting with the same operator - coords = mop.match(self.A) - self.assertEqual(len(coords), 4) - self.assertEqual(tuple(coords), ((2, 2), (4, 2), (2, 4), (4, 4))) - - coords = mop.get_on_pixels(Aout) - self.assertEqual(len(coords), 4) - self.assertEqual(tuple(coords), ((2, 2), (4, 2), (2, 4), (4, 4))) - - def test_mirroring(self): - # Test 'M' for mirroring - mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"]) - count, Aout = mop.apply(self.A) - self.assertEqual(count, 7) - self.assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ..1.1.. - ....... - ....... - ....... - ....... - """, - ) - - def test_negate(self): - # Test 'N' for negate - mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"]) - count, Aout = mop.apply(self.A) - self.assertEqual(count, 8) - self.assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ..1.... - ....... - ....... - ....... - ....... - """, - ) - - def test_non_binary_images(self): - im = hopper("RGB") - mop = ImageMorph.MorphOp(op_name="erosion8") - - with self.assertRaises(Exception) as e: - mop.apply(im) - self.assertEqual( - str(e.exception), "Image must be binary, meaning it must use mode L" - ) - with self.assertRaises(Exception) as e: - mop.match(im) - self.assertEqual( - str(e.exception), "Image must be binary, meaning it must use mode L" - ) - with self.assertRaises(Exception) as e: - mop.get_on_pixels(im) - self.assertEqual( - str(e.exception), "Image must be binary, meaning it must use mode L" - ) - - def test_add_patterns(self): - # Arrange - lb = ImageMorph.LutBuilder(op_name="corner") - self.assertEqual(lb.patterns, ["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) - new_patterns = ["M:(00. 01. ...)->1", "N:(00. 01. ...)->1"] - - # Act - lb.add_patterns(new_patterns) - - # Assert - self.assertEqual( - lb.patterns, - [ - "1:(... ... ...)->0", - "4:(00. 01. ...)->1", - "M:(00. 01. ...)->1", - "N:(00. 01. ...)->1", - ], - ) - - def test_unknown_pattern(self): - self.assertRaises(Exception, ImageMorph.LutBuilder, op_name="unknown") - - def test_pattern_syntax_error(self): - # Arrange - lb = ImageMorph.LutBuilder(op_name="corner") - new_patterns = ["a pattern with a syntax error"] - lb.add_patterns(new_patterns) - - # Act / Assert - with self.assertRaises(Exception) as e: - lb.build_lut() - self.assertEqual( - str(e.exception), 'Syntax error in pattern "a pattern with a syntax error"' - ) - - def test_load_invalid_mrl(self): - # Arrange - invalid_mrl = "Tests/images/hopper.png" - mop = ImageMorph.MorphOp() - - # Act / Assert - with self.assertRaises(Exception) as e: - mop.load_lut(invalid_mrl) - self.assertEqual(str(e.exception), "Wrong size operator file!") - - def test_roundtrip_mrl(self): - # Arrange - tempfile = self.tempfile("temp.mrl") - mop = ImageMorph.MorphOp(op_name="corner") - initial_lut = mop.lut - - # Act - mop.save_lut(tempfile) - mop.load_lut(tempfile) - - # Act / Assert - self.assertEqual(mop.lut, initial_lut) - - def test_set_lut(self): - # Arrange - lb = ImageMorph.LutBuilder(op_name="corner") +def create_lut(): + for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): + lb = ImageMorph.LutBuilder(op_name=op) lut = lb.build_lut() - mop = ImageMorph.MorphOp() + with open(f"Tests/images/{op}.lut", "wb") as f: + f.write(lut) - # Act - mop.set_lut(lut) - # Assert - self.assertEqual(mop.lut, lut) +# create_lut() +def test_lut(): + for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): + lb = ImageMorph.LutBuilder(op_name=op) + assert lb.get_lut() is None - def test_wrong_mode(self): - lut = ImageMorph.LutBuilder(op_name="corner").build_lut() - imrgb = Image.new("RGB", (10, 10)) - iml = Image.new("L", (10, 10)) + lut = lb.build_lut() + with open(f"Tests/images/{op}.lut", "rb") as f: + assert lut == bytearray(f.read()) - with self.assertRaises(RuntimeError): - _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) - with self.assertRaises(RuntimeError): - _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) +def test_no_operator_loaded(): + mop = ImageMorph.MorphOp() + with pytest.raises(Exception) as e: + mop.apply(None) + assert str(e.value) == "No operator loaded" + with pytest.raises(Exception) as e: + mop.match(None) + assert str(e.value) == "No operator loaded" + with pytest.raises(Exception) as e: + mop.save_lut(None) + assert str(e.value) == "No operator loaded" - with self.assertRaises(RuntimeError): - _imagingmorph.match(bytes(lut), imrgb.im.id) - # Should not raise - _imagingmorph.match(bytes(lut), iml.im.id) +# Test the named patterns +def test_erosion8(): + # erosion8 + mop = ImageMorph.MorphOp(op_name="erosion8") + count, Aout = mop.apply(A) + assert count == 8 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ....... + ...1... + ....... + ....... + ....... + """, + ) + + +def test_dialation8(): + # dialation8 + mop = ImageMorph.MorphOp(op_name="dilation8") + count, Aout = mop.apply(A) + assert count == 16 + assert_img_equal_img_string( + Aout, + """ + ....... + .11111. + .11111. + .11111. + .11111. + .11111. + ....... + """, + ) + + +def test_erosion4(): + # erosion4 + mop = ImageMorph.MorphOp(op_name="dilation4") + count, Aout = mop.apply(A) + assert count == 12 + assert_img_equal_img_string( + Aout, + """ + ....... + ..111.. + .11111. + .11111. + .11111. + ..111.. + ....... + """, + ) + + +def test_edge(): + # edge + mop = ImageMorph.MorphOp(op_name="edge") + count, Aout = mop.apply(A) + assert count == 1 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..111.. + ..1.1.. + ..111.. + ....... + ....... + """, + ) + + +def test_corner(): + # Create a corner detector pattern + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) + count, Aout = mop.apply(A) + assert count == 5 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..1.1.. + ....... + ..1.1.. + ....... + ....... + """, + ) + + # Test the coordinate counting with the same operator + coords = mop.match(A) + assert len(coords) == 4 + assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) + + coords = mop.get_on_pixels(Aout) + assert len(coords) == 4 + assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) + + +def test_mirroring(): + # Test 'M' for mirroring + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"]) + count, Aout = mop.apply(A) + assert count == 7 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..1.1.. + ....... + ....... + ....... + ....... + """, + ) + + +def test_negate(): + # Test 'N' for negate + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"]) + count, Aout = mop.apply(A) + assert count == 8 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..1.... + ....... + ....... + ....... + ....... + """, + ) + + +def test_non_binary_images(): + im = hopper("RGB") + mop = ImageMorph.MorphOp(op_name="erosion8") + + with pytest.raises(Exception) as e: + mop.apply(im) + assert str(e.value) == "Image must be binary, meaning it must use mode L" + with pytest.raises(Exception) as e: + mop.match(im) + assert str(e.value) == "Image must be binary, meaning it must use mode L" + with pytest.raises(Exception) as e: + mop.get_on_pixels(im) + assert str(e.value) == "Image must be binary, meaning it must use mode L" + + +def test_add_patterns(): + # Arrange + lb = ImageMorph.LutBuilder(op_name="corner") + assert lb.patterns == ["1:(... ... ...)->0", "4:(00. 01. ...)->1"] + new_patterns = ["M:(00. 01. ...)->1", "N:(00. 01. ...)->1"] + + # Act + lb.add_patterns(new_patterns) + + # Assert + assert lb.patterns == [ + "1:(... ... ...)->0", + "4:(00. 01. ...)->1", + "M:(00. 01. ...)->1", + "N:(00. 01. ...)->1", + ] + + +def test_unknown_pattern(): + with pytest.raises(Exception): + ImageMorph.LutBuilder(op_name="unknown") + + +def test_pattern_syntax_error(): + # Arrange + lb = ImageMorph.LutBuilder(op_name="corner") + new_patterns = ["a pattern with a syntax error"] + lb.add_patterns(new_patterns) + + # Act / Assert + with pytest.raises(Exception) as e: + lb.build_lut() + assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' + + +def test_load_invalid_mrl(): + # Arrange + invalid_mrl = "Tests/images/hopper.png" + mop = ImageMorph.MorphOp() + + # Act / Assert + with pytest.raises(Exception) as e: + mop.load_lut(invalid_mrl) + assert str(e.value) == "Wrong size operator file!" + + +def test_roundtrip_mrl(tmp_path): + # Arrange + tempfile = str(tmp_path / "temp.mrl") + mop = ImageMorph.MorphOp(op_name="corner") + initial_lut = mop.lut + + # Act + mop.save_lut(tempfile) + mop.load_lut(tempfile) + + # Act / Assert + assert mop.lut == initial_lut + + +def test_set_lut(): + # Arrange + lb = ImageMorph.LutBuilder(op_name="corner") + lut = lb.build_lut() + mop = ImageMorph.MorphOp() + + # Act + mop.set_lut(lut) + + # Assert + assert mop.lut == lut + + +def test_wrong_mode(): + lut = ImageMorph.LutBuilder(op_name="corner").build_lut() + imrgb = Image.new("RGB", (10, 10)) + iml = Image.new("L", (10, 10)) + + with pytest.raises(RuntimeError): + _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) + + with pytest.raises(RuntimeError): + _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) + + with pytest.raises(RuntimeError): + _imagingmorph.match(bytes(lut), imrgb.im.id) + + # Should not raise + _imagingmorph.match(bytes(lut), iml.im.id) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index d0fd73689..f17bfdd2f 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,287 +1,364 @@ -from PIL import Image, ImageOps +import pytest -from .helper import PillowTestCase, hopper +from PIL import Image, ImageDraw, ImageOps, ImageStat, features -try: - from PIL import _webp - - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_tuple_approx_equal, + hopper, +) -class TestImageOps(PillowTestCase): - class Deformer: - def getmesh(self, im): - x, y = im.size - return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] +class Deformer: + def getmesh(self, im): + x, y = im.size + return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] - deformer = Deformer() - def test_sanity(self): +deformer = Deformer() - ImageOps.autocontrast(hopper("L")) - ImageOps.autocontrast(hopper("RGB")) - ImageOps.autocontrast(hopper("L"), cutoff=10) - ImageOps.autocontrast(hopper("L"), ignore=[0, 255]) +def test_sanity(): - ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) - ImageOps.colorize(hopper("L"), "black", "white") + ImageOps.autocontrast(hopper("L")) + ImageOps.autocontrast(hopper("RGB")) - ImageOps.pad(hopper("L"), (128, 128)) - ImageOps.pad(hopper("RGB"), (128, 128)) + ImageOps.autocontrast(hopper("L"), cutoff=10) + ImageOps.autocontrast(hopper("L"), cutoff=(2, 10)) + ImageOps.autocontrast(hopper("L"), ignore=[0, 255]) + ImageOps.autocontrast(hopper("L"), mask=hopper("L")) - ImageOps.crop(hopper("L"), 1) - ImageOps.crop(hopper("RGB"), 1) + ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) + ImageOps.colorize(hopper("L"), "black", "white") - ImageOps.deform(hopper("L"), self.deformer) - ImageOps.deform(hopper("RGB"), self.deformer) + ImageOps.pad(hopper("L"), (128, 128)) + ImageOps.pad(hopper("RGB"), (128, 128)) - ImageOps.equalize(hopper("L")) - ImageOps.equalize(hopper("RGB")) + ImageOps.crop(hopper("L"), 1) + ImageOps.crop(hopper("RGB"), 1) - ImageOps.expand(hopper("L"), 1) - ImageOps.expand(hopper("RGB"), 1) - ImageOps.expand(hopper("L"), 2, "blue") - ImageOps.expand(hopper("RGB"), 2, "blue") + ImageOps.deform(hopper("L"), deformer) + ImageOps.deform(hopper("RGB"), deformer) - ImageOps.fit(hopper("L"), (128, 128)) - ImageOps.fit(hopper("RGB"), (128, 128)) + ImageOps.equalize(hopper("L")) + ImageOps.equalize(hopper("RGB")) - ImageOps.flip(hopper("L")) - ImageOps.flip(hopper("RGB")) + ImageOps.expand(hopper("L"), 1) + ImageOps.expand(hopper("RGB"), 1) + ImageOps.expand(hopper("L"), 2, "blue") + ImageOps.expand(hopper("RGB"), 2, "blue") - ImageOps.grayscale(hopper("L")) - ImageOps.grayscale(hopper("RGB")) + ImageOps.fit(hopper("L"), (128, 128)) + ImageOps.fit(hopper("RGB"), (128, 128)) - ImageOps.invert(hopper("L")) - ImageOps.invert(hopper("RGB")) + ImageOps.flip(hopper("L")) + ImageOps.flip(hopper("RGB")) - ImageOps.mirror(hopper("L")) - ImageOps.mirror(hopper("RGB")) + ImageOps.grayscale(hopper("L")) + ImageOps.grayscale(hopper("RGB")) - ImageOps.posterize(hopper("L"), 4) - ImageOps.posterize(hopper("RGB"), 4) + ImageOps.invert(hopper("L")) + ImageOps.invert(hopper("RGB")) - ImageOps.solarize(hopper("L")) - ImageOps.solarize(hopper("RGB")) + ImageOps.mirror(hopper("L")) + ImageOps.mirror(hopper("RGB")) - ImageOps.exif_transpose(hopper("L")) - ImageOps.exif_transpose(hopper("RGB")) + ImageOps.posterize(hopper("L"), 4) + ImageOps.posterize(hopper("RGB"), 4) - def test_1pxfit(self): - # Division by zero in equalize if image is 1 pixel high - newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) - self.assertEqual(newimg.size, (35, 35)) + ImageOps.solarize(hopper("L")) + ImageOps.solarize(hopper("RGB")) - newimg = ImageOps.fit(hopper("RGB").resize((1, 100)), (35, 35)) - self.assertEqual(newimg.size, (35, 35)) + ImageOps.exif_transpose(hopper("L")) + ImageOps.exif_transpose(hopper("RGB")) - newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) - self.assertEqual(newimg.size, (35, 35)) - def test_fit_same_ratio(self): - # 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 - # 1.3245033112582782 * 755 = 1000.0000000000001 - # then centering this greater width causes a negative x offset when cropping - with Image.new("RGB", (1000, 755)) as im: - new_im = ImageOps.fit(im, (1000, 755)) - self.assertEqual(new_im.size, (1000, 755)) +def test_1pxfit(): + # 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) - def test_pad(self): - # Same ratio - im = hopper() - new_size = (im.width * 2, im.height * 2) - new_im = ImageOps.pad(im, new_size) - self.assertEqual(new_im.size, new_size) + newimg = ImageOps.fit(hopper("RGB").resize((1, 100)), (35, 35)) + assert newimg.size == (35, 35) - for label, color, new_size in [ - ("h", None, (im.width * 4, im.height * 2)), - ("v", "#f00", (im.width * 2, im.height * 4)), - ]: - for i, centering in enumerate([(0, 0), (0.5, 0.5), (1, 1)]): - new_im = ImageOps.pad(im, new_size, color=color, centering=centering) - self.assertEqual(new_im.size, new_size) + newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) + assert newimg.size == (35, 35) - target = Image.open( - "Tests/images/imageops_pad_" + label + "_" + str(i) + ".jpg" - ) - self.assert_image_similar(new_im, target, 6) - def test_pil163(self): - # Division by zero in equalize if < 255 pixels in image (@PIL163) +def test_fit_same_ratio(): + # 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 + # 1.3245033112582782 * 755 = 1000.0000000000001 + # then centering this greater width causes a negative x offset when cropping + with Image.new("RGB", (1000, 755)) as im: + new_im = ImageOps.fit(im, (1000, 755)) + assert new_im.size == (1000, 755) - i = hopper("RGB").resize((15, 16)) - ImageOps.equalize(i.convert("L")) - ImageOps.equalize(i.convert("P")) - ImageOps.equalize(i.convert("RGB")) +def test_pad(): + # Same ratio + im = hopper() + new_size = (im.width * 2, im.height * 2) + new_im = ImageOps.pad(im, new_size) + assert new_im.size == new_size - def test_scale(self): - # Test the scaling function - i = hopper("L").resize((50, 50)) + for label, color, new_size in [ + ("h", None, (im.width * 4, im.height * 2)), + ("v", "#f00", (im.width * 2, im.height * 4)), + ]: + for i, centering in enumerate([(0, 0), (0.5, 0.5), (1, 1)]): + new_im = ImageOps.pad(im, new_size, color=color, centering=centering) + assert new_im.size == new_size - with self.assertRaises(ValueError): - ImageOps.scale(i, -1) + with Image.open( + "Tests/images/imageops_pad_" + label + "_" + str(i) + ".jpg" + ) as target: + assert_image_similar(new_im, target, 6) - newimg = ImageOps.scale(i, 1) - self.assertEqual(newimg.size, (50, 50)) - newimg = ImageOps.scale(i, 2) - self.assertEqual(newimg.size, (100, 100)) +def test_pil163(): + # Division by zero in equalize if < 255 pixels in image (@PIL163) - newimg = ImageOps.scale(i, 0.5) - self.assertEqual(newimg.size, (25, 25)) + i = hopper("RGB").resize((15, 16)) - def test_colorize_2color(self): - # Test the colorizing function with 2-color functionality + ImageOps.equalize(i.convert("L")) + ImageOps.equalize(i.convert("P")) + ImageOps.equalize(i.convert("RGB")) - # Open test image (256px by 10px, black to white) - im = Image.open("Tests/images/bw_gradient.png") + +def test_scale(): + # Test the scaling function + i = hopper("L").resize((50, 50)) + + with pytest.raises(ValueError): + ImageOps.scale(i, -1) + + newimg = ImageOps.scale(i, 1) + assert newimg.size == (50, 50) + + newimg = ImageOps.scale(i, 2) + assert newimg.size == (100, 100) + + newimg = ImageOps.scale(i, 0.5) + assert newimg.size == (25, 25) + + +def test_colorize_2color(): + # Test the colorizing function with 2-color functionality + + # Open test image (256px by 10px, black to white) + with Image.open("Tests/images/bw_gradient.png") as im: im = im.convert("L") - # Create image with original 2-color functionality - im_test = ImageOps.colorize(im, "red", "green") + # Create image with original 2-color functionality + im_test = ImageOps.colorize(im, "red", "green") - # Test output image (2-color) - left = (0, 1) - middle = (127, 1) - right = (255, 1) - self.assert_tuple_approx_equal( - im_test.getpixel(left), - (255, 0, 0), - threshold=1, - msg="black test pixel incorrect", - ) - self.assert_tuple_approx_equal( - im_test.getpixel(middle), - (127, 63, 0), - threshold=1, - msg="mid test pixel incorrect", - ) - self.assert_tuple_approx_equal( - im_test.getpixel(right), - (0, 127, 0), - threshold=1, - msg="white test pixel incorrect", - ) + # Test output image (2-color) + left = (0, 1) + middle = (127, 1) + right = (255, 1) + assert_tuple_approx_equal( + im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg="black test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg="mid test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg="white test pixel incorrect", + ) - def test_colorize_2color_offset(self): - # Test the colorizing function with 2-color functionality and offset - # Open test image (256px by 10px, black to white) - im = Image.open("Tests/images/bw_gradient.png") +def test_colorize_2color_offset(): + # Test the colorizing function with 2-color functionality and offset + + # Open test image (256px by 10px, black to white) + with Image.open("Tests/images/bw_gradient.png") as im: im = im.convert("L") - # Create image with original 2-color functionality with offsets - im_test = ImageOps.colorize( - im, black="red", white="green", blackpoint=50, whitepoint=100 - ) + # Create image with original 2-color functionality with offsets + im_test = ImageOps.colorize( + im, black="red", white="green", blackpoint=50, whitepoint=100 + ) - # Test output image (2-color) with offsets - left = (25, 1) - middle = (75, 1) - right = (125, 1) - self.assert_tuple_approx_equal( - im_test.getpixel(left), - (255, 0, 0), - threshold=1, - msg="black test pixel incorrect", - ) - self.assert_tuple_approx_equal( - im_test.getpixel(middle), - (127, 63, 0), - threshold=1, - msg="mid test pixel incorrect", - ) - self.assert_tuple_approx_equal( - im_test.getpixel(right), - (0, 127, 0), - threshold=1, - msg="white test pixel incorrect", - ) + # Test output image (2-color) with offsets + left = (25, 1) + middle = (75, 1) + right = (125, 1) + assert_tuple_approx_equal( + im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg="black test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg="mid test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg="white test pixel incorrect", + ) - def test_colorize_3color_offset(self): - # Test the colorizing function with 3-color functionality and offset - # Open test image (256px by 10px, black to white) - im = Image.open("Tests/images/bw_gradient.png") +def test_colorize_3color_offset(): + # Test the colorizing function with 3-color functionality and offset + + # Open test image (256px by 10px, black to white) + with Image.open("Tests/images/bw_gradient.png") as im: im = im.convert("L") - # Create image with new three color functionality with offsets - im_test = ImageOps.colorize( - im, - black="red", - white="green", - mid="blue", - blackpoint=50, - whitepoint=200, - midpoint=100, - ) + # Create image with new three color functionality with offsets + im_test = ImageOps.colorize( + im, + black="red", + white="green", + mid="blue", + blackpoint=50, + whitepoint=200, + midpoint=100, + ) - # Test output image (3-color) with offsets - left = (25, 1) - left_middle = (75, 1) - middle = (100, 1) - right_middle = (150, 1) - right = (225, 1) - self.assert_tuple_approx_equal( - im_test.getpixel(left), - (255, 0, 0), - threshold=1, - msg="black test pixel incorrect", - ) - self.assert_tuple_approx_equal( - im_test.getpixel(left_middle), - (127, 0, 127), - threshold=1, - msg="low-mid test pixel incorrect", - ) - self.assert_tuple_approx_equal( - im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect" - ) - self.assert_tuple_approx_equal( - im_test.getpixel(right_middle), - (0, 63, 127), - threshold=1, - msg="high-mid test pixel incorrect", - ) - self.assert_tuple_approx_equal( - im_test.getpixel(right), - (0, 127, 0), - threshold=1, - msg="white test pixel incorrect", - ) + # Test output image (3-color) with offsets + left = (25, 1) + left_middle = (75, 1) + middle = (100, 1) + right_middle = (150, 1) + right = (225, 1) + assert_tuple_approx_equal( + im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg="black test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(left_middle), + (127, 0, 127), + threshold=1, + msg="low-mid test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect" + ) + assert_tuple_approx_equal( + im_test.getpixel(right_middle), + (0, 63, 127), + threshold=1, + msg="high-mid test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg="white test pixel incorrect", + ) - def test_exif_transpose(self): - exts = [".jpg"] - if HAVE_WEBP and _webp.HAVE_WEBPANIM: - exts.append(".webp") - for ext in exts: - base_im = Image.open("Tests/images/hopper" + ext) - orientations = [base_im] - for i in range(2, 9): - im = Image.open("Tests/images/hopper_orientation_" + str(i) + ext) - orientations.append(im) - for i, orientation_im in enumerate(orientations): - for im in [orientation_im, orientation_im.copy()]: # ImageFile # Image - if i == 0: - self.assertNotIn("exif", im.info) +def test_exif_transpose(): + 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): + for im in [ + orientation_im, + orientation_im.copy(), + ]: # ImageFile # Image + if orientation_im is base_im: + assert "exif" not in im.info else: original_exif = im.info["exif"] transposed_im = ImageOps.exif_transpose(im) - self.assert_image_similar(base_im, transposed_im, 17) - if i == 0: - self.assertNotIn("exif", im.info) + assert_image_similar(base_im, transposed_im, 17) + if orientation_im is base_im: + assert "exif" not in im.info else: - self.assertNotEqual(transposed_im.info["exif"], original_exif) + assert transposed_im.info["exif"] != original_exif - self.assertNotIn(0x0112, transposed_im.getexif()) + assert 0x0112 not in transposed_im.getexif() - # Repeat the operation, to test that it does not keep transposing + # Repeat the operation to test that it does not keep transposing transposed_im2 = ImageOps.exif_transpose(transposed_im) - self.assert_image_equal(transposed_im2, transposed_im) + assert_image_equal(transposed_im2, transposed_im) + + check(base_im) + for i in range(2, 9): + with Image.open( + "Tests/images/hopper_orientation_" + str(i) + ext + ) as orientation_im: + check(orientation_im) + + +def test_autocontrast_cutoff(): + # Test the cutoff argument of autocontrast + with Image.open("Tests/images/bw_gradient.png") as img: + + def autocontrast(cutoff): + return ImageOps.autocontrast(img, cutoff).histogram() + + assert autocontrast(10) == autocontrast((10, 10)) + assert autocontrast(10) != autocontrast((1, 10)) + + +def test_autocontrast_mask_toy_input(): + # Test the mask argument of autocontrast + with Image.open("Tests/images/bw_gradient.png") as img: + + rect_mask = Image.new("L", img.size, 0) + draw = ImageDraw.Draw(rect_mask) + x0 = img.size[0] // 4 + y0 = img.size[1] // 4 + x1 = 3 * img.size[0] // 4 + y1 = 3 * img.size[1] // 4 + draw.rectangle((x0, y0, x1, y1), fill=255) + + result = ImageOps.autocontrast(img, mask=rect_mask) + result_nomask = ImageOps.autocontrast(img) + + assert result != result_nomask + assert ImageStat.Stat(result, mask=rect_mask).median == [127] + assert ImageStat.Stat(result_nomask).median == [128] + + +def test_auto_contrast_mask_real_input(): + # Test the autocontrast with a rectangular mask + with Image.open("Tests/images/iptc.jpg") as img: + + rect_mask = Image.new("L", img.size, 0) + draw = ImageDraw.Draw(rect_mask) + x0, y0 = img.size[0] // 2, img.size[1] // 2 + x1, y1 = img.size[0] - 40, img.size[1] + draw.rectangle((x0, y0, x1, y1), fill=255) + + result = ImageOps.autocontrast(img, mask=rect_mask) + result_nomask = ImageOps.autocontrast(img) + + assert result_nomask != result + assert_tuple_approx_equal( + ImageStat.Stat(result, mask=rect_mask).median, + [195, 202, 184], + threshold=2, + msg="autocontrast with mask pixel incorrect", + ) + assert_tuple_approx_equal( + ImageStat.Stat(result_nomask).median, + [119, 106, 79], + threshold=2, + msg="autocontrast without mask pixel incorrect", + ) diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 71e858681..8837ed2a2 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,89 +1,111 @@ +import pytest + from PIL import Image, ImageFilter -from .helper import PillowTestCase + +@pytest.fixture +def test_images(): + ims = { + "im": Image.open("Tests/images/hopper.ppm"), + "snakes": Image.open("Tests/images/color_snakes.png"), + } + try: + yield ims + finally: + for im in ims.values(): + im.close() -class TestImageOpsUsm(PillowTestCase): - def setUp(self): - super().setUp() - self.im = Image.open("Tests/images/hopper.ppm") - self.addCleanup(self.im.close) - self.snakes = Image.open("Tests/images/color_snakes.png") - self.addCleanup(self.snakes.close) +def test_filter_api(test_images): + im = test_images["im"] - def test_filter_api(self): + test_filter = ImageFilter.GaussianBlur(2.0) + i = im.filter(test_filter) + assert i.mode == "RGB" + assert i.size == (128, 128) - test_filter = ImageFilter.GaussianBlur(2.0) - i = self.im.filter(test_filter) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) + test_filter = ImageFilter.UnsharpMask(2.0, 125, 8) + i = im.filter(test_filter) + assert i.mode == "RGB" + assert i.size == (128, 128) - test_filter = ImageFilter.UnsharpMask(2.0, 125, 8) - i = self.im.filter(test_filter) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - def test_usm_formats(self): +def test_usm_formats(test_images): + im = test_images["im"] - usm = ImageFilter.UnsharpMask - self.assertRaises(ValueError, self.im.convert("1").filter, usm) - self.im.convert("L").filter(usm) - self.assertRaises(ValueError, self.im.convert("I").filter, usm) - self.assertRaises(ValueError, self.im.convert("F").filter, usm) - self.im.convert("RGB").filter(usm) - self.im.convert("RGBA").filter(usm) - self.im.convert("CMYK").filter(usm) - self.assertRaises(ValueError, self.im.convert("YCbCr").filter, usm) + usm = ImageFilter.UnsharpMask + with pytest.raises(ValueError): + im.convert("1").filter(usm) + im.convert("L").filter(usm) + with pytest.raises(ValueError): + im.convert("I").filter(usm) + with pytest.raises(ValueError): + im.convert("F").filter(usm) + im.convert("RGB").filter(usm) + im.convert("RGBA").filter(usm) + im.convert("CMYK").filter(usm) + with pytest.raises(ValueError): + im.convert("YCbCr").filter(usm) - def test_blur_formats(self): - blur = ImageFilter.GaussianBlur - self.assertRaises(ValueError, self.im.convert("1").filter, blur) - blur(self.im.convert("L")) - self.assertRaises(ValueError, self.im.convert("I").filter, blur) - self.assertRaises(ValueError, self.im.convert("F").filter, blur) - self.im.convert("RGB").filter(blur) - self.im.convert("RGBA").filter(blur) - self.im.convert("CMYK").filter(blur) - self.assertRaises(ValueError, self.im.convert("YCbCr").filter, blur) +def test_blur_formats(test_images): + im = test_images["im"] - def test_usm_accuracy(self): + blur = ImageFilter.GaussianBlur + with pytest.raises(ValueError): + im.convert("1").filter(blur) + blur(im.convert("L")) + with pytest.raises(ValueError): + im.convert("I").filter(blur) + with pytest.raises(ValueError): + im.convert("F").filter(blur) + im.convert("RGB").filter(blur) + im.convert("RGBA").filter(blur) + im.convert("CMYK").filter(blur) + with pytest.raises(ValueError): + im.convert("YCbCr").filter(blur) - src = self.snakes.convert("RGB") - i = src.filter(ImageFilter.UnsharpMask(5, 1024, 0)) - # Image should not be changed because it have only 0 and 255 levels. - self.assertEqual(i.tobytes(), src.tobytes()) - def test_blur_accuracy(self): +def test_usm_accuracy(test_images): + snakes = test_images["snakes"] - i = self.snakes.filter(ImageFilter.GaussianBlur(0.4)) - # These pixels surrounded with pixels with 255 intensity. - # They must be very close to 255. - for x, y, c in [ - (1, 0, 1), - (2, 0, 1), - (7, 8, 1), - (8, 8, 1), - (2, 9, 1), - (7, 3, 0), - (8, 3, 0), - (5, 8, 0), - (5, 9, 0), - (1, 3, 0), - (4, 3, 2), - (4, 2, 2), - ]: - self.assertGreaterEqual(i.im.getpixel((x, y))[c], 250) - # Fuzzy match. + src = snakes.convert("RGB") + i = src.filter(ImageFilter.UnsharpMask(5, 1024, 0)) + # Image should not be changed because it have only 0 and 255 levels. + assert i.tobytes() == src.tobytes() - def gp(x, y): - return i.im.getpixel((x, y)) - self.assertTrue(236 <= gp(7, 4)[0] <= 239) - self.assertTrue(236 <= gp(7, 5)[2] <= 239) - self.assertTrue(236 <= gp(7, 6)[2] <= 239) - self.assertTrue(236 <= gp(7, 7)[1] <= 239) - self.assertTrue(236 <= gp(8, 4)[0] <= 239) - self.assertTrue(236 <= gp(8, 5)[2] <= 239) - self.assertTrue(236 <= gp(8, 6)[2] <= 239) - self.assertTrue(236 <= gp(8, 7)[1] <= 239) +def test_blur_accuracy(test_images): + snakes = test_images["snakes"] + + i = snakes.filter(ImageFilter.GaussianBlur(0.4)) + # These pixels surrounded with pixels with 255 intensity. + # They must be very close to 255. + for x, y, c in [ + (1, 0, 1), + (2, 0, 1), + (7, 8, 1), + (8, 8, 1), + (2, 9, 1), + (7, 3, 0), + (8, 3, 0), + (5, 8, 0), + (5, 9, 0), + (1, 3, 0), + (4, 3, 2), + (4, 2, 2), + ]: + assert i.im.getpixel((x, y))[c] >= 250 + # Fuzzy match. + + def gp(x, y): + return i.im.getpixel((x, y)) + + assert 236 <= gp(7, 4)[0] <= 239 + assert 236 <= gp(7, 5)[2] <= 239 + assert 236 <= gp(7, 6)[2] <= 239 + assert 236 <= gp(7, 7)[1] <= 239 + assert 236 <= gp(8, 4)[0] <= 239 + assert 236 <= gp(8, 5)[2] <= 239 + assert 236 <= gp(8, 6)[2] <= 239 + assert 236 <= gp(8, 7)[1] <= 239 diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 1297712ef..a2b0d2b02 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,136 +1,150 @@ +import pytest + from PIL import Image, ImagePalette -from .helper import PillowTestCase +from .helper import assert_image_equal -class TestImagePalette(PillowTestCase): - def test_sanity(self): +def test_sanity(): - ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - self.assertRaises( - ValueError, ImagePalette.ImagePalette, "RGB", list(range(256)) * 2 - ) + ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + with pytest.raises(ValueError): + ImagePalette.ImagePalette("RGB", list(range(256)) * 2) - def test_getcolor(self): - palette = ImagePalette.ImagePalette() +def test_getcolor(): - test_map = {} - for i in range(256): - test_map[palette.getcolor((i, i, i))] = i + palette = ImagePalette.ImagePalette() - self.assertEqual(len(test_map), 256) - self.assertRaises(ValueError, palette.getcolor, (1, 2, 3)) + test_map = {} + for i in range(256): + test_map[palette.getcolor((i, i, i))] = i - # Test unknown color specifier - self.assertRaises(ValueError, palette.getcolor, "unknown") + assert len(test_map) == 256 + with pytest.raises(ValueError): + palette.getcolor((1, 2, 3)) - def test_file(self): + # Test unknown color specifier + with pytest.raises(ValueError): + palette.getcolor("unknown") - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - f = self.tempfile("temp.lut") +def test_file(tmp_path): + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + + f = str(tmp_path / "temp.lut") + + palette.save(f) + + p = ImagePalette.load(f) + + # load returns raw palette information + assert len(p[0]) == 768 + assert p[1] == "RGB" + + p = ImagePalette.raw(p[1], p[0]) + assert isinstance(p, ImagePalette.ImagePalette) + assert p.palette == palette.tobytes() + + +def test_make_linear_lut(): + # Arrange + black = 0 + white = 255 + + # Act + lut = ImagePalette.make_linear_lut(black, white) + + # Assert + assert isinstance(lut, list) + assert len(lut) == 256 + # Check values + for i in range(0, len(lut)): + assert lut[i] == i + + +def test_make_linear_lut_not_yet_implemented(): + # Update after FIXME + # Arrange + black = 1 + white = 255 + + # Act + with pytest.raises(NotImplementedError): + ImagePalette.make_linear_lut(black, white) + + +def test_make_gamma_lut(): + # Arrange + exp = 5 + + # Act + lut = ImagePalette.make_gamma_lut(exp) + + # Assert + assert isinstance(lut, list) + assert len(lut) == 256 + # Check a few values + assert lut[0] == 0 + assert lut[63] == 0 + assert lut[127] == 8 + assert lut[191] == 60 + assert lut[255] == 255 + + +def test_rawmode_valueerrors(tmp_path): + # Arrange + palette = ImagePalette.raw("RGB", list(range(256)) * 3) + + # Act / Assert + with pytest.raises(ValueError): + palette.tobytes() + with pytest.raises(ValueError): + palette.getcolor((1, 2, 3)) + f = str(tmp_path / "temp.lut") + with pytest.raises(ValueError): palette.save(f) - p = ImagePalette.load(f) - # load returns raw palette information - self.assertEqual(len(p[0]), 768) - self.assertEqual(p[1], "RGB") +def test_getdata(): + # Arrange + data_in = list(range(256)) * 3 + palette = ImagePalette.ImagePalette("RGB", data_in) - p = ImagePalette.raw(p[1], p[0]) - self.assertIsInstance(p, ImagePalette.ImagePalette) - self.assertEqual(p.palette, palette.tobytes()) + # Act + mode, data_out = palette.getdata() - def test_make_linear_lut(self): - # Arrange - black = 0 - white = 255 + # Assert + assert mode == "RGB;L" - # Act - lut = ImagePalette.make_linear_lut(black, white) - # Assert - self.assertIsInstance(lut, list) - self.assertEqual(len(lut), 256) - # Check values - for i in range(0, len(lut)): - self.assertEqual(lut[i], i) +def test_rawmode_getdata(): + # Arrange + data_in = list(range(256)) * 3 + palette = ImagePalette.raw("RGB", data_in) - def test_make_linear_lut_not_yet_implemented(self): - # Update after FIXME - # Arrange - black = 1 - white = 255 + # Act + rawmode, data_out = palette.getdata() - # Act - self.assertRaises( - NotImplementedError, ImagePalette.make_linear_lut, black, white - ) + # Assert + assert rawmode == "RGB" + assert data_in == data_out - def test_make_gamma_lut(self): - # Arrange - exp = 5 - # Act - lut = ImagePalette.make_gamma_lut(exp) +def test_2bit_palette(tmp_path): + # issue #2258, 2 bit palettes are corrupted. + outfile = str(tmp_path / "temp.png") - # Assert - self.assertIsInstance(lut, list) - self.assertEqual(len(lut), 256) - # Check a few values - self.assertEqual(lut[0], 0) - self.assertEqual(lut[63], 0) - self.assertEqual(lut[127], 8) - self.assertEqual(lut[191], 60) - self.assertEqual(lut[255], 255) + rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2 + img = Image.frombytes("P", (6, 1), rgb) + img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB + img.save(outfile, format="PNG") - def test_rawmode_valueerrors(self): - # Arrange - palette = ImagePalette.raw("RGB", list(range(256)) * 3) + with Image.open(outfile) as reloaded: + assert_image_equal(img, reloaded) - # Act / Assert - self.assertRaises(ValueError, palette.tobytes) - self.assertRaises(ValueError, palette.getcolor, (1, 2, 3)) - f = self.tempfile("temp.lut") - self.assertRaises(ValueError, palette.save, f) - def test_getdata(self): - # Arrange - data_in = list(range(256)) * 3 - palette = ImagePalette.ImagePalette("RGB", data_in) - - # Act - mode, data_out = palette.getdata() - - # Assert - self.assertEqual(mode, "RGB;L") - - def test_rawmode_getdata(self): - # Arrange - data_in = list(range(256)) * 3 - palette = ImagePalette.raw("RGB", data_in) - - # Act - rawmode, data_out = palette.getdata() - - # Assert - self.assertEqual(rawmode, "RGB") - self.assertEqual(data_in, data_out) - - def test_2bit_palette(self): - # issue #2258, 2 bit palettes are corrupted. - outfile = self.tempfile("temp.png") - - rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2 - img = Image.frombytes("P", (6, 1), rgb) - img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB - img.save(outfile, format="PNG") - - reloaded = Image.open(outfile) - - self.assert_image_equal(img, reloaded) - - def test_invalid_palette(self): - self.assertRaises(IOError, ImagePalette.load, "Tests/images/hopper.jpg") +def test_invalid_palette(): + with pytest.raises(OSError): + ImagePalette.load("Tests/images/hopper.jpg") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 35c78cd3b..7cc89ae39 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,73 +1,73 @@ import array import struct +import pytest + from PIL import Image, ImagePath -from .helper import PillowTestCase - -class TestImagePath(PillowTestCase): +class TestImagePath: def test_path(self): p = ImagePath.Path(list(range(10))) # sequence interface - self.assertEqual(len(p), 5) - self.assertEqual(p[0], (0.0, 1.0)) - self.assertEqual(p[-1], (8.0, 9.0)) - self.assertEqual(list(p[:1]), [(0.0, 1.0)]) - with self.assertRaises(TypeError) as cm: + assert len(p) == 5 + assert p[0] == (0.0, 1.0) + assert p[-1] == (8.0, 9.0) + assert list(p[:1]) == [(0.0, 1.0)] + with pytest.raises(TypeError) as cm: p["foo"] - self.assertEqual(str(cm.exception), "Path indices must be integers, not str") - self.assertEqual( - list(p), [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)] - ) + assert str(cm.value) == "Path indices must be integers, not str" + assert list(p) == [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)] # method sanity check - self.assertEqual( - p.tolist(), [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)] - ) - self.assertEqual( - p.tolist(1), [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] - ) + assert p.tolist() == [ + (0.0, 1.0), + (2.0, 3.0), + (4.0, 5.0), + (6.0, 7.0), + (8.0, 9.0), + ] + assert p.tolist(1) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] - self.assertEqual(p.getbbox(), (0.0, 1.0, 8.0, 9.0)) + assert p.getbbox() == (0.0, 1.0, 8.0, 9.0) - self.assertEqual(p.compact(5), 2) - self.assertEqual(list(p), [(0.0, 1.0), (4.0, 5.0), (8.0, 9.0)]) + assert p.compact(5) == 2 + assert list(p) == [(0.0, 1.0), (4.0, 5.0), (8.0, 9.0)] p.transform((1, 0, 1, 0, 1, 1)) - self.assertEqual(list(p), [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)]) + assert list(p) == [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)] # alternative constructors p = ImagePath.Path([0, 1]) - self.assertEqual(list(p), [(0.0, 1.0)]) + assert list(p) == [(0.0, 1.0)] p = ImagePath.Path([0.0, 1.0]) - self.assertEqual(list(p), [(0.0, 1.0)]) + assert list(p) == [(0.0, 1.0)] p = ImagePath.Path([0, 1]) - self.assertEqual(list(p), [(0.0, 1.0)]) + assert list(p) == [(0.0, 1.0)] p = ImagePath.Path([(0, 1)]) - self.assertEqual(list(p), [(0.0, 1.0)]) + assert list(p) == [(0.0, 1.0)] p = ImagePath.Path(p) - self.assertEqual(list(p), [(0.0, 1.0)]) + assert list(p) == [(0.0, 1.0)] p = ImagePath.Path(p.tolist(0)) - self.assertEqual(list(p), [(0.0, 1.0)]) + assert list(p) == [(0.0, 1.0)] p = ImagePath.Path(p.tolist(1)) - self.assertEqual(list(p), [(0.0, 1.0)]) + assert list(p) == [(0.0, 1.0)] p = ImagePath.Path(array.array("f", [0, 1])) - self.assertEqual(list(p), [(0.0, 1.0)]) + assert list(p) == [(0.0, 1.0)] arr = array.array("f", [0, 1]) if hasattr(arr, "tobytes"): p = ImagePath.Path(arr.tobytes()) else: p = ImagePath.Path(arr.tostring()) - self.assertEqual(list(p), [(0.0, 1.0)]) + assert list(p) == [(0.0, 1.0)] def test_overflow_segfault(self): # Some Pythons fail getting the argument as an integer, and it falls # through to the sequence. Seeing this on 32-bit Windows. - with self.assertRaises((TypeError, MemoryError)): + with pytest.raises((TypeError, MemoryError)): # post patch, this fails with a memory error x = evil() diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index f69a21d2a..c39bb0a06 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,71 +1,61 @@ +import pytest + from PIL import ImageQt -from .helper import PillowTestCase, hopper +from .helper import hopper if ImageQt.qt_is_installed: from PIL.ImageQt import qRgba - def skip_if_qt_is_not_installed(_): - pass - -else: - - def skip_if_qt_is_not_installed(test_case): - test_case.skipTest("Qt bindings are not installed") - - -class PillowQtTestCase: - def setUp(self): - skip_if_qt_is_not_installed(self) - - def tearDown(self): - pass - - -class PillowQPixmapTestCase(PillowQtTestCase): - def setUp(self): - super().setUp() +@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") +class PillowQPixmapTestCase: + @classmethod + def setup_class(self): try: if ImageQt.qt_version == "5": from PyQt5.QtGui import QGuiApplication elif ImageQt.qt_version == "side2": from PySide2.QtGui import QGuiApplication except ImportError: - self.skipTest("QGuiApplication not installed") + pytest.skip("QGuiApplication not installed") + return self.app = QGuiApplication([]) - def tearDown(self): - super().tearDown() + @classmethod + def teardown_class(self): self.app.quit() + self.app = None -class TestImageQt(PillowQtTestCase, PillowTestCase): - def test_rgb(self): - # from https://doc.qt.io/archives/qt-4.8/qcolor.html - # typedef QRgb - # An ARGB quadruplet on the format #AARRGGBB, - # equivalent to an unsigned int. - if ImageQt.qt_version == "5": - from PyQt5.QtGui import qRgb - elif ImageQt.qt_version == "side2": - from PySide2.QtGui import qRgb +@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") +def test_rgb(): + # from https://doc.qt.io/archives/qt-4.8/qcolor.html + # typedef QRgb + # An ARGB quadruplet on the format #AARRGGBB, + # equivalent to an unsigned int. + if ImageQt.qt_version == "5": + from PyQt5.QtGui import qRgb + elif ImageQt.qt_version == "side2": + from PySide2.QtGui import qRgb - self.assertEqual(qRgb(0, 0, 0), qRgba(0, 0, 0, 255)) + assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) - def checkrgb(r, g, b): - val = ImageQt.rgb(r, g, b) - val = val % 2 ** 24 # drop the alpha - self.assertEqual(val >> 16, r) - self.assertEqual(((val >> 8) % 2 ** 8), g) - self.assertEqual(val % 2 ** 8, b) + def checkrgb(r, g, b): + val = ImageQt.rgb(r, g, b) + val = val % 2 ** 24 # drop the alpha + assert val >> 16 == r + assert ((val >> 8) % 2 ** 8) == g + assert val % 2 ** 8 == b - checkrgb(0, 0, 0) - checkrgb(255, 0, 0) - checkrgb(0, 255, 0) - checkrgb(0, 0, 255) + checkrgb(0, 0, 0) + checkrgb(255, 0, 0) + checkrgb(0, 255, 0) + checkrgb(0, 0, 255) - def test_image(self): - for mode in ("1", "RGB", "RGBA", "L", "P"): - ImageQt.ImageQt(hopper(mode)) + +@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") +def test_image(): + for mode in ("1", "RGB", "RGBA", "L", "P"): + ImageQt.ImageQt(hopper(mode)) diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 17f8100b7..7cf237b46 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,98 +1,106 @@ +import pytest + from PIL import Image, ImageSequence, TiffImagePlugin -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper, skip_unless_feature -class TestImageSequence(PillowTestCase): - def test_sanity(self): +def test_sanity(tmp_path): - test_file = self.tempfile("temp.im") + test_file = str(tmp_path / "temp.im") - im = hopper("RGB") - im.save(test_file) + im = hopper("RGB") + im.save(test_file) - seq = ImageSequence.Iterator(im) + seq = ImageSequence.Iterator(im) - index = 0 - for frame in seq: - self.assert_image_equal(im, frame) - self.assertEqual(im.tell(), index) - index += 1 + index = 0 + for frame in seq: + assert_image_equal(im, frame) + assert im.tell() == index + index += 1 - self.assertEqual(index, 1) + assert index == 1 - self.assertRaises(AttributeError, ImageSequence.Iterator, 0) + with pytest.raises(AttributeError): + ImageSequence.Iterator(0) - def test_iterator(self): - with Image.open("Tests/images/multipage.tiff") as im: - i = ImageSequence.Iterator(im) - for index in range(0, im.n_frames): - self.assertEqual(i[index], next(i)) - self.assertRaises(IndexError, lambda: i[index + 1]) - self.assertRaises(StopIteration, next, i) - def test_iterator_min_frame(self): - with Image.open("Tests/images/hopper.psd") as im: - i = ImageSequence.Iterator(im) - for index in range(1, im.n_frames): - self.assertEqual(i[index], next(i)) +def test_iterator(): + with Image.open("Tests/images/multipage.tiff") as im: + i = ImageSequence.Iterator(im) + for index in range(0, im.n_frames): + assert i[index] == next(i) + with pytest.raises(IndexError): + i[index + 1] + with pytest.raises(StopIteration): + next(i) - def _test_multipage_tiff(self): - with Image.open("Tests/images/multipage.tiff") as im: - for index, frame in enumerate(ImageSequence.Iterator(im)): - frame.load() - self.assertEqual(index, im.tell()) - frame.convert("RGB") - def test_tiff(self): - self._test_multipage_tiff() +def test_iterator_min_frame(): + 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_libtiff(self): - codecs = dir(Image.core) - if "libtiff_encoder" not in codecs or "libtiff_decoder" not in codecs: - self.skipTest("tiff support not available") +def _test_multipage_tiff(): + with Image.open("Tests/images/multipage.tiff") as im: + for index, frame in enumerate(ImageSequence.Iterator(im)): + frame.load() + assert index == im.tell() + frame.convert("RGB") - TiffImagePlugin.READ_LIBTIFF = True - self._test_multipage_tiff() - TiffImagePlugin.READ_LIBTIFF = False - def test_consecutive(self): - with Image.open("Tests/images/multipage.tiff") as im: - firstFrame = None - for frame in ImageSequence.Iterator(im): - if firstFrame is None: - firstFrame = frame.copy() - for frame in ImageSequence.Iterator(im): - self.assert_image_equal(frame, firstFrame) - break +def test_tiff(): + _test_multipage_tiff() - def test_palette_mmap(self): - # Using mmap in ImageFile can require to reload the palette. - with Image.open("Tests/images/multipage-mmap.tiff") as im: - color1 = im.getpalette()[0:3] - im.seek(0) - color2 = im.getpalette()[0:3] - self.assertEqual(color1, color2) - def test_all_frames(self): - # Test a single image - with Image.open("Tests/images/iss634.gif") as im: - ims = ImageSequence.all_frames(im) +@skip_unless_feature("libtiff") +def test_libtiff(): + TiffImagePlugin.READ_LIBTIFF = True + _test_multipage_tiff() + TiffImagePlugin.READ_LIBTIFF = False - self.assertEqual(len(ims), 42) - for i, im_frame in enumerate(ims): - self.assertFalse(im_frame is im) - im.seek(i) - self.assert_image_equal(im, im_frame) +def test_consecutive(): + with Image.open("Tests/images/multipage.tiff") as im: + firstFrame = None + for frame in ImageSequence.Iterator(im): + if firstFrame is None: + firstFrame = frame.copy() + for frame in ImageSequence.Iterator(im): + assert_image_equal(frame, firstFrame) + break - # Test a series of images - ims = ImageSequence.all_frames([im, hopper(), im]) - self.assertEqual(len(ims), 85) - # Test an operation - ims = ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90)) - for i, im_frame in enumerate(ims): - im.seek(i) - self.assert_image_equal(im.rotate(90), im_frame) +def test_palette_mmap(): + # Using mmap in ImageFile can require to reload the palette. + with Image.open("Tests/images/multipage-mmap.tiff") as im: + color1 = im.getpalette()[0:3] + im.seek(0) + color2 = im.getpalette()[0:3] + assert color1 == color2 + + +def test_all_frames(): + # Test a single image + with Image.open("Tests/images/iss634.gif") as im: + ims = ImageSequence.all_frames(im) + + assert len(ims) == 42 + for i, im_frame in enumerate(ims): + assert im_frame is not im + + im.seek(i) + assert_image_equal(im, im_frame) + + # Test a series of images + ims = ImageSequence.all_frames([im, hopper(), im]) + assert len(ims) == 85 + + # Test an operation + ims = ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90)) + for i, im_frame in enumerate(ims): + im.seek(i) + assert_image_equal(im.rotate(90), im_frame) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 858714e57..78e80f521 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,57 +1,65 @@ -import unittest +import pytest from PIL import Image, ImageShow -from .helper import PillowTestCase, hopper, is_win32, on_ci, on_github_actions +from .helper import hopper, is_win32, on_ci -class TestImageShow(PillowTestCase): - def test_sanity(self): - dir(Image) - dir(ImageShow) +def test_sanity(): + dir(Image) + dir(ImageShow) - def test_register(self): - # Test registering a viewer that is not a class - ImageShow.register("not a class") - # Restore original state - ImageShow._viewers.pop() +def test_register(): + # Test registering a viewer that is not a class + ImageShow.register("not a class") - def test_viewer_show(self): - class TestViewer(ImageShow.Viewer): - methodCalled = False + # Restore original state + ImageShow._viewers.pop() - def show_image(self, image, **options): - self.methodCalled = True - return True - viewer = TestViewer() - ImageShow.register(viewer, -1) +@pytest.mark.parametrize( + "order", + [-1, 0], +) +def test_viewer_show(order): + class TestViewer(ImageShow.Viewer): + def show_image(self, image, **options): + self.methodCalled = True + return True - for mode in ("1", "I;16", "LA", "RGB", "RGBA"): - with hopper() as im: - self.assertTrue(ImageShow.show(im)) - self.assertTrue(viewer.methodCalled) + viewer = TestViewer() + ImageShow.register(viewer, order) - # Restore original state - ImageShow._viewers.pop(0) + for mode in ("1", "I;16", "LA", "RGB", "RGBA"): + viewer.methodCalled = False + with hopper(mode) as im: + assert ImageShow.show(im) + assert viewer.methodCalled - @unittest.skipUnless( - on_ci() and not (is_win32() and on_github_actions()), - "Only run on CIs; hangs on Windows on GitHub Actions", - ) - def test_show(self): - for mode in ("1", "I;16", "LA", "RGB", "RGBA"): - im = hopper(mode) - self.assertTrue(ImageShow.show(im)) + # Restore original state + ImageShow._viewers.pop(0) - def test_viewer(self): - viewer = ImageShow.Viewer() - self.assertIsNone(viewer.get_format(None)) +@pytest.mark.skipif( + not on_ci() or is_win32(), + reason="Only run on CIs; hangs on Windows CIs", +) +def test_show(): + for mode in ("1", "I;16", "LA", "RGB", "RGBA"): + im = hopper(mode) + assert ImageShow.show(im) - self.assertRaises(NotImplementedError, viewer.get_command, None) - def test_viewers(self): - for viewer in ImageShow._viewers: - viewer.get_command("test.jpg") +def test_viewer(): + viewer = ImageShow.Viewer() + + assert viewer.get_format(None) is None + + with pytest.raises(NotImplementedError): + viewer.get_command(None) + + +def test_viewers(): + for viewer in ImageShow._viewers: + viewer.get_command("test.jpg") diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index d6c6a7a55..9474ff6f9 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,55 +1,60 @@ +import pytest + from PIL import Image, ImageStat -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestImageStat(PillowTestCase): - def test_sanity(self): +def test_sanity(): - im = hopper() + im = hopper() - st = ImageStat.Stat(im) - st = ImageStat.Stat(im.histogram()) - st = ImageStat.Stat(im, Image.new("1", im.size, 1)) + st = ImageStat.Stat(im) + st = ImageStat.Stat(im.histogram()) + st = ImageStat.Stat(im, Image.new("1", im.size, 1)) - # Check these run. Exceptions will cause failures. - st.extrema - st.sum - st.mean - st.median - st.rms - st.sum2 - st.var - st.stddev + # Check these run. Exceptions will cause failures. + st.extrema + st.sum + st.mean + st.median + st.rms + st.sum2 + st.var + st.stddev - self.assertRaises(AttributeError, lambda: st.spam) + with pytest.raises(AttributeError): + st.spam() - self.assertRaises(TypeError, ImageStat.Stat, 1) + with pytest.raises(TypeError): + ImageStat.Stat(1) - def test_hopper(self): - im = hopper() +def test_hopper(): - st = ImageStat.Stat(im) + im = hopper() - # verify a few values - self.assertEqual(st.extrema[0], (0, 255)) - self.assertEqual(st.median[0], 72) - self.assertEqual(st.sum[0], 1470218) - self.assertEqual(st.sum[1], 1311896) - self.assertEqual(st.sum[2], 1563008) + st = ImageStat.Stat(im) - def test_constant(self): + # verify a few values + assert st.extrema[0] == (0, 255) + assert st.median[0] == 72 + assert st.sum[0] == 1470218 + assert st.sum[1] == 1311896 + assert st.sum[2] == 1563008 - im = Image.new("L", (128, 128), 128) - st = ImageStat.Stat(im) +def test_constant(): - self.assertEqual(st.extrema[0], (128, 128)) - self.assertEqual(st.sum[0], 128 ** 3) - self.assertEqual(st.sum2[0], 128 ** 4) - self.assertEqual(st.mean[0], 128) - self.assertEqual(st.median[0], 128) - self.assertEqual(st.rms[0], 128) - self.assertEqual(st.var[0], 0) - self.assertEqual(st.stddev[0], 0) + im = Image.new("L", (128, 128), 128) + + st = ImageStat.Stat(im) + + assert st.extrema[0] == (128, 128) + assert st.sum[0] == 128 ** 3 + assert st.sum2[0] == 128 ** 4 + assert st.mean[0] == 128 + assert st.median[0] == 128 + assert st.rms[0] == 128 + assert st.var[0] == 0 + assert st.stddev[0] == 0 diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index 048efd639..928b8cbd1 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,87 +1,92 @@ -import unittest +import pytest from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper try: - from PIL import ImageTk - import tkinter as tk + from PIL import ImageTk + dir(ImageTk) HAS_TK = True except (OSError, ImportError): - # Skipped via setUp() + # Skipped via pytestmark HAS_TK = False TK_MODES = ("1", "L", "P", "RGB", "RGBA") -@unittest.skipIf(not HAS_TK, "Tk not installed") -class TestImageTk(PillowTestCase): - def setUp(self): - try: - # setup tk - tk.Frame() - # root = tk.Tk() - except tk.TclError as v: - self.skipTest("TCL Error: %s" % v) +pytestmark = pytest.mark.skipif(not HAS_TK, reason="Tk not installed") - def test_kw(self): - TEST_JPG = "Tests/images/hopper.jpg" - TEST_PNG = "Tests/images/hopper.png" - im1 = Image.open(TEST_JPG) - im2 = Image.open(TEST_PNG) - with open(TEST_PNG, "rb") as fp: - data = fp.read() - kw = {"file": TEST_JPG, "data": data} - # Test "file" - im = ImageTk._get_image_from_kw(kw) - self.assert_image_equal(im, im1) +def setup_module(): + try: + # setup tk + tk.Frame() + # root = tk.Tk() + except tk.TclError as v: + pytest.skip(f"TCL Error: {v}") - # Test "data" - im = ImageTk._get_image_from_kw(kw) - self.assert_image_equal(im, im2) - # Test no relevant entry - im = ImageTk._get_image_from_kw(kw) - self.assertIsNone(im) +def test_kw(): + TEST_JPG = "Tests/images/hopper.jpg" + TEST_PNG = "Tests/images/hopper.png" + with Image.open(TEST_JPG) as im1: + with Image.open(TEST_PNG) as im2: + with open(TEST_PNG, "rb") as fp: + data = fp.read() + kw = {"file": TEST_JPG, "data": data} - def test_photoimage(self): - for mode in TK_MODES: - # test as image: - im = hopper(mode) + # Test "file" + im = ImageTk._get_image_from_kw(kw) + assert_image_equal(im, im1) - # this should not crash - im_tk = ImageTk.PhotoImage(im) + # Test "data" + im = ImageTk._get_image_from_kw(kw) + assert_image_equal(im, im2) - self.assertEqual(im_tk.width(), im.width) - self.assertEqual(im_tk.height(), im.height) + # Test no relevant entry + im = ImageTk._get_image_from_kw(kw) + assert im is None - reloaded = ImageTk.getimage(im_tk) - self.assert_image_equal(reloaded, im.convert("RGBA")) - def test_photoimage_blank(self): - # test a image using mode/size: - for mode in TK_MODES: - im_tk = ImageTk.PhotoImage(mode, (100, 100)) - - self.assertEqual(im_tk.width(), 100) - self.assertEqual(im_tk.height(), 100) - - # reloaded = ImageTk.getimage(im_tk) - # self.assert_image_equal(reloaded, im) - - def test_bitmapimage(self): - im = hopper("1") +def test_photoimage(): + for mode in TK_MODES: + # test as image: + im = hopper(mode) # this should not crash - im_tk = ImageTk.BitmapImage(im) + im_tk = ImageTk.PhotoImage(im) - self.assertEqual(im_tk.width(), im.width) - self.assertEqual(im_tk.height(), im.height) + assert im_tk.width() == im.width + assert im_tk.height() == im.height + + reloaded = ImageTk.getimage(im_tk) + assert_image_equal(reloaded, im.convert("RGBA")) + + +def test_photoimage_blank(): + # test a image using mode/size: + for mode in TK_MODES: + im_tk = ImageTk.PhotoImage(mode, (100, 100)) + + assert im_tk.width() == 100 + assert im_tk.height() == 100 # reloaded = ImageTk.getimage(im_tk) - # self.assert_image_equal(reloaded, im) + # assert_image_equal(reloaded, im) + + +def test_bitmapimage(): + im = hopper("1") + + # this should not crash + im_tk = ImageTk.BitmapImage(im) + + assert im_tk.width() == im.width + assert im_tk.height() == im.height + + # reloaded = ImageTk.getimage(im_tk) + # assert_image_equal(reloaded, im) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 1cd8c674e..9d64d17a3 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,11 +1,11 @@ -import unittest +import pytest from PIL import ImageWin -from .helper import PillowTestCase, hopper, is_win32 +from .helper import hopper, is_win32 -class TestImageWin(PillowTestCase): +class TestImageWin: def test_sanity(self): dir(ImageWin) @@ -18,7 +18,7 @@ class TestImageWin(PillowTestCase): dc2 = int(hdc) # Assert - self.assertEqual(dc2, 50) + assert dc2 == 50 def test_hwnd(self): # Arrange @@ -29,11 +29,11 @@ class TestImageWin(PillowTestCase): wnd2 = int(hwnd) # Assert - self.assertEqual(wnd2, 50) + assert wnd2 == 50 -@unittest.skipUnless(is_win32(), "Windows only") -class TestImageWinDib(PillowTestCase): +@pytest.mark.skipif(not is_win32(), reason="Windows only") +class TestImageWinDib: def test_dib_image(self): # Arrange im = hopper() @@ -42,7 +42,7 @@ class TestImageWinDib(PillowTestCase): dib = ImageWin.Dib(im) # Assert - self.assertEqual(dib.size, im.size) + assert dib.size == im.size def test_dib_mode_string(self): # Arrange @@ -53,7 +53,7 @@ class TestImageWinDib(PillowTestCase): dib = ImageWin.Dib(mode, size) # Assert - self.assertEqual(dib.size, (128, 128)) + assert dib.size == (128, 128) def test_dib_paste(self): # Arrange @@ -67,7 +67,7 @@ class TestImageWinDib(PillowTestCase): dib.paste(im) # Assert - self.assertEqual(dib.size, (128, 128)) + assert dib.size == (128, 128) def test_dib_paste_bbox(self): # Arrange @@ -82,7 +82,7 @@ class TestImageWinDib(PillowTestCase): dib.paste(im, bbox) # Assert - self.assertEqual(dib.size, (128, 128)) + assert dib.size == (128, 128) def test_dib_frombytes_tobytes_roundtrip(self): # Arrange @@ -95,7 +95,7 @@ class TestImageWinDib(PillowTestCase): dib2 = ImageWin.Dib(mode, size) # Confirm they're different - self.assertNotEqual(dib1.tobytes(), dib2.tobytes()) + assert dib1.tobytes() != dib2.tobytes() # Act # Make one the same as the using tobytes()/frombytes() @@ -104,4 +104,4 @@ class TestImageWinDib(PillowTestCase): # Assert # Confirm they're the same - self.assertEqual(dib1.tobytes(), dib2.tobytes()) + assert dib1.tobytes() == dib2.tobytes() diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index d7c22a209..a5cac96e4 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -3,7 +3,7 @@ from io import BytesIO from PIL import Image, ImageWin -from .helper import PillowTestCase, hopper, is_win32 +from .helper import hopper, is_win32 # see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652 @@ -81,34 +81,33 @@ if is_win32(): memcpy(bp + bf.bfOffBits, pixels, bi.biSizeImage) return bytearray(buf) - class TestImageWinPointers(PillowTestCase): - def test_pointer(self): - im = hopper() - (width, height) = im.size - opath = self.tempfile("temp.png") - imdib = ImageWin.Dib(im) + def test_pointer(tmp_path): + im = hopper() + (width, height) = im.size + opath = str(tmp_path / "temp.png") + imdib = ImageWin.Dib(im) - hdr = BITMAPINFOHEADER() - hdr.biSize = ctypes.sizeof(hdr) - hdr.biWidth = width - hdr.biHeight = height - hdr.biPlanes = 1 - hdr.biBitCount = 32 - hdr.biCompression = BI_RGB - hdr.biSizeImage = width * height * 4 - hdr.biClrUsed = 0 - hdr.biClrImportant = 0 + hdr = BITMAPINFOHEADER() + hdr.biSize = ctypes.sizeof(hdr) + hdr.biWidth = width + hdr.biHeight = height + hdr.biPlanes = 1 + hdr.biBitCount = 32 + hdr.biCompression = BI_RGB + hdr.biSizeImage = width * height * 4 + hdr.biClrUsed = 0 + hdr.biClrImportant = 0 - hdc = CreateCompatibleDC(None) - pixels = ctypes.c_void_p() - dib = CreateDIBSection( - hdc, ctypes.byref(hdr), DIB_RGB_COLORS, ctypes.byref(pixels), None, 0 - ) - SelectObject(hdc, dib) + hdc = CreateCompatibleDC(None) + pixels = ctypes.c_void_p() + dib = CreateDIBSection( + hdc, ctypes.byref(hdr), DIB_RGB_COLORS, ctypes.byref(pixels), None, 0 + ) + SelectObject(hdc, dib) - imdib.expose(hdc) - bitmap = serialize_dib(hdr, pixels) - DeleteObject(dib) - DeleteDC(hdc) + imdib.expose(hdc) + bitmap = serialize_dib(hdr, pixels) + DeleteObject(dib) + DeleteDC(hdc) - Image.open(BytesIO(bitmap)).save(opath) + Image.open(BytesIO(bitmap)).save(opath) diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 240783960..37ed3659d 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,38 +1,33 @@ -import unittest +import pytest from PIL import Image -from .helper import PillowTestCase +def test_setmode(): -class TestLibImage(PillowTestCase): - def test_setmode(self): + im = Image.new("L", (1, 1), 255) + im.im.setmode("1") + assert im.im.getpixel((0, 0)) == 255 + im.im.setmode("L") + assert im.im.getpixel((0, 0)) == 255 - im = Image.new("L", (1, 1), 255) - im.im.setmode("1") - self.assertEqual(im.im.getpixel((0, 0)), 255) + im = Image.new("1", (1, 1), 1) + im.im.setmode("L") + assert im.im.getpixel((0, 0)) == 255 + im.im.setmode("1") + assert im.im.getpixel((0, 0)) == 255 + + im = Image.new("RGB", (1, 1), (1, 2, 3)) + im.im.setmode("RGB") + assert im.im.getpixel((0, 0)) == (1, 2, 3) + im.im.setmode("RGBA") + assert im.im.getpixel((0, 0)) == (1, 2, 3, 255) + im.im.setmode("RGBX") + assert im.im.getpixel((0, 0)) == (1, 2, 3, 255) + im.im.setmode("RGB") + assert im.im.getpixel((0, 0)) == (1, 2, 3) + + with pytest.raises(ValueError): im.im.setmode("L") - self.assertEqual(im.im.getpixel((0, 0)), 255) - - im = Image.new("1", (1, 1), 1) - im.im.setmode("L") - self.assertEqual(im.im.getpixel((0, 0)), 255) - im.im.setmode("1") - self.assertEqual(im.im.getpixel((0, 0)), 255) - - im = Image.new("RGB", (1, 1), (1, 2, 3)) - im.im.setmode("RGB") - self.assertEqual(im.im.getpixel((0, 0)), (1, 2, 3)) - im.im.setmode("RGBA") - self.assertEqual(im.im.getpixel((0, 0)), (1, 2, 3, 255)) - im.im.setmode("RGBX") - self.assertEqual(im.im.getpixel((0, 0)), (1, 2, 3, 255)) - im.im.setmode("RGB") - self.assertEqual(im.im.getpixel((0, 0)), (1, 2, 3)) - - self.assertRaises(ValueError, im.im.setmode, "L") - self.assertRaises(ValueError, im.im.setmode, "RGBABCDE") - - -if __name__ == "__main__": - unittest.main() + with pytest.raises(ValueError): + im.im.setmode("RGBABCDE") diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 45958d2eb..8a1460346 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,13 +1,13 @@ import sys -from PIL import Image +import pytest -from .helper import PillowTestCase +from PIL import Image X = 255 -class TestLibPack(PillowTestCase): +class TestLibPack: def assert_pack(self, mode, rawmode, data, *pixels): """ data - either raw bytes with data or just number of bytes in rawmode. @@ -20,7 +20,7 @@ class TestLibPack(PillowTestCase): data_len = data * len(pixels) data = bytes(range(1, data_len + 1)) - self.assertEqual(data, im.tobytes("raw", rawmode)) + assert data == im.tobytes("raw", rawmode) def test_1(self): self.assert_pack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) @@ -44,6 +44,9 @@ class TestLibPack(PillowTestCase): 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): + self.assert_pack("La", "La", 2, (1, 2), (3, 4), (5, 6)) + def test_P(self): 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) @@ -219,7 +222,7 @@ class TestLibPack(PillowTestCase): ) -class TestLibUnpack(PillowTestCase): +class TestLibUnpack: def assert_unpack(self, mode, rawmode, data, *pixels): """ data - either raw bytes with data or just number of bytes in rawmode. @@ -231,7 +234,7 @@ class TestLibUnpack(PillowTestCase): im = Image.frombytes(mode, (len(pixels), 1), data, "raw", rawmode, 0, 1) for x, pixel in enumerate(pixels): - self.assertEqual(pixel, im.getpixel((x, 0))) + assert pixel == im.getpixel((x, 0)) def test_1(self): self.assert_unpack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) @@ -269,6 +272,9 @@ class TestLibUnpack(PillowTestCase): 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): + self.assert_unpack("La", "La", 2, (1, 2), (3, 4), (5, 6)) + def test_P(self): 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) @@ -371,7 +377,7 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack( "RGBA", "RGBa;16L", - b"\x88\x01\x88\x02\x88\x03\x88\x00" b"\x88\x10\x88\x20\x88\x30\x88\xff", + b"\x88\x01\x88\x02\x88\x03\x88\x00\x88\x10\x88\x20\x88\x30\x88\xff", (0, 0, 0, 0), (16, 32, 48, 255), ) @@ -386,7 +392,7 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack( "RGBA", "RGBa;16B", - b"\x01\x88\x02\x88\x03\x88\x00\x88" b"\x10\x88\x20\x88\x30\x88\xff\x88", + b"\x01\x88\x02\x88\x03\x88\x00\x88\x10\x88\x20\x88\x30\x88\xff\x88", (0, 0, 0, 0), (16, 32, 48, 255), ) @@ -713,6 +719,9 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack("CMYK", "CMYK;16N", 8, (1, 3, 5, 7), (9, 11, 13, 15)) def test_value_error(self): - self.assertRaises(ValueError, self.assert_unpack, "L", "L", 0, 0) - self.assertRaises(ValueError, self.assert_unpack, "RGB", "RGB", 2, 0) - self.assertRaises(ValueError, self.assert_unpack, "CMYK", "CMYK", 2, 0) + with pytest.raises(ValueError): + self.assert_unpack("L", "L", 0, 0) + with pytest.raises(ValueError): + self.assert_unpack("RGB", "RGB", 2, 0) + with pytest.raises(ValueError): + self.assert_unpack("CMYK", "CMYK", 2, 0) diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 0b1f330ac..7a07fbbe0 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,10 +1,9 @@ import locale -import unittest + +import pytest from PIL import Image -from .helper import PillowTestCase - # ref https://github.com/python-pillow/Pillow/issues/272 # on windows, in polish locale: @@ -23,17 +22,16 @@ from .helper import PillowTestCase path = "Tests/images/hopper.jpg" -class TestLocale(PillowTestCase): - def test_sanity(self): +def test_sanity(): + with Image.open(path): + pass + try: + locale.setlocale(locale.LC_ALL, "polish") + except locale.Error: + pytest.skip("Polish locale not available") + + try: with Image.open(path): pass - try: - locale.setlocale(locale.LC_ALL, "polish") - except locale.Error: - unittest.skip("Polish locale not available") - - try: - with Image.open(path): - pass - finally: - locale.setlocale(locale.LC_ALL, (None, None)) + finally: + locale.setlocale(locale.LC_ALL, (None, None)) diff --git a/Tests/test_main.py b/Tests/test_main.py index 4f52149b6..46ff63c4e 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -1,41 +1,32 @@ import os import subprocess import sys -import unittest -from unittest import TestCase - -from .helper import is_pypy, is_win32, on_github_actions -class TestMain(TestCase): - @unittest.skipIf( - is_win32() and is_pypy() and on_github_actions(), - "Failing on Windows on GitHub Actions running PyPy", +def test_main(): + out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8") + lines = out.splitlines() + assert lines[0] == "-" * 68 + assert lines[1].startswith("Pillow ") + assert lines[2].startswith("Python ") + lines = lines[3:] + while lines[0].startswith(" "): + lines = lines[1:] + assert lines[0] == "-" * 68 + assert lines[1].startswith("Python modules loaded from ") + assert lines[2].startswith("Binary modules loaded from ") + assert lines[3] == "-" * 68 + jpeg = ( + os.linesep + + "-" * 68 + + os.linesep + + "JPEG image/jpeg" + + os.linesep + + "Extensions: .jfif, .jpe, .jpeg, .jpg" + + os.linesep + + "Features: open, save" + + os.linesep + + "-" * 68 + + os.linesep ) - def test_main(self): - out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8") - lines = out.splitlines() - self.assertEqual(lines[0], "-" * 68) - self.assertTrue(lines[1].startswith("Pillow ")) - self.assertTrue(lines[2].startswith("Python ")) - lines = lines[3:] - while lines[0].startswith(" "): - lines = lines[1:] - self.assertEqual(lines[0], "-" * 68) - self.assertTrue(lines[1].startswith("Python modules loaded from ")) - self.assertTrue(lines[2].startswith("Binary modules loaded from ")) - self.assertEqual(lines[3], "-" * 68) - jpeg = ( - os.linesep - + "-" * 68 - + os.linesep - + "JPEG image/jpeg" - + os.linesep - + "Extensions: .jfif, .jpe, .jpeg, .jpg" - + os.linesep - + "Features: open, save" - + os.linesep - + "-" * 68 - + os.linesep - ) - self.assertIn(jpeg, out) + assert jpeg in out diff --git a/Tests/test_map.py b/Tests/test_map.py index bdc3a7e2c..2b65fb3f9 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,38 +1,36 @@ import sys -import unittest + +import pytest from PIL import Image -from .helper import PillowTestCase, is_win32 +from .helper import is_win32 -try: - import numpy -except ImportError: - numpy = None +pytestmark = pytest.mark.skipif(is_win32(), reason="Win32 does not call map_buffer") -@unittest.skipIf(is_win32(), "Win32 does not call map_buffer") -class TestMap(PillowTestCase): - def test_overflow(self): - # 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 - # (offset + size(bytes)) > SIZE_MAX +def test_overflow(): + # 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 + # (offset + size(bytes)) > SIZE_MAX - # Note that this image triggers the decompression bomb warning: - max_pixels = Image.MAX_IMAGE_PIXELS - Image.MAX_IMAGE_PIXELS = None + # Note that this image triggers the decompression bomb warning: + max_pixels = Image.MAX_IMAGE_PIXELS + Image.MAX_IMAGE_PIXELS = None - # This image hits the offset test. - with Image.open("Tests/images/l2rgb_read.bmp") as im: - with self.assertRaises((ValueError, MemoryError, IOError)): - im.load() + # This image hits the offset test. + with Image.open("Tests/images/l2rgb_read.bmp") as im: + with pytest.raises((ValueError, MemoryError, OSError)): + im.load() - Image.MAX_IMAGE_PIXELS = max_pixels + Image.MAX_IMAGE_PIXELS = max_pixels - @unittest.skipIf(sys.maxsize <= 2 ** 32, "requires 64-bit system") - @unittest.skipIf(numpy is None, "Numpy is not installed") - def test_ysize(self): - # Should not raise 'Integer overflow in ysize' - arr = numpy.zeros((46341, 46341), dtype=numpy.uint8) - Image.fromarray(arr) + +@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") +def test_ysize(): + numpy = pytest.importorskip("numpy", reason="NumPy not installed") + + # Should not raise 'Integer overflow in ysize' + arr = numpy.zeros((46341, 46341), dtype=numpy.uint8) + Image.fromarray(arr) diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index fcd45b3c1..0571aabf4 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,111 +1,106 @@ from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import hopper + +original = hopper().resize((32, 32)).convert("I") -class TestModeI16(PillowTestCase): +def verify(im1): + im2 = original.copy() + assert im1.size == im2.size + pix1 = im1.load() + pix2 = im2.load() + for y in range(im1.size[1]): + for x in range(im1.size[0]): + xy = x, y + p1 = pix1[xy] + p2 = pix2[xy] + assert ( + p1 == p2 + ), f"got {repr(p1)} from mode {im1.mode} at {xy}, expected {repr(p2)}" - original = hopper().resize((32, 32)).convert("I") - def verify(self, im1): - im2 = self.original.copy() - self.assertEqual(im1.size, im2.size) - pix1 = im1.load() - pix2 = im2.load() - for y in range(im1.size[1]): - for x in range(im1.size[0]): - xy = x, y - p1 = pix1[xy] - p2 = pix2[xy] - self.assertEqual( - p1, - p2, - ( - "got {!r} from mode {} at {}, expected {!r}".format( - p1, im1.mode, xy, p2 - ) - ), - ) +def test_basic(tmp_path): + # PIL 1.1 has limited support for 16-bit image data. Check that + # create/copy/transform and save works as expected. - def test_basic(self): - # PIL 1.1 has limited support for 16-bit image data. Check that - # create/copy/transform and save works as expected. + def basic(mode): - def basic(mode): + imIn = original.convert(mode) + verify(imIn) - imIn = self.original.convert(mode) - self.verify(imIn) + w, h = imIn.size - w, h = imIn.size + imOut = imIn.copy() + verify(imOut) # copy - imOut = imIn.copy() - self.verify(imOut) # copy + imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) + verify(imOut) # transform - imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) - self.verify(imOut) # transform + filename = str(tmp_path / "temp.im") + imIn.save(filename) - filename = self.tempfile("temp.im") - imIn.save(filename) + with Image.open(filename) as imOut: - with Image.open(filename) as imOut: + verify(imIn) + verify(imOut) - self.verify(imIn) - self.verify(imOut) + imOut = imIn.crop((0, 0, w, h)) + verify(imOut) - imOut = imIn.crop((0, 0, w, h)) - self.verify(imOut) + imOut = Image.new(mode, (w, h), None) + imOut.paste(imIn.crop((0, 0, w // 2, h)), (0, 0)) + imOut.paste(imIn.crop((w // 2, 0, w, h)), (w // 2, 0)) - imOut = Image.new(mode, (w, h), None) - imOut.paste(imIn.crop((0, 0, w // 2, h)), (0, 0)) - imOut.paste(imIn.crop((w // 2, 0, w, h)), (w // 2, 0)) + verify(imIn) + verify(imOut) - self.verify(imIn) - self.verify(imOut) + imIn = Image.new(mode, (1, 1), 1) + assert imIn.getpixel((0, 0)) == 1 - imIn = Image.new(mode, (1, 1), 1) - self.assertEqual(imIn.getpixel((0, 0)), 1) + imIn.putpixel((0, 0), 2) + assert imIn.getpixel((0, 0)) == 2 - imIn.putpixel((0, 0), 2) - self.assertEqual(imIn.getpixel((0, 0)), 2) + if mode == "L": + maximum = 255 + else: + maximum = 32767 - if mode == "L": - maximum = 255 - else: - maximum = 32767 + imIn = Image.new(mode, (1, 1), 256) + assert imIn.getpixel((0, 0)) == min(256, maximum) - imIn = Image.new(mode, (1, 1), 256) - self.assertEqual(imIn.getpixel((0, 0)), min(256, maximum)) + imIn.putpixel((0, 0), 512) + assert imIn.getpixel((0, 0)) == min(512, maximum) - imIn.putpixel((0, 0), 512) - self.assertEqual(imIn.getpixel((0, 0)), min(512, maximum)) + basic("L") - basic("L") + basic("I;16") + basic("I;16B") + basic("I;16L") - basic("I;16") - basic("I;16B") - basic("I;16L") + basic("I") - basic("I") - def test_tobytes(self): - def tobytes(mode): - return Image.new(mode, (1, 1), 1).tobytes() +def test_tobytes(): + def tobytes(mode): + return Image.new(mode, (1, 1), 1).tobytes() - order = 1 if Image._ENDIAN == "<" else -1 + order = 1 if Image._ENDIAN == "<" else -1 - self.assertEqual(tobytes("L"), b"\x01") - self.assertEqual(tobytes("I;16"), b"\x01\x00") - self.assertEqual(tobytes("I;16B"), b"\x00\x01") - self.assertEqual(tobytes("I"), b"\x01\x00\x00\x00"[::order]) + assert tobytes("L") == b"\x01" + assert tobytes("I;16") == b"\x01\x00" + assert tobytes("I;16B") == b"\x00\x01" + assert tobytes("I") == b"\x01\x00\x00\x00"[::order] - def test_convert(self): - im = self.original.copy() +def test_convert(): - self.verify(im.convert("I;16")) - self.verify(im.convert("I;16").convert("L")) - self.verify(im.convert("I;16").convert("I")) + im = original.copy() - self.verify(im.convert("I;16B")) - self.verify(im.convert("I;16B").convert("L")) - self.verify(im.convert("I;16B").convert("I")) + verify(im.convert("I;16")) + verify(im.convert("I;16").convert("L")) + verify(im.convert("I;16").convert("I")) + + verify(im.convert("I;16B")) + verify(im.convert("I;16B").convert("L")) + verify(im.convert("I;16B").convert("I")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 7de5c3bbd..da367fa46 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,223 +1,237 @@ -import unittest +import pytest from PIL import Image -from .helper import PillowTestCase, hopper - -try: - import numpy -except ImportError: - numpy = None +from .helper import assert_deep_equal, assert_image, hopper +numpy = pytest.importorskip("numpy", reason="NumPy not installed") TEST_IMAGE_SIZE = (10, 10) -@unittest.skipIf(numpy is None, "Numpy is not installed") -class TestNumpy(PillowTestCase): - def test_numpy_to_image(self): - def to_image(dtype, bands=1, boolean=0): - if bands == 1: - if boolean: - data = [0, 255] * 50 - else: - data = list(range(100)) - a = numpy.array(data, dtype=dtype) - a.shape = TEST_IMAGE_SIZE - i = Image.fromarray(a) - if list(i.getdata()) != data: - print("data mismatch for", dtype) +def test_numpy_to_image(): + def to_image(dtype, bands=1, boolean=0): + if bands == 1: + if boolean: + data = [0, 255] * 50 else: data = list(range(100)) - a = numpy.array([[x] * bands for x in data], dtype=dtype) - a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands - i = Image.fromarray(a) - if list(i.getchannel(0).getdata()) != list(range(100)): - print("data mismatch for", dtype) - return i - - # Check supported 1-bit integer formats - self.assert_image(to_image(numpy.bool, 1, 1), "1", TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.bool8, 1, 1), "1", TEST_IMAGE_SIZE) - - # Check supported 8-bit integer formats - self.assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.uint8, 3), "RGB", TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.uint8, 4), "RGBA", TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.int8), "I", TEST_IMAGE_SIZE) - - # Check non-fixed-size integer types - # These may fail, depending on the platform, since we have no native - # 64 bit int image types. - # self.assert_image(to_image(numpy.uint), "I", TEST_IMAGE_SIZE) - # self.assert_image(to_image(numpy.int), "I", TEST_IMAGE_SIZE) - - # Check 16-bit integer formats - if Image._ENDIAN == "<": - self.assert_image(to_image(numpy.uint16), "I;16", TEST_IMAGE_SIZE) + a = numpy.array(data, dtype=dtype) + a.shape = TEST_IMAGE_SIZE + i = Image.fromarray(a) + if list(i.getdata()) != data: + print("data mismatch for", dtype) else: - self.assert_image(to_image(numpy.uint16), "I;16B", TEST_IMAGE_SIZE) + data = list(range(100)) + a = numpy.array([[x] * bands for x in data], dtype=dtype) + a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands + i = Image.fromarray(a) + if list(i.getchannel(0).getdata()) != list(range(100)): + print("data mismatch for", dtype) + return i - self.assert_image(to_image(numpy.int16), "I", TEST_IMAGE_SIZE) + # Check supported 1-bit integer formats + assert_image(to_image(numpy.bool, 1, 1), "1", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.bool8, 1, 1), "1", TEST_IMAGE_SIZE) - # Check 32-bit integer formats - self.assert_image(to_image(numpy.uint32), "I", TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.int32), "I", TEST_IMAGE_SIZE) + # Check supported 8-bit integer formats + assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.uint8, 3), "RGB", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.uint8, 4), "RGBA", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.int8), "I", TEST_IMAGE_SIZE) - # Check 64-bit integer formats - self.assertRaises(TypeError, to_image, numpy.uint64) - self.assertRaises(TypeError, to_image, numpy.int64) + # Check non-fixed-size integer types + # These may fail, depending on the platform, since we have no native + # 64-bit int image types. + # assert_image(to_image(numpy.uint), "I", TEST_IMAGE_SIZE) + # assert_image(to_image(numpy.int), "I", TEST_IMAGE_SIZE) - # Check floating-point formats - self.assert_image(to_image(numpy.float), "F", TEST_IMAGE_SIZE) - self.assertRaises(TypeError, to_image, numpy.float16) - self.assert_image(to_image(numpy.float32), "F", TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.float64), "F", TEST_IMAGE_SIZE) + # Check 16-bit integer formats + if Image._ENDIAN == "<": + assert_image(to_image(numpy.uint16), "I;16", TEST_IMAGE_SIZE) + else: + assert_image(to_image(numpy.uint16), "I;16B", TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.uint8, 2), "LA", (10, 10)) - self.assert_image(to_image(numpy.uint8, 3), "RGB", (10, 10)) - self.assert_image(to_image(numpy.uint8, 4), "RGBA", (10, 10)) + assert_image(to_image(numpy.int16), "I", TEST_IMAGE_SIZE) - # 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(self): - size = (5, TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1]) - a = numpy.ones(size, dtype=numpy.uint8) - self.assert_image(Image.fromarray(a[1, :, :]), "L", TEST_IMAGE_SIZE) - size = (TEST_IMAGE_SIZE[0], 5, TEST_IMAGE_SIZE[1]) - a = numpy.ones(size, dtype=numpy.uint8) - self.assert_image(Image.fromarray(a[:, 1, :]), "L", TEST_IMAGE_SIZE) - size = (TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], 5) - a = numpy.ones(size, dtype=numpy.uint8) - self.assert_image(Image.fromarray(a[:, :, 1]), "L", TEST_IMAGE_SIZE) + # Check 32-bit integer formats + assert_image(to_image(numpy.uint32), "I", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.int32), "I", TEST_IMAGE_SIZE) - def _test_img_equals_nparray(self, img, np): - self.assertGreaterEqual(len(np.shape), 2) - np_size = np.shape[1], np.shape[0] - self.assertEqual(img.size, np_size) - px = img.load() - for x in range(0, img.size[0], int(img.size[0] / 10)): - for y in range(0, img.size[1], int(img.size[1] / 10)): - self.assert_deep_equal(px[x, y], np[y, x]) + # Check 64-bit integer formats + with pytest.raises(TypeError): + to_image(numpy.uint64) + with pytest.raises(TypeError): + to_image(numpy.int64) - def test_16bit(self): - img = Image.open("Tests/images/16bit.cropped.tif") + # Check floating-point formats + assert_image(to_image(numpy.float), "F", TEST_IMAGE_SIZE) + with pytest.raises(TypeError): + to_image(numpy.float16) + assert_image(to_image(numpy.float32), "F", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.float64), "F", TEST_IMAGE_SIZE) + + assert_image(to_image(numpy.uint8, 2), "LA", (10, 10)) + assert_image(to_image(numpy.uint8, 3), "RGB", (10, 10)) + assert_image(to_image(numpy.uint8, 4), "RGBA", (10, 10)) + + +# 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(): + 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) + size = (TEST_IMAGE_SIZE[0], 5, TEST_IMAGE_SIZE[1]) + a = numpy.ones(size, dtype=numpy.uint8) + assert_image(Image.fromarray(a[:, 1, :]), "L", TEST_IMAGE_SIZE) + size = (TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], 5) + a = numpy.ones(size, dtype=numpy.uint8) + assert_image(Image.fromarray(a[:, :, 1]), "L", TEST_IMAGE_SIZE) + + +def test_1d_array(): + a = numpy.ones(5, dtype=numpy.uint8) + assert_image(Image.fromarray(a), "L", (1, 5)) + + +def _test_img_equals_nparray(img, np): + assert len(np.shape) >= 2 + np_size = np.shape[1], np.shape[0] + assert img.size == np_size + px = img.load() + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + assert_deep_equal(px[x, y], np[y, x]) + + +def test_16bit(): + with Image.open("Tests/images/16bit.cropped.tif") as img: np_img = numpy.array(img) - self._test_img_equals_nparray(img, np_img) - self.assertEqual(np_img.dtype, numpy.dtype("u2"), - ("I;16L", "u2"), + ("I;16L", "", 0), (b"\x90\x1F\xA3", 8)) - self.assertEqual( - PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3), (b"\x90\x1F\xA0", 17) - ) - self.assertEqual(PdfParser.get_value(b"(asd)", 0), (b"asd", 5)) - self.assertEqual( - PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0), (b"asd(qwe)zxc", 13) - ) - self.assertEqual( - PdfParser.get_value(b"(Two \\\nwords.)", 0), (b"Two words.", 14) - ) - self.assertEqual(PdfParser.get_value(b"(Two\nlines.)", 0), (b"Two\nlines.", 12)) - self.assertEqual( - PdfParser.get_value(b"(Two\r\nlines.)", 0), (b"Two\nlines.", 13) - ) - self.assertEqual( - PdfParser.get_value(b"(Two\\nlines.)", 0), (b"Two\nlines.", 13) - ) - self.assertEqual(PdfParser.get_value(b"(One\\(paren).", 0), (b"One(paren", 12)) - self.assertEqual(PdfParser.get_value(b"(One\\)paren).", 0), (b"One)paren", 12)) - self.assertEqual(PdfParser.get_value(b"(\\0053)", 0), (b"\x053", 7)) - self.assertEqual(PdfParser.get_value(b"(\\053)", 0), (b"\x2B", 6)) - self.assertEqual(PdfParser.get_value(b"(\\53)", 0), (b"\x2B", 5)) - self.assertEqual(PdfParser.get_value(b"(\\53a)", 0), (b"\x2Ba", 6)) - self.assertEqual(PdfParser.get_value(b"(\\1111)", 0), (b"\x491", 7)) - self.assertEqual(PdfParser.get_value(b" 123 (", 0), (123, 4)) - self.assertAlmostEqual(PdfParser.get_value(b" 123.4 %", 0)[0], 123.4) - self.assertEqual(PdfParser.get_value(b" 123.4 %", 0)[1], 6) - self.assertRaises(PdfFormatError, PdfParser.get_value, b"]", 0) - d = PdfParser.get_value(b"<>", 0)[0] - self.assertIsInstance(d, PdfDict) - self.assertEqual(len(d), 2) - self.assertEqual(d.Name, "value") - self.assertEqual(d[b"Name"], b"value") - self.assertEqual(d.N, PdfName("V")) - a = PdfParser.get_value(b"[/Name (value) /N /V]", 0)[0] - self.assertIsInstance(a, list) - self.assertEqual(len(a), 4) - self.assertEqual(a[0], PdfName("Name")) - s = PdfParser.get_value( - b"<>\nstream\nabcde\nendstream<<...", 0 - )[0] - self.assertIsInstance(s, PdfStream) - self.assertEqual(s.dictionary.Name, "value") - self.assertEqual(s.decode(), b"abcde") - for name in ["CreationDate", "ModDate"]: - for date, value in { - b"20180729214124": "20180729214124", - b"D:20180729214124": "20180729214124", - b"D:2018072921": "20180729210000", - b"D:20180729214124Z": "20180729214124", - b"D:20180729214124+08'00'": "20180729134124", - b"D:20180729214124-05'00'": "20180730024124", - }.items(): - d = PdfParser.get_value( - b"<>", 0 - )[0] - self.assertEqual(time.strftime("%Y%m%d%H%M%S", getattr(d, name)), value) +def test_parsing(): + 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) + assert PdfParser.get_value(b"true[", 0) == (True, 4) + assert PdfParser.get_value(b"false%", 0) == (False, 5) + assert PdfParser.get_value(b"null<", 0) == (None, 4) + assert PdfParser.get_value(b"%cmt\n %cmt\n 123\n", 0) == (123, 15) + assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1F\xA3", 8) + assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1F\xA0", 17) + assert PdfParser.get_value(b"(asd)", 0) == (b"asd", 5) + assert PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0) == (b"asd(qwe)zxc", 13) + assert PdfParser.get_value(b"(Two \\\nwords.)", 0) == (b"Two words.", 14) + assert PdfParser.get_value(b"(Two\nlines.)", 0) == (b"Two\nlines.", 12) + assert PdfParser.get_value(b"(Two\r\nlines.)", 0) == (b"Two\nlines.", 13) + assert PdfParser.get_value(b"(Two\\nlines.)", 0) == (b"Two\nlines.", 13) + assert PdfParser.get_value(b"(One\\(paren).", 0) == (b"One(paren", 12) + assert PdfParser.get_value(b"(One\\)paren).", 0) == (b"One)paren", 12) + assert PdfParser.get_value(b"(\\0053)", 0) == (b"\x053", 7) + assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2B", 6) + assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2B", 5) + assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2Ba", 6) + assert PdfParser.get_value(b"(\\1111)", 0) == (b"\x491", 7) + assert PdfParser.get_value(b" 123 (", 0) == (123, 4) + assert round(abs(PdfParser.get_value(b" 123.4 %", 0)[0] - 123.4), 7) == 0 + assert PdfParser.get_value(b" 123.4 %", 0)[1] == 6 + with pytest.raises(PdfFormatError): + PdfParser.get_value(b"]", 0) + d = PdfParser.get_value(b"<>", 0)[0] + assert isinstance(d, PdfDict) + assert len(d) == 2 + assert d.Name == "value" + assert d[b"Name"] == b"value" + assert d.N == PdfName("V") + a = PdfParser.get_value(b"[/Name (value) /N /V]", 0)[0] + assert isinstance(a, list) + assert len(a) == 4 + assert a[0] == PdfName("Name") + s = PdfParser.get_value( + b"<>\nstream\nabcde\nendstream<<...", 0 + )[0] + assert isinstance(s, PdfStream) + assert s.dictionary.Name == "value" + assert s.decode() == b"abcde" + for name in ["CreationDate", "ModDate"]: + for date, value in { + b"20180729214124": "20180729214124", + b"D:20180729214124": "20180729214124", + b"D:2018072921": "20180729210000", + b"D:20180729214124Z": "20180729214124", + b"D:20180729214124+08'00'": "20180729134124", + b"D:20180729214124-05'00'": "20180730024124", + }.items(): + d = PdfParser.get_value(b"<>", 0)[ + 0 + ] + assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value - def test_pdf_repr(self): - self.assertEqual(bytes(IndirectReference(1, 2)), b"1 2 R") - self.assertEqual(bytes(IndirectObjectDef(*IndirectReference(1, 2))), b"1 2 obj") - self.assertEqual(bytes(PdfName(b"Name#Hash")), b"/Name#23Hash") - self.assertEqual(bytes(PdfName("Name#Hash")), b"/Name#23Hash") - self.assertEqual( - bytes(PdfDict({b"Name": IndirectReference(1, 2)})), b"<<\n/Name 1 2 R\n>>" - ) - self.assertEqual( - bytes(PdfDict({"Name": IndirectReference(1, 2)})), b"<<\n/Name 1 2 R\n>>" - ) - self.assertEqual(pdf_repr(IndirectReference(1, 2)), b"1 2 R") - self.assertEqual( - pdf_repr(IndirectObjectDef(*IndirectReference(1, 2))), b"1 2 obj" - ) - self.assertEqual(pdf_repr(PdfName(b"Name#Hash")), b"/Name#23Hash") - self.assertEqual(pdf_repr(PdfName("Name#Hash")), b"/Name#23Hash") - self.assertEqual( - pdf_repr(PdfDict({b"Name": IndirectReference(1, 2)})), - b"<<\n/Name 1 2 R\n>>", - ) - self.assertEqual( - pdf_repr(PdfDict({"Name": IndirectReference(1, 2)})), b"<<\n/Name 1 2 R\n>>" - ) - self.assertEqual(pdf_repr(123), b"123") - self.assertEqual(pdf_repr(True), b"true") - self.assertEqual(pdf_repr(False), b"false") - self.assertEqual(pdf_repr(None), b"null") - self.assertEqual(pdf_repr(b"a)/b\\(c"), br"(a\)/b\\\(c)") - self.assertEqual( - pdf_repr([123, True, {"a": PdfName(b"b")}]), b"[ 123 true <<\n/a /b\n>> ]" - ) - self.assertEqual(pdf_repr(PdfBinary(b"\x90\x1F\xA0")), b"<901FA0>") + +def test_pdf_repr(): + 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" + assert bytes(PdfName("Name#Hash")) == b"/Name#23Hash" + assert bytes(PdfDict({b"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" + assert bytes(PdfDict({"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" + assert pdf_repr(IndirectReference(1, 2)) == b"1 2 R" + assert pdf_repr(IndirectObjectDef(*IndirectReference(1, 2))) == b"1 2 obj" + assert pdf_repr(PdfName(b"Name#Hash")) == b"/Name#23Hash" + assert pdf_repr(PdfName("Name#Hash")) == b"/Name#23Hash" + assert ( + pdf_repr(PdfDict({b"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" + ) + assert ( + pdf_repr(PdfDict({"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" + ) + assert pdf_repr(123) == b"123" + assert pdf_repr(True) == b"true" + assert pdf_repr(False) == b"false" + assert pdf_repr(None) == b"null" + assert pdf_repr(b"a)/b\\(c") == br"(a\)/b\\\(c)" + assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" + assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 42f47f169..a10dcec8c 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,13 +1,16 @@ +import pickle + +import pytest + from PIL import Image -from .helper import PillowTestCase +from .helper import skip_unless_feature -class TestPickle(PillowTestCase): - def helper_pickle_file(self, pickle, protocol=0, mode=None): - # Arrange - im = Image.open("Tests/images/hopper.jpg") - filename = self.tempfile("temp.pkl") +def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode): + # Arrange + with Image.open(test_file) as im: + filename = str(tmp_path / "temp.pkl") if mode: im = im.convert(mode) @@ -18,12 +21,11 @@ class TestPickle(PillowTestCase): loaded_im = pickle.load(f) # Assert - self.assertEqual(im, loaded_im) + assert im == loaded_im - def helper_pickle_string( - self, pickle, protocol=0, test_file="Tests/images/hopper.jpg", mode=None - ): - im = Image.open(test_file) + +def helper_pickle_string(pickle, protocol, test_file, mode): + with Image.open(test_file) as im: if mode: im = im.convert(mode) @@ -32,94 +34,61 @@ class TestPickle(PillowTestCase): loaded_im = pickle.loads(dumped_string) # Assert - self.assertEqual(im, loaded_im) + assert im == loaded_im - def test_pickle_image(self): - # Arrange - import pickle - # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string(pickle, protocol) - self.helper_pickle_file(pickle, protocol) +@pytest.mark.parametrize( + ("test_file", "test_mode"), + [ + ("Tests/images/hopper.jpg", None), + ("Tests/images/hopper.jpg", "L"), + ("Tests/images/hopper.jpg", "PA"), + pytest.param( + "Tests/images/hopper.webp", None, marks=skip_unless_feature("webp") + ), + ("Tests/images/hopper.tif", None), + ("Tests/images/test-card.png", None), + ("Tests/images/zero_bb.png", None), + ("Tests/images/zero_bb_scale2.png", None), + ("Tests/images/non_zero_bb.png", None), + ("Tests/images/non_zero_bb_scale2.png", None), + ("Tests/images/p_trns_single.png", None), + ("Tests/images/pil123p.png", None), + ("Tests/images/itxt_chunks.png", None), + ], +) +def test_pickle_image(tmp_path, test_file, test_mode): + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + helper_pickle_string(pickle, protocol, test_file, test_mode) + helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode) - def test_cpickle_image(self): - # Arrange - try: - import cPickle - except ImportError: - return - # Act / Assert - for protocol in range(0, cPickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string(cPickle, protocol) - self.helper_pickle_file(cPickle, protocol) - - def test_pickle_p_mode(self): - # Arrange - import pickle - - # Act / Assert - for test_file in [ - "Tests/images/test-card.png", - "Tests/images/zero_bb.png", - "Tests/images/zero_bb_scale2.png", - "Tests/images/non_zero_bb.png", - "Tests/images/non_zero_bb_scale2.png", - "Tests/images/p_trns_single.png", - "Tests/images/pil123p.png", - "Tests/images/itxt_chunks.png", - ]: - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string( - pickle, protocol=protocol, test_file=test_file - ) - - def test_pickle_pa_mode(self): - # Arrange - import pickle - - # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string(pickle, protocol, mode="PA") - self.helper_pickle_file(pickle, protocol, mode="PA") - - def test_pickle_l_mode(self): - # Arrange - import pickle - - # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string(pickle, protocol, mode="L") - self.helper_pickle_file(pickle, protocol, mode="L") - - def test_pickle_la_mode_with_palette(self): - # Arrange - import pickle - - im = Image.open("Tests/images/hopper.jpg") - filename = self.tempfile("temp.pkl") +def test_pickle_la_mode_with_palette(tmp_path): + # Arrange + filename = str(tmp_path / "temp.pkl") + with Image.open("Tests/images/hopper.jpg") as im: im = im.convert("PA") - # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - im.mode = "LA" - with open(filename, "wb") as f: - pickle.dump(im, f, protocol) - with open(filename, "rb") as f: - loaded_im = pickle.load(f) + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + im.mode = "LA" + with open(filename, "wb") as f: + pickle.dump(im, f, protocol) + with open(filename, "rb") as f: + loaded_im = pickle.load(f) - im.mode = "PA" - self.assertEqual(im, loaded_im) + im.mode = "PA" + assert im == loaded_im - def test_cpickle_l_mode(self): - # Arrange - try: - import cPickle - except ImportError: - return - # Act / Assert - for protocol in range(0, cPickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string(cPickle, protocol, mode="L") - self.helper_pickle_file(cPickle, protocol, mode="L") +@skip_unless_feature("webp") +def test_pickle_tell(): + # Arrange + image = Image.open("Tests/images/hopper.webp") + + # Act: roundtrip + unpickled_image = pickle.loads(pickle.dumps(image)) + + # Assert + assert unpickled_image.tell() == 0 diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 0ef9acb06..31f0f493b 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -4,57 +4,55 @@ from io import StringIO from PIL import Image, PSDraw -from .helper import PillowTestCase +def _create_document(ps): + title = "hopper" + box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points -class TestPsDraw(PillowTestCase): - def _create_document(self, ps): - im = Image.open("Tests/images/hopper.ppm") - title = "hopper" - box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points + ps.begin_document(title) - ps.begin_document(title) + # draw diagonal lines in a cross + ps.line((1 * 72, 2 * 72), (7 * 72, 10 * 72)) + ps.line((7 * 72, 2 * 72), (1 * 72, 10 * 72)) - # draw diagonal lines in a cross - ps.line((1 * 72, 2 * 72), (7 * 72, 10 * 72)) - ps.line((7 * 72, 2 * 72), (1 * 72, 10 * 72)) - - # draw the image (75 dpi) + # draw the image (75 dpi) + with Image.open("Tests/images/hopper.ppm") as im: ps.image(box, im, 75) - ps.rectangle(box) + ps.rectangle(box) - # draw title - ps.setfont("Courier", 36) - ps.text((3 * 72, 4 * 72), title) + # draw title + ps.setfont("Courier", 36) + ps.text((3 * 72, 4 * 72), title) - ps.end_document() + ps.end_document() - def test_draw_postscript(self): - # Based on Pillow tutorial, but there is no textsize: - # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript +def test_draw_postscript(tmp_path): + # Based on Pillow tutorial, but there is no textsize: + # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript - # Arrange - tempfile = self.tempfile("temp.ps") - with open(tempfile, "wb") as fp: - # Act - ps = PSDraw.PSDraw(fp) - self._create_document(ps) + # Arrange + tempfile = str(tmp_path / "temp.ps") + with open(tempfile, "wb") as fp: + # Act + ps = PSDraw.PSDraw(fp) + _create_document(ps) - # Assert - # Check non-zero file was created - self.assertTrue(os.path.isfile(tempfile)) - self.assertGreater(os.path.getsize(tempfile), 0) + # Assert + # Check non-zero file was created + assert os.path.isfile(tempfile) + assert os.path.getsize(tempfile) > 0 - def test_stdout(self): - # Temporarily redirect stdout - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() - ps = PSDraw.PSDraw() - self._create_document(ps) +def test_stdout(): + # Temporarily redirect stdout + old_stdout = sys.stdout + sys.stdout = mystdout = StringIO() - # Reset stdout - sys.stdout = old_stdout + ps = PSDraw.PSDraw() + _create_document(ps) - self.assertNotEqual(mystdout.getvalue(), "") + # Reset stdout + sys.stdout = old_stdout + + assert mystdout.getvalue() != "" diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index accad5451..aa05c2cfd 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,32 +1,25 @@ -import unittest +import pytest from PIL import __version__ -from .helper import PillowTestCase - -try: - import pyroma -except ImportError: - pyroma = None +pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") -@unittest.skipIf(pyroma is None, "Pyroma is not installed") -class TestPyroma(PillowTestCase): - def test_pyroma(self): - # Arrange - data = pyroma.projectdata.get_data(".") +def test_pyroma(): + # Arrange + data = pyroma.projectdata.get_data(".") - # Act - rating = pyroma.ratings.rate(data) + # Act + rating = pyroma.ratings.rate(data) - # Assert - if "rc" in __version__: - # Pyroma needs to chill about RC versions and not kill all our tests. - self.assertEqual( - rating, - (9, ["The package's version number does not comply with PEP-386."]), - ) + # Assert + if "rc" in __version__: + # Pyroma needs to chill about RC versions and not kill all our tests. + assert rating == ( + 9, + ["The package's version number does not comply with PEP-386."], + ) - else: - # Should have a perfect score - self.assertEqual(rating, (10, [])) + else: + # Should have a perfect score + assert rating == (10, []) diff --git a/Tests/test_qt_image_fromqpixmap.py b/Tests/test_qt_image_fromqpixmap.py index 1cff26d88..cb1b385ec 100644 --- a/Tests/test_qt_image_fromqpixmap.py +++ b/Tests/test_qt_image_fromqpixmap.py @@ -1,14 +1,14 @@ from PIL import ImageQt -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper from .test_imageqt import PillowQPixmapTestCase -class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase): +class TestFromQPixmap(PillowQPixmapTestCase): def roundtrip(self, expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - self.assert_image_equal(result, expected.convert("RGB")) + assert_image_equal(result, expected.convert("RGB")) def test_sanity(self): for mode in ("1", "RGB", "RGBA", "L", "P"): diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 39d60d197..8d599f9bf 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,56 +1,61 @@ +import pytest + from PIL import Image, ImageQt -from .helper import PillowTestCase, hopper -from .test_imageqt import PillowQtTestCase +from .helper import assert_image_equal, hopper + +pytestmark = pytest.mark.skipif( + not ImageQt.qt_is_installed, reason="Qt bindings are not installed" +) if ImageQt.qt_is_installed: from PIL.ImageQt import QImage try: from PyQt5 import QtGui - from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication + from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget except (ImportError, RuntimeError): from PySide2 import QtGui - from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication + from PySide2.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget -class TestToQImage(PillowQtTestCase, PillowTestCase): - def test_sanity(self): - for mode in ("RGB", "RGBA", "L", "P", "1"): - src = hopper(mode) - data = ImageQt.toqimage(src) +def test_sanity(tmp_path): + for mode in ("RGB", "RGBA", "L", "P", "1"): + src = hopper(mode) + data = ImageQt.toqimage(src) - self.assertIsInstance(data, QImage) - self.assertFalse(data.isNull()) + assert isinstance(data, QImage) + assert not data.isNull() - # reload directly from the qimage - rt = ImageQt.fromqimage(data) - if mode in ("L", "P", "1"): - self.assert_image_equal(rt, src.convert("RGB")) - else: - self.assert_image_equal(rt, src) + # reload directly from the qimage + rt = ImageQt.fromqimage(data) + if mode in ("L", "P", "1"): + assert_image_equal(rt, src.convert("RGB")) + else: + assert_image_equal(rt, src) - if mode == "1": - # BW appears to not save correctly on QT4 and QT5 - # kicks out errors on console: - # libpng warning: Invalid color type/bit depth combination - # in IHDR - # libpng error: Invalid IHDR data - continue + if mode == "1": + # BW appears to not save correctly on QT4 and QT5 + # kicks out errors on console: + # libpng warning: Invalid color type/bit depth combination + # in IHDR + # libpng error: Invalid IHDR data + continue - # Test saving the file - tempfile = self.tempfile("temp_{}.png".format(mode)) - data.save(tempfile) + # Test saving the file + tempfile = str(tmp_path / f"temp_{mode}.png") + data.save(tempfile) - # Check that it actually worked. - reloaded = Image.open(tempfile) - self.assert_image_equal(reloaded, src) + # Check that it actually worked. + with Image.open(tempfile) as reloaded: + assert_image_equal(reloaded, src) - def test_segfault(self): - app = QApplication([]) - ex = Example() - assert app # Silence warning - assert ex # Silence warning + +def test_segfault(): + app = QApplication([]) + ex = Example() + assert app # Silence warning + assert ex # Silence warning if ImageQt.qt_is_installed: diff --git a/Tests/test_qt_image_toqpixmap.py b/Tests/test_qt_image_toqpixmap.py index 2c07f1bf5..f38cc7f13 100644 --- a/Tests/test_qt_image_toqpixmap.py +++ b/Tests/test_qt_image_toqpixmap.py @@ -1,20 +1,20 @@ from PIL import ImageQt -from .helper import PillowTestCase, hopper +from .helper import hopper from .test_imageqt import PillowQPixmapTestCase if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap -class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase): - def test_sanity(self): +class TestToQPixmap(PillowQPixmapTestCase): + def test_sanity(self, tmp_path): for mode in ("1", "RGB", "RGBA", "L", "P"): data = ImageQt.toqpixmap(hopper(mode)) - self.assertIsInstance(data, QPixmap) - self.assertFalse(data.isNull()) + assert isinstance(data, QPixmap) + assert not data.isNull() # Test saving the file - tempfile = self.tempfile("temp_{}.png".format(mode)) + tempfile = str(tmp_path / f"temp_{mode}.png") data.save(tempfile) diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py new file mode 100644 index 000000000..2b671244a --- /dev/null +++ b/Tests/test_sgi_crash.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import pytest + +from PIL import Image + + +@pytest.mark.parametrize( + "test_file", + ["Tests/images/sgi_overrun_expandrowF04.bin", "Tests/images/sgi_crash.bin"], +) +def test_crashes(test_file): + with open(test_file, "rb") as f: + im = Image.open(f) + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 3e74647eb..d25d42dfc 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,15 +1,10 @@ import shutil +import pytest + from PIL import GifImagePlugin, Image, JpegImagePlugin -from .helper import ( - PillowTestCase, - cjpeg_available, - djpeg_available, - is_win32, - netpbm_available, - unittest, -) +from .helper import cjpeg_available, djpeg_available, is_win32, netpbm_available TEST_JPG = "Tests/images/hopper.jpg" TEST_GIF = "Tests/images/hopper.gif" @@ -17,38 +12,38 @@ TEST_GIF = "Tests/images/hopper.gif" test_filenames = ("temp_';", 'temp_";', "temp_'\"|", "temp_'\"||", "temp_'\"&&") -@unittest.skipIf(is_win32(), "requires Unix or macOS") -class TestShellInjection(PillowTestCase): - def assert_save_filename_check(self, src_img, save_func): +@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") +class TestShellInjection: + def assert_save_filename_check(self, tmp_path, src_img, save_func): for filename in test_filenames: - dest_file = self.tempfile(filename) + dest_file = str(tmp_path / filename) save_func(src_img, 0, dest_file) # If file can't be opened, shell injection probably occurred with Image.open(dest_file) as im: im.load() - @unittest.skipUnless(djpeg_available(), "djpeg not available") - def test_load_djpeg_filename(self): + @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") + def test_load_djpeg_filename(self, tmp_path): for filename in test_filenames: - src_file = self.tempfile(filename) + src_file = str(tmp_path / filename) shutil.copy(TEST_JPG, src_file) with Image.open(src_file) as im: im.load_djpeg() - @unittest.skipUnless(cjpeg_available(), "cjpeg not available") - def test_save_cjpeg_filename(self): - im = Image.open(TEST_JPG) - self.assert_save_filename_check(im, JpegImagePlugin._save_cjpeg) + @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") + def test_save_cjpeg_filename(self, tmp_path): + with Image.open(TEST_JPG) as im: + self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg) - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_filename_bmp_mode(self): + @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") + def test_save_netpbm_filename_bmp_mode(self, tmp_path): with Image.open(TEST_GIF) as im: im = im.convert("RGB") - self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) + self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_filename_l_mode(self): + @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") + def test_save_netpbm_filename_l_mode(self, tmp_path): with Image.open(TEST_GIF) as im: im = im.convert("L") - self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) + 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 new file mode 100644 index 000000000..9c293e014 --- /dev/null +++ b/Tests/test_tiff_crashes.py @@ -0,0 +1,36 @@ +# Reproductions/tests for crashes/read errors in TiffDecode.c + +# When run in Python, all of these images should fail for +# one reason or another, either as a buffer overrun, +# unrecognized datastream, or truncated image file. +# There shouldn't be any segfaults. +# +# if run like +# `valgrind --tool=memcheck pytest test_tiff_crashes.py 2>&1 | grep TiffDecode.c` +# the output should be empty. There may be Python issues +# in the valgrind especially if run in a debug Python +# version. + +import pytest + +from PIL import Image + +from .helper import on_ci + + +@pytest.mark.parametrize( + "test_file", ["Tests/images/crash_1.tif", "Tests/images/crash_2.tif"] +) +@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") +@pytest.mark.filterwarnings("ignore:Metadata warning") +def test_tiff_crashes(test_file): + try: + with Image.open(test_file) as im: + im.load() + except FileNotFoundError: + if not on_ci(): + pytest.skip("test image not found") + return + raise + except OSError: + pass diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index dedbbfe6d..1697a8d49 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,60 +1,66 @@ from fractions import Fraction -from PIL import Image, TiffImagePlugin +from PIL import Image, TiffImagePlugin, features from PIL.TiffImagePlugin import IFDRational -from .helper import PillowTestCase, hopper +from .helper import hopper -class Test_IFDRational(PillowTestCase): - def _test_equal(self, num, denom, target): +def _test_equal(num, denom, target): - t = IFDRational(num, denom) + t = IFDRational(num, denom) - self.assertEqual(target, t) - self.assertEqual(t, target) + assert target == t + assert t == target - def test_sanity(self): - self._test_equal(1, 1, 1) - self._test_equal(1, 1, Fraction(1, 1)) +def test_sanity(): - self._test_equal(2, 2, 1) - self._test_equal(1.0, 1, Fraction(1, 1)) + _test_equal(1, 1, 1) + _test_equal(1, 1, Fraction(1, 1)) - self._test_equal(Fraction(1, 1), 1, Fraction(1, 1)) - self._test_equal(IFDRational(1, 1), 1, 1) + _test_equal(2, 2, 1) + _test_equal(1.0, 1, Fraction(1, 1)) - self._test_equal(1, 2, Fraction(1, 2)) - self._test_equal(1, 2, IFDRational(1, 2)) + _test_equal(Fraction(1, 1), 1, Fraction(1, 1)) + _test_equal(IFDRational(1, 1), 1, 1) - def test_nonetype(self): - # Fails if the _delegate function doesn't return a valid function + _test_equal(1, 2, Fraction(1, 2)) + _test_equal(1, 2, IFDRational(1, 2)) - xres = IFDRational(72) - yres = IFDRational(72) - self.assertIsNotNone(xres._val) - self.assertIsNotNone(xres.numerator) - self.assertIsNotNone(xres.denominator) - self.assertIsNotNone(yres._val) - self.assertTrue(xres and 1) - self.assertTrue(xres and yres) +def test_ranges(): + for num in range(1, 10): + for denom in range(1, 10): + assert IFDRational(num, denom) == IFDRational(num, denom) - def test_ifd_rational_save(self): - methods = (True, False) - if "libtiff_encoder" not in dir(Image.core): - methods = (False,) - for libtiff in methods: - TiffImagePlugin.WRITE_LIBTIFF = libtiff +def test_nonetype(): + # Fails if the _delegate function doesn't return a valid function - im = hopper() - out = self.tempfile("temp.tiff") - res = IFDRational(301, 1) - im.save(out, dpi=(res, res), compression="raw") + xres = IFDRational(72) + yres = IFDRational(72) + assert xres._val is not None + assert xres.numerator is not None + assert xres.denominator is not None + assert yres._val is not None - with Image.open(out) as reloaded: - self.assertEqual( - float(IFDRational(301, 1)), float(reloaded.tag_v2[282]) - ) + assert xres and 1 + assert xres and yres + + +def test_ifd_rational_save(tmp_path): + methods = (True, False) + if not features.check("libtiff"): + methods = (False,) + + for libtiff in methods: + TiffImagePlugin.WRITE_LIBTIFF = libtiff + + im = hopper() + out = str(tmp_path / "temp.tiff") + res = IFDRational(301, 1) + im.save(out, dpi=(res, res), compression="raw") + + with Image.open(out) as reloaded: + assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index 46dbd824a..720926e53 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -1,13 +1,13 @@ -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, assert_image_similar, hopper -class TestUploader(PillowTestCase): - def check_upload_equal(self): - result = hopper("P").convert("RGB") - target = hopper("RGB") - self.assert_image_equal(result, target) +def check_upload_equal(): + result = hopper("P").convert("RGB") + target = hopper("RGB") + assert_image_equal(result, target) - def check_upload_similar(self): - result = hopper("P").convert("RGB") - target = hopper("RGB") - self.assert_image_similar(result, target, 0) + +def check_upload_similar(): + 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 6a111b5b9..b5bfca012 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,70 +1,72 @@ -import unittest +import pytest from PIL import _util -from .helper import PillowTestCase + +def test_is_path(): + # Arrange + fp = "filename.ext" + + # Act + it_is = _util.isPath(fp) + + # Assert + assert it_is -class TestUtil(PillowTestCase): - def test_is_path(self): - # Arrange - fp = "filename.ext" +def test_path_obj_is_path(): + # Arrange + from pathlib import Path - # Act - it_is = _util.isPath(fp) + test_path = Path("filename.ext") - # Assert - self.assertTrue(it_is) + # Act + it_is = _util.isPath(test_path) - @unittest.skipIf(not _util.py36, "os.path support for Paths added in 3.6") - def test_path_obj_is_path(self): - # Arrange - from pathlib import Path + # Assert + assert it_is - test_path = Path("filename.ext") - # Act - it_is = _util.isPath(test_path) +def test_is_not_path(tmp_path): + # Arrange + with (tmp_path / "temp.ext").open("w") as fp: + pass - # Assert - self.assertTrue(it_is) + # Act + it_is_not = _util.isPath(fp) - def test_is_not_path(self): - # Arrange - filename = self.tempfile("temp.ext") - fp = open(filename, "w").close() + # Assert + assert not it_is_not - # Act - it_is_not = _util.isPath(fp) - # Assert - self.assertFalse(it_is_not) +def test_is_directory(): + # Arrange + directory = "Tests" - def test_is_directory(self): - # Arrange - directory = "Tests" + # Act + it_is = _util.isDirectory(directory) - # Act - it_is = _util.isDirectory(directory) + # Assert + assert it_is - # Assert - self.assertTrue(it_is) - def test_is_not_directory(self): - # Arrange - text = "abc" +def test_is_not_directory(): + # Arrange + text = "abc" - # Act - it_is_not = _util.isDirectory(text) + # Act + it_is_not = _util.isDirectory(text) - # Assert - self.assertFalse(it_is_not) + # Assert + assert not it_is_not - def test_deferred_error(self): - # Arrange - # Act - thing = _util.deferred_error(ValueError("Some error text")) +def test_deferred_error(): + # Arrange - # Assert - self.assertRaises(ValueError, lambda: thing.some_attr) + # Act + thing = _util.deferred_error(ValueError("Some error text")) + + # Assert + with pytest.raises(ValueError): + thing.some_attr diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 713fc161e..34197c14f 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -1,14 +1,13 @@ -import unittest from io import BytesIO -from PIL import Image, features +from PIL import Image -from .helper import PillowLeakTestCase +from .helper import PillowLeakTestCase, skip_unless_feature test_file = "Tests/images/hopper.webp" -@unittest.skipUnless(features.check("webp"), "WebP is not installed") +@skip_unless_feature("webp") class TestWebPLeaks(PillowLeakTestCase): mem_limit = 3 * 1024 # kb diff --git a/Tests/threaded_save.py b/Tests/threaded_save.py deleted file mode 100644 index d47c2cf99..000000000 --- a/Tests/threaded_save.py +++ /dev/null @@ -1,57 +0,0 @@ -import io -import queue -import sys -import threading -import time - -from PIL import Image - -test_format = sys.argv[1] if len(sys.argv) > 1 else "PNG" - -im = Image.open("Tests/images/hopper.ppm") -im.load() - -queue = queue.Queue() - -result = [] - - -class Worker(threading.Thread): - def run(self): - while True: - im = queue.get() - if im is None: - queue.task_done() - sys.stdout.write("x") - break - f = io.BytesIO() - im.save(f, test_format, optimize=1) - data = f.getvalue() - result.append(len(data)) - im = Image.open(io.BytesIO(data)) - im.load() - sys.stdout.write(".") - queue.task_done() - - -t0 = time.time() - -threads = 20 -jobs = 100 - -for i in range(threads): - w = Worker() - w.start() - -for i in range(jobs): - queue.put(im) - -for i in range(threads): - queue.put(None) - -queue.join() - -print() -print(time.time() - t0) -print(len(result), sum(result)) -print(result) diff --git a/Tests/versions.py b/Tests/versions.py deleted file mode 100644 index d6433b063..000000000 --- a/Tests/versions.py +++ /dev/null @@ -1,26 +0,0 @@ -from PIL import Image - - -def version(module, version): - v = getattr(module.core, version + "_version", None) - if v: - print(version, v) - - -version(Image, "jpeglib") -version(Image, "zlib") -version(Image, "libtiff") - -try: - from PIL import ImageFont -except ImportError: - pass -else: - version(ImageFont, "freetype2") - -try: - from PIL import ImageCms -except ImportError: - pass -else: - version(ImageCms, "littlecms") diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index dd77f3278..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,72 +0,0 @@ -# Python package -# Create and test a Python package on multiple Python versions. -# Add steps that analyze code, save the dist with the build record, -# publish to a PyPI-compatible index, and more: -# https://docs.microsoft.com/azure/devops/pipelines/ecosystems/python - -jobs: - -- template: .azure-pipelines/jobs/lint.yml - parameters: - name: Lint - vmImage: 'Ubuntu-16.04' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'alpine' - name: 'alpine' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'arch' - name: 'arch' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'ubuntu-16.04-xenial-amd64' - name: 'ubuntu_16_04_xenial_amd64' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'ubuntu-18.04-bionic-amd64' - name: 'ubuntu_18_04_bionic_amd64' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'debian-9-stretch-x86' - name: 'debian_9_stretch_x86' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'debian-10-buster-x86' - name: 'debian_10_buster_x86' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'centos-6-amd64' - name: 'centos_6_amd64' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'centos-7-amd64' - name: 'centos_7_amd64' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'amazon-1-amd64' - name: 'amazon_1_amd64' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'amazon-2-amd64' - name: 'amazon_2_amd64' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'fedora-30-amd64' - name: 'fedora_30_amd64' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'fedora-31-amd64' - name: 'fedora_31_amd64' diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..f3afccc1c --- /dev/null +++ b/codecov.yml @@ -0,0 +1,22 @@ +# Documentation: https://docs.codecov.io/docs/codecov-yaml + +codecov: + # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" + # https://github.com/codecov/support/issues/363 + # https://docs.codecov.io/docs/comparing-commits + allow_coverage_offsets: true + +comment: false + +coverage: + status: + project: + default: + threshold: 0.01% + +# Matches 'omit:' in .coveragerc +ignore: + - "Tests/32bit_segfault_check.py" + - "Tests/bench_cffi_access.py" + - "Tests/check_*.py" + - "Tests/createfontdatachunk.py" diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000..e123cca80 --- /dev/null +++ b/conftest.py @@ -0,0 +1 @@ +pytest_plugins = ["Tests.helper"] diff --git a/depends/README.rst b/depends/README.rst index 069d2b81f..ce88fa47b 100644 --- a/depends/README.rst +++ b/depends/README.rst @@ -7,24 +7,3 @@ build & install non-packaged dependencies; useful for testing with Travis CI. ``install_extra_test_images.sh`` can be used to install additional test images that are used for Travis CI and AppVeyor. - -The other scripts can be used to install all of the dependencies for -the listed operating systems/distros. The ``ubuntu_14.04.sh`` and -``debian_8.2.sh`` scripts have been tested on bare AWS images and will -install all required dependencies for the system Python 2.7 and 3.4 -for all of the optional dependencies. Git may also be required prior -to running the script to actually download Pillow. - -e.g.:: - - $ sudo apt-get install git - $ git clone https://github.com/python-pillow/Pillow.git - $ cd Pillow/depends - $ ./debian_8.2.sh - $ cd .. - $ git checkout [branch or tag] - $ virtualenv -p /usr/bin/python2.7 ~/vpy27 - $ source ~/vpy27/bin/activate - $ make install - $ make test - diff --git a/depends/diffcover-install.sh b/depends/diffcover-install.sh index a0b462b56..18be4f9f9 100755 --- a/depends/diffcover-install.sh +++ b/depends/diffcover-install.sh @@ -5,4 +5,4 @@ git fetch origin master:refs/remotes/origin/master # CFLAGS=-O0 means build with no optimisation. # Makes build much quicker for lxml and other dependencies. -time CFLAGS=-O0 pip install diff_cover +time CFLAGS=-O0 python3 -m pip install diff_cover diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 0e1364b2a..0da660bf5 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -4,12 +4,12 @@ # Use SVN to just fetch a single Git subdirectory svn_export() { - if [ ! -z $1 ]; then - echo "" - echo "Retrying svn export..." - echo "" - fi + if [ ! -z $1 ]; then + echo "" + echo "Retrying svn export..." + echo "" + fi - svn export --force https://github.com/python-pillow/pillow-depends/trunk/test_images ../Tests/images + svn export --force https://github.com/python-pillow/pillow-depends/trunk/test_images ../Tests/images } svn_export || svn_export retry || svn_export retry || svn_export retry \ No newline at end of file diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 0120dbc0b..ae70bde8f 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-2.12.5 +archive=libimagequant-2.13.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 7ccd09301..9b1882c43 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.0.3 +archive=libwebp-1.1.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz diff --git a/depends/termux.sh b/depends/termux.sh index f117790c5..1acc09c44 100755 --- a/depends/termux.sh +++ b/depends/termux.sh @@ -1,5 +1,5 @@ #!/bin/sh -pkg -y install python python-dev ndk-sysroot clang make \ - libjpeg-turbo-dev +pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo diff --git a/docs/COPYING b/docs/COPYING index a1e258129..ec2a5d8cb 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-2019 by Alex Clark and contributors + Copyright © 2010-2020 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/Makefile b/docs/Makefile index 1a912039e..d7c20bca4 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -42,7 +42,7 @@ clean: -rm -rf $(BUILDDIR)/* html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + $(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." @@ -142,7 +142,7 @@ changes: @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." diff --git a/docs/PIL.rst b/docs/PIL.rst index fe69fed62..fa036b9cc 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -4,106 +4,97 @@ PIL Package (autodoc of remaining modules) Reference for modules whose documentation has not yet been ported or written can be found here. -:mod:`BdfFontFile` Module -------------------------- +:mod:`PIL` Module +----------------- + +.. py:module:: PIL + +.. autoexception:: UnidentifiedImageError + :show-inheritance: + +:mod:`~PIL.BdfFontFile` Module +------------------------------ .. automodule:: PIL.BdfFontFile :members: :undoc-members: :show-inheritance: -:mod:`ContainerIO` Module -------------------------- +:mod:`~PIL.ContainerIO` Module +------------------------------ .. automodule:: PIL.ContainerIO :members: :undoc-members: :show-inheritance: -:mod:`FontFile` Module ----------------------- +:mod:`~PIL.FontFile` Module +--------------------------- .. automodule:: PIL.FontFile :members: :undoc-members: :show-inheritance: -:mod:`GdImageFile` Module -------------------------- +:mod:`~PIL.GdImageFile` Module +------------------------------ .. automodule:: PIL.GdImageFile :members: :undoc-members: :show-inheritance: -:mod:`GimpGradientFile` Module ------------------------------- +:mod:`~PIL.GimpGradientFile` Module +----------------------------------- .. automodule:: PIL.GimpGradientFile :members: :undoc-members: :show-inheritance: -:mod:`GimpPaletteFile` Module ------------------------------ +:mod:`~PIL.GimpPaletteFile` Module +---------------------------------- .. automodule:: PIL.GimpPaletteFile :members: :undoc-members: :show-inheritance: -.. intentionally skipped documenting this because it's not documented anywhere - -:mod:`ImageDraw2` Module ------------------------- +:mod:`~PIL.ImageDraw2` Module +----------------------------- .. automodule:: PIL.ImageDraw2 :members: + :member-order: bysource :undoc-members: :show-inheritance: -:mod:`ImageShow` Module ------------------------ - -.. automodule:: PIL.ImageShow - :members: - :undoc-members: - :show-inheritance: - -:mod:`ImageTransform` Module ----------------------------- +:mod:`~PIL.ImageTransform` Module +--------------------------------- .. automodule:: PIL.ImageTransform :members: :undoc-members: :show-inheritance: -:mod:`JpegPresets` Module -------------------------- - -.. automodule:: PIL.JpegPresets - :members: - :undoc-members: - :show-inheritance: - -:mod:`PaletteFile` Module -------------------------- +:mod:`~PIL.PaletteFile` Module +------------------------------ .. automodule:: PIL.PaletteFile :members: :undoc-members: :show-inheritance: -:mod:`PcfFontFile` Module -------------------------- +:mod:`~PIL.PcfFontFile` Module +------------------------------ .. automodule:: PIL.PcfFontFile :members: :undoc-members: :show-inheritance: -:class:`PngImagePlugin.iTXt` Class ----------------------------------- +:class:`.PngImagePlugin.iTXt` Class +----------------------------------- .. autoclass:: PIL.PngImagePlugin.iTXt :members: @@ -116,8 +107,8 @@ can be found here. :param lang: language code :param tkey: UTF-8 version of the key name -:class:`PngImagePlugin.PngInfo` Class -------------------------------------- +:class:`.PngImagePlugin.PngInfo` Class +-------------------------------------- .. autoclass:: PIL.PngImagePlugin.PngInfo :members: @@ -125,27 +116,18 @@ can be found here. :show-inheritance: -:mod:`TarIO` Module -------------------- +:mod:`~PIL.TarIO` Module +------------------------ .. automodule:: PIL.TarIO :members: :undoc-members: :show-inheritance: -:mod:`WalImageFile` Module --------------------------- +:mod:`~PIL.WalImageFile` Module +------------------------------- .. automodule:: PIL.WalImageFile :members: :undoc-members: :show-inheritance: - -:mod:`_binary` Module ---------------------- - -.. automodule:: PIL._binary - :members: - :undoc-members: - :show-inheritance: - diff --git a/docs/about.rst b/docs/about.rst index ce6537e14..acd361fa8 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -10,7 +10,7 @@ The fork author's goal is to foster and support active development of PIL throug - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ -.. _Travis CI: https://travis-ci.org/python-pillow/Pillow +.. _Travis CI: https://travis-ci.com/github/python-pillow/Pillow .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions .. _GitHub: https://github.com/python-pillow/Pillow @@ -19,7 +19,7 @@ The fork author's goal is to foster and support active development of PIL throug License ------- -Like PIL, Pillow is `licensed under the open source PIL Software License `_ +Like PIL, Pillow is `licensed under the open source HPND License `_ Why a fork? ----------- @@ -36,10 +36,4 @@ What about PIL? Prior to Pillow 2.0.0, very few image code changes were made. Pillow 2.0.0 added Python 3 support and includes many bug fixes from many contributors. -As more time passes since the last PIL release (1.1.7 in 2009), the likelihood of a new PIL release decreases. However, we've yet to hear an official "PIL is dead" announcement. So if you still want to support PIL, please `report issues here first`_, then `open corresponding Pillow tickets here`_. - -.. _report issues here first: https://bitbucket.org/effbot/pil-2009-raclette/issues - -.. _open corresponding Pillow tickets here: https://github.com/python-pillow/Pillow/issues - -Please provide a link to the first ticket so we can track the issue(s) upstream. +As more time passes since the last PIL release (1.1.7 in 2009), the likelihood of a new PIL release decreases. However, we've yet to hear an official "PIL is dead" announcement. diff --git a/docs/conf.py b/docs/conf.py index 66effdfb2..b5f8ad59d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,18 +16,26 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) -import PIL import sphinx_rtd_theme +import PIL + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' +needs_sphinx = "2.4" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.intersphinx"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinx_removed_in", +] + +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: @@ -42,7 +50,7 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" -copyright = "1995-2011 Fredrik Lundh, 2010-2019 Alex Clark and Contributors" +copyright = "1995-2011 Fredrik Lundh, 2010-2020 Alex Clark and Contributors" author = "Fredrik Lundh, Alex Clark and Contributors" # The version info for the project you're documenting, acts as replacement for @@ -69,7 +77,7 @@ language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ["_build"] +exclude_patterns = ["_build", "releasenotes/template.rst"] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -98,6 +106,17 @@ pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False +# If true, Sphinx will warn about all references where the target cannot be found. +# Default is False. You can activate this mode temporarily using the -n command-line +# switch. +nitpicky = True + +# A list of (type, target) tuples (by default empty) that should be ignored when +# generating warnings in “nitpicky mode”. Note that type should include the domain name +# if present. Example entries would be ('py:func', 'int') or +# ('envvar', 'LD_LIBRARY_PATH'). +# nitpick_ignore = [] + # -- Options for HTML output ---------------------------------------------- @@ -129,7 +148,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -# html_favicon = None +html_favicon = "resources/favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -289,4 +308,5 @@ texinfo_documents = [ def setup(app): - app.add_javascript("js/script.js") + app.add_js_file("js/script.js") + app.add_css_file("css/dark.css") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a9b534dca..e3ad2a9e3 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,35 +12,42 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -Python 2.7 -~~~~~~~~~~ +Image.show command parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 6.0.0 +.. deprecated:: 7.2.0 -Python 2.7 reaches end-of-life on 2020-01-01. +The ``command`` parameter was deprecated and will be removed in a future release. +Use a subclass of :py:class:`.ImageShow.Viewer` instead. -Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python 2.7, making -Pillow 6.x the last series to support Python 2. +Image._showxv +~~~~~~~~~~~~~ -ImageCms.CmsProfile attributes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. deprecated:: 7.2.0 -.. deprecated:: 3.2.0 +``Image._showxv`` has been deprecated. Use :py:meth:`.Image.Image.show` +instead. If custom behaviour is required, use :py:func:`.ImageShow.register` to add +a custom :py:class:`.ImageShow.Viewer` class. -Some attributes in ``ImageCms.CmsProfile`` are deprecated. From 6.0.0, they issue a -``DeprecationWarning``: +ImageFile.raise_ioerror +~~~~~~~~~~~~~~~~~~~~~~~ -======================== =============================== -Deprecated Use instead -======================== =============================== -``color_space`` Padded ``xcolor_space`` -``pcs`` Padded ``connection_space`` -``product_copyright`` Unicode ``copyright`` -``product_desc`` Unicode ``profile_description`` -``product_description`` Unicode ``profile_description`` -``product_manufacturer`` Unicode ``manufacturer`` -``product_model`` Unicode ``model`` -======================== =============================== +.. deprecated:: 7.2.0 + +``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror`` +is now deprecated and will be removed in a future release. Use +``ImageFile.raise_oserror`` instead. + +PILLOW_VERSION constant +~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.2.0 + +``PILLOW_VERSION`` has been deprecated and will be removed in a future release. Use +``__version__`` instead. + +It was initially removed in Pillow 7.0.0, but brought back in 7.1.0 to give projects +more time to upgrade. Removed features ---------------- @@ -48,10 +55,69 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +im.offset +~~~~~~~~~ + +.. deprecated:: 1.1.2 +.. versionremoved:: 8.0.0 + +``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead. + +It was documented as deprecated in PIL 1.1.2, +raised a ``DeprecationWarning`` since 1.1.5, +an ``Exception`` since Pillow 3.0.0 +and ``NotImplementedError`` since 3.3.0. + +Image.fromstring, im.fromstring and im.tostring +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 2.0.0 +.. versionremoved:: 8.0.0 + +* ``Image.fromstring()`` has been removed, call :py:func:`.Image.frombytes()` instead. +* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead. +* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead. + +They issued a ``DeprecationWarning`` since 2.0.0, +an ``Exception`` since 3.0.0 +and ``NotImplementedError`` since 3.3.0. + +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 ``DeprecationWarning``: + +======================== =================================================== + +Removed Use instead +======================== =================================================== +``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space` +``pcs`` Padded :py:attr:`~.CmsProfile.connection_space` +``product_copyright`` Unicode :py:attr:`~.CmsProfile.copyright` +``product_desc`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_description`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_manufacturer`` Unicode :py:attr:`~.CmsProfile.manufacturer` +``product_model`` Unicode :py:attr:`~.CmsProfile.model` +======================== =================================================== + +Python 2.7 +~~~~~~~~~~ + +.. deprecated:: 6.0.0 +.. versionremoved:: 7.0.0 + +Python 2.7 reached end-of-life on 2020-01-01. Pillow 6.x was the last series to +support Python 2. + Image.__del__ ~~~~~~~~~~~~~ -*Removed in version 7.0.0.* +.. deprecated:: 6.1.0 +.. versionremoved:: 7.0.0 Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. Use a context manager or call ``Image.close()`` instead to close the file in a @@ -71,17 +137,11 @@ Use instead: with Image.open("hopper.png") as im: im.save("out.jpg") -PILLOW_VERSION constant -~~~~~~~~~~~~~~~~~~~~~~~ - -*Removed in version 7.0.0.* - -``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. - PIL.*ImagePlugin.__version__ attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*Removed in version 7.0.0.* +.. deprecated:: 6.0.0 +.. versionremoved:: 7.0.0 The version constants of individual plugins have been removed. Use ``PIL.__version__`` instead. @@ -106,7 +166,8 @@ Removed Removed Removed PyQt4 and PySide ~~~~~~~~~~~~~~~~ -*Removed in version 7.0.0.* +.. deprecated:: 6.0.0 +.. versionremoved:: 7.0.0 Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since 2018-08-31 and PySide since 2015-10-14. @@ -117,7 +178,8 @@ or PySide2. Setting the size of TIFF images ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*Removed in version 7.0.0.* +.. deprecated:: 5.3.0 +.. versionremoved:: 7.0.0 Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws an error. Use ``Image.resize`` instead. @@ -125,7 +187,8 @@ an error. Use ``Image.resize`` instead. VERSION constant ~~~~~~~~~~~~~~~~ -*Removed in version 6.0.0.* +.. deprecated:: 5.2.0 +.. versionremoved:: 6.0.0 ``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use ``__version__`` instead. @@ -133,7 +196,8 @@ VERSION constant Undocumented ImageOps functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*Removed in version 6.0.0.* +.. deprecated:: 4.3.0 +.. versionremoved:: 6.0.0 Several undocumented functions in ``ImageOps`` have been removed. Use the equivalents in ``ImageFilter`` instead: @@ -151,9 +215,10 @@ Removed Use instead PIL.OleFileIO ~~~~~~~~~~~~~ -*Removed in version 6.0.0.* +.. deprecated:: 4.0.0 +.. versionremoved:: 6.0.0 PIL.OleFileIO was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of the upstream olefile Python package, and replaced with an ``ImportError`` in 5.0.0 (2018-01). The deprecated file has now been removed from Pillow. If needed, install from -PyPI (eg. ``pip install olefile``). +PyPI (eg. ``python3 -m pip install olefile``). diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 45f63f164..78aa3ce72 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -212,10 +212,10 @@ class DdsImageFile(ImageFile.ImageFile): def _open(self): magic, header_size = struct.unpack("`_ for details. + library before building the Python Imaging Library. See the + :doc:`installation documentation <../installation>` for details. + +.. _apng-sequences: + +APNG sequences +~~~~~~~~~~~~~~ + +The PNG loader includes limited support for reading and writing Animated Portable +Network Graphics (APNG) files. +When an APNG file is loaded, :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` +will return ``"image/apng"``. The value of the :py:attr:`~PIL.Image.Image.is_animated` +property will be ``True`` when the :py:attr:`~PIL.Image.Image.n_frames` property is +greater than 1. For APNG files, the ``n_frames`` property depends on both the animation +frame count as well as the presence or absence of a default image. See the +``default_image`` property documentation below for more details. +The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods +are supported. + +``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame. + +These :py:attr:`~PIL.Image.Image.info` properties will be set for APNG frames, +where applicable: + +**default_image** + Specifies whether or not this APNG file contains a separate default image, + which is not a part of the actual APNG animation. + + When an APNG file contains a default image, the initially loaded image (i.e. + the result of ``seek(0)``) will be the default image. + To account for the presence of the default image, the + :py:attr:`~PIL.Image.Image.n_frames` property will be set to ``frame_count + 1``, + where ``frame_count`` is the actual APNG animation frame count. + To load the first APNG animation frame, ``seek(1)`` must be called. + + * ``True`` - The APNG contains default image, which is not an animation frame. + * ``False`` - The APNG does not contain a default image. The ``n_frames`` property + will be set to the actual APNG animation frame count. + The initially loaded image (i.e. ``seek(0)``) will be the first APNG animation + frame. + +**loop** + The number of times to loop this APNG, 0 indicates infinite looping. + +**duration** + The time to display this APNG frame (in milliseconds). + +.. note:: + + The APNG loader returns images the same size as the APNG file's logical screen size. + The returned image contains the pixel data for a given frame, after applying + any APNG frame disposal and frame blend operations (i.e. it contains what a web + browser would render for this frame - the composite of all previous frames and this + frame). + + Any APNG file containing sequence errors is treated as an invalid image. The APNG + loader will not attempt to repair and reorder files containing sequence errors. + +.. _apng-saving: + +Saving +~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save`, by default only a single frame PNG file +will be saved. To save an APNG file (including a single frame APNG), the ``save_all`` +parameter must be set to ``True``. The following parameters can also be set: + +**default_image** + Boolean value, specifying whether or not the base image is a default image. + If ``True``, the base image will be used as the default image, and the first image + from the ``append_images`` sequence will be the first APNG animation frame. + If ``False``, the base image will be used as the first APNG animation frame. + Defaults to ``False``. + +**append_images** + A list or tuple of images to append as additional frames. Each of the + images in the list can be single or multiframe images. The size of each frame + should match the size of the base image. Also note that if a frame's mode does + not match that of the base image, the frame will be converted to the base image + mode. + +**loop** + Integer number of times to loop this APNG, 0 indicates infinite looping. + Defaults to 0. + +**duration** + Integer (or list or tuple of integers) length of time to display this APNG frame + (in milliseconds). + Defaults to 0. + +**disposal** + An integer (or list or tuple of integers) specifying the APNG disposal + operation to be used for this frame before rendering the next frame. + Defaults to 0. + + * 0 (:py:data:`~PIL.PngImagePlugin.APNG_DISPOSE_OP_NONE`, default) - + No disposal is done on this frame before rendering the next frame. + * 1 (:py:data:`PIL.PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`) - + This frame's modified region is cleared to fully transparent black before + rendering the next frame. + * 2 (:py:data:`~PIL.PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`) - + This frame's modified region is reverted to the previous frame's contents before + rendering the next frame. + +**blend** + An integer (or list or tuple of integers) specifying the APNG blend + operation to be used for this frame before rendering the next frame. + Defaults to 0. + + * 0 (:py:data:`~PIL.PngImagePlugin.APNG_BLEND_OP_SOURCE`) - + All color components of this frame, including alpha, overwrite the previous output + image contents. + * 1 (:py:data:`~PIL.PngImagePlugin.APNG_BLEND_OP_OVER`) - + This frame should be alpha composited with the previous output image contents. + +.. note:: + + The ``duration``, ``disposal`` and ``blend`` parameters can be set to lists or tuples to + specify values for each individual frame in the animation. The length of the list or tuple + must be identical to the total number of actual frames in the APNG animation. + If the APNG contains a default image (i.e. ``default_image`` is set to ``True``), + these list or tuple parameters should not include an entry for the default image. + PPM ^^^ @@ -573,7 +707,7 @@ Pillow also reads SPIDER stack files containing sequences of SPIDER images. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods are supported, and random access is allowed. -The :py:meth:`~PIL.Image.Image.open` method sets the following attributes: +The :py:meth:`~PIL.Image.open` method sets the following attributes: **format** Set to ``SPIDER`` @@ -584,8 +718,8 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following attributes: **n_frames** Set to the number of images in the stack. -A convenience method, :py:meth:`~PIL.Image.Image.convert2byte`, is provided for -converting floating point data to byte data (mode ``L``):: +A convenience method, :py:meth:`~PIL.SpiderImagePlugin.SpiderImageFile.convert2byte`, +is provided for converting floating point data to byte data (mode ``L``):: im = Image.open('image001.spi').convert2byte() @@ -626,7 +760,7 @@ uncompressed files. support for reading Packbits, LZW and JPEG compressed TIFFs without using libtiff. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **compression** @@ -636,8 +770,8 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following **dpi** Image resolution as an ``(xdpi, ydpi)`` tuple, where applicable. You can use - the :py:attr:`~PIL.Image.Image.tag` attribute to get more detailed - information about the image resolution. + the :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag` attribute to get more + detailed information about the image resolution. .. versionadded:: 1.1.5 @@ -648,9 +782,9 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following .. versionadded:: 1.1.5 -The :py:attr:`~PIL.Image.Image.tag_v2` attribute contains a dictionary -of TIFF metadata. The keys are numerical indexes from -:py:attr:`~PIL.TiffTags.TAGS_V2`. Values are strings or numbers for single +The :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag_v2` attribute contains a +dictionary of TIFF metadata. The keys are numerical indexes from +:py:data:`.TiffTags.TAGS_V2`. Values are strings or numbers for single items, multiple values are returned in a tuple of values. Rational numbers are returned as a :py:class:`~PIL.TiffImagePlugin.IFDRational` object. @@ -658,8 +792,8 @@ object. .. versionadded:: 3.0.0 For compatibility with legacy code, the -:py:attr:`~PIL.Image.Image.tag` attribute contains a dictionary of -decoded TIFF fields as returned prior to version 3.0.0. Values are +:py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag` attribute contains a dictionary +of decoded TIFF fields as returned prior to version 3.0.0. Values are returned as either strings or tuples of numeric values. Rational numbers are returned as a tuple of ``(numerator, denominator)``. @@ -703,7 +837,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum object and setting the type in :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` with the appropriate numerical value from - ``TiffTags.TYPES``. + :py:data:`.TiffTags.TYPES`. .. versionadded:: 2.3.0 @@ -720,7 +854,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum Previous versions only supported some tags when writing using libtiff. The supported list is found in - :py:attr:`~PIL:TiffTags.LIBTIFF_CORE`. + :py:data:`.TiffTags.LIBTIFF_CORE`. .. versionadded:: 6.1.0 @@ -733,7 +867,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum **compression** A string containing the desired compression method for the file. (valid only with libtiff installed) Valid compression - methods are: ``None``, ``"tiff_ccitt"``, ``"group3"``, + methods are: :data:`None`, ``"tiff_ccitt"``, ``"group3"``, ``"group4"``, ``"tiff_jpeg"``, ``"tiff_adobe_deflate"``, ``"tiff_thunderscan"``, ``"tiff_deflate"``, ``"tiff_sgilog"``, ``"tiff_sgilog24"``, ``"tiff_raw_16"`` @@ -813,9 +947,10 @@ Saving sequences library is v0.5.0 or later. You can check webp animation support at runtime by calling ``features.check("webp_anim")``. -When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, the -following options are available when the ``save_all`` argument is present and -true. +When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +options will also be available. **append_images** A list of images to append as additional frames. Each of the @@ -897,7 +1032,7 @@ FLI, FLC Pillow reads Autodesk FLI and FLC animations. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **duration** @@ -930,7 +1065,7 @@ GBR The GBR decoder reads GIMP brush files, version 1 and 2. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **comment** @@ -945,7 +1080,7 @@ GD Pillow reads uncompressed GD2 files. Note that you must use :py:func:`PIL.GdImageFile.open` to read such a file. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **transparency** @@ -1019,12 +1154,49 @@ this format. By default, a Quake2 standard palette is attached to the texture. To override the palette, use the putpalette method. +WMF +^^^ + +Pillow can identify WMF files. + +On Windows, it can read WMF files. By default, it will load the image at 72 +dpi. To load it at another resolution: + +.. code-block:: python + + from PIL import Image + with Image.open("drawing.wmf") as im: + im.load(dpi=144) + +To add other read or write support, use +:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF handler. + +.. code-block:: python + + from PIL import Image + from PIL import WmfImagePlugin + + class WmfHandler: + def open(self, im): + ... + def load(self, im): + ... + return image + def save(self, im, fp, filename): + ... + + wmf_handler = WmfHandler() + + WmfImagePlugin.register_handler(wmf_handler) + + im = Image.open("sample.wmf") + XPM ^^^ Pillow reads X pixmap files (mode ``P``) with 256 colors or less. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **transparency** @@ -1065,7 +1237,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum **append** Set to True to append pages to an existing PDF file. If the file doesn't - exist, an :py:exc:`IOError` will be raised. + exist, an :py:exc:`OSError` will be raised. .. versionadded:: 5.1.0 @@ -1176,35 +1348,3 @@ MPEG ^^^^ Pillow identifies MPEG files. - -WMF -^^^ - -Pillow can identify playable WMF files. - -In PIL 1.1.4 and earlier, the WMF driver provides some limited rendering -support, but not enough to be useful for any real application. - -In PIL 1.1.5 and later, the WMF driver is a stub driver. To add WMF read or -write support to your application, use -:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF handler. - -:: - - from PIL import Image - from PIL import WmfImagePlugin - - class WmfHandler: - def open(self, im): - ... - def load(self, im): - ... - return image - def save(self, im, fp, filename): - ... - - wmf_handler = WmfHandler() - - WmfImagePlugin.register_handler(wmf_handler) - - im = Image.open("sample.wmf") diff --git a/docs/handbook/text-anchors.rst b/docs/handbook/text-anchors.rst new file mode 100644 index 000000000..0aecd3483 --- /dev/null +++ b/docs/handbook/text-anchors.rst @@ -0,0 +1,140 @@ + +.. _text-anchors: + +Text anchors +============ + +The ``anchor`` parameter determines the alignment of drawn text relative to the ``xy`` parameter. +The default alignment is top left, specifically ``la`` (left-ascender) for horizontal text +and ``lt`` (left-top) for vertical text. + +This parameter is only supported by OpenType/TrueType fonts. +Other fonts may ignore the parameter and use the default (top left) alignment. + +Specifying an anchor +^^^^^^^^^^^^^^^^^^^^ + +An anchor is specified with a two-character string. The first character is the +horizontal alignment, the second character is the vertical alignment. +For example, the default value of ``la`` for horizontal text means left-ascender +aligned text. + +When drawing text with :py:meth:`PIL.ImageDraw.ImageDraw.text` with a specific anchor, +the text will be placed such that the specified anchor point is at the ``xy`` coordinates. + +For example, in the following image, the text is ``ms`` (middle-baseline) aligned, with +``xy`` at the intersection of the two lines: + +.. image:: ../../Tests/images/test_anchor_quick_ms.png + :alt: ms (middle-baseline) aligned text. + :align: left + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + im = Image.new("RGB", (200, 200), "white") + d = ImageDraw.Draw(im) + d.line(((0, 100), (200, 100)), "gray") + d.line(((100, 0), (100, 200)), "gray") + d.text((100, 100), "Quick", fill="black", anchor="ms", font=font) + +.. container:: clearer + + | + +.. only: comment + The container above prevents the image alignment from affecting the following text. + +Quick reference +^^^^^^^^^^^^^^^ + +.. image:: ../resources/anchor_horizontal.svg + :alt: Horizontal text + :align: center + +.. image:: ../resources/anchor_vertical.svg + :alt: Vertical text + :align: center + +Horizontal anchor alignment +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``l`` --- left + Anchor is to the left of the text. + + For *horizontal* text this is the origin of the first glyph, as shown in the `FreeType tutorial`_. + +``m`` --- middle + Anchor is horizontally centered with the text. + + For *vertical* text it is recommended to use ``s`` (baseline) alignment instead, + as it does not change based on the specific glyphs of the given text. + +``r`` --- right + Anchor is to the right of the text. + + For *horizontal* text this is the advanced origin of the last glyph, as shown in the `FreeType tutorial`_. + +``s`` --- baseline *(vertical text only)* + Anchor is at the baseline (middle) of the text. The exact alignment depends on the font. + + For *vertical* text this is the recommended alignment, + as it does not change based on the specific glyphs of the given text + (see image for vertical text above). + +Vertical anchor alignment +^^^^^^^^^^^^^^^^^^^^^^^^^ + +``a`` --- ascender / top *(horizontal text only)* + Anchor is at the ascender line (top) of the first line of text, as defined by the font. + + See `Font metrics on Wikipedia`_ for more information. + +``t`` --- top *(single-line text only)* + Anchor is at the top of the text. + + For *vertical* text this is the origin of the first glyph, as shown in the `FreeType tutorial`_. + + For *horizontal* text it is recommended to use ``a`` (ascender) alignment instead, + as it does not change based on the specific glyphs of the given text. + +``m`` --- middle + Anchor is vertically centered with the text. + + For *horizontal* text this is the midpoint of the first ascender line and the last descender line. + +``s`` --- baseline *(horizontal text only)* + Anchor is at the baseline (bottom) of the first line of text, only descenders extend below the anchor. + + See `Font metrics on Wikipedia`_ for more information. + +``b`` --- bottom *(single-line text only)* + Anchor is at the bottom of the text. + + For *vertical* text this is the advanced origin of the last glyph, as shown in the `FreeType tutorial`_. + + For *horizontal* text it is recommended to use ``d`` (descender) alignment instead, + as it does not change based on the specific glyphs of the given text. + +``d`` --- descender / bottom *(horizontal text only)* + Anchor is at the descender line (bottom) of the last line of text, as defined by the font. + + See `Font metrics on Wikipedia`_ for more information. + +Examples +^^^^^^^^ + +The following image shows several examples of anchors for horizontal text. +In each section the ``xy`` parameter was set to the center shown by the intersection +of the two lines. + +.. comment: Image generated with ../example/anchors.py + +.. image:: ../example/anchors.png + :alt: Text anchor examples + :align: center + +.. _Font metrics on Wikipedia: https://en.wikipedia.org/wiki/Typeface#Font_metrics +.. _FreeType tutorial: https://freetype.org/freetype2/docs/tutorial/step2.html diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 63030de21..6b68a0562 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -29,7 +29,7 @@ bands in the image, and also the pixel type and depth. Common modes are “L” (luminance) for greyscale images, “RGB” for true color images, and “CMYK” for pre-press images. -If the file cannot be opened, an :py:exc:`IOError` exception is raised. +If the file cannot be opened, an :py:exc:`OSError` exception is raised. Once you have an instance of the :py:class:`~PIL.Image.Image` class, you can use the methods defined by this class to process and manipulate the image. For @@ -74,8 +74,9 @@ Convert files to JPEG outfile = f + ".jpg" if infile != outfile: try: - Image.open(infile).save(outfile) - except IOError: + with Image.open(infile) as im: + im.save(outfile) + except OSError: print("cannot convert", infile) A second argument can be supplied to the :py:meth:`~PIL.Image.Image.save` @@ -99,7 +100,7 @@ Create JPEG thumbnails with Image.open(infile) as im: im.thumbnail(size) im.save(outfile, "JPEG") - except IOError: + except OSError: print("cannot create thumbnail for", infile) It is important to note that the library doesn’t decode or load the raster data @@ -123,8 +124,8 @@ Identify Image Files for infile in sys.argv[1:]: try: with Image.open(infile) as im: - print(infile, im.format, "%dx%d" % im.size, im.mode) - except IOError: + print(infile, im.format, f"{im.size}x{im.mode}") + except OSError: pass Cutting, pasting, and merging images @@ -405,13 +406,13 @@ Using the ImageSequence Iterator class # ...do something to frame... -Postscript printing +PostScript printing ------------------- The Python Imaging Library includes functions to print images, text and -graphics on Postscript printers. Here’s a simple example: +graphics on PostScript printers. Here’s a simple example: -Drawing Postscript +Drawing PostScript ^^^^^^^^^^^^^^^^^^ :: @@ -441,7 +442,7 @@ More on reading images As described earlier, the :py:func:`~PIL.Image.open` function of the :py:mod:`~PIL.Image` module is used to open an image file. In most cases, you -simply pass it the filename as an argument. ``Image.open()`` can be used a +simply pass it the filename as an argument. ``Image.open()`` can be used as a context manager:: from PIL import Image @@ -449,11 +450,11 @@ context manager:: ... If everything goes well, the result is an :py:class:`PIL.Image.Image` object. -Otherwise, an :exc:`IOError` exception is raised. +Otherwise, an :exc:`OSError` exception is raised. You can use a file-like object instead of the filename. The object must -implement :py:meth:`~file.read`, :py:meth:`~file.seek` and -:py:meth:`~file.tell` methods, and be opened in binary mode. +implement ``file.read``, ``file.seek`` and ``file.tell`` methods, +and be opened in binary mode. Reading from an open file ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -464,17 +465,17 @@ Reading from an open file with open("hopper.ppm", "rb") as fp: im = Image.open(fp) -To read an image from string data, use the :py:class:`~StringIO.StringIO` +To read an image from binary data, use the :py:class:`~io.BytesIO` class: -Reading from a string -^^^^^^^^^^^^^^^^^^^^^ +Reading from binary data +^^^^^^^^^^^^^^^^^^^^^^^^ :: from PIL import Image - import StringIO - im = Image.open(StringIO.StringIO(buffer)) + import io + im = Image.open(io.BytesIO(buffer)) Note that the library rewinds the file (using ``seek(0)``) before reading the image header. In addition, seek will also be used when the image data is read diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst index 0763109ab..97cb5abe1 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-file-decoder.rst @@ -3,9 +3,9 @@ Writing Your Own Image Plugin ============================= -The Pillow uses a plug-in model which allows you to add your own +Pillow uses a plugin model which allows you to add your own decoders to the library, without any changes to the library -itself. Such plug-ins usually have names like +itself. Such plugins usually have names like :file:`XxxImagePlugin.py`, where ``Xxx`` is a unique format name (usually an abbreviation). @@ -14,11 +14,11 @@ itself. Such plug-ins usually have names like :file:`ImagePlugin.py`. You will need to import your image plugin manually. -Pillow decodes files in 2 stages: +Pillow decodes files in two stages: 1. It loops over the available image plugins in the loaded order, and - calls the plugin's ``accept`` function with the first 16 bytes of - the file. If the ``accept`` function returns true, the plugin's + calls the plugin's ``_accept`` function with the first 16 bytes of + the file. If the ``_accept`` function returns true, the plugin's ``_open`` method is called to set up the image metadata and image tiles. The ``_open`` method is not for decoding the actual image data. @@ -26,24 +26,24 @@ Pillow decodes files in 2 stages: called, which sets up a decoder for each tile and feeds the data to it. -An image plug-in should contain a format handler derived from the +An image plugin should contain a format handler derived from the :py:class:`PIL.ImageFile.ImageFile` base class. This class should -provide an :py:meth:`_open` method, which reads the file header and +provide an ``_open`` method, which reads the file header and sets up at least the :py:attr:`~PIL.Image.Image.mode` and :py:attr:`~PIL.Image.Image.size` attributes. To be able to load the -file, the method must also create a list of :py:attr:`tile` -descriptors, which contain a decoder name, extents of the tile, and +file, the method must also create a list of ``tile`` descriptors, +which contain a decoder name, extents of the tile, and any decoder-specific data. The format handler class must be explicitly registered, via a call to the :py:mod:`~PIL.Image` module. .. note:: For performance reasons, it is important that the - :py:meth:`_open` method quickly rejects files that do not have the + ``_open`` method quickly rejects files that do not have the appropriate contents. Example ------- -The following plug-in supports a simple format, which has a 128-byte header +The following plugin supports a simple format, which has a 128-byte header consisting of the words “SPAM” followed by the width, height, and pixel size in bits. The header fields are separated by spaces. The image data follows directly after the header, and can be either bi-level, greyscale, or 24-bit @@ -52,7 +52,11 @@ true color. **SpamImagePlugin.py**:: from PIL import Image, ImageFile - import string + + + def _accept(prefix): + return prefix[:4] == b"SPAM" + class SpamImageFile(ImageFile.ImageFile): @@ -61,12 +65,7 @@ true color. def _open(self): - # check header - header = self.fp.read(128) - if header[:4] != "SPAM": - raise SyntaxError, "not a SPAM file" - - header = string.split(header) + header = self.fp.read(128).split() # size in pixels (width, height) self._size = int(header[1]), int(header[2]) @@ -80,17 +79,19 @@ true color. elif bits == 24: self.mode = "RGB" else: - raise SyntaxError, "unknown number of bits" + raise SyntaxError("unknown number of bits") # data descriptor - self.tile = [ - ("raw", (0, 0) + self.size, 128, (self.mode, 0, 1)) - ] + self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] - Image.register_open(SpamImageFile.format, SpamImageFile) - Image.register_extension(SpamImageFile.format, ".spam") - Image.register_extension(SpamImageFile.format, ".spa") # dos version + Image.register_open(SpamImageFile.format, SpamImageFile, _accept) + + Image.register_extensions(SpamImageFile.format, [ + ".spam", + ".spa", # DOS version + ]) + The format handler must always set the :py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.Image.Image.mode` @@ -104,10 +105,19 @@ Note that the image plugin must be explicitly registered using :py:func:`PIL.Image.register_open`. Although not required, it is also a good idea to register any extensions used by this format. -The :py:attr:`tile` attribute ------------------------------ +Once the plugin has been imported, it can be used: -To be able to read the file as well as just identifying it, the :py:attr:`tile` +.. code-block:: python + + from PIL import Image + import SpamImagePlugin + with Image.open("hopper.spam") as im: + pass + +The ``tile`` attribute +---------------------- + +To be able to read the file as well as just identifying it, the ``tile`` attribute must also be set. This attribute consists of a list of tile descriptors, where each descriptor specifies how data should be loaded to a given region in the image. In most cases, only a single descriptor is used, @@ -133,9 +143,9 @@ The fields are used as follows: **parameters** Parameters to the decoder. The contents of this field depends on the decoder specified by the first field in the tile descriptor tuple. If the - decoder doesn’t need any parameters, use None for this field. + decoder doesn’t need any parameters, use :data:`None` for this field. -Note that the :py:attr:`tile` attribute contains a list of tile descriptors, +Note that the ``tile`` attribute contains a list of tile descriptors, not just a single descriptor. Decoders @@ -149,6 +159,8 @@ can be used with most uncompressed file formats, such as PPM, BMP, uncompressed TIFF, and many others. To use the raw decoder with the :py:func:`PIL.Image.frombytes` function, use the following syntax:: +.. code-block:: python + image = Image.frombytes( mode, size, data, "raw", raw mode, stride, orientation @@ -176,11 +188,11 @@ The fields are used as follows: The **raw mode** field is used to determine how the data should be unpacked to match PIL’s internal pixel layout. PIL supports a large set of raw modes; for a -complete list, see the table in the :py:mod:`Unpack.c` module. The following +complete list, see the table in the :file:`Unpack.c` module. The following table describes some commonly used **raw modes**: +-----------+-----------------------------------------------------------------+ -| mode | description | +| mode | description | +===========+=================================================================+ | ``1`` | 1-bit bilevel, stored with the leftmost pixel in the most | | | significant bit. 0 means black, 1 means white. | @@ -212,7 +224,7 @@ Note that for the most common cases, the raw mode is simply the same as the mode The Python Imaging Library supports many other decoders, including JPEG, PNG, and PackBits. For details, see the :file:`decode.c` source file, and the -standard plug-in implementations provided with the library. +standard plugin implementations provided with the library. Decoding floating point data ---------------------------- @@ -224,7 +236,7 @@ You can use the ``raw`` decoder to read images where data is packed in any standard machine data type, using one of the following raw modes: ============ ======================================= -mode description +mode description ============ ======================================= ``F`` 32-bit native floating point. ``F;8`` 8-bit unsigned integer. @@ -259,6 +271,8 @@ image memory. To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use the following syntax:: +.. code-block:: python + image = Image.frombytes( mode, size, data, "bit", bits, pad, fill, sign, orientation @@ -351,7 +365,7 @@ interest in this object are: The target image, will be set by Pillow. **state** - An ImagingCodecStateInstance, will be set by Pillow. The **context** + An ImagingCodecStateInstance, will be set by Pillow. The ``context`` member is an opaque struct that can be used by the decoder to store any format specific state or options. @@ -415,4 +429,3 @@ Python-based file decoder: 3. Cleanup: The decoder instance's ``cleanup`` method is called. - diff --git a/docs/index.rst b/docs/index.rst index 034da6eed..eafcb1fec 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,27 +3,52 @@ Pillow Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. -Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. - -.. image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg - :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow +Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. .. image:: https://readthedocs.org/projects/pillow/badge/?version=latest :target: https://pillow.readthedocs.io/?badge=latest :alt: Documentation Status -.. image:: https://img.shields.io/travis/python-pillow/Pillow/master.svg?label=Linux%20build - :target: https://travis-ci.org/python-pillow/Pillow +.. image:: https://img.shields.io/travis/com/python-pillow/Pillow/master.svg?label=Linux%20build + :target: https://travis-ci.com/github/python-pillow/Pillow :alt: Travis CI build status (Linux) -.. image:: https://img.shields.io/travis/python-pillow/pillow-wheels/master.svg?label=macOS%20build - :target: https://travis-ci.org/python-pillow/pillow-wheels +.. image:: https://img.shields.io/travis/com/python-pillow/pillow-wheels/master.svg?label=macOS%20build + :target: https://travis-ci.com/github/python-pillow/pillow-wheels :alt: Travis CI build status (macOS) -.. image:: https://img.shields.io/appveyor/ci/python-pillow/Pillow/master.svg?label=Windows%20build +.. image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/master.svg?label=Windows%20build :target: https://ci.appveyor.com/project/python-pillow/Pillow :alt: AppVeyor CI build status (Windows) +.. image:: https://github.com/python-pillow/Pillow/workflows/Lint/badge.svg + :target: https://github.com/python-pillow/Pillow/actions?query=workflow%3ALint + :alt: GitHub Actions build status (Lint) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg + :target: https://github.com/python-pillow/Pillow/actions?query=workflow%3A%22Test+Docker%22 + :alt: GitHub Actions build status (Test Docker) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Test/badge.svg + :target: https://github.com/python-pillow/Pillow/actions?query=workflow%3ATest + :alt: GitHub Actions build status (Test Linux and macOS) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Windows/badge.svg + :target: https://github.com/python-pillow/Pillow/actions?query=workflow%3A%22Test+Windows%22 + :alt: GitHub Actions build status (Test Windows) + +.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg + :target: https://codecov.io/gh/python-pillow/Pillow + :alt: Code coverage + +.. image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg + :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow + :alt: Zenodo + +.. image:: https://tidelift.com/badges/package/pypi/Pillow?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge + :alt: Tidelift + .. image:: https://img.shields.io/pypi/v/pillow.svg :target: https://pypi.org/project/Pillow/ :alt: Latest PyPI version @@ -32,9 +57,14 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more = 2.1.0 no longer supports "import _imaging". Please use "from PIL.Image import core as _imaging" instead. -Notes ------ +Python Support +-------------- -.. note:: Pillow is supported on the following Python versions +Pillow supports these Python versions. -+-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|**Python** |**2.4**|**2.5**|**2.6**|**2.7**|**3.2**|**3.3**|**3.4**|**3.5**|**3.6**|**3.7**|**3.8**| -+-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow < 2 | Yes | Yes | Yes | Yes | | | | | | | | -+-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 2 - 3 | | | Yes | Yes | Yes | Yes | Yes | Yes | | | | -+-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 4 | | | | Yes | | Yes | Yes | Yes | Yes | | | -+-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 5.0 - 5.1 | | | | Yes | | | Yes | Yes | Yes | | | -+-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 5.2 - 5.4 | | | | Yes | | | Yes | Yes | Yes | Yes | | -+-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 6.0 - 6.2.0 | | | | Yes | | | | Yes | Yes | Yes | | -+-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 6.2.1 | | | | Yes | | | | Yes | Yes | Yes | Yes | -+-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow >= 7 | | | | | | | | Yes | Yes | Yes | Yes | -+-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ ++----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +| **Python** |**3.9**|**3.8**|**3.7**|**3.6**|**3.5**|**3.4**|**3.3**|**3.2**|**2.7**|**2.6**|**2.5**|**2.4**| ++----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +| Pillow >= 8.0 | Yes | Yes | Yes | Yes | | | | | | | | | ++----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +| Pillow 7.0 - 7.2 | | Yes | Yes | Yes | Yes | | | | | | | | ++----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +| Pillow 6.2.1 - 6.2.2 | | Yes | Yes | Yes | Yes | | | | Yes | | | | ++----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +| Pillow 6.0 - 6.2.0 | | | Yes | Yes | Yes | | | | Yes | | | | ++----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +| Pillow 5.2 - 5.4 | | | Yes | Yes | Yes | Yes | | | Yes | | | | ++----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +| Pillow 5.0 - 5.1 | | | | Yes | Yes | Yes | | | Yes | | | | ++----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +| Pillow 4 | | | | Yes | Yes | Yes | Yes | | Yes | | | | ++----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +| Pillow 2 - 3 | | | | | Yes | Yes | Yes | Yes | Yes | Yes | | | ++----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +| Pillow < 2 | | | | | | | | | Yes | Yes | Yes | Yes | ++----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ Basic Installation ------------------ @@ -46,18 +48,22 @@ Basic Installation Install Pillow with :command:`pip`:: - $ pip install Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow Windows Installation ^^^^^^^^^^^^^^^^^^^^ We provide Pillow binaries for Windows compiled for the matrix of -supported Pythons in both 32 and 64-bit versions in wheel, egg, and -executable installers. These binaries have all of the optional -libraries included except for raqm and libimagequant:: +supported Pythons in both 32 and 64-bit versions in the wheel format. +These binaries have all of the optional libraries included except +for raqm, libimagequant, and libxcb:: - > pip install Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + +To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. macOS Installation @@ -65,10 +71,11 @@ macOS Installation We provide binaries for macOS for each of the supported Python versions in the wheel format. These include support for all optional -libraries except libimagequant. Raqm support requires libraqm, -fribidi, and harfbuzz to be installed separately:: +libraries except libimagequant and libxcb. Raqm support requires +libraqm, fribidi, and harfbuzz to be installed separately:: - $ pip install Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow Linux Installation ^^^^^^^^^^^^^^^^^^ @@ -78,7 +85,8 @@ versions in the manylinux wheel format. These include support for all optional libraries except libimagequant. Raqm support requires libraqm, fribidi, and harfbuzz to be installed separately:: - $ pip install Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow Most major Linux distributions, including Fedora, Debian/Ubuntu and ArchLinux also include Pillow in packages that previously contained @@ -91,18 +99,17 @@ Pillow can be installed on FreeBSD via the official Ports or Packages systems: **Ports**:: - $ cd /usr/ports/graphics/py-pillow && make install clean + cd /usr/ports/graphics/py-pillow && make install clean **Packages**:: - $ pkg install py27-pillow + pkg install py36-pillow .. note:: The `Pillow FreeBSD port `_ and packages - are tested by the ports team with all supported FreeBSD versions - and against Python 2.7 and 3.x. + are tested by the ports team with all supported FreeBSD versions. Building From Source @@ -133,7 +140,7 @@ Many of Pillow's features require external libraries: * **libjpeg** provides JPEG functionality. - * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9c** and + * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and libjpeg-turbo version **8**. * Starting with Pillow 3.0.0, libjpeg is required by default, but may be disabled with the ``--disable-jpeg`` flag. @@ -145,14 +152,14 @@ Many of Pillow's features require external libraries: * **libtiff** provides compressed TIFF functionality - * Pillow has been tested with libtiff versions **3.x** and **4.0** + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.1** * **libfreetype** provides type related services * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.9**. + above uses liblcms2. Tested with **1.19** and **2.7-2.11**. * **libwebp** provides the WebP format. @@ -170,12 +177,10 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-2.12.5** + * Pillow has been tested with libimagequant **2.6-2.13.0** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. - * Windows support: Libimagequant requires VS2015/MSVC 19 to compile, - so it is unlikely to work with Python 2.7 on Windows. * **libraqm** provides complex text layout support. @@ -189,11 +194,14 @@ Many of Pillow's features require external libraries: libraqm. * libraqm is dynamically loaded in Pillow 5.0.0 and above, so support is available if all the libraries are installed. - * Windows support: Raqm support is currently unsupported on Windows. + * Windows support: Raqm is not included in prebuilt wheels + +* **libxcb** provides X11 screengrab support. Once you have installed the prerequisites, run:: - $ pip install Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow If the prerequisites are installed in the standard library locations for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no @@ -203,7 +211,7 @@ those locations by editing :file:`setup.py` or :file:`setup.cfg`, or by adding environment variables on the command line:: - $ CFLAGS="-I/usr/pkg/include" pip install pillow + CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow If Pillow has been previously built without the required prerequisites, it may be necessary to manually clear the pip cache or @@ -223,14 +231,14 @@ Build Options * Build flags: ``--disable-zlib``, ``--disable-jpeg``, ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``, - ``--disable-imagequant``. + ``--disable-imagequant``, ``--disable-xcb``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Build flags: ``--enable-zlib``, ``--enable-jpeg``, ``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``, ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``, - ``--enable-imagequant``. + ``--enable-imagequant``, ``--enable-xcb``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. @@ -247,11 +255,11 @@ Build Options Sample usage:: - $ MAX_CONCURRENCY=1 python setup.py build_ext --enable-[feature] install + MAX_CONCURRENCY=1 python3 setup.py build_ext --enable-[feature] install or using pip:: - $ pip install pillow --global-option="build_ext" --global-option="--enable-[feature]" + python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" Building on macOS @@ -267,45 +275,79 @@ tools. The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: - $ brew install libtiff libjpeg webp little-cms2 + brew install libtiff libjpeg webp little-cms2 To install libraqm on macOS use Homebrew to install its dependencies:: - $ brew install freetype harfbuzz fribidi + brew install freetype harfbuzz fribidi Then see ``depends/install_raqm_cmake.sh`` to install libraqm. Now install Pillow with:: - $ pip install Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow or from within the uncompressed source directory:: - $ python setup.py install + python3 setup.py install Building on Windows ^^^^^^^^^^^^^^^^^^^ -We don't recommend trying to build on Windows. It is a maze of twisty -passages, mostly dead ends. There are build scripts and notes for the -Windows build in the ``winbuild`` directory. +We recommend you use prebuilt wheels from PyPI. +If you wish to compile Pillow manually, you can use the build scripts +in the ``winbuild`` directory used for CI testing and development. +These scripts require Visual Studio 2017 or newer and NASM. + +Building on Windows using MSYS2/MinGW +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or +**MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. + +The following instructions target the 64-bit build, for 32-bit +replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. + +Make sure you have Python and GCC installed:: + + pacman -S \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-python3 \ + mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python3-setuptools + +Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: + + pacman -S \ + mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-zlib \ + mingw-w64-x86_64-libtiff \ + mingw-w64-x86_64-freetype \ + mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libwebp \ + mingw-w64-x86_64-openjpeg2 \ + mingw-w64-x86_64-libimagequant \ + mingw-w64-x86_64-libraqm + +Now install Pillow with:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + Building on FreeBSD ^^^^^^^^^^^^^^^^^^^ .. Note:: Only FreeBSD 10 and 11 tested -Make sure you have Python's development libraries installed.:: +Make sure you have Python's development libraries installed:: - $ sudo pkg install python2 - -Or for Python 3:: - - $ sudo pkg install python3 + sudo pkg install python3 Prerequisites are installed on **FreeBSD 10 or 11** with:: - $ sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb Then see ``depends/install_raqm_cmake.sh`` to install libraqm. @@ -318,37 +360,29 @@ development libraries installed. In Debian or Ubuntu:: - $ sudo apt-get install python-dev python-setuptools - -Or for Python 3:: - - $ sudo apt-get install python3-dev python3-setuptools + sudo apt-get install python3-dev python3-setuptools In Fedora, the command is:: - $ sudo dnf install python-devel redhat-rpm-config - -Or for Python 3:: - - $ sudo dnf install python3-devel redhat-rpm-config + sudo dnf install python3-devel redhat-rpm-config .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. -Prerequisites are installed on **Ubuntu 16.04 LTS** with:: +Prerequisites for **Ubuntu 16.04 LTS - 20.04 LTS** are installed with:: - $ sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk \ - libharfbuzz-dev libfribidi-dev + sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ + libharfbuzz-dev libfribidi-dev libxcb1-dev Then see ``depends/install_raqm.sh`` to install libraqm. -Prerequisites are installed on recent **RedHat** **Centos** or **Fedora** with:: +Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: - $ sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ + sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ - harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel + harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel -Note that the package manager may be yum or dnf, depending on the +Note that the package manager may be yum or DNF, depending on the exact distribution. See also the ``Dockerfile``\s in the Test Infrastructure repo @@ -361,8 +395,8 @@ Building on Android Basic Android support has been added for compilation within the Termux environment. The dependencies can be installed by:: - $ pkg -y install python python-dev ndk-sysroot clang make \ - libjpeg-turbo-dev + pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo This has been tested within the Termux app on ChromeOS, on x86. @@ -381,40 +415,46 @@ Continuous Integration Targets These platforms are built and tested for every change. -+----------------------------------+-------------------------------+-----------------------+ -|**Operating system** |**Tested Python versions** |**Tested Architecture**| -+----------------------------------+-------------------------------+-----------------------+ -| Alpine | 2.7, 3.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Arch | 2.7, 3.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Amazon Linux 1 | 2.7, 3.6 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Amazon Linux 2 | 2.7, 3.6 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| CentOS 6 | 2.7, 3.6 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| CentOS 7 | 2.7, 3.6 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Debian 9 Stretch | 2.7, 3.5 |x86 | -+----------------------------------+-------------------------------+-----------------------+ -| Debian 10 Buster | 2.7, 3.7 |x86 | -+----------------------------------+-------------------------------+-----------------------+ -| Fedora 30 | 2.7, 3.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Fedora 31 | 3.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| macOS 10.13 High Sierra* | 2.7, 3.5, 3.6, 3.7, 3.8 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Ubuntu Linux 16.04 LTS | 2.7, 3.5, 3.6, 3.7, 3.8, |x86-64 | -| | PyPy, PyPy3 | | -+----------------------------------+-------------------------------+-----------------------+ -| Windows Server 2012 R2 | 2.7, 3.5, 3.6, 3.7, 3.8 |x86, x86-64 | -| +-------------------------------+-----------------------+ -| | PyPy, 3.7/MinGW |x86 | -+----------------------------------+-------------------------------+-----------------------+ ++----------------------------------+--------------------------+-----------------------+ +|**Operating system** |**Tested Python versions**|**Tested architecture**| ++----------------------------------+--------------------------+-----------------------+ +| Alpine | 3.8 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Arch | 3.8 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Amazon Linux 1 | 3.6 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Amazon Linux 2 | 3.7 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| CentOS 6 | 3.6 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| CentOS 7 | 3.6 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| CentOS 8 | 3.6 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Debian 10 Buster | 3.7 |x86 | ++----------------------------------+--------------------------+-----------------------+ +| Fedora 32 | 3.8 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Fedora 33 | 3.9 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Ubuntu Linux 16.04 LTS (Xenial) | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Ubuntu Linux 18.04 LTS (Bionic) | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Ubuntu Linux 20.04 LTS (Focal) | 3.8 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Windows Server 2016 | 3.6 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Windows Server 2019 | 3.6, 3.7, 3.8, 3.9 |x86, x86-64 | +| +--------------------------+-----------------------+ +| | PyPy3 |x86 | +| +--------------------------+-----------------------+ +| | 3.8/MinGW |x86, x86-64 | ++----------------------------------+--------------------------+-----------------------+ -\* macOS CI is not run for every commit, but is run for every release. Other Platforms ^^^^^^^^^^^^^^^ @@ -429,7 +469,13 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+------------------------------+--------------------------------+-----------------------+ |**Operating system** |**Tested Python versions** |**Latest tested Pillow version**|**Tested processors** | +----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 10.14 Mojave | 2.7, 3.5, 3.6, 3.7 | 6.0.0 |x86-64 | +| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.0.1 |x86-64 | +| +------------------------------+--------------------------------+ + +| | 3.5 | 7.2.0 | | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | +| +------------------------------+--------------------------------+ + +| | 2.7 | 6.0.0 | | | +------------------------------+--------------------------------+ + | | 3.4 | 5.4.1 | | +----------------------------------+------------------------------+--------------------------------+-----------------------+ @@ -451,14 +497,14 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+------------------------------+--------------------------------+-----------------------+ | Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Ubuntu Linux 12.04 LTS | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | +| Ubuntu Linux 12.04 LTS (Precise) | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | | | PyPy5.3.1, PyPy3 v2.4.0 | | | | +------------------------------+--------------------------------+-----------------------+ | | 2.7 | 4.3.0 |x86-64 | | +------------------------------+--------------------------------+-----------------------+ | | 2.7, 3.2 | 3.4.1 |ppc | +----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Ubuntu Linux 10.04 LTS | 2.6 | 2.3.0 |x86,x86-64 | +| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ | Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ @@ -474,11 +520,13 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+------------------------------+--------------------------------+-----------------------+ | FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Windows 10 | 3.7 | 7.1.0 |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ | Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ | Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Windows 7 Pro | 2.7, 3.2, 3.3 | 3.4.1 |x86-64 | +| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ | Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ diff --git a/docs/porting.rst b/docs/porting.rst index 50b713fac..2943d72fd 100644 --- a/docs/porting.rst +++ b/docs/porting.rst @@ -3,9 +3,14 @@ Porting **Porting existing PIL-based code to Pillow** -Pillow is a functional drop-in replacement for the Python Imaging Library. To -run your existing PIL-compatible code with Pillow, it needs to be modified to -import the ``Image`` module from the ``PIL`` namespace *instead* of the +Pillow is a functional drop-in replacement for the Python Imaging Library. + +PIL is Python 2 only. Pillow dropped support for Python 2 in Pillow +7.0. So if you would like to run the latest version of Pillow, you will first +and foremost need to port your code from Python 2 to 3. + +To run your existing PIL-compatible code with Pillow, it needs to be modified +to import the ``Image`` module from the ``PIL`` namespace *instead* of the global namespace. Change this:: import Image @@ -14,7 +19,8 @@ to this:: from PIL import Image -The :py:mod:`_imaging` module has been moved. You can now import it like this:: +The :py:mod:`PIL._imaging` module has been moved to :py:mod:`PIL.Image.core`. +You can now import it like this:: from PIL.Image import core as _imaging diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 9fc7cd13b..4567d4d3e 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -1,13 +1,14 @@ .. py:module:: PIL.ExifTags .. py:currentmodule:: PIL.ExifTags -:py:mod:`ExifTags` Module -========================== +:py:mod:`~PIL.ExifTags` Module +============================== -The :py:mod:`ExifTags` module exposes two dictionaries which +The :py:mod:`~PIL.ExifTags` module exposes two dictionaries which provide constants and clear-text names for various well-known EXIF tags. -.. py:class:: PIL.ExifTags.TAGS +.. py:data:: TAGS + :type: dict The TAG dictionary maps 16-bit integer EXIF tag enumerations to descriptive string names. For instance: @@ -16,7 +17,8 @@ provide constants and clear-text names for various well-known EXIF tags. >>> TAGS[0x010e] 'ImageDescription' -.. py:class:: PIL.ExifTags.GPSTAGS +.. py:data:: GPSTAGS + :type: dict The GPSTAGS dictionary maps 8-bit integer EXIF gps enumerations to descriptive string names. For instance: diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 8af56f6c1..8d63c173b 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -1,8 +1,8 @@ .. py:module:: PIL.Image .. py:currentmodule:: PIL.Image -:py:mod:`Image` Module -====================== +:py:mod:`~PIL.Image` Module +=========================== The :py:mod:`~PIL.Image` module provides a class with the same name which is used to represent a PIL image. The module also provides a number of factory @@ -52,14 +52,22 @@ Functions .. warning:: To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files which decompress into a huge amount of data and are designed to crash or cause disruption by using up - a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the image is over a certain - limit. If desired, the warning can be turned into an error with - ``warnings.simplefilter('error', Image.DecompressionBombWarning)`` or suppressed entirely with - ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also `the logging - documentation`_ to have warnings output to the logging facility instead of stderr. + a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the number of pixels in an + image is over a certain limit, :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. - .. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb - .. _the logging documentation: https://docs.python.org/3/library/logging.html#integration-with-the-warnings-module + This threshold can be changed by setting :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. It can be disabled + by setting ``Image.MAX_IMAGE_PIXELS = None``. + + If desired, the warning can be turned into an error with + ``warnings.simplefilter('error', Image.DecompressionBombWarning)`` or suppressed entirely with + ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also + `the logging documentation`_ to have warnings output to the logging facility instead of stderr. + + If the number of pixels is greater than twice :py:data:`PIL.Image.MAX_IMAGE_PIXELS`, then a + ``DecompressionBombError`` will be raised instead. + + .. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb + .. _the logging documentation: https://docs.python.org/3/library/logging.html#integration-with-the-warnings-module Image processing ^^^^^^^^^^^^^^^^ @@ -76,9 +84,16 @@ Constructing images .. autofunction:: new .. autofunction:: fromarray .. autofunction:: frombytes -.. autofunction:: fromstring .. autofunction:: frombuffer +Generating images +^^^^^^^^^^^^^^^^^ + +.. autofunction:: effect_mandelbrot +.. autofunction:: effect_noise +.. autofunction:: linear_gradient +.. autofunction:: radial_gradient + Registering plugins ^^^^^^^^^^^^^^^^^^^ @@ -88,12 +103,14 @@ Registering plugins ignore them. .. autofunction:: register_open -.. autofunction:: register_decoder .. autofunction:: register_mime .. autofunction:: register_save -.. autofunction:: register_encoder +.. autofunction:: register_save_all .. autofunction:: register_extension - +.. autofunction:: register_extensions +.. autofunction:: registered_extensions +.. autofunction:: register_decoder +.. autofunction:: register_encoder The Image Class --------------- @@ -140,7 +157,10 @@ This crops the input image with the provided coordinates: .. automethod:: PIL.Image.Image.draft +.. automethod:: PIL.Image.Image.effect_spread +.. automethod:: PIL.Image.Image.entropy .. automethod:: PIL.Image.Image.filter +.. automethod:: PIL.Image.Image.frombytes This blurs the input image using a filter from the ``ImageFilter`` module: @@ -176,13 +196,15 @@ This helps to get the bounding box coordinates of the input image: print(im.getbbox()) # Returns four coordinates in the format (left, upper, right, lower) +.. automethod:: PIL.Image.Image.getchannel .. automethod:: PIL.Image.Image.getcolors .. automethod:: PIL.Image.Image.getdata +.. automethod:: PIL.Image.Image.getexif .. automethod:: PIL.Image.Image.getextrema .. automethod:: PIL.Image.Image.getpalette .. automethod:: PIL.Image.Image.getpixel +.. automethod:: PIL.Image.Image.getprojection .. automethod:: PIL.Image.Image.histogram -.. automethod:: PIL.Image.Image.offset .. automethod:: PIL.Image.Image.paste .. automethod:: PIL.Image.Image.point .. automethod:: PIL.Image.Image.putalpha @@ -190,6 +212,8 @@ This helps to get the bounding box coordinates of the input image: .. automethod:: PIL.Image.Image.putpalette .. automethod:: PIL.Image.Image.putpixel .. automethod:: PIL.Image.Image.quantize +.. automethod:: PIL.Image.Image.reduce +.. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.resize This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``: @@ -204,7 +228,6 @@ This resizes the given image from ``(width, height)`` to ``(width/2, height/2)`` (width, height) = (im.width // 2, im.height // 2) im_resized = im.resize((width, height)) -.. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.rotate This rotates the input image by ``theta`` degrees counter clockwise: @@ -224,16 +247,14 @@ This rotates the input image by ``theta`` degrees counter clockwise: .. automethod:: PIL.Image.Image.seek .. automethod:: PIL.Image.Image.show .. automethod:: PIL.Image.Image.split -.. automethod:: PIL.Image.Image.getchannel .. automethod:: PIL.Image.Image.tell .. automethod:: PIL.Image.Image.thumbnail .. automethod:: PIL.Image.Image.tobitmap .. automethod:: PIL.Image.Image.tobytes -.. automethod:: PIL.Image.Image.tostring .. automethod:: PIL.Image.Image.transform .. automethod:: PIL.Image.Image.transpose -This flips the input image by using the ``Image.FLIP_LEFT_RIGHT`` method. +This flips the input image by using the :data:`FLIP_LEFT_RIGHT` method. .. code-block:: python @@ -249,67 +270,59 @@ This flips the input image by using the ``Image.FLIP_LEFT_RIGHT`` method. .. automethod:: PIL.Image.Image.verify -.. automethod:: PIL.Image.Image.fromstring - .. automethod:: PIL.Image.Image.load .. automethod:: PIL.Image.Image.close -Attributes ----------- +Image Attributes +---------------- Instances of the :py:class:`Image` class have the following attributes: -.. py:attribute:: filename +.. py:attribute:: Image.filename + :type: str The filename or path of the source file. Only images created with the factory function ``open`` have a filename attribute. If the input is a file like object, the filename attribute is set to an empty string. - :type: :py:class:`string` - -.. py:attribute:: format +.. py:attribute:: Image.format + :type: Optional[str] The file format of the source file. For images created by the library itself (via a factory function, or by running a method on an existing - image), this attribute is set to ``None``. + image), this attribute is set to :data:`None`. - :type: :py:class:`string` or ``None`` - -.. py:attribute:: mode +.. py:attribute:: Image.mode + :type: str Image mode. This is a string specifying the pixel format used by the image. Typical values are “1”, “L”, “RGB”, or “CMYK.” See :ref:`concept-modes` for a full list. - :type: :py:class:`string` - -.. py:attribute:: size +.. py:attribute:: Image.size + :type: tuple[int] Image size, in pixels. The size is given as a 2-tuple (width, height). - :type: ``(width, height)`` - -.. py:attribute:: width +.. py:attribute:: Image.width + :type: int Image width, in pixels. - :type: :py:class:`int` - -.. py:attribute:: height +.. py:attribute:: Image.height + :type: int Image height, in pixels. - :type: :py:class:`int` - -.. py:attribute:: palette +.. py:attribute:: Image.palette + :type: Optional[PIL.ImagePalette.ImagePalette] Colour palette table, if any. If mode is "P" or "PA", this should be an instance of the :py:class:`~PIL.ImagePalette.ImagePalette` class. - Otherwise, it should be set to ``None``. + Otherwise, it should be set to :data:`None`. - :type: :py:class:`~PIL.ImagePalette.ImagePalette` or ``None`` - -.. py:attribute:: info +.. py:attribute:: Image.info + :type: dict A dictionary holding data associated with the image. This dictionary is used by file handlers to pass on various non-image information read from @@ -322,4 +335,177 @@ Instances of the :py:class:`Image` class have the following attributes: Unless noted elsewhere, this dictionary does not affect saving files. - :type: :py:class:`dict` +.. py:attribute:: Image.is_animated + :type: bool + + ``True`` if this image has more than one frame, or ``False`` otherwise. + + This attribute is only defined by image plugins that support animated images. + Plugins may leave this attribute undefined if they don't support loading + animated images, even if the given format supports animated images. + + Given that this attribute is not present for all images use + ``getattr(image, "is_animated", False)`` to check if Pillow is aware of multiple + frames in an image regardless of its format. + + .. seealso:: :attr:`~Image.n_frames`, :func:`~Image.seek` and :func:`~Image.tell` + +.. py:attribute:: Image.n_frames + :type: int + + The number of frames in this image. + + This attribute is only defined by image plugins that support animated images. + Plugins may leave this attribute undefined if they don't support loading + animated images, even if the given format supports animated images. + + Given that this attribute is not present for all images use + ``getattr(image, "n_frames", 1)`` to check the number of frames that Pillow is + aware of in an image regardless of its format. + + .. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell` + +Classes +------- + +.. autoclass:: PIL.Image.Exif + :members: + :undoc-members: + :show-inheritance: +.. autoclass:: PIL.Image.ImagePointHandler +.. autoclass:: PIL.Image.ImageTransformHandler + +Constants +--------- + +.. data:: NONE +.. data:: MAX_IMAGE_PIXELS + + Set to 89,478,485, approximately 0.25GB for a 24-bit (3 bpp) image. + See :py:meth:`~PIL.Image.open` for more information about how this is used. + +Transpose methods +^^^^^^^^^^^^^^^^^ + +Used to specify the :meth:`Image.transpose` method to use. + +.. data:: FLIP_LEFT_RIGHT +.. data:: FLIP_TOP_BOTTOM +.. data:: ROTATE_90 +.. data:: ROTATE_180 +.. data:: ROTATE_270 +.. data:: TRANSPOSE +.. data:: TRANSVERSE + +Transform methods +^^^^^^^^^^^^^^^^^ + +Used to specify the :meth:`Image.transform` method to use. + +.. data:: AFFINE + + Affine transform + +.. data:: EXTENT + + Cut out a rectangular subregion + +.. data:: PERSPECTIVE + + Perspective transform + +.. data:: QUAD + + Map a quadrilateral to a rectangle + +.. data:: MESH + + Map a number of source quadrilaterals in one operation + +Resampling filters +^^^^^^^^^^^^^^^^^^ + +See :ref:`concept-filters` for details. + +.. data:: NEAREST + :noindex: +.. data:: BOX + :noindex: +.. data:: BILINEAR + :noindex: +.. data:: HAMMING + :noindex: +.. data:: BICUBIC + :noindex: +.. data:: LANCZOS + :noindex: + +Some filters are also available under the following names for backwards compatibility: + +.. data:: NONE + :noindex: + :value: NEAREST +.. data:: LINEAR + :value: BILINEAR +.. data:: CUBIC + :value: BICUBIC +.. data:: ANTIALIAS + :value: LANCZOS + +Dither modes +^^^^^^^^^^^^ + +Used to specify the dithering method to use for the +:meth:`~Image.convert` and :meth:`~Image.quantize` methods. + +.. data:: NONE + :noindex: + + No dither + +.. comment: (not implemented) + .. data:: ORDERED + .. data:: RASTERIZE + +.. data:: FLOYDSTEINBERG + + Floyd-Steinberg dither + +Palettes +^^^^^^^^ + +Used to specify the pallete to use for the :meth:`~Image.convert` method. + +.. data:: WEB +.. data:: ADAPTIVE + +Quantization methods +^^^^^^^^^^^^^^^^^^^^ + +Used to specify the quantization method to use for the :meth:`~Image.quantize` method. + +.. data:: MEDIANCUT + + Median cut + +.. data:: MAXCOVERAGE + + Maximum coverage + +.. data:: FASTOCTREE + + Fast octree + +.. data:: LIBIMAGEQUANT + + libimagequant + + Check support using :py:func:`PIL.features.check_feature` + with ``feature="libimagequant"``. + +.. comment: These are not referenced anywhere? + Categories + ^^^^^^^^^^ + .. data:: NORMAL + .. data:: SEQUENCE + .. data:: CONTAINER diff --git a/docs/reference/ImageChops.rst b/docs/reference/ImageChops.rst index 6c8f11253..9519361a7 100644 --- a/docs/reference/ImageChops.rst +++ b/docs/reference/ImageChops.rst @@ -1,15 +1,15 @@ .. py:module:: PIL.ImageChops .. py:currentmodule:: PIL.ImageChops -:py:mod:`ImageChops` ("Channel Operations") Module -================================================== +:py:mod:`~PIL.ImageChops` ("Channel Operations") Module +======================================================= -The :py:mod:`ImageChops` module contains a number of arithmetical image +The :py:mod:`~PIL.ImageChops` module contains a number of arithmetical image operations, called channel operations (“chops”). These can be used for various purposes, including special effects, image compositions, algorithmic painting, and more. -For more pre-made operations, see :py:mod:`ImageOps`. +For more pre-made operations, see :py:mod:`~PIL.ImageOps`. At this time, most channel operations are only implemented for 8-bit images (e.g. “L” and “RGB”). @@ -36,12 +36,10 @@ operations in this module). .. autofunction:: PIL.ImageChops.logical_or .. autofunction:: PIL.ImageChops.logical_xor .. autofunction:: PIL.ImageChops.multiply -.. py:method:: PIL.ImageChops.offset(image, xoffset, yoffset=None) - - Returns a copy of the image where data has been offset by the given - distances. Data wraps around the edges. If **yoffset** is omitted, it - is assumed to be equal to **xoffset**. - +.. autofunction:: PIL.ImageChops.soft_light +.. autofunction:: PIL.ImageChops.hard_light +.. autofunction:: PIL.ImageChops.overlay +.. autofunction:: PIL.ImageChops.offset .. autofunction:: PIL.ImageChops.screen .. autofunction:: PIL.ImageChops.subtract .. autofunction:: PIL.ImageChops.subtract_modulo diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 922e1685a..8fed4d092 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -1,16 +1,37 @@ .. py:module:: PIL.ImageCms .. py:currentmodule:: PIL.ImageCms -:py:mod:`ImageCms` Module -========================= +:py:mod:`~PIL.ImageCms` Module +============================== -The :py:mod:`ImageCms` module provides color profile management +The :py:mod:`~PIL.ImageCms` module provides color profile management support using the LittleCMS2 color management engine, based on Kevin Cazabon's PyCMS library. -.. automodule:: PIL.ImageCms - :members: - :noindex: +.. autoclass:: ImageCmsTransform +.. autoexception:: PyCMSError + +Functions +--------- + +.. autofunction:: applyTransform +.. autofunction:: buildProofTransform +.. autofunction:: buildProofTransformFromOpenProfiles +.. autofunction:: buildTransform +.. autofunction:: buildTransformFromOpenProfiles +.. autofunction:: createProfile +.. autofunction:: getDefaultIntent +.. autofunction:: getOpenProfile +.. autofunction:: getProfileCopyright +.. autofunction:: getProfileDescription +.. autofunction:: getProfileInfo +.. autofunction:: getProfileManufacturer +.. autofunction:: getProfileModel +.. autofunction:: getProfileName +.. autofunction:: get_display_profile +.. autofunction:: isIntentSupported +.. autofunction:: profileToProfile +.. autofunction:: versions CmsProfile ---------- @@ -25,87 +46,73 @@ can be easily displayed in a chromaticity diagram, for example). .. py:class:: CmsProfile .. py:attribute:: creation_date + :type: Optional[datetime.datetime] Date and time this profile was first created (see 7.2.1 of ICC.1:2010). - :type: :py:class:`datetime.datetime` or ``None`` - .. py:attribute:: version + :type: float The version number of the ICC standard that this profile follows (e.g. ``2.0``). - :type: :py:class:`float` - .. py:attribute:: icc_version + :type: int Same as ``version``, but in encoded format (see 7.2.4 of ICC.1:2010). .. py:attribute:: device_class + :type: str 4-character string identifying the profile class. One of ``scnr``, ``mntr``, ``prtr``, ``link``, ``spac``, ``abst``, ``nmcl`` (see 7.2.5 of ICC.1:2010 for details). - :type: :py:class:`string` - .. py:attribute:: xcolor_space + :type: str 4-character string (padded with whitespace) identifying the color space, e.g. ``XYZ␣``, ``RGB␣`` or ``CMYK`` (see 7.2.6 of ICC.1:2010 for details). - Note that the deprecated attribute ``color_space`` contains an - interpreted (non-padded) variant of this (but can be empty on - unknown input). - - :type: :py:class:`string` - .. py:attribute:: connection_space + :type: str 4-character string (padded with whitespace) identifying the color space on the B-side of the transform (see 7.2.7 of ICC.1:2010 for details). - Note that the deprecated attribute ``pcs`` contains an interpreted - (non-padded) variant of this (but can be empty on unknown input). - - :type: :py:class:`string` - .. py:attribute:: header_flags + :type: int The encoded header flags of the profile (see 7.2.11 of ICC.1:2010 for details). - :type: :py:class:`int` - .. py:attribute:: header_manufacturer + :type: str 4-character string (padded with whitespace) identifying the device manufacturer, which shall match the signature contained in the appropriate section of the ICC signature registry found at www.color.org (see 7.2.12 of ICC.1:2010). - :type: :py:class:`string` - .. py:attribute:: header_model + :type: str 4-character string (padded with whitespace) identifying the device model, which shall match the signature contained in the appropriate section of the ICC signature registry found at www.color.org (see 7.2.13 of ICC.1:2010). - :type: :py:class:`string` - .. py:attribute:: attributes + :type: int Flags used to identify attributes unique to the particular device setup for which the profile is applicable (see 7.2.14 of ICC.1:2010 for details). - :type: :py:class:`int` - .. py:attribute:: rendering_intent + :type: int The rendering intent to use when combining this profile with another profile (usually overridden at run-time, but provided here @@ -114,84 +121,82 @@ can be easily displayed in a chromaticity diagram, for example). One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, ``ImageCms.INTENT_PERCEPTUAL``, ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and ``ImageCms.INTENT_SATURATION``. - :type: :py:class:`int` - .. py:attribute:: profile_id + :type: bytes A sequence of 16 bytes identifying the profile (via a specially constructed MD5 sum), or 16 binary zeroes if the profile ID has not been calculated (see 7.2.18 of ICC.1:2010). - :type: :py:class:`bytes` - .. py:attribute:: copyright + :type: Optional[str] The text copyright information for the profile (see 9.2.21 of ICC.1:2010). - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: manufacturer + :type: Optional[str] The (English) display string for the device manufacturer (see 9.2.22 of ICC.1:2010). - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: model + :type: Optional[str] The (English) display string for the device model of the device for which this profile is created (see 9.2.23 of ICC.1:2010). - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: profile_description + :type: Optional[str] The (English) display string for the profile description (see 9.2.41 of ICC.1:2010). - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: target + :type: Optional[str] The name of the registered characterization data set, or the measurement data for a characterization target (see 9.2.14 of ICC.1:2010). - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: red_colorant + :type: Optional[tuple[tuple[float]]] The first column in the matrix used in matrix/TRC transforms (see 9.2.44 of ICC.1:2010). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: green_colorant + :type: Optional[tuple[tuple[float]]] The second column in the matrix used in matrix/TRC transforms (see 9.2.30 of ICC.1:2010). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: blue_colorant + :type: Optional[tuple[tuple[float]]] The third column in the matrix used in matrix/TRC transforms (see 9.2.4 of ICC.1:2010). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: luminance + :type: Optional[tuple[tuple[float]]] The absolute luminance of emissive devices in candelas per square metre as described by the Y channel (see 9.2.32 of ICC.1:2010). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: chromaticity + :type: Optional[tuple[tuple[float]]] The data of the phosphor/colorant chromaticity set used (red, green and blue channels, see 9.2.16 of ICC.1:2010). - :type: ``((x, y, Y), (x, y, Y), (x, y, Y))`` or ``None`` + The value is in the format ``((x, y, Y), (x, y, Y), (x, y, Y))``, if available. .. py:attribute:: chromatic_adaption + :type: tuple[tuple[float]] The chromatic adaption matrix converts a color measured using the actual illumination conditions and relative to the actual adopted @@ -199,58 +204,52 @@ can be easily displayed in a chromaticity diagram, for example). complete adaptation from the actual adopted white chromaticity to the PCS adopted white chromaticity (see 9.2.15 of ICC.1:2010). - Two matrices are returned, one in (X, Y, Z) space and one in (x, y, Y) space. - - :type: 2-tuple of 3-tuple, the first with (X, Y, Z) and the second with (x, y, Y) values + Two 3-tuples of floats are returned in a 2-tuple, + one in (X, Y, Z) space and one in (x, y, Y) space. .. py:attribute:: colorant_table + :type: list[str] This tag identifies the colorants used in the profile by a unique name and set of PCSXYZ or PCSLAB values (see 9.2.19 of ICC.1:2010). - :type: list of strings - .. py:attribute:: colorant_table_out + :type: list[str] This tag identifies the colorants used in the profile by a unique name and set of PCSLAB values (for DeviceLink profiles only, see 9.2.19 of ICC.1:2010). - :type: list of strings - .. py:attribute:: colorimetric_intent + :type: Optional[str] 4-character string (padded with whitespace) identifying the image state of PCS colorimetry produced using the colorimetric intent transforms (see 9.2.20 of ICC.1:2010 for details). - :type: :py:class:`string` or ``None`` - .. py:attribute:: perceptual_rendering_intent_gamut + :type: Optional[str] 4-character string (padded with whitespace) identifying the (one) standard reference medium gamut (see 9.2.37 of ICC.1:2010 for details). - :type: :py:class:`string` or ``None`` - .. py:attribute:: saturation_rendering_intent_gamut + :type: Optional[str] 4-character string (padded with whitespace) identifying the (one) standard reference medium gamut (see 9.2.37 of ICC.1:2010 for details). - :type: :py:class:`string` or ``None`` - .. py:attribute:: technology + :type: Optional[str] 4-character string (padded with whitespace) identifying the device technology (see 9.2.47 of ICC.1:2010 for details). - :type: :py:class:`string` or ``None`` - .. py:attribute:: media_black_point + :type: Optional[tuple[tuple[float]]] This tag specifies the media black point and is used for generating absolute colorimetry. @@ -258,57 +257,57 @@ can be easily displayed in a chromaticity diagram, for example). This tag was available in ICC 3.2, but it is removed from version 4. - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: media_white_point_temperature + :type: Optional[float] Calculates the white point temperature (see the LCMS documentation for more information). - :type: :py:class:`float` or ``None`` - .. py:attribute:: viewing_condition + :type: Optional[str] The (English) display string for the viewing conditions (see 9.2.48 of ICC.1:2010). - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: screening_description + :type: Optional[str] The (English) display string for the screening conditions. This tag was available in ICC 3.2, but it is removed from version 4. - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: red_primary + :type: Optional[tuple[tuple[float]]] The XYZ-transformed of the RGB primary color red (1, 0, 0). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: green_primary + :type: Optional[tuple[tuple[float]]] The XYZ-transformed of the RGB primary color green (0, 1, 0). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: blue_primary + :type: Optional[tuple[tuple[float]]] The XYZ-transformed of the RGB primary color blue (0, 0, 1). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: is_matrix_shaper + :type: bool True if this profile is implemented as a matrix shaper (see documentation on LCMS). - :type: :py:class:`bool` - .. py:attribute:: clut + :type: dict[tuple[bool]] Returns a dictionary of all supported intents and directions for the CLUT model. @@ -326,9 +325,8 @@ can be easily displayed in a chromaticity diagram, for example). The elements of the tuple are booleans. If the value is ``True``, that intent is supported for that direction. - :type: :py:class:`dict` of boolean 3-tuples - .. py:attribute:: intent_supported + :type: dict[tuple[bool]] Returns a dictionary of all supported intents and directions. @@ -345,64 +343,6 @@ can be easily displayed in a chromaticity diagram, for example). The elements of the tuple are booleans. If the value is ``True``, that intent is supported for that direction. - :type: :py:class:`dict` of boolean 3-tuples - - .. py:attribute:: color_space - - Deprecated but retained for backwards compatibility. - Interpreted value of :py:attr:`.xcolor_space`. May be the - empty string if value could not be decoded. - - :type: :py:class:`string` - - .. py:attribute:: pcs - - Deprecated but retained for backwards compatibility. - Interpreted value of :py:attr:`.connection_space`. May be - the empty string if value could not be decoded. - - :type: :py:class:`string` - - .. py:attribute:: product_model - - Deprecated but retained for backwards compatibility. - ASCII-encoded value of :py:attr:`.model`. - - :type: :py:class:`string` - - .. py:attribute:: product_manufacturer - - Deprecated but retained for backwards compatibility. - ASCII-encoded value of :py:attr:`.manufacturer`. - - :type: :py:class:`string` - - .. py:attribute:: product_copyright - - Deprecated but retained for backwards compatibility. - ASCII-encoded value of :py:attr:`.copyright`. - - :type: :py:class:`string` - - .. py:attribute:: product_description - - Deprecated but retained for backwards compatibility. - ASCII-encoded value of :py:attr:`.profile_description`. - - :type: :py:class:`string` - - .. py:attribute:: product_desc - - Deprecated but retained for backwards compatibility. - ASCII-encoded value of :py:attr:`.profile_description`. - - This alias of :py:attr:`.product_description` used to - contain a derived informative string about the profile, - depending on the value of the description, copyright, - manufacturer and model fields). - - :type: :py:class:`string` - There is one function defined on the class: .. py:method:: is_intent_supported(intent, direction) @@ -413,10 +353,10 @@ can be easily displayed in a chromaticity diagram, for example). with :py:attr:`.intent_supported`. :param intent: One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, - ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` - and ``ImageCms.INTENT_SATURATION``. + ``ImageCms.INTENT_PERCEPTUAL``, + ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` + and ``ImageCms.INTENT_SATURATION``. :param direction: One of ``ImageCms.DIRECTION_INPUT``, - ``ImageCms.DIRECTION_OUTPUT`` - and ``ImageCms.DIRECTION_PROOF`` + ``ImageCms.DIRECTION_OUTPUT`` + and ``ImageCms.DIRECTION_PROOF`` :return: Boolean if the intent and direction is supported. diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst index 187306f1b..e32a77b54 100644 --- a/docs/reference/ImageColor.rst +++ b/docs/reference/ImageColor.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageColor .. py:currentmodule:: PIL.ImageColor -:py:mod:`ImageColor` Module -=========================== +:py:mod:`~PIL.ImageColor` Module +================================ -The :py:mod:`ImageColor` module contains color tables and converters from +The :py:mod:`~PIL.ImageColor` module contains color tables and converters from CSS3-style color specifiers to RGB tuples. This module is used by :py:meth:`PIL.Image.new` and the :py:mod:`~PIL.ImageDraw` module, among others. diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 51eaf925e..57d1c2dda 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageDraw .. py:currentmodule:: PIL.ImageDraw -:py:mod:`ImageDraw` Module -========================== +:py:mod:`~PIL.ImageDraw` Module +=============================== -The :py:mod:`ImageDraw` module provides simple 2D graphics for +The :py:mod:`~PIL.ImageDraw` module provides simple 2D graphics for :py:class:`~PIL.Image.Image` objects. You can use this module to create new images, annotate or retouch existing images, and to generate graphics on the fly for web use. @@ -20,14 +20,14 @@ Example: Draw a gray cross over an image from PIL import Image, ImageDraw - im = Image.open("hopper.jpg") + with Image.open("hopper.jpg") as im: - draw = ImageDraw.Draw(im) - draw.line((0, 0) + im.size, fill=128) - draw.line((0, im.size[1], im.size[0], 0), fill=128) + draw = ImageDraw.Draw(im) + draw.line((0, 0) + im.size, fill=128) + draw.line((0, im.size[1], im.size[0], 0), fill=128) - # write to stdout - im.save(sys.stdout, "PNG") + # write to stdout + im.save(sys.stdout, "PNG") Concepts @@ -81,13 +81,13 @@ Example: Draw Partial Opacity Text from PIL import Image, ImageDraw, ImageFont # get an image - base = Image.open('Pillow/Tests/images/hopper.png').convert('RGBA') + base = Image.open("Pillow/Tests/images/hopper.png").convert("RGBA") # make a blank image for the text, initialized to transparent text color - txt = Image.new('RGBA', base.size, (255,255,255,0)) + txt = Image.new("RGBA", base.size, (255,255,255,0)) # get a font - fnt = ImageFont.truetype('Pillow/Tests/fonts/FreeMono.ttf', 40) + fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) # get a drawing context d = ImageDraw.Draw(txt) @@ -100,12 +100,31 @@ Example: Draw Partial Opacity Text out.show() +Example: Draw Multiline Text +---------------------------- + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + # create an image + out = Image.new("RGB", (150, 100), (255, 255, 255)) + + # get a font + fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) + # get a drawing context + d = ImageDraw.Draw(out) + + # draw multiline text + d.multiline_text((10,10), "Hello\nWorld", font=fnt, fill=(0, 0, 0)) + + out.show() Functions --------- -.. py:class:: PIL.ImageDraw.Draw(im, mode=None) +.. py:method:: Draw(im, mode=None) Creates an object that can be used to draw in the given image. @@ -121,13 +140,13 @@ Functions Methods ------- -.. py:method:: PIL.ImageDraw.ImageDraw.getfont() +.. py:method:: ImageDraw.getfont() Get the current default font. :returns: An image font. -.. py:method:: PIL.ImageDraw.ImageDraw.arc(xy, start, end, fill=None, width=0) +.. py:method:: ImageDraw.arc(xy, start, end, fill=None, width=0) Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. @@ -143,7 +162,7 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.bitmap(xy, bitmap, fill=None) +.. py:method:: ImageDraw.bitmap(xy, bitmap, fill=None) Draws a bitmap (mask) at the given position, using the current fill color for the non-zero portions. The bitmap should be a valid transparency mask @@ -154,7 +173,7 @@ Methods To paste pixel data into an image, use the :py:meth:`~PIL.Image.Image.paste` method on the image itself. -.. py:method:: PIL.ImageDraw.ImageDraw.chord(xy, start, end, fill=None, outline=None, width=0) +.. py:method:: ImageDraw.chord(xy, start, end, fill=None, outline=None, width=1) Same as :py:meth:`~PIL.ImageDraw.ImageDraw.arc`, but connects the end points with a straight line. @@ -168,7 +187,7 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.ellipse(xy, fill=None, outline=None, width=0) +.. py:method:: ImageDraw.ellipse(xy, fill=None, outline=None, width=1) Draws an ellipse inside the given bounding box. @@ -181,9 +200,9 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.line(xy, fill=None, width=0, joint=None) +.. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) - Draws a line between the coordinates in the **xy** list. + Draws a line between the coordinates in the ``xy`` list. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. @@ -193,12 +212,11 @@ Methods .. versionadded:: 1.1.5 .. note:: This option was broken until version 1.1.6. - :param joint: Joint type between a sequence of lines. It can be "curve", - for rounded edges, or None. + :param joint: Joint type between a sequence of lines. It can be ``"curve"``, for rounded edges, or :data:`None`. .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=0) +.. py:method:: ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=1) Same as arc, but also draws straight lines between the end points and the center of the bounding box. @@ -215,7 +233,7 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.point(xy, fill=None) +.. py:method:: ImageDraw.point(xy, fill=None) Draws points (individual pixels) at the given coordinates. @@ -223,7 +241,7 @@ Methods numeric values like ``[x, y, x, y, ...]``. :param fill: Color to use for the point. -.. py:method:: PIL.ImageDraw.ImageDraw.polygon(xy, fill=None, outline=None) +.. py:method:: ImageDraw.polygon(xy, fill=None, outline=None) Draws a polygon. @@ -236,7 +254,25 @@ Methods :param outline: Color to use for the outline. :param fill: Color to use for the fill. -.. py:method:: PIL.ImageDraw.ImageDraw.rectangle(xy, fill=None, outline=None, width=0) + +.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None) + + Draws a regular polygon inscribed in ``bounding_circle``, + with ``n_sides``, and rotation of ``rotation`` degrees. + + :param bounding_circle: The bounding circle is a tuple defined + by a point and radius. + (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``). + The polygon is inscribed in this circle. + :param n_sides: Number of sides + (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon). + :param rotation: Apply an arbitrary rotation to the polygon + (e.g. ``rotation=90``, applies a 90 degree rotation). + :param fill: Color to use for the fill. + :param outline: Color to use for the outline. + + +.. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) Draws a rectangle. @@ -249,27 +285,39 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.shape(shape, fill=None, outline=None) +.. py:method:: ImageDraw.shape(shape, fill=None, outline=None) .. warning:: This method is experimental. Draw a shape. -.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None) +.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False) Draws the string at the given position. - :param xy: Top left corner of the text. - :param text: Text to be drawn. If it contains any newline characters, - the text is passed on to multiline_text() + :param xy: The anchor coordinates of the text. + :param text: String to be drawn. If it contains any newline characters, + the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`. :param fill: Color to use for the text. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: If the text is passed on to multiline_text(), + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. This parameter is + ignored for non-TrueType fonts. + + .. note:: This parameter was present in earlier versions + of Pillow, but implemented only in version 8.0.0. + + :param spacing: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, the number of pixels between lines. - :param align: If the text is passed on to multiline_text(), - "left", "center" or "right". - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). + :param align: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, + ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. + Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. .. versionadded:: 4.2.0 @@ -277,12 +325,11 @@ Methods :param features: A list of OpenType font features to be used during text layout. This is usually used to turn on optional font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` to disable kerning. To get all supported - features, see - https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + features, see `OpenType docs`_. Requires libraqm. .. versionadded:: 4.2.0 @@ -291,8 +338,68 @@ Methods different glyph shapes or ligatures. This parameter tells the font which language the text is in, and to apply the correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - ` + It should be a `BCP 47 language code`_. + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param stroke_fill: Color to use for the text stroke. If not given, will default to + the ``fill`` parameter. + + .. versionadded:: 6.2.0 + + :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + + .. versionadded:: 8.0.0 + + +.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False) + + Draws the string at the given position. + + :param xy: The anchor coordinates of the text. + :param text: String to be drawn. + :param fill: Color to use for the text. + :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. This parameter is + ignored for non-TrueType fonts. + + .. note:: This parameter was present in earlier versions + of Pillow, but implemented only in version 8.0.0. + + :param spacing: The number of pixels between lines. + :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. + Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` + to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. Requires libraqm. .. versionadded:: 6.0.0 @@ -306,68 +413,42 @@ Methods .. versionadded:: 6.2.0 -.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None) + :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). - Draws the string at the given position. + .. versionadded:: 8.0.0 - :param xy: Top left corner of the text. - :param text: Text to be drawn. - :param fill: Color to use for the text. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: The number of pixels between lines. - :param align: "left", "center" or "right". - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - ` - Requires libraqm. - - .. versionadded:: 6.0.0 - -.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) +.. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) Return the size of the given string, in pixels. + Use :py:meth:`textlength()` to measure the offset of following text with + 1/64 pixel precision. + Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. + + .. note:: For historical reasons this function measures text height from + the ascender line instead of the top, see :ref:`text-anchors`. + If you wish to measure text height from the top, it is recommended + to use :meth:`textbbox` with ``anchor='lt'`` instead. + :param text: Text to be measured. If it contains any newline characters, - the text is passed on to multiline_textsize() + the text is passed on to :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: If the text is passed on to multiline_textsize(), + :param spacing: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`, the number of pixels between lines. - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. .. versionadded:: 4.2.0 :param features: A list of OpenType font features to be used during text layout. This is usually used to turn on optional font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` to disable kerning. To get all supported - features, see - https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + features, see `OpenType docs`_. Requires libraqm. .. versionadded:: 4.2.0 @@ -375,8 +456,7 @@ Methods different glyph shapes or ligatures. This parameter tells the font which language the text is in, and to apply the correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - ` + It should be a `BCP 47 language code`_. Requires libraqm. .. versionadded:: 6.0.0 @@ -385,15 +465,25 @@ Methods .. versionadded:: 6.2.0 -.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) +.. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) Return the size of the given string, in pixels. + Use :py:meth:`textlength()` to measure the offset of following text with + 1/64 pixel precision. + Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. + + .. note:: For historical reasons this function measures text height as the + distance between the top ascender line and bottom descender line, + not the top and bottom of the text, see :ref:`text-anchors`. + If you wish to measure text height from the top to the bottom of text, + it is recommended to use :meth:`multiline_textbbox` instead. + :param text: Text to be measured. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param spacing: The number of pixels between lines. - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. .. versionadded:: 4.2.0 @@ -401,12 +491,11 @@ Methods :param features: A list of OpenType font features to be used during text layout. This is usually used to turn on optional font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` to disable kerning. To get all supported - features, see - https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + features, see `OpenType docs`_. Requires libraqm. .. versionadded:: 4.2.0 @@ -415,8 +504,7 @@ Methods different glyph shapes or ligatures. This parameter tells the font which language the text is in, and to apply the correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - ` + It should be a `BCP 47 language code`_. Requires libraqm. .. versionadded:: 6.0.0 @@ -425,7 +513,165 @@ Methods .. versionadded:: 6.2.0 -.. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None) +.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False) + + Returns length (in pixels with 1/64 precision) of given text when rendered + in font with provided direction, features, and language. + + This is the amount by which following text should be offset. + Text bounding box may extend past the length in some fonts, + e.g. when using italics or accents. + + The result is returned as a float; it is a whole number if using basic layout. + + Note that the sum of two lengths may not equal the length of a concatenated + string due to kerning. If you need to adjust for kerning, include the following + character and subtract its length. + + For example, instead of + + .. code-block:: python + + hello = draw.textlength("Hello", font) + world = draw.textlength("World", font) + hello_world = hello + world # not adjusted for kerning + assert hello_world == draw.textlength("HelloWorld", font) # may fail + + use + + .. code-block:: python + + hello = draw.textlength("HelloW", font) - draw.textlength("W", font) # adjusted for kerning + world = draw.textlength("World", font) + hello_world = hello + world # adjusted for kerning + assert hello_world == draw.textlength("HelloWorld", font) # True + + or disable kerning with (requires libraqm) + + .. code-block:: python + + hello = draw.textlength("Hello", font, features=["-kern"]) + world = draw.textlength("World", font, features=["-kern"]) + hello_world = hello + world # kerning is disabled, no need to adjust + assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) # True + + .. versionadded:: 8.0.0 + + :param text: Text to be measured. May not contain any newline characters. + :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` + to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + +.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) + + Returns bounding box (in pixels) of given text relative to given anchor + when rendered in font with provided direction, features, and language. + Only supported for TrueType fonts. + + Use :py:meth:`textlength` to get the offset of following text with + 1/64 pixel precision. The bounding box includes extra margins for + some fonts, e.g. italics or accents. + + .. versionadded:: 8.0.0 + + :param xy: The anchor coordinates of the text. + :param text: Text to be measured. If it contains any newline characters, + the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`. + :param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance. + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. This parameter is + ignored for non-TrueType fonts. + :param spacing: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`, + the number of pixels between lines. + :param align: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`, + ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. + Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` + to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + :param stroke_width: The width of the text stroke. + :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + +.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) + + Returns bounding box (in pixels) of given text relative to given anchor + when rendered in font with provided direction, features, and language. + Only supported for TrueType fonts. + + Use :py:meth:`textlength` to get the offset of following text with + 1/64 pixel precision. The bounding box includes extra margins for + some fonts, e.g. italics or accents. + + .. versionadded:: 8.0.0 + + :param xy: The anchor coordinates of the text. + :param text: Text to be measured. + :param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance. + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. This parameter is + ignored for non-TrueType fonts. + :param spacing: The number of pixels between lines. + :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. + Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` + to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + :param stroke_width: The width of the text stroke. + :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + +.. py:method:: getdraw(im=None, hints=None) .. warning:: This method is experimental. @@ -436,7 +682,7 @@ Methods :param hints: An optional list of hints. :returns: A (drawing context, drawing resource factory) tuple. -.. py:method:: PIL.ImageDraw.floodfill(image, xy, value, border=None, thresh=0) +.. py:method:: floodfill(image, xy, value, border=None, thresh=0) .. warning:: This method is experimental. @@ -453,3 +699,6 @@ Methods tolerable difference of a pixel value from the 'background' in order for it to be replaced. Useful for filling regions of non- homogeneous, but similar, colors. + +.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/ +.. _OpenType docs: https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index b172054b2..29ceee314 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageEnhance .. py:currentmodule:: PIL.ImageEnhance -:py:mod:`ImageEnhance` Module -============================= +:py:mod:`~PIL.ImageEnhance` Module +================================== -The :py:mod:`ImageEnhance` module contains a number of classes that can be used +The :py:mod:`~PIL.ImageEnhance` module contains a number of classes that can be used for image enhancement. Example: Vary the sharpness of an image @@ -18,7 +18,7 @@ Example: Vary the sharpness of an image for i in range(8): factor = i / 4.0 - enhancer.enhance(factor).show("Sharpness %f" % factor) + enhancer.enhance(factor).show(f"Sharpness {factor:f}") Also see the :file:`enhancer.py` demo program in the :file:`Scripts/` directory. @@ -29,7 +29,8 @@ Classes All enhancement classes implement a common interface, containing a single method: -.. py:class:: PIL.ImageEnhance._Enhance +.. py:class:: _Enhance + .. py:method:: enhance(factor) Returns an enhanced image. @@ -40,7 +41,7 @@ method: etc), and higher values more. There are no restrictions on this value. -.. py:class:: PIL.ImageEnhance.Color(image) +.. py:class:: Color(image) Adjust image color balance. @@ -49,7 +50,7 @@ method: factor of 0.0 gives a black and white image. A factor of 1.0 gives the original image. -.. py:class:: PIL.ImageEnhance.Contrast(image) +.. py:class:: Contrast(image) Adjust image contrast. @@ -57,7 +58,7 @@ method: to the contrast control on a TV set. An enhancement factor of 0.0 gives a solid grey image. A factor of 1.0 gives the original image. -.. py:class:: PIL.ImageEnhance.Brightness(image) +.. py:class:: Brightness(image) Adjust image brightness. @@ -65,7 +66,7 @@ method: enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the original image. -.. py:class:: PIL.ImageEnhance.Sharpness(image) +.. py:class:: Sharpness(image) Adjust image sharpness. diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index d93dfb3a3..e0ce389e8 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageFile .. py:currentmodule:: PIL.ImageFile -:py:mod:`ImageFile` Module -========================== +:py:mod:`~PIL.ImageFile` Module +=============================== -The :py:mod:`ImageFile` module provides support functions for the image open +The :py:mod:`~PIL.ImageFile` module provides support functions for the image open and save functions. In addition, it provides a :py:class:`Parser` class which can be used to decode @@ -34,14 +34,28 @@ Example: Parse an image im.save("copy.jpg") -:py:class:`~PIL.ImageFile.Parser` ---------------------------------- +Classes +------- .. autoclass:: PIL.ImageFile.Parser() :members: -:py:class:`~PIL.ImageFile.PyDecoder` ------------------------------------- - .. autoclass:: PIL.ImageFile.PyDecoder() :members: + +.. autoclass:: PIL.ImageFile.ImageFile() + :member-order: bysource + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: PIL.ImageFile.StubImageFile() + :members: + :show-inheritance: + +Constants +--------- + +.. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES +.. autodata:: PIL.ImageFile.ERRORS + :annotation: diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index 52a7d7500..c85da4fb5 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageFilter .. py:currentmodule:: PIL.ImageFilter -:py:mod:`ImageFilter` Module -============================ +:py:mod:`~PIL.ImageFilter` Module +================================= -The :py:mod:`ImageFilter` module contains definitions for a pre-defined set of +The :py:mod:`~PIL.ImageFilter` module contains definitions for a pre-defined set of filters, which can be be used with the :py:meth:`Image.filter() ` method. @@ -66,3 +66,29 @@ image enhancement filters: .. autoclass:: PIL.ImageFilter.ModeFilter :members: + +.. class:: Filter + + An abstract mixin used for filtering images + (for use with :py:meth:`~PIL.Image.Image.filter`). + + Implementors must provide the following method: + + .. method:: filter(self, image) + + Applies a filter to a single-band image, or a single band of an image. + + :returns: A filtered copy of the image. + +.. class:: MultibandFilter + + An abstract mixin used for filtering multi-band images + (for use with :py:meth:`~PIL.Image.Image.filter`). + + Implementors must provide the following method: + + .. method:: filter(self, image) + + Applies a filter to a multi-band image. + + :returns: A filtered copy of the image. diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index bb7538096..ff79bdcd8 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -1,21 +1,22 @@ .. py:module:: PIL.ImageFont .. py:currentmodule:: PIL.ImageFont -:py:mod:`ImageFont` Module -========================== +:py:mod:`~PIL.ImageFont` Module +=============================== -The :py:mod:`ImageFont` module defines a class with the same name. Instances of +The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instances of this class store bitmap fonts, and are used with the -:py:meth:`PIL.ImageDraw.Draw.text` method. +:py:meth:`PIL.ImageDraw.ImageDraw.text` method. PIL uses its own font file format to store bitmap fonts. You can use the -:command:`pilfont` utility to convert BDF and PCF font descriptors (X window -font formats) to this format. +:command:`pilfont` utility from +`pillow-scripts `_ +to convert BDF and PCF font descriptors (X window font formats) to this format. Starting with version 1.1.4, PIL can be configured to support TrueType and OpenType fonts (as well as other font formats supported by the FreeType library). For earlier versions, TrueType support is only available as part of -the imToolkit package +the imToolkit package. Example ------- @@ -55,3 +56,19 @@ Methods .. autoclass:: PIL.ImageFont.TransposedFont :members: + +Constants +--------- + +.. data:: PIL.ImageFont.LAYOUT_BASIC + + Use basic text layout for TrueType font. + Advanced features such as text direction are not supported. + +.. data:: PIL.ImageFont.LAYOUT_RAQM + + Use Raqm text layout for TrueType font. + Advanced features are supported. + + Requires Raqm, you can check support using + :py:func:`PIL.features.check_feature` with ``feature="raqm"``. diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index e94e21cb9..ac83b2255 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -1,23 +1,21 @@ .. py:module:: PIL.ImageGrab .. py:currentmodule:: PIL.ImageGrab -:py:mod:`ImageGrab` Module (macOS and Windows only) -=================================================== +:py:mod:`~PIL.ImageGrab` Module +=============================== -The :py:mod:`ImageGrab` module can be used to copy the contents of the screen +The :py:mod:`~PIL.ImageGrab` module can be used to copy the contents of the screen or the clipboard to a PIL image memory. -.. note:: The current version works on macOS and Windows only. - .. versionadded:: 1.1.3 -.. py:function:: PIL.ImageGrab.grab(bbox=None, include_layered_windows=False, all_screens=False) +.. 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 "RGB" image on Windows or "RGBA" on macOS. + returned as an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, the entire screen is copied. - .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS) + .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux (X11)) :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. @@ -27,11 +25,18 @@ or the clipboard to a PIL image memory. :param all_screens: Capture all monitors. Windows OS only. .. versionadded:: 6.2.0 + + :param xdisplay: + X11 Display address. Pass :data:`None` to grab the default system screen. Pass ``""`` to grab the default X11 screen on Windows or macOS. + + You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``. + + .. versionadded:: 7.1.0 :return: An image -.. py:function:: PIL.ImageGrab.grabclipboard() +.. py:function:: grabclipboard() - Take a snapshot of the clipboard image, if any. + Take a snapshot of the clipboard image, if any. Only macOS and Windows are currently supported. .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS) diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index ca30244d1..821f60cf5 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageMath .. py:currentmodule:: PIL.ImageMath -:py:mod:`ImageMath` Module -========================== +:py:mod:`~PIL.ImageMath` Module +=============================== -The :py:mod:`ImageMath` module can be used to evaluate “image expressions”. The +The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”. The module provides a single :py:meth:`~PIL.ImageMath.eval` function, which takes an expression string and one or more images. @@ -60,9 +60,8 @@ point values, as necessary. For example, if you add two 8-bit images, the result will be a 32-bit integer image. If you add a floating point constant to an 8-bit image, the result will be a 32-bit floating point image. -You can force conversion using the :py:func:`~PIL.ImageMath.convert`, -:py:func:`~PIL.ImageMath.float`, and :py:func:`~PIL.ImageMath.int` functions -described below. +You can force conversion using the ``convert()``, ``float()``, and ``int()`` +functions described below. Bitwise Operators ^^^^^^^^^^^^^^^^^ @@ -98,20 +97,24 @@ These functions are applied to each individual pixel. .. py:currentmodule:: None .. py:function:: abs(image) + :noindex: Absolute value. .. py:function:: convert(image, mode) + :noindex: Convert image to the given mode. The mode must be given as a string constant. .. py:function:: float(image) + :noindex: Convert image to 32-bit floating point. This is equivalent to convert(image, “F”). .. py:function:: int(image) + :noindex: Convert image to 32-bit integer. This is equivalent to convert(image, “I”). @@ -119,9 +122,11 @@ These functions are applied to each individual pixel. integers if necessary to get a correct result. .. py:function:: max(image1, image2) + :noindex: Maximum value. .. py:function:: min(image1, image2) + :noindex: Minimum value. diff --git a/docs/reference/ImageMorph.rst b/docs/reference/ImageMorph.rst index be9d59348..d4522a06a 100644 --- a/docs/reference/ImageMorph.rst +++ b/docs/reference/ImageMorph.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageMorph .. py:currentmodule:: PIL.ImageMorph -:py:mod:`ImageMorph` Module -=========================== +:py:mod:`~PIL.ImageMorph` Module +================================ -The :py:mod:`ImageMorph` module provides morphology operations on images. +The :py:mod:`~PIL.ImageMorph` module provides morphology operations on images. .. automodule:: PIL.ImageMorph :members: diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index 1c86d168f..9a16d6625 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageOps .. py:currentmodule:: PIL.ImageOps -:py:mod:`ImageOps` Module -========================== +:py:mod:`~PIL.ImageOps` Module +============================== -The :py:mod:`ImageOps` module contains a number of ‘ready-made’ image +The :py:mod:`~PIL.ImageOps` module contains a number of ‘ready-made’ image processing operations. This module is somewhat experimental, and most operators only work on L and RGB images. diff --git a/docs/reference/ImagePalette.rst b/docs/reference/ImagePalette.rst index 15b8aed8f..f14c1c3a4 100644 --- a/docs/reference/ImagePalette.rst +++ b/docs/reference/ImagePalette.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImagePalette .. py:currentmodule:: PIL.ImagePalette -:py:mod:`ImagePalette` Module -============================= +:py:mod:`~PIL.ImagePalette` Module +================================== -The :py:mod:`ImagePalette` module contains a class of the same name to +The :py:mod:`~PIL.ImagePalette` module contains a class of the same name to represent the color palette of palette mapped images. .. note:: diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 5ab350ef3..b9bdfc507 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImagePath .. py:currentmodule:: PIL.ImagePath -:py:mod:`ImagePath` Module -========================== +:py:mod:`~PIL.ImagePath` Module +=============================== -The :py:mod:`ImagePath` module is used to store and manipulate 2-dimensional +The :py:mod:`~PIL.ImagePath` module is used to store and manipulate 2-dimensional vector data. Path objects can be passed to the methods on the :py:mod:`~PIL.ImageDraw` module. @@ -33,7 +33,7 @@ vector data. Path objects can be passed to the methods on the method modifies the path in place, and returns the number of points left in the path. - **distance** is measured as `Manhattan distance`_ and defaults to two + ``distance`` is measured as `Manhattan distance`_ and defaults to two pixels. .. _Manhattan distance: https://en.wikipedia.org/wiki/Manhattan_distance @@ -55,7 +55,7 @@ vector data. Path objects can be passed to the methods on the :param flat: By default, this function returns a list of 2-tuples [(x, y), ...]. If this argument is ``True``, it returns a flat list [x, y, ...] instead. - :return: A list of coordinates. See **flat**. + :return: A list of coordinates. See ``flat``. .. py:method:: PIL.ImagePath.Path.transform(matrix) diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 7dd7084db..34d4cef51 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -1,15 +1,15 @@ .. py:module:: PIL.ImageQt .. py:currentmodule:: PIL.ImageQt -:py:mod:`ImageQt` Module -======================== +:py:mod:`~PIL.ImageQt` Module +============================= -The :py:mod:`ImageQt` module contains support for creating PyQt5 or PySide2 QImage +The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt5 or PySide2 QImage objects from PIL images. .. versionadded:: 1.1.6 -.. py:class:: ImageQt.ImageQt(image) +.. py:class:: ImageQt(image) Creates an :py:class:`~PIL.ImageQt.ImageQt` object from a PIL :py:class:`~PIL.Image.Image` object. This class is a subclass of diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index 251ea3a93..1bfb554b6 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageSequence .. py:currentmodule:: PIL.ImageSequence -:py:mod:`ImageSequence` Module -============================== +:py:mod:`~PIL.ImageSequence` Module +=================================== -The :py:mod:`ImageSequence` module contains a wrapper class that lets you +The :py:mod:`~PIL.ImageSequence` module contains a wrapper class that lets you iterate over the frames of an image sequence. Extracting frames from an animation @@ -14,12 +14,11 @@ Extracting frames from an animation from PIL import Image, ImageSequence - im = Image.open("animation.fli") - - index = 1 - for frame in ImageSequence.Iterator(im): - frame.save("frame%d.png" % index) - index += 1 + with Image.open("animation.fli") as im: + index = 1 + for frame in ImageSequence.Iterator(im): + frame.save(f"frame{index}.png") + index += 1 The :py:class:`~PIL.ImageSequence.Iterator` class ------------------------------------------------- diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst new file mode 100644 index 000000000..a30a6caed --- /dev/null +++ b/docs/reference/ImageShow.rst @@ -0,0 +1,27 @@ +.. py:module:: PIL.ImageShow +.. py:currentmodule:: PIL.ImageShow + +:py:mod:`~PIL.ImageShow` Module +=============================== + +The :py:mod:`~PIL.ImageShow` Module is used to display images. +All default viewers convert the image to be shown to PNG format. + +.. autofunction:: PIL.ImageShow.show + +.. autoclass:: WindowsViewer +.. autoclass:: MacViewer + +.. class:: UnixViewer + + The following viewers may be registered on Unix-based systems, if the given command is found: + + .. autoclass:: PIL.ImageShow.DisplayViewer + .. autoclass:: PIL.ImageShow.EogViewer + .. autoclass:: PIL.ImageShow.XVViewer + +.. autofunction:: PIL.ImageShow.register +.. autoclass:: PIL.ImageShow.Viewer + :member-order: bysource + :members: + :undoc-members: diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index 32f5917c1..5bb735296 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -1,13 +1,13 @@ .. py:module:: PIL.ImageStat .. py:currentmodule:: PIL.ImageStat -:py:mod:`ImageStat` Module -========================== +:py:mod:`~PIL.ImageStat` Module +=============================== -The :py:mod:`ImageStat` module calculates global statistics for an image, or +The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or for a region of an image. -.. py:class:: PIL.ImageStat.Stat(image_or_list, mask=None) +.. py:class:: Stat(image_or_list, mask=None) Calculate statistics for the given image. If a mask is included, only the regions covered by that mask are included in the @@ -22,13 +22,13 @@ for a region of an image. .. note:: - This relies on the :py:meth:`~PIL.Image.histogram` method, and + This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and simply returns the low and high bins used. This is correct for images with 8 bits per channel, but fails for other modes such as - ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.getextrema` to + ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to return per-band extrema for the image. This is more correct and efficient because, for non-8-bit modes, the histogram method uses - :py:meth:`~PIL.Image.getextrema` to determine the bins used. + :py:meth:`~PIL.Image.Image.getextrema` to determine the bins used. .. py:attribute:: count diff --git a/docs/reference/ImageTk.rst b/docs/reference/ImageTk.rst index 7ee4af029..134ef5651 100644 --- a/docs/reference/ImageTk.rst +++ b/docs/reference/ImageTk.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageTk .. py:currentmodule:: PIL.ImageTk -:py:mod:`ImageTk` Module -======================== +:py:mod:`~PIL.ImageTk` Module +============================= -The :py:mod:`ImageTk` module contains support to create and modify Tkinter +The :py:mod:`~PIL.ImageTk` module contains support to create and modify Tkinter BitmapImage and PhotoImage objects from PIL images. For examples, see the demo programs in the Scripts directory. diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index ff3d6a7fc..2ee3cadb7 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageWin .. py:currentmodule:: PIL.ImageWin -:py:mod:`ImageWin` Module (Windows-only) -======================================== +:py:mod:`~PIL.ImageWin` Module (Windows-only) +============================================= -The :py:mod:`ImageWin` module contains support to create and display images on +The :py:mod:`~PIL.ImageWin` module contains support to create and display images on Windows. ImageWin can be used with PythonWin and other user interface toolkits that diff --git a/docs/reference/JpegPresets.rst b/docs/reference/JpegPresets.rst new file mode 100644 index 000000000..aafae44cf --- /dev/null +++ b/docs/reference/JpegPresets.rst @@ -0,0 +1,11 @@ +.. py:currentmodule:: PIL.JpegPresets + +:py:mod:`~PIL.JpegPresets` Module +================================= + +.. automodule:: PIL.JpegPresets + + .. data:: presets + :type: dict + + A dictionary of all supported presets. diff --git a/docs/reference/PSDraw.rst b/docs/reference/PSDraw.rst index 2b5b9b340..3e8512e7a 100644 --- a/docs/reference/PSDraw.rst +++ b/docs/reference/PSDraw.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.PSDraw .. py:currentmodule:: PIL.PSDraw -:py:mod:`PSDraw` Module -======================= +:py:mod:`~PIL.PSDraw` Module +============================ -The :py:mod:`PSDraw` module provides simple print support for Postscript +The :py:mod:`~PIL.PSDraw` module provides simple print support for PostScript printers. You can print text, graphics and images through this module. .. autoclass:: PIL.PSDraw.PSDraw diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 8a8569922..f28e58f86 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -17,8 +17,8 @@ changes it. .. code-block:: python from PIL import Image - im = Image.open('hopper.jpg') - px = im.load() + with Image.open('hopper.jpg') as im: + px = im.load() print (px[4,4]) px[4,4] = (0,0,0) print (px[4,4]) diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index 6a492cd86..486c9fc21 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.PyAccess .. py:currentmodule:: PIL.PyAccess -:py:mod:`PyAccess` Module -========================= +:py:mod:`~PIL.PyAccess` Module +============================== -The :py:mod:`PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version. +The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version. .. note:: Accessing individual pixels is fairly slow. If you are looping over all of the pixels in an image, there is likely @@ -18,8 +18,8 @@ The following script loads an image, accesses one pixel from it, then changes it .. code-block:: python from PIL import Image - im = Image.open('hopper.jpg') - px = im.load() + with Image.open('hopper.jpg') as im: + px = im.load() print (px[4,4]) px[4,4] = (0,0,0) print (px[4,4]) diff --git a/docs/reference/TiffTags.rst b/docs/reference/TiffTags.rst index 3b261625a..1441185dd 100644 --- a/docs/reference/TiffTags.rst +++ b/docs/reference/TiffTags.rst @@ -1,17 +1,17 @@ .. py:module:: PIL.TiffTags .. py:currentmodule:: PIL.TiffTags -:py:mod:`TiffTags` Module -========================= +:py:mod:`~PIL.TiffTags` Module +============================== -The :py:mod:`TiffTags` module exposes many of the standard TIFF +The :py:mod:`~PIL.TiffTags` module exposes many of the standard TIFF metadata tag numbers, names, and type information. .. method:: lookup(tag) :param tag: Integer tag number - :returns: Taginfo namedtuple, From the ``TAGS_V2`` info if possible, - otherwise just populating the value and name from ``TAGS``. + :returns: Taginfo namedtuple, From the :py:data:`~PIL.TiffTags.TAGS_V2` info if possible, + otherwise just populating the value and name from :py:data:`~PIL.TiffTags.TAGS`. If the tag is not recognized, "unknown" is returned for the name .. versionadded:: 3.1.0 @@ -22,7 +22,7 @@ metadata tag numbers, names, and type information. :param value: Integer Tag Number :param name: Tag Name - :param type: Integer type from :py:attr:`PIL.TiffTags.TYPES` + :param type: Integer type from :py:data:`PIL.TiffTags.TYPES` :param length: Array length: 0 == variable, 1 == single value, n = fixed :param enum: Dict of name:integer value options for an enumeration @@ -33,15 +33,17 @@ metadata tag numbers, names, and type information. .. versionadded:: 3.0.0 -.. py:attribute:: PIL.TiffTags.TAGS_V2 +.. py:data:: PIL.TiffTags.TAGS_V2 + :type: dict The ``TAGS_V2`` dictionary maps 16-bit integer tag numbers to - :py:class:`PIL.TagTypes.TagInfo` tuples for metadata fields defined in the TIFF + :py:class:`PIL.TiffTags.TagInfo` tuples for metadata fields defined in the TIFF spec. .. versionadded:: 3.0.0 -.. py:attribute:: PIL.TiffTags.TAGS +.. py:data:: PIL.TiffTags.TAGS + :type: dict The ``TAGS`` dictionary maps 16-bit integer TIFF tag number to descriptive string names. For instance: @@ -50,10 +52,16 @@ metadata tag numbers, names, and type information. >>> TAGS[0x010e] 'ImageDescription' - This dictionary contains a superset of the tags in TAGS_V2, common + This dictionary contains a superset of the tags in :py:data:`~PIL.TiffTags.TAGS_V2`, common EXIF tags, and other well known metadata tags. -.. py:attribute:: PIL.TiffTags.TYPES +.. py:data:: PIL.TiffTags.TYPES + :type: dict The ``TYPES`` dictionary maps the TIFF type short integer to a human readable type name. + +.. py:data:: PIL.TiffTags.LIBTIFF_CORE + :type: list + + A list of supported tag IDs when writing using LibTIFF. diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst index 400f236dc..1abe5280f 100644 --- a/docs/reference/block_allocator.rst +++ b/docs/reference/block_allocator.rst @@ -40,7 +40,7 @@ variables: * ``PILLOW_BLOCK_SIZE``, in bytes, K, or M. Specifies the maximum block size for ``ImagingAllocateArray``. Valid values are - integers, with an optional `k` or `m` suffix. Defaults to 16M. + integers, with an optional ``k`` or ``m`` suffix. Defaults to 16M. * ``PILLOW_BLOCKS_MAX`` Specifies the number of freed blocks to retain to fill future memory requests. Any freed blocks over this diff --git a/docs/reference/features.rst b/docs/reference/features.rst new file mode 100644 index 000000000..dd218fa0e --- /dev/null +++ b/docs/reference/features.rst @@ -0,0 +1,66 @@ +.. py:module:: PIL.features +.. py:currentmodule:: PIL.features + +:py:mod:`~PIL.features` Module +============================== + +The :py:mod:`PIL.features` module can be used to detect which Pillow features are available on your system. + +.. autofunction:: PIL.features.pilinfo +.. autofunction:: PIL.features.check +.. autofunction:: PIL.features.version +.. autofunction:: PIL.features.get_supported + +Modules +------- + +Support for the following modules can be checked: + +* ``pil``: The Pillow core module, required for all functionality. +* ``tkinter``: Tkinter support. Version number not available. +* ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`. +* ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. +* ``webp``: WebP image support. + +.. autofunction:: PIL.features.check_module +.. autofunction:: PIL.features.version_module +.. autofunction:: PIL.features.get_supported_modules + +Codecs +------ + +Support for these is only checked during Pillow compilation. +If the required library was uninstalled from the system, the ``pil`` core module may fail to load instead. +Except for ``jpg``, the version number is checked at run-time. + +Support for the following codecs can be checked: + +* ``jpg``: (compile time) Libjpeg support, required for JPEG based image formats. Only compile time version number is available. +* ``jpg_2000``: (compile time) OpenJPEG support, required for JPEG 2000 image formats. +* ``zlib``: (compile time) Zlib support, required for zlib compressed formats, such as PNG. +* ``libtiff``: (compile time) LibTIFF support, required for TIFF based image formats. + +.. autofunction:: PIL.features.check_codec +.. autofunction:: PIL.features.version_codec +.. autofunction:: PIL.features.get_supported_codecs + +Features +-------- + +Some of these are only checked during Pillow compilation. +If the required library was uninstalled from the system, the relevant module may fail to load instead. +Feature version numbers are available only where stated. + +Support for the following features can be checked: + +* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. +* ``transp_webp``: Support for transparency in WebP images. +* ``webp_mux``: (compile time) Support for EXIF data in WebP images. +* ``webp_anim``: (compile time) Support for animated WebP images. +* ``raqm``: Raqm library, required for ``ImageFont.LAYOUT_RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. +* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. +* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. + +.. autofunction:: PIL.features.check_feature +.. autofunction:: PIL.features.version_feature +.. autofunction:: PIL.features.get_supported_features diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 8c09e7b67..5d6affa94 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -7,8 +7,8 @@ Reference Image ImageChops - ImageColor ImageCms + ImageColor ImageDraw ImageEnhance ImageFile @@ -22,14 +22,17 @@ Reference ImagePath ImageQt ImageSequence + ImageShow ImageStat ImageTk ImageWin ExifTags TiffTags + JpegPresets PSDraw PixelAccess PyAccess + features ../PIL plugins internal_design diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index bbc9050cf..5f911db51 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -7,4 +7,4 @@ Internal Reference Docs open_files limits block_allocator - + internal_modules diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst new file mode 100644 index 000000000..1105ff76e --- /dev/null +++ b/docs/reference/internal_modules.rst @@ -0,0 +1,47 @@ +Internal Modules +================ + +:mod:`~PIL._binary` Module +-------------------------- + +.. automodule:: PIL._binary + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL._tkinter_finder` Module +---------------------------------- + +.. automodule:: PIL._tkinter_finder + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL._util` Module +------------------------ + +.. automodule:: PIL._util + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL._version` Module +--------------------------- + +.. module:: PIL._version + +.. data:: __version__ + :annotation: + :type: str + + This is the master version number for Pillow, + all other uses reference this module. + +:mod:`PIL.Image.core` Module +---------------------------- + +.. module:: PIL._imaging +.. module:: PIL.Image.core + +An internal interface module previously known as :mod:`~PIL._imaging`, +implemented in :file:`_imaging.c`. diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 46f657fce..7094f8784 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -1,340 +1,332 @@ Plugin reference ================ -:mod:`BmpImagePlugin` Module ----------------------------- +:mod:`~PIL.BmpImagePlugin` Module +--------------------------------- .. automodule:: PIL.BmpImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`BufrStubImagePlugin` Module ---------------------------------- +:mod:`~PIL.BufrStubImagePlugin` Module +-------------------------------------- .. automodule:: PIL.BufrStubImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`CurImagePlugin` Module ----------------------------- +:mod:`~PIL.CurImagePlugin` Module +--------------------------------- .. automodule:: PIL.CurImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`DcxImagePlugin` Module ----------------------------- +:mod:`~PIL.DcxImagePlugin` Module +--------------------------------- .. automodule:: PIL.DcxImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`EpsImagePlugin` Module ----------------------------- +:mod:`~PIL.EpsImagePlugin` Module +--------------------------------- .. automodule:: PIL.EpsImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`FitsStubImagePlugin` Module ---------------------------------- +:mod:`~PIL.FitsStubImagePlugin` Module +-------------------------------------- .. automodule:: PIL.FitsStubImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`FliImagePlugin` Module ----------------------------- +:mod:`~PIL.FliImagePlugin` Module +--------------------------------- .. automodule:: PIL.FliImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`FpxImagePlugin` Module ----------------------------- +:mod:`~PIL.FpxImagePlugin` Module +--------------------------------- .. automodule:: PIL.FpxImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`GbrImagePlugin` Module ----------------------------- +:mod:`~PIL.GbrImagePlugin` Module +--------------------------------- .. automodule:: PIL.GbrImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`GifImagePlugin` Module ----------------------------- +:mod:`~PIL.GifImagePlugin` Module +--------------------------------- .. automodule:: PIL.GifImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`GribStubImagePlugin` Module ---------------------------------- +:mod:`~PIL.GribStubImagePlugin` Module +-------------------------------------- .. automodule:: PIL.GribStubImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`Hdf5StubImagePlugin` Module ---------------------------------- +:mod:`~PIL.Hdf5StubImagePlugin` Module +-------------------------------------- .. automodule:: PIL.Hdf5StubImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`IcnsImagePlugin` Module ------------------------------ +:mod:`~PIL.IcnsImagePlugin` Module +---------------------------------- .. automodule:: PIL.IcnsImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`IcoImagePlugin` Module ----------------------------- +:mod:`~PIL.IcoImagePlugin` Module +--------------------------------- .. automodule:: PIL.IcoImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`ImImagePlugin` Module ---------------------------- +:mod:`~PIL.ImImagePlugin` Module +-------------------------------- .. automodule:: PIL.ImImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`ImtImagePlugin` Module ----------------------------- +:mod:`~PIL.ImtImagePlugin` Module +--------------------------------- .. automodule:: PIL.ImtImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`IptcImagePlugin` Module ------------------------------ +:mod:`~PIL.IptcImagePlugin` Module +---------------------------------- .. automodule:: PIL.IptcImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`JpegImagePlugin` Module ------------------------------ +:mod:`~PIL.JpegImagePlugin` Module +---------------------------------- .. automodule:: PIL.JpegImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`Jpeg2KImagePlugin` Module -------------------------------- +:mod:`~PIL.Jpeg2KImagePlugin` Module +------------------------------------ .. automodule:: PIL.Jpeg2KImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`McIdasImagePlugin` Module -------------------------------- +:mod:`~PIL.McIdasImagePlugin` Module +------------------------------------ .. automodule:: PIL.McIdasImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`MicImagePlugin` Module ----------------------------- +:mod:`~PIL.MicImagePlugin` Module +--------------------------------- .. automodule:: PIL.MicImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`MpegImagePlugin` Module ------------------------------ +:mod:`~PIL.MpegImagePlugin` Module +---------------------------------- .. automodule:: PIL.MpegImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`MspImagePlugin` Module ----------------------------- +:mod:`~PIL.MspImagePlugin` Module +--------------------------------- .. automodule:: PIL.MspImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PalmImagePlugin` Module ------------------------------ +:mod:`~PIL.PalmImagePlugin` Module +---------------------------------- .. automodule:: PIL.PalmImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PcdImagePlugin` Module ----------------------------- +:mod:`~PIL.PcdImagePlugin` Module +--------------------------------- .. automodule:: PIL.PcdImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PcxImagePlugin` Module ----------------------------- +:mod:`~PIL.PcxImagePlugin` Module +--------------------------------- .. automodule:: PIL.PcxImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PdfImagePlugin` Module ----------------------------- +:mod:`~PIL.PdfImagePlugin` Module +--------------------------------- .. automodule:: PIL.PdfImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PixarImagePlugin` Module ------------------------------- +:mod:`~PIL.PixarImagePlugin` Module +----------------------------------- .. automodule:: PIL.PixarImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PngImagePlugin` Module ----------------------------- +:mod:`~PIL.PngImagePlugin` Module +--------------------------------- .. automodule:: PIL.PngImagePlugin - :members: ChunkStream, PngImageFile, PngStream, getchunks, is_cid, putchunk - :show-inheritance: -.. autoclass:: PIL.PngImagePlugin.ChunkStream - :members: - :undoc-members: - :show-inheritance: -.. autoclass:: PIL.PngImagePlugin.PngImageFile - :members: - :undoc-members: - :show-inheritance: -.. autoclass:: PIL.PngImagePlugin.PngStream - :members: + :members: ChunkStream, PngImageFile, PngStream, getchunks, is_cid, putchunk, + MAX_TEXT_CHUNK, MAX_TEXT_MEMORY, APNG_BLEND_OP_SOURCE, APNG_BLEND_OP_OVER, + APNG_DISPOSE_OP_NONE, APNG_DISPOSE_OP_BACKGROUND, APNG_DISPOSE_OP_PREVIOUS :undoc-members: :show-inheritance: + :member-order: groupwise -:mod:`PpmImagePlugin` Module ----------------------------- +:mod:`~PIL.PpmImagePlugin` Module +--------------------------------- .. automodule:: PIL.PpmImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PsdImagePlugin` Module ----------------------------- +:mod:`~PIL.PsdImagePlugin` Module +--------------------------------- .. automodule:: PIL.PsdImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`SgiImagePlugin` Module ----------------------------- +:mod:`~PIL.SgiImagePlugin` Module +--------------------------------- .. automodule:: PIL.SgiImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`SpiderImagePlugin` Module -------------------------------- +:mod:`~PIL.SpiderImagePlugin` Module +------------------------------------ .. automodule:: PIL.SpiderImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`SunImagePlugin` Module ----------------------------- +:mod:`~PIL.SunImagePlugin` Module +--------------------------------- .. automodule:: PIL.SunImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`TgaImagePlugin` Module ----------------------------- +:mod:`~PIL.TgaImagePlugin` Module +--------------------------------- .. automodule:: PIL.TgaImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`TiffImagePlugin` Module ------------------------------ +:mod:`~PIL.TiffImagePlugin` Module +---------------------------------- .. automodule:: PIL.TiffImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`WebPImagePlugin` Module ------------------------------ +:mod:`~PIL.WebPImagePlugin` Module +---------------------------------- .. automodule:: PIL.WebPImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`WmfImagePlugin` Module ----------------------------- +:mod:`~PIL.WmfImagePlugin` Module +--------------------------------- .. automodule:: PIL.WmfImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`XVThumbImagePlugin` Module --------------------------------- +:mod:`~PIL.XVThumbImagePlugin` Module +------------------------------------- .. automodule:: PIL.XVThumbImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`XbmImagePlugin` Module ----------------------------- +:mod:`~PIL.XbmImagePlugin` Module +--------------------------------- .. automodule:: PIL.XbmImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`XpmImagePlugin` Module ----------------------------- +:mod:`~PIL.XpmImagePlugin` Module +--------------------------------- .. automodule:: PIL.XpmImagePlugin :members: diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index 931f9fd1e..03000528f 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -29,53 +29,53 @@ Image resizing filters Image resizing methods :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells which filter should be used for resampling. Possible values are: -:py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`, -:py:attr:`PIL.Image.BICUBIC` and :py:attr:`PIL.Image.ANTIALIAS`. +:py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BILINEAR`, +:py:data:`PIL.Image.BICUBIC` and :py:data:`PIL.Image.ANTIALIAS`. Almost all of them were changed in this version. Bicubic and bilinear downscaling ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -From the beginning :py:attr:`~PIL.Image.BILINEAR` and -:py:attr:`~PIL.Image.BICUBIC` filters were based on affine transformations +From the beginning :py:data:`~PIL.Image.BILINEAR` and +:py:data:`~PIL.Image.BICUBIC` filters were based on affine transformations and used a fixed number of pixels from the source image for every destination -pixel (2x2 pixels for :py:attr:`~PIL.Image.BILINEAR` and 4x4 for -:py:attr:`~PIL.Image.BICUBIC`). This gave an unsatisfactory result for +pixel (2x2 pixels for :py:data:`~PIL.Image.BILINEAR` and 4x4 for +:py:data:`~PIL.Image.BICUBIC`). This gave an unsatisfactory result for downscaling. At the same time, a high quality convolutions-based algorithm with -flexible kernel was used for :py:attr:`~PIL.Image.ANTIALIAS` filter. +flexible kernel was used for :py:data:`~PIL.Image.ANTIALIAS` filter. Starting from Pillow 2.7.0, a high quality convolutions-based algorithm is used for all of these three filters. If you have previously used any tricks to maintain quality when downscaling with -:py:attr:`~PIL.Image.BILINEAR` and :py:attr:`~PIL.Image.BICUBIC` filters +:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters (for example, reducing within several steps), they are unnecessary now. Antialias renamed to Lanczos ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A new :py:attr:`PIL.Image.LANCZOS` constant was added instead of -:py:attr:`~PIL.Image.ANTIALIAS`. +A new :py:data:`PIL.Image.LANCZOS` constant was added instead of +:py:data:`~PIL.Image.ANTIALIAS`. -When :py:attr:`~PIL.Image.ANTIALIAS` was initially added, it was the only +When :py:data:`~PIL.Image.ANTIALIAS` was initially added, it was the only high-quality filter based on convolutions. It's name was supposed to reflect this. Starting from Pillow 2.7.0 all resize method are based on convolutions. All of them are antialias from now on. And the real name of the -:py:attr:`~PIL.Image.ANTIALIAS` filter is Lanczos filter. +:py:data:`~PIL.Image.ANTIALIAS` filter is Lanczos filter. -The :py:attr:`~PIL.Image.ANTIALIAS` constant is left for backward compatibility -and is an alias for :py:attr:`~PIL.Image.LANCZOS`. +The :py:data:`~PIL.Image.ANTIALIAS` constant is left for backward compatibility +and is an alias for :py:data:`~PIL.Image.LANCZOS`. Lanczos upscaling quality ^^^^^^^^^^^^^^^^^^^^^^^^^ -The image upscaling quality with :py:attr:`~PIL.Image.LANCZOS` filter was -almost the same as :py:attr:`~PIL.Image.BILINEAR` due to bug. This has been fixed. +The image upscaling quality with :py:data:`~PIL.Image.LANCZOS` filter was +almost the same as :py:data:`~PIL.Image.BILINEAR` due to bug. This has been fixed. Bicubic upscaling quality ^^^^^^^^^^^^^^^^^^^^^^^^^ -The :py:attr:`~PIL.Image.BICUBIC` filter for affine transformations produced +The :py:data:`~PIL.Image.BICUBIC` filter for affine transformations produced sharp, slightly pixelated image for upscaling. Bicubic for convolutions is more soft. @@ -84,42 +84,42 @@ Resize performance In most cases, convolution is more a expensive algorithm for downscaling because it takes into account all the pixels of source image. Therefore -:py:attr:`~PIL.Image.BILINEAR` and :py:attr:`~PIL.Image.BICUBIC` filters' +:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters' performance can be lower than before. On the other hand the quality of -:py:attr:`~PIL.Image.BILINEAR` and :py:attr:`~PIL.Image.BICUBIC` was close to -:py:attr:`~PIL.Image.NEAREST`. So if such quality is suitable for your tasks -you can switch to :py:attr:`~PIL.Image.NEAREST` filter for downscaling, +:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` was close to +:py:data:`~PIL.Image.NEAREST`. So if such quality is suitable for your tasks +you can switch to :py:data:`~PIL.Image.NEAREST` filter for downscaling, which will give a huge improvement in performance. At the same time performance of convolution resampling for downscaling has been improved by around a factor of two compared to the previous version. -The upscaling performance of the :py:attr:`~PIL.Image.LANCZOS` filter has -remained the same. For :py:attr:`~PIL.Image.BILINEAR` filter it has improved by -1.5 times and for :py:attr:`~PIL.Image.BICUBIC` by four times. +The upscaling performance of the :py:data:`~PIL.Image.LANCZOS` filter has +remained the same. For :py:data:`~PIL.Image.BILINEAR` filter it has improved by +1.5 times and for :py:data:`~PIL.Image.BICUBIC` by four times. Default filter for thumbnails ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In Pillow 2.5 the default filter for :py:meth:`~PIL.Image.Image.thumbnail` was -changed from :py:attr:`~PIL.Image.NEAREST` to :py:attr:`~PIL.Image.ANTIALIAS`. +changed from :py:data:`~PIL.Image.NEAREST` to :py:data:`~PIL.Image.ANTIALIAS`. Antialias was chosen because all the other filters gave poor quality for -reduction. Starting from Pillow 2.7.0, :py:attr:`~PIL.Image.ANTIALIAS` has been -replaced with :py:attr:`~PIL.Image.BICUBIC`, because it's faster and -:py:attr:`~PIL.Image.ANTIALIAS` doesn't give any advantages after +reduction. Starting from Pillow 2.7.0, :py:data:`~PIL.Image.ANTIALIAS` has been +replaced with :py:data:`~PIL.Image.BICUBIC`, because it's faster and +:py:data:`~PIL.Image.ANTIALIAS` doesn't give any advantages after downscaling with libjpeg, which uses supersampling internally, not convolutions. Image transposition ------------------- -A new method :py:attr:`PIL.Image.TRANSPOSE` has been added for the +A new method :py:data:`PIL.Image.TRANSPOSE` has been added for the :py:meth:`~PIL.Image.Image.transpose` operation in addition to -:py:attr:`~PIL.Image.FLIP_LEFT_RIGHT`, :py:attr:`~PIL.Image.FLIP_TOP_BOTTOM`, -:py:attr:`~PIL.Image.ROTATE_90`, :py:attr:`~PIL.Image.ROTATE_180`, -:py:attr:`~PIL.Image.ROTATE_270`. :py:attr:`~PIL.Image.TRANSPOSE` is an algebra +:py:data:`~PIL.Image.FLIP_LEFT_RIGHT`, :py:data:`~PIL.Image.FLIP_TOP_BOTTOM`, +:py:data:`~PIL.Image.ROTATE_90`, :py:data:`~PIL.Image.ROTATE_180`, +:py:data:`~PIL.Image.ROTATE_270`. :py:data:`~PIL.Image.TRANSPOSE` is an algebra transpose, with an image reflected across its main diagonal. -The speed of :py:attr:`~PIL.Image.ROTATE_90`, :py:attr:`~PIL.Image.ROTATE_270` -and :py:attr:`~PIL.Image.TRANSPOSE` has been significantly improved for large +The speed of :py:data:`~PIL.Image.ROTATE_90`, :py:data:`~PIL.Image.ROTATE_270` +and :py:data:`~PIL.Image.TRANSPOSE` has been significantly improved for large images which don't fit in the processor cache. Gaussian blur and unsharp mask diff --git a/docs/releasenotes/4.1.0.rst b/docs/releasenotes/4.1.0.rst index dc5d73479..4d6598d8e 100644 --- a/docs/releasenotes/4.1.0.rst +++ b/docs/releasenotes/4.1.0.rst @@ -10,9 +10,9 @@ Several deprecated items have been removed. resolution', 'resolution unit', and 'date time' has been removed. Underscores should be used instead. -* The methods :py:meth:`PIL.ImageDraw.ImageDraw.setink`, - :py:meth:`PIL.ImageDraw.ImageDraw.setfill`, and - :py:meth:`PIL.ImageDraw.ImageDraw.setfont` have been removed. +* The methods ``PIL.ImageDraw.ImageDraw.setink``, + ``PIL.ImageDraw.ImageDraw.setfill``, and + ``PIL.ImageDraw.ImageDraw.setfont`` have been removed. Closing Files When Opening Images diff --git a/docs/releasenotes/4.2.0.rst b/docs/releasenotes/4.2.0.rst index e07fd9071..1e9637f1e 100644 --- a/docs/releasenotes/4.2.0.rst +++ b/docs/releasenotes/4.2.0.rst @@ -6,10 +6,9 @@ Added Complex Text Rendering Pillow now supports complex text rendering for scripts requiring glyph composition and bidirectional flow. This optional feature adds three -dependencies: harfbuzz, fribidi, and raqm. See the `install -documentation <../installation.html>`_ for further details. This feature is -tested and works on Unix and Mac, but has not yet been built on Windows -platforms. +dependencies: harfbuzz, fribidi, and raqm. See the :doc:`install documentation +<../installation>` for further details. This feature is tested and works on +Unix and Mac, but has not yet been built on Windows platforms. New Optional Parameters ======================= @@ -27,16 +26,16 @@ New DecompressionBomb Warning :py:meth:`PIL.Image.Image.crop` now may raise a DecompressionBomb warning if the crop region enlarges the image over the threshold -specified by :py:attr:`PIL.Image.MAX_PIXELS`. +specified by :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. Removed Deprecated Items ======================== Several deprecated items have been removed. -* The methods :py:meth:`PIL.ImageWin.Dib.fromstring`, - :py:meth:`PIL.ImageWin.Dib.tostring` and - :py:meth:`PIL.TiffImagePlugin.ImageFileDirectory_v2.as_dict` have +* The methods ``PIL.ImageWin.Dib.fromstring``, + ``PIL.ImageWin.Dib.tostring`` and + ``PIL.TiffImagePlugin.ImageFileDirectory_v2.as_dict`` have been removed. * Before Pillow 4.2.0, attempting to save an RGBA image as JPEG would diff --git a/docs/releasenotes/4.3.0.rst b/docs/releasenotes/4.3.0.rst index 6fa554e23..ea81fc45e 100644 --- a/docs/releasenotes/4.3.0.rst +++ b/docs/releasenotes/4.3.0.rst @@ -124,7 +124,7 @@ This release contains several performance improvements: * ``Image.transpose`` has been accelerated 15% or more by using a cache friendly algorithm. * ImageFilters based on Kernel convolution are significantly faster - due to the new MultibandFilter feature. + due to the new :py:class:`~PIL.ImageFilter.MultibandFilter` feature. * All memory allocation for images is now done in blocks, rather than falling back to an allocation for each scan line for images larger than the block size. diff --git a/docs/releasenotes/5.3.0.rst b/docs/releasenotes/5.3.0.rst index cce671c32..bff56566b 100644 --- a/docs/releasenotes/5.3.0.rst +++ b/docs/releasenotes/5.3.0.rst @@ -34,7 +34,7 @@ Curved joints for line sequences ``ImageDraw.Draw.line`` draws a line, or lines, between points. Previously, when multiple points are given, for a larger ``width``, the joints between these lines looked unsightly. There is now an additional optional argument, -``joint``, defaulting to ``None``. When it is set to ``curved``, the joints +``joint``, defaulting to :data:`None`. When it is set to ``curved``, the joints between the lines will become rounded. ImageOps.colorize diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst index 0145347f2..3e3b945a0 100644 --- a/docs/releasenotes/6.0.0.rst +++ b/docs/releasenotes/6.0.0.rst @@ -17,7 +17,7 @@ Removed deprecated PIL.OleFileIO PIL.OleFileIO was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of the upstream olefile Python package, and replaced with an ``ImportError``. The deprecated file has now been removed from Pillow. If needed, install from PyPI (eg. -``pip install olefile``). +``python3 -m pip install olefile``). Removed deprecated ImageOps functions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index c31ade197..faf84e6bd 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -69,6 +69,27 @@ There has been a longstanding warning that the defaults of ``Image.frombuffer`` may change in the future for the "raw" decoder. The change will now take place in Pillow 7.0. +Security +======== + +This release catches several buffer overruns, as well as addressing +CVE-2019-16865. The CVE is regarding DOS problems, such as consuming large +amounts of memory, or taking a large amount of time to process an image. + +In RawDecode.c, an error is now thrown if skip is calculated to be less than +zero. It is intended to skip padding between lines, not to go backwards. + +In PsdImagePlugin, if the combined sizes of the individual parts is larger than +the declared size of the extra data field, then it looked for the next layer by +seeking backwards. This is now corrected by seeking to (the start of the layer ++ the size of the extra data field) instead of (the read parts of the layer + +the rest of the layer). + +Decompression bomb checks have been added to GIF and ICO formats. + +An error is now raised if a TIFF dimension is a string, rather than trying to +perform operations on it. + Other Changes ============= diff --git a/docs/releasenotes/6.2.2.rst b/docs/releasenotes/6.2.2.rst new file mode 100644 index 000000000..a138c7d60 --- /dev/null +++ b/docs/releasenotes/6.2.2.rst @@ -0,0 +1,17 @@ +6.2.2 +----- + +Security +======== + +This release addresses several security problems. + +CVE-2019-19911 is regarding FPX images. If an image reports that it has a large number +of bands, a large amount of resources will be used when trying to process the +image. This is fixed by limiting the number of bands to those usable by Pillow. + +Buffer overruns were found when processing an SGI (CVE-2020-5311), PCX (CVE-2020-5312) +or FLI image (CVE-2020-5313). Checks have been added to prevent this. + +CVE-2020-5310: Overflow checks have been added when calculating the size of a memory +block to be reallocated in the processing of a TIFF image. diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 386a26757..80002b0ce 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -4,6 +4,11 @@ Backwards Incompatible Changes ============================== +Python 2.7 +^^^^^^^^^^ + +Pillow has dropped support for Python 2.7, which reached end-of-life on 2020-01-01. + PILLOW_VERSION constant ^^^^^^^^^^^^^^^^^^^^^^^ @@ -47,17 +52,30 @@ Setting the size of TIFF images Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws an error. Use ``Image.resize`` instead. +Default resampling filter +^^^^^^^^^^^^^^^^^^^^^^^^^ -API Changes -=========== +The default resampling filter has been changed to the high-quality convolution +``Image.BICUBIC`` instead of ``Image.NEAREST``, for the :py:meth:`~PIL.Image.Image.resize` +method and the :py:meth:`~PIL.ImageOps.pad`, :py:meth:`~PIL.ImageOps.scale` +and :py:meth:`~PIL.ImageOps.fit` functions. +``Image.NEAREST`` is still always used for images in "P" and "1" modes. +See :ref:`concept-filters` to learn the difference. In short, +``Image.NEAREST`` is a very fast filter, but simple and low-quality. -Deprecations -^^^^^^^^^^^^ +Image.draft() return value +^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO -~~~~ +If the :py:meth:`~PIL.Image.Image.draft` method has no effect, it returns :data:`None`. +If it does have an effect, then it previously returned the image itself. +However, unlike other `chain methods`_, :py:meth:`~PIL.Image.Image.draft` does not +return a modified version of the image, but modifies it in-place. So instead, if +:py:meth:`~PIL.Image.Image.draft` has an effect, Pillow will now return a tuple +of the image mode and a co-ordinate box. The box is the original coordinates in the +bounds of resulting image. This may be useful in a subsequent +:py:meth:`~PIL.Image.Image.resize` call. -TODO +.. _chain methods: https://en.wikipedia.org/wiki/Method_chaining API Additions @@ -67,7 +85,46 @@ Custom unidentified image error ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be -identified. For backwards compatibility, this will inherit from ``IOError``. +identified. For backwards compatibility, this will inherit from ``OSError``. + +New argument ``reducing_gap`` for Image.resize() and Image.thumbnail() methods +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Speeds up resizing by resizing the image in two steps. The bigger ``reducing_gap``, +the closer the result to the fair resampling. The smaller ``reducing_gap``, +the faster resizing. With ``reducing_gap`` greater or equal to 3.0, +the result is indistinguishable from fair resampling. + +The default value for :py:meth:`~PIL.Image.Image.resize` is :data:`None`, +which means that the optimization is turned off by default. + +The default value for :py:meth:`~PIL.Image.Image.thumbnail` is 2.0, +which is very close to fair resampling while still being faster in many cases. +In addition, the same gap is applied when :py:meth:`~PIL.Image.Image.thumbnail` +calls :py:meth:`~PIL.Image.Image.draft`, which may greatly improve the quality +of JPEG thumbnails. As a result, :py:meth:`~PIL.Image.Image.thumbnail` +in the new version provides equally high speed and high quality from any +source (JPEG or arbitrary images). + +New Image.reduce() method +^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.reduce` is a highly efficient operation +to reduce an image by integer times. Normally, it shouldn't be used directly. +Used internally by :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` +methods to speed up resize when a new argument ``reducing_gap`` is set. + +Loading WMF images at a given DPI +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +On Windows, Pillow can read WMF files, with a default DPI of 72. An image can +now also be loaded at another resolution: + +.. code-block:: python + + from PIL import Image + with Image.open("drawing.wmf") as im: + im.load(dpi=144) Other Changes ============= @@ -76,8 +133,8 @@ Image.__del__ ^^^^^^^^^^^^^ Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. -Use a context manager or call ``Image.close()`` instead to close the file in a -deterministic way. +Use a context manager or call :py:meth:`~PIL.Image.Image.close` instead to close +the file in a deterministic way. Previous method: @@ -92,3 +149,13 @@ Use instead: with Image.open("hopper.png") as im: im.save("out.jpg") + +Better thumbnail geometry +^^^^^^^^^^^^^^^^^^^^^^^^^ + +When calculating the new dimensions in :py:meth:`~PIL.Image.Image.thumbnail`, +round to the nearest integer, instead of always rounding down. +This better preserves the original aspect ratio. + +When the image width or height is not divisible by 8 the last row and column +in the image get the correct weight after JPEG DCT scaling. diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst new file mode 100644 index 000000000..35e5c4464 --- /dev/null +++ b/docs/releasenotes/7.1.0.rst @@ -0,0 +1,102 @@ +7.1.0 +----- + +API Changes +=========== + +Allow saving of zero quality JPEG images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If no quality was specified when saving a JPEG, Pillow internally used a value +of zero to indicate that the default quality should be used. However, this +removed the ability to actually save a JPEG with zero quality. This has now +been resolved. + +.. code-block:: python + + from PIL import Image + im = Image.open("hopper.jpg") + im.save("out.jpg", quality=0) + +API Additions +============= + +New channel operations +^^^^^^^^^^^^^^^^^^^^^^ + +Three new channel operations have been added: :py:meth:`~PIL.ImageChops.soft_light`, +:py:meth:`~PIL.ImageChops.hard_light` and :py:meth:`~PIL.ImageChops.overlay`. + +PILLOW_VERSION constant +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been re-added but is deprecated and will be removed in a future +release. Use ``__version__`` instead. + +It was initially removed in Pillow 7.0.0, but brought back in 7.1.0 to give projects +more time to upgrade. + +Reading JPEG comments +^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info`. + +Support for different charset encodings in PcfFontFile +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously ``PcfFontFile`` output only bitmap PIL fonts with ISO 8859-1 encoding, even +though the PCF format supports Unicode, making it hard to work with Pillow with bitmap +fonts in languages which use different character sets. + +Now it's possible to set a different charset encoding in ``PcfFontFile``'s class +constructor. By default, it generates a PIL font file with ISO 8859-1 as before. The +generated PIL font file still contains up to 256 characters, but the character set is +different depending on the selected encoding. + +To use such a font with ``ImageDraw.text``, call it with a bytes object with the same +encoding as the font file. + +X11 ImageGrab.grab() +^^^^^^^^^^^^^^^^^^^^ +Support has been added for ``ImageGrab.grab()`` on Linux using the X server +with the XCB library. + +An optional ``xdisplay`` parameter has been added to select the X server, +with the default value of :data:`None` using the default X server. + +Passing a different value on Windows or macOS will force taking a snapshot +using the selected X server; pass an empty string to use the default X server. +XCB support is not included in pre-compiled wheels for Windows and macOS. + +Security +======== + +This release includes security fixes. + +* CVE-2020-10177 Fix multiple OOB reads in FLI decoding +* CVE-2020-10378 Fix bounds overflow in PCX decoding +* CVE-2020-10379 Fix two buffer overflows in TIFF decoding +* CVE-2020-10994 Fix bounds overflow in JPEG 2000 decoding +* CVE-2020-11538 Fix buffer overflow in SGI-RLE decoding + +Other Changes +============= + +If present, only use alpha channel for bounding box +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When the :py:meth:`~PIL.Image.Image.getbbox` method calculates the bounding +box, for an RGB image it trims black pixels. Similarly, for an RGBA image it +would trim black transparent pixels. This is now changed so that if an image +has an alpha channel (RGBA, RGBa, PA, LA, La), any transparent pixels are +trimmed. + +Improved APNG support +^^^^^^^^^^^^^^^^^^^^^ + +Added support for reading and writing Animated Portable Network Graphics (APNG) images. +The PNG plugin now supports using the :py:meth:`~PIL.Image.Image.seek` method and the +:py:class:`~PIL.ImageSequence.Iterator` class to read APNG frame sequences. +The PNG plugin also now supports using the ``append_images`` argument to write APNG frame +sequences. See :ref:`apng-sequences` for further details. diff --git a/docs/releasenotes/7.1.1.rst b/docs/releasenotes/7.1.1.rst new file mode 100644 index 000000000..4c33adf8e --- /dev/null +++ b/docs/releasenotes/7.1.1.rst @@ -0,0 +1,25 @@ +7.1.1 +----- + +Fix regression seeking PNG files +================================ + +This fixes a regression introduced in 7.1.0 when adding support for APNG files when calling +``seek`` and ``tell``: + +.. code-block:: python + + >>> from PIL import Image + >>> with Image.open("Tests/images/hopper.png") as im: + ... im.seek(0) + ... + Traceback (most recent call last): + File "", line 2, in + File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/PIL/PngImagePlugin.py", line 739, in seek + if not self._seek_check(frame): + File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/PIL/ImageFile.py", line 306, in _seek_check + return self.tell() != frame + File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/PIL/PngImagePlugin.py", line 827, in tell + return self.__frame + AttributeError: 'PngImageFile' object has no attribute '_PngImageFile__frame' + >>> diff --git a/docs/releasenotes/7.1.2.rst b/docs/releasenotes/7.1.2.rst new file mode 100644 index 000000000..c9d0d54eb --- /dev/null +++ b/docs/releasenotes/7.1.2.rst @@ -0,0 +1,16 @@ +7.1.2 +----- + +Fix another regression seeking PNG files +======================================== + +This fixes a regression introduced in 7.1.0 when adding support for APNG files. + +When calling ``seek(n)`` on a regular PNG where ``n > 0``, it failed to raise an +``EOFError`` as it should have done, resulting in: + +.. code-block:: python + + AttributeError: 'NoneType' object has no attribute 'read' + +Pillow 7.1.2 now raises the correct exception. diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst new file mode 100644 index 000000000..ff1b7c9e7 --- /dev/null +++ b/docs/releasenotes/7.2.0.rst @@ -0,0 +1,58 @@ +7.2.0 +----- + +API Changes +=========== + +Replaced TiffImagePlugin DEBUG with logging +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``TiffImagePlugin.DEBUG = True`` has been a way to print various debugging +information when interacting with TIFF images. This has now been removed +in favour of Python's ``logging`` module, already used in other places in the +Pillow source code. + +Corrected default offset when writing EXIF data +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, the default ``offset`` argument for +:py:meth:`~PIL.Image.Exif.tobytes` was 0, which did not include the magic +header. It is now 8. + +Moved to ImageFileDirectory_v2 in Image.Exif +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Moved from the legacy :py:class:`PIL.TiffImagePlugin.ImageFileDirectory_v1` to +:py:class:`PIL.TiffImagePlugin.ImageFileDirectory_v2` in +:py:class:`PIL.Image.Exif`. This means that Exif RATIONALs and SIGNED_RATIONALs +are now read as :py:class:`PIL.TiffImagePlugin.IFDRational`, instead of as a +tuple with a numerator and a denominator. + +TIFF BYTE tags format +^^^^^^^^^^^^^^^^^^^^^ + +TIFF BYTE tags were previously read as a tuple containing a bytestring. They +are now read as just a single bytestring. + +Deprecations +^^^^^^^^^^^^ + +Image.show command parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``command`` parameter was deprecated and will be removed in a future release. +Use a subclass of :py:class:`PIL.ImageShow.Viewer` instead. + +Image._showxv +~~~~~~~~~~~~~ + +``Image._showxv`` has been deprecated. Use :py:meth:`~PIL.Image.Image.show` +instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add +a custom :py:class:`~PIL.ImageShow.Viewer` class. + +ImageFile.raise_ioerror +~~~~~~~~~~~~~~~~~~~~~~~ + +``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror`` +is now deprecated and will be removed in a future release. Use +``ImageFile.raise_oserror`` instead. diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst new file mode 100644 index 000000000..1bef62e00 --- /dev/null +++ b/docs/releasenotes/8.0.0.rst @@ -0,0 +1,179 @@ +8.0.0 +----- + +Backwards Incompatible Changes +============================== + +Python 3.5 +^^^^^^^^^^ + +Pillow has dropped support for Python 3.5, which reached end-of-life on 2020-09-13. + +PyPy 7.1.x +^^^^^^^^^^ + +Pillow has dropped support for PyPy3 7.1.1. +PyPy3 7.2.0, released on 2019-10-14, is now the minimum compatible version. + +im.offset +^^^^^^^^^ + +``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead. + +Image.fromstring, im.fromstring and im.tostring +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``Image.fromstring()`` has been removed, call :py:func:`.Image.frombytes()` instead. +* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead. +* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead. + +ImageCms.CmsProfile attributes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed: + +======================== =================================================== +Removed Use instead +======================== =================================================== +``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space` +``pcs`` Padded :py:attr:`~.CmsProfile.connection_space` +``product_copyright`` Unicode :py:attr:`~.CmsProfile.copyright` +``product_desc`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_description`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_manufacturer`` Unicode :py:attr:`~.CmsProfile.manufacturer` +``product_model`` Unicode :py:attr:`~.CmsProfile.model` +======================== =================================================== + +API Changes +=========== + +ImageDraw.text: stroke_width +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Fixed issue where passing ``stroke_width`` with a non-zero value +to :py:meth:`.ImageDraw.text` would cause the text to be offset by that amount. + +ImageDraw.text: anchor +^^^^^^^^^^^^^^^^^^^^^^ + +The ``anchor`` parameter of :py:meth:`.ImageDraw.text` has been implemented. + +Use this parameter to change the position of text relative to the +specified ``xy`` point. See :ref:`text-anchors` for details. + +Add MIME type to PsdImagePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +"image/vnd.adobe.photoshop" is now registered as the +:py:class:`.PsdImagePlugin.PsdImageFile` MIME type. + +API Additions +============= + +Image.open: add formats parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Added a new ``formats`` parameter to :py:func:`.Image.open`: + +* A list or tuple of formats to attempt to load the file in. + This can be used to restrict the set of formats checked. + Pass ``None`` to try all supported formats. You can print the set of + available formats by running ``python -m PIL`` or using + the :py:func:`PIL.features.pilinfo` function. + +ImageOps.autocontrast: add mask parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:func:`.ImageOps.autocontrast` can now take a ``mask`` parameter: + +* Histogram used in contrast operation is computed using pixels within the mask. + If no mask is given the entire image is used for histogram computation. + +ImageOps.autocontrast cutoffs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, the ``cutoff`` parameter of :py:func:`.ImageOps.autocontrast` could only +be a single number, used as the percent to cut off from the histogram on the low and +high ends. + +Now, it can also be a tuple ``(low, high)``. + +ImageDraw.regular_polygon +^^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method :py:meth:`.ImageDraw.regular_polygon`, draws a regular polygon of ``n_sides``, inscribed in a ``bounding_circle``. + +For example ``draw.regular_polygon(((100, 100), 50), 5)`` +draws a pentagon centered at the point ``(100, 100)`` with a polygon radius of ``50``. + +ImageDraw.text: embedded_color +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The methods :py:meth:`.ImageDraw.text` and :py:meth:`.ImageDraw.multiline_text` +now support fonts with embedded color data. + +To render text with embedded color data, use the parameter ``embedded_color=True``. + +Support for CBDT fonts requires FreeType 2.5 compiled with libpng. +Support for COLR fonts requires FreeType 2.10. +SBIX and SVG fonts are not yet supported. + +ImageDraw.textlength +^^^^^^^^^^^^^^^^^^^^ + +Two new methods :py:meth:`.ImageDraw.textlength` and :py:meth:`.FreeTypeFont.getlength` +were added, returning the exact advance length of text with 1/64 pixel precision. + +These can be used for word-wrapping or rendering text in parts. + +ImageDraw.textbbox +^^^^^^^^^^^^^^^^^^ + +Three new methods :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.multiline_textbbox`, +and :py:meth:`.FreeTypeFont.getbbox` return the bounding box of rendered text. + +These functions accept an ``anchor`` parameter, see :ref:`text-anchors` for details. + +Other Changes +============= + +Improved ellipse-drawing algorithm +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ellipse-drawing algorithm has been changed from drawing a 360-sided polygon to one +which resembles Bresenham's algorithm for circles. It should be faster and produce +smoother curves, especially for smaller ellipses. + +ImageDraw.text and ImageDraw.multiline_text +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Fixed multiple issues in methods :py:meth:`.ImageDraw.text` and :py:meth:`.ImageDraw.multiline_text` +sometimes causing unexpected text alignment issues. + +The ``align`` parameter of :py:meth:`.ImageDraw.multiline_text` now gives better results in some cases. + +TrueType fonts with embedded bitmaps are now supported. + +Added writing of subIFDs +^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving EXIF data, Pillow is now able to write subIFDs, such as the GPS IFD. This +should happen automatically when saving an image using the EXIF data that it was opened +with, such as in :py:meth:`~PIL.ImageOps.exif_transpose`. + +Previously, the code of the first tag of the subIFD was incorrectly written as the +offset. + +Error for large BMP files +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, if a BMP file was too large, an ``OSError`` would be raised. Now, +``DecompressionBombError`` is used instead, as Pillow already uses for other formats. + +Dark theme for docs +^^^^^^^^^^^^^^^^^^^ + +The https://pillow.readthedocs.io documentation will use a dark theme if the the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query. + + + diff --git a/docs/releasenotes/8.0.1.rst b/docs/releasenotes/8.0.1.rst new file mode 100644 index 000000000..e50596e5c --- /dev/null +++ b/docs/releasenotes/8.0.1.rst @@ -0,0 +1,25 @@ +8.0.1 +----- + +Security +======== + +Update FreeType used in binary wheels to `2.10.4`_ to fix CVE-2020-15999_: + + - A heap buffer overflow has been found in the handling of embedded PNG bitmaps, + introduced in FreeType version 2.6. + + https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-15999 + + If you use option ``FT_CONFIG_OPTION_USE_PNG`` you should upgrade immediately. + +We strongly recommend updating to Pillow 8.0.1 if you are using Pillow 8.0.0, which improved support for bitmap fonts. + +In Pillow 7.2.0 and earlier bitmap fonts were disabled with ``FT_LOAD_NO_BITMAP``, but it is not +clear if this prevents the exploit and we recommend updating to Pillow 8.0.1. + +Pillow 8.0.0 and earlier are potentially vulnerable releases, including the last release +to support Python 2.7, namely Pillow 6.2.2. + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ +.. _CVE-2020-15999: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-15999 diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 6f8ecb95c..5c74bed9b 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -1,12 +1,26 @@ Release Notes ============= +Pillow is released quarterly on January 2nd, April 1st, July 1st and October 15th. +Patch releases are created if the latest release contains severe bugs, or if security +fixes are put together before a scheduled release. + +Please use the latest version of Pillow. Functionality and security fixes should not be +expected to be backported to earlier versions. + .. note:: Contributors please include release notes as needed or appropriate with your bug fixes, feature additions and tests. .. toctree:: :maxdepth: 2 + 8.0.1 + 8.0.0 + 7.2.0 + 7.1.2 + 7.1.1 + 7.1.0 7.0.0 + 6.2.2 6.2.1 6.2.0 6.1.0 diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst new file mode 100644 index 000000000..bf381114e --- /dev/null +++ b/docs/releasenotes/template.rst @@ -0,0 +1,45 @@ +x.y.z +----- + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/resources/anchor_horizontal.svg b/docs/resources/anchor_horizontal.svg new file mode 100644 index 000000000..a0648a10c --- /dev/null +++ b/docs/resources/anchor_horizontal.svg @@ -0,0 +1,467 @@ + + + + + Pillow horizontal text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Pillow horizontal text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (d) descender + (s) baseline + (a) ascender + (m) middle + (t) top + (b) bottom + (l) left + (r) right + (m) middle + + + Horizontal text + + diff --git a/docs/resources/anchor_vertical.svg b/docs/resources/anchor_vertical.svg new file mode 100644 index 000000000..95da30ffd --- /dev/null +++ b/docs/resources/anchor_vertical.svg @@ -0,0 +1,841 @@ + + + + + Pillow vertical text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Pillow vertical text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (l)left + (s) baseline + (r)right + (t) top + (m) middle + (b) bottom + (m)middle + (l)left + (s) baseline + (r)right + (t) top + (m) middle + (b) bottom + (m)middle + + + Verticaltext + + diff --git a/docs/resources/css/dark.css b/docs/resources/css/dark.css new file mode 100644 index 000000000..c746e76cf --- /dev/null +++ b/docs/resources/css/dark.css @@ -0,0 +1,1996 @@ +@media (prefers-color-scheme: dark) { + html { + background-color: #181a1b !important; + } + + html, body, input, textarea, select, button { + background-color: #181a1b; + } + + html, body, input, textarea, select, button { + border-color: #736b5e; + color: #e8e6e3; + } + + a { + color: #3391ff; + } + + table { + border-color: #545b5e; + } + + ::placeholder { + color: #b2aba1; + } + + input:-webkit-autofill, + textarea:-webkit-autofill, + select:-webkit-autofill { + background-color: #555b00 !important; + color: #e8e6e3 !important; + } + + ::selection { + background-color: #004daa !important; + color: #e8e6e3 !important; + } + + ::-moz-selection { + background-color: #004daa !important; + color: #e8e6e3 !important; + } + + /* Invert Style */ + .jfk-bubble.gtx-bubble, embed[type="application/pdf"] { + filter: invert(100%) hue-rotate(180deg) contrast(90%) !important; + } + + /* Override Style */ + .vimvixen-hint { + background-color: #7b5300 !important; + border-color: #d8b013 !important; + color: #f3e8c8 !important; + } + + ::placeholder { + opacity: 0.5 !important; + } + + /* Variables Style */ + :root { + --darkreader-neutral-background: #181a1b; + --darkreader-neutral-text: #e8e6e3; + --darkreader-selection-background: #004daa; + --darkreader-selection-text: #e8e6e3; + } + + /* Modified CSS */ + a:hover, + a:active { + outline-color: initial; + } + + abbr[title] { + border-bottom-color: initial; + } + + ins { + background-image: initial; + background-color: rgb(112, 112, 0); + color: rgb(232, 230, 227); + text-decoration-color: initial; + } + + mark { + background-image: initial; + background-color: rgb(204, 204, 0); + color: rgb(232, 230, 227); + } + + ul, + ol, + dl { + list-style-image: none; + } + + li { + list-style-image: initial; + } + + img { + border-color: initial; + } + + fieldset { + border-color: initial; + } + + legend { + border-color: initial; + } + + .chromeframe { + background-image: initial; + background-color: rgb(53, 57, 59); + color: rgb(232, 230, 227); + } + + .ir { + border-color: initial; + background-color: transparent; + } + + .visuallyhidden { + border-color: initial; + } + + .fa-border { + border-color: rgb(53, 57, 59); + } + + .fa-inverse { + color: rgb(232, 230, 227); + } + + .sr-only { + border-color: initial; + } + + .fa::before, + .wy-menu-vertical li span.toctree-expand::before, + .wy-menu-vertical li.on a span.toctree-expand::before, + .wy-menu-vertical li.current > a span.toctree-expand::before, + .rst-content .admonition-title::before, + .rst-content h1 .headerlink::before, + .rst-content h2 .headerlink::before, + .rst-content h3 .headerlink::before, + .rst-content h4 .headerlink::before, + .rst-content h5 .headerlink::before, + .rst-content h6 .headerlink::before, + .rst-content dl dt .headerlink::before, + .rst-content p.caption .headerlink::before, + .rst-content table > caption .headerlink::before, + .rst-content .code-block-caption .headerlink::before, + .rst-content tt.download span:first-child::before, + .rst-content code.download span:first-child::before, + .icon::before, + .wy-dropdown .caret::before, + .wy-inline-validate.wy-inline-validate-success .wy-input-context::before, + .wy-inline-validate.wy-inline-validate-danger .wy-input-context::before, + .wy-inline-validate.wy-inline-validate-warning .wy-input-context::before, + .wy-inline-validate.wy-inline-validate-info .wy-input-context::before { + text-decoration-color: inherit; + } + + a .fa, + a .wy-menu-vertical li span.toctree-expand, + .wy-menu-vertical li a span.toctree-expand, + .wy-menu-vertical li.on a span.toctree-expand, + .wy-menu-vertical li.current > a span.toctree-expand, + a .rst-content .admonition-title, + .rst-content a .admonition-title, + a .rst-content h1 .headerlink, + .rst-content h1 a .headerlink, + a .rst-content h2 .headerlink, + .rst-content h2 a .headerlink, + a .rst-content h3 .headerlink, + .rst-content h3 a .headerlink, + a .rst-content h4 .headerlink, + .rst-content h4 a .headerlink, + a .rst-content h5 .headerlink, + .rst-content h5 a .headerlink, + a .rst-content h6 .headerlink, + .rst-content h6 a .headerlink, + a .rst-content dl dt .headerlink, + .rst-content dl dt a .headerlink, + a .rst-content p.caption .headerlink, + .rst-content p.caption a .headerlink, + a .rst-content table > caption .headerlink, + .rst-content table > caption a .headerlink, + a .rst-content .code-block-caption .headerlink, + .rst-content .code-block-caption a .headerlink, + a .rst-content tt.download span:first-child, + .rst-content tt.download a span:first-child, + a .rst-content code.download span:first-child, + .rst-content code.download a span:first-child, + a .icon { + text-decoration-color: inherit; + } + + .wy-alert, + .rst-content .note, + .rst-content .attention, + .rst-content .caution, + .rst-content .danger, + .rst-content .error, + .rst-content .hint, + .rst-content .important, + .rst-content .tip, + .rst-content .warning, + .rst-content .seealso, + .rst-content .admonition-todo, + .rst-content .admonition { + background-image: initial; + background-color: rgb(32, 35, 36); + } + + .wy-alert-title, + .rst-content .admonition-title { + color: rgb(232, 230, 227); + background-image: initial; + background-color: rgb(29, 91, 131); + } + + .wy-alert.wy-alert-danger, + .rst-content .wy-alert-danger.note, + .rst-content .wy-alert-danger.attention, + .rst-content .wy-alert-danger.caution, + .rst-content .danger, + .rst-content .error, + .rst-content .wy-alert-danger.hint, + .rst-content .wy-alert-danger.important, + .rst-content .wy-alert-danger.tip, + .rst-content .wy-alert-danger.warning, + .rst-content .wy-alert-danger.seealso, + .rst-content .wy-alert-danger.admonition-todo, + .rst-content .wy-alert-danger.admonition { + background-image: initial; + background-color: rgb(52, 12, 8); + } + + .wy-alert.wy-alert-danger .wy-alert-title, + .rst-content .wy-alert-danger.note .wy-alert-title, + .rst-content .wy-alert-danger.attention .wy-alert-title, + .rst-content .wy-alert-danger.caution .wy-alert-title, + .rst-content .danger .wy-alert-title, + .rst-content .error .wy-alert-title, + .rst-content .wy-alert-danger.hint .wy-alert-title, + .rst-content .wy-alert-danger.important .wy-alert-title, + .rst-content .wy-alert-danger.tip .wy-alert-title, + .rst-content .wy-alert-danger.warning .wy-alert-title, + .rst-content .wy-alert-danger.seealso .wy-alert-title, + .rst-content .wy-alert-danger.admonition-todo .wy-alert-title, + .rst-content .wy-alert-danger.admonition .wy-alert-title, + .wy-alert.wy-alert-danger .rst-content .admonition-title, + .rst-content .wy-alert.wy-alert-danger .admonition-title, + .rst-content .wy-alert-danger.note .admonition-title, + .rst-content .wy-alert-danger.attention .admonition-title, + .rst-content .wy-alert-danger.caution .admonition-title, + .rst-content .danger .admonition-title, + .rst-content .error .admonition-title, + .rst-content .wy-alert-danger.hint .admonition-title, + .rst-content .wy-alert-danger.important .admonition-title, + .rst-content .wy-alert-danger.tip .admonition-title, + .rst-content .wy-alert-danger.warning .admonition-title, + .rst-content .wy-alert-danger.seealso .admonition-title, + .rst-content .wy-alert-danger.admonition-todo .admonition-title, + .rst-content .wy-alert-danger.admonition .admonition-title { + background-image: initial; + background-color: rgb(108, 22, 13); + } + + .wy-alert.wy-alert-warning, + .rst-content .wy-alert-warning.note, + .rst-content .attention, + .rst-content .caution, + .rst-content .wy-alert-warning.danger, + .rst-content .wy-alert-warning.error, + .rst-content .wy-alert-warning.hint, + .rst-content .wy-alert-warning.important, + .rst-content .wy-alert-warning.tip, + .rst-content .warning, + .rst-content .wy-alert-warning.seealso, + .rst-content .admonition-todo, + .rst-content .wy-alert-warning.admonition { + background-image: initial; + background-color: rgb(82, 53, 0); + } + + .wy-alert.wy-alert-warning .wy-alert-title, + .rst-content .wy-alert-warning.note .wy-alert-title, + .rst-content .attention .wy-alert-title, + .rst-content .caution .wy-alert-title, + .rst-content .wy-alert-warning.danger .wy-alert-title, + .rst-content .wy-alert-warning.error .wy-alert-title, + .rst-content .wy-alert-warning.hint .wy-alert-title, + .rst-content .wy-alert-warning.important .wy-alert-title, + .rst-content .wy-alert-warning.tip .wy-alert-title, + .rst-content .warning .wy-alert-title, + .rst-content .wy-alert-warning.seealso .wy-alert-title, + .rst-content .admonition-todo .wy-alert-title, + .rst-content .wy-alert-warning.admonition .wy-alert-title, + .wy-alert.wy-alert-warning .rst-content .admonition-title, + .rst-content .wy-alert.wy-alert-warning .admonition-title, + .rst-content .wy-alert-warning.note .admonition-title, + .rst-content .attention .admonition-title, + .rst-content .caution .admonition-title, + .rst-content .wy-alert-warning.danger .admonition-title, + .rst-content .wy-alert-warning.error .admonition-title, + .rst-content .wy-alert-warning.hint .admonition-title, + .rst-content .wy-alert-warning.important .admonition-title, + .rst-content .wy-alert-warning.tip .admonition-title, + .rst-content .warning .admonition-title, + .rst-content .wy-alert-warning.seealso .admonition-title, + .rst-content .admonition-todo .admonition-title, + .rst-content .wy-alert-warning.admonition .admonition-title { + background-image: initial; + background-color: rgb(123, 65, 14); + } + + .wy-alert.wy-alert-info, + .rst-content .note, + .rst-content .wy-alert-info.attention, + .rst-content .wy-alert-info.caution, + .rst-content .wy-alert-info.danger, + .rst-content .wy-alert-info.error, + .rst-content .wy-alert-info.hint, + .rst-content .wy-alert-info.important, + .rst-content .wy-alert-info.tip, + .rst-content .wy-alert-info.warning, + .rst-content .seealso, + .rst-content .wy-alert-info.admonition-todo, + .rst-content .wy-alert-info.admonition { + background-image: initial; + background-color: rgb(32, 35, 36); + } + + .wy-alert.wy-alert-info .wy-alert-title, + .rst-content .note .wy-alert-title, + .rst-content .wy-alert-info.attention .wy-alert-title, + .rst-content .wy-alert-info.caution .wy-alert-title, + .rst-content .wy-alert-info.danger .wy-alert-title, + .rst-content .wy-alert-info.error .wy-alert-title, + .rst-content .wy-alert-info.hint .wy-alert-title, + .rst-content .wy-alert-info.important .wy-alert-title, + .rst-content .wy-alert-info.tip .wy-alert-title, + .rst-content .wy-alert-info.warning .wy-alert-title, + .rst-content .seealso .wy-alert-title, + .rst-content .wy-alert-info.admonition-todo .wy-alert-title, + .rst-content .wy-alert-info.admonition .wy-alert-title, + .wy-alert.wy-alert-info .rst-content .admonition-title, + .rst-content .wy-alert.wy-alert-info .admonition-title, + .rst-content .note .admonition-title, + .rst-content .wy-alert-info.attention .admonition-title, + .rst-content .wy-alert-info.caution .admonition-title, + .rst-content .wy-alert-info.danger .admonition-title, + .rst-content .wy-alert-info.error .admonition-title, + .rst-content .wy-alert-info.hint .admonition-title, + .rst-content .wy-alert-info.important .admonition-title, + .rst-content .wy-alert-info.tip .admonition-title, + .rst-content .wy-alert-info.warning .admonition-title, + .rst-content .seealso .admonition-title, + .rst-content .wy-alert-info.admonition-todo .admonition-title, + .rst-content .wy-alert-info.admonition .admonition-title { + background-image: initial; + background-color: rgb(29, 91, 131); + } + + .wy-alert.wy-alert-success, + .rst-content .wy-alert-success.note, + .rst-content .wy-alert-success.attention, + .rst-content .wy-alert-success.caution, + .rst-content .wy-alert-success.danger, + .rst-content .wy-alert-success.error, + .rst-content .hint, + .rst-content .important, + .rst-content .tip, + .rst-content .wy-alert-success.warning, + .rst-content .wy-alert-success.seealso, + .rst-content .wy-alert-success.admonition-todo, + .rst-content .wy-alert-success.admonition { + background-image: initial; + background-color: rgb(9, 66, 58); + } + + .wy-alert.wy-alert-success .wy-alert-title, + .rst-content .wy-alert-success.note .wy-alert-title, + .rst-content .wy-alert-success.attention .wy-alert-title, + .rst-content .wy-alert-success.caution .wy-alert-title, + .rst-content .wy-alert-success.danger .wy-alert-title, + .rst-content .wy-alert-success.error .wy-alert-title, + .rst-content .hint .wy-alert-title, + .rst-content .important .wy-alert-title, + .rst-content .tip .wy-alert-title, + .rst-content .wy-alert-success.warning .wy-alert-title, + .rst-content .wy-alert-success.seealso .wy-alert-title, + .rst-content .wy-alert-success.admonition-todo .wy-alert-title, + .rst-content .wy-alert-success.admonition .wy-alert-title, + .wy-alert.wy-alert-success .rst-content .admonition-title, + .rst-content .wy-alert.wy-alert-success .admonition-title, + .rst-content .wy-alert-success.note .admonition-title, + .rst-content .wy-alert-success.attention .admonition-title, + .rst-content .wy-alert-success.caution .admonition-title, + .rst-content .wy-alert-success.danger .admonition-title, + .rst-content .wy-alert-success.error .admonition-title, + .rst-content .hint .admonition-title, + .rst-content .important .admonition-title, + .rst-content .tip .admonition-title, + .rst-content .wy-alert-success.warning .admonition-title, + .rst-content .wy-alert-success.seealso .admonition-title, + .rst-content .wy-alert-success.admonition-todo .admonition-title, + .rst-content .wy-alert-success.admonition .admonition-title { + background-image: initial; + background-color: rgb(21, 150, 125); + } + + .wy-alert.wy-alert-neutral, + .rst-content .wy-alert-neutral.note, + .rst-content .wy-alert-neutral.attention, + .rst-content .wy-alert-neutral.caution, + .rst-content .wy-alert-neutral.danger, + .rst-content .wy-alert-neutral.error, + .rst-content .wy-alert-neutral.hint, + .rst-content .wy-alert-neutral.important, + .rst-content .wy-alert-neutral.tip, + .rst-content .wy-alert-neutral.warning, + .rst-content .wy-alert-neutral.seealso, + .rst-content .wy-alert-neutral.admonition-todo, + .rst-content .wy-alert-neutral.admonition { + background-image: initial; + background-color: rgb(27, 36, 36); + } + + .wy-alert.wy-alert-neutral .wy-alert-title, + .rst-content .wy-alert-neutral.note .wy-alert-title, + .rst-content .wy-alert-neutral.attention .wy-alert-title, + .rst-content .wy-alert-neutral.caution .wy-alert-title, + .rst-content .wy-alert-neutral.danger .wy-alert-title, + .rst-content .wy-alert-neutral.error .wy-alert-title, + .rst-content .wy-alert-neutral.hint .wy-alert-title, + .rst-content .wy-alert-neutral.important .wy-alert-title, + .rst-content .wy-alert-neutral.tip .wy-alert-title, + .rst-content .wy-alert-neutral.warning .wy-alert-title, + .rst-content .wy-alert-neutral.seealso .wy-alert-title, + .rst-content .wy-alert-neutral.admonition-todo .wy-alert-title, + .rst-content .wy-alert-neutral.admonition .wy-alert-title, + .wy-alert.wy-alert-neutral .rst-content .admonition-title, + .rst-content .wy-alert.wy-alert-neutral .admonition-title, + .rst-content .wy-alert-neutral.note .admonition-title, + .rst-content .wy-alert-neutral.attention .admonition-title, + .rst-content .wy-alert-neutral.caution .admonition-title, + .rst-content .wy-alert-neutral.danger .admonition-title, + .rst-content .wy-alert-neutral.error .admonition-title, + .rst-content .wy-alert-neutral.hint .admonition-title, + .rst-content .wy-alert-neutral.important .admonition-title, + .rst-content .wy-alert-neutral.tip .admonition-title, + .rst-content .wy-alert-neutral.warning .admonition-title, + .rst-content .wy-alert-neutral.seealso .admonition-title, + .rst-content .wy-alert-neutral.admonition-todo .admonition-title, + .rst-content .wy-alert-neutral.admonition .admonition-title { + color: rgb(192, 186, 178); + background-image: initial; + background-color: rgb(40, 43, 45); + } + + .wy-alert.wy-alert-neutral a, + .rst-content .wy-alert-neutral.note a, + .rst-content .wy-alert-neutral.attention a, + .rst-content .wy-alert-neutral.caution a, + .rst-content .wy-alert-neutral.danger a, + .rst-content .wy-alert-neutral.error a, + .rst-content .wy-alert-neutral.hint a, + .rst-content .wy-alert-neutral.important a, + .rst-content .wy-alert-neutral.tip a, + .rst-content .wy-alert-neutral.warning a, + .rst-content .wy-alert-neutral.seealso a, + .rst-content .wy-alert-neutral.admonition-todo a, + .rst-content .wy-alert-neutral.admonition a { + color: rgb(84, 164, 217); + } + + .wy-tray-container li { + background-image: initial; + background-color: transparent; + color: rgb(232, 230, 227); + box-shadow: rgba(0, 0, 0, 0.1) 0px 5px 5px 0px; + } + + .wy-tray-container li.wy-tray-item-success { + background-image: initial; + background-color: rgb(31, 139, 77); + } + + .wy-tray-container li.wy-tray-item-info { + background-image: initial; + background-color: rgb(33, 102, 148); + } + + .wy-tray-container li.wy-tray-item-warning { + background-image: initial; + background-color: rgb(178, 94, 20); + } + + .wy-tray-container li.wy-tray-item-danger { + background-image: initial; + background-color: rgb(162, 33, 20); + } + + .btn { + color: rgb(232, 230, 227); + border-color: rgba(140, 130, 115, 0.1); + background-color: rgb(31, 139, 77); + text-decoration-color: initial; + box-shadow: rgba(24, 26, 27, 0.5) 0px 1px 2px -1px inset, + rgba(0, 0, 0, 0.1) 0px -2px 0px 0px inset; + } + + .btn-hover { + background-image: initial; + background-color: rgb(37, 114, 165); + color: rgb(232, 230, 227); + } + + .btn:hover { + background-image: initial; + background-color: rgb(35, 156, 86); + color: rgb(232, 230, 227); + } + + .btn:focus { + background-image: initial; + background-color: rgb(35, 156, 86); + outline-color: initial; + } + + .btn:active { + box-shadow: rgba(0, 0, 0, 0.05) 0px -1px 0px 0px inset, + rgba(0, 0, 0, 0.1) 0px 2px 0px 0px inset; + } + + .btn:visited { + color: rgb(232, 230, 227); + } + + .btn:disabled { + background-image: none; + box-shadow: none; + } + + .btn-disabled { + background-image: none; + box-shadow: none; + } + + .btn-disabled:hover, + .btn-disabled:focus, + .btn-disabled:active { + background-image: none; + box-shadow: none; + } + + .btn-info { + background-color: rgb(33, 102, 148) !important; + } + + .btn-info:hover { + background-color: rgb(37, 114, 165) !important; + } + + .btn-neutral { + background-color: rgb(27, 36, 36) !important; + color: rgb(192, 186, 178) !important; + } + + .btn-neutral:hover { + color: rgb(192, 186, 178); + background-color: rgb(34, 44, 44) !important; + } + + .btn-neutral:visited { + color: rgb(192, 186, 178) !important; + } + + .btn-success { + background-color: rgb(31, 139, 77) !important; + } + + .btn-success:hover { + background-color: rgb(27, 122, 68) !important; + } + + .btn-danger { + background-color: rgb(162, 33, 20) !important; + } + + .btn-danger:hover { + background-color: rgb(149, 30, 18) !important; + } + + .btn-warning { + background-color: rgb(178, 94, 20) !important; + } + + .btn-warning:hover { + background-color: rgb(165, 87, 18) !important; + } + + .btn-invert { + background-color: rgb(26, 28, 29); + } + + .btn-invert:hover { + background-color: rgb(35, 38, 40) !important; + } + + .btn-link { + color: rgb(84, 164, 217); + box-shadow: none; + background-color: transparent !important; + border-color: transparent !important; + } + + .btn-link:hover { + box-shadow: none; + background-color: transparent !important; + color: rgb(79, 162, 216) !important; + } + + .btn-link:active { + box-shadow: none; + background-color: transparent !important; + color: rgb(79, 162, 216) !important; + } + + .btn-link:visited { + color: rgb(164, 103, 188); + } + + .wy-dropdown-menu { + background-image: initial; + background-color: rgb(26, 28, 29); + border-color: rgb(60, 65, 67); + box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 2px 0px; + } + + .wy-dropdown-menu > dd > a { + color: rgb(192, 186, 178); + } + + .wy-dropdown-menu > dd > a:hover { + background-image: initial; + background-color: rgb(33, 102, 148); + color: rgb(232, 230, 227); + } + + .wy-dropdown-menu > dd.divider { + border-top-color: rgb(60, 65, 67); + } + + .wy-dropdown-menu > dd.call-to-action { + background-image: initial; + background-color: rgb(40, 43, 45); + } + + .wy-dropdown-menu > dd.call-to-action:hover { + background-image: initial; + background-color: rgb(40, 43, 45); + } + + .wy-dropdown-menu > dd.call-to-action .btn { + color: rgb(232, 230, 227); + } + + .wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu { + background-image: initial; + background-color: rgb(26, 28, 29); + } + + .wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover { + background-image: initial; + background-color: rgb(33, 102, 148); + color: rgb(232, 230, 227); + } + + .wy-dropdown-arrow::before { + border-bottom-color: rgb(51, 55, 57); + border-left-color: transparent; + border-right-color: transparent; + } + + fieldset { + border-color: initial; + } + + legend { + border-color: initial; + } + + label { + color: rgb(200, 195, 188); + } + + .wy-control-group.wy-control-group-required > label::after { + color: rgb(233, 88, 73); + } + + .wy-form-message-inline { + color: rgb(168, 160, 149); + } + + .wy-form-message { + color: rgb(168, 160, 149); + } + + input[type="text"], input[type="password"], input[type="email"], input[type="url"], input[type="date"], input[type="month"], input[type="time"], input[type="datetime"], input[type="datetime-local"], input[type="week"], input[type="number"], input[type="search"], input[type="tel"], input[type="color"] { + border-color: rgb(62, 68, 70); + box-shadow: rgb(43, 47, 49) 0px 1px 3px inset; + } + + input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus, input[type="url"]:focus, input[type="date"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="week"]:focus, input[type="number"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="color"]:focus { + outline-color: initial; + border-color: rgb(123, 114, 101); + } + + input.no-focus:focus { + border-color: rgb(62, 68, 70) !important; + } + + input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus { + outline-color: rgb(13, 113, 167); + } + + input[type="text"][disabled], input[type="password"][disabled], input[type="email"][disabled], input[type="url"][disabled], input[type="date"][disabled], input[type="month"][disabled], input[type="time"][disabled], input[type="datetime"][disabled], input[type="datetime-local"][disabled], input[type="week"][disabled], input[type="number"][disabled], input[type="search"][disabled], input[type="tel"][disabled], input[type="color"][disabled] { + background-color: rgb(27, 29, 30); + } + + input:focus:invalid, + textarea:focus:invalid, + select:focus:invalid { + color: rgb(233, 88, 73); + border-color: rgb(149, 31, 18); + } + + input:focus:invalid:focus, + textarea:focus:invalid:focus, + select:focus:invalid:focus { + border-color: rgb(149, 31, 18); + } + + input[type="file"]:focus:invalid:focus, input[type="radio"]:focus:invalid:focus, input[type="checkbox"]:focus:invalid:focus { + outline-color: rgb(149, 31, 18); + } + + select, + textarea { + border-color: rgb(62, 68, 70); + box-shadow: rgb(43, 47, 49) 0px 1px 3px inset; + } + + select { + border-color: rgb(62, 68, 70); + background-color: rgb(24, 26, 27); + } + + select:focus, + textarea:focus { + outline-color: initial; + } + + select[disabled], + textarea[disabled], + input[readonly], + select[readonly], + textarea[readonly] { + background-color: rgb(27, 29, 30); + } + + .wy-checkbox, + .wy-radio { + color: rgb(192, 186, 178); + } + + .wy-input-prefix .wy-input-context, + .wy-input-suffix .wy-input-context { + background-color: rgb(27, 36, 36); + border-color: rgb(62, 68, 70); + color: rgb(168, 160, 149); + } + + .wy-input-suffix .wy-input-context { + border-left-color: initial; + } + + .wy-input-prefix .wy-input-context { + border-right-color: initial; + } + + .wy-switch::before { + background-image: initial; + background-color: rgb(53, 57, 59); + } + + .wy-switch::after { + background-image: initial; + background-color: rgb(82, 88, 92); + } + + .wy-switch span { + color: rgb(200, 195, 188); + } + + .wy-switch.active::before { + background-image: initial; + background-color: rgb(24, 106, 58); + } + + .wy-switch.active::after { + background-image: initial; + background-color: rgb(31, 139, 77); + } + + .wy-control-group.wy-control-group-error .wy-form-message, + .wy-control-group.wy-control-group-error > label { + color: rgb(233, 88, 73); + } + + .wy-control-group.wy-control-group-error input[type="text"], .wy-control-group.wy-control-group-error input[type="password"], .wy-control-group.wy-control-group-error input[type="email"], .wy-control-group.wy-control-group-error input[type="url"], .wy-control-group.wy-control-group-error input[type="date"], .wy-control-group.wy-control-group-error input[type="month"], .wy-control-group.wy-control-group-error input[type="time"], .wy-control-group.wy-control-group-error input[type="datetime"], .wy-control-group.wy-control-group-error input[type="datetime-local"], .wy-control-group.wy-control-group-error input[type="week"], .wy-control-group.wy-control-group-error input[type="number"], .wy-control-group.wy-control-group-error input[type="search"], .wy-control-group.wy-control-group-error input[type="tel"], .wy-control-group.wy-control-group-error input[type="color"] { + border-color: rgb(149, 31, 18); + } + + .wy-control-group.wy-control-group-error textarea { + border-color: rgb(149, 31, 18); + } + + .wy-inline-validate.wy-inline-validate-success .wy-input-context { + color: rgb(92, 218, 145); + } + + .wy-inline-validate.wy-inline-validate-danger .wy-input-context { + color: rgb(233, 88, 73); + } + + .wy-inline-validate.wy-inline-validate-warning .wy-input-context { + color: rgb(232, 138, 54); + } + + .wy-inline-validate.wy-inline-validate-info .wy-input-context { + color: rgb(84, 164, 217); + } + + .wy-table caption, + .rst-content table.docutils caption, + .rst-content table.field-list caption { + color: rgb(232, 230, 227); + } + + .wy-table thead, + .rst-content table.docutils thead, + .rst-content table.field-list thead { + color: rgb(232, 230, 227); + } + + .wy-table thead th, + .rst-content table.docutils thead th, + .rst-content table.field-list thead th { + border-bottom-color: rgb(56, 61, 63); + } + + .wy-table td, + .rst-content table.docutils td, + .rst-content table.field-list td { + background-color: transparent; + } + + .wy-table-secondary { + color: rgb(152, 143, 129); + } + + .wy-table-tertiary { + color: rgb(152, 143, 129); + } + + .wy-table-odd td, + .wy-table-striped tr:nth-child(2n-1) td, + .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { + background-color: rgb(27, 36, 36); + } + + .wy-table-backed { + background-color: rgb(27, 36, 36); + } + + .wy-table-bordered-all, + .rst-content table.docutils { + border-color: rgb(56, 61, 63); + } + + .wy-table-bordered-all td, + .rst-content table.docutils td { + border-bottom-color: rgb(56, 61, 63); + border-left-color: rgb(56, 61, 63); + } + + .wy-table-bordered { + border-color: rgb(56, 61, 63); + } + + .wy-table-bordered-rows td { + border-bottom-color: rgb(56, 61, 63); + } + + .wy-table-horizontal td, + .wy-table-horizontal th { + border-bottom-color: rgb(56, 61, 63); + } + + a { + color: rgb(84, 164, 217); + text-decoration-color: initial; + } + + a:hover { + color: rgb(68, 156, 214); + } + + a:visited { + color: rgb(164, 103, 188); + } + + body { + color: rgb(192, 186, 178); + background-image: initial; + background-color: rgb(33, 35, 37); + } + + .wy-text-strike { + text-decoration-color: initial; + } + + .wy-text-warning { + color: rgb(232, 138, 54) !important; + } + + a.wy-text-warning:hover { + color: rgb(236, 157, 87) !important; + } + + .wy-text-info { + color: rgb(84, 164, 217) !important; + } + + a.wy-text-info:hover { + color: rgb(79, 162, 216) !important; + } + + .wy-text-success { + color: rgb(92, 218, 145) !important; + } + + a.wy-text-success:hover { + color: rgb(73, 214, 133) !important; + } + + .wy-text-danger { + color: rgb(233, 88, 73) !important; + } + + a.wy-text-danger:hover { + color: rgb(237, 118, 104) !important; + } + + .wy-text-neutral { + color: rgb(192, 186, 178) !important; + } + + a.wy-text-neutral:hover { + color: rgb(176, 169, 159) !important; + } + + hr { + border-right-color: initial; + border-bottom-color: initial; + border-left-color: initial; + border-top-color: rgb(56, 61, 63); + } + + code, + .rst-content tt, + .rst-content code { + background-image: initial; + background-color: rgb(24, 26, 27); + border-color: rgb(56, 61, 63); + color: rgb(233, 88, 73); + } + + .wy-plain-list-disc, + .rst-content .section ul, + .rst-content .toctree-wrapper ul, + article ul { + list-style-image: initial; + } + + .wy-plain-list-disc li, + .rst-content .section ul li, + .rst-content .toctree-wrapper ul li, + article ul li { + list-style-image: initial; + } + + .wy-plain-list-disc li li, + .rst-content .section ul li li, + .rst-content .toctree-wrapper ul li li, + article ul li li { + list-style-image: initial; + } + + .wy-plain-list-disc li li li, + .rst-content .section ul li li li, + .rst-content .toctree-wrapper ul li li li, + article ul li li li { + list-style-image: initial; + } + + .wy-plain-list-disc li ol li, + .rst-content .section ul li ol li, + .rst-content .toctree-wrapper ul li ol li, + article ul li ol li { + list-style-image: initial; + } + + .wy-plain-list-decimal, + .rst-content .section ol, + .rst-content ol.arabic, + article ol { + list-style-image: initial; + } + + .wy-plain-list-decimal li, + .rst-content .section ol li, + .rst-content ol.arabic li, + article ol li { + list-style-image: initial; + } + + .wy-plain-list-decimal li ul li, + .rst-content .section ol li ul li, + .rst-content ol.arabic li ul li, + article ol li ul li { + list-style-image: initial; + } + + .wy-breadcrumbs li code, + .wy-breadcrumbs li .rst-content tt, + .rst-content .wy-breadcrumbs li tt { + border-color: initial; + background-image: none; + background-color: initial; + } + + .wy-breadcrumbs li code.literal, + .wy-breadcrumbs li .rst-content tt.literal, + .rst-content .wy-breadcrumbs li tt.literal { + color: rgb(192, 186, 178); + } + + .wy-breadcrumbs-extra { + color: rgb(184, 178, 169); + } + + .wy-menu a:hover { + text-decoration-color: initial; + } + + .wy-menu-horiz li:hover { + background-image: initial; + background-color: rgba(24, 26, 27, 0.1); + } + + .wy-menu-horiz li.divide-left { + border-left-color: rgb(119, 110, 98); + } + + .wy-menu-horiz li.divide-right { + border-right-color: rgb(119, 110, 98); + } + + .wy-menu-vertical header, + .wy-menu-vertical p.caption { + color: rgb(99, 161, 201); + } + + .wy-menu-vertical li.divide-top { + border-top-color: rgb(119, 110, 98); + } + + .wy-menu-vertical li.divide-bottom { + border-bottom-color: rgb(119, 110, 98); + } + + .wy-menu-vertical li.current { + background-image: initial; + background-color: rgb(40, 43, 45); + } + + .wy-menu-vertical li.current a { + color: rgb(152, 143, 129); + border-right-color: rgb(63, 69, 71); + } + + .wy-menu-vertical li.current a:hover { + background-image: initial; + background-color: rgb(47, 51, 53); + } + + .wy-menu-vertical li code, + .wy-menu-vertical li .rst-content tt, + .rst-content .wy-menu-vertical li tt { + border-color: initial; + background-image: inherit; + background-color: inherit; + color: inherit; + } + + .wy-menu-vertical li span.toctree-expand { + color: rgb(183, 177, 168); + } + + .wy-menu-vertical li.on a, + .wy-menu-vertical li.current > a { + color: rgb(192, 186, 178); + background-image: initial; + background-color: rgb(26, 28, 29); + border-color: initial; + } + + .wy-menu-vertical li.on a:hover, + .wy-menu-vertical li.current > a:hover { + background-image: initial; + background-color: rgb(26, 28, 29); + } + + .wy-menu-vertical li.on a:hover span.toctree-expand, + .wy-menu-vertical li.current > a:hover span.toctree-expand { + color: rgb(152, 143, 129); + } + + .wy-menu-vertical li.on a span.toctree-expand, + .wy-menu-vertical li.current > a span.toctree-expand { + color: rgb(200, 195, 188); + } + + .wy-menu-vertical li.toctree-l1.current > a { + border-bottom-color: rgb(63, 69, 71); + border-top-color: rgb(63, 69, 71); + } + + .wy-menu-vertical li.toctree-l2 a, + .wy-menu-vertical li.toctree-l3 a, + .wy-menu-vertical li.toctree-l4 a { + color: rgb(192, 186, 178); + } + + .wy-menu-vertical li.toctree-l2.current > a { + background-image: initial; + background-color: rgb(54, 59, 61); + } + + .wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a { + background-image: initial; + background-color: rgb(54, 59, 61); + } + + .wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand { + color: rgb(152, 143, 129); + } + + .wy-menu-vertical li.toctree-l2 span.toctree-expand { + color: rgb(174, 167, 156); + } + + .wy-menu-vertical li.toctree-l3.current > a { + background-image: initial; + background-color: rgb(61, 66, 69); + } + + .wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a { + background-image: initial; + background-color: rgb(61, 66, 69); + } + + .wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand { + color: rgb(152, 143, 129); + } + + .wy-menu-vertical li.toctree-l3 span.toctree-expand { + color: rgb(166, 158, 146); + } + + .wy-menu-vertical li ul li a { + color: rgb(208, 204, 198); + } + + .wy-menu-vertical a { + color: rgb(208, 204, 198); + } + + .wy-menu-vertical a:hover { + background-color: rgb(57, 62, 64); + } + + .wy-menu-vertical a:hover span.toctree-expand { + color: rgb(208, 204, 198); + } + + .wy-menu-vertical a:active { + background-color: rgb(33, 102, 148); + color: rgb(232, 230, 227); + } + + .wy-menu-vertical a:active span.toctree-expand { + color: rgb(232, 230, 227); + } + + .wy-side-nav-search { + background-color: rgb(33, 102, 148); + color: rgb(230, 228, 225); + } + + .wy-side-nav-search input[type="text"] { + border-color: rgb(35, 111, 160); + } + + .wy-side-nav-search img { + background-color: rgb(33, 102, 148); + } + + .wy-side-nav-search > a, + .wy-side-nav-search .wy-dropdown > a { + color: rgb(230, 228, 225); + } + + .wy-side-nav-search > a:hover, + .wy-side-nav-search .wy-dropdown > a:hover { + background-image: initial; + background-color: rgba(24, 26, 27, 0.1); + } + + .wy-side-nav-search > a img.logo, + .wy-side-nav-search .wy-dropdown > a img.logo { + background-image: initial; + background-color: transparent; + } + + .wy-side-nav-search > div.version { + color: rgba(232, 230, 227, 0.3); + } + + .wy-nav .wy-menu-vertical header { + color: rgb(84, 164, 217); + } + + .wy-nav .wy-menu-vertical a { + color: rgb(184, 178, 169); + } + + .wy-nav .wy-menu-vertical a:hover { + background-color: rgb(33, 102, 148); + color: rgb(232, 230, 227); + } + + .wy-body-for-nav { + background-image: initial; + background-color: rgb(26, 28, 29); + } + + .wy-nav-side { + color: rgb(169, 161, 150); + background-image: initial; + background-color: rgb(38, 41, 43); + } + + .wy-nav-top { + background-image: initial; + background-color: rgb(33, 102, 148); + color: rgb(232, 230, 227); + } + + .wy-nav-top a { + color: rgb(232, 230, 227); + } + + .wy-nav-top img { + background-color: rgb(33, 102, 148); + } + + .wy-nav-content-wrap { + background-image: initial; + background-color: rgb(26, 28, 29); + } + + .wy-body-mask { + background-image: initial; + background-color: rgba(0, 0, 0, 0.2); + } + + footer { + color: rgb(152, 143, 129); + } + + footer span.commit code, + footer span.commit .rst-content tt, + .rst-content footer span.commit tt { + background-image: none; + background-color: initial; + border-color: initial; + color: rgb(152, 143, 129); + } + + #search-results .search li { + border-bottom-color: rgb(56, 61, 63); + } + + #search-results .search li:first-child { + border-top-color: rgb(56, 61, 63); + } + + #search-results .context { + color: rgb(152, 143, 129); + } + + .wy-body-for-nav { + background-image: initial; + background-color: rgb(26, 28, 29); + } + + @media screen and (min-width: 1100px) { + .wy-nav-content-wrap { + background-image: initial; + background-color: rgba(0, 0, 0, 0.05); + } + + .wy-nav-content { + background-image: initial; + background-color: rgb(26, 28, 29); + } + } + .rst-versions { + color: rgb(230, 228, 225); + background-image: initial; + background-color: rgb(23, 24, 25); + } + + .rst-versions a { + color: rgb(84, 164, 217); + text-decoration-color: initial; + } + + .rst-versions .rst-current-version { + background-color: rgb(29, 31, 32); + color: rgb(92, 218, 145); + } + + .rst-versions .rst-current-version .fa, + .rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand, + .wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand, + .rst-versions .rst-current-version .rst-content .admonition-title, + .rst-content .rst-versions .rst-current-version .admonition-title, + .rst-versions .rst-current-version .rst-content h1 .headerlink, + .rst-content h1 .rst-versions .rst-current-version .headerlink, + .rst-versions .rst-current-version .rst-content h2 .headerlink, + .rst-content h2 .rst-versions .rst-current-version .headerlink, + .rst-versions .rst-current-version .rst-content h3 .headerlink, + .rst-content h3 .rst-versions .rst-current-version .headerlink, + .rst-versions .rst-current-version .rst-content h4 .headerlink, + .rst-content h4 .rst-versions .rst-current-version .headerlink, + .rst-versions .rst-current-version .rst-content h5 .headerlink, + .rst-content h5 .rst-versions .rst-current-version .headerlink, + .rst-versions .rst-current-version .rst-content h6 .headerlink, + .rst-content h6 .rst-versions .rst-current-version .headerlink, + .rst-versions .rst-current-version .rst-content dl dt .headerlink, + .rst-content dl dt .rst-versions .rst-current-version .headerlink, + .rst-versions .rst-current-version .rst-content p.caption .headerlink, + .rst-content p.caption .rst-versions .rst-current-version .headerlink, + .rst-versions .rst-current-version .rst-content table > caption .headerlink, + .rst-content table > caption .rst-versions .rst-current-version .headerlink, + .rst-versions .rst-current-version .rst-content .code-block-caption .headerlink, + .rst-content .code-block-caption .rst-versions .rst-current-version .headerlink, + .rst-versions .rst-current-version .rst-content tt.download span:first-child, + .rst-content tt.download .rst-versions .rst-current-version span:first-child, + .rst-versions .rst-current-version .rst-content code.download span:first-child, + .rst-content code.download .rst-versions .rst-current-version span:first-child, + .rst-versions .rst-current-version .icon { + color: rgb(230, 228, 225); + } + + .rst-versions .rst-current-version.rst-out-of-date { + background-color: rgb(162, 33, 20); + color: rgb(232, 230, 227); + } + + .rst-versions .rst-current-version.rst-active-old-version { + background-color: rgb(192, 156, 11); + color: rgb(232, 230, 227); + } + + .rst-versions .rst-other-versions { + color: rgb(152, 143, 129); + } + + .rst-versions .rst-other-versions hr { + border-right-color: initial; + border-bottom-color: initial; + border-left-color: initial; + border-top-color: rgb(119, 111, 98); + } + + .rst-versions .rst-other-versions dd a { + color: rgb(230, 228, 225); + } + + .rst-versions.rst-badge { + border-color: initial; + } + + .rst-content abbr[title] { + text-decoration-color: initial; + } + + .rst-content.style-external-links a.reference.external::after { + color: rgb(184, 178, 169); + } + + .rst-content pre.literal-block, .rst-content div[class^="highlight"] { + border-color: rgb(56, 61, 63); + } + + .rst-content pre.literal-block div[class^="highlight"], .rst-content div[class^="highlight"] div[class^="highlight"] { + border-color: initial; + } + + .rst-content .linenodiv pre { + border-right-color: rgb(54, 59, 61); + } + + .rst-content .admonition table { + border-color: rgba(140, 130, 115, 0.1); + } + + .rst-content .admonition table td, + .rst-content .admonition table th { + background-image: initial !important; + background-color: transparent !important; + border-color: rgba(140, 130, 115, 0.1) !important; + } + + .rst-content .section ol.loweralpha, + .rst-content .section ol.loweralpha li { + list-style-image: initial; + } + + .rst-content .section ol.upperalpha, + .rst-content .section ol.upperalpha li { + list-style-image: initial; + } + + .rst-content .toc-backref { + color: rgb(192, 186, 178); + } + + .rst-content .sidebar { + background-image: initial; + background-color: rgb(27, 36, 36); + border-color: rgb(56, 61, 63); + } + + .rst-content .sidebar .sidebar-title { + background-image: initial; + background-color: rgb(40, 43, 45); + } + + .rst-content .highlighted { + background-image: initial; + background-color: rgb(192, 156, 11); + } + + .rst-content table.docutils.citation, + .rst-content table.docutils.footnote { + background-image: none; + background-color: initial; + border-color: initial; + color: rgb(152, 143, 129); + } + + .rst-content table.docutils.citation td, + .rst-content table.docutils.citation tr, + .rst-content table.docutils.footnote td, + .rst-content table.docutils.footnote tr { + border-color: initial; + background-color: transparent !important; + } + + .rst-content table.docutils.citation tt, + .rst-content table.docutils.citation code, + .rst-content table.docutils.footnote tt, + .rst-content table.docutils.footnote code { + color: rgb(178, 172, 162); + } + + .rst-content table.docutils th { + border-color: rgb(56, 61, 63); + } + + .rst-content table.field-list { + border-color: initial; + } + + .rst-content table.field-list td { + border-color: initial; + } + + .rst-content tt, + .rst-content tt, + .rst-content code { + color: rgb(232, 230, 227); + } + + .rst-content tt.literal, + .rst-content tt.literal, + .rst-content code.literal { + color: rgb(233, 88, 73); + } + + .rst-content tt.xref, + a .rst-content tt, + .rst-content tt.xref, + .rst-content code.xref, + a .rst-content tt, + a .rst-content code { + color: rgb(192, 186, 178); + } + + .rst-content a tt, + .rst-content a tt, + .rst-content a code { + color: rgb(84, 164, 217); + } + + .rst-content dl:not(.docutils) dt { + background-image: initial; + background-color: rgb(32, 35, 36); + color: rgb(84, 164, 217); + border-top-color: rgb(28, 89, 128); + } + + .rst-content dl:not(.docutils) dt::before { + color: rgb(109, 178, 223); + } + + .rst-content dl:not(.docutils) dt .headerlink { + color: rgb(192, 186, 178); + } + + .rst-content dl:not(.docutils) dl dt { + border-top-color: initial; + border-right-color: initial; + border-bottom-color: initial; + border-left-color: rgb(62, 68, 70); + background-image: initial; + background-color: rgb(32, 35, 37); + color: rgb(178, 172, 162); + } + + .rst-content dl:not(.docutils) dl dt .headerlink { + color: rgb(192, 186, 178); + } + + .rst-content dl:not(.docutils) tt.descname, + .rst-content dl:not(.docutils) tt.descclassname, + .rst-content dl:not(.docutils) tt.descname, + .rst-content dl:not(.docutils) code.descname, + .rst-content dl:not(.docutils) tt.descclassname, + .rst-content dl:not(.docutils) code.descclassname { + background-color: transparent; + border-color: initial; + } + + .rst-content dl:not(.docutils) .optional { + color: rgb(232, 230, 227); + } + + .rst-content .viewcode-link, + .rst-content .viewcode-back { + color: rgb(92, 218, 145); + } + + .rst-content tt.download, + .rst-content code.download { + background-image: inherit; + background-color: inherit; + color: inherit; + border-color: inherit; + } + + .rst-content .guilabel { + border-color: rgb(27, 84, 122); + background-image: initial; + background-color: rgb(32, 35, 36); + } + + span[id*="MathJax-Span"] { + color: rgb(192, 186, 178); + } + + .highlight .hll { + background-color: rgb(82, 82, 0); + } + + .highlight { + background-image: initial; + background-color: rgb(61, 82, 0); + } + + .highlight .c { + color: rgb(119, 179, 195); + } + + .highlight .err { + border-color: rgb(179, 0, 0); + } + + .highlight .k { + color: rgb(126, 255, 163); + } + + .highlight .o { + color: rgb(168, 160, 149); + } + + .highlight .ch { + color: rgb(119, 179, 195); + } + + .highlight .cm { + color: rgb(119, 179, 195); + } + + .highlight .cp { + color: rgb(126, 255, 163); + } + + .highlight .cpf { + color: rgb(119, 179, 195); + } + + .highlight .c1 { + color: rgb(119, 179, 195); + } + + .highlight .cs { + color: rgb(119, 179, 195); + background-color: rgb(60, 0, 0); + } + + .highlight .gd { + color: rgb(255, 92, 92); + } + + .highlight .gr { + color: rgb(255, 26, 26); + } + + .highlight .gh { + color: rgb(127, 174, 255); + } + + .highlight .gi { + color: rgb(92, 255, 92); + } + + .highlight .go { + color: rgb(200, 195, 188); + } + + .highlight .gp { + color: rgb(246, 147, 68); + } + + .highlight .gu { + color: rgb(255, 114, 255); + } + + .highlight .gt { + color: rgb(71, 160, 255); + } + + .highlight .kc { + color: rgb(126, 255, 163); + } + + .highlight .kd { + color: rgb(126, 255, 163); + } + + .highlight .kn { + color: rgb(126, 255, 163); + } + + .highlight .kp { + color: rgb(126, 255, 163); + } + + .highlight .kr { + color: rgb(126, 255, 163); + } + + .highlight .kt { + color: rgb(255, 137, 103); + } + + .highlight .m { + color: rgb(125, 222, 174); + } + + .highlight .s { + color: rgb(123, 166, 202); + } + + .highlight .na { + color: rgb(123, 166, 202); + } + + .highlight .nb { + color: rgb(126, 255, 163); + } + + .highlight .nc { + color: rgb(81, 194, 242); + } + + .highlight .no { + color: rgb(103, 177, 215); + } + + .highlight .nd { + color: rgb(178, 172, 162); + } + + .highlight .ni { + color: rgb(217, 100, 73); + } + + .highlight .ne { + color: rgb(126, 255, 163); + } + + .highlight .nf { + color: rgb(131, 186, 249); + } + + .highlight .nl { + color: rgb(137, 193, 255); + } + + .highlight .nn { + color: rgb(81, 194, 242); + } + + .highlight .nt { + color: rgb(138, 191, 249); + } + + .highlight .nv { + color: rgb(190, 103, 215); + } + + .highlight .ow { + color: rgb(126, 255, 163); + } + + .highlight .w { + color: rgb(189, 183, 175); + } + + .highlight .mb { + color: rgb(125, 222, 174); + } + + .highlight .mf { + color: rgb(125, 222, 174); + } + + .highlight .mh { + color: rgb(125, 222, 174); + } + + .highlight .mi { + color: rgb(125, 222, 174); + } + + .highlight .mo { + color: rgb(125, 222, 174); + } + + .highlight .sa { + color: rgb(123, 166, 202); + } + + .highlight .sb { + color: rgb(123, 166, 202); + } + + .highlight .sc { + color: rgb(123, 166, 202); + } + + .highlight .dl { + color: rgb(123, 166, 202); + } + + .highlight .sd { + color: rgb(123, 166, 202); + } + + .highlight .s2 { + color: rgb(123, 166, 202); + } + + .highlight .se { + color: rgb(123, 166, 202); + } + + .highlight .sh { + color: rgb(123, 166, 202); + } + + .highlight .si { + color: rgb(117, 168, 209); + } + + .highlight .sx { + color: rgb(246, 147, 68); + } + + .highlight .sr { + color: rgb(133, 182, 224); + } + + .highlight .s1 { + color: rgb(123, 166, 202); + } + + .highlight .ss { + color: rgb(188, 230, 128); + } + + .highlight .bp { + color: rgb(126, 255, 163); + } + + .highlight .fm { + color: rgb(131, 186, 249); + } + + .highlight .vc { + color: rgb(190, 103, 215); + } + + .highlight .vg { + color: rgb(190, 103, 215); + } + + .highlight .vi { + color: rgb(190, 103, 215); + } + + .highlight .vm { + color: rgb(190, 103, 215); + } + + .highlight .il { + color: rgb(125, 222, 174); + } + + .rst-other-versions a { + border-color: initial; + } + + .ethical-sidebar .ethical-image-link, + .ethical-footer .ethical-image-link { + border-color: initial; + } + + .ethical-sidebar, + .ethical-footer { + background-color: rgb(34, 36, 38); + border-color: rgb(62, 68, 70); + color: rgb(226, 223, 219); + } + + .ethical-sidebar ul { + list-style-image: initial; + } + + .ethical-sidebar ul li { + background-color: rgb(5, 77, 121); + color: rgb(232, 230, 227); + } + + .ethical-sidebar a, + .ethical-sidebar a:visited, + .ethical-sidebar a:hover, + .ethical-sidebar a:active, + .ethical-footer a, + .ethical-footer a:visited, + .ethical-footer a:hover, + .ethical-footer a:active { + color: rgb(226, 223, 219); + text-decoration-color: initial !important; + border-bottom-color: initial !important; + } + + .ethical-callout a { + color: rgb(161, 153, 141) !important; + text-decoration-color: initial !important; + } + + .ethical-fixedfooter { + background-color: rgb(34, 36, 38); + border-top-color: rgb(66, 72, 74); + color: rgb(192, 186, 178); + } + + .ethical-fixedfooter .ethical-text::before { + background-color: rgb(61, 140, 64); + color: rgb(232, 230, 227); + } + + .ethical-fixedfooter .ethical-callout { + color: rgb(168, 160, 149); + } + + .ethical-fixedfooter a, + .ethical-fixedfooter a:hover, + .ethical-fixedfooter a:active, + .ethical-fixedfooter a:visited { + color: rgb(192, 186, 178); + text-decoration-color: initial; + } + + .ethical-rtd .ethical-sidebar { + color: rgb(184, 178, 169); + } + + .ethical-alabaster a.ethical-image-link { + border-color: initial !important; + } + + .ethical-dark-theme .ethical-sidebar { + background-color: rgb(58, 62, 65); + border-color: rgb(75, 81, 84); + color: rgb(193, 188, 180) !important; + } + + .ethical-dark-theme a, + .ethical-dark-theme a:visited { + color: rgb(216, 213, 208) !important; + border-bottom-color: initial !important; + } + + .ethical-dark-theme .ethical-callout a { + color: rgb(184, 178, 169) !important; + } + + .keep-us-sustainable { + border-color: rgb(87, 133, 38); + } + + .keep-us-sustainable a, + .keep-us-sustainable a:hover, + .keep-us-sustainable a:visited { + text-decoration-color: initial; + } + + .wy-body-for-nav .keep-us-sustainable { + color: rgb(184, 178, 169); + } + + .wy-body-for-nav .keep-us-sustainable a { + color: rgb(222, 219, 215); + } + + /* For black-on-white/transparent images at handbook/text-anchors.html */ + #text-anchors img { + filter: invert(1) brightness(0.85) hue-rotate(-60deg); + } +} diff --git a/docs/resources/favicon.ico b/docs/resources/favicon.ico new file mode 100644 index 000000000..78eef9ae3 Binary files /dev/null and b/docs/resources/favicon.ico differ diff --git a/requirements.txt b/requirements.txt index 082d8e516..9758f91fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,7 @@ # Development, documentation & testing requirements. -black; python_version >= '3.6' +black check-manifest coverage -coveralls jarn.viewdoc olefile pycodestyle @@ -10,4 +9,6 @@ pyflakes pyroma pytest pytest-cov +sphinx>=2.4 +sphinx-removed-in sphinx-rtd-theme diff --git a/selftest.py b/selftest.py index ea52256f7..a9a02ef71 100755 --- a/selftest.py +++ b/selftest.py @@ -47,7 +47,7 @@ def testimage(): ('PPM', 'RGB', (128, 128)) >>> try: ... _info(Image.open("Tests/images/hopper.jpg")) - ... except IOError as v: + ... except OSError as v: ... print(v) ('JPEG', 'RGB', (128, 128)) diff --git a/setup.cfg b/setup.cfg index 1c6ebc84c..129adeee7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,15 +1,10 @@ -[aliases] -test=pytest - [flake8] -extend-ignore = E203, W503 +extend-ignore = E203 max-line-length = 88 [isort] -combine_as_imports = True -include_trailing_comma = True -line_length = 88 -multi_line_output = 3 +profile = black [tool:pytest] -addopts = -rs +addopts = -ra --color=yes +testpaths = Tests diff --git a/setup.py b/setup.py index f5024990f..15a6ec282 100755 --- a/setup.py +++ b/setup.py @@ -14,15 +14,14 @@ import struct import subprocess import sys import warnings -from distutils import ccompiler, sysconfig -from distutils.command.build_ext import build_ext from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext def get_version(): version_file = "src/PIL/_version.py" - with open(version_file, "r") as f: + with open(version_file) as f: exec(compile(f.read(), version_file, "exec")) return locals()["__version__"] @@ -38,12 +37,12 @@ TIFF_ROOT = None ZLIB_ROOT = None -if sys.platform == "win32" and sys.version_info >= (3, 9): +if sys.platform == "win32" and sys.version_info >= (3, 10): warnings.warn( - "Pillow {} does not support Python {}.{} and does not provide prebuilt " - "Windows binaries. We do not recommend building from source on Windows.".format( - PILLOW_VERSION, sys.version_info.major, sys.version_info.minor - ), + f"Pillow {PILLOW_VERSION} does not support Python " + f"{sys.version_info.major}.{sys.version_info.minor} and does not provide " + "prebuilt Windows binaries. We do not recommend building from source on " + "Windows.", RuntimeWarning, ) @@ -54,6 +53,7 @@ _LIB_IMAGING = ( "Access", "AlphaComposite", "Resample", + "Reduce", "Bands", "BcnDecode", "BitDecode", @@ -130,21 +130,9 @@ class RequiredDependencyException(Exception): pass -PLATFORM_MINGW = "mingw" in ccompiler.get_default_compiler() +PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version PLATFORM_PYPY = hasattr(sys, "pypy_version_info") -if sys.platform == "win32" and PLATFORM_MINGW: - from distutils import cygwinccompiler - - cygwin_versions = cygwinccompiler.get_versions() - if cygwin_versions[1] is None: - # ld version is None - # distutils cygwinccompiler might fetch the ld path from gcc - # Try the normal path instead - cygwin_versions = list(cygwin_versions) - cygwin_versions[1] = cygwinccompiler._find_exe_version("ld -v") - cygwinccompiler.get_versions = lambda: tuple(cygwin_versions) - def _dbg(s, tp=None): if DEBUG: @@ -174,7 +162,7 @@ def _find_library_dirs_ldconfig(): # Assuming GLIBC's ldconfig (with option -p) # Alpine Linux uses musl that can't print cache args = ["/sbin/ldconfig", "-p"] - expr = r".*\(%s.*\) => (.*)" % abi_type + expr = fr".*\({abi_type}.*\) => (.*)" env = dict(os.environ) env["LC_ALL"] = "C" env["LANG"] = "C" @@ -243,11 +231,6 @@ def _cmd_exists(cmd): ) -def _read(file): - with open(file, "rb") as fp: - return fp.read() - - def _pkg_config(name): try: command = os.environ.get("PKG_CONFIG", "pkg-config") @@ -285,6 +268,7 @@ class pil_build_ext(build_ext): "webpmux", "jpeg2000", "imagequant", + "xcb", ] required = {"jpeg", "zlib"} @@ -306,8 +290,8 @@ class pil_build_ext(build_ext): user_options = ( build_ext.user_options - + [("disable-%s" % x, None, "Disable support for %s" % x) for x in feature] - + [("enable-%s" % x, None, "Enable support for %s" % x) for x in feature] + + [(f"disable-{x}", None, f"Disable support for {x}") for x in feature] + + [(f"enable-{x}", None, f"Enable support for {x}") for x in feature] + [ ("disable-platform-guessing", None, "Disable platform guessing on Linux"), ("debug", None, "Debug logging"), @@ -320,8 +304,8 @@ class pil_build_ext(build_ext): self.add_imaging_libs = "" build_ext.initialize_options(self) for x in self.feature: - setattr(self, "disable_%s" % x, None) - setattr(self, "enable_%s" % x, None) + setattr(self, f"disable_{x}", None) + setattr(self, f"enable_{x}", None) def finalize_options(self): build_ext.finalize_options(self) @@ -338,25 +322,39 @@ class pil_build_ext(build_ext): except TypeError: self.parallel = None for x in self.feature: - if getattr(self, "disable_%s" % x): + if getattr(self, f"disable_{x}"): setattr(self.feature, x, False) self.feature.required.discard(x) _dbg("Disabling %s", x) - if getattr(self, "enable_%s" % x): + if getattr(self, f"enable_{x}"): raise ValueError( - "Conflicting options: --enable-{} and --disable-{}".format(x, x) + f"Conflicting options: --enable-{x} and --disable-{x}" ) - if getattr(self, "enable_%s" % x): + if getattr(self, f"enable_{x}"): _dbg("Requiring %s", x) self.feature.required.add(x) + def _update_extension(self, name, libraries, define_macros=None, include_dirs=None): + for extension in self.extensions: + if extension.name == name: + extension.libraries += libraries + if define_macros is not None: + extension.define_macros += define_macros + if include_dirs is not None: + extension.include_dirs += include_dirs + break + + def _remove_extension(self, name): + for extension in self.extensions: + if extension.name == name: + self.extensions.remove(extension) + break + def build_extensions(self): library_dirs = [] include_dirs = [] - _add_directory(include_dirs, "src/libImaging") - pkg_config = None if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): pkg_config = _pkg_config @@ -381,12 +379,12 @@ class pil_build_ext(build_ext): if root is None and pkg_config: if isinstance(lib_name, tuple): for lib_name2 in lib_name: - _dbg("Looking for `%s` using pkg-config." % lib_name2) + _dbg(f"Looking for `{lib_name2}` using pkg-config.") root = pkg_config(lib_name2) if root: break else: - _dbg("Looking for `%s` using pkg-config." % lib_name) + _dbg(f"Looking for `{lib_name}` using pkg-config.") root = pkg_config(lib_name) if isinstance(root, tuple): @@ -416,10 +414,8 @@ class pil_build_ext(build_ext): for d in os.environ[k].split(os.path.pathsep): _add_directory(library_dirs, d) - prefix = sysconfig.get_config_var("prefix") - if prefix: - _add_directory(library_dirs, os.path.join(prefix, "lib")) - _add_directory(include_dirs, os.path.join(prefix, "include")) + _add_directory(library_dirs, os.path.join(sys.prefix, "lib")) + _add_directory(include_dirs, os.path.join(sys.prefix, "include")) # # add platform directories @@ -464,6 +460,9 @@ class pil_build_ext(build_ext): # add Homebrew's include and lib directories _add_directory(library_dirs, os.path.join(prefix, "lib")) _add_directory(include_dirs, os.path.join(prefix, "include")) + _add_directory( + include_dirs, os.path.join(prefix, "opt", "zlib", "include") + ) ft_prefix = os.path.join(prefix, "opt", "freetype") if ft_prefix and os.path.isdir(ft_prefix): @@ -476,6 +475,18 @@ class pil_build_ext(build_ext): _add_directory(library_dirs, "/usr/X11/lib") _add_directory(include_dirs, "/usr/X11/include") + # SDK install path + try: + sdk_path = ( + subprocess.check_output(["xcrun", "--show-sdk-path"]) + .strip() + .decode("latin1") + ) + except Exception: + sdk_path = None + if sdk_path: + _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) + _add_directory(include_dirs, os.path.join(sdk_path, "usr", "include")) elif ( sys.platform.startswith("linux") or sys.platform.startswith("gnu") @@ -515,11 +526,6 @@ class pil_build_ext(build_ext): _add_directory(library_dirs, "/lib") if sys.platform == "win32": - if PLATFORM_MINGW: - _add_directory( - include_dirs, "C:\\msys64\\mingw32\\include\\libimagequant" - ) - # on Windows, look for the OpenJPEG libraries in the location that # the official installer puts them program_files = os.environ.get("ProgramFiles", "") @@ -577,7 +583,7 @@ class pil_build_ext(build_ext): try: listdir = os.listdir(directory) except Exception: - # WindowsError, FileNotFoundError + # OSError, FileNotFoundError continue for name in listdir: if name.startswith("openjpeg-") and os.path.isfile( @@ -680,6 +686,12 @@ class pil_build_ext(build_ext): ): feature.webpmux = "libwebpmux" + if feature.want("xcb"): + _dbg("Looking for xcb") + if _find_include_file(self, "xcb/xcb.h"): + if _find_library_file(self, "xcb"): + feature.xcb = "xcb" + for f in feature: if not getattr(feature, f) and feature.require(f): if f in ("jpeg", "zlib"): @@ -689,12 +701,6 @@ class pil_build_ext(build_ext): # # core library - files = ["src/_imaging.c"] - for src_file in _IMAGING: - files.append("src/" + src_file + ".c") - for src_file in _LIB_IMAGING: - files.append(os.path.join("src/libImaging", src_file + ".c")) - libs = self.add_imaging_libs.split() defs = [] if feature.jpeg: @@ -703,7 +709,7 @@ class pil_build_ext(build_ext): if feature.jpeg2000: libs.append(feature.jpeg2000) defs.append(("HAVE_OPENJPEG", None)) - if sys.platform == "win32": + if sys.platform == "win32" and not PLATFORM_MINGW: defs.append(("OPJ_STATIC", None)) if feature.zlib: libs.append(feature.zlib) @@ -714,17 +720,28 @@ class pil_build_ext(build_ext): if feature.tiff: libs.append(feature.tiff) defs.append(("HAVE_LIBTIFF", None)) + # FIXME the following define should be detected automatically + # based on system libtiff, see #4237 + if PLATFORM_MINGW: + defs.append(("USE_WIN32_FILEIO", None)) + if feature.xcb: + libs.append(feature.xcb) + defs.append(("HAVE_XCB", None)) if sys.platform == "win32": libs.extend(["kernel32", "user32", "gdi32"]) if struct.unpack("h", b"\0\1")[0] == 1: defs.append(("WORDS_BIGENDIAN", None)) - if sys.platform == "win32" and not (PLATFORM_PYPY or PLATFORM_MINGW): - defs.append(("PILLOW_VERSION", '"\\"%s\\""' % PILLOW_VERSION)) + if ( + sys.platform == "win32" + and sys.version_info < (3, 9) + and not (PLATFORM_PYPY or PLATFORM_MINGW) + ): + defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) else: - defs.append(("PILLOW_VERSION", '"%s"' % PILLOW_VERSION)) + defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"')) - exts = [(Extension("PIL._imaging", files, libraries=libs, define_macros=defs))] + self._update_extension("PIL._imaging", libs, defs) # # additional libraries @@ -732,26 +749,17 @@ class pil_build_ext(build_ext): if feature.freetype: libs = ["freetype"] defs = [] - exts.append( - Extension( - "PIL._imagingft", - ["src/_imagingft.c"], - libraries=libs, - define_macros=defs, - ) - ) + self._update_extension("PIL._imagingft", libs, defs) + else: + self._remove_extension("PIL._imagingft") if feature.lcms: extra = [] if sys.platform == "win32": extra.extend(["user32", "gdi32"]) - exts.append( - Extension( - "PIL._imagingcms", - ["src/_imagingcms.c"], - libraries=[feature.lcms] + extra, - ) - ) + self._update_extension("PIL._imagingcms", [feature.lcms] + extra) + else: + self._remove_extension("PIL._imagingcms") if feature.webp: libs = [feature.webp] @@ -762,26 +770,12 @@ class pil_build_ext(build_ext): libs.append(feature.webpmux) libs.append(feature.webpmux.replace("pmux", "pdemux")) - exts.append( - Extension( - "PIL._webp", ["src/_webp.c"], libraries=libs, define_macros=defs - ) - ) + self._update_extension("PIL._webp", libs, defs) + else: + self._remove_extension("PIL._webp") tk_libs = ["psapi"] if sys.platform == "win32" else [] - exts.append( - Extension( - "PIL._imagingtk", - ["src/_imagingtk.c", "src/Tk/tkImaging.c"], - include_dirs=["src/Tk"], - libraries=tk_libs, - ) - ) - - exts.append(Extension("PIL._imagingmath", ["src/_imagingmath.c"])) - exts.append(Extension("PIL._imagingmorph", ["src/_imagingmorph.c"])) - - self.extensions[:] = exts + self._update_extension("PIL._imagingtk", tk_libs) build_ext.build_extensions(self) @@ -795,11 +789,11 @@ class pil_build_ext(build_ext): print("-" * 68) print("PIL SETUP SUMMARY") print("-" * 68) - print("version Pillow %s" % PILLOW_VERSION) + print(f"version Pillow {PILLOW_VERSION}") v = sys.version.split("[") - print("platform {} {}".format(sys.platform, v[0].strip())) + print(f"platform {sys.platform} {v[0].strip()}") for v in v[1:]: - print(" [%s" % v.strip()) + print(f" [{v.strip()}") print("-" * 68) options = [ @@ -812,6 +806,7 @@ class pil_build_ext(build_ext): (feature.lcms, "LITTLECMS2"), (feature.webp, "WEBP"), (feature.webpmux, "WEBPMUX"), + (feature.xcb, "XCB (X protocol)"), ] all = 1 @@ -819,10 +814,10 @@ class pil_build_ext(build_ext): if option[0]: version = "" if len(option) >= 3 and option[2]: - version = " (%s)" % option[2] - print("--- {} support available{}".format(option[1], version)) + version = f" ({option[2]})" + print(f"--- {option[1]} support available{version}") else: - print("*** %s support not available" % option[1]) + print(f"*** {option[1]} support not available") all = 0 print("-" * 68) @@ -844,15 +839,31 @@ def debug_build(): return hasattr(sys, "gettotalrefcount") -needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv) -pytest_runner = ["pytest-runner"] if needs_pytest else [] +files = ["src/_imaging.c"] +for src_file in _IMAGING: + files.append("src/" + src_file + ".c") +for src_file in _LIB_IMAGING: + files.append(os.path.join("src/libImaging", src_file + ".c")) +ext_modules = [ + Extension("PIL._imaging", files), + Extension("PIL._imagingft", ["src/_imagingft.c"]), + Extension("PIL._imagingcms", ["src/_imagingcms.c"]), + Extension("PIL._webp", ["src/_webp.c"]), + Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), + Extension("PIL._imagingmath", ["src/_imagingmath.c"]), + Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), +] + +with open("README.md") as f: + long_description = f.read() try: setup( name=NAME, version=PILLOW_VERSION, description="Python Imaging Library (Fork)", - long_description=_read("README.rst").decode("utf-8"), + long_description=long_description, + long_description_content_type="text/markdown", license="HPND", author="Alex Clark (PIL Fork Author)", author_email="aclark@python-pillow.org", @@ -860,16 +871,17 @@ try: project_urls={ "Documentation": "https://pillow.readthedocs.io", "Source": "https://github.com/python-pillow/Pillow", - "Funding": "https://tidelift.com/subscription/pkg/pypi-pillow", + "Funding": "https://tidelift.com/subscription/pkg/pypi-pillow?" + "utm_source=pypi-pillow&utm_medium=pypi", }, classifiers=[ "Development Status :: 6 - Mature", "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", # noqa: E501 "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", @@ -879,40 +891,33 @@ try: "Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Multimedia :: Graphics :: Viewers", ], - python_requires=">=3.5", + python_requires=">=3.6", cmdclass={"build_ext": pil_build_ext}, - ext_modules=[Extension("PIL._imaging", ["_imaging.c"])], + ext_modules=ext_modules, include_package_data=True, - setup_requires=pytest_runner, - tests_require=["pytest"], packages=["PIL"], package_dir={"": "src"}, keywords=["Imaging"], zip_safe=not (debug_build() or PLATFORM_MINGW), ) except RequiredDependencyException as err: - msg = """ + msg = f""" -The headers or library files could not be found for %s, +The headers or library files could not be found for {str(err)}, a required dependency when compiling Pillow from source. Please see the install instructions at: https://pillow.readthedocs.io/en/latest/installation.html -""" % ( - str(err) - ) +""" sys.stderr.write(msg) raise RequiredDependencyException(msg) except DependencyException as err: - msg = """ + msg = f""" -The headers or library files could not be found for %s, -which was requested by the option flag --enable-%s +The headers or library files could not be found for {str(err)}, +which was requested by the option flag --enable-{str(err)} -""" % ( - str(err), - str(err), - ) +""" sys.stderr.write(msg) raise DependencyException(msg) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index 7a485cf80..102b72e1d 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -17,13 +17,13 @@ # See the README file for information on usage and redistribution. # +""" +Parse X Bitmap Distribution Format (BDF) +""" + from . import FontFile, Image -# -------------------------------------------------------------------- -# parse X Bitmap Distribution Format (BDF) -# -------------------------------------------------------------------- - bdf_slant = { "R": "Roman", "I": "Italic", @@ -78,11 +78,9 @@ def bdf_char(f): return id, int(props["ENCODING"]), bbox, im -## -# Font file plugin for the X11 BDF format. - - class BdfFontFile(FontFile.FontFile): + """Font file plugin for the X11 BDF format.""" + def __init__(self, fp): super().__init__() diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 5ccba37db..d5d7c0e05 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -250,7 +250,7 @@ class BlpImageFile(ImageFile.ImageFile): decoder = "BLP2" self.mode = "RGBA" if self._blp_alpha_depth else "RGB" else: - raise BLPFormatError("Bad BLP magic %r" % (self.magic)) + raise BLPFormatError(f"Bad BLP magic {repr(self.magic)}") self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] @@ -282,8 +282,8 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): self.magic = self.fd.read(4) self._read_blp_header() self._load() - except struct.error: - raise OSError("Truncated Blp file") + except struct.error as e: + raise OSError("Truncated Blp file") from e return 0, 0 def _read_palette(self): @@ -336,11 +336,11 @@ class BLP1Decoder(_BLPBaseDecoder): self.set_as_raw(bytes(data)) else: raise BLPFormatError( - "Unsupported BLP encoding %r" % (self._blp_encoding) + f"Unsupported BLP encoding {repr(self._blp_encoding)}" ) else: raise BLPFormatError( - "Unsupported BLP compression %r" % (self._blp_encoding) + f"Unsupported BLP compression {repr(self._blp_encoding)}" ) def _decode_jpeg_stream(self): @@ -400,13 +400,15 @@ class BLP2Decoder(_BLPBaseDecoder): data += d else: raise BLPFormatError( - "Unsupported alpha encoding %r" % (self._blp_alpha_encoding) + f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}" ) else: - raise BLPFormatError("Unknown BLP encoding %r" % (self._blp_encoding)) + raise BLPFormatError(f"Unknown BLP encoding {repr(self._blp_encoding)}") else: - raise BLPFormatError("Unknown BLP compression %r" % (self._blp_compression)) + raise BLPFormatError( + f"Unknown BLP compression {repr(self._blp_compression)}" + ) self.set_as_raw(bytes(data)) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index cdef77ced..1bcbe93b2 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -25,7 +25,12 @@ from . import Image, ImageFile, ImagePalette -from ._binary import i8, i16le as i16, i32le as i32, o8, o16le as o16, o32le as o32 +from ._binary import i8 +from ._binary import i16le as i16 +from ._binary import i32le as i32 +from ._binary import o8 +from ._binary import o16le as o16 +from ._binary import o32le as o32 # # -------------------------------------------------------------------- @@ -144,7 +149,7 @@ class BmpImageFile(ImageFile.ImageFile): file_info["a_mask"], ) else: - raise OSError("Unsupported BMP header type (%d)" % file_info["header_size"]) + raise OSError(f"Unsupported BMP header type ({file_info['header_size']})") # ------------------ Special case : header is reported 40, which # ---------------------- is shorter than real size for bpp >= 16 @@ -157,14 +162,10 @@ class BmpImageFile(ImageFile.ImageFile): else (1 << file_info["bits"]) ) - # ------------------------------- Check abnormal values for DOS attacks - if file_info["width"] * file_info["height"] > 2 ** 31: - raise OSError("Unsupported BMP Size: (%dx%d)" % self.size) - # ---------------------- Check bit depth for unusual unsupported values self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) if self.mode is None: - raise OSError("Unsupported BMP pixel depth (%d)" % file_info["bits"]) + raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})") # ---------------- Process BMP with Bitfields compression (not palette) if file_info["compression"] == self.BITFIELDS: @@ -209,14 +210,14 @@ class BmpImageFile(ImageFile.ImageFile): if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset raw_mode, self.mode = "BGRA", "RGBA" else: - raise OSError("Unsupported BMP compression (%d)" % file_info["compression"]) + raise OSError(f"Unsupported BMP compression ({file_info['compression']})") # --------------- Once the header is processed, process the palette/LUT if self.mode == "P": # Paletted for 1, 4 and 8 bit images # ---------------------------------------------------- 1-bit images if not (0 < file_info["colors"] <= 65536): - raise OSError("Unsupported BMP Palette size (%d)" % file_info["colors"]) + raise OSError(f"Unsupported BMP Palette size ({file_info['colors']})") else: padding = file_info["palette_padding"] palette = read(padding * file_info["colors"]) @@ -263,7 +264,7 @@ class BmpImageFile(ImageFile.ImageFile): # read 14 bytes: magic number, filesize, reserved, header final offset head_data = self.fp.read(14) # choke if the file does not have the required magic bytes - if head_data[0:2] != b"BM": + if not _accept(head_data): raise SyntaxError("Not a BMP file") # read the start position of the BMP image data (u32) offset = i32(head_data[10:14]) @@ -304,8 +305,8 @@ def _dib_save(im, fp, filename): def _save(im, fp, filename, bitmap_header=True): try: rawmode, bits, colors = SAVE[im.mode] - except KeyError: - raise OSError("cannot write mode %s as BMP" % im.mode) + except KeyError as e: + raise OSError(f"cannot write mode {im.mode} as BMP") from e info = im.encoderinfo @@ -321,12 +322,15 @@ def _save(im, fp, filename, bitmap_header=True): # bitmap header if bitmap_header: offset = 14 + header + colors * 4 + file_size = offset + image + if file_size > 2 ** 32 - 1: + raise ValueError("File size is too large for the BMP format") fp.write( - b"BM" - + o32(offset + image) # file type (magic) - + o32(0) # file size - + o32(offset) # reserved - ) # image data offset + b"BM" # file type (magic) + + o32(file_size) # file size + + o32(0) # reserved + + o32(offset) # image data offset + ) # bitmap info header fp.write( diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 9727601ab..45e80b39a 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -14,14 +14,16 @@ # See the README file for information on usage and redistribution. # -## -# A file object that provides read access to a part of an existing -# file (for example a TAR file). import io class ContainerIO: + """ + 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): """ Create file object. @@ -82,7 +84,7 @@ class ContainerIO: else: n = self.length - self.pos if not n: # EOF - return "" + return b"" if "b" in self.fh.mode else "" self.pos = self.pos + n return self.fh.read(n) @@ -92,13 +94,14 @@ class ContainerIO: :returns: An 8-bit string. """ - s = "" + s = b"" if "b" in self.fh.mode else "" + newline_character = b"\n" if "b" in self.fh.mode else "\n" while True: c = self.read(1) if not c: break s = s + c - if c == "\n": + if c == newline_character: break return s diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index 3a1b6d2e5..35123f789 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -16,7 +16,9 @@ # See the README file for information on usage and redistribution. # from . import BmpImagePlugin, Image -from ._binary import i8, i16le as i16, i32le as i32 +from ._binary import i8 +from ._binary import i16le as i16 +from ._binary import i32le as i32 # # -------------------------------------------------------------------- diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index 7d2aff325..de21db8f0 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -46,7 +46,7 @@ class DcxImageFile(PcxImageFile): # Header s = self.fp.read(4) - if i32(s) != MAGIC: + if not _accept(s): raise SyntaxError("not a DCX file") # Component directory @@ -59,16 +59,10 @@ class DcxImageFile(PcxImageFile): self.__fp = self.fp self.frame = None + self.n_frames = len(self._offset) + self.is_animated = self.n_frames > 1 self.seek(0) - @property - def n_frames(self): - return len(self._offset) - - @property - def is_animated(self): - return len(self._offset) > 1 - def seek(self, frame): if not self._seek_check(frame): return diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 9ba6e0ff8..df2d0060c 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -94,6 +94,9 @@ DXT5_FOURCC = 0x35545844 # dxgiformat.h +DXGI_FORMAT_R8G8B8A8_TYPELESS = 27 +DXGI_FORMAT_R8G8B8A8_UNORM = 28 +DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 DXGI_FORMAT_BC7_TYPELESS = 97 DXGI_FORMAT_BC7_UNORM = 98 DXGI_FORMAT_BC7_UNORM_SRGB = 99 @@ -106,10 +109,10 @@ class DdsImageFile(ImageFile.ImageFile): def _open(self): magic, header_size = struct.unpack(" 1 # image characteristics self.mode = "P" @@ -59,6 +62,7 @@ class FliImageFile(ImageFile.ImageFile): # animation speed duration = i32(s[16:20]) + magic = i16(s[4:6]) if magic == 0xAF11: duration = (duration * 1000) // 70 self.info["duration"] = duration @@ -110,14 +114,6 @@ class FliImageFile(ImageFile.ImageFile): palette[i] = (r, g, b) i += 1 - @property - def n_frames(self): - return self.__framecount - - @property - def is_animated(self): - return self.__framecount > 1 - def seek(self, frame): if not self._seek_check(frame): return @@ -137,7 +133,7 @@ class FliImageFile(ImageFile.ImageFile): self.load() if frame != self.__frame + 1: - raise ValueError("cannot seek to frame %d" % frame) + raise ValueError(f"cannot seek to frame {frame}") self.__frame = frame # move to next frame diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 979a1e33c..c5fc80b37 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -23,18 +23,15 @@ WIDTH = 800 def puti16(fp, values): - # write network order (big-endian) 16-bit sequence + """Write network order (big-endian) 16-bit sequence""" for v in values: if v < 0: v += 65536 fp.write(_binary.o16be(v)) -## -# Base class for raster font file handlers. - - class FontFile: + """Base class for raster font file handlers.""" bitmap = None @@ -104,7 +101,7 @@ class FontFile: # font metrics with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: fp.write(b"PILfont\n") - fp.write((";;;;;;%d;\n" % self.ysize).encode("ascii")) # HACK!!! + fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! fp.write(b"DATA\n") for id in range(256): m = self.metrics[id] diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 3938f0f09..14070eebf 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -17,7 +17,8 @@ import olefile from . import Image, ImageFile -from ._binary import i8, i32le as i32 +from ._binary import i8 +from ._binary import i32le as i32 # we map from colour field tuples to (mode, rawmode) descriptors MODES = { @@ -59,8 +60,8 @@ class FpxImageFile(ImageFile.ImageFile): try: self.ole = olefile.OleFileIO(self.fp) - except OSError: - raise SyntaxError("not an FPX file; invalid OLE file") + except OSError as e: + raise SyntaxError("not an FPX file; invalid OLE file") from e if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": raise SyntaxError("not an FPX file; bad root CLSID") @@ -72,7 +73,7 @@ class FpxImageFile(ImageFile.ImageFile): # get the Image Contents Property Set prop = self.ole.getproperties( - ["Data Object Store %06d" % index, "\005Image Contents"] + [f"Data Object Store {index:06d}", "\005Image Contents"] ) # size (highest resolution) @@ -97,7 +98,10 @@ class FpxImageFile(ImageFile.ImageFile): s = prop[0x2000002 | id] colors = [] - for i in range(i32(s, 4)): + bands = i32(s, 4) + if bands > 4: + raise OSError("Invalid number of bands") + for i in range(bands): # note: for now, we ignore the "uncalibrated" flag colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF) @@ -117,8 +121,8 @@ class FpxImageFile(ImageFile.ImageFile): # setup tile descriptors for a given subimage stream = [ - "Data Object Store %06d" % index, - "Resolution %04d" % subimage, + f"Data Object Store {index:06d}", + f"Resolution {subimage:04d}", "Subimage 0000 Header", ] diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 096ccacac..900661238 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -89,7 +89,7 @@ class FtexImageFile(ImageFile.ImageFile): elif format == FORMAT_UNCOMPRESSED: self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] else: - raise ValueError("Invalid texture compression format: %r" % (format)) + raise ValueError(f"Invalid texture compression format: {repr(format)}") self.fp.close() self.fp = BytesIO(data) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 292de435c..5e447b411 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -47,7 +47,7 @@ class GbrImageFile(ImageFile.ImageFile): if header_size < 20: raise SyntaxError("not a GIMP brush") if version not in (1, 2): - raise SyntaxError("Unsupported GIMP brush version: %s" % version) + raise SyntaxError(f"Unsupported GIMP brush version: {version}") width = i32(self.fp.read(4)) height = i32(self.fp.read(4)) @@ -55,7 +55,7 @@ class GbrImageFile(ImageFile.ImageFile): if width <= 0 or height <= 0: raise SyntaxError("not a GIMP brush") if color_depth not in (1, 4): - raise SyntaxError("Unsupported GIMP brush color depth: %s" % color_depth) + raise SyntaxError(f"Unsupported GIMP brush color depth: {color_depth}") if version == 1: comment_length = header_size - 20 @@ -84,6 +84,10 @@ class GbrImageFile(ImageFile.ImageFile): self._data_size = width * height * color_depth def load(self): + if self.im: + # Already loaded + return + self.im = Image.core.new(self.mode, self.size) self.frombytes(self.fp.read(self._data_size)) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 54c88712c..8561f7b74 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -14,26 +14,32 @@ # -# NOTE: This format cannot be automatically recognized, so the -# class is not registered for use with Image.open(). To open a -# gd file, use the GdImageFile.open() function instead. +""" +.. note:: + This format cannot be automatically recognized, so the + class is not registered for use with :py:func:`PIL.Image.open()`. To open a + gd file, use the :py:func:`PIL.GdImageFile.open()` function instead. -# THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This -# implementation is provided for convenience and demonstrational -# purposes only. +.. warning:: + THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This + implementation is provided for convenience and demonstrational + purposes only. +""" from . import ImageFile, ImagePalette, UnidentifiedImageError -from ._binary import i8, i16be as i16, i32be as i32 - -## -# Image plugin for the GD uncompressed format. Note that this format -# is not supported by the standard Image.open function. To use -# this plugin, you have to import the GdImageFile module and -# use the GdImageFile.open function. +from ._binary import i8 +from ._binary import i16be as i16 +from ._binary import i32be as i32 class GdImageFile(ImageFile.ImageFile): + """ + Image plugin for the GD uncompressed format. Note that this format + is not supported by the standard :py:func:`PIL.Image.open()` function. To use + this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and + use the :py:func:`PIL.GdImageFile.open()` function. + """ format = "GD" format_description = "GD uncompressed images" @@ -74,12 +80,12 @@ def open(fp, mode="r"): :param mode: Optional mode. In this version, if the mode argument is given, it must be "r". :returns: An image instance. - :raises IOError: If the image could not be read. + :raises OSError: If the image could not be read. """ if mode != "r": raise ValueError("bad mode") try: return GdImageFile(fp) - except SyntaxError: - raise UnidentifiedImageError("cannot identify this image file") + except SyntaxError as e: + raise UnidentifiedImageError("cannot identify this image file") from e diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 7717fc707..4ca5a697e 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -25,11 +25,15 @@ # import itertools +import math import os import subprocess from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence -from ._binary import i8, i16le as i16, o8, o16le as o16 +from ._binary import i8 +from ._binary import i16le as i16 +from ._binary import o8 +from ._binary import o16le as o16 # -------------------------------------------------------------------- # Identify/read GIF files @@ -62,7 +66,7 @@ class GifImageFile(ImageFile.ImageFile): # Screen s = self.fp.read(13) - if s[:6] not in [b"GIF87a", b"GIF89a"]: + if not _accept(s): raise SyntaxError("not a GIF file") self.info["version"] = s[:6] @@ -129,9 +133,9 @@ class GifImageFile(ImageFile.ImageFile): for f in range(self.__frame + 1, frame + 1): try: self._seek(f) - except EOFError: + except EOFError as e: self.seek(last_frame) - raise EOFError("no more images in GIF file") + raise EOFError("no more images in GIF file") from e def _seek(self, frame): @@ -150,7 +154,7 @@ class GifImageFile(ImageFile.ImageFile): self.load() if frame != self.__frame + 1: - raise ValueError("cannot seek to frame %d" % frame) + raise ValueError(f"cannot seek to frame {frame}") self.__frame = frame self.tile = [] @@ -254,7 +258,7 @@ class GifImageFile(ImageFile.ImageFile): else: pass - # raise IOError, "illegal GIF tag `%x`" % i8(s) + # raise OSError, "illegal GIF tag `%x`" % i8(s) try: if self.disposal_method < 2: @@ -569,8 +573,11 @@ def _write_local_header(fp, im, offset, flags): if "comment" in im.encoderinfo and 1 <= len(im.encoderinfo["comment"]): fp.write(b"!" + o8(254)) # extension intro - for i in range(0, len(im.encoderinfo["comment"]), 255): - subblock = im.encoderinfo["comment"][i : i + 255] + comment = im.encoderinfo["comment"] + if isinstance(comment, str): + comment = comment.encode() + for i in range(0, len(comment), 255): + subblock = comment[i : i + 255] fp.write(o8(len(subblock)) + subblock) fp.write(o8(0)) if "loop" in im.encoderinfo: @@ -616,38 +623,42 @@ def _save_netpbm(im, fp, filename): # below for information on how to enable this. tempfile = im._dump() - with open(filename, "wb") as f: - if im.mode != "RGB": - subprocess.check_call( - ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL - ) - else: - # Pipe ppmquant output into ppmtogif - # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) - quant_cmd = ["ppmquant", "256", tempfile] - togif_cmd = ["ppmtogif"] - quant_proc = subprocess.Popen( - quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL - ) - togif_proc = subprocess.Popen( - togif_cmd, stdin=quant_proc.stdout, stdout=f, stderr=subprocess.DEVNULL - ) - - # Allow ppmquant to receive SIGPIPE if ppmtogif exits - quant_proc.stdout.close() - - retcode = quant_proc.wait() - if retcode: - raise subprocess.CalledProcessError(retcode, quant_cmd) - - retcode = togif_proc.wait() - if retcode: - raise subprocess.CalledProcessError(retcode, togif_cmd) - try: - os.unlink(tempfile) - except OSError: - pass + with open(filename, "wb") as f: + if im.mode != "RGB": + subprocess.check_call( + ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL + ) + else: + # Pipe ppmquant output into ppmtogif + # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) + quant_cmd = ["ppmquant", "256", tempfile] + togif_cmd = ["ppmtogif"] + quant_proc = subprocess.Popen( + quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) + togif_proc = subprocess.Popen( + togif_cmd, + stdin=quant_proc.stdout, + stdout=f, + stderr=subprocess.DEVNULL, + ) + + # Allow ppmquant to receive SIGPIPE if ppmtogif exits + quant_proc.stdout.close() + + retcode = quant_proc.wait() + if retcode: + raise subprocess.CalledProcessError(retcode, quant_cmd) + + retcode = togif_proc.wait() + if retcode: + raise subprocess.CalledProcessError(retcode, togif_cmd) + finally: + try: + os.unlink(tempfile) + except OSError: + pass # Force optimization so that we can test performance against @@ -694,14 +705,12 @@ def _get_optimize(im, info): def _get_color_table_size(palette_bytes): # calculate the palette size for the header - import math - if not palette_bytes: return 0 elif len(palette_bytes) < 9: return 1 else: - return int(math.ceil(math.log(len(palette_bytes) // 3, 2))) - 1 + return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1 def _get_header_palette(palette_bytes): diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index 851e24d62..7ab7f9990 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -13,17 +13,19 @@ # See the README file for information on usage and redistribution. # +""" +Stuff to translate curve segments to palette values (derived from +the corresponding code in GIMP, written by Federico Mena Quintero. +See the GIMP distribution for more information.) +""" + + from math import log, pi, sin, sqrt from ._binary import o8 -# -------------------------------------------------------------------- -# Stuff to translate curve segments to palette values (derived from -# the corresponding code in GIMP, written by Federico Mena Quintero. -# See the GIMP distribution for more information.) -# - EPSILON = 1e-10 +"""""" # Enable auto-doc for data member def linear(middle, pos): @@ -58,6 +60,7 @@ def sphere_decreasing(middle, pos): SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] +"""""" # Enable auto-doc for data member class GradientFile: @@ -73,7 +76,7 @@ class GradientFile: for i in range(entries): - x = i / float(entries - 1) + x = i / (entries - 1) while x1 < x: ix += 1 @@ -98,11 +101,9 @@ class GradientFile: return b"".join(palette), "RGBA" -## -# File handler for GIMP's gradient format. - - class GimpGradientFile(GradientFile): + """File handler for GIMP's gradient format.""" + def __init__(self, fp): if fp.readline()[:13] != b"GIMP Gradient": diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index e3060ab8a..10fd3ad81 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -18,11 +18,9 @@ import re from ._binary import o8 -## -# File handler for GIMP's palette format. - class GimpPaletteFile: + """File handler for GIMP's palette format.""" rawmode = "RGB" diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 18e8403d1..b9b1ae007 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -23,10 +23,10 @@ import subprocess import sys import tempfile -from PIL import Image, ImageFile, PngImagePlugin +from PIL import Image, ImageFile, PngImagePlugin, features from PIL._binary import i8 -enable_jpeg2k = hasattr(Image.core, "jp2klib_version") +enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: from PIL import Jpeg2KImagePlugin @@ -83,7 +83,7 @@ def read_32(fobj, start_length, size): if bytesleft <= 0: break if bytesleft != 0: - raise SyntaxError("Error reading channel [%r left]" % bytesleft) + raise SyntaxError(f"Error reading channel [{repr(bytesleft)} left]") band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) im.im.putband(band.im, band_ix) return {"RGB": im} @@ -321,7 +321,7 @@ def _save(im, fp, filename): last_w = None second_path = None for w in [16, 32, 128, 256, 512]: - prefix = "icon_{}x{}".format(w, w) + prefix = f"icon_{w}x{w}" first_path = os.path.join(iconset, prefix + ".png") if last_w == w: @@ -337,6 +337,10 @@ def _save(im, fp, filename): # iconutil -c icns -o {} {} + fp_only = not filename + if fp_only: + f, filename = tempfile.mkstemp(".icns") + os.close(f) convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] convert_proc = subprocess.Popen( convert_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL @@ -349,6 +353,10 @@ def _save(im, fp, filename): if retcode: raise subprocess.CalledProcessError(retcode, convert_cmd) + if fp_only: + with open(filename, "rb") as f: + fp.write(f.read()) + Image.register_open(IcnsImageFile.format, IcnsImageFile, lambda x: x[:4] == b"icns") Image.register_extension(IcnsImageFile.format, ".icns") @@ -365,13 +373,12 @@ if __name__ == "__main__": print("Syntax: python IcnsImagePlugin.py [file]") sys.exit() - imf = IcnsImageFile(open(sys.argv[1], "rb")) - for size in imf.info["sizes"]: - imf.size = size - imf.load() - im = imf.im - im.save("out-%s-%s-%s.png" % size) - im = Image.open(sys.argv[1]) - im.save("out.png") - if sys.platform == "windows": - os.startfile("out.png") + with open(sys.argv[1], "rb") as fp: + imf = IcnsImageFile(fp) + for size in imf.info["sizes"]: + imf.size = size + imf.save("out-%s-%s-%s.png" % size) + with Image.open(sys.argv[1]) as im: + im.save("out.png") + if sys.platform == "windows": + os.startfile("out.png") diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 19cb548a6..57bb17ee9 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -28,7 +28,9 @@ from io import BytesIO from math import ceil, log from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin -from ._binary import i8, i16le as i16, i32le as i32 +from ._binary import i8 +from ._binary import i16le as i16 +from ._binary import i32le as i32 # # -------------------------------------------------------------------- @@ -63,8 +65,9 @@ def _save(im, fp, filename): fp.write(struct.pack("= (3, 7): + + def __getattr__(name): + if name == "PILLOW_VERSION": + _raise_version_warning() + return __version__ + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +else: + + from . import PILLOW_VERSION + + # Silence warning + assert PILLOW_VERSION + + logger = logging.getLogger(__name__) @@ -56,7 +81,7 @@ class DecompressionBombError(Exception): pass -# Limit to around a quarter gigabyte for a 24 bit (3 bpp) image +# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3) @@ -71,8 +96,8 @@ try: if __version__ != getattr(core, "PILLOW_VERSION", None): raise ImportError( "The _imaging extension was built for another version of Pillow or PIL:\n" - "Core version: %s\n" - "Pillow version: %s" % (getattr(core, "PILLOW_VERSION", None), __version__) + f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n" + f"Pillow version: {__version__}" ) except ImportError as v: @@ -144,6 +169,9 @@ HAMMING = 5 BICUBIC = CUBIC = 3 LANCZOS = ANTIALIAS = 1 +_filters_support = {BOX: 0.5, BILINEAR: 1.0, HAMMING: 1.0, BICUBIC: 2.0, LANCZOS: 3.0} + + # dithers NEAREST = NONE = 0 ORDERED = 1 # Not yet implemented @@ -375,7 +403,7 @@ def init(): for plugin in _plugins: try: logger.debug("Importing %s", plugin) - __import__("PIL.%s" % plugin, globals(), locals(), []) + __import__(f"PIL.{plugin}", globals(), locals(), []) except ImportError as e: logger.debug("Image: failed to import %s: %s", plugin, e) @@ -398,15 +426,17 @@ def _getdecoder(mode, decoder_name, args, extra=()): try: decoder = DECODERS[decoder_name] - return decoder(mode, *args + extra) except KeyError: pass + else: + return decoder(mode, *args + extra) + try: # get decoder decoder = getattr(core, decoder_name + "_decoder") - return decoder(mode, *args + extra) - except AttributeError: - raise OSError("decoder %s not available" % decoder_name) + except AttributeError as e: + raise OSError(f"decoder {decoder_name} not available") from e + return decoder(mode, *args + extra) def _getencoder(mode, encoder_name, args, extra=()): @@ -419,15 +449,17 @@ def _getencoder(mode, encoder_name, args, extra=()): try: encoder = ENCODERS[encoder_name] - return encoder(mode, *args + extra) except KeyError: pass + else: + return encoder(mode, *args + extra) + try: # get encoder encoder = getattr(core, encoder_name + "_encoder") - return encoder(mode, *args + extra) - except AttributeError: - raise OSError("encoder %s not available" % encoder_name) + except AttributeError as e: + raise OSError(f"encoder {encoder_name} not available") from e + return encoder(mode, *args + extra) # -------------------------------------------------------------------- @@ -562,7 +594,8 @@ class Image: try: if hasattr(self, "_close__fp"): self._close__fp() - self.fp.close() + if self.fp: + self.fp.close() self.fp = None except Exception as msg: logger.debug("Error closing: %s", msg) @@ -632,7 +665,7 @@ class Image: ) def _repr_png_(self): - """ iPython display hook support + """iPython display hook support :returns: png version of the image as bytes """ @@ -685,7 +718,7 @@ class Image: :param encoder_name: What encoder to use. The default is to use the standard "raw" encoder. :param args: Extra arguments to the encoder. - :rtype: A bytes object. + :returns: A :py:class:`bytes` object. """ # may pass tuple instead of argument list @@ -710,15 +743,10 @@ class Image: if s: break if s < 0: - raise RuntimeError("encoder error %d in tobytes" % s) + raise RuntimeError(f"encoder error {s} in tobytes") return b"".join(data) - def tostring(self, *args, **kw): - raise NotImplementedError( - "tostring() has been removed. Please call tobytes() instead." - ) - def tobitmap(self, name="image"): """ Returns the image converted to an X11 bitmap. @@ -736,9 +764,9 @@ class Image: data = self.tobytes("xbm") return b"".join( [ - ("#define %s_width %d\n" % (name, self.size[0])).encode("ascii"), - ("#define %s_height %d\n" % (name, self.size[1])).encode("ascii"), - ("static char %s_bits[] = {\n" % name).encode("ascii"), + f"#define {name}_width {self.size[0]}\n".encode("ascii"), + f"#define {name}_height {self.size[1]}\n".encode("ascii"), + f"static char {name}_bits[] = {{\n".encode("ascii"), data, b"};", ] @@ -770,11 +798,6 @@ class Image: if s[1] != 0: raise ValueError("cannot decode image data") - def fromstring(self, *args, **kw): - raise NotImplementedError( - "fromstring() has been removed. Please call frombytes() instead." - ) - def load(self): """ Allocates storage for the image and loads the pixel data. In @@ -833,7 +856,7 @@ class Image: and the palette can be represented without a palette. The current version supports all possible conversions between - "L", "RGB" and "CMYK." The **matrix** argument only supports "L" + "L", "RGB" and "CMYK." The ``matrix`` argument only supports "L" and "RGB". When translating a color image to greyscale (mode "L"), @@ -844,24 +867,24 @@ class Image: The default method of converting a greyscale ("L") or "RGB" image into a bilevel (mode "1") image uses Floyd-Steinberg dither to approximate the original image luminosity levels. If - dither is NONE, all values larger than 128 are set to 255 (white), + dither is :data:`NONE`, all values larger than 128 are set to 255 (white), all other values to 0 (black). To use other thresholds, use the :py:meth:`~PIL.Image.Image.point` method. - When converting from "RGBA" to "P" without a **matrix** argument, + When converting from "RGBA" to "P" without a ``matrix`` argument, this passes the operation to :py:meth:`~PIL.Image.Image.quantize`, - and **dither** and **palette** are ignored. + and ``dither`` and ``palette`` are ignored. :param mode: The requested mode. See: :ref:`concept-modes`. :param matrix: An optional conversion matrix. If given, this should be 4- or 12-tuple containing floating point values. :param dither: Dithering method, used when converting from mode "RGB" to "P" or from "RGB" or "L" to "1". - Available methods are NONE or FLOYDSTEINBERG (default). - Note that this is not used when **matrix** is supplied. + Available methods are :data:`NONE` or :data:`FLOYDSTEINBERG` (default). + Note that this is not used when ``matrix`` is supplied. :param palette: Palette to use when converting from mode "RGB" - to "P". Available palettes are WEB or ADAPTIVE. - :param colors: Number of colors to use for the ADAPTIVE palette. + to "P". Available palettes are :data:`WEB` or :data:`ADAPTIVE`. + :param colors: Number of colors to use for the :data:`ADAPTIVE` palette. Defaults to 256. :rtype: :py:class:`~PIL.Image.Image` :returns: An :py:class:`~PIL.Image.Image` object. @@ -939,10 +962,10 @@ class Image: if isinstance(t, tuple): try: t = trns_im.palette.getcolor(t) - except Exception: + except Exception as e: raise ValueError( "Couldn't allocate a palette color for transparency" - ) + ) from e trns_im.putpixel((0, 0), t) if mode in ("L", "RGB"): @@ -995,8 +1018,8 @@ class Image: # normalize source image and try again im = self.im.convert(getmodebase(self.mode)) im = im.convert(mode, dither) - except KeyError: - raise ValueError("illegal conversion") + except KeyError as e: + raise ValueError("illegal conversion") from e new_im = self._new(im) if delete_trns: @@ -1019,16 +1042,18 @@ class Image: of colors. :param colors: The desired number of colors, <= 256 - :param method: 0 = median cut - 1 = maximum coverage - 2 = fast octree - 3 = libimagequant + :param method: :data:`MEDIANCUT` (median cut), + :data:`MAXCOVERAGE` (maximum coverage), + :data:`FASTOCTREE` (fast octree), + :data:`LIBIMAGEQUANT` (libimagequant; check support using + :py:func:`PIL.features.check_feature` + with ``feature="libimagequant"``). :param kmeans: Integer :param palette: Quantize to the palette of given :py:class:`PIL.Image.Image`. :param dither: Dithering method, used when converting from mode "RGB" to "P" or from "RGB" or "L" to "1". - Available methods are NONE or FLOYDSTEINBERG (default). + Available methods are :data:`NONE` or :data:`FLOYDSTEINBERG` (default). Default: 1 (legacy setting) :returns: A new image @@ -1126,16 +1151,18 @@ class Image: """ Configures the image file loader so it returns a version of the image that as closely as possible matches the given mode and - size. For example, you can use this method to convert a color - JPEG to greyscale while loading it, or to extract a 128x192 - version from a PCD file. + size. For example, you can use this method to convert a color + JPEG to greyscale while loading it. + + If any changes are made, returns a tuple with the chosen ``mode`` and + ``box`` with coordinates of the original image within the altered one. Note that this method modifies the :py:class:`~PIL.Image.Image` object - in place. If the image has already been loaded, this method has no + in place. If the image has already been loaded, this method has no effect. Note: This method is not implemented for most images. It is - currently implemented only for JPEG and PCD images. + currently implemented only for JPEG and MPO images. :param mode: The requested mode. :param size: The requested size. @@ -1154,7 +1181,7 @@ class Image: available filters, see the :py:mod:`~PIL.ImageFilter` module. :param filter: Filter kernel. - :returns: An :py:class:`~PIL.Image.Image` object. """ + :returns: An :py:class:`~PIL.Image.Image` object.""" from . import ImageFilter @@ -1179,7 +1206,7 @@ class Image: def getbands(self): """ Returns a tuple containing the name of each band in this image. - For example, **getbands** on an RGB image returns ("R", "G", "B"). + For example, ``getbands`` on an RGB image returns ("R", "G", "B"). :returns: A tuple containing band names. :rtype: tuple @@ -1233,7 +1260,7 @@ class Image: Note that the sequence object returned by this method is an internal PIL data type, which only supports certain sequence operations. To convert it to an ordinary sequence (e.g. for - printing), use **list(im.getdata())**. + printing), use ``list(im.getdata())``. :param band: What band to return. The default is to return all bands. To return a single band, pass in the index @@ -1267,7 +1294,28 @@ class Image: def getexif(self): if self._exif is None: self._exif = Exif() - self._exif.load(self.info.get("exif")) + + exif_info = self.info.get("exif") + if exif_info is None and "Raw profile type exif" in self.info: + exif_info = bytes.fromhex( + "".join(self.info["Raw profile type exif"].split("\n")[3:]) + ) + self._exif.load(exif_info) + + # XMP tags + if 0x0112 not in self._exif: + xmp_tags = self.info.get("XML:com.adobe.xmp") + if xmp_tags: + root = xml.etree.ElementTree.fromstring(xmp_tags) + for elem in root.iter(): + if elem.tag.endswith("}Description"): + orientation = elem.attrib.get( + "{http://ns.adobe.com/tiff/1.0/}Orientation" + ) + if orientation: + self._exif[0x0112] = int(orientation) + break + return self._exif def getim(self): @@ -1377,11 +1425,6 @@ class Image: return self.im.entropy(extrema) return self.im.entropy() - def offset(self, xoffset, yoffset=None): - raise NotImplementedError( - "offset() has been removed. Please call ImageChops.offset() instead." - ) - def paste(self, im, box=None, mask=None): """ Pastes another image into this image. The box argument is either @@ -1463,7 +1506,7 @@ class Image: self.im.paste(im, box) def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): - """ 'In-place' analog of Image.alpha_composite. Composites an image + """'In-place' analog of Image.alpha_composite. Composites an image onto this image. :param im: image to composite over this one @@ -1520,6 +1563,13 @@ class Image: single argument. The function is called once for each possible pixel value, and the resulting table is applied to all bands of the image. + + It may also be an :py:class:`~PIL.Image.ImagePointHandler` + object:: + + class Example(Image.ImagePointHandler): + def point(self, data): + # Return result :param mode: Output mode (default is same as input). In the current version, this can only be used if the source image has mode "L" or "P", and the output has mode "1" or the @@ -1568,16 +1618,16 @@ class Image: mode = getmodebase(self.mode) + "A" try: self.im.setmode(mode) - except (AttributeError, ValueError): + except (AttributeError, ValueError) as e: # do things the hard way im = self.im.convert(mode) if im.mode not in ("LA", "PA", "RGBA"): - raise ValueError # sanity check + raise ValueError from e # sanity check self.im = im self.pyaccess = None self.mode = self.im.mode - except (KeyError, ValueError): - raise ValueError("illegal image mode") + except (KeyError, ValueError) as e: + raise ValueError("illegal image mode") from e if self.mode in ("LA", "PA"): band = 1 @@ -1691,7 +1741,7 @@ class Image: Rewrites the image to reorder the palette. :param dest_map: A list of indexes into the original palette. - e.g. [1,0] would swap a two item palette, and list(range(256)) + e.g. ``[1,0]`` would swap a two item palette, and ``list(range(256))`` is the identity transform. :param source_palette: Bytes or None. :returns: An :py:class:`~PIL.Image.Image` object. @@ -1761,28 +1811,58 @@ class Image: return m_im - def resize(self, size, resample=NEAREST, box=None): + def _get_safe_box(self, size, resample, box): + """Expands the box so it includes adjacent pixels + that may be used by resampling with the given resampling filter. + """ + filter_support = _filters_support[resample] - 0.5 + scale_x = (box[2] - box[0]) / size[0] + scale_y = (box[3] - box[1]) / size[1] + support_x = filter_support * scale_x + support_y = filter_support * scale_y + + return ( + max(0, int(box[0] - support_x)), + max(0, int(box[1] - support_y)), + min(self.size[0], math.ceil(box[2] + support_x)), + min(self.size[1], math.ceil(box[3] + support_y)), + ) + + def resize(self, size, resample=BICUBIC, box=None, reducing_gap=None): """ Returns a resized copy of this image. :param size: The requested size in pixels, as a 2-tuple: (width, height). :param resample: An optional resampling filter. This can be - one of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BOX`, - :py:attr:`PIL.Image.BILINEAR`, :py:attr:`PIL.Image.HAMMING`, - :py:attr:`PIL.Image.BICUBIC` or :py:attr:`PIL.Image.LANCZOS`. - If omitted, or if the image has mode "1" or "P", it is - set :py:attr:`PIL.Image.NEAREST`. + one of :py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BOX`, + :py:data:`PIL.Image.BILINEAR`, :py:data:`PIL.Image.HAMMING`, + :py:data:`PIL.Image.BICUBIC` or :py:data:`PIL.Image.LANCZOS`. + Default filter is :py:data:`PIL.Image.BICUBIC`. + If the image has mode "1" or "P", it is + always set to :py:data:`PIL.Image.NEAREST`. See: :ref:`concept-filters`. - :param box: An optional 4-tuple of floats giving the region - of the source image which should be scaled. - The values should be within (0, 0, width, height) rectangle. + :param box: An optional 4-tuple of floats providing + the source image region to be scaled. + The values must be within (0, 0, width, height) rectangle. If omitted or None, the entire source is used. + :param reducing_gap: Apply optimization by resizing the image + in two steps. First, reducing the image by integer times + using :py:meth:`~PIL.Image.Image.reduce`. + Second, resizing using regular resampling. The last step + changes size no less than by ``reducing_gap`` times. + ``reducing_gap`` may be None (no first step is performed) + or should be greater than 1.0. The bigger ``reducing_gap``, + the closer the result to the fair resampling. + The smaller ``reducing_gap``, the faster resizing. + With ``reducing_gap`` greater or equal to 3.0, the result is + indistinguishable from fair resampling in most cases. + The default value is None (no optimization). :returns: An :py:class:`~PIL.Image.Image` object. """ if resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING): - message = "Unknown resampling filter ({}).".format(resample) + message = f"Unknown resampling filter ({resample})." filters = [ "{} ({})".format(filter[1], filter[0]) @@ -1799,6 +1879,9 @@ class Image: message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] ) + if reducing_gap is not None and reducing_gap < 1.0: + raise ValueError("reducing_gap must be 1.0 or greater") + size = tuple(size) if box is None: @@ -1819,8 +1902,58 @@ class Image: self.load() + if reducing_gap is not None and resample != NEAREST: + factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 + factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 + 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) + box = ( + (box[0] - reduce_box[0]) / factor_x, + (box[1] - reduce_box[1]) / factor_y, + (box[2] - reduce_box[0]) / factor_x, + (box[3] - reduce_box[1]) / factor_y, + ) + return self._new(self.im.resize(size, resample, box)) + def reduce(self, factor, box=None): + """ + Returns a copy of the image reduced ``factor`` times. + If the size of the image is not dividable by ``factor``, + the resulting size will be rounded up. + + :param factor: A greater than 0 integer or tuple of two integers + for width and height separately. + :param box: An optional 4-tuple of ints providing + the source image region to be reduced. + The values must be within ``(0, 0, width, height)`` rectangle. + If omitted or ``None``, the entire source is used. + """ + if not isinstance(factor, (list, tuple)): + factor = (factor, factor) + + if box is None: + box = (0, 0) + self.size + else: + box = tuple(box) + + if factor == (1, 1) and box == (0, 0) + self.size: + return self.copy() + + if self.mode in ["LA", "RGBA"]: + im = self.convert(self.mode[:-1] + "a") + im = im.reduce(factor, box) + return im.convert(self.mode) + + self.load() + + return self._new(self.im.reduce(factor, box)) + def rotate( self, angle, @@ -1837,12 +1970,12 @@ class Image: :param angle: In degrees counter clockwise. :param resample: An optional resampling filter. This can be - one of :py:attr:`PIL.Image.NEAREST` (use nearest neighbour), - :py:attr:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:attr:`PIL.Image.BICUBIC` + one of :py:data:`PIL.Image.NEAREST` (use nearest neighbour), + :py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 + environment), or :py:data:`PIL.Image.BICUBIC` (cubic spline interpolation in a 4x4 environment). If omitted, or if the image has mode "1" or "P", it is - set :py:attr:`PIL.Image.NEAREST`. See :ref:`concept-filters`. + set to :py:data:`PIL.Image.NEAREST`. See :ref:`concept-filters`. :param expand: Optional expansion flag. If true, expands the output image to make it large enough to hold the entire rotated image. If false or omitted, make the output image the same size as the @@ -1927,8 +2060,8 @@ class Image: x, y = transform(x, y, matrix) xx.append(x) yy.append(y) - nw = int(math.ceil(max(xx)) - math.floor(min(xx))) - nh = int(math.ceil(max(yy)) - math.floor(min(yy))) + nw = math.ceil(max(xx)) - math.floor(min(xx)) + nh = math.ceil(max(yy)) - math.floor(min(yy)) # We multiply a translation matrix from the right. Because of its # special form, this is the same as taking the image of the @@ -1964,7 +2097,7 @@ class Image: :returns: None :exception ValueError: If the output format could not be determined from the file name. Use the format option to solve this. - :exception IOError: If the file could not be written. The file + :exception OSError: If the file could not be written. The file may have been created, and may contain partial data. """ @@ -1996,8 +2129,8 @@ class Image: init() try: format = EXTENSION[ext] - except KeyError: - raise ValueError("unknown file extension: {}".format(ext)) + except KeyError as e: + raise ValueError(f"unknown file extension: {ext}") from e if format.upper() not in SAVE: init() @@ -2025,11 +2158,14 @@ class Image: """ Seeks to the given frame in this sequence file. If you seek beyond the end of the sequence, the method raises an - **EOFError** exception. When a sequence file is opened, the + ``EOFError`` exception. When a sequence file is opened, the library automatically seeks to frame 0. See :py:meth:`~PIL.Image.Image.tell`. + If defined, :attr:`~PIL.Image.Image.n_frames` refers to the + number of available frames. + :param frame: Frame number, starting at 0. :exception EOFError: If the call attempts to seek beyond the end of the sequence. @@ -2041,8 +2177,10 @@ class Image: def show(self, title=None, command=None): """ - Displays this image. This method is mainly intended for - debugging purposes. + Displays this image. This method is mainly intended for debugging purposes. + + This method calls :py:func:`PIL.ImageShow.show` internally. You can use + :py:func:`PIL.ImageShow.register` to override its default behaviour. The image is first saved to a temporary file. By default, it will be in PNG format. @@ -2054,11 +2192,16 @@ class Image: On Windows, the image is opened with the standard PNG display utility. - :param title: Optional title to use for the image window, - where possible. - :param command: command used to show the image + :param title: Optional title to use for the image window, where possible. """ + if command is not None: + warnings.warn( + "The command parameter is deprecated and will be removed in a future " + "release. Use a subclass of ImageShow.Viewer instead.", + DeprecationWarning, + ) + _show(self, title=title, command=command) def split(self): @@ -2098,8 +2241,8 @@ class Image: if isinstance(channel, str): try: channel = self.getbands().index(channel) - except ValueError: - raise ValueError('The image has no channel "{}"'.format(channel)) + except ValueError as e: + raise ValueError(f'The image has no channel "{channel}"') from e return self._new(self.im.getband(channel)) @@ -2107,11 +2250,14 @@ class Image: """ Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`. + If defined, :attr:`~PIL.Image.Image.n_frames` refers to the + number of available frames. + :returns: Frame number, starting with 0. """ return 0 - def thumbnail(self, size, resample=BICUBIC): + def thumbnail(self, size, resample=BICUBIC, reducing_gap=2.0): """ Make this image into a thumbnail. This method modifies the image to contain a thumbnail version of itself, no larger than @@ -2127,30 +2273,54 @@ class Image: :param size: Requested size. :param resample: Optional resampling filter. This can be one - of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`, - :py:attr:`PIL.Image.BICUBIC`, or :py:attr:`PIL.Image.LANCZOS`. - If omitted, it defaults to :py:attr:`PIL.Image.BICUBIC`. - (was :py:attr:`PIL.Image.NEAREST` prior to version 2.5.0) + of :py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BOX`, + :py:data:`PIL.Image.BILINEAR`, :py:data:`PIL.Image.HAMMING`, + :py:data:`PIL.Image.BICUBIC` or :py:data:`PIL.Image.LANCZOS`. + If omitted, it defaults to :py:data:`PIL.Image.BICUBIC`. + (was :py:data:`PIL.Image.NEAREST` prior to version 2.5.0). + See: :ref:`concept-filters`. + :param reducing_gap: Apply optimization by resizing the image + in two steps. First, reducing the image by integer times + using :py:meth:`~PIL.Image.Image.reduce` or + :py:meth:`~PIL.Image.Image.draft` for JPEG images. + Second, resizing using regular resampling. The last step + changes size no less than by ``reducing_gap`` times. + ``reducing_gap`` may be None (no first step is performed) + or should be greater than 1.0. The bigger ``reducing_gap``, + the closer the result to the fair resampling. + The smaller ``reducing_gap``, the faster resizing. + With ``reducing_gap`` greater or equal to 3.0, the result is + indistinguishable from fair resampling in most cases. + The default value is 2.0 (very close to fair resampling + while still being faster in many cases). :returns: None """ - # preserve aspect ratio - x, y = self.size - if x > size[0]: - y = int(max(y * size[0] / x, 1)) - x = int(size[0]) - if y > size[1]: - x = int(max(x * size[1] / y, 1)) - y = int(size[1]) - size = x, y - - if size == self.size: + x, y = map(math.floor, size) + if x >= self.width and y >= self.height: return - self.draft(None, size) + def round_aspect(number, key): + return max(min(math.floor(number), math.ceil(number), key=key), 1) + + # preserve aspect ratio + aspect = self.width / self.height + if x / y >= aspect: + x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) + else: + y = round_aspect( + x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) + ) + size = (x, y) + + box = None + if reducing_gap is not None: + res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) + if res is not None: + box = res[1] if self.size != size: - im = self.resize(size, resample) + im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) self.im = im.im self._size = size @@ -2171,34 +2341,37 @@ class Image: :param size: The output size. :param method: The transformation method. This is one of - :py:attr:`PIL.Image.EXTENT` (cut out a rectangular subregion), - :py:attr:`PIL.Image.AFFINE` (affine transform), - :py:attr:`PIL.Image.PERSPECTIVE` (perspective transform), - :py:attr:`PIL.Image.QUAD` (map a quadrilateral to a rectangle), or - :py:attr:`PIL.Image.MESH` (map a number of source quadrilaterals + :py:data:`PIL.Image.EXTENT` (cut out a rectangular subregion), + :py:data:`PIL.Image.AFFINE` (affine transform), + :py:data:`PIL.Image.PERSPECTIVE` (perspective transform), + :py:data:`PIL.Image.QUAD` (map a quadrilateral to a rectangle), or + :py:data:`PIL.Image.MESH` (map a number of source quadrilaterals in one operation). It may also be an :py:class:`~PIL.Image.ImageTransformHandler` object:: + class Example(Image.ImageTransformHandler): - def transform(size, method, data, resample, fill=1): + def transform(self, size, data, resample, fill=1): # Return result - It may also be an object with a :py:meth:`~method.getdata` method - that returns a tuple supplying new **method** and **data** values:: - class Example(object): + It may also be an object with a ``method.getdata`` method + that returns a tuple supplying new ``method`` and ``data`` values:: + + class Example: def getdata(self): method = Image.EXTENT data = (0, 0, 100, 100) return method, data :param data: Extra data to the transformation method. :param resample: Optional resampling filter. It can be one of - :py:attr:`PIL.Image.NEAREST` (use nearest neighbour), - :py:attr:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:attr:`PIL.Image.BICUBIC` (cubic spline + :py:data:`PIL.Image.NEAREST` (use nearest neighbour), + :py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 + environment), or :py:data:`PIL.Image.BICUBIC` (cubic spline interpolation in a 4x4 environment). If omitted, or if the image - has mode "1" or "P", it is set to :py:attr:`PIL.Image.NEAREST`. - :param fill: If **method** is an + has mode "1" or "P", it is set to :py:data:`PIL.Image.NEAREST`. + See: :ref:`concept-filters`. + :param fill: If ``method`` is an :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of the arguments passed to it. Otherwise, it is unused. :param fillcolor: Optional fill color for the area outside the @@ -2253,8 +2426,8 @@ class Image: elif method == EXTENT: # convert extent to an affine transform x0, y0, x1, y1 = data - xs = float(x1 - x0) / w - ys = float(y1 - y0) / h + xs = (x1 - x0) / w + ys = (y1 - y0) / h method = AFFINE data = (xs, 0, x0, 0, ys, y0) @@ -2291,9 +2464,9 @@ class Image: BOX: "Image.BOX", HAMMING: "Image.HAMMING", LANCZOS: "Image.LANCZOS/Image.ANTIALIAS", - }[resample] + " ({}) cannot be used.".format(resample) + }[resample] + f" ({resample}) cannot be used." else: - message = "Unknown resampling filter ({}).".format(resample) + message = f"Unknown resampling filter ({resample})." filters = [ "{} ({})".format(filter[1], filter[0]) @@ -2320,10 +2493,10 @@ class Image: """ Transpose image (flip or rotate in 90 degree steps) - :param method: One of :py:attr:`PIL.Image.FLIP_LEFT_RIGHT`, - :py:attr:`PIL.Image.FLIP_TOP_BOTTOM`, :py:attr:`PIL.Image.ROTATE_90`, - :py:attr:`PIL.Image.ROTATE_180`, :py:attr:`PIL.Image.ROTATE_270`, - :py:attr:`PIL.Image.TRANSPOSE` or :py:attr:`PIL.Image.TRANSVERSE`. + :param method: One of :py:data:`PIL.Image.FLIP_LEFT_RIGHT`, + :py:data:`PIL.Image.FLIP_TOP_BOTTOM`, :py:data:`PIL.Image.ROTATE_90`, + :py:data:`PIL.Image.ROTATE_180`, :py:data:`PIL.Image.ROTATE_270`, + :py:data:`PIL.Image.TRANSPOSE` or :py:data:`PIL.Image.TRANSVERSE`. :returns: Returns a flipped or rotated copy of this image. """ @@ -2361,12 +2534,20 @@ class Image: class ImagePointHandler: - # used as a mixin by point transforms (for use with im.point) + """ + Used as a mixin by point transforms + (for use with :py:meth:`~PIL.Image.Image.point`) + """ + pass class ImageTransformHandler: - # used as a mixin by geometry transforms (for use with im.transform) + """ + Used as a mixin by geometry transforms + (for use with :py:meth:`~PIL.Image.Image.transform`) + """ + pass @@ -2478,12 +2659,6 @@ def frombytes(mode, size, data, decoder_name="raw", *args): return im -def fromstring(*args, **kw): - raise NotImplementedError( - "fromstring() has been removed. Please call frombytes() instead." - ) - - def frombuffer(mode, size, data, decoder_name="raw", *args): """ Creates an image memory referencing pixel data in a byte buffer. @@ -2495,7 +2670,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): Note that this function decodes pixel data only, not entire images. If you have an entire image file in a string, wrap it in a - **BytesIO** object, and use :py:func:`~PIL.Image.open` to load it. + :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load it. In the current version, the default parameters used for the "raw" decoder differs from that used for :py:func:`~PIL.Image.frombytes`. This is a @@ -2530,7 +2705,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): args = mode, 0, 1 if args[0] in _MAPMODES: im = new(mode, (1, 1)) - im = im._new(core.map_buffer(data, size, decoder_name, None, 0, args)) + im = im._new(core.map_buffer(data, size, decoder_name, 0, args)) im.readonly = 1 return im @@ -2542,7 +2717,7 @@ def fromarray(obj, mode=None): Creates an image memory from an object exporting the array interface (using the buffer protocol). - If **obj** is not contiguous, then the tobytes method is called + If ``obj`` is not contiguous, then the ``tobytes`` method is called and :py:func:`~PIL.Image.frombuffer` is used. If you have an image in NumPy:: @@ -2570,9 +2745,12 @@ def fromarray(obj, mode=None): if mode is None: try: typekey = (1, 1) + shape[2:], arr["typestr"] + except KeyError as e: + raise TypeError("Cannot handle this data type") from e + try: mode, rawmode = _fromarray_typemap[typekey] - except KeyError: - raise TypeError("Cannot handle this data type") + except KeyError as e: + raise TypeError("Cannot handle this data type: %s, %s" % typekey) from e else: rawmode = mode if mode in ["1", "L", "I", "P", "F"]: @@ -2582,9 +2760,9 @@ def fromarray(obj, mode=None): else: ndmax = 4 if ndim > ndmax: - raise ValueError("Too many dimensions: %d > %d." % (ndim, ndmax)) + raise ValueError(f"Too many dimensions: {ndim} > {ndmax}.") - size = shape[1], shape[0] + size = 1 if ndim == 1 else shape[1], shape[0] if strides is not None: if hasattr(obj, "tobytes"): obj = obj.tobytes() @@ -2648,19 +2826,19 @@ def _decompression_bomb_check(size): if pixels > 2 * MAX_IMAGE_PIXELS: raise DecompressionBombError( - "Image size (%d pixels) exceeds limit of %d pixels, " - "could be decompression bomb DOS attack." % (pixels, 2 * MAX_IMAGE_PIXELS) + f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} " + "pixels, could be decompression bomb DOS attack." ) if pixels > MAX_IMAGE_PIXELS: warnings.warn( - "Image size (%d pixels) exceeds limit of %d pixels, " - "could be decompression bomb DOS attack." % (pixels, MAX_IMAGE_PIXELS), + f"Image size ({pixels} pixels) exceeds limit of {MAX_IMAGE_PIXELS} pixels, " + "could be decompression bomb DOS attack.", DecompressionBombWarning, ) -def open(fp, mode="r"): +def open(fp, mode="r", formats=None): """ Opens and identifies the given image file. @@ -2671,17 +2849,36 @@ def open(fp, mode="r"): :py:func:`~PIL.Image.new`. See :ref:`file-handling`. :param fp: A filename (string), pathlib.Path object or a file object. - The file object must implement :py:meth:`~file.read`, - :py:meth:`~file.seek`, and :py:meth:`~file.tell` methods, + The file object must implement ``file.read``, + ``file.seek``, and ``file.tell`` methods, and be opened in binary mode. :param mode: The mode. If given, this argument must be "r". + :param formats: A list or tuple of formats to attempt to load the file in. + This can be used to restrict the set of formats checked. + Pass ``None`` to try all supported formats. You can print the set of + available formats by running ``python -m PIL`` or using + the :py:func:`PIL.features.pilinfo` function. :returns: An :py:class:`~PIL.Image.Image` object. - :exception IOError: If the file cannot be found, or the image cannot be - opened and identified. + :exception FileNotFoundError: If the file cannot be found. + :exception PIL.UnidentifiedImageError: If the image cannot be opened and + identified. + :exception ValueError: If the ``mode`` is not "r", or if a ``StringIO`` + instance is used for ``fp``. + :exception TypeError: If ``formats`` is not ``None``, a list or a tuple. """ if mode != "r": - raise ValueError("bad mode %r" % mode) + raise ValueError(f"bad mode {repr(mode)}") + elif isinstance(fp, io.StringIO): + raise ValueError( + "StringIO cannot be used to open an image. " + "Binary data must be used instead." + ) + + if formats is None: + formats = ID + elif not isinstance(formats, (list, tuple)): + raise TypeError("formats must be a list or tuple") exclusive_fp = False filename = "" @@ -2706,8 +2903,8 @@ def open(fp, mode="r"): accept_warnings = [] - def _open_core(fp, filename, prefix): - for i in ID: + def _open_core(fp, filename, prefix, formats): + for i in formats: try: factory, accept = OPEN[i] result = not accept or accept(prefix) @@ -2729,11 +2926,11 @@ def open(fp, mode="r"): raise return None - im = _open_core(fp, filename, prefix) + im = _open_core(fp, filename, prefix, formats) if im is None: if init(): - im = _open_core(fp, filename, prefix) + im = _open_core(fp, filename, prefix, formats) if im: im._exclusive_fp = exclusive_fp @@ -2962,17 +3159,25 @@ def register_encoder(name, encoder): # -------------------------------------------------------------------- -# Simple display support. User code may override this. +# Simple display support. def _show(image, **options): - # override me, as necessary + options["_internal_pillow"] = True _showxv(image, **options) def _showxv(image, title=None, **options): from . import ImageShow + if "_internal_pillow" in options: + del options["_internal_pillow"] + else: + warnings.warn( + "_showxv is deprecated and will be removed in a future release. " + "Use Image.show instead.", + DeprecationWarning, + ) ImageShow.show(image, title, **options) @@ -3049,13 +3254,13 @@ def _apply_env_variables(env=None): try: var = int(var) * units except ValueError: - warnings.warn("{} is not int".format(var_name)) + warnings.warn(f"{var_name} is not int") continue try: setter(var) except ValueError as e: - warnings.warn("{}: {}".format(var_name, e)) + warnings.warn(f"{var_name}: {e}") _apply_env_variables() @@ -3073,14 +3278,14 @@ class Exif(MutableMapping): def _fixup(self, value): try: - if len(value) == 1 and not isinstance(value, dict): + if len(value) == 1 and isinstance(value, tuple): return value[0] except Exception: pass return value def _fixup_dict(self, src_dict): - # Helper function for _getexif() + # Helper function # returns a dict with any single item tuples/lists as individual values return {k: self._fixup(v) for k, v in src_dict.items()} @@ -3094,7 +3299,7 @@ class Exif(MutableMapping): else: from . import TiffImagePlugin - info = TiffImagePlugin.ImageFileDirectory_v1(self.head) + info = TiffImagePlugin.ImageFileDirectory_v2(self.head) info.load(self.fp) return self._fixup_dict(info) @@ -3114,12 +3319,14 @@ class Exif(MutableMapping): if not data: return - self.fp = io.BytesIO(data[6:]) + if data.startswith(b"Exif\x00\x00"): + data = data[6:] + self.fp = io.BytesIO(data) self.head = self.fp.read(8) # process dictionary from . import TiffImagePlugin - self._info = TiffImagePlugin.ImageFileDirectory_v1(self.head) + self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) self.endian = self._info._endian self.fp.seek(self._info.next) self._info.load(self.fp) @@ -3130,7 +3337,7 @@ class Exif(MutableMapping): self._data.update(ifd) self._ifds[0x8769] = ifd - def tobytes(self, offset=0): + def tobytes(self, offset=8): from . import TiffImagePlugin if self.endian == "<": @@ -3176,8 +3383,8 @@ class Exif(MutableMapping): if len(data) != size: warnings.warn( "Possibly corrupt EXIF MakerNote data. " - "Expecting to read %d bytes but only got %d." - " Skipping tag %s" % (size, len(data), ifd_tag) + f"Expecting to read {size} bytes but only got " + f"{len(data)}. Skipping tag {ifd_tag}" ) continue @@ -3259,7 +3466,8 @@ class Exif(MutableMapping): def __delitem__(self, tag): if self._info is not None and tag in self._info: del self._info[tag] - del self._data[tag] + else: + del self._data[tag] def __iter__(self): keys = set(self._data) diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index b1f71b5e7..61d3a295b 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -54,7 +54,7 @@ def invert(image): def lighter(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the lighter values. At least one of the images must have mode "1". + the lighter values. .. code-block:: python @@ -71,7 +71,7 @@ def lighter(image1, image2): def darker(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the darker values. At least one of the images must have mode "1". + the darker values. .. code-block:: python @@ -88,7 +88,7 @@ def darker(image1, image2): def difference(image1, image2): """ Returns the absolute value of the pixel-by-pixel difference between the two - images. At least one of the images must have mode "1". + images. .. code-block:: python @@ -107,8 +107,7 @@ def multiply(image1, image2): Superimposes two images on top of each other. If you multiply an image with a solid black image, the result is black. If - you multiply with a solid white image, the image is unaffected. At least - one of the images must have mode "1". + you multiply with a solid white image, the image is unaffected. .. code-block:: python @@ -124,8 +123,7 @@ def multiply(image1, image2): def screen(image1, image2): """ - Superimposes two inverted images on top of each other. At least one of the - images must have mode "1". + Superimposes two inverted images on top of each other. .. code-block:: python @@ -139,11 +137,46 @@ def screen(image1, image2): return image1._new(image1.im.chop_screen(image2.im)) +def soft_light(image1, image2): + """ + Superimposes two images on top of each other using the Soft Light algorithm + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_soft_light(image2.im)) + + +def hard_light(image1, image2): + """ + Superimposes two images on top of each other using the Hard Light algorithm + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_hard_light(image2.im)) + + +def overlay(image1, image2): + """ + Superimposes two images on top of each other using the Overlay algorithm + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_overlay(image2.im)) + + def add(image1, image2, scale=1.0, offset=0): """ Adds two images, dividing the result by scale and adding the offset. If omitted, scale defaults to 1.0, and offset to 0.0. - At least one of the images must have mode "1". .. code-block:: python @@ -160,8 +193,7 @@ def add(image1, image2, scale=1.0, offset=0): def subtract(image1, image2, scale=1.0, offset=0): """ Subtracts two images, dividing the result by scale and adding the offset. - If omitted, scale defaults to 1.0, and offset to 0.0. At least one of the - images must have mode "1". + If omitted, scale defaults to 1.0, and offset to 0.0. .. code-block:: python @@ -176,8 +208,7 @@ def subtract(image1, image2, scale=1.0, offset=0): def add_modulo(image1, image2): - """Add two images, without clipping the result. At least one of the images - must have mode "1". + """Add two images, without clipping the result. .. code-block:: python @@ -192,8 +223,7 @@ def add_modulo(image1, image2): def subtract_modulo(image1, image2): - """Subtract two images, without clipping the result. At least one of the - images must have mode "1". + """Subtract two images, without clipping the result. .. code-block:: python @@ -208,8 +238,12 @@ def subtract_modulo(image1, image2): def logical_and(image1, image2): - """Logical AND between two images. At least one of the images must have - mode "1". + """Logical AND between two images. + + Both of the images must have mode "1". If you would like to perform a + logical AND on an image with a mode other than "1", try + :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask + as the second image. .. code-block:: python @@ -224,8 +258,9 @@ def logical_and(image1, image2): def logical_or(image1, image2): - """Logical OR between two images. At least one of the images must have - mode "1". + """Logical OR between two images. + + Both of the images must have mode "1". .. code-block:: python @@ -240,8 +275,9 @@ def logical_or(image1, image2): def logical_xor(image1, image2): - """Logical XOR between two images. At least one of the images must have - mode "1". + """Logical XOR between two images. + + Both of the images must have mode "1". .. code-block:: python @@ -257,7 +293,7 @@ def logical_xor(image1, image2): def blend(image1, image2, alpha): """Blend images using constant transparency weight. Alias for - :py:meth:`PIL.Image.Image.blend`. + :py:func:`PIL.Image.blend`. :rtype: :py:class:`~PIL.Image.Image` """ @@ -267,7 +303,7 @@ def blend(image1, image2, alpha): def composite(image1, image2, mask): """Create composite using transparency mask. Alias for - :py:meth:`PIL.Image.Image.composite`. + :py:func:`PIL.Image.composite`. :rtype: :py:class:`~PIL.Image.Image` """ @@ -277,8 +313,8 @@ def composite(image1, image2, mask): def offset(image, xoffset, yoffset=None): """Returns a copy of the image where data has been offset by the given - distances. Data wraps around the edges. If **yoffset** is omitted, it - is assumed to be equal to **xoffset**. + distances. Data wraps around the edges. If ``yoffset`` is omitted, it + is assumed to be equal to ``xoffset``. :param xoffset: The horizontal distance. :param yoffset: The vertical distance. If omitted, both diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index d05b3102c..8c4740ddc 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -159,6 +159,14 @@ class ImageCmsProfile: """ if isinstance(profile, str): + if sys.platform == "win32": + profile_bytes_path = profile.encode() + try: + profile_bytes_path.decode("ascii") + except UnicodeDecodeError: + with open(profile, "rb") as f: + self._set(core.profile_frombytes(f.read())) + return self._set(core.profile_open(profile), profile) elif hasattr(profile, "read"): self._set(core.profile_frombytes(profile.read())) @@ -192,9 +200,9 @@ class ImageCmsTransform(Image.ImagePointHandler): """ Transform. This can be used with the procedural API, or with the standard - Image.point() method. + :py:func:`~PIL.Image.Image.point` method. - Will return the output profile in the output.info['icc_profile']. + Will return the output profile in the ``output.info['icc_profile']``. """ def __init__( @@ -250,24 +258,23 @@ class ImageCmsTransform(Image.ImagePointHandler): def get_display_profile(handle=None): - """ (experimental) Fetches the profile for the current display device. - :returns: None if the profile is not known. + """ + (experimental) Fetches the profile for the current display device. + + :returns: ``None`` if the profile is not known. """ - if sys.platform == "win32": - from PIL import ImageWin + if sys.platform != "win32": + return None - if isinstance(handle, ImageWin.HDC): - profile = core.get_display_profile_win32(handle, 1) - else: - profile = core.get_display_profile_win32(handle or 0) + from PIL import ImageWin + + if isinstance(handle, ImageWin.HDC): + profile = core.get_display_profile_win32(handle, 1) else: - try: - get = _imagingcms.get_display_profile - except AttributeError: - return None - else: - profile = get() + profile = core.get_display_profile_win32(handle or 0) + if profile is None: + return None return ImageCmsProfile(profile) @@ -278,8 +285,8 @@ def get_display_profile(handle=None): class PyCMSError(Exception): - """ (pyCMS) Exception class. - This is used for all errors in the pyCMS API. """ + """(pyCMS) Exception class. + This is used for all errors in the pyCMS API.""" pass @@ -295,27 +302,28 @@ def profileToProfile( ): """ (pyCMS) Applies an ICC transformation to a given image, mapping from - inputProfile to outputProfile. + ``inputProfile`` to ``outputProfile``. If the input or output profiles specified are not valid filenames, a - PyCMSError will be raised. If inPlace is True and outputMode != im.mode, - a PyCMSError will be raised. If an error occurs during application of - the profiles, a PyCMSError will be raised. If outputMode is not a mode - supported by the outputProfile (or by pyCMS), a PyCMSError will be - raised. + :exc:`PyCMSError` will be raised. If ``inPlace`` is ``True`` and + ``outputMode != im.mode``, a :exc:`PyCMSError` will be raised. + If an error occurs during application of the profiles, + a :exc:`PyCMSError` will be raised. + If ``outputMode`` is not a mode supported by the ``outputProfile`` (or by pyCMS), + a :exc:`PyCMSError` will be raised. - This function applies an ICC transformation to im from inputProfile's - color space to outputProfile's color space using the specified rendering + This function applies an ICC transformation to im from ``inputProfile``'s + color space to ``outputProfile``'s color space using the specified rendering intent to decide how to handle out-of-gamut colors. - OutputMode can be used to specify that a color mode conversion is to + ``outputMode`` can be used to specify that a color mode conversion is to be done using these profiles, but the specified profiles must be able to handle that mode. I.e., if converting im from RGB to CMYK using profiles, the input profile must handle RGB data, and the output profile must handle CMYK data. - :param im: An open PIL image object (i.e. Image.new(...) or - Image.open(...), etc.) + :param im: An open :py:class:`~PIL.Image.Image` object (i.e. Image.new(...) + or Image.open(...), etc.) :param inputProfile: String, as a valid filename path to the ICC input profile you wish to use for this image, or a profile object :param outputProfile: String, as a valid filename path to the ICC output @@ -335,12 +343,12 @@ def profileToProfile( MUST be the same mode as the input, or omitted completely. If omitted, the outputMode will be the same as the mode of the input image (im.mode) - :param inPlace: Boolean. If True, the original image is modified in-place, - and None is returned. If False (default), a new Image object is - returned with the transform applied. + :param inPlace: Boolean. If ``True``, the original image is modified in-place, + and ``None`` is returned. If ``False`` (default), a new + :py:class:`~PIL.Image.Image` object is returned with the transform applied. :param flags: Integer (0-...) specifying additional flags - :returns: Either None or a new PIL image object, depending on value of - inPlace + :returns: Either None or a new :py:class:`~PIL.Image.Image` object, depending on + the value of ``inPlace`` :exception PyCMSError: """ @@ -372,7 +380,7 @@ def profileToProfile( else: imOut = transform.apply(im) except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v return imOut @@ -384,8 +392,8 @@ def getOpenProfile(profileFilename): The PyCMSProfile object can be passed back into pyCMS for use in creating transforms and such (as in ImageCms.buildTransformFromOpenProfiles()). - If profileFilename is not a valid filename for an ICC profile, a PyCMSError - will be raised. + If ``profileFilename`` is not a valid filename for an ICC profile, + a :exc:`PyCMSError` will be raised. :param profileFilename: String, as a valid filename path to the ICC profile you wish to open, or a file-like object. @@ -396,7 +404,7 @@ def getOpenProfile(profileFilename): try: return ImageCmsProfile(profileFilename) except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def buildTransform( @@ -408,21 +416,21 @@ def buildTransform( flags=0, ): """ - (pyCMS) Builds an ICC transform mapping from the inputProfile to the - outputProfile. Use applyTransform to apply the transform to a given + (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the + ``outputProfile``. Use applyTransform to apply the transform to a given image. If the input or output profiles specified are not valid filenames, a - PyCMSError will be raised. If an error occurs during creation of the - transform, a PyCMSError will be raised. + :exc:`PyCMSError` will be raised. If an error occurs during creation + of the transform, a :exc:`PyCMSError` will be raised. - If inMode or outMode are not a mode supported by the outputProfile (or - by pyCMS), a PyCMSError will be raised. + If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` + (or by pyCMS), a :exc:`PyCMSError` will be raised. - This function builds and returns an ICC transform from the inputProfile - to the outputProfile using the renderingIntent to determine what to do + This function builds and returns an ICC transform from the ``inputProfile`` + to the ``outputProfile`` using the ``renderingIntent`` to determine what to do with out-of-gamut colors. It will ONLY work for converting images that - are in inMode to images that are in outMode color format (PIL mode, + are in ``inMode`` to images that are in ``outMode`` color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.). Building the transform is a fair part of the overhead in @@ -435,7 +443,7 @@ def buildTransform( The reason pyCMS returns a class object rather than a handle directly to the transform is that it needs to keep track of the PIL input/output modes that the transform is meant for. These attributes are stored in - the "inMode" and "outMode" attributes of the object (which can be + the ``inMode`` and ``outMode`` attributes of the object (which can be manually overridden if you really want to, but I don't know of any time that would be of use, or would even work). @@ -477,7 +485,7 @@ def buildTransform( inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags ) except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def buildProofTransform( @@ -491,25 +499,25 @@ def buildProofTransform( flags=FLAGS["SOFTPROOFING"], ): """ - (pyCMS) Builds an ICC transform mapping from the inputProfile to the - outputProfile, but tries to simulate the result that would be - obtained on the proofProfile device. + (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the + ``outputProfile``, but tries to simulate the result that would be + obtained on the ``proofProfile`` device. If the input, output, or proof profiles specified are not valid - filenames, a PyCMSError will be raised. + filenames, a :exc:`PyCMSError` will be raised. - If an error occurs during creation of the transform, a PyCMSError will - be raised. + If an error occurs during creation of the transform, + a :exc:`PyCMSError` will be raised. - If inMode or outMode are not a mode supported by the outputProfile - (or by pyCMS), a PyCMSError will be raised. + If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` + (or by pyCMS), a :exc:`PyCMSError` will be raised. - This function builds and returns an ICC transform from the inputProfile - to the outputProfile, but tries to simulate the result that would be - obtained on the proofProfile device using renderingIntent and - proofRenderingIntent to determine what to do with out-of-gamut + This function builds and returns an ICC transform from the ``inputProfile`` + to the ``outputProfile``, but tries to simulate the result that would be + obtained on the ``proofProfile`` device using ``renderingIntent`` and + ``proofRenderingIntent`` to determine what to do with out-of-gamut colors. This is known as "soft-proofing". It will ONLY work for - converting images that are in inMode to images that are in outMode + converting images that are in ``inMode`` to images that are in outMode color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.). Usage of the resulting transform object is exactly the same as with @@ -517,7 +525,7 @@ def buildProofTransform( Proof profiling is generally used when using an output device to get a good idea of what the final printed/displayed image would look like on - the proofProfile device when it's quicker and easier to use the + the ``proofProfile`` device when it's quicker and easier to use the output device for judging color. Generally, this means that the output device is a monitor, or a dye-sub printer (etc.), and the simulated device is something more expensive, complicated, or time consuming @@ -588,7 +596,7 @@ def buildProofTransform( flags, ) except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v buildTransformFromOpenProfiles = buildTransform @@ -599,39 +607,40 @@ def applyTransform(im, transform, inPlace=False): """ (pyCMS) Applies a transform to a given image. - If im.mode != transform.inMode, a PyCMSError is raised. + If ``im.mode != transform.inMode``, a :exc:`PyCMSError` is raised. - If inPlace is True and transform.inMode != transform.outMode, a - PyCMSError is raised. + If ``inPlace`` is ``True`` and ``transform.inMode != transform.outMode``, a + :exc:`PyCMSError` is raised. - If im.mode, transform.inMode, or transform.outMode is not supported by - pyCMSdll or the profiles you used for the transform, a PyCMSError is - raised. + If ``im.mode``, ``transform.inMode`` or ``transform.outMode`` is not + supported by pyCMSdll or the profiles you used for the transform, a + :exc:`PyCMSError` is raised. - If an error occurs while the transform is being applied, a PyCMSError - is raised. + If an error occurs while the transform is being applied, + a :exc:`PyCMSError` is raised. This function applies a pre-calculated transform (from ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles()) - to an image. The transform can be used for multiple images, saving + to an image. The transform can be used for multiple images, saving considerable calculation time if doing the same conversion multiple times. If you want to modify im in-place instead of receiving a new image as - the return value, set inPlace to True. This can only be done if - transform.inMode and transform.outMode are the same, because we can't + the return value, set ``inPlace`` to ``True``. This can only be done if + ``transform.inMode`` and ``transform.outMode`` are the same, because we can't change the mode in-place (the buffer sizes for some modes are - different). The default behavior is to return a new Image object of - the same dimensions in mode transform.outMode. + different). The default behavior is to return a new :py:class:`~PIL.Image.Image` + object of the same dimensions in mode ``transform.outMode``. - :param im: A PIL Image object, and im.mode must be the same as the inMode - supported by the transform. + :param im: An :py:class:`~PIL.Image.Image` object, and im.mode must be the same + as the ``inMode`` supported by the transform. :param transform: A valid CmsTransform class object - :param inPlace: Bool. If True, im is modified in place and None is - returned, if False, a new Image object with the transform applied is - returned (and im is not changed). The default is False. - :returns: Either None, or a new PIL Image object, depending on the value of - inPlace. The profile will be returned in the image's - info['icc_profile']. + :param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is + returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the + transform applied is returned (and ``im`` is not changed). The default is + ``False``. + :returns: Either ``None``, or a new :py:class:`~PIL.Image.Image` object, + depending on the value of ``inPlace``. The profile will be returned in + the image's ``info['icc_profile']``. :exception PyCMSError: """ @@ -642,7 +651,7 @@ def applyTransform(im, transform, inPlace=False): else: imOut = transform.apply(im) except (TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v return imOut @@ -651,11 +660,14 @@ def createProfile(colorSpace, colorTemp=-1): """ (pyCMS) Creates a profile. - If colorSpace not in ["LAB", "XYZ", "sRGB"], a PyCMSError is raised + If colorSpace not in ``["LAB", "XYZ", "sRGB"]``, + a :exc:`PyCMSError` is raised. - If using LAB and colorTemp != a positive integer, a PyCMSError is raised. + If using LAB and ``colorTemp`` is not a positive integer, + a :exc:`PyCMSError` is raised. - If an error occurs while creating the profile, a PyCMSError is raised. + If an error occurs while creating the profile, + a :exc:`PyCMSError` is raised. Use this function to create common profiles on-the-fly instead of having to supply a profile on disk and knowing the path to it. It @@ -676,22 +688,21 @@ def createProfile(colorSpace, colorTemp=-1): if colorSpace not in ["LAB", "XYZ", "sRGB"]: raise PyCMSError( - "Color space not supported for on-the-fly profile creation (%s)" - % colorSpace + f"Color space not supported for on-the-fly profile creation ({colorSpace})" ) if colorSpace == "LAB": try: colorTemp = float(colorTemp) - except (TypeError, ValueError): + except (TypeError, ValueError) as e: raise PyCMSError( - 'Color temperature must be numeric, "%s" not valid' % colorTemp - ) + f'Color temperature must be numeric, "{colorTemp}" not valid' + ) from e try: return core.createProfile(colorSpace, colorTemp) except (TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileName(profile): @@ -699,9 +710,9 @@ def getProfileName(profile): (pyCMS) Gets the internal product name for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised If an error occurs while trying to obtain the - name tag, a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, + a :exc:`PyCMSError` is raised If an error occurs while trying + to obtain the name tag, a :exc:`PyCMSError` is raised. Use this function to obtain the INTERNAL name of the profile (stored in an ICC tag in the profile itself), usually the one used when the @@ -730,21 +741,21 @@ def getProfileName(profile): return (profile.profile.profile_description or "") + "\n" if not manufacturer or len(model) > 30: return model + "\n" - return "{} - {}\n".format(model, manufacturer) + return f"{model} - {manufacturer}\n" except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileInfo(profile): """ (pyCMS) Gets the internal product information for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, + a :exc:`PyCMSError` is raised. - If an error occurs while trying to obtain the info tag, a PyCMSError - is raised + If an error occurs while trying to obtain the info tag, + a :exc:`PyCMSError` is raised. Use this function to obtain the information stored in the profile's info tag. This often contains details about the profile, and how it @@ -773,18 +784,18 @@ def getProfileInfo(profile): return "\r\n\r\n".join(arr) + "\r\n\r\n" except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileCopyright(profile): """ (pyCMS) Gets the copyright for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. - If an error occurs while trying to obtain the copyright tag, a PyCMSError - is raised + If an error occurs while trying to obtain the copyright tag, + a :exc:`PyCMSError` is raised. Use this function to obtain the information stored in the profile's copyright tag. @@ -801,18 +812,18 @@ def getProfileCopyright(profile): profile = ImageCmsProfile(profile) return (profile.profile.copyright or "") + "\n" except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileManufacturer(profile): """ (pyCMS) Gets the manufacturer for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. If an error occurs while trying to obtain the manufacturer tag, a - PyCMSError is raised + :exc:`PyCMSError` is raised. Use this function to obtain the information stored in the profile's manufacturer tag. @@ -829,18 +840,18 @@ def getProfileManufacturer(profile): profile = ImageCmsProfile(profile) return (profile.profile.manufacturer or "") + "\n" except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileModel(profile): """ (pyCMS) Gets the model for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. - If an error occurs while trying to obtain the model tag, a PyCMSError - is raised + If an error occurs while trying to obtain the model tag, + a :exc:`PyCMSError` is raised. Use this function to obtain the information stored in the profile's model tag. @@ -858,18 +869,18 @@ def getProfileModel(profile): profile = ImageCmsProfile(profile) return (profile.profile.model or "") + "\n" except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileDescription(profile): """ (pyCMS) Gets the description for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. - If an error occurs while trying to obtain the description tag, a PyCMSError - is raised + If an error occurs while trying to obtain the description tag, + a :exc:`PyCMSError` is raised. Use this function to obtain the information stored in the profile's description tag. @@ -887,18 +898,18 @@ def getProfileDescription(profile): profile = ImageCmsProfile(profile) return (profile.profile.profile_description or "") + "\n" except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getDefaultIntent(profile): """ (pyCMS) Gets the default intent name for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. If an error occurs while trying to obtain the default intent, a - PyCMSError is raised. + :exc:`PyCMSError` is raised. Use this function to determine the default (and usually best optimized) rendering intent for this profile. Most profiles support multiple @@ -926,7 +937,7 @@ def getDefaultIntent(profile): profile = ImageCmsProfile(profile) return profile.profile.rendering_intent except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def isIntentSupported(profile, intent, direction): @@ -934,15 +945,15 @@ def isIntentSupported(profile, intent, direction): (pyCMS) Checks if a given intent is supported. Use this function to verify that you can use your desired - renderingIntent with profile, and that profile can be used for the + ``intent`` with ``profile``, and that ``profile`` can be used for the input/output/proof profile as you desire. Some profiles are created specifically for one "direction", can cannot - be used for others. Some profiles can only be used for certain - rendering intents... so it's best to either verify this before trying + be used for others. Some profiles can only be used for certain + rendering intents, so it's best to either verify this before trying to create a transform with them (using this function), or catch the - potential PyCMSError that will occur if they don't support the modes - you select. + potential :exc:`PyCMSError` that will occur if they don't + support the modes you select. :param profile: EITHER a valid CmsProfile object, OR a string of the filename of an ICC profile. @@ -977,7 +988,7 @@ def isIntentSupported(profile, intent, direction): else: return -1 except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def versions(): diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index 692d7d2c3..909117449 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -113,7 +113,7 @@ def getrgb(color): m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) if m: return (int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))) - raise ValueError("unknown color specifier: %r" % color) + raise ValueError(f"unknown color specifier: {repr(color)}") def getcolor(color, mode): @@ -134,7 +134,9 @@ def getcolor(color, mode): if Image.getmodebase(mode) == "L": r, g, b = color - color = (r * 299 + g * 587 + b * 114) // 1000 + # ITU-R Recommendation 601-2 for nonlinear RGB + # scaled to 24 bits to match the convert's implementation. + color = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 if mode[-1] == "A": return (color, alpha) else: diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index c6e12150e..b823be9a2 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -35,7 +35,6 @@ import numbers from . import Image, ImageColor - """ A simple 2D drawing interface for PIL images.

@@ -119,7 +118,7 @@ class ImageDraw: fill = self.draw.draw_ink(fill) return ink, fill - def arc(self, xy, start, end, fill=None, width=0): + def arc(self, xy, start, end, fill=None, width=1): """Draw an arc.""" ink, fill = self._getink(fill) if ink is not None: @@ -134,20 +133,20 @@ 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=0): + def chord(self, xy, start, end, fill=None, outline=None, width=1): """Draw a chord.""" ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_chord(xy, start, end, fill, 1) - if ink is not None and ink != fill: + 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=0): + def ellipse(self, xy, fill=None, outline=None, width=1): """Draw an ellipse.""" ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_ellipse(xy, fill, 1) - if ink is not None and ink != fill: + 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): @@ -156,6 +155,8 @@ class ImageDraw: 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] angles = [ @@ -219,12 +220,12 @@ 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=0): + def pieslice(self, xy, start, end, fill=None, outline=None, width=1): """Draw a pieslice.""" ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_pieslice(xy, start, end, fill, 1) - if ink is not None and ink != fill: + 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): @@ -241,12 +242,19 @@ class ImageDraw: if ink is not None and ink != fill: self.draw.draw_polygon(xy, ink, 0) - def rectangle(self, xy, fill=None, outline=None, width=0): + def regular_polygon( + self, bounding_circle, n_sides, rotation=0, fill=None, outline=None + ): + """Draw a regular polygon.""" + xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) + self.polygon(xy, fill, outline) + + def rectangle(self, xy, fill=None, outline=None, width=1): """Draw a rectangle.""" ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_rectangle(xy, fill, 1) - if ink is not None and ink != fill: + if ink is not None and ink != fill and width != 0: self.draw.draw_rectangle(xy, ink, 0, width) def _multiline_check(self, text): @@ -274,8 +282,9 @@ class ImageDraw: language=None, stroke_width=0, stroke_fill=None, + embedded_color=False, *args, - **kwargs + **kwargs, ): if self._multiline_check(text): return self.multiline_text( @@ -291,8 +300,12 @@ class ImageDraw: language, stroke_width, stroke_fill, + embedded_color, ) + if embedded_color and self.mode not in ("RGB", "RGBA"): + raise ValueError("Embedded color supported only in RGB and RGBA modes") + if font is None: font = self.getfont() @@ -303,15 +316,20 @@ class ImageDraw: return ink def draw_text(ink, stroke_width=0, stroke_offset=None): + mode = self.fontmode + if stroke_width == 0 and embedded_color: + mode = "RGBA" coord = xy try: mask, offset = font.getmask2( text, - self.fontmode, + mode, direction=direction, features=features, language=language, stroke_width=stroke_width, + anchor=anchor, + ink=ink, *args, **kwargs, ) @@ -320,11 +338,13 @@ class ImageDraw: try: mask = font.getmask( text, - self.fontmode, + mode, direction, features, language, stroke_width, + anchor, + ink, *args, **kwargs, ) @@ -332,7 +352,15 @@ class ImageDraw: mask = font.getmask(text) if stroke_offset: coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] - self.draw.draw_bitmap(coord, mask, ink) + 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) + coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1] + self.im.paste(color, coord + coord2, mask) + else: + self.draw.draw_bitmap(coord, mask, ink) ink = getink(fill) if ink is not None: @@ -345,7 +373,7 @@ class ImageDraw: draw_text(stroke_ink, stroke_width) # Draw normal text - draw_text(ink, 0, (stroke_width, stroke_width)) + draw_text(ink, 0) else: # Only draw normal text draw_text(ink) @@ -364,7 +392,18 @@ class ImageDraw: language=None, stroke_width=0, stroke_fill=None, + embedded_color=False, ): + if direction == "ttb": + raise ValueError("ttb direction is unsupported for multiline text") + + if anchor is None: + anchor = "la" + elif len(anchor) != 2: + raise ValueError("anchor must be a 2 character string") + elif anchor[1] in "tb": + raise ValueError("anchor not supported for multiline text") + widths = [] max_width = 0 lines = self._multiline_split(text) @@ -372,26 +411,38 @@ class ImageDraw: self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing ) for line in lines: - line_width, line_height = self.textsize( - line, - font, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, + line_width = self.textlength( + line, font, direction=direction, features=features, language=language ) widths.append(line_width) max_width = max(max_width, line_width) - left, top = xy + + top = xy[1] + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + for idx, line in enumerate(lines): + left = xy[0] + width_difference = max_width - widths[idx] + + # first align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + + # then align by align parameter if align == "left": - pass # left = x + pass elif align == "center": - left += (max_width - widths[idx]) / 2.0 + left += width_difference / 2.0 elif align == "right": - left += max_width - widths[idx] + left += width_difference else: raise ValueError('align must be "left", "center" or "right"') + self.text( (left, top), line, @@ -403,9 +454,9 @@ class ImageDraw: language=language, stroke_width=stroke_width, stroke_fill=stroke_fill, + embedded_color=embedded_color, ) top += line_spacing - left = xy[0] def textsize( self, @@ -449,6 +500,172 @@ class ImageDraw: max_width = max(max_width, line_width) return max_width, len(lines) * line_spacing - spacing + def textlength( + self, + text, + font=None, + direction=None, + features=None, + language=None, + embedded_color=False, + ): + """Get the length of a given string, in pixels with 1/64 precision.""" + if self._multiline_check(text): + raise ValueError("can't measure length of multiline text") + if embedded_color and self.mode not in ("RGB", "RGBA"): + raise ValueError("Embedded color supported only in RGB and RGBA modes") + + if font is None: + font = self.getfont() + mode = "RGBA" if embedded_color else self.fontmode + try: + return font.getlength(text, mode, direction, features, language) + except AttributeError: + size = self.textsize( + text, font, direction=direction, features=features, language=language + ) + if direction == "ttb": + return size[1] + return size[0] + + def textbbox( + self, + xy, + text, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + embedded_color=False, + ): + """Get the bounding box of a given string, in pixels.""" + if embedded_color and self.mode not in ("RGB", "RGBA"): + raise ValueError("Embedded color supported only in RGB and RGBA modes") + + if self._multiline_check(text): + return self.multiline_textbbox( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + ) + + if font is None: + font = self.getfont() + mode = "RGBA" if embedded_color else self.fontmode + bbox = font.getbbox( + text, mode, direction, features, language, stroke_width, anchor + ) + return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1] + + def multiline_textbbox( + self, + xy, + text, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + embedded_color=False, + ): + if direction == "ttb": + raise ValueError("ttb direction is unsupported for multiline text") + + if anchor is None: + anchor = "la" + elif len(anchor) != 2: + raise ValueError("anchor must be a 2 character string") + elif anchor[1] in "tb": + raise ValueError("anchor not supported for multiline text") + + widths = [] + max_width = 0 + lines = self._multiline_split(text) + line_spacing = ( + self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing + ) + for line in lines: + line_width = self.textlength( + line, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + widths.append(line_width) + max_width = max(max_width, line_width) + + top = xy[1] + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + + bbox = None + + for idx, line in enumerate(lines): + left = xy[0] + width_difference = max_width - widths[idx] + + # first align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + + # then align by align parameter + if align == "left": + pass + elif align == "center": + left += width_difference / 2.0 + elif align == "right": + left += width_difference + else: + raise ValueError('align must be "left", "center" or "right"') + + bbox_line = self.textbbox( + (left, top), + line, + font, + anchor, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + embedded_color=embedded_color, + ) + if bbox is None: + bbox = bbox_line + else: + bbox = ( + min(bbox[0], bbox_line[0]), + min(bbox[1], bbox_line[1]), + max(bbox[2], bbox_line[2]), + max(bbox[3], bbox_line[3]), + ) + + top += line_spacing + + if bbox is None: + return xy[0], xy[1], xy[0], xy[1] + return bbox + def Draw(im, mode=None): """ @@ -554,6 +771,123 @@ def floodfill(image, xy, value, border=None, thresh=0): edge = new_edge +def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): + """ + Generate a list of vertices for a 2D regular polygon. + + :param bounding_circle: The bounding circle is a tuple defined + by a point and radius. The polygon is inscribed in this circle. + (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) + :param n_sides: Number of sides + (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon) + :param rotation: Apply an arbitrary rotation to the polygon + (e.g. ``rotation=90``, applies a 90 degree rotation) + :return: List of regular polygon vertices + (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``) + + How are the vertices computed? + 1. Compute the following variables + - theta: Angle between the apothem & the nearest polygon vertex + - side_length: Length of each polygon edge + - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle) + - polygon_radius: Polygon radius (last element of bounding_circle) + - angles: Location of each polygon vertex in polar grid + (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0]) + + 2. For each angle in angles, get the polygon vertex at that angle + The vertex is computed using the equation below. + X= xcos(φ) + ysin(φ) + Y= −xsin(φ) + ycos(φ) + + Note: + φ = angle in degrees + x = 0 + y = polygon_radius + + The formula above assumes rotation around the origin. + In our case, we are rotating around the centroid. + To account for this, we use the formula below + X = xcos(φ) + ysin(φ) + centroid_x + Y = −xsin(φ) + ycos(φ) + centroid_y + """ + # 1. Error Handling + # 1.1 Check `n_sides` has an appropriate value + if not isinstance(n_sides, int): + raise TypeError("n_sides should be an int") + if n_sides < 3: + raise ValueError("n_sides should be an int > 2") + + # 1.2 Check `bounding_circle` has an appropriate value + if not isinstance(bounding_circle, (list, tuple)): + raise TypeError("bounding_circle should be a tuple") + + if len(bounding_circle) == 3: + *centroid, polygon_radius = bounding_circle + elif len(bounding_circle) == 2: + centroid, polygon_radius = bounding_circle + else: + raise ValueError( + "bounding_circle should contain 2D coordinates " + "and a radius (e.g. (x, y, r) or ((x, y), r) )" + ) + + if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)): + raise ValueError("bounding_circle should only contain numeric data") + + if not len(centroid) == 2: + raise ValueError( + "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" + ) + + if polygon_radius <= 0: + raise ValueError("bounding_circle radius should be > 0") + + # 1.3 Check `rotation` has an appropriate value + if not isinstance(rotation, (int, float)): + raise ValueError("rotation should be an int or float") + + # 2. Define Helper Functions + def _apply_rotation(point, degrees, centroid): + return ( + round( + point[0] * math.cos(math.radians(360 - degrees)) + - point[1] * math.sin(math.radians(360 - degrees)) + + centroid[0], + 2, + ), + round( + point[1] * math.cos(math.radians(360 - degrees)) + + point[0] * math.sin(math.radians(360 - degrees)) + + centroid[1], + 2, + ), + ) + + def _compute_polygon_vertex(centroid, polygon_radius, angle): + start_point = [polygon_radius, 0] + return _apply_rotation(start_point, angle, centroid) + + def _get_angles(n_sides, rotation): + angles = [] + degrees = 360 / n_sides + # Start with the bottom left polygon vertex + current_angle = (270 - 0.5 * degrees) + rotation + for _ in range(0, n_sides): + angles.append(current_angle) + current_angle += degrees + if current_angle > 360: + current_angle -= 360 + return angles + + # 3. Variable Declarations + angles = _get_angles(n_sides, rotation) + + # 4. Compute Vertices + return [ + _compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles + ] + + def _color_diff(color1, color2): """ Uses 1-norm distance to calculate difference between two values. diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 20b5fe4c4..1f63110fd 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -16,21 +16,35 @@ # See the README file for information on usage and redistribution. # + +""" +(Experimental) WCK-style drawing interface operations + +.. seealso:: :py:mod:`PIL.ImageDraw` +""" + + from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath class Pen: + """Stores an outline color and width.""" + def __init__(self, color, width=1, opacity=255): self.color = ImageColor.getrgb(color) self.width = width class Brush: + """Stores a fill color""" + def __init__(self, color, opacity=255): self.color = ImageColor.getrgb(color) class Font: + """Stores a TrueType font and color""" + def __init__(self, color, file, size=12): # FIXME: add support for bitmap fonts self.color = ImageColor.getrgb(color) @@ -38,6 +52,10 @@ class Font: class Draw: + """ + (Experimental) WCK-style drawing interface + """ + def __init__(self, image, size=None, color=None): if not hasattr(image, "im"): image = Image.new(image, size, color) @@ -73,35 +91,89 @@ class Draw: getattr(self.draw, op)(xy, fill=fill, outline=outline) def settransform(self, offset): + """Sets a transformation offset.""" (xoffset, yoffset) = offset self.transform = (1, 0, xoffset, 0, 1, yoffset) def arc(self, xy, start, end, *options): + """ + Draws an arc (a portion of a circle outline) between the start and end + angles, inside the given bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` + """ self.render("arc", xy, start, end, *options) def chord(self, xy, start, end, *options): + """ + Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points + with a straight line. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` + """ self.render("chord", xy, start, end, *options) def ellipse(self, xy, *options): + """ + Draws an ellipse inside the given bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` + """ self.render("ellipse", xy, *options) def line(self, xy, *options): + """ + Draws a line between the coordinates in the ``xy`` list. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` + """ self.render("line", xy, *options) def pieslice(self, xy, start, end, *options): + """ + Same as arc, but also draws straight lines between the end points and the + center of the bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` + """ self.render("pieslice", xy, start, end, *options) def polygon(self, xy, *options): + """ + Draws a polygon. + + The polygon outline consists of straight lines between the given + coordinates, plus a straight line between the last and the first + coordinate. + + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` + """ self.render("polygon", xy, *options) def rectangle(self, xy, *options): + """ + Draws a rectangle. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` + """ self.render("rectangle", xy, *options) def text(self, xy, text, font): + """ + Draws the string at the given position. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text` + """ if self.transform: xy = ImagePath.Path(xy) xy.transform(self.transform) self.draw.text(xy, text, font=font.font, fill=font.color) def textsize(self, text, font): + """ + Return the size of the given string, in pixels. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textsize` + """ return self.draw.textsize(text, font=font.font) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index a343bd43c..49633ef51 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -30,6 +30,7 @@ import io import struct import sys +import warnings from . import Image from ._util import isPath @@ -39,6 +40,7 @@ MAXBLOCK = 65536 SAFEBLOCK = 1024 * 1024 LOAD_TRUNCATED_IMAGES = False +"""Whether or not to load truncated image files. User code may change this.""" ERRORS = { -1: "image buffer overrun error", @@ -47,16 +49,7 @@ ERRORS = { -8: "bad configuration", -9: "out of memory error", } - - -def raise_ioerror(error): - try: - message = Image.core.getcodecstatus(error) - except AttributeError: - message = ERRORS.get(error) - if not message: - message = "decoder error %d" % error - raise OSError(message + " when reading image file") +"""Dict of known error codes returned from :meth:`.PyDecoder.decode`.""" # @@ -64,6 +57,25 @@ def raise_ioerror(error): # Helpers +def raise_oserror(error): + try: + message = Image.core.getcodecstatus(error) + except AttributeError: + message = ERRORS.get(error) + if not message: + message = f"decoder error {error}" + raise OSError(message + " when reading image file") + + +def raise_ioerror(error): + warnings.warn( + "raise_ioerror is deprecated and will be removed in a future release. " + "Use raise_oserror instead.", + DeprecationWarning, + ) + return raise_oserror(error) + + def _tilesort(t): # sort on offset return t[2] @@ -75,7 +87,7 @@ def _tilesort(t): class ImageFile(Image.Image): - "Base class for image file format handlers." + """Base class for image file format handlers.""" def __init__(self, fp=None, filename=None): super().__init__() @@ -85,6 +97,8 @@ class ImageFile(Image.Image): self.custom_mimetype = None self.tile = None + """ A list of tile descriptors, or ``None`` """ + self.readonly = 1 # until we know better self.decoderconfig = () @@ -112,7 +126,7 @@ class ImageFile(Image.Image): EOFError, # got header but not the first frame struct.error, ) as v: - raise SyntaxError(v) + raise SyntaxError(v) from v if not self.mode or self.size[0] <= 0: raise SyntaxError("not identified by this driver") @@ -122,11 +136,6 @@ class ImageFile(Image.Image): self.fp.close() raise - def draft(self, mode, size): - """Set draft mode""" - - pass - def get_format_mimetype(self): if self.custom_mimetype: return self.custom_mimetype @@ -145,10 +154,10 @@ class ImageFile(Image.Image): def load(self): """Load image data based on tile list""" - pixel = Image.Image.load(self) - if self.tile is None: raise OSError("cannot load this image") + + pixel = Image.Image.load(self) if not self.tile: return pixel @@ -194,12 +203,12 @@ class ImageFile(Image.Image): # use mmap, if possible import mmap - with open(self.filename, "r") as fp: + with open(self.filename) as fp: self.map = mmap.mmap( fp.fileno(), 0, access=mmap.ACCESS_READ ) self.im = Image.core.map_buffer( - self.map, self.size, decoder_name, extents, offset, args + self.map, self.size, decoder_name, offset, args ) readonly = 1 # After trashing self.im, @@ -236,12 +245,12 @@ class ImageFile(Image.Image): while True: try: s = read(self.decodermaxblock) - except (IndexError, struct.error): + except (IndexError, struct.error) as e: # truncated png/gif if LOAD_TRUNCATED_IMAGES: break else: - raise OSError("image file is truncated") + raise OSError("image file is truncated") from e if not s: # truncated jpeg if LOAD_TRUNCATED_IMAGES: @@ -249,7 +258,7 @@ class ImageFile(Image.Image): else: raise OSError( "image file is truncated " - "(%d bytes not processed)" % len(b) + f"({len(b)} bytes not processed)" ) b = b + s @@ -272,7 +281,7 @@ class ImageFile(Image.Image): if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0: # still raised if decoder fails to return anything - raise_ioerror(err_code) + raise_oserror(err_code) return Image.Image.load(self) @@ -325,7 +334,7 @@ class StubImageFile(ImageFile): def load(self): loader = self._load() if loader is None: - raise OSError("cannot find loader for this %s file" % self.format) + raise OSError(f"cannot find loader for this {self.format} file") image = loader.load(self) assert image is not None # become the other object (!) @@ -363,7 +372,7 @@ class Parser: (Consumer) Feed data to the parser. :param data: A string buffer. - :exception IOError: If the parser failed to parse the image file. + :exception OSError: If the parser failed to parse the image file. """ # collect data @@ -395,7 +404,7 @@ class Parser: if e < 0: # decoding error self.image = None - raise_ioerror(e) + raise_oserror(e) else: # end of image return @@ -449,7 +458,7 @@ class Parser: (Consumer) Close the stream. :returns: An image object. - :exception IOError: If the parser failed to parse the image file either + :exception OSError: If the parser failed to parse the image file either because it cannot be identified or cannot be decoded. """ @@ -500,7 +509,7 @@ def _save(im, fp, tile, bufsize=0): try: fh = fp.fileno() fp.flush() - except (AttributeError, io.UnsupportedOperation): + except (AttributeError, io.UnsupportedOperation) as exc: # compress to Python file-compatible object for e, b, o, a in tile: e = Image._getencoder(im.mode, e, a, im.encoderconfig) @@ -517,7 +526,7 @@ def _save(im, fp, tile, bufsize=0): if s: break if s < 0: - raise OSError("encoder error %d when writing image file" % s) + raise OSError(f"encoder error {s} when writing image file") from exc e.cleanup() else: # slight speedup: compress to real file object @@ -532,7 +541,7 @@ def _save(im, fp, tile, bufsize=0): else: s = e.encode_to_file(fh, bufsize) if s < 0: - raise OSError("encoder error %d when writing image file" % s) + raise OSError(f"encoder error {s} when writing image file") e.cleanup() if hasattr(fp, "flush"): fp.flush() @@ -576,7 +585,7 @@ class PyCodecState: class PyDecoder: """ Python implementation of a format decoder. Override this class and - add the decoding logic in the `decode` method. + add the decoding logic in the :meth:`decode` method. See :ref:`Writing Your Own File Decoder in Python` """ @@ -608,9 +617,9 @@ class PyDecoder: Override to perform the decoding process. :param buffer: A bytes object with the data to be decoded. - :returns: A tuple of (bytes consumed, errcode). + :returns: A tuple of ``(bytes consumed, errcode)``. If finished with decoding return <0 for the bytes consumed. - Err codes are from `ERRORS` + Err codes are from :data:`.ImageFile.ERRORS`. """ raise NotImplementedError() diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 6b0f5eb37..9ca17d9ad 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -49,7 +49,7 @@ class Kernel(BuiltinFilter): version, this must be (3,3) or (5,5). :param kernel: A sequence containing kernel weights. :param scale: Scale factor. If given, the result for each pixel is - divided by this value. the default is the sum of the + divided by this value. The default is the sum of the kernel weights. :param offset: Offset. If given, this value is added to the result, after it has been divided by the scale factor. @@ -69,7 +69,7 @@ class Kernel(BuiltinFilter): class RankFilter(Filter): """ Create a rank filter. The rank filter sorts all pixels in - a window of the given size, and returns the **rank**'th value. + a window of the given size, and returns the ``rank``'th value. :param size: The kernel size, in pixels. :param rank: What pixel value to pick. Use 0 for a min filter, @@ -401,9 +401,8 @@ class Color3DLUT(MultibandFilter): raise ValueError( "The table should have either channels * size**3 float items " "or size**3 items of channels-sized tuples with floats. " - "Table should be: {}x{}x{}x{}. Actual length: {}".format( - channels, size[0], size[1], size[2], len(table) - ) + f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. " + f"Actual length: {len(table)}" ) self.table = table @@ -411,10 +410,10 @@ class Color3DLUT(MultibandFilter): def _check_size(size): try: _, _, _ = size - except ValueError: + except ValueError as e: raise ValueError( "Size should be either an integer or a tuple of three integers." - ) + ) from e except TypeError: size = (size, size, size) size = [int(x) for x in size] @@ -513,12 +512,12 @@ class Color3DLUT(MultibandFilter): def __repr__(self): r = [ - "{} from {}".format(self.__class__.__name__, self.table.__class__.__name__), + f"{self.__class__.__name__} from {self.table.__class__.__name__}", "size={:d}x{:d}x{:d}".format(*self.size), - "channels={:d}".format(self.channels), + f"channels={self.channels:d}", ] if self.mode: - r.append("target_mode={}".format(self.mode)) + r.append(f"target_mode={self.mode}") return "<{}>".format(" ".join(r)) def filter(self, image): diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 619800829..89d180ce8 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -71,7 +71,10 @@ class ImageFont: def _load_pilfont(self, filename): with open(filename, "rb") as fp: + image = None for ext in (".png", ".gif", ".pbm"): + if image: + image.close() try: fullname = os.path.splitext(filename)[0] + ext image = Image.open(fullname) @@ -81,11 +84,14 @@ class ImageFont: if image and image.mode in ("1", "L"): break else: + if image: + image.close() raise OSError("cannot find glyph data file") self.file = fullname - return self._load_pilfont_data(fp, image) + self._load_pilfont_data(fp, image) + image.close() def _load_pilfont_data(self, file, image): @@ -209,6 +215,147 @@ class FreeTypeFont: """ return self.font.ascent, self.font.descent + def getlength(self, text, mode="", direction=None, features=None, language=None): + """ + Returns length (in pixels with 1/64 precision) of given text when rendered + in font with provided direction, features, and language. + + This is the amount by which following text should be offset. + Text bounding box may extend past the length in some fonts, + e.g. when using italics or accents. + + The result is returned as a float; it is a whole number if using basic layout. + + Note that the sum of two lengths may not equal the length of a concatenated + string due to kerning. If you need to adjust for kerning, include the following + character and subtract its length. + + For example, instead of + + .. code-block:: python + + hello = font.getlength("Hello") + world = font.getlength("World") + hello_world = hello + world # not adjusted for kerning + assert hello_world == font.getlength("HelloWorld") # may fail + + use + + .. code-block:: python + + hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning + world = font.getlength("World") + hello_world = hello + world # adjusted for kerning + assert hello_world == font.getlength("HelloWorld") # True + + or disable kerning with (requires libraqm) + + .. code-block:: python + + hello = draw.textlength("Hello", font, features=["-kern"]) + world = draw.textlength("World", font, features=["-kern"]) + hello_world = hello + world # kerning is disabled, no need to adjust + assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) + + .. versionadded:: 8.0.0 + + :param text: Text to measure. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + ` + Requires libraqm. + + :return: Width for horizontal, height for vertical text. + """ + return self.font.getlength(text, mode, direction, features, language) / 64 + + def getbbox( + self, + text, + mode="", + direction=None, + features=None, + language=None, + stroke_width=0, + anchor=None, + ): + """ + Returns bounding box (in pixels) of given text relative to given anchor + when rendered in font with provided direction, features, and language. + + Use :py:meth:`getlength()` to get the offset of following text with + 1/64 pixel precision. The bounding box includes extra margins for + some fonts, e.g. italics or accents. + + .. versionadded:: 8.0.0 + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + ` + Requires libraqm. + + :param stroke_width: The width of the text stroke. + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. + + :return: ``(left, top, right, bottom)`` bounding box + """ + size, offset = self.font.getsize( + text, mode, direction, features, language, anchor + ) + left, top = offset[0] - stroke_width, offset[1] - stroke_width + width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width + return left, top, left + width, top + height + def getsize( self, text, direction=None, features=None, language=None, stroke_width=0 ): @@ -216,6 +363,15 @@ class FreeTypeFont: Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language. + Use :py:meth:`getlength()` to measure the offset of following text with + 1/64 pixel precision. + Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. + + .. note:: For historical reasons this function measures text height from + the ascender line instead of the top, see :ref:`text-anchors`. + If you wish to measure text height from the top, it is recommended + to use the bottom value of :meth:`getbbox` with ``anchor='lt'`` instead. + :param text: Text to measure. :param direction: Direction of the text. It can be 'rtl' (right to @@ -253,9 +409,11 @@ class FreeTypeFont: :return: (width, height) """ - size, offset = self.font.getsize(text, direction, features, language) + # vertical offset is added for historical reasons + # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 + size, offset = self.font.getsize(text, "L", direction, features, language) return ( - size[0] + stroke_width * 2 + offset[0], + size[0] + stroke_width * 2, size[1] + stroke_width * 2 + offset[1], ) @@ -339,12 +497,15 @@ class FreeTypeFont: features=None, language=None, stroke_width=0, + anchor=None, + ink=0, ): """ Create a bitmap for the text. If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. Otherwise, it should have mode ``1``. + maximum value of 255. If the font has embedded color data, the bitmap + should have mode ``RGBA``. Otherwise, it should have mode ``1``. :param text: Text to render. :param mode: Used by some graphics drivers to indicate what mode the @@ -387,6 +548,16 @@ class FreeTypeFont: .. versionadded:: 6.2.0 + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. + + .. versionadded:: 8.0.0 + + :param ink: Foreground ink for rendering in RGBA mode. + + .. versionadded:: 8.0.0 + :return: An internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module. """ @@ -397,6 +568,8 @@ class FreeTypeFont: features=features, language=language, stroke_width=stroke_width, + anchor=anchor, + ink=ink, )[0] def getmask2( @@ -408,14 +581,17 @@ class FreeTypeFont: features=None, language=None, stroke_width=0, + anchor=None, + ink=0, *args, - **kwargs + **kwargs, ): """ Create a bitmap for the text. If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. Otherwise, it should have mode ``1``. + maximum value of 255. If the font has embedded color data, the bitmap + should have mode ``RGBA``. Otherwise, it should have mode ``1``. :param text: Text to render. :param mode: Used by some graphics drivers to indicate what mode the @@ -458,15 +634,28 @@ class FreeTypeFont: .. versionadded:: 6.2.0 + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. + + .. versionadded:: 8.0.0 + + :param ink: Foreground ink for rendering in RGBA mode. + + .. versionadded:: 8.0.0 + :return: A tuple of an internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ - size, offset = self.font.getsize(text, direction, features, language) + size, offset = self.font.getsize( + text, mode, direction, features, language, anchor + ) size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 - im = fill("L", size, 0) + offset = offset[0] - stroke_width, offset[1] - stroke_width + im = fill("RGBA" if mode == "RGBA" else "L", size, 0) self.font.render( - text, im.id, mode == "1", direction, features, language, stroke_width + text, im.id, mode, direction, features, language, stroke_width, ink ) return im, offset @@ -493,18 +682,18 @@ class FreeTypeFont: def get_variation_names(self): """ :returns: A list of the named styles in a variation font. - :exception IOError: If the font is not a variation font. + :exception OSError: If the font is not a variation font. """ try: names = self.font.getvarnames() - except AttributeError: - raise NotImplementedError("FreeType 2.9.1 or greater is required") + except AttributeError as e: + raise NotImplementedError("FreeType 2.9.1 or greater is required") from e return [name.replace(b"\x00", b"") for name in names] def set_variation_by_name(self, name): """ :param name: The name of the style. - :exception IOError: If the font is not a variation font. + :exception OSError: If the font is not a variation font. """ names = self.get_variation_names() if not isinstance(name, bytes): @@ -523,12 +712,12 @@ class FreeTypeFont: def get_variation_axes(self): """ :returns: A list of the axes in a variation font. - :exception IOError: If the font is not a variation font. + :exception OSError: If the font is not a variation font. """ try: axes = self.font.getvaraxes() - except AttributeError: - raise NotImplementedError("FreeType 2.9.1 or greater is required") + except AttributeError as e: + raise NotImplementedError("FreeType 2.9.1 or greater is required") from e for axis in axes: axis["name"] = axis["name"].replace(b"\x00", b"") return axes @@ -536,12 +725,12 @@ class FreeTypeFont: def set_variation_by_axes(self, axes): """ :param axes: A list of values for each axis. - :exception IOError: If the font is not a variation font. + :exception OSError: If the font is not a variation font. """ try: self.font.setvaraxes(axes) - except AttributeError: - raise NotImplementedError("FreeType 2.9.1 or greater is required") + except AttributeError as e: + raise NotImplementedError("FreeType 2.9.1 or greater is required") from e class TransposedFont: @@ -580,7 +769,7 @@ def load(filename): :param filename: Name of font file. :return: A font object. - :exception IOError: If the file could not be read. + :exception OSError: If the file could not be read. """ f = ImageFont() f._load_pilfont(filename) @@ -630,9 +819,14 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): This specifies the character set to use. It does not alter the encoding of any text provided in subsequent operations. :param layout_engine: Which layout engine to use, if available: - `ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`. + :data:`.ImageFont.LAYOUT_BASIC` or :data:`.ImageFont.LAYOUT_RAQM`. + + You can check support for Raqm layout using + :py:func:`PIL.features.check_feature` with ``feature="raqm"``. + + .. versionadded:: 4.2.0 :return: A font object. - :exception IOError: If the file could not be read. + :exception OSError: If the file could not be read. """ def freetype(font): @@ -692,7 +886,7 @@ def load_path(filename): :param filename: Name of font file. :return: A font object. - :exception IOError: If the file could not be read. + :exception OSError: If the file could not be read. """ for directory in sys.path: if isDirectory(directory): diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index e587d942d..b93ec3f2a 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -2,7 +2,7 @@ # The Python Imaging Library # $Id$ # -# screen grabber (macOS and Windows only) +# screen grabber # # History: # 2001-04-26 fl created @@ -15,43 +15,56 @@ # See the README file for information on usage and redistribution. # -import os -import subprocess import sys -import tempfile from . import Image -if sys.platform not in ["win32", "darwin"]: - raise ImportError("ImageGrab is macOS and Windows only") +if sys.platform == "darwin": + import os + import subprocess + import tempfile -def grab(bbox=None, include_layered_windows=False, all_screens=False): - if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - subprocess.call(["screencapture", "-x", filepath]) - im = Image.open(filepath) - im.load() - os.unlink(filepath) - if bbox: - im = im.crop(bbox) - else: - offset, size, data = Image.core.grabscreen(include_layered_windows, all_screens) - im = Image.frombytes( - "RGB", - size, - data, - # RGB, 32-bit line padding, origin lower left corner - "raw", - "BGR", - (size[0] * 3 + 3) & -4, - -1, - ) - if bbox: - x0, y0 = offset - left, top, right, bottom = bbox - im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) +def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None): + if xdisplay is None: + if sys.platform == "darwin": + fh, filepath = tempfile.mkstemp(".png") + os.close(fh) + subprocess.call(["screencapture", "-x", filepath]) + im = Image.open(filepath) + im.load() + os.unlink(filepath) + if bbox: + im_cropped = im.crop(bbox) + im.close() + return im_cropped + return im + elif sys.platform == "win32": + offset, size, data = Image.core.grabscreen_win32( + include_layered_windows, all_screens + ) + im = Image.frombytes( + "RGB", + size, + data, + # RGB, 32-bit line padding, origin lower left corner + "raw", + "BGR", + (size[0] * 3 + 3) & -4, + -1, + ) + if bbox: + x0, y0 = offset + left, top, right, bottom = bbox + im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) + return im + # use xdisplay=None for default display on non-win32/macOS systems + if not Image.core.HAVE_XCB: + raise OSError("Pillow was built without XCB support") + size, data = Image.core.grabscreen_x11(xdisplay) + im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) + if bbox: + im = im.crop(bbox) return im @@ -79,11 +92,29 @@ def grabclipboard(): im.load() os.unlink(filepath) return im - else: - data = Image.core.grabclipboard() + elif sys.platform == "win32": + fmt, data = Image.core.grabclipboard_win32() + if fmt == "file": # CF_HDROP + import struct + + o = struct.unpack_from("I", data)[0] + if data[16] != 0: + files = data[o:].decode("utf-16le").split("\0") + else: + files = data[o:].decode("mbcs").split("\0") + return files[: files.index("")] if isinstance(data, bytes): - from . import BmpImagePlugin import io - return BmpImagePlugin.DibImageFile(io.BytesIO(data)) - return data + data = io.BytesIO(data) + if fmt == "png": + from . import PngImagePlugin + + return PngImagePlugin.PngImageFile(data) + elif fmt == "DIB": + from . import BmpImagePlugin + + return BmpImagePlugin.DibImageFile(data) + return None + else: + raise NotImplementedError("ImageGrab.grabclipboard() is macOS and Windows only") diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index adbb94000..7f9c88e14 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -41,7 +41,7 @@ class _Operand: elif im1.im.mode in ("I", "F"): return im1.im else: - raise ValueError("unsupported mode: %s" % im1.im.mode) + raise ValueError(f"unsupported mode: {im1.im.mode}") else: # argument was a constant if _isconstant(im1) and self.im.mode in ("1", "L", "I"): @@ -57,8 +57,8 @@ class _Operand: im1.load() try: op = getattr(_imagingmath, op + "_" + im1.mode) - except AttributeError: - raise TypeError("bad operand type for '%s'" % op) + except AttributeError as e: + raise TypeError(f"bad operand type for '{op}'") from e _imagingmath.unop(op, out.im.id, im1.im.id) else: # binary operation @@ -85,8 +85,8 @@ class _Operand: im2.load() try: op = getattr(_imagingmath, op + "_" + im1.mode) - except AttributeError: - raise TypeError("bad operand type for '%s'" % op) + except AttributeError as e: + raise TypeError(f"bad operand type for '{op}'") from e _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id) return _Operand(out) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index d1ec09eac..b76dfa01f 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -28,36 +28,36 @@ MIRROR_MATRIX = [ class LutBuilder: """A class for building a MorphLut from a descriptive language - The input patterns is a list of a strings sequences like these:: + The input patterns is a list of a strings sequences like these:: - 4:(... - .1. - 111)->1 + 4:(... + .1. + 111)->1 - (whitespaces including linebreaks are ignored). The option 4 - describes a series of symmetry operations (in this case a - 4-rotation), the pattern is described by: + (whitespaces including linebreaks are ignored). The option 4 + describes a series of symmetry operations (in this case a + 4-rotation), the pattern is described by: - - . or X - Ignore - - 1 - Pixel is on - - 0 - Pixel is off + - . or X - Ignore + - 1 - Pixel is on + - 0 - Pixel is off - The result of the operation is described after "->" string. + The result of the operation is described after "->" string. - The default is to return the current pixel value, which is - returned if no other match is found. + The default is to return the current pixel value, which is + returned if no other match is found. - Operations: + Operations: - - 4 - 4 way rotation - - N - Negate - - 1 - Dummy op for no other operation (an op must always be given) - - M - Mirroring + - 4 - 4 way rotation + - N - Negate + - 1 - Dummy op for no other operation (an op must always be given) + - M - Mirroring - Example:: + Example:: - lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) - lut = lb.build_lut() + lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) + lut = lb.build_lut() """ diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 3ffe50806..14602a5c8 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -61,20 +61,25 @@ def _lut(image, lut): # actions -def autocontrast(image, cutoff=0, ignore=None): +def autocontrast(image, cutoff=0, ignore=None, mask=None): """ Maximize (normalize) image contrast. This function calculates a - histogram of the input image, removes **cutoff** percent of the + histogram of the input image (or mask region), removes ``cutoff`` percent of the lightest and darkest pixels from the histogram, and remaps the image so that the darkest pixel becomes black (0), and the lightest becomes white (255). :param image: The image to process. - :param cutoff: How many percent to cut off from the histogram. + :param cutoff: The percent to cut off from the histogram on the low and + high ends. Either a tuple of (low, high), or a single + number for both. :param ignore: The background pixel value (use None for no background). + :param mask: Histogram used in contrast operation is computed using pixels + within the mask. If no mask is given the entire image is used + for histogram computation. :return: An image. """ - histogram = image.histogram() + histogram = image.histogram(mask) lut = [] for layer in range(0, len(histogram), 256): h = histogram[layer : layer + 256] @@ -88,12 +93,14 @@ def autocontrast(image, cutoff=0, ignore=None): h[ix] = 0 if cutoff: # cut off pixels from both ends of the histogram + if not isinstance(cutoff, tuple): + cutoff = (cutoff, cutoff) # get number of pixels n = 0 for ix in range(256): n = n + h[ix] # remove cutoff% pixels from the low end - cut = n * cutoff // 100 + cut = n * cutoff[0] // 100 for lo in range(256): if cut > h[lo]: cut = cut - h[lo] @@ -103,8 +110,8 @@ def autocontrast(image, cutoff=0, ignore=None): cut = 0 if cut <= 0: break - # remove cutoff% samples from the hi end - cut = n * cutoff // 100 + # remove cutoff% samples from the high end + cut = n * cutoff[1] // 100 for hi in range(255, -1, -1): if cut > h[hi]: cut = cut - h[hi] @@ -142,14 +149,14 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi Colorize grayscale image. This function calculates a color wedge which maps all black pixels in the source image to the first color and all white pixels to the - second color. If **mid** is specified, it uses three-color mapping. - The **black** and **white** arguments should be RGB tuples or color names; - optionally you can use three-color mapping by also specifying **mid**. + second color. If ``mid`` is specified, it uses three-color mapping. + The ``black`` and ``white`` arguments should be RGB tuples or color names; + optionally you can use three-color mapping by also specifying ``mid``. Mapping positions for any of the colors can be specified - (e.g. **blackpoint**), where these parameters are the integer + (e.g. ``blackpoint``), where these parameters are the integer value corresponding to where the corresponding color should be mapped. These parameters must have logical order, such that - **blackpoint** <= **midpoint** <= **whitepoint** (if **mid** is specified). + ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified). :param image: The image to colorize. :param black: The color to use for black input pixels. @@ -221,7 +228,7 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi return _lut(image, red + green + blue) -def pad(image, size, method=Image.NEAREST, color=None, centering=(0.5, 0.5)): +def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)): """ Returns a sized and padded version of the image, expanded to fill the requested aspect ratio and size. @@ -230,10 +237,11 @@ def pad(image, size, method=Image.NEAREST, color=None, centering=(0.5, 0.5)): :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: What resampling method to use. Default is - :py:attr:`PIL.Image.NEAREST`. + :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. :param color: The background color of the padded image. :param centering: Control the position of the original image within the padded version. + (0.5, 0.5) will keep the image centered (0, 0) will keep the image aligned to the top left (1, 1) will keep the image aligned to the bottom @@ -242,7 +250,7 @@ def pad(image, size, method=Image.NEAREST, color=None, centering=(0.5, 0.5)): """ im_ratio = image.width / image.height - dest_ratio = float(size[0]) / size[1] + dest_ratio = size[0] / size[1] if im_ratio == dest_ratio: out = image.resize(size, resample=method) @@ -280,7 +288,7 @@ def crop(image, border=0): return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) -def scale(image, factor, resample=Image.NEAREST): +def scale(image, factor, resample=Image.BICUBIC): """ 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 @@ -288,8 +296,8 @@ def scale(image, factor, resample=Image.NEAREST): :param image: The image to rescale. :param factor: The expansion factor, as a float. - :param resample: An optional resampling filter. Same values possible as - in the PIL.Image.resize function. + :param resample: What resampling method to use. Default is + :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. :returns: An :py:class:`~PIL.Image.Image` object. """ if factor == 1: @@ -297,7 +305,7 @@ def scale(image, factor, resample=Image.NEAREST): elif factor <= 0: raise ValueError("the factor must be greater than 0") else: - size = (int(round(factor * image.width)), int(round(factor * image.height))) + size = (round(factor * image.width), round(factor * image.height)) return image.resize(size, resample) @@ -307,7 +315,7 @@ def deform(image, deformer, resample=Image.BILINEAR): :param image: The image to deform. :param deformer: A deformer object. Any object that implements a - **getmesh** method can be used. + ``getmesh`` method can be used. :param resample: An optional resampling filter. Same values possible as in the PIL.Image.transform function. :return: An image. @@ -363,7 +371,7 @@ def expand(image, border=0, fill=0): return out -def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): +def fit(image, size, method=Image.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): """ Returns a sized and cropped version of the image, cropped to the requested aspect ratio and size. @@ -374,7 +382,7 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: What resampling method to use. Default is - :py:attr:`PIL.Image.NEAREST`. + :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. :param bleed: Remove a border around the outside of the image from all four edges. The value is a decimal percentage (use 0.01 for one percent). The default value is 0 (no border). @@ -419,10 +427,10 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): ) # calculate the aspect ratio of the live_size - live_size_ratio = float(live_size[0]) / live_size[1] + live_size_ratio = live_size[0] / live_size[1] # calculate the aspect ratio of the output image - output_ratio = float(size[0]) / size[1] + output_ratio = size[0] / size[1] # figure out if the sides or top/bottom will be cropped off if live_size_ratio == output_ratio: diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index e0d439c98..d0604112f 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -97,13 +97,13 @@ class ImagePalette: if isinstance(color, tuple): try: return self.colors[color] - except KeyError: + except KeyError as e: # allocate new color slot if isinstance(self.palette, bytes): self.palette = bytearray(self.palette) index = len(self.colors) if index >= 256: - raise ValueError("cannot allocate more than 256 colors") + raise ValueError("cannot allocate more than 256 colors") from e self.colors[color] = index self.palette[index] = color[0] self.palette[index + 256] = color[1] @@ -111,7 +111,7 @@ class ImagePalette: self.dirty = 1 return index else: - raise ValueError("unknown color specifier: %r" % color) + raise ValueError(f"unknown color specifier: {repr(color)}") def save(self, fp): """Save palette to text file. @@ -123,12 +123,12 @@ class ImagePalette: if isinstance(fp, str): fp = open(fp, "w") fp.write("# Palette\n") - fp.write("# Mode: %s\n" % self.mode) + fp.write(f"# Mode: {self.mode}\n") for i in range(256): - fp.write("%d" % i) + fp.write(f"{i}") for j in range(i * len(self.mode), (i + 1) * len(self.mode)): try: - fp.write(" %d" % self.palette[j]) + fp.write(f" {self.palette[j]}") except IndexError: fp.write(" 0") fp.write("\n") diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index dfe2f80bd..91be53488 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -29,11 +29,11 @@ qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=Tr for qt_version, qt_module in qt_versions: try: if qt_module == "PyQt5": - from PyQt5.QtGui import QImage, qRgba, QPixmap from PyQt5.QtCore import QBuffer, QIODevice + from PyQt5.QtGui import QImage, QPixmap, qRgba elif qt_module == "PySide2": - from PySide2.QtGui import QImage, qRgba, QPixmap from PySide2.QtCore import QBuffer, QIODevice + from PySide2.QtGui import QImage, QPixmap, qRgba except (ImportError, RuntimeError): continue qt_is_installed = True @@ -142,15 +142,10 @@ def _toqclass_helper(im): data = im.tobytes("raw", "BGRX") format = QImage.Format_RGB32 elif im.mode == "RGBA": - try: - data = im.tobytes("raw", "BGRA") - except SystemError: - # workaround for earlier versions - r, g, b, a = im.split() - im = Image.merge("RGBA", (b, g, r, a)) + data = im.tobytes("raw", "BGRA") format = QImage.Format_ARGB32 else: - raise ValueError("unsupported image mode %r" % im.mode) + raise ValueError(f"unsupported image mode {repr(im.mode)}") __data = data or align8to32(im.tobytes(), im.size[0], im.mode) return {"data": __data, "im": im, "format": format, "colortable": colortable} diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 4e9f5c210..9df910a43 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -38,8 +38,8 @@ class Iterator: try: self.im.seek(ix) return self.im - except EOFError: - raise IndexError # end of sequence + except EOFError as e: + raise IndexError from e # end of sequence def __iter__(self): return self @@ -49,8 +49,8 @@ class Iterator: self.im.seek(self.position) self.position += 1 return self.im - except EOFError: - raise StopIteration + except EOFError as e: + raise StopIteration from e def all_frames(im, func=None): diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index f7e809279..1ada8252c 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -24,6 +24,14 @@ _viewers = [] def register(viewer, order=1): + """ + The :py:func:`register` function is used to register additional viewers. + + :param viewer: The viewer to be registered. + :param order: + Zero or a negative integer to prepend this viewer to the list, + a positive integer to append it. + """ try: if issubclass(viewer, Viewer): viewer = viewer() @@ -31,7 +39,7 @@ def register(viewer, order=1): pass # raised if viewer wasn't a class if order > 0: _viewers.append(viewer) - elif order < 0: + else: _viewers.insert(0, viewer) @@ -40,9 +48,9 @@ def show(image, title=None, **options): Display a given image. :param image: An image object. - :param title: Optional title. Not all viewers can display the title. + :param title: Optional title. Not all viewers can display the title. :param \**options: Additional viewer options. - :returns: True if a suitable viewer was found, false otherwise. + :returns: ``True`` if a suitable viewer was found, ``False`` otherwise. """ for viewer in _viewers: if viewer.show(image, title=title, **options): @@ -56,10 +64,15 @@ class Viewer: # main api def show(self, image, **options): + """ + The main function for displaying an image. + Converts the given image to the target format and displays it. + """ # save temporary image to disk if not ( - image.mode in ("1", "RGBA") or (self.format == "PNG" and image.mode == "LA") + image.mode in ("1", "RGBA") + or (self.format == "PNG" and image.mode in ("I;16", "LA")) ): base = Image.getmodebase(image.mode) if image.mode != base: @@ -70,25 +83,31 @@ class Viewer: # hook methods format = None + """The format to convert the image into.""" options = {} + """Additional options used to convert the image.""" def get_format(self, image): - """Return format name, or None to save as PGM/PPM""" + """Return format name, or ``None`` to save as PGM/PPM.""" return self.format def get_command(self, file, **options): + """ + Returns the command used to display the file. + Not implemented in the base class. + """ raise NotImplementedError def save_image(self, image): - """Save to temporary file, and return filename""" + """Save to temporary file and return filename.""" return image._dump(format=self.get_format(image), **self.options) def show_image(self, image, **options): - """Display given image""" + """Display the given image.""" return self.show_file(self.save_image(image), **options) def show_file(self, file, **options): - """Display given file""" + """Display the given file.""" os.system(self.get_command(file, **options)) return 1 @@ -96,104 +115,114 @@ class Viewer: # -------------------------------------------------------------------- +class WindowsViewer(Viewer): + """The default viewer on Windows is the default system application for PNG files.""" + + format = "PNG" + options = {"compress_level": 1} + + def get_command(self, file, **options): + return ( + f'start "Pillow" /WAIT "{file}" ' + "&& ping -n 2 127.0.0.1 >NUL " + f'&& del /f "{file}"' + ) + + if sys.platform == "win32": - - class WindowsViewer(Viewer): - format = "PNG" - options = {"compress_level": 1} - - def get_command(self, file, **options): - return ( - 'start "Pillow" /WAIT "%s" ' - "&& ping -n 2 127.0.0.1 >NUL " - '&& del /f "%s"' % (file, file) - ) - register(WindowsViewer) -elif sys.platform == "darwin": - class MacViewer(Viewer): - format = "PNG" - options = {"compress_level": 1} +class MacViewer(Viewer): + """The default viewer on MacOS using ``Preview.app``.""" - def get_command(self, file, **options): - # on darwin open returns immediately resulting in the temp - # file removal while app is opening - command = "open -a Preview.app" - command = "({} {}; sleep 20; rm -f {})&".format( - command, quote(file), quote(file) + format = "PNG" + options = {"compress_level": 1} + + def get_command(self, file, **options): + # 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, file, **options): + """Display given file""" + fd, path = tempfile.mkstemp() + with os.fdopen(fd, "w") as f: + f.write(file) + with open(path) as f: + subprocess.Popen( + ["im=$(cat); open -a Preview.app $im; sleep 20; rm -f $im"], + shell=True, + stdin=f, ) - return command + os.remove(path) + return 1 - def show_file(self, file, **options): - """Display given file""" - fd, path = tempfile.mkstemp() - with os.fdopen(fd, "w") as f: - f.write(file) - with open(path, "r") as f: - subprocess.Popen( - ["im=$(cat); open -a Preview.app $im; sleep 20; rm -f $im"], - shell=True, - stdin=f, - ) - os.remove(path) - return 1 +if sys.platform == "darwin": register(MacViewer) -else: - # unixoids +class UnixViewer(Viewer): + format = "PNG" + options = {"compress_level": 1} - class UnixViewer(Viewer): - format = "PNG" - options = {"compress_level": 1} + def get_command(self, file, **options): + command = self.get_command_ex(file, **options)[0] + return f"({command} {quote(file)}; rm -f {quote(file)})&" - def get_command(self, file, **options): + def show_file(self, file, **options): + """Display given file""" + fd, path = tempfile.mkstemp() + with os.fdopen(fd, "w") as f: + f.write(file) + with open(path) as f: command = self.get_command_ex(file, **options)[0] - return "({} {}; rm -f {})&".format(command, quote(file), quote(file)) + subprocess.Popen( + ["im=$(cat);" + command + " $im; rm -f $im"], shell=True, stdin=f + ) + os.remove(path) + return 1 - def show_file(self, file, **options): - """Display given file""" - fd, path = tempfile.mkstemp() - with os.fdopen(fd, "w") as f: - f.write(file) - with open(path, "r") as f: - command = self.get_command_ex(file, **options)[0] - subprocess.Popen( - ["im=$(cat);" + command + " $im; rm -f $im"], shell=True, stdin=f - ) - os.remove(path) - return 1 - # implementations +class DisplayViewer(UnixViewer): + """The ImageMagick ``display`` command.""" - class DisplayViewer(UnixViewer): - def get_command_ex(self, file, **options): - command = executable = "display" - return command, executable + def get_command_ex(self, file, **options): + command = executable = "display" + return command, executable + +class EogViewer(UnixViewer): + """The GNOME Image Viewer ``eog`` command.""" + + def get_command_ex(self, file, **options): + command = executable = "eog" + return command, executable + + +class XVViewer(UnixViewer): + """ + The X Viewer ``xv`` command. + This viewer supports the ``title`` parameter. + """ + + def get_command_ex(self, file, title=None, **options): + # note: xv is pretty outdated. most modern systems have + # imagemagick's display command instead. + command = executable = "xv" + if title: + command += f" -name {quote(title)}" + return command, executable + + +if sys.platform not in ("win32", "darwin"): # unixoids if shutil.which("display"): register(DisplayViewer) - - class EogViewer(UnixViewer): - def get_command_ex(self, file, **options): - command = executable = "eog" - return command, executable - if shutil.which("eog"): register(EogViewer) - - class XVViewer(UnixViewer): - def get_command_ex(self, file, title=None, **options): - # note: xv is pretty outdated. most modern systems have - # imagemagick's display command instead. - command = executable = "xv" - if title: - command += " -name %s" % quote(title) - return command, executable - if shutil.which("xv"): register(XVViewer) @@ -203,4 +232,5 @@ if __name__ == "__main__": print("Syntax: python ImageShow.py imagefile [title]") sys.exit() - print(show(Image.open(sys.argv[1]), *sys.argv[2:])) + with Image.open(sys.argv[1]) as im: + print(show(im, *sys.argv[2:])) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index ee707cffb..62db7a717 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -41,7 +41,7 @@ def _pilbitmap_check(): if _pilbitmap_ok is None: try: im = Image.new("1", (1, 1)) - tkinter.BitmapImage(data="PIL:%d" % im.im.id) + tkinter.BitmapImage(data=f"PIL:{im.im.id}") _pilbitmap_ok = 1 except tkinter.TclError: _pilbitmap_ok = 0 @@ -69,7 +69,7 @@ class PhotoImage: image, pixels having alpha 0 are treated as transparent. The constructor takes either a PIL image, or a mode and a size. - Alternatively, you can use the **file** or **data** options to initialize + Alternatively, you can use the ``file`` or ``data`` options to initialize the photo image object. :param image: Either a PIL image, or a mode string. If a mode string is @@ -210,7 +210,7 @@ class BitmapImage: The given image must have mode "1". Pixels having value 0 are treated as transparent. Options, if any, are passed on to Tkinter. The most commonly - used option is **foreground**, which is used to specify the color for the + used option is ``foreground``, which is used to specify the color for the non-transparent parts. See the Tkinter documentation for information on how to specify colours. @@ -229,7 +229,7 @@ class BitmapImage: if _pilbitmap_check(): # fast way (requires the pilbitmap booster patch) image.load() - kw["data"] = "PIL:%d" % image.im.id + kw["data"] = f"PIL:{image.im.id}" self.__im = image # must keep a reference else: # slow but safe way diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 927b1694b..ca9b14c8a 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -59,7 +59,7 @@ class Dib: with 20 greylevels. To make sure that palettes work properly under Windows, you must call the - **palette** method upon certain events from Windows. + ``palette`` method upon certain events from Windows. :param image: Either a PIL image, or a mode string. If a mode string is used, a size must also be given. The mode can be one of "1", @@ -88,8 +88,8 @@ class Dib: Copy the bitmap contents to a device context. :param handle: Device context (HDC), cast to a Python integer, or an - HDC or HWND instance. In PythonWin, you can use the - :py:meth:`CDC.GetHandleAttrib` to get a suitable handle. + HDC or HWND instance. In PythonWin, you can use + ``CDC.GetHandleAttrib()`` to get a suitable handle. """ if isinstance(handle, HWND): dc = self.image.getdc(handle) @@ -173,7 +173,7 @@ class Dib: Load display memory contents from byte data. :param buffer: A buffer containing display data (usually - data returned from tobytes) + data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`) """ return self.image.frombytes(buffer) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 86a7ee8cb..f407b7e5f 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -18,7 +18,10 @@ import os import tempfile from . import Image, ImageFile -from ._binary import i8, i16be as i16, i32be as i32, o8 +from ._binary import i8 +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 COMPRESSION = {1: "raw", 5: "jpeg"} @@ -118,8 +121,8 @@ class IptcImageFile(ImageFile.ImageFile): # compression try: compression = COMPRESSION[self.getint((3, 120))] - except KeyError: - raise OSError("Unknown IPTC image compression") + except KeyError as e: + raise OSError("Unknown IPTC image compression") from e # tile if tag == (8, 10): @@ -158,9 +161,9 @@ class IptcImageFile(ImageFile.ImageFile): o.close() try: - _im = Image.open(outfile) - _im.load() - self.im = _im.im + with Image.open(outfile) as _im: + _im.load() + self.im = _im.im finally: try: os.unlink(outfile) @@ -181,9 +184,10 @@ def getiptcinfo(im): :returns: A dictionary containing IPTC information, or None if no IPTC information block was found. """ - from . import TiffImagePlugin, JpegImagePlugin import io + from . import JpegImagePlugin, TiffImagePlugin + data = None if isinstance(im, IptcImageFile): diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 2c51d3678..0b0d433db 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -176,7 +176,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): if self.size is None or self.mode is None: raise SyntaxError("unable to determine size/mode") - self.reduce = 0 + self._reduce = 0 self.layers = 0 fd = -1 @@ -200,23 +200,33 @@ class Jpeg2KImageFile(ImageFile.ImageFile): "jpeg2k", (0, 0) + self.size, 0, - (self.codec, self.reduce, self.layers, fd, length), + (self.codec, self._reduce, self.layers, fd, length), ) ] + @property + def reduce(self): + # https://github.com/python-pillow/Pillow/issues/4343 found that the + # new Image 'reduce' method was shadowed by this plugin's 'reduce' + # property. This attempts to allow for both scenarios + return self._reduce or super().reduce + + @reduce.setter + def reduce(self, value): + self._reduce = value + def load(self): - if self.reduce: - power = 1 << self.reduce + if self.tile and self._reduce: + power = 1 << self._reduce adjust = power >> 1 self._size = ( int((self.size[0] + adjust) / power), int((self.size[1] + adjust) / power), ) - if self.tile: # Update the reduce and layers settings t = self.tile[0] - t3 = (t[3][0], self.reduce, self.layers, t[3][3], t[3][4]) + t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4]) self.tile = [(t[0], (0, 0) + self.size, t[2], t3)] return ImageFile.ImageFile.load(self) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index ea2f795a7..29bc61aa8 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -36,11 +36,15 @@ import io import os import struct import subprocess +import sys import tempfile import warnings from . import Image, ImageFile, TiffImagePlugin -from ._binary import i8, i16be as i16, i32be as i32, o8 +from ._binary import i8 +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 from .JpegPresets import presets # @@ -100,27 +104,25 @@ def APP(self, marker): # reassemble the profile, rather than assuming that the APP2 # markers appear in the correct sequence. self.icclist.append(s) - elif marker == 0xFFED: - if s[:14] == b"Photoshop 3.0\x00": - blocks = s[14:] - # parse the image resource block - offset = 0 - photoshop = {} - while blocks[offset : offset + 4] == b"8BIM": + elif marker == 0xFFED and s[:14] == b"Photoshop 3.0\x00": + # parse the image resource block + offset = 14 + photoshop = self.info.setdefault("photoshop", {}) + while s[offset : offset + 4] == b"8BIM": + try: offset += 4 # resource code - code = i16(blocks, offset) + code = i16(s, offset) offset += 2 # resource name (usually empty) - name_len = i8(blocks[offset]) - # name = blocks[offset+1:offset+1+name_len] - offset = 1 + offset + name_len - if offset & 1: - offset += 1 + name_len = i8(s[offset]) + # name = s[offset+1:offset+1+name_len] + offset += 1 + name_len + offset += offset & 1 # align # resource data block - size = i32(blocks, offset) + size = i32(s, offset) offset += 4 - data = blocks[offset : offset + size] + data = s[offset : offset + size] if code == 0x03ED: # ResolutionInfo data = { "XResolution": i32(data[:4]) / 65536, @@ -129,10 +131,11 @@ def APP(self, marker): "DisplayedUnitsY": i16(data[12:]), } photoshop[code] = data - offset = offset + size - if offset & 1: - offset += 1 - self.info["photoshop"] = photoshop + offset += size + offset += offset & 1 # align + except struct.error: + break # insufficient data + elif marker == 0xFFEE and s[:5] == b"Adobe": self.info["adobe"] = i16(s, 5) # extract Adobe custom properties @@ -163,10 +166,11 @@ def APP(self, marker): # 1 dpcm = 2.54 dpi dpi *= 2.54 self.info["dpi"] = int(dpi + 0.5), int(dpi + 0.5) - except (KeyError, SyntaxError, ZeroDivisionError): + except (KeyError, SyntaxError, ValueError, ZeroDivisionError): # SyntaxError for invalid/unreadable EXIF # KeyError for dpi not included # ZeroDivisionError for invalid dpi rational value + # ValueError for x_resolution[0] being an invalid float self.info["dpi"] = 72, 72 @@ -176,6 +180,7 @@ def COM(self, marker): n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) + self.info["comment"] = s self.app["COM"] = s # compatibility self.applist.append(("COM", s)) @@ -194,7 +199,7 @@ def SOF(self, marker): self.bits = i8(s[0]) if self.bits != 8: - raise SyntaxError("cannot handle %d-bit layers" % self.bits) + raise SyntaxError(f"cannot handle {self.bits}-bit layers") self.layers = i8(s[5]) if self.layers == 1: @@ -204,7 +209,7 @@ def SOF(self, marker): elif self.layers == 4: self.mode = "CMYK" else: - raise SyntaxError("cannot handle %d-layer images" % self.layers) + raise SyntaxError(f"cannot handle {self.layers}-layer images") if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: self.info["progressive"] = self.info["progression"] = 1 @@ -220,7 +225,7 @@ def SOF(self, marker): else: icc_profile = None # wrong number of fragments self.info["icc_profile"] = icc_profile - self.icclist = None + self.icclist = [] for i in range(6, len(s), 3): t = s[i : i + 3] @@ -230,9 +235,8 @@ def SOF(self, marker): def DQT(self, marker): # - # Define quantization table. Support baseline 8-bit tables - # only. Note that there might be more than one table in - # each marker. + # Define quantization table. Note that there might be more + # than one table in each marker. # FIXME: The quantization tables can be used to estimate the # compression quality. @@ -240,15 +244,16 @@ def DQT(self, marker): n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) while len(s): - if len(s) < 65: - raise SyntaxError("bad quantization table marker") v = i8(s[0]) - if v // 16 == 0: - self.quantization[v & 15] = array.array("B", s[1:65]) - s = s[65:] - else: - return # FIXME: add code to read 16-bit tables! - # raise SyntaxError, "bad quantization table element size" + precision = 1 if (v // 16 == 0) else 2 # in bytes + qt_length = 1 + precision * 64 + if len(s) < qt_length: + raise SyntaxError("bad quantization table marker") + data = array.array("B" if precision == 1 else "H", s[1:qt_length]) + if sys.byteorder == "little" and precision > 1: + data.byteswap() # the values are always big-endian + self.quantization[v & 15] = data + s = s[qt_length:] # @@ -322,7 +327,8 @@ MARKER = { def _accept(prefix): - return prefix[0:1] == b"\377" + # Magic number was taken from https://en.wikipedia.org/wiki/JPEG + return prefix[0:3] == b"\xFF\xD8\xFF" ## @@ -336,10 +342,11 @@ class JpegImageFile(ImageFile.ImageFile): def _open(self): - s = self.fp.read(1) + s = self.fp.read(3) - if i8(s) != 255: + if not _accept(s): raise SyntaxError("not a JPEG file") + s = b"\xFF" # Create attributes self.bits = self.layers = 0 @@ -409,7 +416,8 @@ class JpegImageFile(ImageFile.ImageFile): return d, e, o, a = self.tile[0] - scale = 0 + scale = 1 + original_size = self.size if a[0] == "RGB" and mode in ["L", "YCbCr"]: self.mode = mode @@ -432,7 +440,8 @@ class JpegImageFile(ImageFile.ImageFile): self.tile = [(d, e, o, a)] self.decoderconfig = (scale, 0) - return self + box = (0, 0, original_size[0] / scale, original_size[1] / scale) + return (self.mode, box) def load_djpeg(self): @@ -446,9 +455,9 @@ class JpegImageFile(ImageFile.ImageFile): raise ValueError("Invalid Filename") try: - _im = Image.open(path) - _im.load() - self.im = _im.im + with Image.open(path) as _im: + _im.load() + self.im = _im.im finally: try: os.unlink(path) @@ -467,13 +476,6 @@ class JpegImageFile(ImageFile.ImageFile): return _getmp(self) -def _fixup_dict(src_dict): - # Helper function for _getexif() - # returns a dict with any single item tuples/lists as individual values - exif = Image.Exif() - return exif._fixup_dict(src_dict) - - def _getexif(self): if "exif" not in self.info: return None @@ -483,7 +485,7 @@ def _getexif(self): def _getmp(self): # Extract MP information. This method was inspired by the "highly # experimental" _getexif version that's been in use for years now, - # itself based on the ImageFileDirectory class in the TIFF plug-in. + # itself based on the ImageFileDirectory class in the TIFF plugin. # The MP record essentially consists of a TIFF file embedded in a JPEG # application marker. @@ -500,20 +502,20 @@ def _getmp(self): file_contents.seek(info.next) info.load(file_contents) mp = dict(info) - except Exception: - raise SyntaxError("malformed MP Index (unreadable directory)") + except Exception as e: + raise SyntaxError("malformed MP Index (unreadable directory)") from e # it's an error not to have a number of images try: quant = mp[0xB001] - except KeyError: - raise SyntaxError("malformed MP Index (no number of images)") + except KeyError as e: + raise SyntaxError("malformed MP Index (no number of images)") from e # get MP entries mpentries = [] try: rawmpentries = mp[0xB002] for entrynum in range(0, quant): unpackedentry = struct.unpack_from( - "{}LLLHH".format(endianness), rawmpentries, entrynum * 16 + f"{endianness}LLLHH", rawmpentries, entrynum * 16 ) labels = ("Attribute", "Size", "DataOffset", "EntryNo1", "EntryNo2") mpentry = dict(zip(labels, unpackedentry)) @@ -542,8 +544,8 @@ def _getmp(self): mpentry["Attribute"] = mpentryattr mpentries.append(mpentry) mp[0xB002] = mpentries - except KeyError: - raise SyntaxError("malformed MP Index (bad MP Entry)") + except KeyError as e: + raise SyntaxError("malformed MP Index (bad MP Entry)") from e # Next we should try and parse the individual image unique ID list; # we don't because I've never seen this actually used in a real MPO # file and so can't test it. @@ -590,9 +592,9 @@ def convert_dict_qtables(qtables): def get_sampling(im): - # There's no subsampling when image have only 1 layer + # There's no subsampling when images have only 1 layer # (grayscale images) or when they are CMYK (4 layers), - # so set subsampling to default value. + # so set subsampling to the default value. # # NOTE: currently Pillow can't encode JPEG to YCCK format. # If YCCK support is added in the future, subsampling code will have @@ -607,24 +609,24 @@ def _save(im, fp, filename): try: rawmode = RAWMODE[im.mode] - except KeyError: - raise OSError("cannot write mode %s as JPEG" % im.mode) + except KeyError as e: + raise OSError(f"cannot write mode {im.mode} as JPEG") from e info = im.encoderinfo - dpi = [int(round(x)) for x in info.get("dpi", (0, 0))] + dpi = [round(x) for x in info.get("dpi", (0, 0))] - quality = info.get("quality", 0) + quality = info.get("quality", -1) subsampling = info.get("subsampling", -1) qtables = info.get("qtables") if quality == "keep": - quality = 0 + quality = -1 subsampling = "keep" qtables = "keep" elif quality in presets: preset = presets[quality] - quality = 0 + quality = -1 subsampling = preset.get("subsampling", -1) qtables = preset.get("quantization") elif not isinstance(quality, int): @@ -660,8 +662,8 @@ def _save(im, fp, filename): for line in qtables.splitlines() for num in line.split("#", 1)[0].split() ] - except ValueError: - raise ValueError("Invalid quantization table") + except ValueError as e: + raise ValueError("Invalid quantization table") from e else: qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] if isinstance(qtables, (tuple, list, dict)): @@ -675,9 +677,9 @@ def _save(im, fp, filename): try: if len(table) != 64: raise TypeError - table = array.array("B", table) - except TypeError: - raise ValueError("Invalid quantization table") + table = array.array("H", table) + except TypeError as e: + raise ValueError("Invalid quantization table") from e else: qtables[idx] = list(table) return qtables @@ -747,8 +749,8 @@ def _save(im, fp, filename): # CMYK can be bigger if im.mode == "CMYK": bufsize = 4 * im.size[0] * im.size[1] - # keep sets quality to 0, but the actual value may be high. - elif quality >= 95 or quality == 0: + # keep sets quality to -1, but the actual value may be high. + elif quality >= 95 or quality == -1: bufsize = 2 * im.size[0] * im.size[1] else: bufsize = im.size[0] * im.size[1] diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 012bf81b0..79d10ebb2 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -1,9 +1,11 @@ """ JPEG quality settings equivalent to the Photoshop settings. +Can be used when saving JPEG files. -More presets can be added to the presets dict if needed. - -Can be use when saving JPEG file. +The following presets are available by default: +``web_low``, ``web_medium``, ``web_high``, ``web_very_high``, ``web_maximum``, +``low``, ``medium``, ``high``, ``maximum``. +More presets can be added to the :py:data:`presets` dict if needed. To apply the preset, specify:: @@ -21,7 +23,6 @@ Example:: im.save("image_name.jpg", quality="web_high") - Subsampling ----------- @@ -33,7 +34,7 @@ Possible subsampling values are 0, 1 and 2 that correspond to 4:4:4, 4:2:2 and 4:2:0. You can get the subsampling of a JPEG with the -`JpegImagePlugin.get_sampling(im)` function. +:func:`.JpegImagePlugin.get_sampling` function. In JPEG compressed data a JPEG marker is used instead of an EXIF tag. (ref.: https://www.exiv2.org/tags.html) @@ -63,7 +64,7 @@ The tables format between im.quantization and quantization in presets differ in 3. The zigzag order is remove in the preset (needed by libjpeg >= 6a). You can convert the dict format to the preset format with the -`JpegImagePlugin.convert_dict_qtables(dict_qtables)` function. +:func:`.JpegImagePlugin.convert_dict_qtables()` function. Libjpeg ref.: https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html @@ -71,7 +72,7 @@ https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/li """ # fmt: off -presets = { # noqa: E128 +presets = { 'web_low': {'subsampling': 2, # "4:2:0" 'quantization': [ [20, 16, 25, 39, 50, 46, 62, 68, @@ -112,16 +113,16 @@ presets = { # noqa: E128 ]}, 'web_high': {'subsampling': 0, # "4:4:4" 'quantization': [ - [6, 4, 4, 6, 9, 11, 12, 16, - 4, 5, 5, 6, 8, 10, 12, 12, - 4, 5, 5, 6, 10, 12, 14, 19, - 6, 6, 6, 11, 12, 15, 19, 28, - 9, 8, 10, 12, 16, 20, 27, 31, + [6, 4, 4, 6, 9, 11, 12, 16, + 4, 5, 5, 6, 8, 10, 12, 12, + 4, 5, 5, 6, 10, 12, 14, 19, + 6, 6, 6, 11, 12, 15, 19, 28, + 9, 8, 10, 12, 16, 20, 27, 31, 11, 10, 12, 15, 20, 27, 31, 31, 12, 12, 14, 19, 27, 31, 31, 31, 16, 12, 19, 28, 31, 31, 31, 31], - [7, 7, 13, 24, 26, 31, 31, 31, - 7, 12, 16, 21, 31, 31, 31, 31, + [7, 7, 13, 24, 26, 31, 31, 31, + 7, 12, 16, 21, 31, 31, 31, 31, 13, 16, 17, 31, 31, 31, 31, 31, 24, 21, 31, 31, 31, 31, 31, 31, 26, 31, 31, 31, 31, 31, 31, 31, @@ -131,18 +132,18 @@ presets = { # noqa: E128 ]}, 'web_very_high': {'subsampling': 0, # "4:4:4" 'quantization': [ - [2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 4, 5, 7, 9, - 2, 2, 2, 4, 5, 7, 9, 12, - 3, 3, 4, 5, 8, 10, 12, 12, - 4, 4, 5, 7, 10, 12, 12, 12, - 5, 5, 7, 9, 12, 12, 12, 12, - 6, 6, 9, 12, 12, 12, 12, 12], - [3, 3, 5, 9, 13, 15, 15, 15, - 3, 4, 6, 11, 14, 12, 12, 12, - 5, 6, 9, 14, 12, 12, 12, 12, - 9, 11, 14, 12, 12, 12, 12, 12, + [2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 4, 5, 7, 9, + 2, 2, 2, 4, 5, 7, 9, 12, + 3, 3, 4, 5, 8, 10, 12, 12, + 4, 4, 5, 7, 10, 12, 12, 12, + 5, 5, 7, 9, 12, 12, 12, 12, + 6, 6, 9, 12, 12, 12, 12, 12], + [3, 3, 5, 9, 13, 15, 15, 15, + 3, 4, 6, 11, 14, 12, 12, 12, + 5, 6, 9, 14, 12, 12, 12, 12, + 9, 11, 14, 12, 12, 12, 12, 12, 13, 14, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12, @@ -189,8 +190,8 @@ presets = { # noqa: E128 'medium': {'subsampling': 2, # "4:2:0" 'quantization': [ [12, 8, 8, 12, 17, 21, 24, 17, - 8, 9, 9, 11, 15, 19, 12, 12, - 8, 9, 10, 12, 19, 12, 12, 12, + 8, 9, 9, 11, 15, 19, 12, 12, + 8, 9, 10, 12, 19, 12, 12, 12, 12, 11, 12, 21, 12, 12, 12, 12, 17, 15, 19, 12, 12, 12, 12, 12, 21, 19, 12, 12, 12, 12, 12, 12, @@ -207,16 +208,16 @@ presets = { # noqa: E128 ]}, 'high': {'subsampling': 0, # "4:4:4" 'quantization': [ - [6, 4, 4, 6, 9, 11, 12, 16, - 4, 5, 5, 6, 8, 10, 12, 12, - 4, 5, 5, 6, 10, 12, 12, 12, - 6, 6, 6, 11, 12, 12, 12, 12, - 9, 8, 10, 12, 12, 12, 12, 12, + [6, 4, 4, 6, 9, 11, 12, 16, + 4, 5, 5, 6, 8, 10, 12, 12, + 4, 5, 5, 6, 10, 12, 12, 12, + 6, 6, 6, 11, 12, 12, 12, 12, + 9, 8, 10, 12, 12, 12, 12, 12, 11, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 16, 12, 12, 12, 12, 12, 12, 12], - [7, 7, 13, 24, 20, 20, 17, 17, - 7, 12, 16, 14, 14, 12, 12, 12, + [7, 7, 13, 24, 20, 20, 17, 17, + 7, 12, 16, 14, 14, 12, 12, 12, 13, 16, 14, 14, 12, 12, 12, 12, 24, 14, 14, 12, 12, 12, 12, 12, 20, 14, 12, 12, 12, 12, 12, 12, @@ -226,18 +227,18 @@ presets = { # noqa: E128 ]}, 'maximum': {'subsampling': 0, # "4:4:4" 'quantization': [ - [2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 4, 5, 7, 9, - 2, 2, 2, 4, 5, 7, 9, 12, - 3, 3, 4, 5, 8, 10, 12, 12, - 4, 4, 5, 7, 10, 12, 12, 12, - 5, 5, 7, 9, 12, 12, 12, 12, - 6, 6, 9, 12, 12, 12, 12, 12], - [3, 3, 5, 9, 13, 15, 15, 15, - 3, 4, 6, 10, 14, 12, 12, 12, - 5, 6, 9, 14, 12, 12, 12, 12, - 9, 10, 14, 12, 12, 12, 12, 12, + [2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 4, 5, 7, 9, + 2, 2, 2, 4, 5, 7, 9, 12, + 3, 3, 4, 5, 8, 10, 12, 12, + 4, 4, 5, 7, 10, 12, 12, 12, + 5, 5, 7, 9, 12, 12, 12, 12, + 6, 6, 9, 12, 12, 12, 12, 12], + [3, 3, 5, 9, 13, 15, 15, 15, + 3, 4, 6, 10, 14, 12, 12, 12, + 5, 6, 9, 14, 12, 12, 12, 12, + 9, 10, 14, 12, 12, 12, 12, 12, 13, 14, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12, diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 8610988fc..2aed26030 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -46,8 +46,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): try: self.ole = olefile.OleFileIO(self.fp) - except OSError: - raise SyntaxError("not an MIC file; invalid OLE file") + except OSError as e: + raise SyntaxError("not an MIC file; invalid OLE file") from e # find ACI subfiles with Image members (maybe not the # best way to identify MIC files, but what the... ;-) @@ -64,27 +64,21 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): self.__fp = self.fp self.frame = None + self._n_frames = len(self.images) + self.is_animated = self._n_frames > 1 if len(self.images) > 1: self.category = Image.CONTAINER self.seek(0) - @property - def n_frames(self): - return len(self.images) - - @property - def is_animated(self): - return len(self.images) > 1 - def seek(self, frame): if not self._seek_check(frame): return try: filename = self.images[frame] - except IndexError: - raise EOFError("no such frame") + except IndexError as e: + raise EOFError("no such frame") from e self.fp = self.ole.openstream(filename) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index a63e76fe7..575cc9c8e 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -48,15 +48,16 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): def _after_jpeg_open(self, mpheader=None): self.mpinfo = mpheader if mpheader is not None else self._getmp() - self.__framecount = self.mpinfo[0xB001] + self.n_frames = self.mpinfo[0xB001] self.__mpoffsets = [ mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002] ] self.__mpoffsets[0] = 0 # Note that the following assertion will only be invalid if something # gets broken within JpegImagePlugin. - assert self.__framecount == len(self.__mpoffsets) + assert self.n_frames == len(self.__mpoffsets) del self.info["mpoffset"] # no longer needed + self.is_animated = self.n_frames > 1 self.__fp = self.fp # FIXME: hack self.__fp.seek(self.__mpoffsets[0]) # get ready to read first frame self.__frame = 0 @@ -67,14 +68,6 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): def load_seek(self, pos): self.__fp.seek(pos) - @property - def n_frames(self): - return self.__framecount - - @property - def is_animated(self): - return self.__framecount > 1 - def seek(self, frame): if not self._seek_check(frame): return @@ -82,7 +75,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): self.offset = self.__mpoffsets[frame] self.fp.seek(self.offset + 2) # skip SOI marker - if i16(self.fp.read(2)) == 0xFFE1: # APP1 + segment = self.fp.read(2) + if not segment: + raise ValueError("No data found for frame") + if i16(segment) == 0xFFE1: # APP1 n = i16(self.fp.read(2)) - 2 self.info["exif"] = ImageFile._safe_read(self.fp, n) diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 2b2937ecf..9dd6e9f32 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -27,7 +27,9 @@ import io import struct from . import Image, ImageFile -from ._binary import i8, i16le as i16, o16le as o16 +from ._binary import i8 +from ._binary import i16le as i16 +from ._binary import o16le as o16 # # read MSP files @@ -51,7 +53,7 @@ class MspImageFile(ImageFile.ImageFile): # Header s = self.fp.read(32) - if s[:4] not in [b"DanM", b"LinS"]: + if not _accept(s): raise SyntaxError("not an MSP file") # Header checksum @@ -114,10 +116,10 @@ class MspDecoder(ImageFile.PyDecoder): try: self.fd.seek(32) rowmap = struct.unpack_from( - "<%dH" % (self.state.ysize), self.fd.read(self.state.ysize * 2) + f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2) ) - except struct.error: - raise OSError("Truncated MSP file in row map") + except struct.error as e: + raise OSError("Truncated MSP file in row map") from e for x, rowlen in enumerate(rowmap): try: @@ -142,8 +144,8 @@ class MspDecoder(ImageFile.PyDecoder): img.write(row[idx : idx + runcount]) idx += runcount - except struct.error: - raise OSError("Corrupted MSP file in row %d" % x) + except struct.error as e: + raise OSError(f"Corrupted MSP file in row {x}") from e self.set_as_raw(img.getvalue(), ("1", 0, 1)) @@ -160,7 +162,7 @@ Image.register_decoder("MSP", MspDecoder) def _save(im, fp, filename): if im.mode != "1": - raise OSError("cannot write mode %s as MSP" % im.mode) + raise OSError(f"cannot write mode {im.mode} as MSP") # create MSP header header = [0] * 16 diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 90bcad036..c1bd933d3 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -2,7 +2,7 @@ # The Python Imaging Library # $Id$ # -# simple postscript graphics interface +# Simple PostScript graphics interface # # History: # 1996-04-20 fl Created @@ -20,13 +20,13 @@ import sys from . import EpsImagePlugin ## -# Simple Postscript graphics interface. +# Simple PostScript graphics interface. class PSDraw: """ - Sets up printing to the given file. If **fp** is omitted, - :py:attr:`sys.stdout` is assumed. + Sets up printing to the given file. If ``fp`` is omitted, + :py:data:`sys.stdout` is assumed. """ def __init__(self, fp=None): @@ -41,7 +41,7 @@ class PSDraw: self.fp.write(bytes(to_write, "UTF-8")) def begin_document(self, id=None): - """Set up printing of a document. (Write Postscript DSC header.)""" + """Set up printing of a document. (Write PostScript DSC header.)""" # FIXME: incomplete self._fp_write( "%!PS-Adobe-3.0\n" @@ -57,7 +57,7 @@ class PSDraw: self.isofont = {} def end_document(self): - """Ends printing. (Write Postscript DSC footer.)""" + """Ends printing. (Write PostScript DSC footer.)""" self._fp_write("%%EndDocument\nrestore showpage\n%%End\n") if hasattr(self.fp, "flush"): self.fp.flush() @@ -66,24 +66,23 @@ class PSDraw: """ Selects which font to use. - :param font: A Postscript font name + :param font: A PostScript font name :param size: Size in points. """ if font not in self.isofont: # reencode font - self._fp_write("/PSDraw-{} ISOLatin1Encoding /{} E\n".format(font, font)) + self._fp_write(f"/PSDraw-{font} ISOLatin1Encoding /{font} E\n") self.isofont[font] = 1 # rough - self._fp_write("/F0 %d /PSDraw-%s F\n" % (size, font)) + self._fp_write(f"/F0 {size} /PSDraw-{font} F\n") def line(self, xy0, xy1): """ Draws a line between the two points. Coordinates are given in - Postscript point coordinates (72 points per inch, (0, 0) is the lower + PostScript point coordinates (72 points per inch, (0, 0) is the lower left corner of the page). """ - xy = xy0 + xy1 - self._fp_write("%d %d %d %d Vl\n" % xy) + self._fp_write("%d %d %d %d Vl\n" % (*xy0, *xy1)) def rectangle(self, box): """ @@ -107,8 +106,7 @@ class PSDraw: """ text = "\\(".join(text.split("(")) text = "\\)".join(text.split(")")) - xy = xy + (text,) - self._fp_write("%d %d M (%s) S\n" % xy) + self._fp_write(f"{xy[0]} {xy[1]} M ({text}) S\n") def image(self, box, im, dpi=None): """Draw a PIL image, centered in the given box.""" @@ -119,8 +117,8 @@ class PSDraw: else: dpi = 100 # greyscale # image size (on paper) - x = float(im.size[0] * 72) / dpi - y = float(im.size[1] * 72) / dpi + x = im.size[0] * 72 / dpi + y = im.size[1] * 72 / dpi # max allowed size xmax = float(box[2] - box[0]) ymax = float(box[3] - box[1]) @@ -132,21 +130,21 @@ class PSDraw: y = ymax dx = (xmax - x) / 2 + box[0] dy = (ymax - y) / 2 + box[1] - self._fp_write("gsave\n{:f} {:f} translate\n".format(dx, dy)) + self._fp_write(f"gsave\n{dx:f} {dy:f} translate\n") if (x, y) != im.size: # EpsImagePlugin._save prints the image at (0,0,xsize,ysize) sx = x / im.size[0] sy = y / im.size[1] - self._fp_write("{:f} {:f} scale\n".format(sx, sy)) + self._fp_write(f"{sx:f} {sy:f} scale\n") EpsImagePlugin._save(im, self.fp, None, 0) self._fp_write("\ngrestore\n") # -------------------------------------------------------------------- -# Postscript driver +# PostScript driver # -# EDROFF.PS -- Postscript driver for Edroff 2 +# EDROFF.PS -- PostScript driver for Edroff 2 # # History: # 94-01-25 fl: created (edroff 2.04) @@ -176,7 +174,7 @@ EDROFF_PS = """\ """ # -# VDI.PS -- Postscript driver for VDI meta commands +# VDI.PS -- PostScript driver for VDI meta commands # # History: # 94-01-25 fl: created (edroff 2.04) diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 73f1b4b27..6ccaa1f53 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -15,11 +15,9 @@ from ._binary import o8 -## -# File handler for Teragon-style palette files. - class PaletteFile: + """File handler for Teragon-style palette files.""" rawmode = "RGB" diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 804ece34a..700f10e3f 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -8,10 +8,11 @@ ## from . import Image, ImageFile -from ._binary import o8, o16be as o16b +from ._binary import o8 +from ._binary import o16be as o16b # fmt: off -_Palm8BitColormapValues = ( # noqa: E131 +_Palm8BitColormapValues = ( (255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255), (255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204), (255, 153, 204), (255, 102, 204), (255, 51, 204), (255, 0, 204), @@ -30,15 +31,15 @@ _Palm8BitColormapValues = ( # noqa: E131 (102, 255, 204), (102, 204, 204), (102, 153, 204), (102, 102, 204), (102, 51, 204), (102, 0, 204), (102, 255, 153), (102, 204, 153), (102, 153, 153), (102, 102, 153), (102, 51, 153), (102, 0, 153), - (51, 255, 255), (51, 204, 255), (51, 153, 255), (51, 102, 255), - (51, 51, 255), (51, 0, 255), (51, 255, 204), (51, 204, 204), - (51, 153, 204), (51, 102, 204), (51, 51, 204), (51, 0, 204), - (51, 255, 153), (51, 204, 153), (51, 153, 153), (51, 102, 153), - (51, 51, 153), (51, 0, 153), (0, 255, 255), (0, 204, 255), - (0, 153, 255), (0, 102, 255), (0, 51, 255), (0, 0, 255), - (0, 255, 204), (0, 204, 204), (0, 153, 204), (0, 102, 204), - (0, 51, 204), (0, 0, 204), (0, 255, 153), (0, 204, 153), - (0, 153, 153), (0, 102, 153), (0, 51, 153), (0, 0, 153), + (51, 255, 255), (51, 204, 255), (51, 153, 255), (51, 102, 255), + (51, 51, 255), (51, 0, 255), (51, 255, 204), (51, 204, 204), + (51, 153, 204), (51, 102, 204), (51, 51, 204), (51, 0, 204), + (51, 255, 153), (51, 204, 153), (51, 153, 153), (51, 102, 153), + (51, 51, 153), (51, 0, 153), (0, 255, 255), (0, 204, 255), + (0, 153, 255), (0, 102, 255), (0, 51, 255), (0, 0, 255), + (0, 255, 204), (0, 204, 204), (0, 153, 204), (0, 102, 204), + (0, 51, 204), (0, 0, 204), (0, 255, 153), (0, 204, 153), + (0, 153, 153), (0, 102, 153), (0, 51, 153), (0, 0, 153), (255, 255, 102), (255, 204, 102), (255, 153, 102), (255, 102, 102), (255, 51, 102), (255, 0, 102), (255, 255, 51), (255, 204, 51), (255, 153, 51), (255, 102, 51), (255, 51, 51), (255, 0, 51), @@ -57,25 +58,25 @@ _Palm8BitColormapValues = ( # noqa: E131 (102, 255, 51), (102, 204, 51), (102, 153, 51), (102, 102, 51), (102, 51, 51), (102, 0, 51), (102, 255, 0), (102, 204, 0), (102, 153, 0), (102, 102, 0), (102, 51, 0), (102, 0, 0), - (51, 255, 102), (51, 204, 102), (51, 153, 102), (51, 102, 102), - (51, 51, 102), (51, 0, 102), (51, 255, 51), (51, 204, 51), - (51, 153, 51), (51, 102, 51), (51, 51, 51), (51, 0, 51), - (51, 255, 0), (51, 204, 0), (51, 153, 0), (51, 102, 0), - (51, 51, 0), (51, 0, 0), (0, 255, 102), (0, 204, 102), - (0, 153, 102), (0, 102, 102), (0, 51, 102), (0, 0, 102), - (0, 255, 51), (0, 204, 51), (0, 153, 51), (0, 102, 51), - (0, 51, 51), (0, 0, 51), (0, 255, 0), (0, 204, 0), - (0, 153, 0), (0, 102, 0), (0, 51, 0), (17, 17, 17), - (34, 34, 34), (68, 68, 68), (85, 85, 85), (119, 119, 119), + (51, 255, 102), (51, 204, 102), (51, 153, 102), (51, 102, 102), + (51, 51, 102), (51, 0, 102), (51, 255, 51), (51, 204, 51), + (51, 153, 51), (51, 102, 51), (51, 51, 51), (51, 0, 51), + (51, 255, 0), (51, 204, 0), (51, 153, 0), (51, 102, 0), + (51, 51, 0), (51, 0, 0), (0, 255, 102), (0, 204, 102), + (0, 153, 102), (0, 102, 102), (0, 51, 102), (0, 0, 102), + (0, 255, 51), (0, 204, 51), (0, 153, 51), (0, 102, 51), + (0, 51, 51), (0, 0, 51), (0, 255, 0), (0, 204, 0), + (0, 153, 0), (0, 102, 0), (0, 51, 0), (17, 17, 17), + (34, 34, 34), (68, 68, 68), (85, 85, 85), (119, 119, 119), (136, 136, 136), (170, 170, 170), (187, 187, 187), (221, 221, 221), (238, 238, 238), (192, 192, 192), (128, 0, 0), (128, 0, 128), - (0, 128, 0), (0, 128, 128), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)) + (0, 128, 0), (0, 128, 128), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)) # fmt: on @@ -137,7 +138,7 @@ def _save(im, fp, filename): bpp = im.info["bpp"] im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval)) else: - raise OSError("cannot write mode %s as Palm" % im.mode) + raise OSError(f"cannot write mode {im.mode} as Palm") # we ignore the palette here im.mode = "P" @@ -153,7 +154,7 @@ def _save(im, fp, filename): else: - raise OSError("cannot write mode %s as Palm" % im.mode) + raise OSError(f"cannot write mode {im.mode} as Palm") # # make sure image data is available diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 605cbbdf3..6a4eb22a6 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -19,7 +19,11 @@ import io from . import FontFile, Image -from ._binary import i8, i16be as b16, i16le as l16, i32be as b32, i32le as l32 +from ._binary import i8 +from ._binary import i16be as b16 +from ._binary import i16le as l16 +from ._binary import i32be as b32 +from ._binary import i32le as l32 # -------------------------------------------------------------------- # declarations @@ -48,15 +52,14 @@ def sz(s, o): return s[o : s.index(b"\0", o)] -## -# Font file plugin for the X11 PCF format. - - class PcfFontFile(FontFile.FontFile): + """Font file plugin for the X11 PCF format.""" name = "name" - def __init__(self, fp): + def __init__(self, fp, charset_encoding="iso8859-1"): + + self.charset_encoding = charset_encoding magic = l32(fp.read(4)) if magic != PCF_MAGIC: @@ -229,12 +232,17 @@ class PcfFontFile(FontFile.FontFile): nencoding = (lastCol - firstCol + 1) * (lastRow - firstRow + 1) - for i in range(nencoding): - encodingOffset = i16(fp.read(2)) - if encodingOffset != 0xFFFF: - try: - encoding[i + firstCol] = encodingOffset - except IndexError: - break # only load ISO-8859-1 glyphs + encodingOffsets = [i16(fp.read(2)) for _ in range(nencoding)] + + for i in range(firstCol, len(encoding)): + try: + encodingOffset = encodingOffsets[ + ord(bytearray([i]).decode(self.charset_encoding)) + ] + if encodingOffset != 0xFFFF: + encoding[i] = encodingOffset + except UnicodeDecodeError: + # character is not supported in selected encoding + pass return encoding diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 6cf10deb3..767f9945a 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -29,7 +29,10 @@ import io import logging from . import Image, ImageFile, ImagePalette -from ._binary import i8, i16le as i16, o8, o16le as o16 +from ._binary import i8 +from ._binary import i16le as i16 +from ._binary import o8 +from ._binary import o16le as o16 logger = logging.getLogger(__name__) @@ -131,8 +134,8 @@ def _save(im, fp, filename): try: version, bits, planes, rawmode = SAVE[im.mode] - except KeyError: - raise ValueError("Cannot save %s images as PCX" % im.mode) + except KeyError as e: + raise ValueError(f"Cannot save {im.mode} images as PCX") from e # bytes per plane stride = (im.size[0] * bits + 7) // 8 diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index d9bbf6fab..36c8fb849 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -77,7 +77,7 @@ def _save(im, fp, filename, save_all=False): existing_pdf.start_writing() existing_pdf.write_header() - existing_pdf.write_comment("created by Pillow {} PDF driver".format(__version__)) + existing_pdf.write_comment(f"created by Pillow {__version__} PDF driver") # # pages @@ -121,6 +121,7 @@ def _save(im, fp, filename, save_all=False): bits = 8 params = None + decode = None if im.mode == "1": filter = "ASCIIHexDecode" @@ -129,7 +130,7 @@ def _save(im, fp, filename, save_all=False): bits = 1 elif im.mode == "L": filter = "DCTDecode" - # params = "<< /Predictor 15 /Columns %d >>" % (width-2) + # params = f"<< /Predictor 15 /Columns {width-2} >>" colorspace = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "P": @@ -150,8 +151,9 @@ def _save(im, fp, filename, save_all=False): filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceCMYK") procset = "ImageC" # color images + decode = [1, 0, 1, 0, 1, 0, 1, 0] else: - raise ValueError("cannot save mode %s" % im.mode) + raise ValueError(f"cannot save mode {im.mode}") # # image @@ -173,7 +175,7 @@ def _save(im, fp, filename, save_all=False): elif filter == "RunLengthDecode": ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)]) else: - raise ValueError("unsupported PDF filter (%s)" % filter) + raise ValueError(f"unsupported PDF filter ({filter})") # # Get image characteristics @@ -189,6 +191,7 @@ def _save(im, fp, filename, save_all=False): Height=height, # * 72.0 / resolution, Filter=PdfParser.PdfName(filter), BitsPerComponent=bits, + Decode=decode, DecodeParams=params, ColorSpace=colorspace, ) @@ -214,9 +217,9 @@ def _save(im, fp, filename, save_all=False): # # page contents - page_contents = PdfParser.make_bytes( - "q %d 0 0 %d 0 0 cm /image Do Q\n" - % (int(width * 72.0 / resolution), int(height * 72.0 / resolution)) + page_contents = b"q %d 0 0 %d 0 0 cm /image Do Q\n" % ( + int(width * 72.0 / resolution), + int(height * 72.0 / resolution), ) existing_pdf.write_obj(contents_refs[pageNumber], stream=page_contents) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 3267ee491..975905f96 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,10 +8,6 @@ import time import zlib -def make_bytes(s): - return s.encode("us-ascii") - - # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set # on page 656 def encode_text(s): @@ -179,26 +175,24 @@ class XrefTable: else: contiguous_keys = keys keys = None - f.write(make_bytes("%d %d\n" % (contiguous_keys[0], len(contiguous_keys)))) + f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys))) for object_id in contiguous_keys: if object_id in self.new_entries: - f.write(make_bytes("%010d %05d n \n" % self.new_entries[object_id])) + f.write(b"%010d %05d n \n" % self.new_entries[object_id]) else: this_deleted_object_id = deleted_keys.pop(0) check_format_condition( object_id == this_deleted_object_id, - "expected the next deleted object ID to be %s, instead found %s" - % (object_id, this_deleted_object_id), + f"expected the next deleted object ID to be {object_id}, " + f"instead found {this_deleted_object_id}", ) try: next_in_linked_list = deleted_keys[0] except IndexError: next_in_linked_list = 0 f.write( - make_bytes( - "%010d %05d f \n" - % (next_in_linked_list, self.deleted_entries[object_id]) - ) + b"%010d %05d f \n" + % (next_in_linked_list, self.deleted_entries[object_id]) ) return startxref @@ -224,7 +218,7 @@ class PdfName: return hash(self.name) def __repr__(self): - return "PdfName(%s)" % repr(self.name) + return f"PdfName({repr(self.name)})" @classmethod def from_pdf_stream(cls, data): @@ -238,18 +232,14 @@ class PdfName: if b in self.allowed_chars: result.append(b) else: - result.extend(make_bytes("#%02X" % b)) + result.extend(b"#%02X" % b) return bytes(result) - __str__ = __bytes__ - class PdfArray(list): def __bytes__(self): return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" - __str__ = __bytes__ - class PdfDict(collections.UserDict): def __setattr__(self, key, value): @@ -261,8 +251,8 @@ class PdfDict(collections.UserDict): def __getattr__(self, key): try: value = self[key.encode("us-ascii")] - except KeyError: - raise AttributeError(key) + except KeyError as e: + raise AttributeError(key) from e if isinstance(value, bytes): value = decode_text(value) if key.endswith("Date"): @@ -304,7 +294,7 @@ class PdfBinary: self.data = data def __bytes__(self): - return make_bytes("<%s>" % "".join("%02X" % b for b in self.data)) + return b"<%s>" % b"".join(b"%02X" % b for b in self.data) class PdfStream: @@ -325,7 +315,7 @@ class PdfStream: return zlib.decompress(self.buf, bufsize=int(expected_length)) else: raise NotImplementedError( - "stream filter %s unknown/unsupported" % repr(self.dictionary.Filter) + f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported" ) @@ -433,7 +423,7 @@ class PdfParser: self.f.write(b"%PDF-1.4\n") def write_comment(self, s): - self.f.write(("% {}\n".format(s)).encode("utf-8")) + self.f.write(f"% {s}\n".encode("utf-8")) def write_catalog(self): self.del_root() @@ -495,7 +485,7 @@ class PdfParser: self.f.write( b"trailer\n" + bytes(PdfDict(trailer_dict)) - + make_bytes("\nstartxref\n%d\n%%%%EOF" % start_xref) + + b"\nstartxref\n%d\n%%%%EOF" % start_xref ) def write_page(self, ref, *objs, **dict_obj): @@ -821,11 +811,11 @@ class PdfParser: if m: try: stream_len = int(result[b"Length"]) - except (TypeError, KeyError, ValueError): + except (TypeError, KeyError, ValueError) as e: raise PdfFormatError( "bad or missing Length in stream dict (%r)" % result.get(b"Length", None) - ) + ) from e stream_data = data[m.end() : m.end() + stream_len] m = cls.re_stream_end.match(data, m.end() + stream_len) check_format_condition(m, "stream end not found") @@ -976,9 +966,8 @@ class PdfParser: offset, generation = self.xref_table[ref[0]] check_format_condition( generation == ref[1], - "expected to find generation %s for object ID %s in xref table, " - "instead found generation %s at offset %s" - % (ref[1], ref[0], generation, offset), + f"expected to find generation {ref[1]} for object ID {ref[0]} in xref " + f"table, instead found generation {generation} at offset {offset}", ) value = self.get_value( self.buf, diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 5ea32ba89..91f0314b5 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -43,7 +43,7 @@ class PixarImageFile(ImageFile.ImageFile): # assuming a 4-byte magic label s = self.fp.read(4) - if s != b"\200\350\000\000": + if not _accept(s): raise SyntaxError("not a PIXAR file") # read rest of header diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 9f98cd3d7..6af41af6e 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -31,13 +31,20 @@ # See the README file for information on usage and redistribution. # +import itertools import logging import re import struct +import warnings import zlib -from . import Image, ImageFile, ImagePalette -from ._binary import i8, i16be as i16, i32be as i32, o16be as o16, o32be as o32 +from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence +from ._binary import i8 +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 +from ._binary import o16be as o16 +from ._binary import o32be as o32 logger = logging.getLogger(__name__) @@ -74,11 +81,50 @@ _MODES = { _simple_palette = re.compile(b"^\xff*\x00\xff*$") -# Maximum decompressed size for a iTXt or zTXt chunk. -# Eliminates decompression bombs where compressed chunks can expand 1000x MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK -# Set the maximum total text chunk size. +""" +Maximum decompressed size for a iTXt or zTXt chunk. +Eliminates decompression bombs where compressed chunks can expand 1000x. +See :ref:`Text in PNG File Format`. +""" MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK +""" +Set the maximum total text chunk size. +See :ref:`Text in PNG File Format`. +""" + + +# APNG frame disposal modes +APNG_DISPOSE_OP_NONE = 0 +""" +No disposal is done on this frame before rendering the next frame. +See :ref:`Saving APNG sequences`. +""" +APNG_DISPOSE_OP_BACKGROUND = 1 +""" +This frame’s modified region is cleared to fully transparent black before rendering +the next frame. +See :ref:`Saving APNG sequences`. +""" +APNG_DISPOSE_OP_PREVIOUS = 2 +""" +This frame’s modified region is reverted to the previous frame’s contents before +rendering the next frame. +See :ref:`Saving APNG sequences`. +""" + +# APNG frame blend modes +APNG_BLEND_OP_SOURCE = 0 +""" +All color components of this frame, including alpha, overwrite the previous output +image contents. +See :ref:`Saving APNG sequences`. +""" +APNG_BLEND_OP_OVER = 1 +""" +This frame should be alpha composited with the previous output image contents. +See :ref:`Saving APNG sequences`. +""" def _safe_zlib_decompress(s): @@ -118,7 +164,7 @@ class ChunkStream: if not is_cid(cid): if not ImageFile.LOAD_TRUNCATED_IMAGES: - raise SyntaxError("broken PNG file (chunk %s)" % repr(cid)) + raise SyntaxError(f"broken PNG file (chunk {repr(cid)})") return cid, pos, length @@ -155,9 +201,13 @@ class ChunkStream: crc1 = _crc32(data, _crc32(cid)) crc2 = i32(self.fp.read(4)) if crc1 != crc2: - raise SyntaxError("broken PNG file (bad header checksum in %r)" % cid) - except struct.error: - raise SyntaxError("broken PNG file (incomplete checksum in %r)" % cid) + raise SyntaxError( + f"broken PNG file (bad header checksum in {repr(cid)})" + ) + except struct.error as e: + raise SyntaxError( + f"broken PNG file (incomplete checksum in {repr(cid)})" + ) from e def crc_skip(self, cid, data): """Read checksum. Used if the C module is not present""" @@ -174,8 +224,8 @@ class ChunkStream: while True: try: cid, pos, length = self.read() - except struct.error: - raise OSError("truncated PNG file") + except struct.error as e: + raise OSError("truncated PNG file") from e if cid == endchunk: break @@ -216,15 +266,20 @@ class PngInfo: def __init__(self): self.chunks = [] - def add(self, cid, data): + def add(self, cid, data, after_idat=False): """Appends an arbitrary chunk. Use with caution. :param cid: a byte string, 4 bytes long. :param data: a byte string of the encoded data + :param after_idat: for use with private chunks. Whether the chunk + should be written after IDAT """ - self.chunks.append((cid, data)) + chunk = [cid, data] + if after_idat: + chunk.append(True) + self.chunks.append(tuple(chunk)) def add_itxt(self, key, value, lang="", tkey="", zip=False): """Appends an iTXt chunk. @@ -298,6 +353,9 @@ class PngStream(ChunkStream): self.im_tile = None self.im_palette = None self.im_custom_mimetype = None + self.im_n_frames = None + self._seq_num = None + self.rewind_state = None self.text_memory = 0 @@ -305,10 +363,22 @@ class PngStream(ChunkStream): self.text_memory += chunklen if self.text_memory > MAX_TEXT_MEMORY: raise ValueError( - "Too much memory used in text chunks: %s>MAX_TEXT_MEMORY" - % self.text_memory + "Too much memory used in text chunks: " + f"{self.text_memory}>MAX_TEXT_MEMORY" ) + def save_rewind(self): + self.rewind_state = { + "info": self.im_info.copy(), + "tile": self.im_tile, + "seq_num": self._seq_num, + } + + def rewind(self): + self.im_info = self.rewind_state["info"] + self.im_tile = self.rewind_state["tile"] + self._seq_num = self.rewind_state["seq_num"] + def chunk_iCCP(self, pos, length): # ICC profile @@ -323,9 +393,7 @@ class PngStream(ChunkStream): logger.debug("Compression method %s", i8(s[i])) comp_method = i8(s[i]) if comp_method != 0: - raise SyntaxError( - "Unknown compression method %s in iCCP chunk" % comp_method - ) + raise SyntaxError(f"Unknown compression method {comp_method} in iCCP chunk") try: icc_profile = _safe_zlib_decompress(s[i + 2 :]) except ValueError: @@ -356,7 +424,13 @@ class PngStream(ChunkStream): def chunk_IDAT(self, pos, length): # image data - self.im_tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] + if "bbox" in self.im_info: + tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] + else: + if self.im_n_frames is not None: + self.im_info["default_image"] = True + tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] + self.im_tile = tile self.im_idat = length raise EOFError @@ -445,10 +519,11 @@ class PngStream(ChunkStream): v = b"" if k: k = k.decode("latin-1", "strict") - v = v.decode("latin-1", "replace") + v_str = v.decode("latin-1", "replace") - self.im_info[k] = self.im_text[k] = v - self.check_text_memory(len(v)) + self.im_info[k] = v if k == "exif" else v_str + self.im_text[k] = v_str + self.check_text_memory(len(v_str)) return s @@ -466,9 +541,7 @@ class PngStream(ChunkStream): else: comp_method = 0 if comp_method != 0: - raise SyntaxError( - "Unknown compression method %s in zTXt chunk" % comp_method - ) + raise SyntaxError(f"Unknown compression method {comp_method} in zTXt chunk") try: v = _safe_zlib_decompress(v[1:]) except ValueError: @@ -537,9 +610,49 @@ class PngStream(ChunkStream): # APNG chunks def chunk_acTL(self, pos, length): s = ImageFile._safe_read(self.fp, length) + if self.im_n_frames is not None: + self.im_n_frames = None + warnings.warn("Invalid APNG, will use default PNG image if possible") + return s + n_frames = i32(s) + if n_frames == 0 or n_frames > 0x80000000: + warnings.warn("Invalid APNG, will use default PNG image if possible") + return s + self.im_n_frames = n_frames + self.im_info["loop"] = i32(s[4:]) self.im_custom_mimetype = "image/apng" return s + def chunk_fcTL(self, pos, length): + s = ImageFile._safe_read(self.fp, length) + seq = i32(s) + if (self._seq_num is None and seq != 0) or ( + self._seq_num is not None and self._seq_num != seq - 1 + ): + raise SyntaxError("APNG contains frame sequence errors") + self._seq_num = seq + width, height = i32(s[4:]), i32(s[8:]) + px, py = i32(s[12:]), i32(s[16:]) + im_w, im_h = self.im_size + if px + width > im_w or py + height > im_h: + raise SyntaxError("APNG contains invalid frames") + self.im_info["bbox"] = (px, py, px + width, py + height) + delay_num, delay_den = i16(s[20:]), i16(s[22:]) + if delay_den == 0: + delay_den = 100 + self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000 + self.im_info["disposal"] = i8(s[24]) + self.im_info["blend"] = i8(s[25]) + return s + + def chunk_fdAT(self, pos, length): + s = ImageFile._safe_read(self.fp, 4) + seq = i32(s) + if self._seq_num != seq - 1: + raise SyntaxError("APNG contains frame sequence errors") + self._seq_num = seq + return self.chunk_IDAT(pos + 4, length - 4) + # -------------------------------------------------------------------- # PNG reader @@ -560,12 +673,15 @@ class PngImageFile(ImageFile.ImageFile): def _open(self): - if self.fp.read(8) != _MAGIC: + if not _accept(self.fp.read(8)): raise SyntaxError("not a PNG file") + self.__fp = self.fp + self.__frame = 0 # - # Parse headers up to the first IDAT chunk + # Parse headers up to the first IDAT or fDAT chunk + self.private_chunks = [] self.png = PngStream(self.fp) while True: @@ -582,6 +698,8 @@ class PngImageFile(ImageFile.ImageFile): except AttributeError: logger.debug("%r %s %s (unknown)", cid, pos, length) s = ImageFile._safe_read(self.fp, length) + if cid[1:2].islower(): + self.private_chunks.append((cid, s)) self.png.crc(cid, s) @@ -598,12 +716,28 @@ class PngImageFile(ImageFile.ImageFile): self._text = None self.tile = self.png.im_tile self.custom_mimetype = self.png.im_custom_mimetype + self.n_frames = self.png.im_n_frames or 1 + self.default_image = self.info.get("default_image", False) if self.png.im_palette: rawmode, data = self.png.im_palette self.palette = ImagePalette.raw(rawmode, data) - self.__prepare_idat = length # used by load_prepare() + if cid == b"fdAT": + self.__prepare_idat = length - 4 + else: + self.__prepare_idat = length # used by load_prepare() + + if self.png.im_n_frames is not None: + self._close_exclusive_fp_after_loading = False + self.png.save_rewind() + self.__rewind_idat = self.__prepare_idat + self.__rewind = self.__fp.tell() + if self.default_image: + # IDAT chunk contains default image and not first animation frame + self.n_frames += 1 + self._seek(0) + self.is_animated = self.n_frames > 1 @property def text(self): @@ -611,7 +745,13 @@ class PngImageFile(ImageFile.ImageFile): if self._text is None: # iTxt, tEXt and zTXt chunks may appear at the end of the file # So load the file to ensure that they are read + if self.is_animated: + frame = self.__frame + # for APNG, seek to the final frame before loading + self.seek(self.n_frames - 1) self.load() + if self.is_animated: + self.seek(frame) return self._text def verify(self): @@ -630,6 +770,97 @@ class PngImageFile(ImageFile.ImageFile): self.fp.close() self.fp = None + def seek(self, frame): + if not self._seek_check(frame): + return + if frame < self.__frame: + self._seek(0, True) + + last_frame = self.__frame + for f in range(self.__frame + 1, frame + 1): + try: + self._seek(f) + except EOFError as e: + self.seek(last_frame) + raise EOFError("no more images in APNG file") from e + + def _seek(self, frame, rewind=False): + if frame == 0: + if rewind: + self.__fp.seek(self.__rewind) + self.png.rewind() + self.__prepare_idat = self.__rewind_idat + self.im = None + if self.pyaccess: + self.pyaccess = None + self.info = self.png.im_info + self.tile = self.png.im_tile + self.fp = self.__fp + self._prev_im = None + self.dispose = None + self.default_image = self.info.get("default_image", False) + self.dispose_op = self.info.get("disposal") + self.blend_op = self.info.get("blend") + self.dispose_extent = self.info.get("bbox") + self.__frame = 0 + return + else: + if frame != self.__frame + 1: + raise ValueError(f"cannot seek to frame {frame}") + + # ensure previous frame was loaded + self.load() + + self.fp = self.__fp + + # advance to the next frame + if self.__prepare_idat: + ImageFile._safe_read(self.fp, self.__prepare_idat) + self.__prepare_idat = 0 + frame_start = False + while True: + self.fp.read(4) # CRC + + try: + cid, pos, length = self.png.read() + except (struct.error, SyntaxError): + break + + if cid == b"IEND": + raise EOFError("No more images in APNG file") + if cid == b"fcTL": + if frame_start: + # there must be at least one fdAT chunk between fcTL chunks + raise SyntaxError("APNG missing frame data") + frame_start = True + + try: + self.png.call(cid, pos, length) + except UnicodeDecodeError: + break + except EOFError: + if cid == b"fdAT": + length -= 4 + if frame_start: + self.__prepare_idat = length + break + ImageFile._safe_read(self.fp, length) + except AttributeError: + logger.debug("%r %s %s (unknown)", cid, pos, length) + ImageFile._safe_read(self.fp, length) + + self.__frame = frame + self.tile = self.png.im_tile + self.dispose_op = self.info.get("disposal") + self.blend_op = self.info.get("blend") + self.dispose_extent = self.info.get("bbox") + + if not self.tile: + raise EOFError + + def tell(self): + return self.__frame + def load_prepare(self): """internal: prepare to read PNG file""" @@ -649,11 +880,18 @@ class PngImageFile(ImageFile.ImageFile): cid, pos, length = self.png.read() - if cid not in [b"IDAT", b"DDAT"]: + if cid not in [b"IDAT", b"DDAT", b"fdAT"]: self.png.push(cid, pos, length) return b"" - self.__idat = length # empty chunks are allowed + if cid == b"fdAT": + try: + self.png.call(cid, pos, length) + except EOFError: + pass + self.__idat = length - 4 # sequence_num has already been read + else: + self.__idat = length # empty chunks are allowed # read more data from this chunk if read_bytes <= 0: @@ -677,31 +915,77 @@ class PngImageFile(ImageFile.ImageFile): if cid == b"IEND": break + elif cid == b"fcTL" and self.is_animated: + # start of the next frame, stop reading + self.__prepare_idat = 0 + self.png.push(cid, pos, length) + break try: self.png.call(cid, pos, length) except UnicodeDecodeError: break except EOFError: + if cid == b"fdAT": + length -= 4 ImageFile._safe_read(self.fp, length) except AttributeError: logger.debug("%r %s %s (unknown)", cid, pos, length) - ImageFile._safe_read(self.fp, length) + s = ImageFile._safe_read(self.fp, length) + if cid[1:2].islower(): + self.private_chunks.append((cid, s, True)) self._text = self.png.im_text - self.png.close() - self.png = None + if not self.is_animated: + self.png.close() + self.png = None + else: + # setup frame disposal (actual disposal done when needed in _seek()) + if self._prev_im is None and self.dispose_op == APNG_DISPOSE_OP_PREVIOUS: + self.dispose_op = APNG_DISPOSE_OP_BACKGROUND + + if self.dispose_op == APNG_DISPOSE_OP_PREVIOUS: + dispose = self._prev_im.copy() + dispose = self._crop(dispose, self.dispose_extent) + elif self.dispose_op == APNG_DISPOSE_OP_BACKGROUND: + dispose = Image.core.fill(self.im.mode, self.size) + dispose = self._crop(dispose, self.dispose_extent) + else: + dispose = None + + if self._prev_im and self.blend_op == APNG_BLEND_OP_OVER: + updated = self._crop(self.im, self.dispose_extent) + self._prev_im.paste( + updated, self.dispose_extent, updated.convert("RGBA") + ) + self.im = self._prev_im + if self.pyaccess: + self.pyaccess = None + self._prev_im = self.im.copy() + + if dispose: + self._prev_im.paste(dispose, self.dispose_extent) def _getexif(self): if "exif" not in self.info: self.load() - if "exif" not in self.info: + if "exif" not in self.info and "Raw profile type exif" not in self.info: return None return dict(self.getexif()) def getexif(self): if "exif" not in self.info: self.load() - return ImageFile.ImageFile.getexif(self) + + return super().getexif() + + def _close__fp(self): + try: + if self.__fp != self.fp: + self.__fp.close() + except AttributeError: + pass + finally: + self.__fp = None # -------------------------------------------------------------------- @@ -748,7 +1032,152 @@ class _idat: self.chunk(self.fp, b"IDAT", data) -def _save(im, fp, filename, chunk=putchunk): +class _fdat: + # wrap encoder output in fdAT chunks + + def __init__(self, fp, chunk, seq_num): + self.fp = fp + self.chunk = chunk + self.seq_num = seq_num + + def write(self, data): + self.chunk(self.fp, b"fdAT", o32(self.seq_num), data) + self.seq_num += 1 + + +def _write_multiple_frames(im, fp, chunk, rawmode): + default_image = im.encoderinfo.get("default_image", im.info.get("default_image")) + duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) + loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) + disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) + blend = im.encoderinfo.get("blend", im.info.get("blend")) + + if default_image: + chain = itertools.chain(im.encoderinfo.get("append_images", [])) + else: + chain = itertools.chain([im], im.encoderinfo.get("append_images", [])) + + im_frames = [] + frame_count = 0 + for im_seq in chain: + for im_frame in ImageSequence.Iterator(im_seq): + im_frame = im_frame.copy() + if im_frame.mode != im.mode: + if im.mode == "P": + im_frame = im_frame.convert(im.mode, palette=im.palette) + else: + im_frame = im_frame.convert(im.mode) + encoderinfo = im.encoderinfo.copy() + if isinstance(duration, (list, tuple)): + encoderinfo["duration"] = duration[frame_count] + if isinstance(disposal, (list, tuple)): + encoderinfo["disposal"] = disposal[frame_count] + if isinstance(blend, (list, tuple)): + encoderinfo["blend"] = blend[frame_count] + frame_count += 1 + + if im_frames: + previous = im_frames[-1] + prev_disposal = previous["encoderinfo"].get("disposal") + prev_blend = previous["encoderinfo"].get("blend") + if prev_disposal == APNG_DISPOSE_OP_PREVIOUS and len(im_frames) < 2: + prev_disposal = APNG_DISPOSE_OP_BACKGROUND + + if prev_disposal == APNG_DISPOSE_OP_BACKGROUND: + base_im = previous["im"] + dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) + bbox = previous["bbox"] + if bbox: + dispose = dispose.crop(bbox) + else: + bbox = (0, 0) + im.size + base_im.paste(dispose, bbox) + elif prev_disposal == APNG_DISPOSE_OP_PREVIOUS: + base_im = im_frames[-2]["im"] + else: + base_im = previous["im"] + delta = ImageChops.subtract_modulo( + im_frame.convert("RGB"), base_im.convert("RGB") + ) + bbox = delta.getbbox() + if ( + not bbox + and prev_disposal == encoderinfo.get("disposal") + and prev_blend == encoderinfo.get("blend") + ): + duration = encoderinfo.get("duration", 0) + if duration: + if "duration" in previous["encoderinfo"]: + previous["encoderinfo"]["duration"] += duration + else: + previous["encoderinfo"]["duration"] = duration + continue + else: + bbox = None + im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) + + # animation control + chunk( + fp, + b"acTL", + o32(len(im_frames)), # 0: num_frames + o32(loop), # 4: num_plays + ) + + # default image IDAT (if it exists) + if default_image: + ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + + seq_num = 0 + for frame, frame_data in enumerate(im_frames): + im_frame = frame_data["im"] + if not frame_data["bbox"]: + bbox = (0, 0) + im_frame.size + else: + bbox = frame_data["bbox"] + im_frame = im_frame.crop(bbox) + size = im_frame.size + duration = int(round(frame_data["encoderinfo"].get("duration", 0))) + disposal = frame_data["encoderinfo"].get("disposal", APNG_DISPOSE_OP_NONE) + blend = frame_data["encoderinfo"].get("blend", APNG_BLEND_OP_SOURCE) + # frame control + chunk( + fp, + b"fcTL", + o32(seq_num), # sequence_number + o32(size[0]), # width + o32(size[1]), # height + o32(bbox[0]), # x_offset + o32(bbox[1]), # y_offset + o16(duration), # delay_numerator + o16(1000), # delay_denominator + o8(disposal), # dispose_op + o8(blend), # blend_op + ) + seq_num += 1 + # frame data + if frame == 0 and not default_image: + # first frame must be in IDAT chunks for backwards compatibility + ImageFile._save( + im_frame, + _idat(fp, chunk), + [("zip", (0, 0) + im_frame.size, 0, rawmode)], + ) + else: + fdat_chunks = _fdat(fp, chunk, seq_num) + ImageFile._save( + im_frame, + fdat_chunks, + [("zip", (0, 0) + im_frame.size, 0, rawmode)], + ) + seq_num = fdat_chunks.seq_num + + +def _save_all(im, fp, filename): + _save(im, fp, filename, save_all=True) + + +def _save(im, fp, filename, chunk=putchunk, save_all=False): # save an image to disk (called by the save method) mode = im.mode @@ -776,7 +1205,7 @@ def _save(im, fp, filename, chunk=putchunk): else: bits = 8 if bits != 8: - mode = "%s;%d" % (mode, bits) + mode = f"{mode};{bits}" # encoder options im.encoderconfig = ( @@ -789,8 +1218,8 @@ def _save(im, fp, filename, chunk=putchunk): # get the corresponding PNG mode try: rawmode, mode = _OUTMODES[mode] - except KeyError: - raise OSError("cannot write mode %s as PNG" % mode) + except KeyError as e: + raise OSError(f"cannot write mode {mode} as PNG") from e # # write minimal PNG file @@ -829,12 +1258,18 @@ def _save(im, fp, filename, chunk=putchunk): info = im.encoderinfo.get("pnginfo") if info: chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] - for cid, data in info.chunks: + for info_chunk in info.chunks: + cid, data = info_chunk[:2] if cid in chunks: chunks.remove(cid) chunk(fp, cid, data) elif cid in chunks_multiple_allowed: chunk(fp, cid, data) + elif cid[1:2].islower(): + # Private chunk + after_idat = info_chunk[2:3] + if not after_idat: + chunk(fp, cid, data) if im.mode == "P": palette_byte_number = (2 ** bits) * 3 @@ -882,10 +1317,10 @@ def _save(im, fp, filename, chunk=putchunk): b"\x01", ) - info = im.encoderinfo.get("pnginfo") if info: chunks = [b"bKGD", b"hIST"] - for cid, data in info.chunks: + for info_chunk in info.chunks: + cid, data = info_chunk[:2] if cid in chunks: chunks.remove(cid) chunk(fp, cid, data) @@ -898,7 +1333,19 @@ def _save(im, fp, filename, chunk=putchunk): exif = exif[6:] chunk(fp, b"eXIf", exif) - ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + if save_all: + _write_multiple_frames(im, fp, chunk, rawmode) + else: + ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + + if info: + for info_chunk in info.chunks: + cid, data = info_chunk[:2] + if cid[1:2].islower(): + # Private chunk + after_idat = info_chunk[2:3] + if after_idat: + chunk(fp, cid, data) chunk(fp, b"IEND", b"") @@ -943,6 +1390,7 @@ def getchunks(im, **params): Image.register_open(PngImageFile.format, PngImageFile, _accept) Image.register_save(PngImageFile.format, _save) +Image.register_save_all(PngImageFile.format, _save_all) Image.register_extensions(PngImageFile.format, [".png", ".apng"]) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 35a77bafb..abf4d651d 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -104,7 +104,7 @@ class PpmImageFile(ImageFile.ImageFile): # maxgrey if s > 255: if not mode == "L": - raise ValueError("Too many colors for band: %s" % s) + raise ValueError(f"Too many colors for band: {s}") if s < 2 ** 16: self.mode = "I" rawmode = "I;16B" @@ -135,7 +135,7 @@ def _save(im, fp, filename): elif im.mode == "RGBA": rawmode, head = "RGB", b"P6" else: - raise OSError("cannot write mode %s as PPM" % im.mode) + raise OSError(f"cannot write mode {im.mode} as PPM") fp.write(head + ("\n%d %d\n" % im.size).encode("ascii")) if head == b"P6": fp.write(b"255\n") diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index cceb85c5b..8d1dbf2b2 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -19,7 +19,9 @@ import io from . import Image, ImageFile, ImagePalette -from ._binary import i8, i16be as i16, i32be as i32 +from ._binary import i8 +from ._binary import i16be as i16 +from ._binary import i32be as i32 MODES = { # (photoshop mode, bits) -> (pil mode, required channels) @@ -61,7 +63,7 @@ class PsdImageFile(ImageFile.ImageFile): # header s = read(26) - if s[:4] != b"8BPS" or i16(s[4:]) != 1: + if not _accept(s) or i16(s[4:]) != 1: raise SyntaxError("not a PSD file") psd_bits = i16(s[22:]) @@ -119,6 +121,8 @@ class PsdImageFile(ImageFile.ImageFile): if size: self.layers = _layerinfo(self.fp) self.fp.seek(end) + self.n_frames = len(self.layers) + self.is_animated = self.n_frames > 1 # # image descriptor @@ -130,14 +134,6 @@ class PsdImageFile(ImageFile.ImageFile): self.frame = 1 self._min_frame = 1 - @property - def n_frames(self): - return len(self.layers) - - @property - def is_animated(self): - return len(self.layers) > 1 - def seek(self, layer): if not self._seek_check(layer): return @@ -150,8 +146,8 @@ class PsdImageFile(ImageFile.ImageFile): self.frame = layer self.fp = self.__fp return name, bbox - except IndexError: - raise EOFError("no such layer") + except IndexError as e: + raise EOFError("no such layer") from e def tell(self): # return layer number (0=image, 1..max=layers) @@ -313,3 +309,5 @@ def _maketile(file, mode, bbox, channels): Image.register_open(PsdImageFile.format, PsdImageFile, _accept) Image.register_extension(PsdImageFile.format, ".psd") + +Image.register_mime(PsdImageFile.format, "image/vnd.adobe.photoshop") diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 359a94919..494f5f9f4 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -23,23 +23,29 @@ import logging import sys -from cffi import FFI +try: + from cffi import FFI + + defs = """ + struct Pixel_RGBA { + unsigned char r,g,b,a; + }; + struct Pixel_I16 { + unsigned char l,r; + }; + """ + ffi = FFI() + ffi.cdef(defs) +except ImportError as ex: + # Allow error import for doc purposes, but error out when accessing + # anything in core. + from ._util import deferred_error + + FFI = ffi = deferred_error(ex) logger = logging.getLogger(__name__) -defs = """ -struct Pixel_RGBA { - unsigned char r,g,b,a; -}; -struct Pixel_I16 { - unsigned char l,r; -}; -""" -ffi = FFI() -ffi.cdef(defs) - - class PyAccess: def __init__(self, img, readonly=False): vals = dict(img.im.unsafe_ptrs) diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index ddd3de379..f878fefa9 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -26,7 +26,9 @@ import os import struct from . import Image, ImageFile -from ._binary import i8, i16be as i16, o8 +from ._binary import i8 +from ._binary import i16be as i16 +from ._binary import o8 def _accept(prefix): @@ -58,8 +60,7 @@ class SgiImageFile(ImageFile.ImageFile): headlen = 512 s = self.fp.read(headlen) - # magic number : 474 - if i16(s) != 474: + if not _accept(s): raise ValueError("Not an SGI image file") # compression : verbatim or RLE @@ -159,9 +160,7 @@ def _save(im, fp, filename): # assert we've got the right number of bands. if len(im.getbands()) != z: raise ValueError( - "incorrect number of bands in SGI write: {} vs {}".format( - z, len(im.getbands()) - ) + f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}" ) # Minimum Byte value diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index dd0620c14..819f2ed0a 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -111,8 +111,8 @@ class SpiderImageFile(ImageFile.ImageFile): hdrlen = isSpiderHeader(t) if hdrlen == 0: raise SyntaxError("not a valid Spider file") - except struct.error: - raise SyntaxError("not a valid Spider file") + except struct.error as e: + raise SyntaxError("not a valid Spider file") from e h = (99,) + t # add 1 value : spider header index starts at 1 iform = int(h[5]) @@ -213,7 +213,7 @@ def loadImageSeries(filelist=None): imglist = [] for img in filelist: if not os.path.exists(img): - print("unable to find %s" % img) + print(f"unable to find {img}") continue try: with Image.open(img) as im: @@ -304,21 +304,21 @@ if __name__ == "__main__": print("input image must be in Spider format") sys.exit() - im = Image.open(filename) - print("image: " + str(im)) - print("format: " + str(im.format)) - print("size: " + str(im.size)) - print("mode: " + str(im.mode)) - print("max, min: ", end=" ") - print(im.getextrema()) + with Image.open(filename) as im: + print("image: " + str(im)) + print("format: " + str(im.format)) + print("size: " + str(im.size)) + print("mode: " + str(im.mode)) + print("max, min: ", end=" ") + print(im.getextrema()) - if len(sys.argv) > 2: - outfile = sys.argv[2] + if len(sys.argv) > 2: + outfile = sys.argv[2] - # perform some image operation - im = im.transpose(Image.FLIP_LEFT_RIGHT) - print( - "saving a flipped version of %s as %s " - % (os.path.basename(filename), outfile) - ) - im.save(outfile, SpiderImageFile.format) + # perform some image operation + im = im.transpose(Image.FLIP_LEFT_RIGHT) + print( + f"saving a flipped version of {os.path.basename(filename)} " + f"as {outfile} " + ) + im.save(outfile, SpiderImageFile.format) diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index fd7ca8a40..d99884293 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -53,7 +53,7 @@ class SunImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(32) - if i32(s) != 0x59A66A95: + if not _accept(s): raise SyntaxError("not an SUN raster file") offset = 32 diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index ede646453..d108362fc 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -18,12 +18,10 @@ import io from . import ContainerIO -## -# A file object that provides read access to a given member of a TAR -# file. - class TarIO(ContainerIO.ContainerIO): + """A file object that provides read access to a given member of a TAR file.""" + def __init__(self, tarfile, file): """ Create file object. diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index fd71e545d..69b3e0678 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -20,7 +20,10 @@ import warnings from . import Image, ImageFile, ImagePalette -from ._binary import i8, i16le as i16, o8, o16le as o16 +from ._binary import i8 +from ._binary import i16le as i16 +from ._binary import o8 +from ._binary import o16le as o16 # # -------------------------------------------------------------------- @@ -167,8 +170,8 @@ def _save(im, fp, filename): try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] - except KeyError: - raise OSError("cannot write mode %s as TGA" % im.mode) + except KeyError as e: + raise OSError(f"cannot write mode {im.mode} as TGA") from e if "rle" in im.encoderinfo: rle = im.encoderinfo["rle"] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 47b69a003..bbfd28cc2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -38,9 +38,9 @@ # # See the README file for information on usage and redistribution. # -import distutils.version import io import itertools +import logging import os import struct import warnings @@ -52,7 +52,7 @@ from . import Image, ImageFile, ImagePalette, TiffTags from ._binary import i8, o8 from .TiffTags import TYPES -DEBUG = False # Needs to be merged with the new logging approach. +logger = logging.getLogger(__name__) # Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False @@ -264,8 +264,18 @@ def _limit_rational(val, max_val): return n_d[::-1] if inv else n_d -def _libtiff_version(): - return Image.core.libtiff_version.split("\n")[0].split("Version ")[1] +def _limit_signed_rational(val, max_val, min_val): + frac = Fraction(val) + n_d = frac.numerator, frac.denominator + + if min(n_d) < min_val: + n_d = _limit_rational(val, abs(min_val)) + + if max(n_d) > max_val: + val = Fraction(*n_d) + n_d = _limit_rational(val, max_val) + + return n_d ## @@ -276,7 +286,7 @@ _write_dispatch = {} class IFDRational(Rational): - """ Implements a rational class where 0/0 is a legal value to match + """Implements a rational class where 0/0 is a legal value to match the in the wild use of exif rationals. e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used @@ -343,6 +353,8 @@ class IFDRational(Rational): return self._val.__hash__() def __eq__(self, other): + if isinstance(other, IFDRational): + other = other._val return self._val == other def _delegate(op): @@ -404,7 +416,7 @@ class ImageFileDirectory_v2(MutableMapping): The tiff metadata type of each item is stored in a dictionary of tag types in - `~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types + :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types are read from a tiff file, guessed from the type added, or added manually. @@ -454,7 +466,7 @@ class ImageFileDirectory_v2(MutableMapping): :param prefix: Override the endianness of the file. """ if ifh[:4] not in PREFIXES: - raise SyntaxError("not a TIFF file (header %r not valid)" % ifh) + raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)") self._prefix = prefix if prefix is not None else ifh[:2] if self._prefix == MM: self._endian = ">" @@ -462,6 +474,8 @@ class ImageFileDirectory_v2(MutableMapping): self._endian = "<" else: raise SyntaxError("not a TIFF IFD") + self.tagtype = {} + """ Dictionary of tag types """ self.reset() (self.next,) = self._unpack("L", ifh[4:]) self._legacy_api = False @@ -525,17 +539,28 @@ class ImageFileDirectory_v2(MutableMapping): else: self.tagtype[tag] = TiffTags.UNDEFINED if all(isinstance(v, IFDRational) for v in values): - self.tagtype[tag] = TiffTags.RATIONAL + self.tagtype[tag] = ( + TiffTags.RATIONAL + if all(v >= 0 for v in values) + else TiffTags.SIGNED_RATIONAL + ) elif all(isinstance(v, int) for v in values): - if all(v < 2 ** 16 for v in values): + if all(0 <= v < 2 ** 16 for v in values): self.tagtype[tag] = TiffTags.SHORT + elif all(-(2 ** 15) < v < 2 ** 15 for v in values): + self.tagtype[tag] = TiffTags.SIGNED_SHORT else: - self.tagtype[tag] = TiffTags.LONG + self.tagtype[tag] = ( + TiffTags.LONG + if all(v >= 0 for v in values) + else TiffTags.SIGNED_LONG + ) elif all(isinstance(v, float) for v in values): self.tagtype[tag] = TiffTags.DOUBLE - else: - if all(isinstance(v, str) for v in values): - self.tagtype[tag] = TiffTags.ASCII + elif all(isinstance(v, str) for v in values): + self.tagtype[tag] = TiffTags.ASCII + elif all(isinstance(v, bytes) for v in values): + self.tagtype[tag] = TiffTags.BYTE if self.tagtype[tag] == TiffTags.UNDEFINED: values = [ @@ -544,7 +569,9 @@ class ImageFileDirectory_v2(MutableMapping): elif self.tagtype[tag] == TiffTags.RATIONAL: values = [float(v) if isinstance(v, int) else v for v in values] - values = tuple(info.cvt_enum(value) for value in values) + is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) + if not is_ifd: + values = tuple(info.cvt_enum(value) for value in values) dest = self._tags_v1 if legacy_api else self._tags_v2 @@ -553,8 +580,10 @@ class ImageFileDirectory_v2(MutableMapping): # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. # Don't mess with the legacy api, since it's frozen. - if (info.length == 1) or ( - info.length is None and len(values) == 1 and not legacy_api + if not is_ifd and ( + (info.length == 1) + or self.tagtype[tag] == TiffTags.BYTE + or (info.length is None and len(values) == 1 and not legacy_api) ): # Don't mess with the legacy api, since it's frozen. if legacy_api and self.tagtype[tag] in [ @@ -567,8 +596,8 @@ class ImageFileDirectory_v2(MutableMapping): except ValueError: # We've got a builtin tag with 1 expected entry warnings.warn( - "Metadata Warning, tag %s had too many entries: %s, expected 1" - % (tag, len(values)) + f"Metadata Warning, tag {tag} had too many entries: " + f"{len(values)}, expected 1" ) dest[tag] = values[0] @@ -636,6 +665,7 @@ class ImageFileDirectory_v2(MutableMapping): (TiffTags.SIGNED_LONG, "l", "signed long"), (TiffTags.FLOAT, "f", "float"), (TiffTags.DOUBLE, "d", "double"), + (TiffTags.IFD, "L", "long"), ], ) ) @@ -671,7 +701,7 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(5) def write_rational(self, *values): return b"".join( - self._pack("2L", *_limit_rational(frac, 2 ** 31)) for frac in values + self._pack("2L", *_limit_rational(frac, 2 ** 32 - 1)) for frac in values ) @_register_loader(7, 1) @@ -694,7 +724,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(10) def write_signed_rational(self, *values): return b"".join( - self._pack("2L", *_limit_rational(frac, 2 ** 30)) for frac in values + self._pack("2l", *_limit_signed_rational(frac, 2 ** 31 - 1, -(2 ** 31))) + for frac in values ) def _ensure_read(self, fp, size): @@ -702,7 +733,7 @@ class ImageFileDirectory_v2(MutableMapping): if len(ret) != size: raise OSError( "Corrupt EXIF data. " - + "Expecting to read %d bytes but only got %d. " % (size, len(ret)) + f"Expecting to read {size} bytes but only got {len(ret)}. " ) return ret @@ -714,29 +745,21 @@ class ImageFileDirectory_v2(MutableMapping): try: for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]): tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12)) - if DEBUG: - tagname = TiffTags.lookup(tag).name - typname = TYPES.get(typ, "unknown") - print( - "tag: %s (%d) - type: %s (%d)" % (tagname, tag, typname, typ), - end=" ", - ) + + tagname = TiffTags.lookup(tag).name + typname = TYPES.get(typ, "unknown") + msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})" try: unit_size, handler = self._load_dispatch[typ] except KeyError: - if DEBUG: - print("- unsupported type", typ) + logger.debug(msg + f" - unsupported type {typ}") continue # ignore unsupported type size = count * unit_size if size > 4: here = fp.tell() (offset,) = self._unpack("L", data) - if DEBUG: - print( - "Tag Location: {} - Data Location: {}".format(here, offset), - end=" ", - ) + msg += f" Tag Location: {here} - Data Location: {offset}" fp.seek(offset) data = ImageFile._safe_read(fp, size) fp.seek(here) @@ -746,22 +769,23 @@ class ImageFileDirectory_v2(MutableMapping): if len(data) != size: warnings.warn( "Possibly corrupt EXIF data. " - "Expecting to read %d bytes but only got %d." - " Skipping tag %s" % (size, len(data), tag) + f"Expecting to read {size} bytes but only got {len(data)}." + f" Skipping tag {tag}" ) + logger.debug(msg) continue if not data: + logger.debug(msg) continue self._tagdata[tag] = data self.tagtype[tag] = typ - if DEBUG: - if size > 32: - print("- value: " % size) - else: - print("- value:", self[tag]) + msg += " - value: " + ( + "" % size if size > 32 else repr(data) + ) + logger.debug(msg) (self.next,) = self._unpack("L", self._ensure_read(fp, 4)) except OSError as msg: @@ -782,24 +806,33 @@ class ImageFileDirectory_v2(MutableMapping): if tag == STRIPOFFSETS: stripoffsets = len(entries) typ = self.tagtype.get(tag) - if DEBUG: - print("Tag {}, Type: {}, Value: {}".format(tag, typ, value)) - values = value if isinstance(value, tuple) else (value,) - data = self._write_dispatch[typ](self, *values) - if DEBUG: - tagname = TiffTags.lookup(tag).name - typname = TYPES.get(typ, "unknown") - print( - "save: %s (%d) - type: %s (%d)" % (tagname, tag, typname, typ), - end=" ", - ) - if len(data) >= 16: - print("- value: " % len(data)) + logger.debug(f"Tag {tag}, Type: {typ}, Value: {repr(value)}") + is_ifd = typ == TiffTags.LONG and isinstance(value, dict) + if is_ifd: + if self._endian == "<": + ifh = b"II\x2A\x00\x08\x00\x00\x00" else: - print("- value:", values) + ifh = b"MM\x00\x2A\x00\x00\x00\x08" + ifd = ImageFileDirectory_v2(ifh) + for ifd_tag, ifd_value in self._tags_v2[tag].items(): + ifd[ifd_tag] = ifd_value + data = ifd.tobytes(offset) + else: + values = value if isinstance(value, tuple) else (value,) + data = self._write_dispatch[typ](self, *values) + + tagname = TiffTags.lookup(tag).name + typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") + msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})" + msg += " - value: " + ( + "" % len(data) if len(data) >= 16 else str(values) + ) + logger.debug(msg) # count is sum of lengths for string and arbitrary data - if typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: + if is_ifd: + count = 1 + elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: count = len(data) else: count = len(values) @@ -820,8 +853,7 @@ class ImageFileDirectory_v2(MutableMapping): # pass 2: write entries to file for tag, typ, count, value, data in entries: - if DEBUG: - print(tag, typ, count, repr(value), repr(data)) + logger.debug(f"{tag} {typ} {count} {repr(value)} {repr(data)}") result += self._pack("HHL4s", tag, typ, count, value) # -- overwrite here for multi-page -- @@ -869,7 +901,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): ('Some Data',) Also contains a dictionary of tag types as read from the tiff image file, - `~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. + :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. Values are returned as a tuple. @@ -883,9 +915,13 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): tags = property(lambda self: self._tags_v1) tagdata = property(lambda self: self._tagdata) + # defined in ImageFileDirectory_v2 + tagtype: dict + """Dictionary of tag types""" + @classmethod def from_v2(cls, original): - """ Returns an + """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` instance with the same data as is contained in the original :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` @@ -902,7 +938,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): return ifd def to_v2(self): - """ Returns an + """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` instance with the same data as is contained in the original :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` @@ -958,17 +994,25 @@ class TiffImageFile(ImageFile.ImageFile): format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False + def __init__(self, fp=None, filename=None): + self.tag_v2 = None + """ Image file directory (tag dictionary) """ + + self.tag = None + """ Legacy tag entries """ + + super().__init__(fp, filename) + def _open(self): """Open the first image in a TIFF file""" # Header ifh = self.fp.read(8) - # image file directory (tag dictionary) self.tag_v2 = ImageFileDirectory_v2(ifh) - # legacy tag/ifd entries will be filled in later - self.tag = self.ifd = None + # legacy IFD entries will be filled in later + self.ifd = None # setup frame pointers self.__first = self.__next = self.tag_v2.next @@ -977,10 +1021,9 @@ class TiffImageFile(ImageFile.ImageFile): self._frame_pos = [] self._n_frames = None - if DEBUG: - print("*** TiffImageFile._open ***") - print("- __first:", self.__first) - print("- ifh: ", ifh) + logger.debug("*** TiffImageFile._open ***") + logger.debug(f"- __first: {self.__first}") + logger.debug(f"- ifh: {repr(ifh)}") # Use repr to avoid str(bytes) # and load the first frame self._seek(0) @@ -995,10 +1038,6 @@ class TiffImageFile(ImageFile.ImageFile): self.seek(current) return self._n_frames - @property - def is_animated(self): - return self._is_animated - def seek(self, frame): """Select a given frame as current image""" if not self._seek_check(frame): @@ -1015,24 +1054,22 @@ class TiffImageFile(ImageFile.ImageFile): while len(self._frame_pos) <= frame: if not self.__next: raise EOFError("no more images in TIFF file") - if DEBUG: - print( - "Seeking to frame %s, on frame %s, __next %s, location: %s" - % (frame, self.__frame, self.__next, self.fp.tell()) - ) + logger.debug( + f"Seeking to frame {frame}, on frame {self.__frame}, " + f"__next {self.__next}, location: {self.fp.tell()}" + ) # reset buffered io handle in case fp # was passed to libtiff, invalidating the buffer self.fp.tell() self.fp.seek(self.__next) self._frame_pos.append(self.__next) - if DEBUG: - print("Loading tags, location: %s" % self.fp.tell()) + logger.debug("Loading tags, location: %s" % self.fp.tell()) self.tag_v2.load(self.fp) self.__next = self.tag_v2.next if self.__next == 0: self._n_frames = frame + 1 if len(self._frame_pos) == 1: - self._is_animated = self.__next != 0 + self.is_animated = self.__next != 0 self.__frame += 1 self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) @@ -1046,7 +1083,7 @@ class TiffImageFile(ImageFile.ImageFile): return self.__frame def load(self): - if self.use_load_libtiff: + if self.tile and self.use_load_libtiff: return self._load_libtiff() return super().load() @@ -1067,19 +1104,14 @@ class TiffImageFile(ImageFile.ImageFile): # allow closing if we're on the first frame, there's no next # This is the ImageFile.load path only, libtiff specific below. - if not self._is_animated: + if not self.is_animated: self._close_exclusive_fp_after_loading = True def _load_libtiff(self): - """ Overload method triggered when we detect a compressed tiff - Calls out to libtiff """ + """Overload method triggered when we detect a compressed tiff + Calls out to libtiff""" - pixel = Image.Image.load(self) - - if self.tile is None: - raise OSError("cannot load this image") - if not self.tile: - return pixel + Image.Image.load(self) self.load_prepare() @@ -1103,7 +1135,7 @@ class TiffImageFile(ImageFile.ImageFile): if hasattr(self.fp, "flush"): self.fp.flush() except OSError: - # io.BytesIO have a fileno, but returns an IOError if + # io.BytesIO have a fileno, but returns an OSError if # it doesn't use a file descriptor. fp = False @@ -1115,10 +1147,10 @@ class TiffImageFile(ImageFile.ImageFile): ) try: decoder.setimage(self.im, extents) - except ValueError: - raise OSError("Couldn't set the image") + except ValueError as e: + raise OSError("Couldn't set the image") from e - close_self_fp = self._exclusive_fp and not self._is_animated + close_self_fp = self._exclusive_fp and not self.is_animated if hasattr(self.fp, "getvalue"): # We've got a stringio like thing passed in. Yay for all in memory. # The decoder needs the entire file in one shot, so there's not @@ -1127,23 +1159,20 @@ class TiffImageFile(ImageFile.ImageFile): # underlying string for stringio. # # Rearranging for supporting byteio items, since they have a fileno - # that returns an IOError if there's no underlying fp. Easier to + # that returns an OSError if there's no underlying fp. Easier to # deal with here by reordering. - if DEBUG: - print("have getvalue. just sending in a string from getvalue") + logger.debug("have getvalue. just sending in a string from getvalue") n, err = decoder.decode(self.fp.getvalue()) elif fp: # we've got a actual file on disk, pass in the fp. - if DEBUG: - print("have fileno, calling fileno version of the decoder.") + logger.debug("have fileno, calling fileno version of the decoder.") if not close_self_fp: self.fp.seek(0) # 4 bytes, otherwise the trace might error out n, err = decoder.decode(b"fpfp") else: # we have something else. - if DEBUG: - print("don't have fileno or getvalue. just reading") + logger.debug("don't have fileno or getvalue. just reading") self.fp.seek(0) # UNDONE -- so much for that buffer size thing. n, err = decoder.decode(self.fp.read()) @@ -1183,21 +1212,19 @@ class TiffImageFile(ImageFile.ImageFile): fillorder = self.tag_v2.get(FILLORDER, 1) - if DEBUG: - print("*** Summary ***") - print("- compression:", self._compression) - print("- photometric_interpretation:", photo) - print("- planar_configuration:", self._planar_configuration) - print("- fill_order:", fillorder) - print("- YCbCr subsampling:", self.tag.get(530)) + logger.debug("*** Summary ***") + logger.debug(f"- compression: {self._compression}") + logger.debug(f"- photometric_interpretation: {photo}") + logger.debug(f"- planar_configuration: {self._planar_configuration}") + logger.debug(f"- fill_order: {fillorder}") + logger.debug(f"- YCbCr subsampling: {self.tag.get(530)}") # size xsize = int(self.tag_v2.get(IMAGEWIDTH)) ysize = int(self.tag_v2.get(IMAGELENGTH)) self._size = xsize, ysize - if DEBUG: - print("- size:", self.size) + logger.debug(f"- size: {self.size}") sampleFormat = self.tag_v2.get(SAMPLEFORMAT, (1,)) if len(sampleFormat) > 1 and max(sampleFormat) == min(sampleFormat) == 1: @@ -1231,18 +1258,15 @@ class TiffImageFile(ImageFile.ImageFile): bps_tuple, extra_tuple, ) - if DEBUG: - print("format key:", key) + logger.debug(f"format key: {key}") try: self.mode, rawmode = OPEN_INFO[key] - except KeyError: - if DEBUG: - print("- unsupported format") - raise SyntaxError("unknown pixel mode") + except KeyError as e: + logger.debug("- unsupported format") + raise SyntaxError("unknown pixel mode") from e - if DEBUG: - print("- raw mode:", rawmode) - print("- pil mode:", self.mode) + logger.debug(f"- raw mode: {rawmode}") + logger.debug(f"- pil mode: {self.mode}") self.info["compression"] = self._compression @@ -1283,8 +1307,7 @@ class TiffImageFile(ImageFile.ImageFile): if fillorder == 2: # Replace fillorder with fillorder=1 key = key[:3] + (1,) + key[4:] - if DEBUG: - print("format key:", key) + logger.debug(f"format key: {key}") # this should always work, since all the # fillorder==2 modes have a corresponding # fillorder=1 mode @@ -1346,8 +1369,7 @@ class TiffImageFile(ImageFile.ImageFile): x = y = 0 layer += 1 else: - if DEBUG: - print("- unsupported data organization") + logger.debug("- unsupported data organization") raise SyntaxError("unknown data organization") # Fix up info. @@ -1408,14 +1430,17 @@ def _save(im, fp, filename): try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] - except KeyError: - raise OSError("cannot write mode %s as TIFF" % im.mode) + except KeyError as e: + raise OSError(f"cannot write mode {im.mode} as TIFF") from e ifd = ImageFileDirectory_v2(prefix=prefix) compression = im.encoderinfo.get("compression", im.info.get("compression")) if compression is None: compression = "raw" + elif compression == "tiff_jpeg": + # OJPEG is obsolete, so use new-style JPEG compression instead + compression = "jpeg" libtiff = WRITE_LIBTIFF or compression != "raw" @@ -1427,8 +1452,7 @@ def _save(im, fp, filename): # write any arbitrary tags passed in as an ImageFileDirectory info = im.encoderinfo.get("tiffinfo", {}) - if DEBUG: - print("Tiffinfo Keys: %s" % list(info)) + logger.debug("Tiffinfo Keys: %s" % list(info)) if isinstance(info, ImageFileDirectory_v1): info = info.to_v2() for key in info: @@ -1497,7 +1521,10 @@ def _save(im, fp, filename): # data orientation stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) ifd[ROWSPERSTRIP] = im.size[1] - ifd[STRIPBYTECOUNTS] = stride * im.size[1] + strip_byte_counts = stride * im.size[1] + if strip_byte_counts >= 2 ** 16: + ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG + ifd[STRIPBYTECOUNTS] = strip_byte_counts ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer # no compression by default: ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) @@ -1513,9 +1540,8 @@ def _save(im, fp, filename): ) ifd[JPEGQUALITY] = quality - if DEBUG: - print("Saving using libtiff encoder") - print("Items: %s" % sorted(ifd.items())) + logger.debug("Saving using libtiff encoder") + logger.debug("Items: %s" % sorted(ifd.items())) _fp = 0 if hasattr(fp, "fileno"): try: @@ -1534,7 +1560,6 @@ def _save(im, fp, filename): # BITSPERSAMPLE, etc), passing arrays with a different length will result in # segfaults. Block these tags until we add extra validation. blocklist = [ - COLORMAP, REFERENCEBLACKWHITE, SAMPLEFORMAT, STRIPBYTECOUNTS, @@ -1559,17 +1584,17 @@ def _save(im, fp, filename): # Custom items are supported for int, float, unicode, string and byte # values. Other types and tuples require a tagtype. if tag not in TiffTags.LIBTIFF_CORE: - if TiffTags.lookup(tag).type == TiffTags.UNDEFINED: - continue - if distutils.version.StrictVersion( - _libtiff_version() - ) < distutils.version.StrictVersion("4.0"): + if not Image.core.libtiff_support_custom_tags: continue if tag in ifd.tagtype: types[tag] = ifd.tagtype[tag] elif not (isinstance(value, (int, float, str, bytes))): continue + else: + type = TiffTags.lookup(tag).type + if type: + types[tag] = type if tag not in atts and tag not in blocklist: if isinstance(value, str): atts[tag] = value.encode("ascii", "replace") + b"\0" @@ -1578,8 +1603,7 @@ def _save(im, fp, filename): else: atts[tag] = value - if DEBUG: - print("Converted items: %s" % sorted(atts.items())) + logger.debug("Converted items: %s" % sorted(atts.items())) # libtiff always expects the bytes in native order. # we're storing image byte order. So, if the rawmode @@ -1604,7 +1628,7 @@ def _save(im, fp, filename): if s: break if s < 0: - raise OSError("encoder error %d when writing image file" % s) + raise OSError(f"encoder error {s} when writing image file") else: offset = ifd.save(fp) @@ -1770,29 +1794,29 @@ class AppendingTiffWriter: self.f.seek(-2, os.SEEK_CUR) bytesWritten = self.f.write(struct.pack(self.longFmt, value)) if bytesWritten is not None and bytesWritten != 4: - raise RuntimeError("wrote only %u bytes but wanted 4" % bytesWritten) + raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 4") def rewriteLastShort(self, value): self.f.seek(-2, os.SEEK_CUR) bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) if bytesWritten is not None and bytesWritten != 2: - raise RuntimeError("wrote only %u bytes but wanted 2" % bytesWritten) + raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 2") def rewriteLastLong(self, value): self.f.seek(-4, os.SEEK_CUR) bytesWritten = self.f.write(struct.pack(self.longFmt, value)) if bytesWritten is not None and bytesWritten != 4: - raise RuntimeError("wrote only %u bytes but wanted 4" % bytesWritten) + raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 4") def writeShort(self, value): bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) if bytesWritten is not None and bytesWritten != 2: - raise RuntimeError("wrote only %u bytes but wanted 2" % bytesWritten) + raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 2") def writeLong(self, value): bytesWritten = self.f.write(struct.pack(self.longFmt, value)) if bytesWritten is not None and bytesWritten != 4: - raise RuntimeError("wrote only %u bytes but wanted 4" % bytesWritten) + raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 4") def close(self): self.finalize() diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 6cc9ff7f3..796ff3479 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -69,6 +69,7 @@ SIGNED_LONG = 9 SIGNED_RATIONAL = 10 FLOAT = 11 DOUBLE = 12 +IFD = 13 TAGS_V2 = { 254: ("NewSubfileType", LONG, 1), @@ -483,7 +484,6 @@ LIBTIFF_CORE = { 65537, } -LIBTIFF_CORE.remove(320) # Array of short, crashes LIBTIFF_CORE.remove(301) # Array of short, crashes LIBTIFF_CORE.remove(532) # Array of long, crashes diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index d5a5c8e67..b578d6981 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -12,13 +12,16 @@ # See the README file for information on usage and redistribution. # -# NOTE: This format cannot be automatically recognized, so the reader -# is not registered for use with Image.open(). To open a WAL file, use -# the WalImageFile.open() function instead. +""" +This reader is based on the specification available from: +https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml +and has been tested with a few sample files found using google. -# This reader is based on the specification available from: -# https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml -# and has been tested with a few sample files found using google. +.. note:: + This format cannot be automatically recognized, so the reader + is not registered for use with :py:func:`PIL.Image.open()`. + To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead. +""" import builtins @@ -31,7 +34,7 @@ def open(filename): Load texture from a Quake2 WAL texture file. By default, a Quake2 standard palette is attached to the texture. - To override the palette, use the putpalette method. + To override the palette, use the :py:func:`PIL.Image.Image.putpalette()` method. :param filename: WAL file name, or an opened file handle. :returns: An image instance. diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index eda685508..2e9746fa3 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -38,6 +38,8 @@ class WebPImageFile(ImageFile.ImageFile): format = "WEBP" format_description = "WebP image" + __loaded = 0 + __logical_frame = 0 def _open(self): if not _webp.HAVE_WEBPANIM: @@ -52,7 +54,8 @@ class WebPImageFile(ImageFile.ImageFile): self._size = width, height self.fp = BytesIO(data) self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] - self._n_frames = 1 + self.n_frames = 1 + self.is_animated = False return # Use the newer AnimDecoder API to parse the (possibly) animated file, @@ -70,7 +73,8 @@ class WebPImageFile(ImageFile.ImageFile): bgcolor & 0xFF, ) self.info["background"] = (bg_r, bg_g, bg_b, bg_a) - self._n_frames = frame_count + self.n_frames = frame_count + self.is_animated = self.n_frames > 1 self.mode = "RGB" if mode == "RGBX" else mode self.rawmode = mode self.tile = [] @@ -88,30 +92,15 @@ class WebPImageFile(ImageFile.ImageFile): # Initialize seek state self._reset(reset=False) - self.seek(0) def _getexif(self): if "exif" not in self.info: return None return dict(self.getexif()) - @property - def n_frames(self): - return self._n_frames - - @property - def is_animated(self): - return self._n_frames > 1 - def seek(self, frame): - if not _webp.HAVE_WEBPANIM: - return super().seek(frame) - - # Perform some simple checks first - if frame >= self._n_frames: - raise EOFError("attempted to seek beyond end of sequence") - if frame < 0: - raise EOFError("negative frame index is not valid") + if not self._seek_check(frame): + return # Set logical frame to requested position self.__logical_frame = frame @@ -325,6 +314,7 @@ def _save(im, fp, filename): if isinstance(exif, Image.Exif): exif = exif.tobytes() xmp = im.encoderinfo.get("xmp", "") + method = im.encoderinfo.get("method", 0) if im.mode not in _VALID_WEBP_LEGACY_MODES: alpha = ( @@ -342,6 +332,7 @@ def _save(im, fp, filename): float(quality), im.mode, icc_profile, + method, exif, xmp, ) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 7695a68e7..87847a107 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -20,7 +20,10 @@ # http://wvware.sourceforge.net/caolan/ora-wmf.html from . import Image, ImageFile -from ._binary import i16le as word, i32le as dword, si16le as short, si32le as _long +from ._binary import i16le as word +from ._binary import i32le as dword +from ._binary import si16le as short +from ._binary import si32le as _long _handler = None @@ -78,6 +81,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): format_description = "Windows Metafile" def _open(self): + self._inch = None # check placable header s = self.fp.read(80) @@ -87,7 +91,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): # placeable windows metafile # get units per inch - inch = word(s, 14) + self._inch = word(s, 14) # get bounding box x0 = short(s, 6) @@ -96,12 +100,14 @@ class WmfStubImageFile(ImageFile.StubImageFile): y1 = short(s, 12) # normalize size to 72 dots per inch - size = (x1 - x0) * 72 // inch, (y1 - y0) * 72 // inch + self.info["dpi"] = 72 + size = ( + (x1 - x0) * self.info["dpi"] // self._inch, + (y1 - y0) * self.info["dpi"] // self._inch, + ) self.info["wmf_bbox"] = x0, y0, x1, y1 - self.info["dpi"] = 72 - # sanity check (standard metafile header) if s[22:26] != b"\x01\x00\t\x00": raise SyntaxError("Unsupported WMF file format") @@ -118,7 +124,6 @@ class WmfStubImageFile(ImageFile.StubImageFile): # get frame (in 0.01 millimeter units) frame = _long(s, 24), _long(s, 28), _long(s, 32), _long(s, 36) - # normalize size to 72 dots per inch size = x1 - x0, y1 - y0 # calculate dots per inch from bbox and frame @@ -145,6 +150,16 @@ class WmfStubImageFile(ImageFile.StubImageFile): def _load(self): return _handler + def load(self, dpi=None): + if dpi is not None and self._inch is not None: + self.info["dpi"] = int(dpi + 0.5) + x0, y0, x1, y1 = self.info["wmf_bbox"] + self._size = ( + (x1 - x0) * self.info["dpi"] // self._inch, + (y1 - y0) * self.info["dpi"] // self._inch, + ) + super().load() + def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index ead9722c8..644cfb39b 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -69,15 +69,15 @@ class XbmImageFile(ImageFile.ImageFile): def _save(im, fp, filename): if im.mode != "1": - raise OSError("cannot write mode %s as XBM" % im.mode) + raise OSError(f"cannot write mode {im.mode} as XBM") - fp.write(("#define im_width %d\n" % im.size[0]).encode("ascii")) - fp.write(("#define im_height %d\n" % im.size[1]).encode("ascii")) + fp.write(f"#define im_width {im.size[0]}\n".encode("ascii")) + fp.write(f"#define im_height {im.size[1]}\n".encode("ascii")) hotspot = im.encoderinfo.get("hotspot") if hotspot: - fp.write(("#define im_x_hot %d\n" % hotspot[0]).encode("ascii")) - fp.write(("#define im_y_hot %d\n" % hotspot[1]).encode("ascii")) + fp.write(f"#define im_x_hot {hotspot[0]}\n".encode("ascii")) + fp.write(f"#define im_y_hot {hotspot[1]}\n".encode("ascii")) fp.write(b"static char im_bits[] = {\n") diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index e7f26488d..d4f5ea76a 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -13,13 +13,72 @@ Use PIL.__version__ for this Pillow version. ;-) """ +import sys +import warnings + from . import _version # VERSION was removed in Pillow 6.0.0. -# PILLOW_VERSION was removed in Pillow 7.0.0. -# Use __version__ instead. __version__ = _version.__version__ + +# PILLOW_VERSION is deprecated and will be removed in a future release. +# Use __version__ instead. +def _raise_version_warning(): + warnings.warn( + "PILLOW_VERSION is deprecated and will be removed in a future release. " + "Use __version__ instead.", + DeprecationWarning, + stacklevel=3, + ) + + +if sys.version_info >= (3, 7): + + def __getattr__(name): + if name == "PILLOW_VERSION": + _raise_version_warning() + return __version__ + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +else: + + class _Deprecated_Version(str): + def __str__(self): + _raise_version_warning() + return super().__str__() + + def __getitem__(self, key): + _raise_version_warning() + return super().__getitem__(key) + + def __eq__(self, other): + _raise_version_warning() + return super().__eq__(other) + + def __ne__(self, other): + _raise_version_warning() + return super().__ne__(other) + + def __gt__(self, other): + _raise_version_warning() + return super().__gt__(other) + + def __lt__(self, other): + _raise_version_warning() + return super().__lt__(other) + + def __ge__(self, other): + _raise_version_warning() + return super().__gt__(other) + + def __le__(self, other): + _raise_version_warning() + return super().__lt__(other) + + PILLOW_VERSION = _Deprecated_Version(__version__) + del _version @@ -72,5 +131,9 @@ _plugins = [ ] -class UnidentifiedImageError(IOError): +class UnidentifiedImageError(OSError): + """ + Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified. + """ + pass diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 529b8c94b..5564f450d 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -11,6 +11,10 @@ # See the README file for information on usage and redistribution. # + +"""Binary input/output support routines.""" + + from struct import pack, unpack_from diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 30493066a..7018a1b79 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -4,13 +4,6 @@ import sys from tkinter import _tkinter as tk if hasattr(sys, "pypy_find_executable"): - # Tested with packages at https://bitbucket.org/pypy/pypy/downloads. - # PyPies 1.6, 2.0 do not have tkinter built in. PyPy3-2.3.1 gives an - # OSError trying to import tkinter. Otherwise: - try: # PyPy 5.1, 4.0.0, 2.6.1, 2.6.0 - TKINTER_LIB = tk.tklib_cffi.__file__ - except AttributeError: - # PyPy3 2.4, 2.1-beta1; PyPy 2.5.1, 2.5.0, 2.4.0, 2.3, 2.2, 2.1 - TKINTER_LIB = tk.tkffi.verifier.modulefilename + TKINTER_LIB = tk.tklib_cffi.__file__ else: TKINTER_LIB = tk.__file__ diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 755b4b272..0c5d3892e 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,20 +1,9 @@ import os -import sys - -py36 = sys.version_info[0:2] >= (3, 6) +from pathlib import Path -if py36: - from pathlib import Path - - def isPath(f): - return isinstance(f, (bytes, str, Path)) - - -else: - - def isPath(f): - return isinstance(f, (bytes, str)) +def isPath(f): + return isinstance(f, (bytes, str, Path)) # Checks if an object is a string, and that it points to a directory. diff --git a/src/PIL/_version.py b/src/PIL/_version.py index eddf15683..4fcb50190 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "7.0.0.dev0" +__version__ = "8.1.0.dev0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 5822febab..da0ca557c 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -1,25 +1,33 @@ import collections import os import sys +import warnings import PIL from . import Image modules = { - "pil": "PIL._imaging", - "tkinter": "PIL._tkinter_finder", - "freetype2": "PIL._imagingft", - "littlecms2": "PIL._imagingcms", - "webp": "PIL._webp", + "pil": ("PIL._imaging", "PILLOW_VERSION"), + "tkinter": ("PIL._tkinter_finder", None), + "freetype2": ("PIL._imagingft", "freetype2_version"), + "littlecms2": ("PIL._imagingcms", "littlecms_version"), + "webp": ("PIL._webp", "webpdecoder_version"), } def check_module(feature): - if not (feature in modules): - raise ValueError("Unknown module %s" % feature) + """ + Checks if a module is available. - module = modules[feature] + :param feature: The module to check for. + :returns: ``True`` if available, ``False`` otherwise. + :raises ValueError: If the module is not defined in this version of Pillow. + """ + if not (feature in modules): + raise ValueError(f"Unknown module {feature}") + + module, ver = modules[feature] try: __import__(module) @@ -28,41 +36,106 @@ def check_module(feature): return False +def version_module(feature): + """ + :param feature: The module to check for. + :returns: + The loaded version number as a string, or ``None`` if unknown or not available. + :raises ValueError: If the module is not defined in this version of Pillow. + """ + if not check_module(feature): + return None + + module, ver = modules[feature] + + if ver is None: + return None + + return getattr(__import__(module, fromlist=[ver]), ver) + + def get_supported_modules(): + """ + :returns: A list of all supported modules. + """ return [f for f in modules if check_module(f)] -codecs = {"jpg": "jpeg", "jpg_2000": "jpeg2k", "zlib": "zip", "libtiff": "libtiff"} +codecs = { + "jpg": ("jpeg", "jpeglib"), + "jpg_2000": ("jpeg2k", "jp2klib"), + "zlib": ("zip", "zlib"), + "libtiff": ("libtiff", "libtiff"), +} def check_codec(feature): - if feature not in codecs: - raise ValueError("Unknown codec %s" % feature) + """ + Checks if a codec is available. - codec = codecs[feature] + :param feature: The codec to check for. + :returns: ``True`` if available, ``False`` otherwise. + :raises ValueError: If the codec is not defined in this version of Pillow. + """ + if feature not in codecs: + raise ValueError(f"Unknown codec {feature}") + + codec, lib = codecs[feature] return codec + "_encoder" in dir(Image.core) +def version_codec(feature): + """ + :param feature: The codec to check for. + :returns: + The version number as a string, or ``None`` if not available. + Checked at compile time for ``jpg``, run-time otherwise. + :raises ValueError: If the codec is not defined in this version of Pillow. + """ + if not check_codec(feature): + return None + + codec, lib = codecs[feature] + + version = getattr(Image.core, lib + "_version") + + if feature == "libtiff": + return version.split("\n")[0].split("Version ")[1] + + return version + + def get_supported_codecs(): + """ + :returns: A list of all supported codecs. + """ return [f for f in codecs if check_codec(f)] features = { - "webp_anim": ("PIL._webp", "HAVE_WEBPANIM"), - "webp_mux": ("PIL._webp", "HAVE_WEBPMUX"), - "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), - "raqm": ("PIL._imagingft", "HAVE_RAQM"), - "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO"), - "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT"), + "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), + "webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None), + "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), + "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), + "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), + "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), + "xcb": ("PIL._imaging", "HAVE_XCB", None), } def check_feature(feature): - if feature not in features: - raise ValueError("Unknown feature %s" % feature) + """ + Checks if a feature is available. - module, flag = features[feature] + :param feature: The feature to check for. + :returns: ``True`` if available, ``False`` if unavailable, ``None`` if unknown. + :raises ValueError: If the feature is not defined in this version of Pillow. + """ + if feature not in features: + raise ValueError(f"Unknown feature {feature}") + + module, flag, ver = features[feature] try: imported_module = __import__(module, fromlist=["PIL"]) @@ -71,22 +144,69 @@ def check_feature(feature): return None +def version_feature(feature): + """ + :param feature: The feature to check for. + :returns: The version number as a string, or ``None`` if not available. + :raises ValueError: If the feature is not defined in this version of Pillow. + """ + if not check_feature(feature): + return None + + module, flag, ver = features[feature] + + if ver is None: + return None + + return getattr(__import__(module, fromlist=[ver]), ver) + + def get_supported_features(): + """ + :returns: A list of all supported features. + """ return [f for f in features if check_feature(f)] def check(feature): - return ( - feature in modules - and check_module(feature) - or feature in codecs - and check_codec(feature) - or feature in features - and check_feature(feature) - ) + """ + :param feature: A module, codec, or feature name. + :returns: + ``True`` if the module, codec, or feature is available, + ``False`` or ``None`` otherwise. + """ + + if feature in modules: + return check_module(feature) + if feature in codecs: + return check_codec(feature) + if feature in features: + return check_feature(feature) + warnings.warn(f"Unknown feature '{feature}'.", stacklevel=2) + return False + + +def version(feature): + """ + :param feature: + The module, codec, or feature to check for. + :returns: + The version number as a string, or ``None`` if unknown or not available. + """ + if feature in modules: + return version_module(feature) + if feature in codecs: + return version_codec(feature) + if feature in features: + return version_feature(feature) + return None def get_supported(): + """ + :returns: A list of all supported modules, features, and codecs. + """ + ret = get_supported_modules() ret.extend(get_supported_features()) ret.extend(get_supported_codecs()) @@ -94,24 +214,34 @@ def get_supported(): def pilinfo(out=None, supported_formats=True): + """ + Prints information about this installation of Pillow. + This function can be called with ``python -m PIL``. + + :param out: + The output stream to print to. Defaults to ``sys.stdout`` if ``None``. + :param supported_formats: + If ``True``, a list of all supported image file formats will be printed. + """ + if out is None: out = sys.stdout Image.init() print("-" * 68, file=out) - print("Pillow {}".format(PIL.__version__), file=out) + print(f"Pillow {PIL.__version__}", file=out) py_version = sys.version.splitlines() - print("Python {}".format(py_version[0].strip()), file=out) + print(f"Python {py_version[0].strip()}", file=out) for py_version in py_version[1:]: - print(" {}".format(py_version.strip()), file=out) + print(f" {py_version.strip()}", file=out) print("-" * 68, file=out) print( - "Python modules loaded from {}".format(os.path.dirname(Image.__file__)), + f"Python modules loaded from {os.path.dirname(Image.__file__)}", file=out, ) print( - "Binary modules loaded from {}".format(os.path.dirname(Image.core.__file__)), + f"Binary modules loaded from {os.path.dirname(Image.core.__file__)}", file=out, ) print("-" * 68, file=out) @@ -131,9 +261,22 @@ def pilinfo(out=None, supported_formats=True): ("libtiff", "LIBTIFF"), ("raqm", "RAQM (Bidirectional Text)"), ("libimagequant", "LIBIMAGEQUANT (Quantization method)"), + ("xcb", "XCB (X protocol)"), ]: if check(name): - print("---", feature, "support ok", file=out) + if name == "jpg" and check_feature("libjpeg_turbo"): + v = "libjpeg-turbo " + version_feature("libjpeg_turbo") + else: + v = version(name) + if v is not None: + version_static = name in ("pil", "jpg") + if name == "littlecms2": + # this check is also in src/_imagingcms.c:setup_module() + version_static = tuple(int(x) for x in v.split(".")) < (2, 7) + t = "compiled for" if version_static else "loaded" + print("---", feature, "support ok,", t, v, file=out) + else: + print("---", feature, "support ok", file=out) else: print("***", feature, "support not installed", file=out) print("-" * 68, file=out) @@ -144,9 +287,9 @@ def pilinfo(out=None, supported_formats=True): extensions[i].append(ext) for i in sorted(Image.ID): - line = "{}".format(i) + line = f"{i}" if i in Image.MIME: - line = "{} {}".format(line, Image.MIME[i]) + line = f"{line} {Image.MIME[i]}" print(line, file=out) if i in extensions: diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 59801f58e..5df3abb72 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -39,7 +39,7 @@ * See the README file for information on usage and redistribution. */ -#include "Imaging.h" +#include "../libImaging/Imaging.h" #include "_tkmini.h" #include @@ -63,13 +63,14 @@ ImagingFind(const char* name) Py_ssize_t id; /* FIXME: use CObject instead? */ -#if defined(_MSC_VER) && defined(_WIN64) +#if defined(_WIN64) id = _atoi64(name); #else id = atol(name); #endif - if (!id) + if (!id) { return NULL; + } return (Imaging) id; } @@ -113,16 +114,17 @@ PyImagingPhotoPut(ClientData clientdata, Tcl_Interp* interp, if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { block.pixelSize = 1; - block.offset[0] = block.offset[1] = block.offset[2] = 0; + block.offset[0] = block.offset[1] = block.offset[2] = block.offset[3] = 0; } else if (strncmp(im->mode, "RGB", 3) == 0) { block.pixelSize = 4; block.offset[0] = 0; block.offset[1] = 1; block.offset[2] = 2; - if (strcmp(im->mode, "RGBA") == 0) + if (strcmp(im->mode, "RGBA") == 0) { block.offset[3] = 3; /* alpha (or reserved, under 8.2) */ - else + } else { block.offset[3] = 0; /* no alpha */ + } } else { TCL_APPEND_RESULT(interp, "Bad mode", (char*) NULL); return TCL_ERROR; @@ -136,10 +138,11 @@ PyImagingPhotoPut(ClientData clientdata, Tcl_Interp* interp, if (TK_LT_85) { /* Tk 8.4 */ TK_PHOTO_PUT_BLOCK_84(photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); - if (strcmp(im->mode, "RGBA") == 0) + if (strcmp(im->mode, "RGBA") == 0) { /* Tk workaround: we need apply ToggleComplexAlphaIfNeeded */ /* (fixed in Tk 8.5a3) */ TK_PHOTO_SET_SIZE_84(photo, block.width, block.height); + } } else { /* Tk >=8.5 */ TK_PHOTO_PUT_BLOCK_85(interp, photo, &block, 0, 0, block.width, diff --git a/src/_imaging.c b/src/_imaging.c index b0cbfc665..3635d2afa 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -82,7 +82,13 @@ #include "zlib.h" #endif -#include "Imaging.h" +#ifdef HAVE_LIBTIFF +#ifndef _TIFFIO_ +#include +#endif +#endif + +#include "libImaging/Imaging.h" #define _USE_MATH_DEFINES #include @@ -164,8 +170,9 @@ PyImagingNew(Imaging imOut) { ImagingObject* imagep; - if (!imOut) + if (!imOut) { return NULL; + } imagep = PyObject_New(ImagingObject, &Imaging_Type); if (imagep == NULL) { @@ -191,8 +198,9 @@ _dealloc(ImagingObject* imagep) printf("imaging %p deleted\n", imagep); #endif - if (imagep->access) + if (imagep->access) { ImagingAccessDelete(imagep->image, imagep->access); + } ImagingDelete(imagep->image); PyObject_Del(imagep); } @@ -262,9 +270,9 @@ static const char* readonly = "image is readonly"; /* static const char* no_content = "image has no content"; */ void * -ImagingError_IOError(void) +ImagingError_OSError(void) { - PyErr_SetString(PyExc_IOError, "error when accessing file"); + PyErr_SetString(PyExc_OSError, "error when accessing file"); return NULL; } @@ -316,8 +324,9 @@ getbands(const char* mode) /* FIXME: add primitive to libImaging to avoid extra allocation */ im = ImagingNew(mode, 0, 0); - if (!im) + if (!im) { return -1; + } bands = im->bands; @@ -367,8 +376,9 @@ getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type) /* malloc check ok, type & ff is just a sizeof(something) calloc checks for overflow */ list = calloc(n, type & 0xff); - if ( ! list) + if ( ! list) { return PyErr_NoMemory(); + } seq = PySequence_Fast(arg, must_be_sequence); if ( ! seq) { @@ -407,8 +417,9 @@ getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type) return NULL; } - if (length) + if (length) { *length = n; + } return list; } @@ -479,8 +490,9 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) case IMAGING_TYPE_FLOAT32: return PyFloat_FromDouble(pixel.f); case IMAGING_TYPE_SPECIAL: - if (strncmp(im->mode, "I;16", 4) == 0) + if (strncmp(im->mode, "I;16", 4) == 0) { return PyLong_FromLong(pixel.h); + } break; } @@ -506,15 +518,26 @@ getink(PyObject* color, Imaging im, char* ink) be cast to either UINT8 or INT32 */ int rIsInt = 0; + if (PyTuple_Check(color) && PyTuple_Size(color) == 1) { + color = PyTuple_GetItem(color, 0); + } if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || im->type == IMAGING_TYPE_SPECIAL) { - if (PyLong_Check(color)) { - r = PyLong_AsLongLong(color); + if (PyLong_Check(color)) { + r = PyLong_AsLongLong(color); + if (r == -1 && PyErr_Occurred()) { + return NULL; + } rIsInt = 1; - } - if (r == -1 && PyErr_Occurred()) { - rIsInt = 0; + } else if (im->type == IMAGING_TYPE_UINT8) { + if (!PyTuple_Check(color)) { + PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); + return NULL; + } + } else { + PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple"); + return NULL; } } @@ -540,12 +563,14 @@ getink(PyObject* color, Imaging im, char* ink) r = (UINT8) r; } else { if (im->bands == 2) { - if (!PyArg_ParseTuple(color, "L|i", &r, &a)) + if (!PyArg_ParseTuple(color, "L|i", &r, &a)) { return NULL; + } g = b = r; } else { - if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) + if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) { return NULL; + } } } ink[0] = (char) CLIP8(r); @@ -556,23 +581,20 @@ getink(PyObject* color, Imaging im, char* ink) return ink; case IMAGING_TYPE_INT32: /* signed integer */ - if (rIsInt != 1) - return NULL; itmp = r; memcpy(ink, &itmp, sizeof(itmp)); return ink; case IMAGING_TYPE_FLOAT32: /* floating point */ f = PyFloat_AsDouble(color); - if (f == -1.0 && PyErr_Occurred()) + if (f == -1.0 && PyErr_Occurred()) { return NULL; + } ftmp = f; memcpy(ink, &ftmp, sizeof(ftmp)); return ink; case IMAGING_TYPE_SPECIAL: if (strncmp(im->mode, "I;16", 4) == 0) { - if (rIsInt != 1) - return NULL; ink[0] = (UINT8) r; ink[1] = (UINT8) (r >> 8); ink[2] = ink[3] = 0; @@ -600,12 +622,14 @@ _fill(PyObject* self, PyObject* args) xsize = ysize = 256; color = NULL; - if (!PyArg_ParseTuple(args, "s|(ii)O", &mode, &xsize, &ysize, &color)) + if (!PyArg_ParseTuple(args, "s|(ii)O", &mode, &xsize, &ysize, &color)) { return NULL; + } im = ImagingNewDirty(mode, xsize, ysize); - if (!im) + if (!im) { return NULL; + } buffer[0] = buffer[1] = buffer[2] = buffer[3] = 0; if (color) { @@ -627,8 +651,9 @@ _new(PyObject* self, PyObject* args) char* mode; int xsize, ysize; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) + if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { return NULL; + } return PyImagingNew(ImagingNew(mode, xsize, ysize)); } @@ -639,8 +664,9 @@ _new_block(PyObject* self, PyObject* args) char* mode; int xsize, ysize; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) + if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { return NULL; + } return PyImagingNew(ImagingNewBlock(mode, xsize, ysize)); } @@ -650,8 +676,9 @@ _linear_gradient(PyObject* self, PyObject* args) { char* mode; - if (!PyArg_ParseTuple(args, "s", &mode)) + if (!PyArg_ParseTuple(args, "s", &mode)) { return NULL; + } return PyImagingNew(ImagingFillLinearGradient(mode)); } @@ -661,8 +688,9 @@ _radial_gradient(PyObject* self, PyObject* args) { char* mode; - if (!PyArg_ParseTuple(args, "s", &mode)) + if (!PyArg_ParseTuple(args, "s", &mode)) { return NULL; + } return PyImagingNew(ImagingFillRadialGradient(mode)); } @@ -675,8 +703,9 @@ _alpha_composite(ImagingObject* self, PyObject* args) if (!PyArg_ParseTuple(args, "O!O!", &Imaging_Type, &imagep1, - &Imaging_Type, &imagep2)) + &Imaging_Type, &imagep2)) { return NULL; + } return PyImagingNew(ImagingAlphaComposite(imagep1->image, imagep2->image)); } @@ -692,8 +721,9 @@ _blend(ImagingObject* self, PyObject* args) if (!PyArg_ParseTuple(args, "O!O!|d", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2, - &alpha)) + &alpha)) { return NULL; + } return PyImagingNew(ImagingBlend(imagep1->image, imagep2->image, (float) alpha)); @@ -756,8 +786,9 @@ _prepare_lut_table(PyObject* table, Py_ssize_t table_size) /* malloc check ok, max is 2 * 4 * 65**3 = 2197000 */ prepared = (INT16*) malloc(sizeof(INT16) * table_size); if ( ! prepared) { - if (free_table_data) + if (free_table_data) { free(table_data); + } return (INT16*) PyErr_NoMemory(); } @@ -874,8 +905,9 @@ _convert(ImagingObject* self, PyObject* args) int dither = 0; ImagingObject *paletteimage = NULL; - if (!PyArg_ParseTuple(args, "s|iO", &mode, &dither, &paletteimage)) + if (!PyArg_ParseTuple(args, "s|iO", &mode, &dither, &paletteimage)) { return NULL; + } if (paletteimage != NULL) { if (!PyImaging_Check(paletteimage)) { PyObject_Print((PyObject *)paletteimage, stderr, 0); @@ -898,11 +930,13 @@ _convert2(ImagingObject* self, PyObject* args) ImagingObject* imagep2; if (!PyArg_ParseTuple(args, "O!O!", &Imaging_Type, &imagep1, - &Imaging_Type, &imagep2)) + &Imaging_Type, &imagep2)) { return NULL; + } - if (!ImagingConvert2(imagep1->image, imagep2->image)) + if (!ImagingConvert2(imagep1->image, imagep2->image)) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -944,8 +978,9 @@ _convert_transparent(ImagingObject* self, PyObject* args) static PyObject* _copy(ImagingObject* self, PyObject* args) { - if (!PyArg_ParseTuple(args, "")) + if (!PyArg_ParseTuple(args, "")) { return NULL; + } return PyImagingNew(ImagingCopy(self->image)); } @@ -954,8 +989,9 @@ static PyObject* _crop(ImagingObject* self, PyObject* args) { int x0, y0, x1, y1; - if (!PyArg_ParseTuple(args, "(iiii)", &x0, &y0, &x1, &y1)) + if (!PyArg_ParseTuple(args, "(iiii)", &x0, &y0, &x1, &y1)) { return NULL; + } return PyImagingNew(ImagingCrop(self->image, x0, y0, x1, y1)); } @@ -965,8 +1001,9 @@ _expand_image(ImagingObject* self, PyObject* args) { int x, y; int mode = 0; - if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) + if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) { return NULL; + } return PyImagingNew(ImagingExpand(self->image, x, y, mode)); } @@ -982,13 +1019,15 @@ _filter(ImagingObject* self, PyObject* args) float divisor, offset; PyObject* kernel = NULL; if (!PyArg_ParseTuple(args, "(ii)ffO", &xsize, &ysize, - &divisor, &offset, &kernel)) + &divisor, &offset, &kernel)) { return NULL; + } /* get user-defined kernel */ kerneldata = getlist(kernel, &kernelsize, NULL, TYPE_FLOAT32); - if (!kerneldata) + if (!kerneldata) { return NULL; + } if (kernelsize != (Py_ssize_t) xsize * (Py_ssize_t) ysize) { free(kerneldata); return ImagingError_ValueError("bad kernel size"); @@ -1016,13 +1055,15 @@ _gaussian_blur(ImagingObject* self, PyObject* args) float radius = 0; int passes = 3; - if (!PyArg_ParseTuple(args, "f|i", &radius, &passes)) + if (!PyArg_ParseTuple(args, "f|i", &radius, &passes)) { return NULL; + } imIn = self->image; imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) + if (!imOut) { return NULL; + } if (!ImagingGaussianBlur(imOut, imIn, radius, passes)) { ImagingDelete(imOut); @@ -1043,8 +1084,9 @@ _getpalette(ImagingObject* self, PyObject* args) char* mode = "RGB"; char* rawmode = "RGB"; - if (!PyArg_ParseTuple(args, "|ss", &mode, &rawmode)) + if (!PyArg_ParseTuple(args, "|ss", &mode, &rawmode)) { return NULL; + } if (!self->image->palette) { PyErr_SetString(PyExc_ValueError, no_palette); @@ -1058,8 +1100,9 @@ _getpalette(ImagingObject* self, PyObject* args) } palette = PyBytes_FromStringAndSize(NULL, palettesize * bits / 8); - if (!palette) + if (!palette) { return NULL; + } pack((UINT8*) PyBytes_AsString(palette), self->image->palette->palette, palettesize); @@ -1083,24 +1126,27 @@ _getxy(PyObject* xy, int* x, int *y) { PyObject* value; - if (!PyTuple_Check(xy) || PyTuple_GET_SIZE(xy) != 2) + if (!PyTuple_Check(xy) || PyTuple_GET_SIZE(xy) != 2) { goto badarg; + } value = PyTuple_GET_ITEM(xy, 0); - if (PyLong_Check(value)) + if (PyLong_Check(value)) { *x = PyLong_AS_LONG(value); - else if (PyFloat_Check(value)) + } else if (PyFloat_Check(value)) { *x = (int) PyFloat_AS_DOUBLE(value); - else + } else { goto badval; + } value = PyTuple_GET_ITEM(xy, 1); - if (PyLong_Check(value)) + if (PyLong_Check(value)) { *y = PyLong_AS_LONG(value); - else if (PyFloat_Check(value)) + } else if (PyFloat_Check(value)) { *y = (int) PyFloat_AS_DOUBLE(value); - else + } else { goto badval; + } return 0; @@ -1135,8 +1181,9 @@ _getpixel(ImagingObject* self, PyObject* args) xy = PyTuple_GET_ITEM(args, 0); - if (_getxy(xy, &x, &y)) + if (_getxy(xy, &x, &y)) { return NULL; + } if (self->access == NULL) { Py_INCREF(Py_None); @@ -1162,20 +1209,23 @@ parse_histogram_extremap(ImagingObject* self, PyObject* extremap, if (extremap) { switch (self->image->type) { case IMAGING_TYPE_UINT8: - if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) + if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) { return NULL; + } ep->u[0] = CLIP8(i0); ep->u[1] = CLIP8(i1); break; case IMAGING_TYPE_INT32: - if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) + if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) { return NULL; + } ep->i[0] = i0; ep->i[1] = i1; break; case IMAGING_TYPE_FLOAT32: - if (!PyArg_ParseTuple(extremap, "dd", &f0, &f1)) + if (!PyArg_ParseTuple(extremap, "dd", &f0, &f1)) { return NULL; + } ep->f[0] = (FLOAT32) f0; ep->f[1] = (FLOAT32) f1; break; @@ -1199,15 +1249,17 @@ _histogram(ImagingObject* self, PyObject* args) PyObject* extremap = NULL; ImagingObject* maskp = NULL; - if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) + if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) { return NULL; + } /* Using a var to avoid allocations. */ ep = parse_histogram_extremap(self, extremap, &extrema); h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); - if (!h) + if (!h) { return NULL; + } /* Build an integer list containing the histogram */ list = PyList_New(h->bands * 256); @@ -1240,15 +1292,17 @@ _entropy(ImagingObject* self, PyObject* args) PyObject* extremap = NULL; ImagingObject* maskp = NULL; - if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) + if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) { return NULL; + } /* Using a local var to avoid allocations. */ ep = parse_histogram_extremap(self, extremap, &extrema); h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); - if (!h) + if (!h) { return NULL; + } /* Calculate the histogram entropy */ /* First, sum the histogram data */ @@ -1280,8 +1334,9 @@ static PyObject* _modefilter(ImagingObject* self, PyObject* args) { int size; - if (!PyArg_ParseTuple(args, "i", &size)) + if (!PyArg_ParseTuple(args, "i", &size)) { return NULL; + } return PyImagingNew(ImagingModeFilter(self->image, size)); } @@ -1291,8 +1346,9 @@ static PyObject* _offset(ImagingObject* self, PyObject* args) { int xoffset, yoffset; - if (!PyArg_ParseTuple(args, "ii", &xoffset, &yoffset)) + if (!PyArg_ParseTuple(args, "ii", &xoffset, &yoffset)) { return NULL; + } return PyImagingNew(ImagingOffset(self->image, xoffset, yoffset)); } @@ -1309,19 +1365,21 @@ _paste(ImagingObject* self, PyObject* args) if (!PyArg_ParseTuple(args, "O(iiii)|O!", &source, &x0, &y0, &x1, &y1, - &Imaging_Type, &maskp)) - return NULL; + &Imaging_Type, &maskp)) { + return NULL; + } - if (PyImaging_Check(source)) + if (PyImaging_Check(source)) { status = ImagingPaste( self->image, PyImaging_AsImaging(source), (maskp) ? maskp->image : NULL, x0, y0, x1, y1 ); - else { - if (!getink(source, self->image, ink)) + } else { + if (!getink(source, self->image, ink)) { return NULL; + } status = ImagingFill2( self->image, ink, (maskp) ? maskp->image : NULL, @@ -1329,8 +1387,9 @@ _paste(ImagingObject* self, PyObject* args) ); } - if (status < 0) + if (status < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -1347,8 +1406,9 @@ _point(ImagingObject* self, PyObject* args) PyObject* list; char* mode; - if (!PyArg_ParseTuple(args, "Oz", &list, &mode)) - return NULL; + if (!PyArg_ParseTuple(args, "Oz", &list, &mode)) { + return NULL; + } if (mode && !strcmp(mode, "F")) { FLOAT32* data; @@ -1356,8 +1416,9 @@ _point(ImagingObject* self, PyObject* args) /* map from 8-bit data to floating point */ n = 256; data = getlist(list, &n, wrong_number, TYPE_FLOAT32); - if (!data) + if (!data) { return NULL; + } im = ImagingPoint(self->image, mode, (void*) data); free(data); @@ -1368,8 +1429,9 @@ _point(ImagingObject* self, PyObject* args) /* FIXME: support arbitrary number of entries (requires API change) */ n = 65536; data = getlist(list, &n, wrong_number, TYPE_UINT8); - if (!data) + if (!data) { return NULL; + } im = ImagingPoint(self->image, mode, (void*) data); free(data); @@ -1379,32 +1441,37 @@ _point(ImagingObject* self, PyObject* args) if (mode) { bands = getbands(mode); - if (bands < 0) + if (bands < 0) { return NULL; - } else + } + } else { bands = self->image->bands; + } /* map to integer data */ n = 256 * bands; data = getlist(list, &n, wrong_number, TYPE_INT32); - if (!data) + if (!data) { return NULL; + } - if (mode && !strcmp(mode, "I")) + if (mode && !strcmp(mode, "I")) { im = ImagingPoint(self->image, mode, (void*) data); - else if (mode && bands > 1) { + } else if (mode && bands > 1) { for (i = 0; i < 256; i++) { lut[i*4] = CLIP8(data[i]); lut[i*4+1] = CLIP8(data[i+256]); lut[i*4+2] = CLIP8(data[i+512]); - if (n > 768) + if (n > 768) { lut[i*4+3] = CLIP8(data[i+768]); + } } im = ImagingPoint(self->image, mode, (void*) lut); } else { /* map individual bands */ - for (i = 0; i < n; i++) + for (i = 0; i < n; i++) { lut[i] = CLIP8(data[i]); + } im = ImagingPoint(self->image, mode, (void*) lut); } free(data); @@ -1418,8 +1485,9 @@ _point_transform(ImagingObject* self, PyObject* args) { double scale = 1.0; double offset = 0.0; - if (!PyArg_ParseTuple(args, "|dd", &scale, &offset)) - return NULL; + if (!PyArg_ParseTuple(args, "|dd", &scale, &offset)) { + return NULL; + } return PyImagingNew(ImagingPointTransform(self->image, scale, offset)); } @@ -1437,8 +1505,9 @@ _putdata(ImagingObject* self, PyObject* args) double scale = 1.0; double offset = 0.0; - if (!PyArg_ParseTuple(args, "O|dd", &data, &scale, &offset)) + if (!PyArg_ParseTuple(args, "O|dd", &data, &scale, &offset)) { return NULL; + } if (!PySequence_Check(data)) { PyErr_SetString(PyExc_TypeError, must_be_sequence); @@ -1457,21 +1526,24 @@ _putdata(ImagingObject* self, PyObject* args) if (PyBytes_Check(data)) { unsigned char* p; p = (unsigned char*) PyBytes_AS_STRING(data); - if (scale == 1.0 && offset == 0.0) + if (scale == 1.0 && offset == 0.0) { /* Plain string data */ for (i = y = 0; i < n; i += image->xsize, y++) { x = n - i; - if (x > (int) image->xsize) + if (x > (int) image->xsize) { x = image->xsize; + } memcpy(image->image8[y], p+i, x); } - else + } else { /* Scaled and clipped string data */ for (i = x = y = 0; i < n; i++) { image->image8[y][x] = CLIP8((int) (p[i] * scale + offset)); - if (++x >= (int) image->xsize) + if (++x >= (int) image->xsize) { x = 0, y++; + } } + } } else { seq = PySequence_Fast(data, must_be_sequence); if (!seq) { @@ -1570,8 +1642,9 @@ _quantize(ImagingObject* self, PyObject* args) int colours = 256; int method = 0; int kmeans = 0; - if (!PyArg_ParseTuple(args, "|iii", &colours, &method, &kmeans)) + if (!PyArg_ParseTuple(args, "|iii", &colours, &method, &kmeans)) { return NULL; + } if (!self->image->xsize || !self->image->ysize) { /* no content; return an empty image */ @@ -1593,8 +1666,9 @@ _putpalette(ImagingObject* self, PyObject* args) char* rawmode; UINT8* palette; Py_ssize_t palettesize; - if (!PyArg_ParseTuple(args, "sy#", &rawmode, &palette, &palettesize)) + if (!PyArg_ParseTuple(args, "sy#", &rawmode, &palette, &palettesize)) { return NULL; + } if (strcmp(self->image->mode, "L") && strcmp(self->image->mode, "LA") && strcmp(self->image->mode, "P") && strcmp(self->image->mode, "PA")) { @@ -1630,8 +1704,9 @@ _putpalettealpha(ImagingObject* self, PyObject* args) { int index; int alpha = 0; - if (!PyArg_ParseTuple(args, "i|i", &index, &alpha)) + if (!PyArg_ParseTuple(args, "i|i", &index, &alpha)) { return NULL; + } if (!self->image->palette) { PyErr_SetString(PyExc_ValueError, no_palette); @@ -1656,8 +1731,9 @@ _putpalettealphas(ImagingObject* self, PyObject* args) int i; UINT8 *values; Py_ssize_t length; - if (!PyArg_ParseTuple(args, "y#", &values, &length)) + if (!PyArg_ParseTuple(args, "y#", &values, &length)) { return NULL; + } if (!self->image->palette) { PyErr_SetString(PyExc_ValueError, no_palette); @@ -1686,8 +1762,9 @@ _putpixel(ImagingObject* self, PyObject* args) int x, y; PyObject* color; - if (!PyArg_ParseTuple(args, "(ii)O", &x, &y, &color)) + if (!PyArg_ParseTuple(args, "(ii)O", &x, &y, &color)) { return NULL; + } im = self->image; @@ -1703,11 +1780,13 @@ _putpixel(ImagingObject* self, PyObject* args) return NULL; } - if (!getink(color, im, ink)) + if (!getink(color, im, ink)) { return NULL; + } - if (self->access) + if (self->access) { self->access->put_pixel(im, x, y, ink); + } Py_INCREF(Py_None); return Py_None; @@ -1718,8 +1797,9 @@ static PyObject* _rankfilter(ImagingObject* self, PyObject* args) { int size, rank; - if (!PyArg_ParseTuple(args, "ii", &size, &rank)) + if (!PyArg_ParseTuple(args, "ii", &size, &rank)) { return NULL; + } return PyImagingNew(ImagingRankFilter(self->image, size, rank)); } @@ -1740,8 +1820,9 @@ _resize(ImagingObject* self, PyObject* args) box[3] = imIn->ysize; if (!PyArg_ParseTuple(args, "(ii)|i(ffff)", &xsize, &ysize, &filter, - &box[0], &box[1], &box[2], &box[3])) + &box[0], &box[1], &box[2], &box[3])) { return NULL; + } if (xsize < 1 || ysize < 1) { return ImagingError_ValueError("height and width must be > 0"); @@ -1787,6 +1868,52 @@ _resize(ImagingObject* self, PyObject* args) return PyImagingNew(imOut); } +static PyObject* +_reduce(ImagingObject* self, PyObject* args) +{ + Imaging imIn; + Imaging imOut; + + int xscale, yscale; + int box[4] = {0, 0, 0, 0}; + + imIn = self->image; + box[2] = imIn->xsize; + box[3] = imIn->ysize; + + if (!PyArg_ParseTuple(args, "(ii)|(iiii)", &xscale, &yscale, + &box[0], &box[1], &box[2], &box[3])) { + return NULL; + } + + if (xscale < 1 || yscale < 1) { + return ImagingError_ValueError("scale must be > 0"); + } + + if (box[0] < 0 || box[1] < 0) { + return ImagingError_ValueError("box offset can't be negative"); + } + + if (box[2] > imIn->xsize || box[3] > imIn->ysize) { + return ImagingError_ValueError("box can't exceed original image size"); + } + + if (box[2] <= box[0] || box[3] <= box[1]) { + return ImagingError_ValueError("box can't be empty"); + } + + if (xscale == 1 && yscale == 1) { + imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]); + } else { + // Change box format: (left, top, width, height) + box[2] -= box[0]; + box[3] -= box[1]; + imOut = ImagingReduce(imIn, xscale, yscale, box); + } + + return PyImagingNew(imOut); +} + #define IS_RGB(mode)\ (!strcmp(mode, "RGB") || !strcmp(mode, "RGBA") || !strcmp(mode, "RGBX")) @@ -1800,8 +1927,9 @@ im_setmode(ImagingObject* self, PyObject* args) char* mode; Py_ssize_t modelen; - if (!PyArg_ParseTuple(args, "s#:setmode", &mode, &modelen)) - return NULL; + if (!PyArg_ParseTuple(args, "s#:setmode", &mode, &modelen)) { + return NULL; + } im = self->image; @@ -1813,16 +1941,19 @@ im_setmode(ImagingObject* self, PyObject* args) /* color to color */ strcpy(im->mode, mode); im->bands = modelen; - if (!strcmp(mode, "RGBA")) + if (!strcmp(mode, "RGBA")) { (void) ImagingFillBand(im, 3, 255); + } } else { /* trying doing an in-place conversion */ - if (!ImagingConvertInPlace(im, mode)) + if (!ImagingConvertInPlace(im, mode)) { return NULL; + } } - if (self->access) + if (self->access) { ImagingAccessDelete(im, self->access); + } self->access = ImagingAccessNew(im); Py_INCREF(Py_None); @@ -1849,8 +1980,9 @@ _transform2(ImagingObject* self, PyObject* args) &x0, &y0, &x1, &y1, &Imaging_Type, &imagep, &method, &data, - &filter, &fill)) - return NULL; + &filter, &fill)) { + return NULL; + } switch (method) { case IMAGING_TRANSFORM_AFFINE: @@ -1867,8 +1999,9 @@ _transform2(ImagingObject* self, PyObject* args) } a = getlist(data, &n, wrong_number, TYPE_DOUBLE); - if (!a) + if (!a) { return NULL; + } imOut = ImagingTransform( self->image, imagep->image, method, @@ -1876,8 +2009,9 @@ _transform2(ImagingObject* self, PyObject* args) free(a); - if (!imOut) + if (!imOut) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -1890,8 +2024,9 @@ _transpose(ImagingObject* self, PyObject* args) Imaging imOut; int op; - if (!PyArg_ParseTuple(args, "i", &op)) - return NULL; + if (!PyArg_ParseTuple(args, "i", &op)) { + return NULL; + } imIn = self->image; @@ -1912,7 +2047,7 @@ _transpose(ImagingObject* self, PyObject* args) return NULL; } - if (imOut) + if (imOut) { switch (op) { case 0: (void) ImagingFlipLeftRight(imOut, imIn); @@ -1936,6 +2071,7 @@ _transpose(ImagingObject* self, PyObject* args) (void) ImagingTransverse(imOut, imIn); break; } + } return PyImagingNew(imOut); } @@ -1949,16 +2085,19 @@ _unsharp_mask(ImagingObject* self, PyObject* args) float radius; int percent, threshold; - if (!PyArg_ParseTuple(args, "fii", &radius, &percent, &threshold)) + if (!PyArg_ParseTuple(args, "fii", &radius, &percent, &threshold)) { return NULL; + } imIn = self->image; imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) + if (!imOut) { return NULL; + } - if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) + if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) { return NULL; + } return PyImagingNew(imOut); } @@ -1972,13 +2111,15 @@ _box_blur(ImagingObject* self, PyObject* args) float radius; int n = 1; - if (!PyArg_ParseTuple(args, "f|i", &radius, &n)) + if (!PyArg_ParseTuple(args, "f|i", &radius, &n)) { return NULL; + } imIn = self->image; imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) + if (!imOut) { return NULL; + } if (!ImagingBoxBlur(imOut, imIn, radius, n)) { ImagingDelete(imOut); @@ -2016,12 +2157,14 @@ _getcolors(ImagingObject* self, PyObject* args) PyObject* out; int maxcolors = 256; - if (!PyArg_ParseTuple(args, "i:getcolors", &maxcolors)) + if (!PyArg_ParseTuple(args, "i:getcolors", &maxcolors)) { return NULL; + } items = ImagingGetColors(self->image, maxcolors, &colors); - if (!items) + if (!items) { return NULL; + } if (colors > maxcolors) { out = Py_None; @@ -2054,10 +2197,11 @@ _getextrema(ImagingObject* self, PyObject* args) int status; status = ImagingGetExtrema(self->image, &extrema); - if (status < 0) + if (status < 0) { return NULL; + } - if (status) + if (status) { switch (self->image->type) { case IMAGING_TYPE_UINT8: return Py_BuildValue("BB", extrema.u[0], extrema.u[1]); @@ -2070,6 +2214,7 @@ _getextrema(ImagingObject* self, PyObject* args) return Py_BuildValue("HH", extrema.s[0], extrema.s[1]); } } + } Py_INCREF(Py_None); return Py_None; @@ -2111,8 +2256,9 @@ _getband(ImagingObject* self, PyObject* args) { int band; - if (!PyArg_ParseTuple(args, "i", &band)) + if (!PyArg_ParseTuple(args, "i", &band)) { return NULL; + } return PyImagingNew(ImagingGetBand(self->image, band)); } @@ -2123,11 +2269,13 @@ _fillband(ImagingObject* self, PyObject* args) int band; int color; - if (!PyArg_ParseTuple(args, "ii", &band, &color)) + if (!PyArg_ParseTuple(args, "ii", &band, &color)) { return NULL; + } - if (!ImagingFillBand(self->image, band, color)) + if (!ImagingFillBand(self->image, band, color)) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -2140,11 +2288,13 @@ _putband(ImagingObject* self, PyObject* args) int band; if (!PyArg_ParseTuple(args, "O!i", &Imaging_Type, &imagep, - &band)) + &band)) { return NULL; + } - if (!ImagingPutBand(self->image, imagep->image, band)) + if (!ImagingPutBand(self->image, imagep->image, band)) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -2162,13 +2312,22 @@ _merge(PyObject* self, PyObject* args) if (!PyArg_ParseTuple(args, "sO!|O!O!O!", &mode, &Imaging_Type, &band0, &Imaging_Type, &band1, - &Imaging_Type, &band2, &Imaging_Type, &band3)) + &Imaging_Type, &band2, &Imaging_Type, &band3)) { return NULL; + } - if (band0) bands[0] = band0->image; - if (band1) bands[1] = band1->image; - if (band2) bands[2] = band2->image; - if (band3) bands[3] = band3->image; + if (band0) { + bands[0] = band0->image; + } + if (band1) { + bands[1] = band1->image; + } + if (band2) { + bands[2] = band2->image; + } + if (band3) { + bands[3] = band3->image; + } return PyImagingNew(ImagingMerge(mode, bands)); } @@ -2182,14 +2341,16 @@ _split(ImagingObject* self, PyObject* args) PyObject* imaging_object; Imaging bands[4] = {NULL, NULL, NULL, NULL}; - if ( ! ImagingSplit(self->image, bands)) + if ( ! ImagingSplit(self->image, bands)) { return NULL; + } list = PyTuple_New(self->image->bands); for (i = 0; i < self->image->bands; i++) { imaging_object = PyImagingNew(bands[i]); - if ( ! imaging_object) + if ( ! imaging_object) { fails += 1; + } PyTuple_SET_ITEM(list, i, imaging_object); } if (fails) { @@ -2214,8 +2375,9 @@ _chop_lighter(ImagingObject* self, PyObject* args) { ImagingObject* imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopLighter(self->image, imagep->image)); } @@ -2225,8 +2387,9 @@ _chop_darker(ImagingObject* self, PyObject* args) { ImagingObject* imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopDarker(self->image, imagep->image)); } @@ -2236,8 +2399,9 @@ _chop_difference(ImagingObject* self, PyObject* args) { ImagingObject* imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopDifference(self->image, imagep->image)); } @@ -2247,8 +2411,9 @@ _chop_multiply(ImagingObject* self, PyObject* args) { ImagingObject* imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopMultiply(self->image, imagep->image)); } @@ -2258,8 +2423,9 @@ _chop_screen(ImagingObject* self, PyObject* args) { ImagingObject* imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopScreen(self->image, imagep->image)); } @@ -2275,8 +2441,9 @@ _chop_add(ImagingObject* self, PyObject* args) offset = 0; if (!PyArg_ParseTuple(args, "O!|fi", &Imaging_Type, &imagep, - &scale, &offset)) + &scale, &offset)) { return NULL; + } return PyImagingNew(ImagingChopAdd(self->image, imagep->image, scale, offset)); @@ -2293,8 +2460,9 @@ _chop_subtract(ImagingObject* self, PyObject* args) offset = 0; if (!PyArg_ParseTuple(args, "O!|fi", &Imaging_Type, &imagep, - &scale, &offset)) + &scale, &offset)) { return NULL; + } return PyImagingNew(ImagingChopSubtract(self->image, imagep->image, scale, offset)); @@ -2305,8 +2473,9 @@ _chop_and(ImagingObject* self, PyObject* args) { ImagingObject* imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopAnd(self->image, imagep->image)); } @@ -2316,8 +2485,9 @@ _chop_or(ImagingObject* self, PyObject* args) { ImagingObject* imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopOr(self->image, imagep->image)); } @@ -2327,8 +2497,9 @@ _chop_xor(ImagingObject* self, PyObject* args) { ImagingObject* imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopXor(self->image, imagep->image)); } @@ -2338,8 +2509,9 @@ _chop_add_modulo(ImagingObject* self, PyObject* args) { ImagingObject* imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopAddModulo(self->image, imagep->image)); } @@ -2349,12 +2521,48 @@ _chop_subtract_modulo(ImagingObject* self, PyObject* args) { ImagingObject* imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopSubtractModulo(self->image, imagep->image)); } +static PyObject* +_chop_soft_light(ImagingObject* self, PyObject* args) +{ + ImagingObject* imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopSoftLight(self->image, imagep->image)); +} + +static PyObject* +_chop_hard_light(ImagingObject* self, PyObject* args) +{ + ImagingObject* imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopHardLight(self->image, imagep->image)); +} + +static PyObject* +_chop_overlay(ImagingObject* self, PyObject* args) +{ + ImagingObject* imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingOverlay(self->image, imagep->image)); +} #endif @@ -2374,8 +2582,9 @@ _font_new(PyObject* self_, PyObject* args) Py_ssize_t glyphdata_length; if (!PyArg_ParseTuple(args, "O!y#", &Imaging_Type, &imagep, - &glyphdata, &glyphdata_length)) + &glyphdata, &glyphdata_length)) { return NULL; + } if (glyphdata_length != 256 * 20) { PyErr_SetString(PyExc_ValueError, wrong_length); @@ -2383,8 +2592,9 @@ _font_new(PyObject* self_, PyObject* args) } self = PyObject_New(ImagingFontObject, &ImagingFont_Type); - if (self == NULL) + if (self == NULL) { return NULL; + } /* glyph bitmap */ self->bitmap = imagep->image; @@ -2403,10 +2613,12 @@ _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)); - if (self->glyphs[i].dy0 < y0) + if (self->glyphs[i].dy0 < y0) { y0 = self->glyphs[i].dy0; - if (self->glyphs[i].dy1 > y1) + } + if (self->glyphs[i].dy1 > y1) { y1 = self->glyphs[i].dy1; + } glyphdata += 20; } @@ -2432,8 +2644,9 @@ textwidth(ImagingFontObject* self, const unsigned char* text) { int xsize; - for (xsize = 0; *text; text++) + for (xsize = 0; *text; text++) { xsize += self->glyphs[*text].dx; + } return xsize; } @@ -2512,15 +2725,17 @@ _font_getmask(ImagingFontObject* self, PyObject* args) self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1 ); - if (!bitmap) + if (!bitmap) { goto failed; + } status = ImagingPaste( im, bitmap, NULL, glyph->dx0+x, glyph->dy0+b, glyph->dx1+x, glyph->dy1+b ); ImagingDelete(bitmap); - if (status < 0) + if (status < 0) { goto failed; + } x = x + glyph->dx; b = b + glyph->dy; } @@ -2540,8 +2755,9 @@ _font_getsize(ImagingFontObject* self, PyObject* args) PyObject* encoded_string; PyObject* val; - if (!PyArg_ParseTuple(args, "O:getsize", &encoded_string)) + if (!PyArg_ParseTuple(args, "O:getsize", &encoded_string)) { return NULL; + } _font_text_asBytes(encoded_string, &text); if (!text) { @@ -2568,12 +2784,14 @@ _draw_new(PyObject* self_, PyObject* args) ImagingObject* imagep; int blend = 0; - if (!PyArg_ParseTuple(args, "O!|i", &Imaging_Type, &imagep, &blend)) + if (!PyArg_ParseTuple(args, "O!|i", &Imaging_Type, &imagep, &blend)) { return NULL; + } self = PyObject_New(ImagingDrawObject, &ImagingDraw_Type); - if (self == NULL) + if (self == NULL) { return NULL; + } /* keep a reference to the image object */ Py_INCREF(imagep); @@ -2600,11 +2818,13 @@ _draw_ink(ImagingDrawObject* self, PyObject* args) { INT32 ink = 0; PyObject* color; - if (!PyArg_ParseTuple(args, "O", &color)) + if (!PyArg_ParseTuple(args, "O", &color)) { return NULL; + } - if (!getink(color, self->image->image, (char*) &ink)) + if (!getink(color, self->image->image, (char*) &ink)) { return NULL; + } return PyLong_FromLong((int) ink); } @@ -2619,13 +2839,14 @@ _draw_arc(ImagingDrawObject* self, PyObject* args) int ink; int width = 0; float start, end; - int op = 0; - if (!PyArg_ParseTuple(args, "Offi|ii", &data, &start, &end, &ink, &width)) + if (!PyArg_ParseTuple(args, "Offi|i", &data, &start, &end, &ink, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n != 2) { PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); free(xy); @@ -2635,13 +2856,14 @@ _draw_arc(ImagingDrawObject* self, PyObject* args) n = ImagingDrawArc(self->image->image, (int) xy[0], (int) xy[1], (int) xy[2], (int) xy[3], - start, end, &ink, width, op + start, end, &ink, width, self->blend ); free(xy); - if (n < 0) + if (n < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -2656,12 +2878,14 @@ _draw_bitmap(ImagingDrawObject* self, PyObject* args) PyObject *data; ImagingObject* bitmap; int ink; - if (!PyArg_ParseTuple(args, "OO!i", &data, &Imaging_Type, &bitmap, &ink)) + if (!PyArg_ParseTuple(args, "OO!i", &data, &Imaging_Type, &bitmap, &ink)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n != 1) { PyErr_SetString(PyExc_TypeError, "coordinate list must contain exactly 1 coordinate" @@ -2677,8 +2901,9 @@ _draw_bitmap(ImagingDrawObject* self, PyObject* args) free(xy); - if (n < 0) + if (n < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -2695,12 +2920,14 @@ _draw_chord(ImagingDrawObject* self, PyObject* args) int width = 0; float start, end; if (!PyArg_ParseTuple(args, "Offii|i", - &data, &start, &end, &ink, &fill, &width)) + &data, &start, &end, &ink, &fill, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n != 2) { PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); free(xy); @@ -2715,8 +2942,9 @@ _draw_chord(ImagingDrawObject* self, PyObject* args) free(xy); - if (n < 0) + if (n < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -2732,12 +2960,14 @@ _draw_ellipse(ImagingDrawObject* self, PyObject* args) int ink; int fill = 0; int width = 0; - if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) + if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n != 2) { PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); free(xy); @@ -2752,8 +2982,9 @@ _draw_ellipse(ImagingDrawObject* self, PyObject* args) free(xy); - if (n < 0) + if (n < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -2768,12 +2999,14 @@ _draw_lines(ImagingDrawObject* self, PyObject* args) PyObject *data; int ink; int width = 0; - if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &width)) + if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (width <= 1) { double *p = NULL; @@ -2787,12 +3020,13 @@ _draw_lines(ImagingDrawObject* self, PyObject* args) return NULL; } } - if (p) /* draw last point */ + if (p) {/* draw last point */ ImagingDrawPoint( self->image->image, (int) p[2], (int) p[3], &ink, self->blend ); + } } else { for (i = 0; i < n-1; i++) { double *p = &xy[i+i]; @@ -2820,12 +3054,14 @@ _draw_points(ImagingDrawObject* self, PyObject* args) PyObject *data; int ink; - if (!PyArg_ParseTuple(args, "Oi", &data, &ink)) + if (!PyArg_ParseTuple(args, "Oi", &data, &ink)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } for (i = 0; i < n; i++) { double *p = &xy[i+i]; @@ -2855,8 +3091,9 @@ _draw_outline(ImagingDrawObject* self, PyObject* args) PyObject* outline_; int ink; int fill = 0; - if (!PyArg_ParseTuple(args, "Oi|i", &outline_, &ink, &fill)) + if (!PyArg_ParseTuple(args, "Oi|i", &outline_, &ink, &fill)) { return NULL; + } outline = PyOutline_AsOutline(outline_); if (!outline) { @@ -2865,8 +3102,9 @@ _draw_outline(ImagingDrawObject* self, PyObject* args) } if (ImagingDrawOutline(self->image->image, outline, - &ink, fill, self->blend) < 0) - return NULL; + &ink, fill, self->blend) < 0) { + return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -2884,12 +3122,14 @@ _draw_pieslice(ImagingDrawObject* self, PyObject* args) int ink, fill; int width = 0; float start, end; - if (!PyArg_ParseTuple(args, "Offii|i", &data, &start, &end, &ink, &fill, &width)) + if (!PyArg_ParseTuple(args, "Offii|i", &data, &start, &end, &ink, &fill, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n != 2) { PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); free(xy); @@ -2904,8 +3144,9 @@ _draw_pieslice(ImagingDrawObject* self, PyObject* args) free(xy); - if (n < 0) + if (n < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -2921,12 +3162,14 @@ _draw_polygon(ImagingDrawObject* self, PyObject* args) PyObject* data; int ink; int fill = 0; - if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &fill)) + if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &fill)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n < 2) { PyErr_SetString(PyExc_TypeError, "coordinate list must contain at least 2 coordinates" @@ -2967,12 +3210,14 @@ _draw_rectangle(ImagingDrawObject* self, PyObject* args) int ink; int fill = 0; int width = 0; - if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) + if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n != 2) { PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); free(xy); @@ -2987,8 +3232,9 @@ _draw_rectangle(ImagingDrawObject* self, PyObject* args) free(xy); - if (n < 0) + if (n < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -3023,12 +3269,14 @@ pixel_access_new(ImagingObject* imagep, PyObject* args) PixelAccessObject *self; int readonly = 0; - if (!PyArg_ParseTuple(args, "|i", &readonly)) + if (!PyArg_ParseTuple(args, "|i", &readonly)) { return NULL; + } self = PyObject_New(PixelAccessObject, &PixelAccess_Type); - if (self == NULL) + if (self == NULL) { return NULL; + } /* keep a reference to the image object */ Py_INCREF(imagep); @@ -3050,8 +3298,9 @@ static PyObject * pixel_access_getitem(PixelAccessObject *self, PyObject *xy) { int x, y; - if (_getxy(xy, &x, &y)) + if (_getxy(xy, &x, &y)) { return NULL; + } return getpixel(self->image->image, self->image->access, x, y); } @@ -3068,8 +3317,9 @@ pixel_access_setitem(PixelAccessObject *self, PyObject *xy, PyObject *color) return -1; } - if (_getxy(xy, &x, &y)) + if (_getxy(xy, &x, &y)) { return -1; + } if (x < 0) { x = im->xsize + x; @@ -3083,11 +3333,13 @@ pixel_access_setitem(PixelAccessObject *self, PyObject *xy, PyObject *color) return -1; } - if (!color) /* FIXME: raise exception? */ + if (!color) {/* FIXME: raise exception? */ return 0; + } - if (!getink(color, im, ink)) + if (!getink(color, im, ink)) { return -1; + } self->image->access->put_pixel(im, x, y, ink); @@ -3113,8 +3365,9 @@ _effect_mandelbrot(ImagingObject* self, PyObject* args) if (!PyArg_ParseTuple(args, "|(ii)(dddd)i", &xsize, &ysize, &extent[0], &extent[1], &extent[2], &extent[3], - &quality)) - return NULL; + &quality)) { + return NULL; + } return PyImagingNew(ImagingEffectMandelbrot(xsize, ysize, extent, quality)); } @@ -3124,8 +3377,9 @@ _effect_noise(ImagingObject* self, PyObject* args) { int xsize, ysize; float sigma = 128; - if (!PyArg_ParseTuple(args, "(ii)|f", &xsize, &ysize, &sigma)) + if (!PyArg_ParseTuple(args, "(ii)|f", &xsize, &ysize, &sigma)) { return NULL; + } return PyImagingNew(ImagingEffectNoise(xsize, ysize, sigma)); } @@ -3135,8 +3389,9 @@ _effect_spread(ImagingObject* self, PyObject* args) { int dist; - if (!PyArg_ParseTuple(args, "i", &dist)) + if (!PyArg_ParseTuple(args, "i", &dist)) { return NULL; + } return PyImagingNew(ImagingEffectSpread(self->image, dist)); } @@ -3154,8 +3409,9 @@ _getcodecstatus(PyObject* self, PyObject* args) int status; char* msg; - if (!PyArg_ParseTuple(args, "i", &status)) + if (!PyArg_ParseTuple(args, "i", &status)) { return NULL; + } switch (status) { case IMAGING_CODEC_OVERRUN: @@ -3185,11 +3441,13 @@ _save_ppm(ImagingObject* self, PyObject* args) { char* filename; - if (!PyArg_ParseTuple(args, "s", &filename)) + if (!PyArg_ParseTuple(args, "s", &filename)) { return NULL; + } - if (!ImagingSavePPM(self->image, filename)) + if (!ImagingSavePPM(self->image, filename)) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -3235,6 +3493,7 @@ static struct PyMethodDef methods[] = { {"rankfilter", (PyCFunction)_rankfilter, 1}, #endif {"resize", (PyCFunction)_resize, 1}, + {"reduce", (PyCFunction)_reduce, 1}, {"transpose", (PyCFunction)_transpose, 1}, {"transform2", (PyCFunction)_transform2, 1}, @@ -3273,6 +3532,10 @@ static struct PyMethodDef methods[] = { {"chop_and", (PyCFunction)_chop_and, 1}, {"chop_or", (PyCFunction)_chop_or, 1}, {"chop_xor", (PyCFunction)_chop_xor, 1}, + {"chop_soft_light", (PyCFunction)_chop_soft_light, 1}, + {"chop_hard_light", (PyCFunction)_chop_hard_light, 1}, + {"chop_overlay", (PyCFunction)_chop_overlay, 1}, + #endif #ifdef WITH_UNSHARPMASK @@ -3369,8 +3632,9 @@ image_item(ImagingObject *self, Py_ssize_t i) if (im->xsize > 0) { x = i % im->xsize; y = i / im->xsize; - } else + } else { x = y = 0; /* leave it to getpixel to raise an exception */ + } return getpixel(im, self->access, x, y); } @@ -3526,12 +3790,14 @@ _get_stats(PyObject* self, PyObject* args) PyObject* d; ImagingMemoryArena arena = &ImagingDefaultArena; - if (!PyArg_ParseTuple(args, ":get_stats")) + if (!PyArg_ParseTuple(args, ":get_stats")) { return NULL; + } d = PyDict_New(); - if ( ! d) + if ( ! d) { return NULL; + } PyDict_SetItemString(d, "new_count", PyLong_FromLong(arena->stats_new_count)); PyDict_SetItemString(d, "allocated_blocks", @@ -3552,8 +3818,9 @@ _reset_stats(PyObject* self, PyObject* args) { ImagingMemoryArena arena = &ImagingDefaultArena; - if (!PyArg_ParseTuple(args, ":reset_stats")) + if (!PyArg_ParseTuple(args, ":reset_stats")) { return NULL; + } arena->stats_new_count = 0; arena->stats_allocated_blocks = 0; @@ -3568,8 +3835,9 @@ _reset_stats(PyObject* self, PyObject* args) static PyObject* _get_alignment(PyObject* self, PyObject* args) { - if (!PyArg_ParseTuple(args, ":get_alignment")) + if (!PyArg_ParseTuple(args, ":get_alignment")) { return NULL; + } return PyLong_FromLong(ImagingDefaultArena.alignment); } @@ -3577,8 +3845,9 @@ _get_alignment(PyObject* self, PyObject* args) static PyObject* _get_block_size(PyObject* self, PyObject* args) { - if (!PyArg_ParseTuple(args, ":get_block_size")) + if (!PyArg_ParseTuple(args, ":get_block_size")) { return NULL; + } return PyLong_FromLong(ImagingDefaultArena.block_size); } @@ -3586,8 +3855,9 @@ _get_block_size(PyObject* self, PyObject* args) static PyObject* _get_blocks_max(PyObject* self, PyObject* args) { - if (!PyArg_ParseTuple(args, ":get_blocks_max")) + if (!PyArg_ParseTuple(args, ":get_blocks_max")) { return NULL; + } return PyLong_FromLong(ImagingDefaultArena.blocks_max); } @@ -3596,8 +3866,9 @@ static PyObject* _set_alignment(PyObject* self, PyObject* args) { int alignment; - if (!PyArg_ParseTuple(args, "i:set_alignment", &alignment)) + if (!PyArg_ParseTuple(args, "i:set_alignment", &alignment)) { return NULL; + } if (alignment < 1 || alignment > 128) { PyErr_SetString(PyExc_ValueError, "alignment should be from 1 to 128"); @@ -3619,8 +3890,9 @@ static PyObject* _set_block_size(PyObject* self, PyObject* args) { int block_size; - if (!PyArg_ParseTuple(args, "i:set_block_size", &block_size)) + if (!PyArg_ParseTuple(args, "i:set_block_size", &block_size)) { return NULL; + } if (block_size <= 0) { PyErr_SetString(PyExc_ValueError, @@ -3644,8 +3916,9 @@ static PyObject* _set_blocks_max(PyObject* self, PyObject* args) { int blocks_max; - if (!PyArg_ParseTuple(args, "i:set_blocks_max", &blocks_max)) + if (!PyArg_ParseTuple(args, "i:set_blocks_max", &blocks_max)) { return NULL; + } if (blocks_max < 0) { PyErr_SetString(PyExc_ValueError, @@ -3673,8 +3946,9 @@ _clear_cache(PyObject* self, PyObject* args) { int i = 0; - if (!PyArg_ParseTuple(args, "|i:clear_cache", &i)) + if (!PyArg_ParseTuple(args, "|i:clear_cache", &i)) { return NULL; + } ImagingMemoryClearCache(&ImagingDefaultArena, i); @@ -3729,6 +4003,9 @@ extern PyObject* PyImaging_ListWindowsWin32(PyObject* self, PyObject* args); extern PyObject* PyImaging_EventLoopWin32(PyObject* self, PyObject* args); extern PyObject* PyImaging_DrawWmf(PyObject* self, PyObject* args); #endif +#ifdef HAVE_XCB +extern PyObject* PyImaging_GrabScreenX11(PyObject* self, PyObject* args); +#endif /* Experimental path stuff (in path.c) */ extern PyObject* PyPath_Create(ImagingObject* self, PyObject* args); @@ -3801,13 +4078,16 @@ static PyMethodDef functions[] = { #ifdef _WIN32 {"display", (PyCFunction)PyImaging_DisplayWin32, 1}, {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, 1}, - {"grabscreen", (PyCFunction)PyImaging_GrabScreenWin32, 1}, - {"grabclipboard", (PyCFunction)PyImaging_GrabClipboardWin32, 1}, + {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, 1}, + {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, 1}, {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, 1}, {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, 1}, {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, 1}, {"drawwmf", (PyCFunction)PyImaging_DrawWmf, 1}, #endif +#ifdef HAVE_XCB + {"grabscreen_x11", (PyCFunction)PyImaging_GrabScreenX11, 1}, +#endif /* Utilities */ {"getcodecstatus", (PyCFunction)_getcodecstatus, 1}, @@ -3857,18 +4137,22 @@ setup_module(PyObject* m) { const char* version = (char*)PILLOW_VERSION; /* Ready object types */ - if (PyType_Ready(&Imaging_Type) < 0) + if (PyType_Ready(&Imaging_Type) < 0) { return -1; + } #ifdef WITH_IMAGEDRAW - if (PyType_Ready(&ImagingFont_Type) < 0) + if (PyType_Ready(&ImagingFont_Type) < 0) { return -1; + } - if (PyType_Ready(&ImagingDraw_Type) < 0) + if (PyType_Ready(&ImagingDraw_Type) < 0) { return -1; + } #endif - if (PyType_Ready(&PixelAccess_Type) < 0) + if (PyType_Ready(&PixelAccess_Type) < 0) { return -1; + } ImagingAccessInit(); @@ -3888,12 +4172,21 @@ setup_module(PyObject* m) { #ifdef LIBJPEG_TURBO_VERSION PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", Py_True); + #define tostr1(a) #a + #define tostr(a) tostr1(a) + PyDict_SetItemString(d, "libjpeg_turbo_version", PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION))); + #undef tostr + #undef tostr1 #else PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", Py_False); #endif #ifdef HAVE_LIBIMAGEQUANT PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", Py_True); + { + extern const char* ImagingImageQuantVersion(void); + PyDict_SetItemString(d, "imagequant_version", PyUnicode_FromString(ImagingImageQuantVersion())); + } #else PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", Py_False); #endif @@ -3915,9 +4208,24 @@ setup_module(PyObject* m) { { extern const char * ImagingTiffVersion(void); PyDict_SetItemString(d, "libtiff_version", PyUnicode_FromString(ImagingTiffVersion())); + + // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 + PyObject* support_custom_tags; +#if TIFFLIB_VERSION >= 20111221 && TIFFLIB_VERSION != 20120218 && TIFFLIB_VERSION != 20120922 + support_custom_tags = Py_True; +#else + support_custom_tags = Py_False; +#endif + PyDict_SetItemString(d, "libtiff_support_custom_tags", support_custom_tags); } #endif +#ifdef HAVE_XCB + PyModule_AddObject(m, "HAVE_XCB", Py_True); +#else + PyModule_AddObject(m, "HAVE_XCB", Py_False); +#endif + PyDict_SetItemString(d, "PILLOW_VERSION", PyUnicode_FromString(version)); return 0; @@ -3937,8 +4245,9 @@ PyInit__imaging(void) { m = PyModule_Create(&module_def); - if (setup_module(m) < 0) + if (setup_module(m) < 0) { return NULL; + } return m; } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 0b22ab695..5693ccbab 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -31,7 +31,7 @@ http://www.cazabon.com\n\ #include "datetime.h" #include "lcms2.h" -#include "Imaging.h" +#include "libImaging/Imaging.h" #define PYCMSVERSION "1.0.0 pil" @@ -88,8 +88,9 @@ cms_profile_new(cmsHPROFILE profile) CmsProfileObject* self; self = PyObject_New(CmsProfileObject, &CmsProfile_Type); - if (!self) + if (!self) { return NULL; + } self->profile = profile; @@ -102,12 +103,13 @@ cms_profile_open(PyObject* self, PyObject* args) cmsHPROFILE hProfile; char* sProfile; - if (!PyArg_ParseTuple(args, "s:profile_open", &sProfile)) + if (!PyArg_ParseTuple(args, "s:profile_open", &sProfile)) { return NULL; + } hProfile = cmsOpenProfileFromFile(sProfile, "r"); if (!hProfile) { - PyErr_SetString(PyExc_IOError, "cannot open profile file"); + PyErr_SetString(PyExc_OSError, "cannot open profile file"); return NULL; } @@ -121,12 +123,13 @@ cms_profile_fromstring(PyObject* self, PyObject* args) char* pProfile; Py_ssize_t nProfile; - if (!PyArg_ParseTuple(args, "y#:profile_frombytes", &pProfile, &nProfile)) + if (!PyArg_ParseTuple(args, "y#:profile_frombytes", &pProfile, &nProfile)) { return NULL; + } hProfile = cmsOpenProfileFromMem(pProfile, nProfile); if (!hProfile) { - PyErr_SetString(PyExc_IOError, "cannot open profile from string"); + PyErr_SetString(PyExc_OSError, "cannot open profile from string"); return NULL; } @@ -150,18 +153,18 @@ cms_profile_tobytes(PyObject* self, PyObject* args) profile = ((CmsProfileObject*)CmsProfile)->profile; if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) { - PyErr_SetString(PyExc_IOError, "Could not determine profile size"); + PyErr_SetString(PyExc_OSError, "Could not determine profile size"); return NULL; } pProfile = (char*)malloc(nProfile); if (!pProfile) { - PyErr_SetString(PyExc_IOError, "Out of Memory"); + PyErr_SetString(PyExc_OSError, "Out of Memory"); return NULL; } if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) { - PyErr_SetString(PyExc_IOError, "Could not get profile"); + PyErr_SetString(PyExc_OSError, "Could not get profile"); free(pProfile); return NULL; } @@ -198,8 +201,9 @@ cms_transform_new(cmsHTRANSFORM transform, char* mode_in, char* mode_out) CmsTransformObject* self; self = PyObject_New(CmsTransformObject, &CmsTransform_Type); - if (!self) + if (!self) { return NULL; + } self->transform = transform; @@ -219,25 +223,6 @@ cms_transform_dealloc(CmsTransformObject* self) /* -------------------------------------------------------------------- */ /* internal functions */ -static const char* -findICmode(cmsColorSpaceSignature cs) -{ - switch (cs) { - case cmsSigXYZData: return "XYZ"; - case cmsSigLabData: return "LAB"; - case cmsSigLuvData: return "LUV"; - case cmsSigYCbCrData: return "YCbCr"; - case cmsSigYxyData: return "YXY"; - case cmsSigRgbData: return "RGB"; - case cmsSigGrayData: return "L"; - case cmsSigHsvData: return "HSV"; - case cmsSigHlsData: return "HLS"; - case cmsSigCmykData: return "CMYK"; - case cmsSigCmyData: return "CMY"; - default: return ""; /* other TBA */ - } -} - static cmsUInt32Number findLCMStype(char* PILmode) { @@ -292,17 +277,19 @@ pyCMSgetAuxChannelChannel (cmsUInt32Number format, int auxChannelNdx) if (T_SWAPFIRST(format) && T_DOSWAP(format)) { // reverse order, before anything but last extra is shifted last - if (auxChannelNdx == numExtras - 1) + if (auxChannelNdx == numExtras - 1) { return numColors + numExtras - 1; - else + } else { return numExtras - 2 - auxChannelNdx; + } } else if (T_SWAPFIRST(format)) { // in order, after color channels, but last extra is shifted to first - if (auxChannelNdx == numExtras - 1) + if (auxChannelNdx == numExtras - 1) { return 0; - else + } else { return numColors + 1 + auxChannelNdx; + } } else if (T_DOSWAP(format)) { // reverse order, before anything @@ -330,23 +317,26 @@ pyCMScopyAux (cmsHTRANSFORM hTransform, Imaging imDst, const Imaging imSrc) int e; // trivially copied - if (imDst == imSrc) + if (imDst == imSrc) { return; + } dstLCMSFormat = cmsGetTransformOutputFormat(hTransform); srcLCMSFormat = cmsGetTransformInputFormat(hTransform); // currently, all Pillow formats are chunky formats, but check it anyway - if (T_PLANAR(dstLCMSFormat) || T_PLANAR(srcLCMSFormat)) + if (T_PLANAR(dstLCMSFormat) || T_PLANAR(srcLCMSFormat)) { return; + } // copy only if channel format is identical, except OPTIMIZED is ignored as it // does not affect the aux channel if (T_FLOAT(dstLCMSFormat) != T_FLOAT(srcLCMSFormat) || T_FLAVOR(dstLCMSFormat) != T_FLAVOR(srcLCMSFormat) || T_ENDIAN16(dstLCMSFormat) != T_ENDIAN16(srcLCMSFormat) - || T_BYTES(dstLCMSFormat) != T_BYTES(srcLCMSFormat)) + || T_BYTES(dstLCMSFormat) != T_BYTES(srcLCMSFormat)) { return; + } numSrcExtras = T_EXTRA(srcLCMSFormat); numDstExtras = T_EXTRA(dstLCMSFormat); @@ -367,8 +357,9 @@ pyCMScopyAux (cmsHTRANSFORM hTransform, Imaging imDst, const Imaging imSrc) char* pDstExtras = imDst->image[y] + dstChannel * channelSize; const char* pSrcExtras = imSrc->image[y] + srcChannel * channelSize; - for (x = 0; x < xSize; x++) + for (x = 0; x < xSize; x++) { memcpy(pDstExtras + x * dstChunkSize, pSrcExtras + x * srcChunkSize, channelSize); + } } } } @@ -378,14 +369,16 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { int i; - if (im->xsize > imOut->xsize || im->ysize > imOut->ysize) + if (im->xsize > imOut->xsize || im->ysize > imOut->ysize) { return -1; + } Py_BEGIN_ALLOW_THREADS // transform color channels only - for (i = 0; i < im->ysize; i++) + for (i = 0; i < im->ysize; i++) { cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize); + } // lcms by default does nothing to the auxiliary channels leaving those // unchanged. To do "the right thing" here, i.e. maintain identical results @@ -417,8 +410,9 @@ _buildTransform(cmsHPROFILE hInputProfile, cmsHPROFILE hOutputProfile, char *sIn Py_END_ALLOW_THREADS - if (!hTransform) + if (!hTransform) { PyErr_SetString(PyExc_ValueError, "cannot build transform"); + } return hTransform; /* if NULL, an exception is set */ } @@ -442,8 +436,9 @@ _buildProofTransform(cmsHPROFILE hInputProfile, cmsHPROFILE hOutputProfile, cmsH Py_END_ALLOW_THREADS - if (!hTransform) + if (!hTransform) { PyErr_SetString(PyExc_ValueError, "cannot build proof transform"); + } return hTransform; /* if NULL, an exception is set */ } @@ -462,13 +457,15 @@ buildTransform(PyObject *self, PyObject *args) { cmsHTRANSFORM transform = NULL; - if (!PyArg_ParseTuple(args, "O!O!ss|ii:buildTransform", &CmsProfile_Type, &pInputProfile, &CmsProfile_Type, &pOutputProfile, &sInMode, &sOutMode, &iRenderingIntent, &cmsFLAGS)) + if (!PyArg_ParseTuple(args, "O!O!ss|ii:buildTransform", &CmsProfile_Type, &pInputProfile, &CmsProfile_Type, &pOutputProfile, &sInMode, &sOutMode, &iRenderingIntent, &cmsFLAGS)) { return NULL; + } transform = _buildTransform(pInputProfile->profile, pOutputProfile->profile, sInMode, sOutMode, iRenderingIntent, cmsFLAGS); - if (!transform) + if (!transform) { return NULL; + } return cms_transform_new(transform, sInMode, sOutMode); } @@ -487,13 +484,15 @@ buildProofTransform(PyObject *self, PyObject *args) cmsHTRANSFORM transform = NULL; - if (!PyArg_ParseTuple(args, "O!O!O!ss|iii:buildProofTransform", &CmsProfile_Type, &pInputProfile, &CmsProfile_Type, &pOutputProfile, &CmsProfile_Type, &pProofProfile, &sInMode, &sOutMode, &iRenderingIntent, &iProofIntent, &cmsFLAGS)) + if (!PyArg_ParseTuple(args, "O!O!O!ss|iii:buildProofTransform", &CmsProfile_Type, &pInputProfile, &CmsProfile_Type, &pOutputProfile, &CmsProfile_Type, &pProofProfile, &sInMode, &sOutMode, &iRenderingIntent, &iProofIntent, &cmsFLAGS)) { return NULL; + } transform = _buildProofTransform(pInputProfile->profile, pOutputProfile->profile, pProofProfile->profile, sInMode, sOutMode, iRenderingIntent, iProofIntent, cmsFLAGS); - if (!transform) + if (!transform) { return NULL; + } return cms_transform_new(transform, sInMode, sOutMode); @@ -509,8 +508,9 @@ cms_transform_apply(CmsTransformObject *self, PyObject *args) int result; - if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) + if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) { return NULL; + } im = (Imaging) idIn; imOut = (Imaging) idOut; @@ -532,8 +532,9 @@ createProfile(PyObject *self, PyObject *args) cmsCIExyY whitePoint; cmsBool result; - if (!PyArg_ParseTuple(args, "s|d:createProfile", &sColorSpace, &dColorTemp)) + if (!PyArg_ParseTuple(args, "s|d:createProfile", &sColorSpace, &dColorTemp)) { return NULL; + } if (strcmp(sColorSpace, "LAB") == 0) { if (dColorTemp > 0.0) { @@ -575,8 +576,9 @@ cms_profile_is_intent_supported(CmsProfileObject *self, PyObject *args) int intent; int direction; - if (!PyArg_ParseTuple(args, "ii:is_intent_supported", &intent, &direction)) + if (!PyArg_ParseTuple(args, "ii:is_intent_supported", &intent, &direction)) { return NULL; + } result = cmsIsIntentSupported(self->profile, intent, direction); @@ -602,8 +604,9 @@ cms_get_display_profile_win32(PyObject* self, PyObject* args) HANDLE handle = 0; int is_dc = 0; - if (!PyArg_ParseTuple(args, "|" F_HANDLE "i:get_display_profile", &handle, &is_dc)) + if (!PyArg_ParseTuple(args, "|" F_HANDLE "i:get_display_profile", &handle, &is_dc)) { return NULL; + } filename_size = sizeof(filename); @@ -615,8 +618,9 @@ cms_get_display_profile_win32(PyObject* self, PyObject* args) ReleaseDC((HWND) handle, dc); } - if (ok) + if (ok) { return PyUnicode_FromStringAndSize(filename, filename_size-1); + } Py_INCREF(Py_None); return Py_None; @@ -655,7 +659,7 @@ _profile_read_mlu(CmsProfileObject* self, cmsTagSignature info) buf = malloc(len); if (!buf) { - PyErr_SetString(PyExc_IOError, "Out of Memory"); + PyErr_SetString(PyExc_OSError, "Out of Memory"); return NULL; } /* Just in case the next call fails. */ @@ -745,10 +749,11 @@ _profile_read_ciexyz(CmsProfileObject* self, cmsTagSignature info, int multi) Py_INCREF(Py_None); return Py_None; } - if (multi) + if (multi) { return _xyz3_py(XYZ); - else + } else { return _xyz_py(XYZ); + } } static PyObject* @@ -826,8 +831,9 @@ static cmsBool _calculate_rgb_primaries(CmsProfileObject* self, cmsCIEXYZTRIPLE* // double array of RGB values with max on each identity hXYZ = cmsCreateXYZProfile(); - if (hXYZ == NULL) + if (hXYZ == NULL) { return 0; + } // transform from our profile to XYZ using doubles for highest precision hTransform = cmsCreateTransform(self->profile, TYPE_RGB_DBL, @@ -835,8 +841,9 @@ static cmsBool _calculate_rgb_primaries(CmsProfileObject* self, cmsCIEXYZTRIPLE* INTENT_RELATIVE_COLORIMETRIC, cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); cmsCloseProfile(hXYZ); - if (hTransform == NULL) + if (hTransform == NULL) { return 0; + } cmsDoTransform(hTransform, (void*) input, result, 3); cmsDeleteTransform(hTransform); @@ -881,8 +888,9 @@ _is_intent_supported(CmsProfileObject* self, int clut) /* Only valid for ICC Intents (otherwise we read invalid memory in lcms cmsio1.c). */ if (!(intent == INTENT_PERCEPTUAL || intent == INTENT_RELATIVE_COLORIMETRIC - || intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) + || intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) { continue; + } id = PyLong_FromLong((long) intent); entry = Py_BuildValue("(OOO)", @@ -929,92 +937,12 @@ static struct PyMethodDef cms_profile_methods[] = { {NULL, NULL} /* sentinel */ }; -static PyObject* -_profile_getattr(CmsProfileObject* self, cmsInfoType field) -{ - // UNDONE -- check that I'm getting the right fields on these. - // return PyUnicode_DecodeFSDefault(cmsTakeProductName(self->profile)); - //wchar_t buf[256]; -- UNDONE need wchar_t for unicode version. - char buf[256]; - cmsUInt32Number written; - written = cmsGetProfileInfoASCII(self->profile, - field, - "en", - "us", - buf, - 256); - if (written) { - return PyUnicode_FromString(buf); - } - // UNDONE suppressing error here by sending back blank string. - return PyUnicode_FromString(""); -} - -static PyObject* -cms_profile_getattr_product_desc(CmsProfileObject* self, void* closure) -{ - PyErr_WarnEx(PyExc_DeprecationWarning, - "product_desc is deprecated. Use Unicode profile_description instead.", 1); - // description was Description != 'Copyright' || or "%s - %s" (manufacturer, model) in 1.x - return _profile_getattr(self, cmsInfoDescription); -} - -/* use these four for the individual fields. - */ -static PyObject* -cms_profile_getattr_product_description(CmsProfileObject* self, void* closure) -{ - PyErr_WarnEx(PyExc_DeprecationWarning, - "product_description is deprecated. Use Unicode profile_description instead.", 1); - return _profile_getattr(self, cmsInfoDescription); -} - -static PyObject* -cms_profile_getattr_product_model(CmsProfileObject* self, void* closure) -{ - PyErr_WarnEx(PyExc_DeprecationWarning, - "product_model is deprecated. Use Unicode model instead.", 1); - return _profile_getattr(self, cmsInfoModel); -} - -static PyObject* -cms_profile_getattr_product_manufacturer(CmsProfileObject* self, void* closure) -{ - PyErr_WarnEx(PyExc_DeprecationWarning, - "product_manufacturer is deprecated. Use Unicode manufacturer instead.", 1); - return _profile_getattr(self, cmsInfoManufacturer); -} - -static PyObject* -cms_profile_getattr_product_copyright(CmsProfileObject* self, void* closure) -{ - PyErr_WarnEx(PyExc_DeprecationWarning, - "product_copyright is deprecated. Use Unicode copyright instead.", 1); - return _profile_getattr(self, cmsInfoCopyright); -} - static PyObject* cms_profile_getattr_rendering_intent(CmsProfileObject* self, void* closure) { return PyLong_FromLong(cmsGetHeaderRenderingIntent(self->profile)); } -static PyObject* -cms_profile_getattr_pcs(CmsProfileObject* self, void* closure) -{ - PyErr_WarnEx(PyExc_DeprecationWarning, - "pcs is deprecated. Use padded connection_space instead.", 1); - return PyUnicode_DecodeFSDefault(findICmode(cmsGetPCS(self->profile))); -} - -static PyObject* -cms_profile_getattr_color_space(CmsProfileObject* self, void* closure) -{ - PyErr_WarnEx(PyExc_DeprecationWarning, - "color_space is deprecated. Use padded xcolor_space instead.", 1); - return PyUnicode_DecodeFSDefault(findICmode(cmsGetColorSpace(self->profile))); -} - /* New-style unicode interfaces. */ static PyObject* cms_profile_getattr_copyright(CmsProfileObject* self, void* closure) @@ -1122,14 +1050,12 @@ cms_profile_getattr_device_class(CmsProfileObject* self, void* closure) return _profile_read_int_as_string(cmsGetDeviceClass(self->profile)); } -/* Duplicate of pcs, but uninterpreted. */ static PyObject* cms_profile_getattr_connection_space(CmsProfileObject* self, void* closure) { return _profile_read_int_as_string(cmsGetPCS(self->profile)); } -/* Duplicate of color_space, but uninterpreted. */ static PyObject* cms_profile_getattr_xcolor_space(CmsProfileObject* self, void* closure) { @@ -1276,8 +1202,9 @@ cms_profile_getattr_red_primary(CmsProfileObject* self, void* closure) cmsBool result = 0; cmsCIEXYZTRIPLE primaries; - if (cmsIsMatrixShaper(self->profile)) + if (cmsIsMatrixShaper(self->profile)) { result = _calculate_rgb_primaries(self, &primaries); + } if (! result) { Py_INCREF(Py_None); return Py_None; @@ -1292,8 +1219,9 @@ cms_profile_getattr_green_primary(CmsProfileObject* self, void* closure) cmsBool result = 0; cmsCIEXYZTRIPLE primaries; - if (cmsIsMatrixShaper(self->profile)) + if (cmsIsMatrixShaper(self->profile)) { result = _calculate_rgb_primaries(self, &primaries); + } if (! result) { Py_INCREF(Py_None); return Py_None; @@ -1308,8 +1236,9 @@ cms_profile_getattr_blue_primary(CmsProfileObject* self, void* closure) cmsBool result = 0; cmsCIEXYZTRIPLE primaries; - if (cmsIsMatrixShaper(self->profile)) + if (cmsIsMatrixShaper(self->profile)) { result = _calculate_rgb_primaries(self, &primaries); + } if (! result) { Py_INCREF(Py_None); return Py_None; @@ -1387,12 +1316,13 @@ cms_profile_getattr_icc_measurement_condition (CmsProfileObject* self, void* clo return Py_None; } - if (mc->Geometry == 1) + if (mc->Geometry == 1) { geo = "45/0, 0/45"; - else if (mc->Geometry == 2) + } else if (mc->Geometry == 2) { geo = "0d, d/0"; - else + } else { geo = "unknown"; + } return Py_BuildValue("{s:i,s:(ddd),s:s,s:d,s:s}", "observer", mc->Observer, @@ -1427,15 +1357,6 @@ cms_profile_getattr_icc_viewing_condition (CmsProfileObject* self, void* closure static struct PyGetSetDef cms_profile_getsetters[] = { - /* Compatibility interfaces. */ - { "product_desc", (getter) cms_profile_getattr_product_desc }, - { "product_description", (getter) cms_profile_getattr_product_description }, - { "product_manufacturer", (getter) cms_profile_getattr_product_manufacturer }, - { "product_model", (getter) cms_profile_getattr_product_model }, - { "product_copyright", (getter) cms_profile_getattr_product_copyright }, - { "pcs", (getter) cms_profile_getattr_pcs }, - { "color_space", (getter) cms_profile_getattr_color_space }, - /* New style interfaces. */ { "rendering_intent", (getter) cms_profile_getattr_rendering_intent }, { "creation_date", (getter) cms_profile_getattr_creation_date }, @@ -1454,7 +1375,6 @@ static struct PyGetSetDef cms_profile_getsetters[] = { { "header_model", (getter) cms_profile_getattr_header_model }, { "device_class", (getter) cms_profile_getattr_device_class }, { "connection_space", (getter) cms_profile_getattr_connection_space }, - /* Similar to color_space, but with full 4-letter signature (including trailing whitespace). */ { "xcolor_space", (getter) cms_profile_getattr_xcolor_space }, { "profile_id", (getter) cms_profile_getattr_profile_id }, { "is_matrix_shaper", (getter) cms_profile_getattr_is_matrix_shaper }, @@ -1577,6 +1497,7 @@ static int setup_module(PyObject* m) { PyObject *d; PyObject *v; + int vn; d = PyModule_GetDict(m); @@ -1591,7 +1512,17 @@ setup_module(PyObject* m) { d = PyModule_GetDict(m); - v = PyUnicode_FromFormat("%d.%d", LCMS_VERSION / 100, LCMS_VERSION % 100); + /* this check is also in PIL.features.pilinfo() */ +#if LCMS_VERSION < 2070 + vn = LCMS_VERSION; +#else + vn = cmsGetEncodedCMMversion(); +#endif + if (vn % 10) { + v = PyUnicode_FromFormat("%d.%d.%d", vn / 1000, (vn / 10) % 100, vn % 10); + } else { + v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); + } PyDict_SetItemString(d, "littlecms_version", v); return 0; @@ -1611,8 +1542,9 @@ PyInit__imagingcms(void) { m = PyModule_Create(&module_def); - if (setup_module(m) < 0) + if (setup_module(m) < 0) { return NULL; + } PyDateTime_IMPORT; diff --git a/src/_imagingft.c b/src/_imagingft.c index 62a4c283e..62db561ea 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -20,18 +20,22 @@ #define PY_SSIZE_T_CLEAN #include "Python.h" -#include "Imaging.h" +#include "libImaging/Imaging.h" #include #include FT_FREETYPE_H #include FT_GLYPH_H +#include FT_BITMAP_H #include FT_STROKER_H #include FT_MULTIPLE_MASTERS_H #include FT_SFNT_NAMES_H +#ifdef FT_COLOR_H +#include FT_COLOR_H +#endif #define KEEP_PY_UNICODE -#if !defined(_MSC_VER) +#ifndef _WIN32 #include #endif @@ -49,7 +53,7 @@ #define FT_ERROR_START_LIST { #define FT_ERROR_END_LIST { 0, 0 } }; -#include +#include "libImaging/raqm.h" #define LAYOUT_FALLBACK 0 #define LAYOUT_RAQM 1 @@ -81,6 +85,7 @@ typedef struct { static PyTypeObject Font_Type; +typedef const char* (*t_raqm_version_string) (void); typedef bool (*t_raqm_version_atleast)(unsigned int major, unsigned int minor, unsigned int micro); @@ -112,6 +117,7 @@ typedef void (*t_raqm_destroy) (raqm_t *rq); typedef struct { void* raqm; int version; + t_raqm_version_string version_string; t_raqm_version_atleast version_atleast; t_raqm_create create; t_raqm_set_text set_text; @@ -129,21 +135,22 @@ typedef struct { static p_raqm_func p_raqm; -/* round a 26.6 pixel coordinate to the nearest larger integer */ -#define PIXEL(x) ((((x)+63) & -64)>>6) +/* round a 26.6 pixel coordinate to the nearest integer */ +#define PIXEL(x) ((((x)+32) & -64)>>6) static PyObject* geterror(int code) { int i; - for (i = 0; ft_errors[i].message; i++) + for (i = 0; ft_errors[i].message; i++) { if (ft_errors[i].code == code) { - PyErr_SetString(PyExc_IOError, ft_errors[i].message); + PyErr_SetString(PyExc_OSError, ft_errors[i].message); return NULL; } + } - PyErr_SetString(PyExc_IOError, "unknown freetype error"); + PyErr_SetString(PyExc_OSError, "unknown freetype error"); return NULL; } @@ -154,20 +161,25 @@ setraqm(void) p_raqm.raqm = NULL; /* Microsoft needs a totally different system */ -#if !defined(_MSC_VER) +#ifndef _WIN32 p_raqm.raqm = dlopen("libraqm.so.0", RTLD_LAZY); if (!p_raqm.raqm) { p_raqm.raqm = dlopen("libraqm.dylib", RTLD_LAZY); } #else p_raqm.raqm = LoadLibrary("libraqm"); + /* MSYS */ + if (!p_raqm.raqm) { + p_raqm.raqm = LoadLibrary("libraqm-0"); + } #endif if (!p_raqm.raqm) { return 1; } -#if !defined(_MSC_VER) +#ifndef _WIN32 + p_raqm.version_string = (t_raqm_version_string)dlsym(p_raqm.raqm, "raqm_version_string"); p_raqm.version_atleast = (t_raqm_version_atleast)dlsym(p_raqm.raqm, "raqm_version_atleast"); p_raqm.create = (t_raqm_create)dlsym(p_raqm.raqm, "raqm_create"); p_raqm.set_text = (t_raqm_set_text)dlsym(p_raqm.raqm, "raqm_set_text"); @@ -201,6 +213,7 @@ setraqm(void) return 2; } #else + p_raqm.version_string = (t_raqm_version_string)GetProcAddress(p_raqm.raqm, "raqm_version_string"); p_raqm.version_atleast = (t_raqm_version_atleast)GetProcAddress(p_raqm.raqm, "raqm_version_atleast"); p_raqm.create = (t_raqm_create)GetProcAddress(p_raqm.raqm, "raqm_create"); p_raqm.set_text = (t_raqm_set_text)GetProcAddress(p_raqm.raqm, "raqm_set_text"); @@ -259,7 +272,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) if (!library) { PyErr_SetString( - PyExc_IOError, + PyExc_OSError, "failed to initialize FreeType library" ); return NULL; @@ -274,8 +287,9 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) self = PyObject_New(FontObject, &Font_Type); if (!self) { - if (filename) + if (filename) { PyMem_Free(filename); + } return NULL; } @@ -299,8 +313,9 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) } } - if (!error) + if (!error) { error = FT_Set_Pixel_Sizes(self->face, 0, size); + } if (!error && encoding && strlen((char*) encoding) == 4) { FT_Encoding encoding_tag = FT_MAKE_TAG( @@ -308,8 +323,9 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) ); error = FT_Select_Charmap(self->face, encoding_tag); } - if (filename) + if (filename) { PyMem_Free(filename); + } if (error) { if (self->font_bytes) { @@ -327,8 +343,9 @@ static int font_getchar(PyObject* string, int index, FT_ULong* char_out) { if (PyUnicode_Check(string)) { - if (index >= PyUnicode_GET_LENGTH(string)) + if (index >= PyUnicode_GET_LENGTH(string)) { return 0; + } *char_out = PyUnicode_READ_CHAR(string, index); return 1; } @@ -337,7 +354,7 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out) static size_t text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject *features, - const char* lang, GlyphInfo **glyph_info, int mask) + const char* lang, GlyphInfo **glyph_info, int mask, int color) { size_t i = 0, count = 0, start = 0; raqm_t *rq; @@ -351,27 +368,6 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * goto failed; } -#if (defined(PYPY_VERSION_NUM) && (PYPY_VERSION_NUM < 0x07020000)) - if (PyUnicode_Check(string)) { - Py_UNICODE *text = PyUnicode_AS_UNICODE(string); - Py_ssize_t size = PyUnicode_GET_SIZE(string); - if (! size) { - /* return 0 and clean up, no glyphs==no size, - and raqm fails with empty strings */ - goto failed; - } - if (!(*p_raqm.set_text)(rq, (const uint32_t *)(text), size)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); - goto failed; - } - if (lang) { - if (!(*p_raqm.set_language)(rq, lang, start, size)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); - goto failed; - } - } - } -#else if (PyUnicode_Check(string)) { Py_UCS4 *text = PyUnicode_AsUCS4Copy(string); Py_ssize_t size = PyUnicode_GET_LENGTH(string); @@ -393,7 +389,6 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * } } } -#endif else { PyErr_SetString(PyExc_TypeError, "expected string"); goto failed; @@ -401,11 +396,11 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * direction = RAQM_DIRECTION_DEFAULT; if (dir) { - if (strcmp(dir, "rtl") == 0) + if (strcmp(dir, "rtl") == 0) { direction = RAQM_DIRECTION_RTL; - else if (strcmp(dir, "ltr") == 0) + } else if (strcmp(dir, "ltr") == 0) { direction = RAQM_DIRECTION_LTR; - else if (strcmp(dir, "ttb") == 0) { + } else if (strcmp(dir, "ttb") == 0) { direction = RAQM_DIRECTION_TTB; if (p_raqm.version_atleast == NULL || !(*p_raqm.version_atleast)(0, 7, 0)) { PyErr_SetString(PyExc_ValueError, "libraqm 0.7 or greater required for 'ttb' direction"); @@ -443,8 +438,9 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * if (PyUnicode_Check(item)) { bytes = PyUnicode_AsUTF8String(item); - if (bytes == NULL) + if (bytes == NULL) { goto failed; + } feature = PyBytes_AS_STRING(bytes); size = PyBytes_GET_SIZE(bytes); } @@ -515,7 +511,7 @@ failed: static size_t text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObject *features, - const char* lang, GlyphInfo **glyph_info, int mask) + const char* lang, GlyphInfo **glyph_info, int mask, int color) { int error, load_flags; FT_ULong ch; @@ -547,10 +543,15 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje return 0; } - load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP; + load_flags = FT_LOAD_DEFAULT; if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } +#ifdef FT_LOAD_COLOR + if (color) { + load_flags |= FT_LOAD_COLOR; + } +#endif for (i = 0; font_getchar(string, i, &ch); i++) { (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); @@ -564,13 +565,15 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje if (kerning && last_index && (*glyph_info)[i].index) { FT_Vector delta; if (FT_Get_Kerning(self->face, last_index, (*glyph_info)[i].index, - ft_kerning_default,&delta) == 0) - (*glyph_info)[i-1].x_advance += PIXEL(delta.x); - (*glyph_info)[i-1].y_advance += PIXEL(delta.y); + ft_kerning_default,&delta) == 0) { + (*glyph_info)[i-1].x_advance += PIXEL(delta.x); + (*glyph_info)[i-1].y_advance += PIXEL(delta.y); + } } (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; - (*glyph_info)[i].y_advance = glyph->metrics.vertAdvance; + // y_advance is only used in ttb, which is not supported by basic layout + (*glyph_info)[i].y_advance = 0; last_index = (*glyph_info)[i].index; (*glyph_info)[i].cluster = ch; } @@ -579,115 +582,181 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje static size_t text_layout(PyObject* string, FontObject* self, const char* dir, PyObject *features, - const char* lang, GlyphInfo **glyph_info, int mask) + const char* lang, GlyphInfo **glyph_info, int mask, int color) { size_t count; if (p_raqm.raqm && self->layout_engine == LAYOUT_RAQM) { - count = text_layout_raqm(string, self, dir, features, lang, glyph_info, mask); + count = text_layout_raqm(string, self, dir, features, lang, glyph_info, mask, color); } else { - count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask); + count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask, color); } return count; } static PyObject* -font_getsize(FontObject* self, PyObject* args) +font_getlength(FontObject* self, PyObject* args) { - int x_position, x_max, x_min, y_max, y_min; - FT_Face face; - int xoffset, yoffset; - int horizontal_dir; + int length; /* length along primary axis, in 26.6 precision */ + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t i, count; /* glyph_info index and length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; const char *dir = NULL; const char *lang = NULL; - size_t i, count; - GlyphInfo *glyph_info = NULL; PyObject *features = Py_None; + PyObject *string; /* calculate size and bearing for a given string */ - PyObject* string; - if (!PyArg_ParseTuple(args, "O|zOz:getsize", &string, &dir, &features, &lang)) + if (!PyArg_ParseTuple(args, "O|zzOz:getlength", &string, &mode, &dir, &features, &lang)) { return NULL; + } - count = text_layout(string, self, dir, features, lang, &glyph_info, 0); + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); if (PyErr_Occurred()) { return NULL; } - face = NULL; - xoffset = yoffset = 0; - x_position = x_max = x_min = y_max = y_min = 0; + length = 0; + for (i = 0; i < count; i++) { + if (horizontal_dir) { + length += glyph_info[i].x_advance; + } else { + length -= glyph_info[i].y_advance; + } + } + + if (glyph_info) { + PyMem_Free(glyph_info); + glyph_info = NULL; + } + + return PyLong_FromLong(length); +} + +static PyObject* +font_getsize(FontObject* self, PyObject* args) +{ + int position; /* pen position along primary axis, in 26.6 precision */ + int advanced; /* pen position along primary axis, in pixels */ + int px, py; /* position of current glyph, in pixels */ + int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ + int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ + int load_flags; /* FreeType load_flags parameter */ + int error; + FT_Face face; + FT_Glyph glyph; + FT_BBox bbox; /* glyph bounding box */ + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t i, count; /* glyph_info index and length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; + const char *dir = NULL; + const char *lang = NULL; + const char *anchor = NULL; + PyObject *features = Py_None; + PyObject *string; + + /* calculate size and bearing for a given string */ + + if (!PyArg_ParseTuple(args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { + return NULL; + } horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - for (i = 0; i < count; i++) { - int index, error, offset, x_advanced; - FT_BBox bbox; - FT_Glyph glyph; - face = self->face; - index = glyph_info[i].index; - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 - * Yifu Yu, 2014-10-15 - */ - error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); - if (error) - return geterror(error); - if (i == 0) { - if (horizontal_dir) { - if (face->glyph->metrics.horiBearingX < 0) { - xoffset = face->glyph->metrics.horiBearingX; - x_position -= xoffset; - } - } else { - if (face->glyph->metrics.vertBearingY < 0) { - yoffset = face->glyph->metrics.vertBearingY; - y_max -= yoffset; - } + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + if (anchor == NULL) { + anchor = horizontal_dir ? "la" : "lt"; + } + if (strlen(anchor) != 2) { + goto bad_anchor; + } + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { + return NULL; + } + + load_flags = FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } +#ifdef FT_LOAD_COLOR + if (color) { + load_flags |= FT_LOAD_COLOR; + } +#endif + + /* + * text bounds are given by: + * - bounding boxes of individual glyphs + * - pen line, i.e. 0 to `advanced` along primary axis + * this means point (0, 0) is part of the text bounding box + */ + face = NULL; + position = x_min = x_max = y_min = y_max = 0; + for (i = 0; i < count; i++) { + face = self->face; + + if (horizontal_dir) { + px = PIXEL(position + glyph_info[i].x_offset); + py = PIXEL(glyph_info[i].y_offset); + + position += glyph_info[i].x_advance; + advanced = PIXEL(position); + if (advanced > x_max) { + x_max = advanced; + } + } else { + px = PIXEL(glyph_info[i].x_offset); + py = PIXEL(position + glyph_info[i].y_offset); + + position += glyph_info[i].y_advance; + advanced = PIXEL(position); + if (advanced < y_min) { + y_min = advanced; } } - FT_Get_Glyph(face->glyph, &glyph); - FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &bbox); - if (horizontal_dir) { - x_position += glyph_info[i].x_advance; + error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); + if (error) { + return geterror(error); + } - x_advanced = x_position; - offset = glyph_info[i].x_advance - - face->glyph->metrics.width - - face->glyph->metrics.horiBearingX; - if (offset < 0) - x_advanced -= offset; - if (x_advanced > x_max) - x_max = x_advanced; + error = FT_Get_Glyph(face->glyph, &glyph); + if (error) { + return geterror(error); + } - bbox.yMax += glyph_info[i].y_offset; - bbox.yMin += glyph_info[i].y_offset; - if (bbox.yMax > y_max) - y_max = bbox.yMax; - if (bbox.yMin < y_min) - y_min = bbox.yMin; - - // find max distance of baseline from top - if (face->glyph->metrics.horiBearingY > yoffset) - yoffset = face->glyph->metrics.horiBearingY; - } else { - y_max -= glyph_info[i].y_advance; - - if (i == count - 1) { - // trim end gap from final glyph - int offset; - offset = -glyph_info[i].y_advance - - face->glyph->metrics.height - - face->glyph->metrics.vertBearingY; - if (offset < 0) - y_max -= offset; - } - - if (bbox.xMax > x_max) - x_max = bbox.xMax; - if (i == 0 || bbox.xMin < x_min) - x_min = bbox.xMin; + FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); + bbox.xMax += px; + if (bbox.xMax > x_max) { + x_max = bbox.xMax; + } + bbox.xMin += px; + if (bbox.xMin < x_min) { + x_min = bbox.xMin; + } + bbox.yMax += py; + if (bbox.yMax > y_max) { + y_max = bbox.yMax; + } + bbox.yMin += py; + if (bbox.yMin < y_min) { + y_min = bbox.yMin; } FT_Done_Glyph(glyph); @@ -698,71 +767,156 @@ font_getsize(FontObject* self, PyObject* args) glyph_info = NULL; } + x_anchor = y_anchor = 0; if (face) { if (horizontal_dir) { - // left bearing - if (xoffset < 0) - x_max -= xoffset; - else - xoffset = 0; - - /* difference between the font ascender and the distance of - * the baseline from the top */ - yoffset = PIXEL(self->face->size->metrics.ascender - yoffset); + switch (anchor[0]) { + case 'l': // left + x_anchor = 0; + break; + case 'm': // middle (left + right) / 2 + x_anchor = PIXEL(position / 2); + break; + case 'r': // right + x_anchor = PIXEL(position); + break; + case 's': // vertical baseline + default: + goto bad_anchor; + } + switch (anchor[1]) { + case 'a': // ascender + y_anchor = PIXEL(self->face->size->metrics.ascender); + break; + case 't': // top + y_anchor = y_max; + break; + case 'm': // middle (ascender + descender) / 2 + y_anchor = PIXEL((self->face->size->metrics.ascender + self->face->size->metrics.descender) / 2); + break; + case 's': // horizontal baseline + y_anchor = 0; + break; + case 'b': // bottom + y_anchor = y_min; + break; + case 'd': // descender + y_anchor = PIXEL(self->face->size->metrics.descender); + break; + default: + goto bad_anchor; + } } else { - // top bearing - if (yoffset < 0) - y_max -= yoffset; - else - yoffset = 0; + switch (anchor[0]) { + case 'l': // left + x_anchor = x_min; + break; + case 'm': // middle (left + right) / 2 + x_anchor = (x_min + x_max) / 2; + break; + case 'r': // right + x_anchor = x_max; + break; + case 's': // vertical baseline + x_anchor = 0; + break; + default: + goto bad_anchor; + } + switch (anchor[1]) { + case 't': // top + y_anchor = 0; + break; + case 'm': // middle (top + bottom) / 2 + y_anchor = PIXEL(position / 2); + break; + case 'b': // bottom + y_anchor = PIXEL(position); + break; + case 'a': // ascender + case 's': // horizontal baseline + case 'd': // descender + default: + goto bad_anchor; + } } } return Py_BuildValue( "(ii)(ii)", - PIXEL(x_max - x_min), PIXEL(y_max - y_min), - PIXEL(xoffset), yoffset - ); + (x_max - x_min), (y_max - y_min), + (-x_anchor + x_min), -(-y_anchor + y_max) + ); + +bad_anchor: + PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); + return NULL; } static PyObject* font_render(FontObject* self, PyObject* args) { - int x; - unsigned int y; - Imaging im; - int index, error, ascender, horizontal_dir; - int load_flags; - unsigned char *source; + int x, y; /* pen position, in 26.6 precision */ + int px, py; /* position of current glyph, in pixels */ + int x_min, y_max; /* text offset in 26.6 precision */ + int load_flags; /* FreeType load_flags parameter */ + int error; FT_Glyph glyph; FT_GlyphSlot glyph_slot; FT_Bitmap bitmap; + FT_Bitmap bitmap_converted; /* initialized lazily, for non-8bpp fonts */ FT_BitmapGlyph bitmap_glyph; - int stroke_width = 0; FT_Stroker stroker = NULL; - FT_Int left; - /* render string into given buffer (the buffer *must* have - the right size, or this will crash) */ - PyObject* string; + int bitmap_converted_ready = 0; /* has bitmap_converted been initialized */ + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t i, count; /* glyph_info index and length */ + int xx, yy; /* pixel offset of current glyph bitmap */ + int x0, x1; /* horizontal bounds of glyph bitmap to copy */ + unsigned int bitmap_y; /* glyph bitmap y index */ + unsigned char *source; /* glyph bitmap source buffer */ + unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ + Imaging im; Py_ssize_t id; - int mask = 0; - int temp; - int xx, x0, x1; - int yy; - unsigned int bitmap_y; + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + int stroke_width = 0; + PY_LONG_LONG foreground_ink_long = 0; + unsigned int foreground_ink; + const char *mode = NULL; const char *dir = NULL; const char *lang = NULL; - size_t i, count; - GlyphInfo *glyph_info; - PyObject *features = NULL; + PyObject *features = Py_None; + PyObject* string; - if (!PyArg_ParseTuple(args, "On|izOzi:render", &string, &id, &mask, &dir, &features, &lang, - &stroke_width)) { + /* render string into given buffer (the buffer *must* have + the right size, or this will crash) */ + + if (!PyArg_ParseTuple(args, "On|zzOziL:render", &string, &id, &mode, &dir, &features, &lang, + &stroke_width, &foreground_ink_long)) { return NULL; } - glyph_info = NULL; - count = text_layout(string, self, dir, features, lang, &glyph_info, mask); + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + foreground_ink = foreground_ink_long; + +#ifdef FT_COLOR_H + if (color) { + FT_Color foreground_color; + FT_Byte* ink = (FT_Byte*)&foreground_ink; + foreground_color.red = ink[0]; + foreground_color.green = ink[1]; + foreground_color.blue = ink[2]; + foreground_color.alpha = (FT_Byte) 255; /* ink alpha is handled in ImageDraw.text */ + FT_Palette_Set_Foreground_Color(self->face, foreground_color); + } +#endif + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); if (PyErr_Occurred()) { return NULL; } @@ -780,19 +934,26 @@ font_render(FontObject* self, PyObject* args) } im = (Imaging) id; - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ - load_flags = FT_LOAD_NO_BITMAP; - if (stroker == NULL) { - load_flags |= FT_LOAD_RENDER; - } + load_flags = FT_LOAD_DEFAULT; if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } +#ifdef FT_LOAD_COLOR + if (color) { + load_flags |= FT_LOAD_COLOR; + } +#endif - ascender = 0; + /* + * calculate x_min and y_max + * must match font_getsize or there may be clipping! + */ + x = y = x_min = y_max = 0; for (i = 0; i < count; i++) { - index = glyph_info[i].index; - error = FT_Load_Glyph(self->face, index, load_flags); + px = PIXEL(x + glyph_info[i].x_offset); + py = PIXEL(y + glyph_info[i].y_offset); + + error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); if (error) { return geterror(error); } @@ -800,17 +961,30 @@ font_render(FontObject* self, PyObject* args) glyph_slot = self->face->glyph; bitmap = glyph_slot->bitmap; - temp = bitmap.rows - glyph_slot->bitmap_top; - temp -= PIXEL(glyph_info[i].y_offset); - if (temp > ascender) - ascender = temp; + if (glyph_slot->bitmap_top + py > y_max) { + y_max = glyph_slot->bitmap_top + py; + } + if (glyph_slot->bitmap_left + px < x_min) { + x_min = glyph_slot->bitmap_left + px; + } + + x += glyph_info[i].x_advance; + y += glyph_info[i].y_advance; + } + + /* set pen position to text origin */ + x = (-x_min + stroke_width) << 6; + y = (-y_max + (-stroke_width)) << 6; + + if (stroker == NULL) { + load_flags |= FT_LOAD_RENDER; } - x = y = 0; - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; for (i = 0; i < count; i++) { - index = glyph_info[i].index; - error = FT_Load_Glyph(self->face, index, load_flags); + px = PIXEL(x + glyph_info[i].x_offset); + py = PIXEL(y + glyph_info[i].y_offset); + + error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags); if (error) { return geterror(error); } @@ -832,83 +1006,160 @@ font_render(FontObject* self, PyObject* args) bitmap_glyph = (FT_BitmapGlyph)glyph; bitmap = bitmap_glyph->bitmap; - left = bitmap_glyph->left; + xx = px + bitmap_glyph->left; + yy = -(py + bitmap_glyph->top); } else { bitmap = glyph_slot->bitmap; - left = glyph_slot->bitmap_left; + xx = px + glyph_slot->bitmap_left; + yy = -(py + glyph_slot->bitmap_top); } - if (horizontal_dir) { - if (i == 0 && glyph_slot->metrics.horiBearingX < 0) { - x = -glyph_slot->metrics.horiBearingX; - } - xx = PIXEL(x) + left; - xx += PIXEL(glyph_info[i].x_offset) + stroke_width; - } else { - if (glyph_slot->metrics.vertBearingX < 0) { - x = -glyph_slot->metrics.vertBearingX; - } - xx = im->xsize / 2 - bitmap.width / 2; + /* convert non-8bpp bitmaps */ + switch (bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + convert_scale = 255; + break; + case FT_PIXEL_MODE_GRAY2: + convert_scale = 255 / 3; + break; + case FT_PIXEL_MODE_GRAY4: + convert_scale = 255 / 15; + break; + default: + convert_scale = 1; + } + switch (bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + case FT_PIXEL_MODE_GRAY2: + case FT_PIXEL_MODE_GRAY4: + if (!bitmap_converted_ready) { + +#if FREETYPE_MAJOR > 2 ||\ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 6) + FT_Bitmap_Init(&bitmap_converted); +#else + FT_Bitmap_New(&bitmap_converted); +#endif + bitmap_converted_ready = 1; + } + error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1); + if (error) { + geterror(error); + goto glyph_error; + } + bitmap = bitmap_converted; + /* bitmap is now FT_PIXEL_MODE_GRAY, fall through */ + case FT_PIXEL_MODE_GRAY: + break; +#ifdef FT_LOAD_COLOR + case FT_PIXEL_MODE_BGRA: + if (color) { + break; + } + /* we didn't ask for color, fall through to default */ +#endif + default: + PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); + goto glyph_error; } + /* clip glyph bitmap width to target image bounds */ x0 = 0; x1 = bitmap.width; - if (xx < 0) + if (xx < 0) { x0 = -xx; - if (xx + x1 > im->xsize) + } + if (xx + x1 > im->xsize) { x1 = im->xsize - xx; + } source = (unsigned char*) bitmap.buffer; - for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++) { - if (horizontal_dir) { - yy = bitmap_y + im->ysize - (PIXEL(glyph_slot->metrics.horiBearingY) + ascender); - yy -= PIXEL(glyph_info[i].y_offset) + stroke_width * 2; - } else { - yy = bitmap_y + PIXEL(y + glyph_slot->metrics.vertBearingY) + ascender; - yy += PIXEL(glyph_info[i].y_offset); - } + for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) { + /* clip glyph bitmap height to target image bounds */ if (yy >= 0 && yy < im->ysize) { - // blend this glyph into the buffer - unsigned char *target = im->image8[yy] + xx; - if (mask) { - // use monochrome mask (on palette images, etc) - int j, k, m = 128; - for (j = k = 0; j < x1; j++) { - if (j >= x0 && (source[k] & m)) - target[j] = 255; - if (!(m >>= 1)) { - m = 128; - k++; + /* blend this glyph into the buffer */ + int k; + unsigned char v; + unsigned char* target; + if (color) { + /* target[RGB] returns the color, target[A] returns the mask */ + /* target bands get split again in ImageDraw.text */ + target = im->image[yy] + xx * 4; + } else { + target = im->image8[yy] + xx; + } +#ifdef FT_LOAD_COLOR + 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]; + } + } + } else +#endif + 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; + } + } + } else { + for (k = x0; k < x1; k++) { + v = source[k] * convert_scale; + if (target[k] < v) { + target[k] = v; + } } } } else { - // use antialiased rendering - int k; - for (k = x0; k < x1; k++) { - if (target[k] < source[k]) - target[k] = source[k]; - } + PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); + goto glyph_error; } } source += bitmap.pitch; } x += glyph_info[i].x_advance; - y -= glyph_info[i].y_advance; + y += glyph_info[i].y_advance; if (stroker != NULL) { FT_Done_Glyph(glyph); } } + if (bitmap_converted_ready) { + FT_Bitmap_Done(library, &bitmap_converted); + } FT_Stroker_Done(stroker); PyMem_Del(glyph_info); Py_RETURN_NONE; + +glyph_error: + if (stroker != NULL) { + FT_Done_Glyph(glyph); + } + if (bitmap_converted_ready) { + FT_Bitmap_Done(library, &bitmap_converted); + } + FT_Stroker_Done(stroker); + PyMem_Del(glyph_info); + return NULL; } #if FREETYPE_MAJOR > 2 ||\ (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) ||\ (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) static PyObject* - font_getvarnames(FontObject* self, PyObject* args) + font_getvarnames(FontObject* self) { int error; FT_UInt i, j, num_namedstyles, name_count; @@ -917,8 +1168,9 @@ font_render(FontObject* self, PyObject* args) PyObject *list_names, *list_name; error = FT_Get_MM_Var(self->face, &master); - if (error) + if (error) { return geterror(error); + } num_namedstyles = master->num_namedstyles; list_names = PyList_New(num_namedstyles); @@ -926,12 +1178,14 @@ font_render(FontObject* self, PyObject* args) name_count = FT_Get_Sfnt_Name_Count(self->face); for (i = 0; i < name_count; i++) { error = FT_Get_Sfnt_Name(self->face, i, &name); - if (error) + if (error) { return geterror(error); + } for (j = 0; j < num_namedstyles; j++) { - if (PyList_GetItem(list_names, j) != NULL) + if (PyList_GetItem(list_names, j) != NULL) { continue; + } if (master->namedstyle[j].strid == name.name_id) { list_name = Py_BuildValue("y#", name.string, name.string_len); @@ -947,7 +1201,7 @@ font_render(FontObject* self, PyObject* args) } static PyObject* - font_getvaraxes(FontObject* self, PyObject* args) + font_getvaraxes(FontObject* self) { int error; FT_UInt i, j, num_axis, name_count; @@ -956,8 +1210,9 @@ font_render(FontObject* self, PyObject* args) FT_SfntName name; PyObject *list_axes, *list_axis, *axis_name; error = FT_Get_MM_Var(self->face, &master); - if (error) + if (error) { return geterror(error); + } num_axis = master->num_axis; name_count = FT_Get_Sfnt_Name_Count(self->face); @@ -976,8 +1231,9 @@ font_render(FontObject* self, PyObject* args) for (j = 0; j < name_count; j++) { error = FT_Get_Sfnt_Name(self->face, j, &name); - if (error) + if (error) { return geterror(error); + } if (name.name_id == axis.strid) { axis_name = Py_BuildValue("y#", name.string, name.string_len); @@ -1000,12 +1256,14 @@ font_render(FontObject* self, PyObject* args) int error; int instance_index; - if (!PyArg_ParseTuple(args, "i", &instance_index)) + if (!PyArg_ParseTuple(args, "i", &instance_index)) { return NULL; + } error = FT_Set_Named_Instance(self->face, instance_index); - if (error) + if (error) { return geterror(error); + } Py_INCREF(Py_None); return Py_None; @@ -1020,8 +1278,9 @@ font_render(FontObject* self, PyObject* args) Py_ssize_t i, num_coords; FT_Fixed *coords; FT_Fixed coord; - if (!PyArg_ParseTuple(args, "O", &axes)) + if (!PyArg_ParseTuple(args, "O", &axes)) { return NULL; + } if (!PyList_Check(axes)) { PyErr_SetString(PyExc_TypeError, "argument must be a list"); @@ -1035,13 +1294,13 @@ font_render(FontObject* self, PyObject* args) } for (i = 0; i < num_coords; i++) { item = PyList_GET_ITEM(axes, i); - if (PyFloat_Check(item)) + if (PyFloat_Check(item)) { coord = PyFloat_AS_DOUBLE(item); - else if (PyLong_Check(item)) + } else if (PyLong_Check(item)) { coord = (float) PyLong_AS_LONG(item); - else if (PyNumber_Check(item)) + } else if (PyNumber_Check(item)) { coord = PyFloat_AsDouble(item); - else { + } else { free(coords); PyErr_SetString(PyExc_TypeError, "list must contain numbers"); return NULL; @@ -1051,8 +1310,9 @@ font_render(FontObject* self, PyObject* args) error = FT_Set_Var_Design_Coordinates(self->face, num_coords, coords); free(coords); - if (error) + if (error) { return geterror(error); + } Py_INCREF(Py_None); return Py_None; @@ -1074,11 +1334,12 @@ font_dealloc(FontObject* self) static PyMethodDef font_methods[] = { {"render", (PyCFunction) font_render, METH_VARARGS}, {"getsize", (PyCFunction) font_getsize, METH_VARARGS}, + {"getlength", (PyCFunction) font_getlength, METH_VARARGS}, #if FREETYPE_MAJOR > 2 ||\ (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) ||\ (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) - {"getvarnames", (PyCFunction) font_getvarnames, METH_VARARGS }, - {"getvaraxes", (PyCFunction) font_getvaraxes, METH_VARARGS }, + {"getvarnames", (PyCFunction) font_getvarnames, METH_NOARGS }, + {"getvaraxes", (PyCFunction) font_getvaraxes, METH_NOARGS }, {"setvarname", (PyCFunction) font_setvarname, METH_VARARGS}, {"setvaraxes", (PyCFunction) font_setvaraxes, METH_VARARGS}, #endif @@ -1088,16 +1349,18 @@ static PyMethodDef font_methods[] = { static PyObject* font_getattr_family(FontObject* self, void* closure) { - if (self->face->family_name) + if (self->face->family_name) { return PyUnicode_FromString(self->face->family_name); + } Py_RETURN_NONE; } static PyObject* font_getattr_style(FontObject* self, void* closure) { - if (self->face->style_name) + if (self->face->style_name) { return PyUnicode_FromString(self->face->style_name); + } Py_RETURN_NONE; } @@ -1198,8 +1461,9 @@ setup_module(PyObject* m) { /* Ready object type */ PyType_Ready(&Font_Type); - if (FT_Init_FreeType(&library)) + if (FT_Init_FreeType(&library)) { return 0; /* leave it uninitialized */ + } FT_Library_Version(library, &major, &minor, &patch); @@ -1210,6 +1474,9 @@ setup_module(PyObject* m) { setraqm(); v = PyBool_FromLong(!!p_raqm.raqm); PyDict_SetItemString(d, "HAVE_RAQM", v); + if (p_raqm.version_string) { + PyDict_SetItemString(d, "raqm_version", PyUnicode_FromString(p_raqm.version_string())); + } return 0; } @@ -1228,8 +1495,9 @@ PyInit__imagingft(void) { m = PyModule_Create(&module_def); - if (setup_module(m) < 0) + if (setup_module(m) < 0) { return NULL; + } return m; } diff --git a/src/_imagingmath.c b/src/_imagingmath.c index bc66a581a..b99356d14 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -15,7 +15,7 @@ #include "Python.h" -#include "Imaging.h" +#include "libImaging/Imaging.h" #include "math.h" #include "float.h" @@ -88,12 +88,14 @@ void name(Imaging out, Imaging im1, Imaging im2)\ static int powi(int x, int y) { double v = pow(x, y) + 0.5; - if (errno == EDOM) + if (errno == EDOM) { return 0; - if (v < MIN_INT32) + } + if (v < MIN_INT32) { v = MIN_INT32; - else if (v > MAX_INT32) + } else if (v > MAX_INT32) { v = MAX_INT32; + } return (int) v; } @@ -167,8 +169,9 @@ _unop(PyObject* self, PyObject* args) void (*unop)(Imaging, Imaging); Py_ssize_t op, i0, i1; - if (!PyArg_ParseTuple(args, "nnn", &op, &i0, &i1)) + if (!PyArg_ParseTuple(args, "nnn", &op, &i0, &i1)) { return NULL; + } out = (Imaging) i0; im1 = (Imaging) i1; @@ -190,8 +193,9 @@ _binop(PyObject* self, PyObject* args) void (*binop)(Imaging, Imaging, Imaging); Py_ssize_t op, i0, i1, i2; - if (!PyArg_ParseTuple(args, "nnnn", &op, &i0, &i1, &i2)) + if (!PyArg_ParseTuple(args, "nnnn", &op, &i0, &i1, &i2)) { return NULL; + } out = (Imaging) i0; im1 = (Imaging) i1; @@ -215,8 +219,9 @@ static void install(PyObject *d, char* name, void* value) { PyObject *v = PyLong_FromSsize_t((Py_ssize_t) value); - if (!v || PyDict_SetItemString(d, name, v)) + if (!v || PyDict_SetItemString(d, name, v)) { PyErr_Clear(); + } Py_XDECREF(v); } @@ -286,8 +291,9 @@ PyInit__imagingmath(void) { m = PyModule_Create(&module_def); - if (setup_module(m) < 0) + if (setup_module(m) < 0) { return NULL; + } return m; } diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 050ae9f02..0fe54ab77 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -12,7 +12,7 @@ */ #include "Python.h" -#include "Imaging.h" +#include "libImaging/Imaging.h" #define LUT_SIZE (1<<9) @@ -85,8 +85,9 @@ apply(PyObject *self, PyObject* args) /* zero boundary conditions. TBD support other modes */ outrow[0] = outrow[width-1] = 0; if (row_idx==0 || row_idx == height-1) { - for(col_idx=0; col_idxinterp; } diff --git a/src/_webp.c b/src/_webp.c index 4581ef89d..6c6ca2aa6 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -1,6 +1,6 @@ #define PY_SSIZE_T_CLEAN #include -#include "Imaging.h" +#include "libImaging/Imaging.h" #include #include #include @@ -21,6 +21,14 @@ #endif +void ImagingSectionEnter(ImagingSectionCookie* cookie) { + *cookie = (PyThreadState *) PyEval_SaveThread(); +} + +void ImagingSectionLeave(ImagingSectionCookie* cookie) { + PyEval_RestoreThread((PyThreadState*) *cookie); +} + /* -------------------------------------------------------------------- */ /* WebP Muxer Error Handling */ /* -------------------------------------------------------------------- */ @@ -62,7 +70,7 @@ PyObject* HandleMuxError(WebPMuxError err, char* chunk) { case WEBP_MUX_BAD_DATA: case WEBP_MUX_NOT_ENOUGH_DATA: - PyErr_SetString(PyExc_IOError, message); + PyErr_SetString(PyExc_OSError, message); break; default: @@ -369,7 +377,7 @@ PyObject* _anim_decoder_dealloc(PyObject* self) Py_RETURN_NONE; } -PyObject* _anim_decoder_get_info(PyObject* self, PyObject* args) +PyObject* _anim_decoder_get_info(PyObject* self) { WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; WebPAnimInfo* info = &(decp->info); @@ -406,7 +414,7 @@ PyObject* _anim_decoder_get_chunk(PyObject* self, PyObject* args) return ret; } -PyObject* _anim_decoder_get_next(PyObject* self, PyObject* args) +PyObject* _anim_decoder_get_next(PyObject* self) { uint8_t* buf; int timestamp; @@ -415,7 +423,7 @@ PyObject* _anim_decoder_get_next(PyObject* self, PyObject* args) WebPAnimDecoderObject* decp = (WebPAnimDecoderObject*)self; if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) { - PyErr_SetString(PyExc_IOError, "failed to read next frame"); + PyErr_SetString(PyExc_OSError, "failed to read next frame"); return NULL; } @@ -428,13 +436,7 @@ PyObject* _anim_decoder_get_next(PyObject* self, PyObject* args) return ret; } -PyObject* _anim_decoder_has_more_frames(PyObject* self, PyObject* args) -{ - WebPAnimDecoderObject* decp = (WebPAnimDecoderObject*)self; - return Py_BuildValue("i", WebPAnimDecoderHasMoreFrames(decp->dec)); -} - -PyObject* _anim_decoder_reset(PyObject* self, PyObject* args) +PyObject* _anim_decoder_reset(PyObject* self) { WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; WebPAnimDecoderReset(decp->dec); @@ -489,11 +491,10 @@ static PyTypeObject WebPAnimEncoder_Type = { // WebPAnimDecoder methods static struct PyMethodDef _anim_decoder_methods[] = { - {"get_info", (PyCFunction)_anim_decoder_get_info, METH_VARARGS, "get_info"}, + {"get_info", (PyCFunction)_anim_decoder_get_info, METH_NOARGS, "get_info"}, {"get_chunk", (PyCFunction)_anim_decoder_get_chunk, METH_VARARGS, "get_chunk"}, - {"get_next", (PyCFunction)_anim_decoder_get_next, METH_VARARGS, "get_next"}, - {"has_more_frames", (PyCFunction)_anim_decoder_has_more_frames, METH_VARARGS, "has_more_frames"}, - {"reset", (PyCFunction)_anim_decoder_reset, METH_VARARGS, "reset"}, + {"get_next", (PyCFunction)_anim_decoder_get_next, METH_NOARGS, "get_next"}, + {"reset", (PyCFunction)_anim_decoder_reset, METH_NOARGS, "reset"}, {NULL, NULL} /* sentinel */ }; @@ -544,6 +545,7 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) int height; int lossless; float quality_factor; + int method; uint8_t* rgb; uint8_t* icc_bytes; uint8_t* exif_bytes; @@ -555,40 +557,75 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) Py_ssize_t exif_size; Py_ssize_t xmp_size; size_t ret_size; + int rgba_mode; + int channels; + int ok; + ImagingSectionCookie cookie; + WebPConfig config; + WebPMemoryWriter writer; + WebPPicture pic; - if (!PyArg_ParseTuple(args, "y#iiifss#s#s#", + if (!PyArg_ParseTuple(args, "y#iiifss#is#s#", (char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode, - &icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) { + &icc_bytes, &icc_size, &method, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) { return NULL; } - if (strcmp(mode, "RGBA")==0){ - if (size < width * height * 4){ - Py_RETURN_NONE; - } - #if WEBP_ENCODER_ABI_VERSION >= 0x0100 - if (lossless) { - ret_size = WebPEncodeLosslessRGBA(rgb, width, height, 4 * width, &output); - } else - #endif - { - ret_size = WebPEncodeRGBA(rgb, width, height, 4 * width, quality_factor, &output); - } - } else if (strcmp(mode, "RGB")==0){ - if (size < width * height * 3){ - Py_RETURN_NONE; - } - #if WEBP_ENCODER_ABI_VERSION >= 0x0100 - if (lossless) { - ret_size = WebPEncodeLosslessRGB(rgb, width, height, 3 * width, &output); - } else - #endif - { - ret_size = WebPEncodeRGB(rgb, width, height, 3 * width, quality_factor, &output); - } - } else { + + rgba_mode = strcmp(mode, "RGBA") == 0; + if (!rgba_mode && strcmp(mode, "RGB") != 0) { Py_RETURN_NONE; } + channels = rgba_mode ? 4 : 3; + if (size < width * height * channels) { + Py_RETURN_NONE; + } + + // Setup config for this frame + if (!WebPConfigInit(&config)) { + PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); + return NULL; + } + config.lossless = lossless; + config.quality = quality_factor; + config.method = method; + + // Validate the config + if (!WebPValidateConfig(&config)) { + PyErr_SetString(PyExc_ValueError, "invalid configuration"); + return NULL; + } + + if (!WebPPictureInit(&pic)) { + PyErr_SetString(PyExc_ValueError, "could not initialise picture"); + return NULL; + } + pic.width = width; + pic.height = height; + pic.use_argb = 1; // Don't convert RGB pixels to YUV + + if (rgba_mode) { + WebPPictureImportRGBA(&pic, rgb, channels * width); + } else { + WebPPictureImportRGB(&pic, rgb, channels * width); + } + + WebPMemoryWriterInit(&writer); + pic.writer = WebPMemoryWrite; + pic.custom_ptr = &writer; + + ImagingSectionEnter(&cookie); + ok = WebPEncode(&config, &pic); + ImagingSectionLeave(&cookie); + + WebPPictureFree(&pic); + if (!ok) { + PyErr_SetString(PyExc_ValueError, "encoding error"); + return NULL; + } + output = writer.mem; + ret_size = writer.size; + #ifndef HAVE_WEBPMUX if (ret_size > 0) { PyObject *ret = PyBytes_FromStringAndSize((char*)output, ret_size); @@ -714,8 +751,9 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) WebPData exif_data = {0}; WebPMux* mux = WebPMuxCreate(&data, copy_data); - if (NULL == mux) + if (NULL == mux) { goto end; + } if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image)) { @@ -728,11 +766,13 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) vp8_status_code = WebPDecode(webp, size, &config); - if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) + if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) { icc_profile = PyBytes_FromStringAndSize((const char*)icc_profile_data.bytes, icc_profile_data.size); + } - if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) + if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) { exif = PyBytes_FromStringAndSize((const char*)exif_data.bytes, exif_data.size); + } WebPDataClear(&image.bitstream); WebPMuxDelete(mux); @@ -740,8 +780,9 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) #endif } - if (vp8_status_code != VP8_STATUS_OK) + if (vp8_status_code != VP8_STATUS_OK) { goto end; + } if (config.output.colorspace < MODE_YUV) { bytes = PyBytes_FromStringAndSize((char*)config.output.u.RGBA.rgba, @@ -767,18 +808,29 @@ end: Py_XDECREF(icc_profile); Py_XDECREF(exif); - if (Py_None == ret) + if (Py_None == ret) { Py_RETURN_NONE; + } return ret; } // Return the decoder's version number, packed in hexadecimal using 8bits for // each of major/minor/revision. E.g: v2.5.7 is 0x020507. -PyObject* WebPDecoderVersion_wrapper(PyObject* self, PyObject* args){ +PyObject* WebPDecoderVersion_wrapper() { return Py_BuildValue("i", WebPGetDecoderVersion()); } +// Version as string +const char* +WebPDecoderVersion_str(void) +{ + static char version[20]; + int version_number = WebPGetDecoderVersion(); + sprintf(version, "%d.%d.%d", version_number >> 16, (version_number >> 8) % 0x100, version_number % 0x100); + return version; +} + /* * The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well. * Files that are valid with 0.3 are reported as being invalid. @@ -787,7 +839,7 @@ int WebPDecoderBuggyAlpha(void) { return WebPGetDecoderVersion()==0x0103; } -PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){ +PyObject* WebPDecoderBuggyAlpha_wrapper() { return Py_BuildValue("i", WebPDecoderBuggyAlpha()); } @@ -803,8 +855,8 @@ static PyMethodDef webpMethods[] = #endif {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, - {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"}, - {"WebPDecoderBuggyAlpha", WebPDecoderBuggyAlpha_wrapper, METH_VARARGS, "WebPDecoderBuggyAlpha"}, + {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_NOARGS, "WebPVersion"}, + {"WebPDecoderBuggyAlpha", WebPDecoderBuggyAlpha_wrapper, METH_NOARGS, "WebPDecoderBuggyAlpha"}, {NULL, NULL} }; @@ -826,19 +878,23 @@ void addAnimFlagToModule(PyObject* m) { void addTransparencyFlagToModule(PyObject* m) { PyModule_AddObject(m, "HAVE_TRANSPARENCY", - PyBool_FromLong(!WebPDecoderBuggyAlpha())); + PyBool_FromLong(!WebPDecoderBuggyAlpha())); } static int setup_module(PyObject* m) { + PyObject* d = PyModule_GetDict(m); addMuxFlagToModule(m); addAnimFlagToModule(m); addTransparencyFlagToModule(m); + PyDict_SetItemString(d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str())); + #ifdef HAVE_WEBPANIM /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || - PyType_Ready(&WebPAnimEncoder_Type) < 0) + PyType_Ready(&WebPAnimEncoder_Type) < 0) { return -1; + } #endif return 0; } @@ -856,8 +912,9 @@ PyInit__webp(void) { }; m = PyModule_Create(&module_def); - if (setup_module(m) < 0) + if (setup_module(m) < 0) { return NULL; + } return m; } diff --git a/src/decode.c b/src/decode.c index 5ab6ca9d1..0ce381aa1 100644 --- a/src/decode.c +++ b/src/decode.c @@ -32,12 +32,12 @@ #define PY_SSIZE_T_CLEAN #include "Python.h" -#include "Imaging.h" +#include "libImaging/Imaging.h" -#include "Gif.h" -#include "Raw.h" -#include "Bit.h" -#include "Sgi.h" +#include "libImaging/Gif.h" +#include "libImaging/Raw.h" +#include "libImaging/Bit.h" +#include "libImaging/Sgi.h" /* -------------------------------------------------------------------- */ @@ -63,12 +63,14 @@ PyImaging_DecoderNew(int contextsize) ImagingDecoderObject *decoder; void *context; - if(PyType_Ready(&ImagingDecoderType) < 0) + if(PyType_Ready(&ImagingDecoderType) < 0) { return NULL; + } decoder = PyObject_New(ImagingDecoderObject, &ImagingDecoderType); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } /* Clear the decoder state */ memset(&decoder->state, 0, sizeof(decoder->state)); @@ -81,8 +83,9 @@ PyImaging_DecoderNew(int contextsize) (void) PyErr_NoMemory(); return NULL; } - } else + } else { context = 0; + } /* Initialize decoder context */ decoder->state.context = context; @@ -104,8 +107,9 @@ PyImaging_DecoderNew(int contextsize) static void _dealloc(ImagingDecoderObject* decoder) { - if (decoder->cleanup) + if (decoder->cleanup) { decoder->cleanup(&decoder->state); + } free(decoder->state.buffer); free(decoder->state.context); Py_XDECREF(decoder->lock); @@ -121,8 +125,9 @@ _decode(ImagingDecoderObject* decoder, PyObject* args) int status; ImagingSectionCookie cookie; - if (!PyArg_ParseTuple(args, "y#", &buffer, &bufsize)) + if (!PyArg_ParseTuple(args, "y#", &buffer, &bufsize)) { return NULL; + } if (!decoder->pulls_fd) { ImagingSectionEnter(&cookie); @@ -164,11 +169,13 @@ _setimage(ImagingDecoderObject* decoder, PyObject* args) x0 = y0 = x1 = y1 = 0; /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) + if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) { return NULL; + } im = PyImaging_AsImaging(op); - if (!im) + if (!im) { return NULL; + } decoder->im = im; @@ -203,8 +210,9 @@ _setimage(ImagingDecoderObject* decoder, PyObject* args) } /* malloc check ok, overflow checked above */ state->buffer = (UINT8*) malloc(state->bytes); - if (!state->buffer) + if (!state->buffer) { return PyErr_NoMemory(); + } } /* Keep a reference to the image object, to make sure it doesn't @@ -223,8 +231,9 @@ _setfd(ImagingDecoderObject* decoder, PyObject* args) PyObject* fd; ImagingCodecState state; - if (!PyArg_ParseTuple(args, "O", &fd)) + if (!PyArg_ParseTuple(args, "O", &fd)) { return NULL; + } state = &decoder->state; @@ -330,8 +339,9 @@ PyImaging_BitDecoderNew(PyObject* self, PyObject* args) int sign = 0; int ystep = 1; if (!PyArg_ParseTuple(args, "s|iiiii", &mode, &bits, &pad, &fill, - &sign, &ystep)) + &sign, &ystep)) { return NULL; + } if (strcmp(mode, "F") != 0) { PyErr_SetString(PyExc_ValueError, "bad image mode"); @@ -339,8 +349,9 @@ PyImaging_BitDecoderNew(PyObject* self, PyObject* args) } decoder = PyImaging_DecoderNew(sizeof(BITSTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } decoder->decode = ImagingBitDecode; @@ -368,8 +379,9 @@ PyImaging_BcnDecoderNew(PyObject* self, PyObject* args) char* actual; int n = 0; int ystep = 1; - if (!PyArg_ParseTuple(args, "s|ii", &mode, &n, &ystep)) + if (!PyArg_ParseTuple(args, "s|ii", &mode, &n, &ystep)) { return NULL; + } switch (n) { case 1: /* BC1: 565 color, 1-bit alpha */ @@ -394,8 +406,9 @@ PyImaging_BcnDecoderNew(PyObject* self, PyObject* args) } decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } decoder->decode = ImagingBcnDecode; decoder->state.state = n; @@ -415,8 +428,9 @@ PyImaging_FliDecoderNew(PyObject* self, PyObject* args) ImagingDecoderObject* decoder; decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } decoder->decode = ImagingFliDecode; @@ -436,8 +450,9 @@ PyImaging_GifDecoderNew(PyObject* self, PyObject* args) char* mode; int bits = 8; int interlace = 0; - if (!PyArg_ParseTuple(args, "s|ii", &mode, &bits, &interlace)) + if (!PyArg_ParseTuple(args, "s|ii", &mode, &bits, &interlace)) { return NULL; + } if (strcmp(mode, "L") != 0 && strcmp(mode, "P") != 0) { PyErr_SetString(PyExc_ValueError, "bad image mode"); @@ -445,8 +460,9 @@ PyImaging_GifDecoderNew(PyObject* self, PyObject* args) } decoder = PyImaging_DecoderNew(sizeof(GIFDECODERSTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } decoder->decode = ImagingGifDecode; @@ -468,15 +484,18 @@ PyImaging_HexDecoderNew(PyObject* self, PyObject* args) char* mode; char* rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) + if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { return NULL; + } decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingHexDecode; @@ -490,7 +509,7 @@ PyImaging_HexDecoderNew(PyObject* self, PyObject* args) #ifdef HAVE_LIBTIFF -#include "TiffDecode.h" +#include "libImaging/TiffDecode.h" #include @@ -504,17 +523,20 @@ PyImaging_LibTiffDecoderNew(PyObject* self, PyObject* args) int fp; uint32 ifdoffset; - if (! PyArg_ParseTuple(args, "sssiI", &mode, &rawmode, &compname, &fp, &ifdoffset)) + if (! PyArg_ParseTuple(args, "sssiI", &mode, &rawmode, &compname, &fp, &ifdoffset)) { return NULL; + } TRACE(("new tiff decoder %s\n", compname)); decoder = PyImaging_DecoderNew(sizeof(TIFFSTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } if (! ImagingLibTiffInit(&decoder->state, fp, ifdoffset)) { Py_DECREF(decoder); @@ -541,15 +563,18 @@ PyImaging_PackbitsDecoderNew(PyObject* self, PyObject* args) char* mode; char* rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) + if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { return NULL; + } decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingPackbitsDecode; @@ -567,12 +592,14 @@ PyImaging_PcdDecoderNew(PyObject* self, PyObject* args) ImagingDecoderObject* decoder; decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } /* Unpack from PhotoYCC to RGB */ - if (get_unpacker(decoder, "RGB", "YCC;P") < 0) + if (get_unpacker(decoder, "RGB", "YCC;P") < 0) { return NULL; + } decoder->decode = ImagingPcdDecode; @@ -592,15 +619,18 @@ PyImaging_PcxDecoderNew(PyObject* self, PyObject* args) char* mode; char* rawmode; int stride; - if (!PyArg_ParseTuple(args, "ssi", &mode, &rawmode, &stride)) + if (!PyArg_ParseTuple(args, "ssi", &mode, &rawmode, &stride)) { return NULL; + } decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->state.bytes = stride; @@ -623,15 +653,18 @@ PyImaging_RawDecoderNew(PyObject* self, PyObject* args) char* rawmode; int stride = 0; int ystep = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &stride, &ystep)) + if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &stride, &ystep)) { return NULL; + } decoder = PyImaging_DecoderNew(sizeof(RAWSTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingRawDecode; @@ -656,15 +689,18 @@ PyImaging_SgiRleDecoderNew(PyObject* self, PyObject* args) char* rawmode; int ystep = 1; int bpc = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &bpc)) + if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &bpc)) { return NULL; + } decoder = PyImaging_DecoderNew(sizeof(SGISTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->pulls_fd = 1; decoder->decode = ImagingSgiRleDecode; @@ -687,15 +723,18 @@ PyImaging_SunRleDecoderNew(PyObject* self, PyObject* args) char* mode; char* rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) + if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { return NULL; + } decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingSunRleDecode; @@ -716,15 +755,18 @@ PyImaging_TgaRleDecoderNew(PyObject* self, PyObject* args) char* rawmode; int ystep = 1; int depth = 8; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &depth)) + if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &depth)) { return NULL; + } decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingTgaRleDecode; @@ -745,11 +787,13 @@ PyImaging_XbmDecoderNew(PyObject* self, PyObject* args) ImagingDecoderObject* decoder; decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, "1", "1;R") < 0) + if (get_unpacker(decoder, "1", "1;R") < 0) { return NULL; + } decoder->decode = ImagingXbmDecode; @@ -763,7 +807,7 @@ PyImaging_XbmDecoderNew(PyObject* self, PyObject* args) #ifdef HAVE_LIBZ -#include "Zip.h" +#include "libImaging/ZipCodecs.h" PyObject* PyImaging_ZipDecoderNew(PyObject* self, PyObject* args) @@ -773,15 +817,18 @@ PyImaging_ZipDecoderNew(PyObject* self, PyObject* args) char* mode; char* rawmode; int interlaced = 0; - if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &interlaced)) + if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &interlaced)) { return NULL; + } decoder = PyImaging_DecoderNew(sizeof(ZIPSTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingZipDecode; decoder->cleanup = ImagingZipDecodeCleanup; @@ -812,7 +859,7 @@ PyImaging_ZipDecoderNew(PyObject* self, PyObject* args) #undef INT16 #undef INT32 -#include "Jpeg.h" +#include "libImaging/Jpeg.h" PyObject* PyImaging_JpegDecoderNew(PyObject* self, PyObject* args) @@ -826,15 +873,18 @@ PyImaging_JpegDecoderNew(PyObject* self, PyObject* args) int draft = 0; if (!PyArg_ParseTuple(args, "ssz|ii", &mode, &rawmode, &jpegmode, - &scale, &draft)) + &scale, &draft)) { return NULL; + } - if (!jpegmode) + if (!jpegmode) { jpegmode = ""; + } decoder = PyImaging_DecoderNew(sizeof(JPEGSTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } // libjpeg-turbo supports different output formats. // We are choosing Pillow's native format (3 color bytes + 1 padding) @@ -843,8 +893,9 @@ PyImaging_JpegDecoderNew(PyObject* self, PyObject* args) rawmode = "RGBX"; } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingJpegDecode; decoder->cleanup = ImagingJpegDecodeCleanup; @@ -865,7 +916,7 @@ PyImaging_JpegDecoderNew(PyObject* self, PyObject* args) #ifdef HAVE_OPENJPEG -#include "Jpeg2K.h" +#include "libImaging/Jpeg2K.h" PyObject* PyImaging_Jpeg2KDecoderNew(PyObject* self, PyObject* args) @@ -882,21 +933,24 @@ PyImaging_Jpeg2KDecoderNew(PyObject* self, PyObject* args) PY_LONG_LONG length = -1; if (!PyArg_ParseTuple(args, "ss|iiiL", &mode, &format, - &reduce, &layers, &fd, &length)) + &reduce, &layers, &fd, &length)) { return NULL; + } - if (strcmp(format, "j2k") == 0) + if (strcmp(format, "j2k") == 0) { codec_format = OPJ_CODEC_J2K; - else if (strcmp(format, "jpt") == 0) + } else if (strcmp(format, "jpt") == 0) { codec_format = OPJ_CODEC_JPT; - else if (strcmp(format, "jp2") == 0) + } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; - else + } else { return NULL; + } decoder = PyImaging_DecoderNew(sizeof(JPEG2KDECODESTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } decoder->pulls_fd = 1; decoder->decode = ImagingJpeg2KDecode; diff --git a/src/display.c b/src/display.c index 4c2faf9e0..c95844bbc 100644 --- a/src/display.c +++ b/src/display.c @@ -22,17 +22,17 @@ * See the README file for information on usage and redistribution. */ - +#define PY_SSIZE_T_CLEAN #include "Python.h" -#include "Imaging.h" +#include "libImaging/Imaging.h" /* -------------------------------------------------------------------- */ -/* Windows DIB support */ +/* Windows DIB support */ #ifdef _WIN32 -#include "ImDib.h" +#include "libImaging/ImDib.h" #if SIZEOF_VOID_P == 8 #define F_HANDLE "K" @@ -52,17 +52,19 @@ _new(const char* mode, int xsize, int ysize) { ImagingDisplayObject *display; - if (PyType_Ready(&ImagingDisplayType) < 0) + if (PyType_Ready(&ImagingDisplayType) < 0) { return NULL; + } display = PyObject_New(ImagingDisplayObject, &ImagingDisplayType); - if (display == NULL) - return NULL; + if (display == NULL) { + return NULL; + } display->dib = ImagingNewDIB(mode, xsize, ysize); if (!display->dib) { - Py_DECREF(display); - return NULL; + Py_DECREF(display); + return NULL; } return display; @@ -71,8 +73,9 @@ _new(const char* mode, int xsize, int ysize) static void _delete(ImagingDisplayObject* display) { - if (display->dib) - ImagingDeleteDIB(display->dib); + if (display->dib) { + ImagingDeleteDIB(display->dib); + } PyObject_Del(display); } @@ -80,8 +83,9 @@ static PyObject* _expose(ImagingDisplayObject* display, PyObject* args) { HDC hdc; - if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) - return NULL; + if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) { + return NULL; + } ImagingExposeDIB(display->dib, hdc); @@ -97,8 +101,9 @@ _draw(ImagingDisplayObject* display, PyObject* args) int src[4]; if (!PyArg_ParseTuple(args, F_HANDLE "(iiii)(iiii)", &hdc, dst+0, dst+1, dst+2, dst+3, - src+0, src+1, src+2, src+3)) - return NULL; + src+0, src+1, src+2, src+3)) { + return NULL; + } ImagingDrawDIB(display->dib, hdc, dst, src); @@ -116,16 +121,20 @@ _paste(ImagingDisplayObject* display, PyObject* args) PyObject* op; int xy[4]; xy[0] = xy[1] = xy[2] = xy[3] = 0; - if (!PyArg_ParseTuple(args, "O|(iiii)", &op, xy+0, xy+1, xy+2, xy+3)) - return NULL; + if (!PyArg_ParseTuple(args, "O|(iiii)", &op, xy+0, xy+1, xy+2, xy+3)) { + return NULL; + } im = PyImaging_AsImaging(op); - if (!im) - return NULL; + if (!im) { + return NULL; + } - if (xy[2] <= xy[0]) - xy[2] = xy[0] + im->xsize; - if (xy[3] <= xy[1]) - xy[3] = xy[1] + im->ysize; + if (xy[2] <= xy[0]) { + xy[2] = xy[0] + im->xsize; + } + if (xy[3] <= xy[1]) { + xy[3] = xy[1] + im->ysize; + } ImagingPasteDIB(display->dib, im, xy); @@ -139,8 +148,9 @@ _query_palette(ImagingDisplayObject* display, PyObject* args) HDC hdc; int status; - if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) - return NULL; + if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) { + return NULL; + } status = ImagingQueryPaletteDIB(display->dib, hdc); @@ -153,12 +163,13 @@ _getdc(ImagingDisplayObject* display, PyObject* args) HWND window; HDC dc; - if (!PyArg_ParseTuple(args, F_HANDLE, &window)) - return NULL; + if (!PyArg_ParseTuple(args, F_HANDLE, &window)) { + return NULL; + } dc = GetDC(window); if (!dc) { - PyErr_SetString(PyExc_IOError, "cannot create dc"); + PyErr_SetString(PyExc_OSError, "cannot create dc"); return NULL; } @@ -171,8 +182,9 @@ _releasedc(ImagingDisplayObject* display, PyObject* args) HWND window; HDC dc; - if (!PyArg_ParseTuple(args, F_HANDLE F_HANDLE, &window, &dc)) - return NULL; + if (!PyArg_ParseTuple(args, F_HANDLE F_HANDLE, &window, &dc)) { + return NULL; + } ReleaseDC(window, dc); @@ -184,10 +196,11 @@ static PyObject* _frombytes(ImagingDisplayObject* display, PyObject* args) { char* ptr; - int bytes; + Py_ssize_t bytes; - if (!PyArg_ParseTuple(args, "y#:frombytes", &ptr, &bytes)) + if (!PyArg_ParseTuple(args, "y#:frombytes", &ptr, &bytes)) { return NULL; + } if (display->dib->ysize * display->dib->linesize != bytes) { PyErr_SetString(PyExc_ValueError, "wrong size"); @@ -203,8 +216,9 @@ _frombytes(ImagingDisplayObject* display, PyObject* args) static PyObject* _tobytes(ImagingDisplayObject* display, PyObject* args) { - if (!PyArg_ParseTuple(args, ":tobytes")) + if (!PyArg_ParseTuple(args, ":tobytes")) { return NULL; + } return PyBytes_FromStringAndSize( display->dib->bits, display->dib->ysize * display->dib->linesize @@ -228,13 +242,13 @@ static struct PyMethodDef methods[] = { static PyObject* _getattr_mode(ImagingDisplayObject* self, void* closure) { - return Py_BuildValue("s", self->dib->mode); + return Py_BuildValue("s", self->dib->mode); } static PyObject* _getattr_size(ImagingDisplayObject* self, void* closure) { - return Py_BuildValue("ii", self->dib->xsize, self->dib->ysize); + return Py_BuildValue("ii", self->dib->xsize, self->dib->ysize); } static struct PyGetSetDef getsetters[] = { @@ -244,13 +258,13 @@ static struct PyGetSetDef getsetters[] = { }; static PyTypeObject ImagingDisplayType = { - PyVarObject_HEAD_INIT(NULL, 0) - "ImagingDisplay", /*tp_name*/ - sizeof(ImagingDisplayObject), /*tp_size*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_delete, /*tp_dealloc*/ - 0, /*tp_print*/ + PyVarObject_HEAD_INIT(NULL, 0) + "ImagingDisplay", /*tp_name*/ + sizeof(ImagingDisplayObject),/*tp_size*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)_delete, /*tp_dealloc*/ + 0, /*tp_print*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ 0, /*tp_compare*/ @@ -284,12 +298,14 @@ PyImaging_DisplayWin32(PyObject* self, PyObject* args) char *mode; int xsize, ysize; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) - return NULL; + if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { + return NULL; + } display = _new(mode, xsize, ysize); - if (display == NULL) - return NULL; + if (display == NULL) { + return NULL; + } return (PyObject*) display; } @@ -324,8 +340,9 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) HMODULE user32; Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; - if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) + if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) { return NULL; + } /* step 1: create a memory DC large enough to hold the entire screen */ @@ -361,25 +378,30 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) FreeLibrary(user32); bitmap = CreateCompatibleBitmap(screen, width, height); - if (!bitmap) + if (!bitmap) { goto error; + } - if (!SelectObject(screen_copy, bitmap)) + if (!SelectObject(screen_copy, bitmap)) { goto error; + } /* step 2: copy bits into memory DC bitmap */ rop = SRCCOPY; - if (includeLayeredWindows) + if (includeLayeredWindows) { rop |= CAPTUREBLT; - if (!BitBlt(screen_copy, 0, 0, width, height, screen, x, y, rop)) + } + if (!BitBlt(screen_copy, 0, 0, width, height, screen, x, y, rop)) { goto error; + } /* step 3: extract bits from bitmap */ buffer = PyBytes_FromStringAndSize(NULL, height * ((width*3 + 3) & -4)); - if (!buffer) + if (!buffer) { return NULL; + } core.bcSize = sizeof(core); core.bcWidth = width; @@ -387,8 +409,9 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) core.bcPlanes = 1; core.bcBitCount = 24; if (!GetDIBits(screen_copy, bitmap, 0, height, PyBytes_AS_STRING(buffer), - (BITMAPINFO*) &core, DIB_RGB_COLORS)) + (BITMAPINFO*) &core, DIB_RGB_COLORS)) { goto error; + } DeleteObject(bitmap); DeleteDC(screen_copy); @@ -397,7 +420,7 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer); error: - PyErr_SetString(PyExc_IOError, "screen grab failed"); + PyErr_SetString(PyExc_OSError, "screen grab failed"); DeleteDC(screen_copy); DeleteDC(screen); @@ -418,12 +441,15 @@ static BOOL CALLBACK list_windows_callback(HWND hwnd, LPARAM lParam) title_size = GetWindowTextLength(hwnd); if (title_size > 0) { title = PyUnicode_FromStringAndSize(NULL, title_size); - if (title) + if (title) { GetWindowTextW(hwnd, PyUnicode_AS_UNICODE(title), title_size+1); - } else + } + } else { title = PyUnicode_FromString(""); - if (!title) + } + if (!title) { return 0; + } /* get bounding boxes */ GetClientRect(hwnd, &inner); @@ -434,15 +460,17 @@ static BOOL CALLBACK list_windows_callback(HWND hwnd, LPARAM lParam) inner.left, inner.top, inner.right, inner.bottom, outer.left, outer.top, outer.right, outer.bottom ); - if (!item) + if (!item) { return 0; + } status = PyList_Append(window_list, item); Py_DECREF(item); - if (status < 0) + if (status < 0) { return 0; + } return 1; } @@ -453,8 +481,9 @@ PyImaging_ListWindowsWin32(PyObject* self, PyObject* args) PyObject* window_list; window_list = PyList_New(0); - if (!window_list) + if (!window_list) { return NULL; + } EnumWindows(list_windows_callback, (LPARAM) window_list); @@ -473,33 +502,45 @@ PyObject* PyImaging_GrabClipboardWin32(PyObject* self, PyObject* args) { int clip; - HANDLE handle; + HANDLE handle = NULL; int size; void* data; PyObject* result; + UINT format; + UINT formats[] = { CF_DIB, CF_DIBV5, CF_HDROP, RegisterClipboardFormatA("PNG"), 0 }; + LPCSTR format_names[] = { "DIB", "DIB", "file", "png", NULL }; - clip = OpenClipboard(NULL); - /* FIXME: check error status */ - - handle = GetClipboardData(CF_DIB); - if (!handle) { - /* FIXME: add CF_HDROP support to allow cut-and-paste from - the explorer */ - CloseClipboard(); - Py_INCREF(Py_None); - return Py_None; + if (!OpenClipboard(NULL)) { + PyErr_SetString(PyExc_OSError, "failed to open clipboard"); + return NULL; + } + + // find best format as set by clipboard owner + format = 0; + while (!handle && (format = EnumClipboardFormats(format))) { + for (UINT i = 0; formats[i] != 0; i++) { + if (format == formats[i]) { + handle = GetClipboardData(format); + format = i; + break; + } + } + } + + if (!handle) { + CloseClipboard(); + return Py_BuildValue("zO", NULL, Py_None); } - size = GlobalSize(handle); data = GlobalLock(handle); + size = GlobalSize(handle); result = PyBytes_FromStringAndSize(data, size); GlobalUnlock(handle); - CloseClipboard(); - return result; + return Py_BuildValue("zN", format_names[format], result); } /* -------------------------------------------------------------------- */ @@ -556,8 +597,9 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) GetWindowLongPtr(wnd, sizeof(PyObject*)); current_threadstate = PyThreadState_Swap(NULL); PyEval_RestoreThread(threadstate); - } else + } else { return DefWindowProc(wnd, message, wParam, lParam); + } } /* process message */ @@ -575,28 +617,31 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) ps.rcPaint.left, ps.rcPaint.top, ps.rcPaint.right, ps.rcPaint.bottom ); - if (result) + if (result) { Py_DECREF(result); - else + } else { callback_error("window damage callback"); + } result = PyObject_CallFunction( callback, "s" F_HANDLE "iiii", "clear", dc, 0, 0, rect.right-rect.left, rect.bottom-rect.top ); - if (result) + if (result) { Py_DECREF(result); - else + } else { callback_error("window clear callback"); + } result = PyObject_CallFunction( callback, "s" F_HANDLE "iiii", "repair", dc, 0, 0, rect.right-rect.left, rect.bottom-rect.top ); - if (result) + if (result) { Py_DECREF(result); - else + } else { callback_error("window repair callback"); + } ReleaseDC(wnd, dc); EndPaint(wnd, &ps); @@ -610,17 +655,19 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) if (result) { InvalidateRect(wnd, NULL, 1); Py_DECREF(result); - } else + } else { callback_error("window resize callback"); + } break; case WM_DESTROY: /* destroy window */ result = PyObject_CallFunction(callback, "s", "destroy"); - if (result) + if (result) { Py_DECREF(result); - else + } else { callback_error("window destroy callback"); + } Py_DECREF(callback); break; @@ -646,13 +693,16 @@ PyImaging_CreateWindowWin32(PyObject* self, PyObject* args) char* title; PyObject* callback; int width = 0, height = 0; - if (!PyArg_ParseTuple(args, "sO|ii", &title, &callback, &width, &height)) - return NULL; + if (!PyArg_ParseTuple(args, "sO|ii", &title, &callback, &width, &height)) { + return NULL; + } - if (width <= 0) + if (width <= 0) { width = CW_USEDEFAULT; - if (height <= 0) + } + if (height <= 0) { height = CW_USEDEFAULT; + } /* register toplevel window class */ windowClass.style = CS_CLASSDC; @@ -677,7 +727,7 @@ PyImaging_CreateWindowWin32(PyObject* self, PyObject* args) ); if (!wnd) { - PyErr_SetString(PyExc_IOError, "failed to create window"); + PyErr_SetString(PyExc_OSError, "failed to create window"); return NULL; } @@ -727,12 +777,13 @@ PyImaging_DrawWmf(PyObject* self, PyObject* args) char* ptr; char* data; - int datasize; + Py_ssize_t datasize; int width, height; int x0, y0, x1, y1; if (!PyArg_ParseTuple(args, "y#(ii)(iiii):_load", &data, &datasize, - &width, &height, &x0, &x1, &y0, &y1)) + &width, &height, &x0, &x1, &y0, &y1)) { return NULL; + } /* step 1: copy metafile contents into METAFILE object */ @@ -755,7 +806,7 @@ PyImaging_DrawWmf(PyObject* self, PyObject* args) } if (!meta) { - PyErr_SetString(PyExc_IOError, "cannot load metafile"); + PyErr_SetString(PyExc_OSError, "cannot load metafile"); return NULL; } @@ -774,12 +825,12 @@ PyImaging_DrawWmf(PyObject* self, PyObject* args) ); if (!bitmap) { - PyErr_SetString(PyExc_IOError, "cannot create bitmap"); + PyErr_SetString(PyExc_OSError, "cannot create bitmap"); goto error; } if (!SelectObject(dc, bitmap)) { - PyErr_SetString(PyExc_IOError, "cannot select bitmap"); + PyErr_SetString(PyExc_OSError, "cannot select bitmap"); goto error; } @@ -793,7 +844,7 @@ PyImaging_DrawWmf(PyObject* self, PyObject* args) FillRect(dc, &rect, GetStockObject(WHITE_BRUSH)); if (!PlayEnhMetaFile(dc, meta, &rect)) { - PyErr_SetString(PyExc_IOError, "cannot render metafile"); + PyErr_SetString(PyExc_OSError, "cannot render metafile"); goto error; } @@ -806,8 +857,9 @@ PyImaging_DrawWmf(PyObject* self, PyObject* args) error: DeleteEnhMetaFile(meta); - if (bitmap) + if (bitmap) { DeleteObject(bitmap); + } DeleteDC(dc); @@ -815,3 +867,90 @@ error: } #endif /* _WIN32 */ + +/* -------------------------------------------------------------------- */ +/* X11 support */ + +#ifdef HAVE_XCB +#include + +/* -------------------------------------------------------------------- */ +/* X11 screen grabber */ + +PyObject* +PyImaging_GrabScreenX11(PyObject* self, PyObject* args) +{ + int width, height; + char* display_name; + xcb_connection_t* connection; + int screen_number; + xcb_screen_iterator_t iter; + xcb_screen_t* screen = NULL; + xcb_get_image_reply_t* reply; + xcb_generic_error_t* error; + PyObject* buffer = NULL; + + if (!PyArg_ParseTuple(args, "|z", &display_name)) { + return NULL; + } + + /* connect to X and get screen data */ + + connection = xcb_connect(display_name, &screen_number); + if (xcb_connection_has_error(connection)) { + PyErr_Format(PyExc_OSError, "X connection failed: error %i", xcb_connection_has_error(connection)); + xcb_disconnect(connection); + return NULL; + } + + iter = xcb_setup_roots_iterator(xcb_get_setup(connection)); + for (; iter.rem; --screen_number, xcb_screen_next(&iter)) { + if (screen_number == 0) { + screen = iter.data; + break; + } + } + if (screen == NULL || screen->root == 0) { + // this case is usually caught with "X connection failed: error 6" above + xcb_disconnect(connection); + PyErr_SetString(PyExc_OSError, "X screen not found"); + return NULL; + } + + width = screen->width_in_pixels; + height = screen->height_in_pixels; + + /* get image data */ + + reply = xcb_get_image_reply(connection, + xcb_get_image(connection, XCB_IMAGE_FORMAT_Z_PIXMAP, screen->root, + 0, 0, width, height, 0x00ffffff), + &error); + if (reply == NULL) { + PyErr_Format(PyExc_OSError, "X get_image failed: error %i (%i, %i, %i)", + error->error_code, error->major_code, error->minor_code, error->resource_id); + free(error); + xcb_disconnect(connection); + return NULL; + } + + /* store data in Python buffer */ + + if (reply->depth == 24) { + buffer = PyBytes_FromStringAndSize((char*)xcb_get_image_data(reply), + xcb_get_image_data_length(reply)); + } else { + PyErr_Format(PyExc_OSError, "unsupported bit depth: %i", reply->depth); + } + + free(reply); + xcb_disconnect(connection); + + if (!buffer) { + return NULL; + } + + return Py_BuildValue("(ii)N", width, height, buffer); +} + +#endif /* HAVE_XCB */ diff --git a/src/encode.c b/src/encode.c index 41ba124c4..41f28722c 100644 --- a/src/encode.c +++ b/src/encode.c @@ -25,8 +25,8 @@ #define PY_SSIZE_T_CLEAN #include "Python.h" -#include "Imaging.h" -#include "Gif.h" +#include "libImaging/Imaging.h" +#include "libImaging/Gif.h" #ifdef HAVE_UNISTD_H #include /* write */ @@ -55,12 +55,14 @@ PyImaging_EncoderNew(int contextsize) ImagingEncoderObject *encoder; void *context; - if(PyType_Ready(&ImagingEncoderType) < 0) + if(PyType_Ready(&ImagingEncoderType) < 0) { return NULL; + } encoder = PyObject_New(ImagingEncoderObject, &ImagingEncoderType); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } /* Clear the encoder state */ memset(&encoder->state, 0, sizeof(encoder->state)); @@ -73,8 +75,9 @@ PyImaging_EncoderNew(int contextsize) (void) PyErr_NoMemory(); return NULL; } - } else + } else { context = 0; + } /* Initialize encoder context */ encoder->state.context = context; @@ -93,8 +96,9 @@ PyImaging_EncoderNew(int contextsize) static void _dealloc(ImagingEncoderObject* encoder) { - if (encoder->cleanup) + if (encoder->cleanup) { encoder->cleanup(&encoder->state); + } free(encoder->state.buffer); free(encoder->state.context); Py_XDECREF(encoder->lock); @@ -125,19 +129,22 @@ _encode(ImagingEncoderObject* encoder, PyObject* args) Py_ssize_t bufsize = 16384; - if (!PyArg_ParseTuple(args, "|n", &bufsize)) + if (!PyArg_ParseTuple(args, "|n", &bufsize)) { return NULL; + } buf = PyBytes_FromStringAndSize(NULL, bufsize); - if (!buf) + if (!buf) { return NULL; + } status = encoder->encode(encoder->im, &encoder->state, (UINT8*) PyBytes_AsString(buf), bufsize); /* adjust string length to avoid slicing in encoder */ - if (_PyBytes_Resize(&buf, (status > 0) ? status : 0) < 0) + if (_PyBytes_Resize(&buf, (status > 0) ? status : 0) < 0) { return NULL; + } result = Py_BuildValue("iiO", status, encoder->state.errcode, buf); @@ -179,14 +186,16 @@ _encode_to_file(ImagingEncoderObject* encoder, PyObject* args) Py_ssize_t fh; Py_ssize_t bufsize = 16384; - if (!PyArg_ParseTuple(args, "n|n", &fh, &bufsize)) + if (!PyArg_ParseTuple(args, "n|n", &fh, &bufsize)) { return NULL; + } /* Allocate an encoder buffer */ /* malloc check ok, either constant int, or checked by PyArg_ParseTuple */ buf = (UINT8*) malloc(bufsize); - if (!buf) + if (!buf) { return PyErr_NoMemory(); + } ImagingSectionEnter(&cookie); @@ -197,12 +206,13 @@ _encode_to_file(ImagingEncoderObject* encoder, PyObject* args) status = encoder->encode(encoder->im, &encoder->state, buf, bufsize); - if (status > 0) + if (status > 0) { if (write(fh, buf, status) < 0) { ImagingSectionLeave(&cookie); free(buf); - return PyErr_SetFromErrno(PyExc_IOError); + return PyErr_SetFromErrno(PyExc_OSError); } + } } while (encoder->state.errcode == 0); @@ -228,11 +238,13 @@ _setimage(ImagingEncoderObject* encoder, PyObject* args) x0 = y0 = x1 = y1 = 0; /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(nnnn)", &op, &x0, &y0, &x1, &y1)) + if (!PyArg_ParseTuple(args, "O|(nnnn)", &op, &x0, &y0, &x1, &y1)) { return NULL; + } im = PyImaging_AsImaging(op); - if (!im) + if (!im) { return NULL; + } encoder->im = im; @@ -264,8 +276,9 @@ _setimage(ImagingEncoderObject* encoder, PyObject* args) state->bytes = (state->bits * state->xsize+7)/8; /* malloc check ok, overflow checked above */ state->buffer = (UINT8*) malloc(state->bytes); - if (!state->buffer) + if (!state->buffer) { return PyErr_NoMemory(); + } } /* Keep a reference to the image object, to make sure it doesn't @@ -284,8 +297,9 @@ _setfd(ImagingEncoderObject* encoder, PyObject* args) PyObject* fd; ImagingCodecState state; - if (!PyArg_ParseTuple(args, "O", &fd)) + if (!PyArg_ParseTuple(args, "O", &fd)) { return NULL; + } state = &encoder->state; @@ -386,8 +400,9 @@ PyImaging_EpsEncoderNew(PyObject* self, PyObject* args) ImagingEncoderObject* encoder; encoder = PyImaging_EncoderNew(0); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } encoder->encode = ImagingEpsEncode; @@ -408,15 +423,18 @@ PyImaging_GifEncoderNew(PyObject* self, PyObject* args) char *rawmode; Py_ssize_t bits = 8; Py_ssize_t interlace = 0; - if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &bits, &interlace)) + if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &bits, &interlace)) { return NULL; + } encoder = PyImaging_EncoderNew(sizeof(GIFENCODERSTATE)); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } - if (get_packer(encoder, mode, rawmode) < 0) + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; + } encoder->encode = ImagingGifEncode; @@ -473,15 +491,18 @@ PyImaging_RawEncoderNew(PyObject* self, PyObject* args) Py_ssize_t stride = 0; Py_ssize_t ystep = 1; - if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &stride, &ystep)) + if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &stride, &ystep)) { return NULL; + } encoder = PyImaging_EncoderNew(0); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } - if (get_packer(encoder, mode, rawmode) < 0) + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; + } encoder->encode = ImagingRawEncode; @@ -505,15 +526,18 @@ PyImaging_TgaRleEncoderNew(PyObject* self, PyObject* args) char *rawmode; Py_ssize_t ystep = 1; - if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &ystep)) + if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &ystep)) { return NULL; + } encoder = PyImaging_EncoderNew(0); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } - if (get_packer(encoder, mode, rawmode) < 0) + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; + } encoder->encode = ImagingTgaRleEncode; @@ -534,11 +558,13 @@ PyImaging_XbmEncoderNew(PyObject* self, PyObject* args) ImagingEncoderObject* encoder; encoder = PyImaging_EncoderNew(0); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } - if (get_packer(encoder, "1", "1;R") < 0) + if (get_packer(encoder, "1", "1;R") < 0) { return NULL; + } encoder->encode = ImagingXbmEncode; @@ -552,7 +578,7 @@ PyImaging_XbmEncoderNew(PyObject* self, PyObject* args) #ifdef HAVE_LIBZ -#include "Zip.h" +#include "libImaging/ZipCodecs.h" PyObject* PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) @@ -569,19 +595,22 @@ PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) if (!PyArg_ParseTuple(args, "ss|nnny#", &mode, &rawmode, &optimize, &compress_level, &compress_type, - &dictionary, &dictionary_size)) + &dictionary, &dictionary_size)) { return NULL; + } /* Copy to avoid referencing Python's memory */ if (dictionary && dictionary_size > 0) { /* malloc check ok, size comes from PyArg_ParseTuple */ char* p = malloc(dictionary_size); - if (!p) + if (!p) { return PyErr_NoMemory(); + } memcpy(p, dictionary, dictionary_size); dictionary = p; - } else + } else { dictionary = NULL; + } encoder = PyImaging_EncoderNew(sizeof(ZIPSTATE)); if (encoder == NULL) { @@ -597,9 +626,10 @@ PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) encoder->encode = ImagingZipEncode; encoder->cleanup = ImagingZipEncodeCleanup; - if (rawmode[0] == 'P') + if (rawmode[0] == 'P') { /* disable filtering */ ((ZIPSTATE*)encoder->state.context)->mode = ZIP_PNG_PALETTE; + } ((ZIPSTATE*)encoder->state.context)->optimize = optimize; ((ZIPSTATE*)encoder->state.context)->compress_level = compress_level; @@ -618,7 +648,7 @@ PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) #ifdef HAVE_LIBTIFF -#include "TiffDecode.h" +#include "libImaging/TiffDecode.h" #include @@ -641,7 +671,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) // This list also exists in TiffTags.py const int core_tags[] = { 256, 257, 258, 259, 262, 263, 266, 269, 274, 277, 278, 280, 281, 340, - 341, 282, 283, 284, 286, 287, 296, 297, 321, 338, 32995, 32998, 32996, + 341, 282, 283, 284, 286, 287, 296, 297, 320, 321, 338, 32995, 32998, 32996, 339, 32997, 330, 531, 530, 65537 }; @@ -675,11 +705,13 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) TRACE(("new tiff encoder %s fp: %d, filename: %s \n", compname, fp, filename)); encoder = PyImaging_EncoderNew(sizeof(TIFFSTATE)); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } - if (get_packer(encoder, mode, rawmode) < 0) + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; + } if (! ImagingLibTiffEncodeInit(&encoder->state, filename, fp)) { Py_DECREF(encoder); @@ -729,12 +761,6 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) } } - if (PyBytes_Check(value) && - (type == TIFF_BYTE || type == TIFF_UNDEFINED)) { - // For backwards compatibility - type = TIFF_ASCII; - } - if (PyTuple_Check(value)) { Py_ssize_t len; len = PyTuple_Size(value); @@ -758,25 +784,40 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) if (!is_core_tag) { // Register field for non core tags. + if (type == TIFF_BYTE) { + is_var_length = 1; + } if (ImagingLibTiffMergeFieldInfo(&encoder->state, type, key_int, is_var_length)) { continue; } } - if (is_var_length) { + if (type == TIFF_BYTE || type == TIFF_UNDEFINED) { + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) key_int, + PyBytes_Size(value), PyBytes_AsString(value)); + } else if (is_var_length) { Py_ssize_t len,i; TRACE(("Setting from Tuple: %d \n", key_int)); len = PyTuple_Size(value); - if (type == TIFF_BYTE) { - UINT8 *av; + if (key_int == TIFFTAG_COLORMAP) { + int stride = 256; + if (len != 768) { + PyErr_SetString(PyExc_ValueError, "Requiring 768 items for for Colormap"); + return NULL; + } + UINT16 *av; /* malloc check ok, calloc checks for overflow */ - av = calloc(len, sizeof(UINT8)); + av = calloc(len, sizeof(UINT16)); if (av) { for (i=0;istate, (ttag_t) key_int, len, av); + status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, + av, + av + stride, + av + stride * 2); free(av); } } else if (type == TIFF_SHORT) { @@ -882,10 +923,6 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, (FLOAT64)PyFloat_AsDouble(value)); - } else if (type == TIFF_BYTE) { - status = ImagingLibTiffSetField(&encoder->state, - (ttag_t) key_int, - (UINT8)PyLong_AsLong(value)); } else if (type == TIFF_SBYTE) { status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, @@ -938,7 +975,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) #undef INT16 #undef INT32 -#include "Jpeg.h" +#include "libImaging/Jpeg.h" static unsigned int* get_qtables_arrays(PyObject* qtables, int* qtablesLen) { PyObject* tables; @@ -1027,12 +1064,14 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) &mode, &rawmode, &quality, &progressive, &smooth, &optimize, &streamtype, &xdpi, &ydpi, &subsampling, &qtables, &extra, &extra_size, - &rawExif, &rawExifLen)) + &rawExif, &rawExifLen)) { return NULL; + } encoder = PyImaging_EncoderNew(sizeof(JPEGENCODERSTATE)); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } // libjpeg-turbo supports different output formats. // We are choosing Pillow's native format (3 color bytes + 1 padding) @@ -1041,8 +1080,9 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) rawmode = "RGBX"; } - if (get_packer(encoder, mode, rawmode) < 0) + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; + } // Freed in JpegEncode, Case 5 qarrays = get_qtables_arrays(qtables, &qtablesLen); @@ -1050,24 +1090,29 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) if (extra && extra_size > 0) { /* malloc check ok, length is from python parsearg */ char* p = malloc(extra_size); // Freed in JpegEncode, Case 5 - if (!p) + if (!p) { return PyErr_NoMemory(); + } memcpy(p, extra, extra_size); extra = p; - } else + } else { extra = NULL; + } if (rawExif && rawExifLen > 0) { /* malloc check ok, length is from python parsearg */ char* pp = malloc(rawExifLen); // Freed in JpegEncode, Case 5 if (!pp) { - if (extra) free(extra); + if (extra) { + free(extra); + } return PyErr_NoMemory(); } memcpy(pp, rawExif, rawExifLen); rawExif = pp; - } else + } else { rawExif = NULL; + } encoder->encode = ImagingJpegEncode; @@ -1095,12 +1140,12 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) /* -------------------------------------------------------------------- */ -/* JPEG 2000 */ +/* JPEG 2000 */ /* -------------------------------------------------------------------- */ #ifdef HAVE_OPENJPEG -#include "Jpeg2K.h" +#include "libImaging/Jpeg2K.h" static void j2k_decode_coord_tuple(PyObject *tuple, int *x, int *y) @@ -1111,10 +1156,12 @@ j2k_decode_coord_tuple(PyObject *tuple, int *x, int *y) *x = (int)PyLong_AsLong(PyTuple_GET_ITEM(tuple, 0)); *y = (int)PyLong_AsLong(PyTuple_GET_ITEM(tuple, 1)); - if (*x < 0) + if (*x < 0) { *x = 0; - if (*y < 0) + } + if (*y < 0) { *y = 0; + } } } @@ -1144,45 +1191,50 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) &quality_mode, &quality_layers, &num_resolutions, &cblk_size, &precinct_size, &irreversible, &progression, &cinema_mode, - &fd)) + &fd)) { return NULL; + } - if (strcmp (format, "j2k") == 0) + if (strcmp (format, "j2k") == 0) { codec_format = OPJ_CODEC_J2K; - else if (strcmp (format, "jpt") == 0) + } else if (strcmp (format, "jpt") == 0) { codec_format = OPJ_CODEC_JPT; - else if (strcmp (format, "jp2") == 0) + } else if (strcmp (format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; - else + } else { return NULL; + } - if (strcmp(progression, "LRCP") == 0) + if (strcmp(progression, "LRCP") == 0) { prog_order = OPJ_LRCP; - else if (strcmp(progression, "RLCP") == 0) + } else if (strcmp(progression, "RLCP") == 0) { prog_order = OPJ_RLCP; - else if (strcmp(progression, "RPCL") == 0) + } else if (strcmp(progression, "RPCL") == 0) { prog_order = OPJ_RPCL; - else if (strcmp(progression, "PCRL") == 0) + } else if (strcmp(progression, "PCRL") == 0) { prog_order = OPJ_PCRL; - else if (strcmp(progression, "CPRL") == 0) + } else if (strcmp(progression, "CPRL") == 0) { prog_order = OPJ_CPRL; - else + } else { return NULL; + } - if (strcmp(cinema_mode, "no") == 0) + if (strcmp(cinema_mode, "no") == 0) { cine_mode = OPJ_OFF; - else if (strcmp(cinema_mode, "cinema2k-24") == 0) + } else if (strcmp(cinema_mode, "cinema2k-24") == 0) { cine_mode = OPJ_CINEMA2K_24; - else if (strcmp(cinema_mode, "cinema2k-48") == 0) + } else if (strcmp(cinema_mode, "cinema2k-48") == 0) { cine_mode = OPJ_CINEMA2K_48; - else if (strcmp(cinema_mode, "cinema4k-24") == 0) + } else if (strcmp(cinema_mode, "cinema4k-24") == 0) { cine_mode = OPJ_CINEMA4K_24; - else + } else { return NULL; + } encoder = PyImaging_EncoderNew(sizeof(JPEG2KENCODESTATE)); - if (!encoder) + if (!encoder) { return NULL; + } encoder->encode = ImagingJpeg2KEncode; encoder->cleanup = ImagingJpeg2KEncodeCleanup; diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 15ffa11fc..755e2639a 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -22,8 +22,9 @@ static inline UINT32 hash(const char* mode) { UINT32 i = ACCESS_TABLE_HASH; - while (*mode) + while (*mode) { i = ((i<<5) + i) ^ (UINT8) *mode++; + } return i % ACCESS_TABLE_SIZE; } @@ -149,10 +150,11 @@ get_pixel_32B(Imaging im, int x, int y, void* color) static void put_pixel(Imaging im, int x, int y, const void* color) { - if (im->image8) + if (im->image8) { im->image8[y][x] = *((UINT8*) color); - else + } else { memcpy(&im->image32[y][x], color, sizeof(INT32)); + } } static void @@ -237,8 +239,9 @@ ImagingAccess ImagingAccessNew(Imaging im) { ImagingAccess access = &access_table[hash(im->mode)]; - if (im->mode[0] != access->mode[0] || strcmp(im->mode, access->mode) != 0) + if (im->mode[0] != access->mode[0] || strcmp(im->mode, access->mode) != 0) { return NULL; + } return access; } diff --git a/src/libImaging/AlphaComposite.c b/src/libImaging/AlphaComposite.c index a074334aa..20b1df9e5 100644 --- a/src/libImaging/AlphaComposite.c +++ b/src/libImaging/AlphaComposite.c @@ -33,19 +33,22 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA") || imDst->type != IMAGING_TYPE_UINT8 || - imDst->bands != 4) + imDst->bands != 4) { return ImagingError_ModeError(); + } if (strcmp(imDst->mode, imSrc->mode) || imDst->type != imSrc->type || imDst->bands != imSrc->bands || imDst->xsize != imSrc->xsize || - imDst->ysize != imSrc->ysize) + imDst->ysize != imSrc->ysize) { return ImagingError_Mismatch(); + } imOut = ImagingNewDirty(imDst->mode, imDst->xsize, imDst->ysize); - if (!imOut) + if (!imOut) { return NULL; + } for (y = 0; y < imDst->ysize; y++) { rgba8* dst = (rgba8*) imDst->image[y]; diff --git a/src/libImaging/Bands.c b/src/libImaging/Bands.c index 7fff04486..39ce5c49c 100644 --- a/src/libImaging/Bands.c +++ b/src/libImaging/Bands.c @@ -26,23 +26,28 @@ ImagingGetBand(Imaging imIn, int band) int x, y; /* Check arguments */ - if (!imIn || imIn->type != IMAGING_TYPE_UINT8) + if (!imIn || imIn->type != IMAGING_TYPE_UINT8) { return (Imaging) ImagingError_ModeError(); + } - if (band < 0 || band >= imIn->bands) + if (band < 0 || band >= imIn->bands) { return (Imaging) ImagingError_ValueError("band index out of range"); + } /* Shortcuts */ - if (imIn->bands == 1) + if (imIn->bands == 1) { return ImagingCopy(imIn); + } /* Special case for LXXA etc */ - if (imIn->bands == 2 && band == 1) + if (imIn->bands == 2 && band == 1) { band = 3; + } imOut = ImagingNewDirty("L", imIn->xsize, imIn->ysize); - if (!imOut) + if (!imOut) { return NULL; + } /* Extract band from image */ for (y = 0; y < imIn->ysize; y++) { @@ -173,24 +178,29 @@ ImagingPutBand(Imaging imOut, Imaging imIn, int band) int x, y; /* Check arguments */ - if (!imIn || imIn->bands != 1 || !imOut) + if (!imIn || imIn->bands != 1 || !imOut) { return (Imaging) ImagingError_ModeError(); + } - if (band < 0 || band >= imOut->bands) + if (band < 0 || band >= imOut->bands) { return (Imaging) ImagingError_ValueError("band index out of range"); + } if (imIn->type != imOut->type || imIn->xsize != imOut->xsize || - imIn->ysize != imOut->ysize) + imIn->ysize != imOut->ysize) { return (Imaging) ImagingError_Mismatch(); + } /* Shortcuts */ - if (imOut->bands == 1) + if (imOut->bands == 1) { return ImagingCopy2(imOut, imIn); + } /* Special case for LXXA etc */ - if (imOut->bands == 2 && band == 1) + if (imOut->bands == 2 && band == 1) { band = 3; + } /* Insert band into image */ for (y = 0; y < imIn->ysize; y++) { @@ -211,15 +221,18 @@ ImagingFillBand(Imaging imOut, int band, int color) int x, y; /* Check arguments */ - if (!imOut || imOut->type != IMAGING_TYPE_UINT8) + if (!imOut || imOut->type != IMAGING_TYPE_UINT8) { return (Imaging) ImagingError_ModeError(); + } - if (band < 0 || band >= imOut->bands) + if (band < 0 || band >= imOut->bands) { return (Imaging) ImagingError_ValueError("band index out of range"); + } /* Special case for LXXA etc */ - if (imOut->bands == 2 && band == 1) + if (imOut->bands == 2 && band == 1) { band = 3; + } color = CLIP8(color); @@ -263,16 +276,18 @@ ImagingMerge(const char* mode, Imaging bands[4]) bandsCount = i; imOut = ImagingNewDirty(mode, firstBand->xsize, firstBand->ysize); - if ( ! imOut) + if ( ! imOut) { return NULL; + } if (imOut->bands != bandsCount) { ImagingDelete(imOut); return (Imaging) ImagingError_ValueError("wrong number of bands"); } - if (imOut->bands == 1) + if (imOut->bands == 1) { return ImagingCopy2(imOut, firstBand); + } if (imOut->bands == 2) { for (y = 0; y < imOut->ysize; y++) { diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index c2c4f21e7..f908a03ad 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -16,841 +16,852 @@ typedef struct { - UINT8 r, g, b, a; + UINT8 r, g, b, a; } rgba; typedef struct { - UINT8 l; + UINT8 l; } lum; typedef struct { - FLOAT32 r, g, b; + FLOAT32 r, g, b; } rgb32f; typedef struct { - UINT16 c0, c1; - UINT32 lut; + UINT16 c0, c1; + UINT32 lut; } bc1_color; typedef struct { - UINT8 a0, a1; - UINT8 lut[6]; + UINT8 a0, a1; + UINT8 lut[6]; } bc3_alpha; #define LOAD16(p) \ - (p)[0] | ((p)[1] << 8) + (p)[0] | ((p)[1] << 8) #define LOAD32(p) \ - (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24) + (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24) static void bc1_color_load(bc1_color *dst, const UINT8 *src) { - dst->c0 = LOAD16(src); - dst->c1 = LOAD16(src + 2); - dst->lut = LOAD32(src + 4); + dst->c0 = LOAD16(src); + dst->c1 = LOAD16(src + 2); + dst->lut = LOAD32(src + 4); } static void bc3_alpha_load(bc3_alpha *dst, const UINT8 *src) { - memcpy(dst, src, sizeof(bc3_alpha)); + memcpy(dst, src, sizeof(bc3_alpha)); } static rgba decode_565(UINT16 x) { - rgba c; - int r, g, b; - r = (x & 0xf800) >> 8; - r |= r >> 5; - c.r = r; - g = (x & 0x7e0) >> 3; - g |= g >> 6; - c.g = g; - b = (x & 0x1f) << 3; - b |= b >> 5; - c.b = b; - c.a = 0xff; - return c; + rgba c; + int r, g, b; + r = (x & 0xf800) >> 8; + r |= r >> 5; + c.r = r; + g = (x & 0x7e0) >> 3; + g |= g >> 6; + c.g = g; + b = (x & 0x1f) << 3; + b |= b >> 5; + c.b = b; + c.a = 0xff; + return c; } static void decode_bc1_color(rgba *dst, const UINT8 *src) { - bc1_color col; - rgba p[4]; - int n, cw; - UINT16 r0, g0, b0, r1, g1, b1; - bc1_color_load(&col, src); + bc1_color col; + rgba p[4]; + int n, cw; + UINT16 r0, g0, b0, r1, g1, b1; + bc1_color_load(&col, src); - p[0] = decode_565(col.c0); - r0 = p[0].r; - g0 = p[0].g; - b0 = p[0].b; - p[1] = decode_565(col.c1); - r1 = p[1].r; - g1 = p[1].g; - b1 = p[1].b; - if (col.c0 > col.c1) { - p[2].r = (2*r0 + 1*r1) / 3; - p[2].g = (2*g0 + 1*g1) / 3; - p[2].b = (2*b0 + 1*b1) / 3; - p[2].a = 0xff; - p[3].r = (1*r0 + 2*r1) / 3; - p[3].g = (1*g0 + 2*g1) / 3; - p[3].b = (1*b0 + 2*b1) / 3; - p[3].a = 0xff; - } else { - p[2].r = (r0 + r1) / 2; - p[2].g = (g0 + g1) / 2; - p[2].b = (b0 + b1) / 2; - p[2].a = 0xff; - p[3].r = 0; - p[3].g = 0; - p[3].b = 0; - p[3].a = 0; - } - for (n = 0; n < 16; n++) { - cw = 3 & (col.lut >> (2 * n)); - dst[n] = p[cw]; - } + p[0] = decode_565(col.c0); + r0 = p[0].r; + g0 = p[0].g; + b0 = p[0].b; + p[1] = decode_565(col.c1); + r1 = p[1].r; + g1 = p[1].g; + b1 = p[1].b; + if (col.c0 > col.c1) { + p[2].r = (2*r0 + 1*r1) / 3; + p[2].g = (2*g0 + 1*g1) / 3; + p[2].b = (2*b0 + 1*b1) / 3; + p[2].a = 0xff; + p[3].r = (1*r0 + 2*r1) / 3; + p[3].g = (1*g0 + 2*g1) / 3; + p[3].b = (1*b0 + 2*b1) / 3; + p[3].a = 0xff; + } else { + p[2].r = (r0 + r1) / 2; + p[2].g = (g0 + g1) / 2; + p[2].b = (b0 + b1) / 2; + p[2].a = 0xff; + p[3].r = 0; + p[3].g = 0; + p[3].b = 0; + p[3].a = 0; + } + for (n = 0; n < 16; n++) { + cw = 3 & (col.lut >> (2 * n)); + dst[n] = p[cw]; + } } static void decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o) { - bc3_alpha b; - UINT16 a0, a1; - UINT8 a[8]; - int n, lut, aw; - bc3_alpha_load(&b, src); + bc3_alpha b; + UINT16 a0, a1; + UINT8 a[8]; + int n, lut, aw; + bc3_alpha_load(&b, src); - a0 = b.a0; - a1 = b.a1; - a[0] = (UINT8)a0; - a[1] = (UINT8)a1; - if (a0 > a1) { - a[2] = (6*a0 + 1*a1) / 7; - a[3] = (5*a0 + 2*a1) / 7; - a[4] = (4*a0 + 3*a1) / 7; - a[5] = (3*a0 + 4*a1) / 7; - a[6] = (2*a0 + 5*a1) / 7; - a[7] = (1*a0 + 6*a1) / 7; - } else { - a[2] = (4*a0 + 1*a1) / 5; - a[3] = (3*a0 + 2*a1) / 5; - a[4] = (2*a0 + 3*a1) / 5; - a[5] = (1*a0 + 4*a1) / 5; - a[6] = 0; - a[7] = 0xff; - } - lut = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); - for (n = 0; n < 8; n++) { - aw = 7 & (lut >> (3 * n)); - dst[stride * n + o] = a[aw]; - } - lut = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); - for (n = 0; n < 8; n++) { - aw = 7 & (lut >> (3 * n)); - dst[stride * (8+n) + o] = a[aw]; - } + a0 = b.a0; + a1 = b.a1; + a[0] = (UINT8)a0; + a[1] = (UINT8)a1; + if (a0 > a1) { + a[2] = (6*a0 + 1*a1) / 7; + a[3] = (5*a0 + 2*a1) / 7; + a[4] = (4*a0 + 3*a1) / 7; + a[5] = (3*a0 + 4*a1) / 7; + a[6] = (2*a0 + 5*a1) / 7; + a[7] = (1*a0 + 6*a1) / 7; + } else { + a[2] = (4*a0 + 1*a1) / 5; + a[3] = (3*a0 + 2*a1) / 5; + a[4] = (2*a0 + 3*a1) / 5; + a[5] = (1*a0 + 4*a1) / 5; + a[6] = 0; + a[7] = 0xff; + } + lut = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); + for (n = 0; n < 8; n++) { + aw = 7 & (lut >> (3 * n)); + dst[stride * n + o] = a[aw]; + } + lut = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); + for (n = 0; n < 8; n++) { + aw = 7 & (lut >> (3 * n)); + dst[stride * (8+n) + o] = a[aw]; + } } static void decode_bc1_block(rgba *col, const UINT8* src) { - decode_bc1_color(col, src); + decode_bc1_color(col, src); } static void decode_bc2_block(rgba *col, const UINT8* src) { - int n, bitI, byI, av; - decode_bc1_color(col, src + 8); - for (n = 0; n < 16; n++) { - bitI = n * 4; - byI = bitI >> 3; - av = 0xf & (src[byI] >> (bitI & 7)); - av = (av << 4) | av; - col[n].a = av; - } + int n, bitI, byI, av; + decode_bc1_color(col, src + 8); + for (n = 0; n < 16; n++) { + bitI = n * 4; + byI = bitI >> 3; + av = 0xf & (src[byI] >> (bitI & 7)); + av = (av << 4) | av; + col[n].a = av; + } } static void decode_bc3_block(rgba *col, const UINT8* src) { - decode_bc1_color(col, src + 8); - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3); + decode_bc1_color(col, src + 8); + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3); } static void decode_bc4_block(lum *col, const UINT8* src) { - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0); + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0); } static void decode_bc5_block(rgba *col, const UINT8* src) { - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0); - decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1); + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0); + decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1); } /* BC6 and BC7 are described here: https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_texture_compression_bptc.txt */ static UINT8 get_bit(const UINT8* src, int bit) { - int by = bit >> 3; - bit &= 7; - return (src[by] >> bit) & 1; + int by = bit >> 3; + bit &= 7; + return (src[by] >> bit) & 1; } static UINT8 get_bits(const UINT8* src, int bit, int count) { - UINT8 v; - int x; - int by = bit >> 3; - bit &= 7; - if (!count) { - return 0; - } - if (bit + count <= 8) { - v = (src[by] >> bit) & ((1 << count) - 1); - } else { - x = src[by] | (src[by+1] << 8); - v = (x >> bit) & ((1 << count) - 1); - } - return v; + UINT8 v; + int x; + int by = bit >> 3; + bit &= 7; + if (!count) { + return 0; + } + if (bit + count <= 8) { + v = (src[by] >> bit) & ((1 << count) - 1); + } else { + x = src[by] | (src[by+1] << 8); + v = (x >> bit) & ((1 << count) - 1); + } + return v; } /* BC7 */ typedef struct { - char ns; - char pb; - char rb; - char isb; - char cb; - char ab; - char epb; - char spb; - char ib; - char ib2; + char ns; + char pb; + char rb; + char isb; + char cb; + char ab; + char epb; + char spb; + char ib; + char ib2; } bc7_mode_info; static const bc7_mode_info bc7_modes[] = { - {3, 4, 0, 0, 4, 0, 1, 0, 3, 0}, - {2, 6, 0, 0, 6, 0, 0, 1, 3, 0}, - {3, 6, 0, 0, 5, 0, 0, 0, 2, 0}, - {2, 6, 0, 0, 7, 0, 1, 0, 2, 0}, - {1, 0, 2, 1, 5, 6, 0, 0, 2, 3}, - {1, 0, 2, 0, 7, 8, 0, 0, 2, 2}, - {1, 0, 0, 0, 7, 7, 1, 0, 4, 0}, - {2, 6, 0, 0, 5, 5, 1, 0, 2, 0} + {3, 4, 0, 0, 4, 0, 1, 0, 3, 0}, + {2, 6, 0, 0, 6, 0, 0, 1, 3, 0}, + {3, 6, 0, 0, 5, 0, 0, 0, 2, 0}, + {2, 6, 0, 0, 7, 0, 1, 0, 2, 0}, + {1, 0, 2, 1, 5, 6, 0, 0, 2, 3}, + {1, 0, 2, 0, 7, 8, 0, 0, 2, 2}, + {1, 0, 0, 0, 7, 7, 1, 0, 4, 0}, + {2, 6, 0, 0, 5, 5, 1, 0, 2, 0} }; /* Subset indices: Table.P2, 1 bit per index */ static const UINT16 bc7_si2[] = { - 0xcccc, 0x8888, 0xeeee, 0xecc8, 0xc880, 0xfeec, 0xfec8, 0xec80, - 0xc800, 0xffec, 0xfe80, 0xe800, 0xffe8, 0xff00, 0xfff0, 0xf000, - 0xf710, 0x008e, 0x7100, 0x08ce, 0x008c, 0x7310, 0x3100, 0x8cce, - 0x088c, 0x3110, 0x6666, 0x366c, 0x17e8, 0x0ff0, 0x718e, 0x399c, - 0xaaaa, 0xf0f0, 0x5a5a, 0x33cc, 0x3c3c, 0x55aa, 0x9696, 0xa55a, - 0x73ce, 0x13c8, 0x324c, 0x3bdc, 0x6996, 0xc33c, 0x9966, 0x0660, - 0x0272, 0x04e4, 0x4e40, 0x2720, 0xc936, 0x936c, 0x39c6, 0x639c, - 0x9336, 0x9cc6, 0x817e, 0xe718, 0xccf0, 0x0fcc, 0x7744, 0xee22}; + 0xcccc, 0x8888, 0xeeee, 0xecc8, 0xc880, 0xfeec, 0xfec8, 0xec80, + 0xc800, 0xffec, 0xfe80, 0xe800, 0xffe8, 0xff00, 0xfff0, 0xf000, + 0xf710, 0x008e, 0x7100, 0x08ce, 0x008c, 0x7310, 0x3100, 0x8cce, + 0x088c, 0x3110, 0x6666, 0x366c, 0x17e8, 0x0ff0, 0x718e, 0x399c, + 0xaaaa, 0xf0f0, 0x5a5a, 0x33cc, 0x3c3c, 0x55aa, 0x9696, 0xa55a, + 0x73ce, 0x13c8, 0x324c, 0x3bdc, 0x6996, 0xc33c, 0x9966, 0x0660, + 0x0272, 0x04e4, 0x4e40, 0x2720, 0xc936, 0x936c, 0x39c6, 0x639c, + 0x9336, 0x9cc6, 0x817e, 0xe718, 0xccf0, 0x0fcc, 0x7744, 0xee22}; /* Table.P3, 2 bits per index */ static const UINT32 bc7_si3[] = { - 0xaa685050, 0x6a5a5040, 0x5a5a4200, 0x5450a0a8, - 0xa5a50000, 0xa0a05050, 0x5555a0a0, 0x5a5a5050, - 0xaa550000, 0xaa555500, 0xaaaa5500, 0x90909090, - 0x94949494, 0xa4a4a4a4, 0xa9a59450, 0x2a0a4250, - 0xa5945040, 0x0a425054, 0xa5a5a500, 0x55a0a0a0, - 0xa8a85454, 0x6a6a4040, 0xa4a45000, 0x1a1a0500, - 0x0050a4a4, 0xaaa59090, 0x14696914, 0x69691400, - 0xa08585a0, 0xaa821414, 0x50a4a450, 0x6a5a0200, - 0xa9a58000, 0x5090a0a8, 0xa8a09050, 0x24242424, - 0x00aa5500, 0x24924924, 0x24499224, 0x50a50a50, - 0x500aa550, 0xaaaa4444, 0x66660000, 0xa5a0a5a0, - 0x50a050a0, 0x69286928, 0x44aaaa44, 0x66666600, - 0xaa444444, 0x54a854a8, 0x95809580, 0x96969600, - 0xa85454a8, 0x80959580, 0xaa141414, 0x96960000, - 0xaaaa1414, 0xa05050a0, 0xa0a5a5a0, 0x96000000, - 0x40804080, 0xa9a8a9a8, 0xaaaaaa44, 0x2a4a5254}; + 0xaa685050, 0x6a5a5040, 0x5a5a4200, 0x5450a0a8, + 0xa5a50000, 0xa0a05050, 0x5555a0a0, 0x5a5a5050, + 0xaa550000, 0xaa555500, 0xaaaa5500, 0x90909090, + 0x94949494, 0xa4a4a4a4, 0xa9a59450, 0x2a0a4250, + 0xa5945040, 0x0a425054, 0xa5a5a500, 0x55a0a0a0, + 0xa8a85454, 0x6a6a4040, 0xa4a45000, 0x1a1a0500, + 0x0050a4a4, 0xaaa59090, 0x14696914, 0x69691400, + 0xa08585a0, 0xaa821414, 0x50a4a450, 0x6a5a0200, + 0xa9a58000, 0x5090a0a8, 0xa8a09050, 0x24242424, + 0x00aa5500, 0x24924924, 0x24499224, 0x50a50a50, + 0x500aa550, 0xaaaa4444, 0x66660000, 0xa5a0a5a0, + 0x50a050a0, 0x69286928, 0x44aaaa44, 0x66666600, + 0xaa444444, 0x54a854a8, 0x95809580, 0x96969600, + 0xa85454a8, 0x80959580, 0xaa141414, 0x96960000, + 0xaaaa1414, 0xa05050a0, 0xa0a5a5a0, 0x96000000, + 0x40804080, 0xa9a8a9a8, 0xaaaaaa44, 0x2a4a5254}; /* Anchor indices: Table.A2 */ static const char bc7_ai0[] = { - 15,15,15,15,15,15,15,15, - 15,15,15,15,15,15,15,15, - 15, 2, 8, 2, 2, 8, 8,15, - 2, 8, 2, 2, 8, 8, 2, 2, - 15,15, 6, 8, 2, 8,15,15, - 2, 8, 2, 2, 2,15,15, 6, - 6, 2, 6, 8,15,15, 2, 2, - 15,15,15,15,15, 2, 2,15}; + 15,15,15,15,15,15,15,15, + 15,15,15,15,15,15,15,15, + 15, 2, 8, 2, 2, 8, 8,15, + 2, 8, 2, 2, 8, 8, 2, 2, + 15,15, 6, 8, 2, 8,15,15, + 2, 8, 2, 2, 2,15,15, 6, + 6, 2, 6, 8,15,15, 2, 2, + 15,15,15,15,15, 2, 2,15}; /* Table.A3a */ static const char bc7_ai1[] = { - 3, 3,15,15, 8, 3,15,15, - 8, 8, 6, 6, 6, 5, 3, 3, - 3, 3, 8,15, 3, 3, 6,10, - 5, 8, 8, 6, 8, 5,15,15, - 8,15, 3, 5, 6,10, 8,15, - 15, 3,15, 5,15,15,15,15, - 3,15, 5, 5, 5, 8, 5,10, - 5,10, 8,13,15,12, 3, 3}; + 3, 3,15,15, 8, 3,15,15, + 8, 8, 6, 6, 6, 5, 3, 3, + 3, 3, 8,15, 3, 3, 6,10, + 5, 8, 8, 6, 8, 5,15,15, + 8,15, 3, 5, 6,10, 8,15, + 15, 3,15, 5,15,15,15,15, + 3,15, 5, 5, 5, 8, 5,10, + 5,10, 8,13,15,12, 3, 3}; /* Table.A3b */ static const char bc7_ai2[] = { - 15, 8, 8, 3,15,15, 3, 8, - 15,15,15,15,15,15,15, 8, - 15, 8,15, 3,15, 8,15, 8, - 3,15, 6,10,15,15,10, 8, - 15, 3,15,10,10, 8, 9,10, - 6,15, 8,15, 3, 6, 6, 8, - 15, 3,15,15,15,15,15,15, - 15,15,15,15, 3,15,15, 8}; + 15, 8, 8, 3,15,15, 3, 8, + 15,15,15,15,15,15,15, 8, + 15, 8,15, 3,15, 8,15, 8, + 3,15, 6,10,15,15,10, 8, + 15, 3,15,10,10, 8, 9,10, + 6,15, 8,15, 3, 6, 6, 8, + 15, 3,15,15,15,15,15,15, + 15,15,15,15, 3,15,15, 8}; /* Interpolation weights */ static const char bc7_weights2[] = {0, 21, 43, 64}; static const char bc7_weights3[] = {0, 9, 18, 27, 37, 46, 55, 64}; static const char bc7_weights4[] = { - 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64}; + 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64}; static const char *bc7_get_weights(int n) { - if (n == 2) { - return bc7_weights2; - } - if (n == 3) { - return bc7_weights3; - } - return bc7_weights4; + if (n == 2) { + return bc7_weights2; + } + if (n == 3) { + return bc7_weights3; + } + return bc7_weights4; } static int bc7_get_subset(int ns, int partition, int n) { - if (ns == 2) { - return 1 & (bc7_si2[partition] >> n); - } - if (ns == 3) { - return 3 & (bc7_si3[partition] >> (2 * n)); - } - return 0; + if (ns == 2) { + return 1 & (bc7_si2[partition] >> n); + } + if (ns == 3) { + return 3 & (bc7_si3[partition] >> (2 * n)); + } + return 0; } static UINT8 expand_quantized(UINT8 v, int bits) { - v = v << (8 - bits); - return v | (v >> bits); + v = v << (8 - bits); + return v | (v >> bits); } static void bc7_lerp(rgba *dst, const rgba *e, int s0, int s1) { - int t0 = 64 - s0; - int t1 = 64 - s1; - dst->r = (UINT8)((t0 * e[0].r + s0 * e[1].r + 32) >> 6); - dst->g = (UINT8)((t0 * e[0].g + s0 * e[1].g + 32) >> 6); - dst->b = (UINT8)((t0 * e[0].b + s0 * e[1].b + 32) >> 6); - dst->a = (UINT8)((t1 * e[0].a + s1 * e[1].a + 32) >> 6); + int t0 = 64 - s0; + int t1 = 64 - s1; + dst->r = (UINT8)((t0 * e[0].r + s0 * e[1].r + 32) >> 6); + dst->g = (UINT8)((t0 * e[0].g + s0 * e[1].g + 32) >> 6); + dst->b = (UINT8)((t0 * e[0].b + s0 * e[1].b + 32) >> 6); + dst->a = (UINT8)((t1 * e[0].a + s1 * e[1].a + 32) >> 6); } static void decode_bc7_block(rgba *col, const UINT8* src) { - rgba endpoints[6]; - int bit = 0, cibit, aibit; - int mode = src[0]; - int i, j; - int numep, cb, ab, ib, ib2, i0, i1, s; - UINT8 index_sel, partition, rotation, val; - const char *cw, *aw; - const bc7_mode_info *info; + rgba endpoints[6]; + int bit = 0, cibit, aibit; + int mode = src[0]; + int i, j; + int numep, cb, ab, ib, ib2, i0, i1, s; + UINT8 index_sel, partition, rotation, val; + const char *cw, *aw; + const bc7_mode_info *info; - /* mode is the number of unset bits before the first set bit: */ - if (!mode) { - /* degenerate case when no bits set */ - for (i = 0; i < 16; i++) { - col[i].r = col[i].g = col[i].b = 0; - col[i].a = 255; - } - return; - } - while (!(mode & (1 << bit++))) ; - mode = bit - 1; - info = &bc7_modes[mode]; - /* color selection bits: {subset}{endpoint} */ - cb = info->cb; - ab = info->ab; - cw = bc7_get_weights(info->ib); - aw = bc7_get_weights((ab && info->ib2) ? info->ib2 : info->ib); + /* mode is the number of unset bits before the first set bit: */ + if (!mode) { + /* degenerate case when no bits set */ + for (i = 0; i < 16; i++) { + col[i].r = col[i].g = col[i].b = 0; + col[i].a = 255; + } + return; + } + while (!(mode & (1 << bit++))) ; + mode = bit - 1; + info = &bc7_modes[mode]; + /* color selection bits: {subset}{endpoint} */ + cb = info->cb; + ab = info->ab; + cw = bc7_get_weights(info->ib); + aw = bc7_get_weights((ab && info->ib2) ? info->ib2 : info->ib); #define LOAD(DST, N) \ - DST = get_bits(src, bit, N); \ - bit += N; - LOAD(partition, info->pb); - LOAD(rotation, info->rb); - LOAD(index_sel, info->isb); - numep = info->ns << 1; + DST = get_bits(src, bit, N); \ + bit += N; + LOAD(partition, info->pb); + LOAD(rotation, info->rb); + LOAD(index_sel, info->isb); + numep = info->ns << 1; - /* red */ - for (i = 0; i < numep; i++) { - LOAD(val, cb); - endpoints[i].r = val; - } + /* red */ + for (i = 0; i < numep; i++) { + LOAD(val, cb); + endpoints[i].r = val; + } - /* green */ - for (i = 0; i < numep; i++) { - LOAD(val, cb); - endpoints[i].g = val; - } + /* green */ + for (i = 0; i < numep; i++) { + LOAD(val, cb); + endpoints[i].g = val; + } - /* blue */ - for (i = 0; i < numep; i++) { - LOAD(val, cb); - endpoints[i].b = val; - } + /* blue */ + for (i = 0; i < numep; i++) { + LOAD(val, cb); + endpoints[i].b = val; + } - /* alpha */ - for (i = 0; i < numep; i++) { - if (ab) { - LOAD(val, ab); - } else { - val = 255; - } - endpoints[i].a = val; - } + /* alpha */ + for (i = 0; i < numep; i++) { + if (ab) { + LOAD(val, ab); + } else { + val = 255; + } + endpoints[i].a = val; + } - /* p-bits */ + /* p-bits */ #define ASSIGN_P(x) x = (x << 1) | val - if (info->epb) { - /* per endpoint */ - cb++; - if (ab) { - ab++; - } - for (i = 0; i < numep; i++) { - LOAD(val, 1); - ASSIGN_P(endpoints[i].r); - ASSIGN_P(endpoints[i].g); - ASSIGN_P(endpoints[i].b); - if (ab) { - ASSIGN_P(endpoints[i].a); - } - } - } - if (info->spb) { - /* per subset */ - cb++; - if (ab) { - ab++; - } - for (i = 0; i < numep; i+=2) { - LOAD(val, 1); - for (j = 0; j < 2; j++) { - ASSIGN_P(endpoints[i+j].r); - ASSIGN_P(endpoints[i+j].g); - ASSIGN_P(endpoints[i+j].b); - if (ab) { - ASSIGN_P(endpoints[i+j].a); - } - } - } - } + if (info->epb) { + /* per endpoint */ + cb++; + if (ab) { + ab++; + } + for (i = 0; i < numep; i++) { + LOAD(val, 1); + ASSIGN_P(endpoints[i].r); + ASSIGN_P(endpoints[i].g); + ASSIGN_P(endpoints[i].b); + if (ab) { + ASSIGN_P(endpoints[i].a); + } + } + } + if (info->spb) { + /* per subset */ + cb++; + if (ab) { + ab++; + } + for (i = 0; i < numep; i+=2) { + LOAD(val, 1); + for (j = 0; j < 2; j++) { + ASSIGN_P(endpoints[i+j].r); + ASSIGN_P(endpoints[i+j].g); + ASSIGN_P(endpoints[i+j].b); + if (ab) { + ASSIGN_P(endpoints[i+j].a); + } + } + } + } #undef ASSIGN_P #define EXPAND(x, b) x = expand_quantized(x, b) - for (i = 0; i < numep; i++) { - EXPAND(endpoints[i].r, cb); - EXPAND(endpoints[i].g, cb); - EXPAND(endpoints[i].b, cb); - if (ab) { - EXPAND(endpoints[i].a, ab); - } - } + for (i = 0; i < numep; i++) { + EXPAND(endpoints[i].r, cb); + EXPAND(endpoints[i].g, cb); + EXPAND(endpoints[i].b, cb); + if (ab) { + EXPAND(endpoints[i].a, ab); + } + } #undef EXPAND #undef LOAD - cibit = bit; - aibit = cibit + 16 * info->ib - info->ns; - for (i = 0; i < 16; i++) { - s = bc7_get_subset(info->ns, partition, i) << 1; - ib = info->ib; - if (i == 0) { - ib--; - } else if (info->ns == 2) { - if (i == bc7_ai0[partition]) { - ib--; - } - } else if (info->ns == 3) { - if (i == bc7_ai1[partition]) { - ib--; - } else if (i == bc7_ai2[partition]) { - ib--; - } - } - i0 = get_bits(src, cibit, ib); - cibit += ib; + cibit = bit; + aibit = cibit + 16 * info->ib - info->ns; + for (i = 0; i < 16; i++) { + s = bc7_get_subset(info->ns, partition, i) << 1; + ib = info->ib; + if (i == 0) { + ib--; + } else if (info->ns == 2) { + if (i == bc7_ai0[partition]) { + ib--; + } + } else if (info->ns == 3) { + if (i == bc7_ai1[partition]) { + ib--; + } else if (i == bc7_ai2[partition]) { + ib--; + } + } + i0 = get_bits(src, cibit, ib); + cibit += ib; - if (ab && info->ib2) { - ib2 = info->ib2; - if (ib2 && i == 0) { - ib2--; - } - i1 = get_bits(src, aibit, ib2); - aibit += ib2; - if (index_sel) { - bc7_lerp(&col[i], &endpoints[s], aw[i1], cw[i0]); - } else { - bc7_lerp(&col[i], &endpoints[s], cw[i0], aw[i1]); - } - } else { - bc7_lerp(&col[i], &endpoints[s], cw[i0], cw[i0]); - } + if (ab && info->ib2) { + ib2 = info->ib2; + if (ib2 && i == 0) { + ib2--; + } + i1 = get_bits(src, aibit, ib2); + aibit += ib2; + if (index_sel) { + bc7_lerp(&col[i], &endpoints[s], aw[i1], cw[i0]); + } else { + bc7_lerp(&col[i], &endpoints[s], cw[i0], aw[i1]); + } + } else { + bc7_lerp(&col[i], &endpoints[s], cw[i0], cw[i0]); + } #define ROTATE(x, y) \ - val = x; \ - x = y; \ - y = val - if (rotation == 1) { - ROTATE(col[i].r, col[i].a); - } else if (rotation == 2) { - ROTATE(col[i].g, col[i].a); - } else if (rotation == 3) { - ROTATE(col[i].b, col[i].a); - } + val = x; \ + x = y; \ + y = val + if (rotation == 1) { + ROTATE(col[i].r, col[i].a); + } else if (rotation == 2) { + ROTATE(col[i].g, col[i].a); + } else if (rotation == 3) { + ROTATE(col[i].b, col[i].a); + } #undef ROTATE - } + } } /* BC6 */ typedef struct { - char ns; /* number of subsets (also called regions) */ - char tr; /* whether endpoints are delta-compressed */ - char pb; /* partition bits */ - char epb; /* endpoint bits */ - char rb; /* red bits (delta) */ - char gb; /* green bits (delta) */ - char bb; /* blue bits (delta) */ + char ns; /* number of subsets (also called regions) */ + char tr; /* whether endpoints are delta-compressed */ + char pb; /* partition bits */ + char epb; /* endpoint bits */ + char rb; /* red bits (delta) */ + char gb; /* green bits (delta) */ + char bb; /* blue bits (delta) */ } bc6_mode_info; static const bc6_mode_info bc6_modes[] = { - // 00 - {2, 1, 5, 10, 5, 5, 5}, - // 01 - {2, 1, 5, 7, 6, 6, 6}, - // 10 - {2, 1, 5, 11, 5, 4, 4}, - {2, 1, 5, 11, 4, 5, 4}, - {2, 1, 5, 11, 4, 4, 5}, - {2, 1, 5, 9, 5, 5, 5}, - {2, 1, 5, 8, 6, 5, 5}, - {2, 1, 5, 8, 5, 6, 5}, - {2, 1, 5, 8, 5, 5, 6}, - {2, 0, 5, 6, 6, 6, 6}, - // 11 - {1, 0, 0, 10, 10, 10, 10}, - {1, 1, 0, 11, 9, 9, 9}, - {1, 1, 0, 12, 8, 8, 8}, - {1, 1, 0, 16, 4, 4, 4} + // 00 + {2, 1, 5, 10, 5, 5, 5}, + // 01 + {2, 1, 5, 7, 6, 6, 6}, + // 10 + {2, 1, 5, 11, 5, 4, 4}, + {2, 1, 5, 11, 4, 5, 4}, + {2, 1, 5, 11, 4, 4, 5}, + {2, 1, 5, 9, 5, 5, 5}, + {2, 1, 5, 8, 6, 5, 5}, + {2, 1, 5, 8, 5, 6, 5}, + {2, 1, 5, 8, 5, 5, 6}, + {2, 0, 5, 6, 6, 6, 6}, + // 11 + {1, 0, 0, 10, 10, 10, 10}, + {1, 1, 0, 11, 9, 9, 9}, + {1, 1, 0, 12, 8, 8, 8}, + {1, 1, 0, 16, 4, 4, 4} }; /* Table.F, encoded as a sequence of bit indices */ static const UINT8 bc6_bit_packings[][75] = { - {116, 132, 176, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, - 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, 172, 160, 161, 162, 163, 80, - 81, 82, 83, 84, 173, 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, - 145, 146, 147, 148, 175}, - {117, 164, 165, 0, 1, 2, 3, 4, 5, 6, 172, 173, 132, 16, 17, 18, 19, 20, 21, - 22, 133, 174, 116, 32, 33, 34, 35, 36, 37, 38, 175, 177, 176, 48, 49, 50, - 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, 69, 160, 161, 162, 163, - 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, - 145, 146, 147, 148, 149}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 10, 112, 113, 114, - 115, 64, 65, 66, 67, 26, 172, 160, 161, 162, 163, 80, 81, 82, 83, 42, 173, - 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, - 175}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 10, 164, 112, 113, 114, - 115, 64, 65, 66, 67, 68, 26, 160, 161, 162, 163, 80, 81, 82, 83, 42, 173, - 128, 129, 130, 131, 96, 97, 98, 99, 172, 174, 144, 145, 146, 147, 116, - 175}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 10, 132, 112, 113, 114, - 115, 64, 65, 66, 67, 26, 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 42, - 128, 129, 130, 131, 96, 97, 98, 99, 173, 174, 144, 145, 146, 147, 176, - 175}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 132, 16, 17, 18, 19, 20, 21, 22, 23, 24, 116, - 32, 33, 34, 35, 36, 37, 38, 39, 40, 176, 48, 49, 50, 51, 52, 164, 112, 113, - 114, 115, 64, 65, 66, 67, 68, 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, - 173, 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, - 175}, - {0, 1, 2, 3, 4, 5, 6, 7, 164, 132, 16, 17, 18, 19, 20, 21, 22, 23, 174, 116, - 32, 33, 34, 35, 36, 37, 38, 39, 175, 176, 48, 49, 50, 51, 52, 53, 112, 113, - 114, 115, 64, 65, 66, 67, 68, 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, - 173, 128, 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, - 149}, - {0, 1, 2, 3, 4, 5, 6, 7, 172, 132, 16, 17, 18, 19, 20, 21, 22, 23, 117, 116, - 32, 33, 34, 35, 36, 37, 38, 39, 165, 176, 48, 49, 50, 51, 52, 164, 112, - 113, 114, 115, 64, 65, 66, 67, 68, 69, 160, 161, 162, 163, 80, 81, 82, 83, - 84, 173, 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, - 148, 175}, - {0, 1, 2, 3, 4, 5, 6, 7, 173, 132, 16, 17, 18, 19, 20, 21, 22, 23, 133, 116, - 32, 33, 34, 35, 36, 37, 38, 39, 177, 176, 48, 49, 50, 51, 52, 164, 112, - 113, 114, 115, 64, 65, 66, 67, 68, 172, 160, 161, 162, 163, 80, 81, 82, 83, - 84, 85, 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, - 148, 175}, - {0, 1, 2, 3, 4, 5, 164, 172, 173, 132, 16, 17, 18, 19, 20, 21, 117, 133, - 174, 116, 32, 33, 34, 35, 36, 37, 165, 175, 177, 176, 48, 49, 50, 51, 52, - 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, 69, 160, 161, 162, 163, 80, 81, - 82, 83, 84, 85, 128, 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, - 146, 147, 148, 149}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, - 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 80, 81, 82, 83, 84, 85, 86, 87, 88, - 89}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 10, - 64, 65, 66, 67, 68, 69, 70, 71, 72, 26, 80, 81, 82, 83, 84, 85, 86, 87, 88, - 42}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 11, 10, - 64, 65, 66, 67, 68, 69, 70, 71, 27, 26, 80, 81, 82, 83, 84, 85, 86, 87, 43, - 42}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 15, 14, 13, 12, 11, 10, - 64, 65, 66, 67, 31, 30, 29, 28, 27, 26, 80, 81, 82, 83, 47, 46, 45, 44, 43, - 42}}; + {116, 132, 176, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, + 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, 172, 160, 161, 162, 163, 80, + 81, 82, 83, 84, 173, 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, + 145, 146, 147, 148, 175}, + {117, 164, 165, 0, 1, 2, 3, 4, 5, 6, 172, 173, 132, 16, 17, 18, 19, 20, 21, + 22, 133, 174, 116, 32, 33, 34, 35, 36, 37, 38, 175, 177, 176, 48, 49, 50, + 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, 69, 160, 161, 162, 163, + 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, + 145, 146, 147, 148, 149}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 10, 112, 113, 114, + 115, 64, 65, 66, 67, 26, 172, 160, 161, 162, 163, 80, 81, 82, 83, 42, 173, + 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, + 175}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 10, 164, 112, 113, 114, + 115, 64, 65, 66, 67, 68, 26, 160, 161, 162, 163, 80, 81, 82, 83, 42, 173, + 128, 129, 130, 131, 96, 97, 98, 99, 172, 174, 144, 145, 146, 147, 116, + 175}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 10, 132, 112, 113, 114, + 115, 64, 65, 66, 67, 26, 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 42, + 128, 129, 130, 131, 96, 97, 98, 99, 173, 174, 144, 145, 146, 147, 176, + 175}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 132, 16, 17, 18, 19, 20, 21, 22, 23, 24, 116, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 176, 48, 49, 50, 51, 52, 164, 112, 113, + 114, 115, 64, 65, 66, 67, 68, 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, + 173, 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, + 175}, + {0, 1, 2, 3, 4, 5, 6, 7, 164, 132, 16, 17, 18, 19, 20, 21, 22, 23, 174, 116, + 32, 33, 34, 35, 36, 37, 38, 39, 175, 176, 48, 49, 50, 51, 52, 53, 112, 113, + 114, 115, 64, 65, 66, 67, 68, 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, + 173, 128, 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, + 149}, + {0, 1, 2, 3, 4, 5, 6, 7, 172, 132, 16, 17, 18, 19, 20, 21, 22, 23, 117, 116, + 32, 33, 34, 35, 36, 37, 38, 39, 165, 176, 48, 49, 50, 51, 52, 164, 112, + 113, 114, 115, 64, 65, 66, 67, 68, 69, 160, 161, 162, 163, 80, 81, 82, 83, + 84, 173, 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, + 148, 175}, + {0, 1, 2, 3, 4, 5, 6, 7, 173, 132, 16, 17, 18, 19, 20, 21, 22, 23, 133, 116, + 32, 33, 34, 35, 36, 37, 38, 39, 177, 176, 48, 49, 50, 51, 52, 164, 112, + 113, 114, 115, 64, 65, 66, 67, 68, 172, 160, 161, 162, 163, 80, 81, 82, 83, + 84, 85, 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, + 148, 175}, + {0, 1, 2, 3, 4, 5, 164, 172, 173, 132, 16, 17, 18, 19, 20, 21, 117, 133, + 174, 116, 32, 33, 34, 35, 36, 37, 165, 175, 177, 176, 48, 49, 50, 51, 52, + 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, 69, 160, 161, 162, 163, 80, 81, + 82, 83, 84, 85, 128, 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, + 146, 147, 148, 149}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 80, 81, 82, 83, 84, 85, 86, 87, 88, + 89}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 10, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 26, 80, 81, 82, 83, 84, 85, 86, 87, 88, + 42}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 11, 10, + 64, 65, 66, 67, 68, 69, 70, 71, 27, 26, 80, 81, 82, 83, 84, 85, 86, 87, 43, + 42}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 15, 14, 13, 12, 11, 10, + 64, 65, 66, 67, 31, 30, 29, 28, 27, 26, 80, 81, 82, 83, 47, 46, 45, 44, 43, + 42}}; static void bc6_sign_extend(UINT16 *v, int prec) { - int x = *v; - if (x & (1 << (prec - 1))) { - x |= -1 << prec; - } - *v = (UINT16)x; + int x = *v; + if (x & (1 << (prec - 1))) { + x |= -1 << prec; + } + *v = (UINT16)x; } static int bc6_unquantize(UINT16 v, int prec, int sign) { - int s = 0; - int x; - if (!sign) { - x = v; - if (prec >= 15) return x; - if (x == 0) return 0; - if (x == ((1 << prec) - 1)) { - return 0xffff; - } - return ((x << 15) + 0x4000) >> (prec - 1); - } else { - x = (INT16)v; - if (prec >= 16) return x; - if (x < 0) { - s = 1; - x = -x; - } + int s = 0; + int x; + if (!sign) { + x = v; + if (prec >= 15) { + return x; + } + if (x == 0) { + return 0; + } + if (x == ((1 << prec) - 1)) { + return 0xffff; + } + return ((x << 15) + 0x4000) >> (prec - 1); + } else { + x = (INT16)v; + if (prec >= 16) { + return x; + } + if (x < 0) { + s = 1; + x = -x; + } - if (x != 0) { - if (x >= ((1 << (prec - 1)) - 1)) { - x = 0x7fff; - } else { - x = ((x << 15) + 0x4000) >> (prec - 1); - } - } + if (x != 0) { + if (x >= ((1 << (prec - 1)) - 1)) { + x = 0x7fff; + } else { + x = ((x << 15) + 0x4000) >> (prec - 1); + } + } - if (s) { - return -x; - } - return x; - } + if (s) { + return -x; + } + return x; + } } static float half_to_float(UINT16 h) { - /* https://gist.github.com/rygorous/2144712 */ - union { - UINT32 u; - float f; - } o, m; - m.u = 0x77800000; - o.u = (h & 0x7fff) << 13; - o.f *= m.f; - m.u = 0x47800000; - if (o.f >= m.f) { - o.u |= 255 << 23; - } - o.u |= (h & 0x8000) << 16; - return o.f; + /* https://gist.github.com/rygorous/2144712 */ + union { + UINT32 u; + float f; + } o, m; + m.u = 0x77800000; + o.u = (h & 0x7fff) << 13; + o.f *= m.f; + m.u = 0x47800000; + if (o.f >= m.f) { + o.u |= 255 << 23; + } + o.u |= (h & 0x8000) << 16; + return o.f; } static float bc6_finalize(int v, int sign) { - if (sign) { - if (v < 0) { - v = ((-v) * 31) / 32; - return half_to_float((UINT16)(0x8000 | v)); - } else { - return half_to_float((UINT16)((v * 31) / 32)); - } - } else { - return half_to_float((UINT16)((v * 31) / 64)); - } + if (sign) { + if (v < 0) { + v = ((-v) * 31) / 32; + return half_to_float((UINT16)(0x8000 | v)); + } else { + return half_to_float((UINT16)((v * 31) / 32)); + } + } else { + return half_to_float((UINT16)((v * 31) / 64)); + } } static void bc6_lerp(rgb32f *col, int *e0, int *e1, int s, int sign) { - int r, g, b; - int t = 64 - s; - r = (e0[0] * t + e1[0] * s) >> 6; - g = (e0[1] * t + e1[1] * s) >> 6; - b = (e0[2] * t + e1[2] * s) >> 6; - col->r = bc6_finalize(r, sign); - col->g = bc6_finalize(g, sign); - col->b = bc6_finalize(b, sign); + int r, g, b; + int t = 64 - s; + r = (e0[0] * t + e1[0] * s) >> 6; + g = (e0[1] * t + e1[1] * s) >> 6; + b = (e0[2] * t + e1[2] * s) >> 6; + col->r = bc6_finalize(r, sign); + col->g = bc6_finalize(g, sign); + col->b = bc6_finalize(b, sign); } static void decode_bc6_block(rgb32f *col, const UINT8* src, int sign) { - UINT16 endpoints[12]; /* storage for r0, g0, b0, r1, ... */ - int ueps[12]; - int i, i0, ib2, di, dw, mask, numep, s; - UINT8 partition; - const bc6_mode_info *info; - const char *cw; - int bit = 5; - int epbits = 75; - int ib = 3; - int mode = src[0] & 0x1f; - if ((mode & 3) == 0 || (mode & 3) == 1) { - mode &= 3; - bit = 2; - } else if ((mode & 3) == 2) { - mode = 2 + (mode >> 2); - epbits = 72; - } else { - mode = 10 + (mode >> 2); - epbits = 60; - ib = 4; - } - if (mode >= 14) { - /* invalid block */ - memset(col, 0, 16 * sizeof(col[0])); - return; - } - info = &bc6_modes[mode]; - cw = bc7_get_weights(ib); - numep = info->ns == 2 ? 12 : 6; - for (i = 0; i < 12; i++) { - endpoints[i] = 0; - } - for (i = 0; i < epbits; i++) { - di = bc6_bit_packings[mode][i]; - dw = di >> 4; - di &= 15; - endpoints[dw] |= (UINT16)get_bit(src, bit + i) << di; - } - bit += epbits; - partition = get_bits(src, bit, info->pb); - bit += info->pb; - mask = (1 << info->epb) - 1; - if (sign) { /* sign-extend e0 if signed */ - bc6_sign_extend(&endpoints[0], info->epb); - bc6_sign_extend(&endpoints[1], info->epb); - bc6_sign_extend(&endpoints[2], info->epb); - } - if (sign || info->tr) { /* sign-extend e1,2,3 if signed or deltas */ - for (i = 3; i < numep; i += 3) { - bc6_sign_extend(&endpoints[i+0], info->rb); - bc6_sign_extend(&endpoints[i+1], info->gb); - bc6_sign_extend(&endpoints[i+2], info->bb); - } - } - if (info->tr) { /* apply deltas */ - for (i = 3; i < numep; i++) { - endpoints[i] = (endpoints[i] + endpoints[0]) & mask; - } - if (sign) { - for (i = 3; i < numep; i += 3) { - bc6_sign_extend(&endpoints[i+0], info->rb); - bc6_sign_extend(&endpoints[i+1], info->gb); - bc6_sign_extend(&endpoints[i+2], info->bb); - } - } - } - for (i = 0; i < numep; i++) { - ueps[i] = bc6_unquantize(endpoints[i], info->epb, sign); - } - for (i = 0; i < 16; i++) { - s = bc7_get_subset(info->ns, partition, i) * 6; - ib2 = ib; - if (i == 0) { - ib2--; - } else if (info->ns == 2) { - if (i == bc7_ai0[partition]) { - ib2--; - } - } - i0 = get_bits(src, bit, ib2); - bit += ib2; + UINT16 endpoints[12]; /* storage for r0, g0, b0, r1, ... */ + int ueps[12]; + int i, i0, ib2, di, dw, mask, numep, s; + UINT8 partition; + const bc6_mode_info *info; + const char *cw; + int bit = 5; + int epbits = 75; + int ib = 3; + int mode = src[0] & 0x1f; + if ((mode & 3) == 0 || (mode & 3) == 1) { + mode &= 3; + bit = 2; + } else if ((mode & 3) == 2) { + mode = 2 + (mode >> 2); + epbits = 72; + } else { + mode = 10 + (mode >> 2); + epbits = 60; + ib = 4; + } + if (mode >= 14) { + /* invalid block */ + memset(col, 0, 16 * sizeof(col[0])); + return; + } + info = &bc6_modes[mode]; + cw = bc7_get_weights(ib); + numep = info->ns == 2 ? 12 : 6; + for (i = 0; i < 12; i++) { + endpoints[i] = 0; + } + for (i = 0; i < epbits; i++) { + di = bc6_bit_packings[mode][i]; + dw = di >> 4; + di &= 15; + endpoints[dw] |= (UINT16)get_bit(src, bit + i) << di; + } + bit += epbits; + partition = get_bits(src, bit, info->pb); + bit += info->pb; + mask = (1 << info->epb) - 1; + if (sign) { /* sign-extend e0 if signed */ + bc6_sign_extend(&endpoints[0], info->epb); + bc6_sign_extend(&endpoints[1], info->epb); + bc6_sign_extend(&endpoints[2], info->epb); + } + if (sign || info->tr) { /* sign-extend e1,2,3 if signed or deltas */ + for (i = 3; i < numep; i += 3) { + bc6_sign_extend(&endpoints[i+0], info->rb); + bc6_sign_extend(&endpoints[i+1], info->gb); + bc6_sign_extend(&endpoints[i+2], info->bb); + } + } + if (info->tr) { /* apply deltas */ + for (i = 3; i < numep; i++) { + endpoints[i] = (endpoints[i] + endpoints[0]) & mask; + } + if (sign) { + for (i = 3; i < numep; i += 3) { + bc6_sign_extend(&endpoints[i+0], info->rb); + bc6_sign_extend(&endpoints[i+1], info->gb); + bc6_sign_extend(&endpoints[i+2], info->bb); + } + } + } + for (i = 0; i < numep; i++) { + ueps[i] = bc6_unquantize(endpoints[i], info->epb, sign); + } + for (i = 0; i < 16; i++) { + s = bc7_get_subset(info->ns, partition, i) * 6; + ib2 = ib; + if (i == 0) { + ib2--; + } else if (info->ns == 2) { + if (i == bc7_ai0[partition]) { + ib2--; + } + } + i0 = get_bits(src, bit, ib2); + bit += ib2; - bc6_lerp(&col[i], &ueps[s], &ueps[s+3], cw[i0], sign); - } + bc6_lerp(&col[i], &ueps[s], &ueps[s+3], cw[i0], sign); + } } static void put_block(Imaging im, ImagingCodecState state, const char *col, int sz, int C) { - int width = state->xsize; - int height = state->ysize; - int xmax = width + state->xoff; - int ymax = height + state->yoff; - int j, i, y, x; - char *dst; - for (j = 0; j < 4; j++) { - y = state->y + j; - if (C) { - if (y >= height) { - continue; - } - if (state->ystep < 0) { - y = state->yoff + ymax - y - 1; - } - dst = im->image[y]; - for (i = 0; i < 4; i++) { - x = state->x + i; - if (x >= width) { - continue; - } - memcpy(dst + sz*x, col + sz*(j*4 + i), sz); - } - } else { - if (state->ystep < 0) { - y = state->yoff + ymax - y - 1; - } - x = state->x; - dst = im->image[y] + sz*x; - memcpy(dst, col + sz*(j*4), 4 * sz); - } - } - state->x += 4; - if (state->x >= xmax) { - state->y += 4; - state->x = state->xoff; - } + int width = state->xsize; + int height = state->ysize; + int xmax = width + state->xoff; + int ymax = height + state->yoff; + int j, i, y, x; + char *dst; + for (j = 0; j < 4; j++) { + y = state->y + j; + if (C) { + if (y >= height) { + continue; + } + if (state->ystep < 0) { + y = state->yoff + ymax - y - 1; + } + dst = im->image[y]; + for (i = 0; i < 4; i++) { + x = state->x + i; + if (x >= width) { + continue; + } + memcpy(dst + sz*x, col + sz*(j*4 + i), sz); + } + } else { + if (state->ystep < 0) { + y = state->yoff + ymax - y - 1; + } + x = state->x; + dst = im->image[y] + sz*x; + memcpy(dst, col + sz*(j*4), 4 * sz); + } + } + state->x += 4; + if (state->x >= xmax) { + state->y += 4; + state->x = state->xoff; + } } static int decode_bcn(Imaging im, ImagingCodecState state, const UINT8* src, int bytes, int N, int C) { - int ymax = state->ysize + state->yoff; - const UINT8 *ptr = src; - switch (N) { + int ymax = state->ysize + state->yoff; + const UINT8 *ptr = src; + switch (N) { #define DECODE_LOOP(NN, SZ, TY, ...) \ - case NN: \ - while (bytes >= SZ) { \ - TY col[16]; \ - memset(col, 0, 16 * sizeof(col[0])); \ - decode_bc##NN##_block(col, ptr); \ - put_block(im, state, (const char *)col, sizeof(col[0]), C); \ - ptr += SZ; \ - bytes -= SZ; \ - if (state->y >= ymax) return -1; \ - } \ - break - DECODE_LOOP(1, 8, rgba); - DECODE_LOOP(2, 16, rgba); - DECODE_LOOP(3, 16, rgba); - DECODE_LOOP(4, 8, lum); - DECODE_LOOP(5, 16, rgba); - case 6: - while (bytes >= 16) { - rgb32f col[16]; - decode_bc6_block(col, ptr, (state->state >> 4) & 1); - put_block(im, state, (const char *)col, sizeof(col[0]), C); - ptr += 16; - bytes -= 16; - if (state->y >= ymax) return -1; \ - } - break; - DECODE_LOOP(7, 16, rgba); + case NN: \ + while (bytes >= SZ) { \ + TY col[16]; \ + memset(col, 0, 16 * sizeof(col[0])); \ + decode_bc##NN##_block(col, ptr); \ + put_block(im, state, (const char *)col, sizeof(col[0]), C); \ + ptr += SZ; \ + bytes -= SZ; \ + if (state->y >= ymax) {\ + return -1; \ + }\ + } \ + break + + DECODE_LOOP(1, 8, rgba); + DECODE_LOOP(2, 16, rgba); + DECODE_LOOP(3, 16, rgba); + DECODE_LOOP(4, 8, lum); + DECODE_LOOP(5, 16, rgba); + case 6: + while (bytes >= 16) { + rgb32f col[16]; + decode_bc6_block(col, ptr, (state->state >> 4) & 1); + put_block(im, state, (const char *)col, sizeof(col[0]), C); + ptr += 16; + bytes -= 16; + if (state->y >= ymax) {\ + return -1; \ + }\ + } + break; + DECODE_LOOP(7, 16, rgba); #undef DECODE_LOOP - } - return (int)(ptr - src); + } + return (int)(ptr - src); } int ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { - int N = state->state & 0xf; - int width = state->xsize; - int height = state->ysize; - if ((width & 3) | (height & 3)) { - return decode_bcn(im, state, buf, bytes, N, 1); - } else { - return decode_bcn(im, state, buf, bytes, N, 0); - } + int N = state->state & 0xf; + int width = state->xsize; + int height = state->ysize; + if ((width & 3) | (height & 3)) { + return decode_bcn(im, state, buf, bytes, N, 1); + } else { + return decode_bcn(im, state, buf, bytes, N, 0); + } } diff --git a/src/libImaging/BitDecode.c b/src/libImaging/BitDecode.c index 7120b3321..92edd746f 100644 --- a/src/libImaging/BitDecode.c +++ b/src/libImaging/BitDecode.c @@ -5,7 +5,7 @@ * decoder for packed bitfields (converts to floating point) * * history: - * 97-05-31 fl created (much more than originally intended) + * 97-05-31 fl created (much more than originally intended) * * Copyright (c) Fredrik Lundh 1997. * Copyright (c) Secret Labs AB 1997. @@ -27,33 +27,35 @@ ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt if (state->state == 0) { - /* Initialize context variables */ + /* Initialize context variables */ /* this decoder only works for float32 image buffers */ if (im->type != IMAGING_TYPE_FLOAT32) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; + state->errcode = IMAGING_CODEC_CONFIG; + return -1; } /* sanity check */ if (bitstate->bits < 1 || bitstate->bits >= 32) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; + state->errcode = IMAGING_CODEC_CONFIG; + return -1; } bitstate->mask = (1<bits)-1; - if (bitstate->sign) + if (bitstate->sign) { bitstate->signmask = (1<<(bitstate->bits-1)); + } - /* check image orientation */ - if (state->ystep < 0) { - state->y = state->ysize-1; - state->ystep = -1; - } else - state->ystep = 1; + /* check image orientation */ + if (state->ystep < 0) { + state->y = state->ysize-1; + state->ystep = -1; + } else { + state->ystep = 1; + } - state->state = 1; + state->state = 1; } @@ -67,12 +69,13 @@ ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt bytes--; /* get a byte from the input stream and insert in the bit buffer */ - if (bitstate->fill&1) + if (bitstate->fill&1) { /* fill MSB first */ bitstate->bitbuffer |= (unsigned long) byte << bitstate->bitcount; - else + } else { /* fill LSB first */ bitstate->bitbuffer = (bitstate->bitbuffer << 8) | byte; + } bitstate->bitcount += 8; @@ -85,41 +88,45 @@ ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt if (bitstate->fill&2) { /* store LSB first */ data = bitstate->bitbuffer & bitstate->mask; - if (bitstate->bitcount > 32) + if (bitstate->bitcount > 32) { /* bitbuffer overflow; restore it from last input byte */ bitstate->bitbuffer = byte >> (8 - (bitstate->bitcount - bitstate->bits)); - else + } else { bitstate->bitbuffer >>= bitstate->bits; - } else + } + } else { /* store MSB first */ data = (bitstate->bitbuffer >> (bitstate->bitcount - bitstate->bits)) & bitstate->mask; + } bitstate->bitcount -= bitstate->bits; if (bitstate->lutsize > 0) { /* map through lookup table */ - if (data <= 0) + if (data <= 0) { pixel = bitstate->lut[0]; - else if (data >= bitstate->lutsize) + } else if (data >= bitstate->lutsize) { pixel = bitstate->lut[bitstate->lutsize-1]; - else + } else { pixel = bitstate->lut[data]; + } } else { /* convert */ - if (data & bitstate->signmask) + if (data & bitstate->signmask) { /* image memory contains signed data */ pixel = (FLOAT32) (INT32) (data | ~bitstate->mask); - else + } else { pixel = (FLOAT32) data; + } } *(FLOAT32*)(&im->image32[state->y][state->x]) = pixel; /* step forward */ - if (++state->x >= state->xsize) { + if (++state->x >= state->xsize) { /* new line */ state->y += state->ystep; if (state->y < 0 || state->y >= state->ysize) { @@ -128,8 +135,9 @@ ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt } state->x = 0; /* reset bit buffer */ - if (bitstate->pad > 0) + if (bitstate->pad > 0) { bitstate->bitcount = 0; + } } } } diff --git a/src/libImaging/Blend.c b/src/libImaging/Blend.c index 19a080d6d..0bac4cda9 100644 --- a/src/libImaging/Blend.c +++ b/src/libImaging/Blend.c @@ -5,9 +5,9 @@ * interpolate between two existing images * * history: - * 96-03-20 fl Created - * 96-05-18 fl Simplified blend expression - * 96-10-05 fl Fixed expression bug, special case for interpolation + * 96-03-20 fl Created + * 96-05-18 fl Simplified blend expression + * 96-10-05 fl Fixed expression bug, special case for interpolation * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -28,52 +28,58 @@ ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha) /* Check arguments */ if (!imIn1 || !imIn2 || imIn1->type != IMAGING_TYPE_UINT8 || imIn1->palette || strcmp(imIn1->mode, "1") == 0 - || imIn2->palette || strcmp(imIn2->mode, "1") == 0) + || imIn2->palette || strcmp(imIn2->mode, "1") == 0) { return ImagingError_ModeError(); + } if (imIn1->type != imIn2->type || - imIn1->bands != imIn2->bands || - imIn1->xsize != imIn2->xsize || - imIn1->ysize != imIn2->ysize) - return ImagingError_Mismatch(); + imIn1->bands != imIn2->bands || + imIn1->xsize != imIn2->xsize || + imIn1->ysize != imIn2->ysize) { + return ImagingError_Mismatch(); + } /* Shortcuts */ - if (alpha == 0.0) - return ImagingCopy(imIn1); - else if (alpha == 1.0) - return ImagingCopy(imIn2); + if (alpha == 0.0) { + return ImagingCopy(imIn1); + } else if (alpha == 1.0) { + return ImagingCopy(imIn2); + } imOut = ImagingNewDirty(imIn1->mode, imIn1->xsize, imIn1->ysize); - if (!imOut) - return NULL; + if (!imOut) { + return NULL; + } if (alpha >= 0 && alpha <= 1.0) { - /* Interpolate between bands */ - for (y = 0; y < imIn1->ysize; y++) { - UINT8* in1 = (UINT8*) imIn1->image[y]; - UINT8* in2 = (UINT8*) imIn2->image[y]; - UINT8* out = (UINT8*) imOut->image[y]; - for (x = 0; x < imIn1->linesize; x++) - out[x] = (UINT8) - ((int) in1[x] + alpha * ((int) in2[x] - (int) in1[x])); - } + /* Interpolate between bands */ + for (y = 0; y < imIn1->ysize; y++) { + UINT8* in1 = (UINT8*) imIn1->image[y]; + UINT8* in2 = (UINT8*) imIn2->image[y]; + UINT8* out = (UINT8*) imOut->image[y]; + for (x = 0; x < imIn1->linesize; x++) { + out[x] = (UINT8) + ((int) in1[x] + alpha * ((int) in2[x] - (int) in1[x])); + } + } } else { - /* Extrapolation; must make sure to clip resulting values */ - for (y = 0; y < imIn1->ysize; y++) { - UINT8* in1 = (UINT8*) imIn1->image[y]; - UINT8* in2 = (UINT8*) imIn2->image[y]; - UINT8* out = (UINT8*) imOut->image[y]; - for (x = 0; x < imIn1->linesize; x++) { - float temp = (float) - ((int) in1[x] + alpha * ((int) in2[x] - (int) in1[x])); - if (temp <= 0.0) - out[x] = 0; - else if (temp >= 255.0) - out[x] = 255; - else - out[x] = (UINT8) temp; - } - } + /* Extrapolation; must make sure to clip resulting values */ + for (y = 0; y < imIn1->ysize; y++) { + UINT8* in1 = (UINT8*) imIn1->image[y]; + UINT8* in2 = (UINT8*) imIn2->image[y]; + UINT8* out = (UINT8*) imOut->image[y]; + for (x = 0; x < imIn1->linesize; x++) { + float temp = (float) + ((int) in1[x] + alpha * ((int) in2[x] - (int) in1[x])); + if (temp <= 0.0) { + out[x] = 0; + } else if (temp >= 255.0) { + out[x] = 255; + } else { + out[x] = (UINT8) temp; + } + } + } } return imOut; diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 9537c4f98..dcdc52cbc 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -184,8 +184,9 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) int edgeB = MAX(imIn->xsize - radius - 1, 0); UINT32 *lineOut = calloc(imIn->xsize, sizeof(UINT32)); - if (lineOut == NULL) + if (lineOut == NULL) { return ImagingError_MemoryError(); + } // printf(">>> %d %d %d\n", radius, ww, fw); @@ -248,11 +249,13 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) imIn->type != imOut->type || imIn->bands != imOut->bands || imIn->xsize != imOut->xsize || - imIn->ysize != imOut->ysize) + imIn->ysize != imOut->ysize) { return ImagingError_Mismatch(); + } - if (imIn->type != IMAGING_TYPE_UINT8) + if (imIn->type != IMAGING_TYPE_UINT8) { return ImagingError_ModeError(); + } if (!(strcmp(imIn->mode, "RGB") == 0 || strcmp(imIn->mode, "RGBA") == 0 || @@ -261,12 +264,14 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) strcmp(imIn->mode, "CMYK") == 0 || strcmp(imIn->mode, "L") == 0 || strcmp(imIn->mode, "LA") == 0 || - strcmp(imIn->mode, "La") == 0)) + strcmp(imIn->mode, "La") == 0)) { return ImagingError_ModeError(); + } imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); - if (!imTransposed) + if (!imTransposed) { return NULL; + } /* Apply blur in one dimension. Use imOut as a destination at first pass, diff --git a/src/libImaging/Chops.c b/src/libImaging/Chops.c index 8059b6ffb..a0a70abd9 100644 --- a/src/libImaging/Chops.c +++ b/src/libImaging/Chops.c @@ -19,24 +19,26 @@ #include "Imaging.h" -#define CHOP(operation, mode)\ +#define CHOP(operation)\ int x, y;\ Imaging imOut;\ - imOut = create(imIn1, imIn2, mode);\ - if (!imOut)\ + imOut = create(imIn1, imIn2, NULL);\ + if (!imOut) {\ return NULL;\ + }\ for (y = 0; y < imOut->ysize; y++) {\ UINT8* out = (UINT8*) imOut->image[y];\ UINT8* in1 = (UINT8*) imIn1->image[y];\ UINT8* in2 = (UINT8*) imIn2->image[y];\ for (x = 0; x < imOut->linesize; x++) {\ int temp = operation;\ - if (temp <= 0)\ + if (temp <= 0) {\ out[x] = 0;\ - else if (temp >= 255)\ + } else if (temp >= 255) {\ out[x] = 255;\ - else\ + } else {\ out[x] = temp;\ + }\ }\ }\ return imOut; @@ -45,8 +47,9 @@ int x, y;\ Imaging imOut;\ imOut = create(imIn1, imIn2, mode);\ - if (!imOut)\ + if (!imOut) {\ return NULL;\ + }\ for (y = 0; y < imOut->ysize; y++) {\ UINT8* out = (UINT8*) imOut->image[y];\ UINT8* in1 = (UINT8*) imIn1->image[y];\ @@ -63,11 +66,13 @@ create(Imaging im1, Imaging im2, char* mode) int xsize, ysize; if (!im1 || !im2 || im1->type != IMAGING_TYPE_UINT8 || - (mode != NULL && (strcmp(im1->mode, "1") || strcmp(im2->mode, "1")))) + (mode != NULL && (strcmp(im1->mode, "1") || strcmp(im2->mode, "1")))) { return (Imaging) ImagingError_ModeError(); + } if (im1->type != im2->type || - im1->bands != im2->bands) + im1->bands != im2->bands) { return (Imaging) ImagingError_Mismatch(); + } xsize = (im1->xsize < im2->xsize) ? im1->xsize : im2->xsize; ysize = (im1->ysize < im2->ysize) ? im1->ysize : im2->ysize; @@ -78,43 +83,43 @@ create(Imaging im1, Imaging im2, char* mode) Imaging ImagingChopLighter(Imaging imIn1, Imaging imIn2) { - CHOP((in1[x] > in2[x]) ? in1[x] : in2[x], NULL); + CHOP((in1[x] > in2[x]) ? in1[x] : in2[x]); } Imaging ImagingChopDarker(Imaging imIn1, Imaging imIn2) { - CHOP((in1[x] < in2[x]) ? in1[x] : in2[x], NULL); + CHOP((in1[x] < in2[x]) ? in1[x] : in2[x]); } Imaging ImagingChopDifference(Imaging imIn1, Imaging imIn2) { - CHOP(abs((int) in1[x] - (int) in2[x]), NULL); + CHOP(abs((int) in1[x] - (int) in2[x])); } Imaging ImagingChopMultiply(Imaging imIn1, Imaging imIn2) { - CHOP((int) in1[x] * (int) in2[x] / 255, NULL); + CHOP((int) in1[x] * (int) in2[x] / 255); } Imaging ImagingChopScreen(Imaging imIn1, Imaging imIn2) { - CHOP(255 - ((int) (255 - in1[x]) * (int) (255 - in2[x])) / 255, NULL); + CHOP(255 - ((int) (255 - in1[x]) * (int) (255 - in2[x])) / 255); } Imaging ImagingChopAdd(Imaging imIn1, Imaging imIn2, float scale, int offset) { - CHOP(((int) in1[x] + (int) in2[x]) / scale + offset, NULL); + CHOP(((int) in1[x] + (int) in2[x]) / scale + offset); } Imaging ImagingChopSubtract(Imaging imIn1, Imaging imIn2, float scale, int offset) { - CHOP(((int) in1[x] - (int) in2[x]) / scale + offset, NULL); + CHOP(((int) in1[x] - (int) in2[x]) / scale + offset); } Imaging @@ -146,3 +151,27 @@ ImagingChopSubtractModulo(Imaging imIn1, Imaging imIn2) { CHOP2(in1[x] - in2[x], NULL); } + +Imaging +ImagingChopSoftLight(Imaging imIn1, Imaging imIn2) +{ + CHOP2( (((255-in1[x]) * (in1[x]*in2[x]) ) / 65536) + + (in1[x] * ( 255 - ( (255 - in1[x]) * (255 - in2[x] ) / 255) )) / 255 + , NULL ); +} + +Imaging +ImagingChopHardLight(Imaging imIn1, Imaging imIn2) +{ + CHOP2( (in2[x]<128) ? ( (in1[x]*in2[x])/127) + : 255 - ( ((255-in2[x]) * (255-in1[x])) / 127) + , NULL); +} + +Imaging +ImagingOverlay(Imaging imIn1, Imaging imIn2) +{ + CHOP2( (in1[x]<128) ? ( (in1[x]*in2[x])/127) + : 255 - ( ((255-in1[x]) * (255-in2[x])) / 127) + , NULL); +} diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 60513c66d..b0b794d72 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -44,7 +44,8 @@ #define L(rgb)\ ((INT32) (rgb)[0]*299 + (INT32) (rgb)[1]*587 + (INT32) (rgb)[2]*114) #define L24(rgb)\ - ((rgb)[0]*19595 + (rgb)[1]*38470 + (rgb)[2]*7471) + ((rgb)[0]*19595 + (rgb)[1]*38470 + (rgb)[2]*7471 + 0x8000) + #ifndef round double round(double x) { @@ -122,8 +123,9 @@ static void l2bit(UINT8* out, const UINT8* in, int xsize) { int x; - for (x = 0; x < xsize; x++) + for (x = 0; x < xsize; x++) { *out++ = (*in++ >= 128) ? 255 : 0; + } } static void @@ -205,8 +207,9 @@ static void la2l(UINT8* out, const UINT8* in, int xsize) { int x; - for (x = 0; x < xsize; x++, in += 4) + for (x = 0; x < xsize; x++, in += 4) { *out++ = in[0]; + } } static void @@ -239,18 +242,20 @@ static void rgb2bit(UINT8* out, const UINT8* in, int xsize) { int x; - for (x = 0; x < xsize; x++, in += 4) + for (x = 0; x < xsize; x++, in += 4) { /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ *out++ = (L(in) >= 128000) ? 255 : 0; + } } static void rgb2l(UINT8* out, const UINT8* in, int xsize) { int x; - for (x = 0; x < xsize; x++, in += 4) + for (x = 0; x < xsize; x++, in += 4) { /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ *out++ = L24(in) >> 16; + } } static void @@ -652,12 +657,13 @@ i2l(UINT8* out, const UINT8* in_, int xsize) for (x = 0; x < xsize; x++, out++, in_ += 4) { INT32 v; memcpy(&v, in_, sizeof(v)); - if (v <= 0) + if (v <= 0) { *out = 0; - else if (v >= 255) + } else if (v >= 255) { *out = 255; - else + } else { *out = (UINT8) v; + } } } @@ -680,12 +686,13 @@ i2rgb(UINT8* out, const UINT8* in_, int xsize) int x; INT32* in = (INT32*) in_; for (x = 0; x < xsize; x++, in++, out+=4) { - if (*in <= 0) + if (*in <= 0) { out[0] = out[1] = out[2] = 0; - else if (*in >= 255) + } else if (*in >= 255) { out[0] = out[1] = out[2] = 255; - else + } else { out[0] = out[1] = out[2] = (UINT8) *in; + } out[3] = 255; } } @@ -740,12 +747,13 @@ f2l(UINT8* out, const UINT8* in_, int xsize) for (x = 0; x < xsize; x++, out++, in_ += 4) { FLOAT32 v; memcpy(&v, in_, sizeof(v)); - if (v <= 0.0) + if (v <= 0.0) { *out = 0; - else if (v >= 255.0) + } else if (v >= 255.0) { *out = 255; - else + } else { *out = (UINT8) v; + } } } @@ -796,8 +804,9 @@ static void ycbcr2l(UINT8* out, const UINT8* in, int xsize) { int x; - for (x = 0; x < xsize; x++, in += 4) + for (x = 0; x < xsize; x++, in += 4) { *out++ = in[0]; + } } static void @@ -907,22 +916,26 @@ static void I16L_L(UINT8* out, const UINT8* in, int xsize) { int x; - for (x = 0; x < xsize; x++, in += 2) - if (in[1] != 0) + for (x = 0; x < xsize; x++, in += 2) { + if (in[1] != 0) { *out++ = 255; - else + } else { *out++ = in[0]; + } + } } static void I16B_L(UINT8* out, const UINT8* in, int xsize) { int x; - for (x = 0; x < xsize; x++, in += 2) - if (in[0] != 0) + for (x = 0; x < xsize; x++, in += 2) { + if (in[0] != 0) { *out++ = 255; - else + } else { *out++ = in[1]; + } + } } static struct { @@ -1055,8 +1068,9 @@ p2bit(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { int x; /* FIXME: precalculate greyscale palette? */ - for (x = 0; x < xsize; x++) + for (x = 0; x < xsize; x++) { *out++ = (L(&palette[in[x]*4]) >= 128000) ? 255 : 0; + } } static void @@ -1064,8 +1078,9 @@ pa2bit(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { int x; /* FIXME: precalculate greyscale palette? */ - for (x = 0; x < xsize; x++, in += 4) + for (x = 0; x < xsize; x++, in += 4) { *out++ = (L(&palette[in[0]*4]) >= 128000) ? 255 : 0; + } } static void @@ -1073,8 +1088,9 @@ p2l(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { int x; /* FIXME: precalculate greyscale palette? */ - for (x = 0; x < xsize; x++) + for (x = 0; x < xsize; x++) { *out++ = L(&palette[in[x]*4]) / 1000; + } } static void @@ -1082,8 +1098,9 @@ pa2l(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { int x; /* FIXME: precalculate greyscale palette? */ - for (x = 0; x < xsize; x++, in += 4) + for (x = 0; x < xsize; x++, in += 4) { *out++ = L(&palette[in[0]*4]) / 1000; + } } static void @@ -1137,8 +1154,9 @@ pa2i(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) { int x; INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++, in += 4) + for (x = 0; x < xsize; x++, in += 4) { *out++ = L(&palette[in[0]*4]) / 1000; + } } static void @@ -1156,8 +1174,9 @@ pa2f(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) { int x; FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++, in += 4) + for (x = 0; x < xsize; x++, in += 4) { *out++ = (float) L(&palette[in[0]*4]) / 1000.0F; + } } static void @@ -1272,46 +1291,50 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) /* Map palette image to L, RGB, RGBA, or CMYK */ - if (!imIn->palette) + if (!imIn->palette) { return (Imaging) ImagingError_ValueError("no palette"); + } alpha = !strcmp(imIn->mode, "PA"); - if (strcmp(mode, "1") == 0) + if (strcmp(mode, "1") == 0) { convert = alpha ? pa2bit : p2bit; - else if (strcmp(mode, "L") == 0) + } else if (strcmp(mode, "L") == 0) { convert = alpha ? pa2l : p2l; - else if (strcmp(mode, "LA") == 0) + } else if (strcmp(mode, "LA") == 0) { convert = alpha ? pa2la : p2la; - else if (strcmp(mode, "PA") == 0) + } else if (strcmp(mode, "PA") == 0) { convert = p2pa; - else if (strcmp(mode, "I") == 0) + } else if (strcmp(mode, "I") == 0) { convert = alpha ? pa2i : p2i; - else if (strcmp(mode, "F") == 0) + } else if (strcmp(mode, "F") == 0) { convert = alpha ? pa2f : p2f; - else if (strcmp(mode, "RGB") == 0) + } else if (strcmp(mode, "RGB") == 0) { convert = alpha ? pa2rgb : p2rgb; - else if (strcmp(mode, "RGBA") == 0) + } else if (strcmp(mode, "RGBA") == 0) { convert = alpha ? pa2rgba : p2rgba; - else if (strcmp(mode, "RGBX") == 0) + } else if (strcmp(mode, "RGBX") == 0) { convert = alpha ? pa2rgba : p2rgba; - else if (strcmp(mode, "CMYK") == 0) + } else if (strcmp(mode, "CMYK") == 0) { convert = alpha ? pa2cmyk : p2cmyk; - else if (strcmp(mode, "YCbCr") == 0) + } else if (strcmp(mode, "YCbCr") == 0) { convert = alpha ? pa2ycbcr : p2ycbcr; - else if (strcmp(mode, "HSV") == 0) + } else if (strcmp(mode, "HSV") == 0) { convert = alpha ? pa2hsv : p2hsv; - else + } else { return (Imaging) ImagingError_ValueError("conversion not supported"); + } imOut = ImagingNew2Dirty(mode, imOut, imIn); - if (!imOut) + if (!imOut) { return NULL; + } ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) + for (y = 0; y < imIn->ysize; y++) { (*convert)((UINT8*) imOut->image[y], (UINT8*) imIn->image[y], imIn->xsize, imIn->palette->palette); + } ImagingSectionLeave(&cookie); return imOut; @@ -1329,26 +1352,30 @@ topalette(Imaging imOut, Imaging imIn, const char *mode, ImagingPalette inpalett ImagingPalette palette = inpalette;; /* Map L or RGB/RGBX/RGBA to palette image */ - if (strcmp(imIn->mode, "L") != 0 && strncmp(imIn->mode, "RGB", 3) != 0) + if (strcmp(imIn->mode, "L") != 0 && strncmp(imIn->mode, "RGB", 3) != 0) { return (Imaging) ImagingError_ValueError("conversion not supported"); + } alpha = !strcmp(mode, "PA"); if (palette == NULL) { /* FIXME: make user configurable */ - if (imIn->bands == 1) + if (imIn->bands == 1) { palette = ImagingPaletteNew("RGB"); /* Initialised to grey ramp */ - else + } else { palette = ImagingPaletteNewBrowser(); /* Standard colour cube */ + } } - if (!palette) + if (!palette) { return (Imaging) ImagingError_ValueError("no palette"); + } imOut = ImagingNew2Dirty(mode, imOut, imIn); if (!imOut) { - if (palette != inpalette) + if (palette != inpalette) { ImagingPaletteDelete(palette); + } return NULL; } @@ -1375,8 +1402,9 @@ topalette(Imaging imOut, Imaging imIn, const char *mode, ImagingPalette inpalett /* Create mapping cache */ if (ImagingPaletteCachePrepare(palette) < 0) { ImagingDelete(imOut); - if (palette != inpalette) + if (palette != inpalette) { ImagingPaletteDelete(palette); + } return NULL; } @@ -1414,8 +1442,9 @@ topalette(Imaging imOut, Imaging imIn, const char *mode, ImagingPalette inpalett /* get closest colour */ cache = &ImagingPaletteCache(palette, r, g, b); - if (cache[0] == 0x100) + if (cache[0] == 0x100) { ImagingPaletteCacheUpdate(palette, r, g, b); + } if (alpha) { out[x*4] = out[x*4+1] = out[x*4+2] = (UINT8) cache[0]; out[x*4+3] = 255; @@ -1463,8 +1492,9 @@ topalette(Imaging imOut, Imaging imIn, const char *mode, ImagingPalette inpalett /* get closest colour */ cache = &ImagingPaletteCache(palette, r, g, b); - if (cache[0] == 0x100) + if (cache[0] == 0x100) { ImagingPaletteCacheUpdate(palette, r, g, b); + } if (alpha) { out[x*4] = out[x*4+1] = out[x*4+2] = (UINT8) cache[0]; out[x*4+3] = 255; @@ -1476,12 +1506,14 @@ topalette(Imaging imOut, Imaging imIn, const char *mode, ImagingPalette inpalett ImagingSectionLeave(&cookie); } - if (inpalette != palette) + if (inpalette != palette) { ImagingPaletteCacheDelete(palette); + } } - if (inpalette != palette) + if (inpalette != palette) { ImagingPaletteDelete(palette); + } return imOut; } @@ -1494,12 +1526,14 @@ tobilevel(Imaging imOut, Imaging imIn, int dither) int* errors; /* Map L or RGB to dithered 1 image */ - if (strcmp(imIn->mode, "L") != 0 && strcmp(imIn->mode, "RGB") != 0) + if (strcmp(imIn->mode, "L") != 0 && strcmp(imIn->mode, "RGB") != 0) { return (Imaging) ImagingError_ValueError("conversion not supported"); + } imOut = ImagingNew2Dirty("1", imOut, imIn); - if (!imOut) + if (!imOut) { return NULL; + } errors = calloc(imIn->xsize + 1, sizeof(int)); if (!errors) { @@ -1581,63 +1615,72 @@ convert(Imaging imOut, Imaging imIn, const char *mode, ImagingShuffler convert; int y; - if (!imIn) + if (!imIn) { return (Imaging) ImagingError_ModeError(); + } if (!mode) { /* Map palette image to full depth */ - if (!imIn->palette) + if (!imIn->palette) { return (Imaging) ImagingError_ModeError(); + } mode = imIn->palette->mode; - } else + } else { /* Same mode? */ - if (!strcmp(imIn->mode, mode)) + if (!strcmp(imIn->mode, mode)) { return ImagingCopy2(imOut, imIn); + } + } /* test for special conversions */ - if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "PA") == 0) + if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "PA") == 0) { return frompalette(imOut, imIn, mode); + } - if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) + if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { return topalette(imOut, imIn, mode, palette, dither); + } - if (dither && strcmp(mode, "1") == 0) + if (dither && strcmp(mode, "1") == 0) { return tobilevel(imOut, imIn, dither); + } /* standard conversion machinery */ convert = NULL; - for (y = 0; converters[y].from; y++) + for (y = 0; converters[y].from; y++) { if (!strcmp(imIn->mode, converters[y].from) && !strcmp(mode, converters[y].to)) { convert = converters[y].convert; break; } + } - if (!convert) + if (!convert) { #ifdef notdef return (Imaging) ImagingError_ValueError("conversion not supported"); #else - { - static char buf[256]; - /* FIXME: may overflow if mode is too large */ - sprintf(buf, "conversion from %s to %s not supported", imIn->mode, mode); - return (Imaging) ImagingError_ValueError(buf); - } + static char buf[256]; + /* FIXME: may overflow if mode is too large */ + sprintf(buf, "conversion from %s to %s not supported", imIn->mode, mode); + return (Imaging) ImagingError_ValueError(buf); #endif + } imOut = ImagingNew2Dirty(mode, imOut, imIn); - if (!imOut) + if (!imOut) { return NULL; + } ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) + for (y = 0; y < imIn->ysize; y++) { (*convert)((UINT8*) imOut->image[y], (UINT8*) imIn->image[y], imIn->xsize); + } ImagingSectionLeave(&cookie); return imOut; @@ -1726,17 +1769,19 @@ ImagingConvertInPlace(Imaging imIn, const char* mode) int y; /* limited support for inplace conversion */ - if (strcmp(imIn->mode, "L") == 0 && strcmp(mode, "1") == 0) + if (strcmp(imIn->mode, "L") == 0 && strcmp(mode, "1") == 0) { convert = l2bit; - else if (strcmp(imIn->mode, "1") == 0 && strcmp(mode, "L") == 0) + } else if (strcmp(imIn->mode, "1") == 0 && strcmp(mode, "L") == 0) { convert = bit2l; - else + } else { return ImagingError_ModeError(); + } ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) + for (y = 0; y < imIn->ysize; y++) { (*convert)((UINT8*) imIn->image[y], (UINT8*) imIn->image[y], imIn->xsize); + } ImagingSectionLeave(&cookie); return imIn; diff --git a/src/libImaging/ConvertYCbCr.c b/src/libImaging/ConvertYCbCr.c index 6ce549111..47de4ff71 100644 --- a/src/libImaging/ConvertYCbCr.c +++ b/src/libImaging/ConvertYCbCr.c @@ -5,7 +5,7 @@ * code to convert YCbCr data * * history: - * 98-07-01 hk Created + * 98-07-01 hk Created * * Copyright (c) Secret Labs AB 1998 * diff --git a/src/libImaging/Copy.c b/src/libImaging/Copy.c index 1bc9b1a70..5b4899f39 100644 --- a/src/libImaging/Copy.c +++ b/src/libImaging/Copy.c @@ -25,21 +25,25 @@ _copy(Imaging imOut, Imaging imIn) ImagingSectionCookie cookie; int y; - if (!imIn) - return (Imaging) ImagingError_ValueError(NULL); + if (!imIn) { + return (Imaging) ImagingError_ValueError(NULL); + } imOut = ImagingNew2Dirty(imIn->mode, imOut, imIn); - if (!imOut) + if (!imOut) { return NULL; + } ImagingCopyPalette(imOut, imIn); ImagingSectionEnter(&cookie); - if (imIn->block != NULL && imOut->block != NULL) - memcpy(imOut->block, imIn->block, imIn->ysize * imIn->linesize); - else - for (y = 0; y < imIn->ysize; y++) + if (imIn->block != NULL && imOut->block != NULL) { + memcpy(imOut->block, imIn->block, imIn->ysize * imIn->linesize); + } else { + for (y = 0; y < imIn->ysize; y++) { memcpy(imOut->image[y], imIn->image[y], imIn->linesize); + } + } ImagingSectionLeave(&cookie); return imOut; diff --git a/src/libImaging/Crop.c b/src/libImaging/Crop.c index 4407c1b1d..d136edbfc 100644 --- a/src/libImaging/Crop.c +++ b/src/libImaging/Crop.c @@ -5,9 +5,9 @@ * cut region from image * * history: - * 95-11-27 fl Created - * 98-07-10 fl Fixed "null result" error - * 99-02-05 fl Rewritten to use Paste primitive + * 95-11-27 fl Created + * 98-07-10 fl Fixed "null result" error + * 99-02-05 fl Rewritten to use Paste primitive * * Copyright (c) Secret Labs AB 1997-99. * Copyright (c) Fredrik Lundh 1995. @@ -27,24 +27,29 @@ ImagingCrop(Imaging imIn, int sx0, int sy0, int sx1, int sy1) int dx0, dy0, dx1, dy1; INT32 zero = 0; - if (!imIn) - return (Imaging) ImagingError_ModeError(); + if (!imIn) { + return (Imaging) ImagingError_ModeError(); + } xsize = sx1 - sx0; - if (xsize < 0) + if (xsize < 0) { xsize = 0; + } ysize = sy1 - sy0; - if (ysize < 0) + if (ysize < 0) { ysize = 0; + } imOut = ImagingNewDirty(imIn->mode, xsize, ysize); - if (!imOut) - return NULL; + if (!imOut) { + return NULL; + } ImagingCopyPalette(imOut, imIn); - if (sx0 < 0 || sy0 < 0 || sx1 > imIn->xsize || sy1 > imIn->ysize) - (void) ImagingFill(imOut, &zero); + if (sx0 < 0 || sy0 < 0 || sx1 > imIn->xsize || sy1 > imIn->ysize) { + (void) ImagingFill(imOut, &zero); + } dx0 = -sx0; dy0 = -sy0; diff --git a/src/libImaging/Dib.c b/src/libImaging/Dib.c index 504290231..202b0c9fa 100644 --- a/src/libImaging/Dib.c +++ b/src/libImaging/Dib.c @@ -40,8 +40,9 @@ ImagingGetModeDIB(int size_out[2]) mode = "P"; if (!(GetDeviceCaps(dc, RASTERCAPS) & RC_PALETTE)) { mode = "RGB"; - if (GetDeviceCaps(dc, BITSPIXEL) == 1) + if (GetDeviceCaps(dc, BITSPIXEL) == 1) { mode = "1"; + } } if (size_out) { @@ -66,14 +67,16 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) /* Check mode */ if (strcmp(mode, "1") != 0 && strcmp(mode, "L") != 0 && - strcmp(mode, "RGB") != 0) + strcmp(mode, "RGB") != 0) { return (ImagingDIB) ImagingError_ModeError(); + } /* Create DIB context and info header */ /* malloc check ok, small constant allocation */ dib = (ImagingDIB) malloc(sizeof(*dib)); - if (!dib) + if (!dib) { return (ImagingDIB) ImagingError_MemoryError(); + } /* malloc check ok, small constant allocation */ dib->info = (BITMAPINFO*) malloc(sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)); @@ -113,9 +116,9 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) dib->pixelsize = strlen(mode); dib->linesize = (xsize * dib->pixelsize + 3) & -4; - if (dib->pixelsize == 1) + if (dib->pixelsize == 1) { dib->pack = dib->unpack = (ImagingShuffler) memcpy; - else { + } else { dib->pack = ImagingPackBGR; dib->unpack = ImagingPackBGR; } @@ -174,14 +177,16 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) * images. */ i = 10; - for (r = 0; r < 256; r += 51) - for (g = 0; g < 256; g += 51) + for (r = 0; r < 256; r += 51) { + for (g = 0; g < 256; g += 51) { for (b = 0; b < 256; b += 51) { pal->palPalEntry[i].peRed = r; pal->palPalEntry[i].peGreen = g; pal->palPalEntry[i].peBlue = b; i++; } + } + } for (r = 1; r < 22-1; r++) { /* Black and white are already provided by the cube. */ pal->palPalEntry[i].peRed = @@ -195,14 +200,16 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) /* Colour DIB. Alternate palette. */ i = 10; - for (r = 0; r < 256; r += 37) - for (g = 0; g < 256; g += 32) + for (r = 0; r < 256; r += 37) { + for (g = 0; g < 256; g += 32) { for (b = 0; b < 256; b += 64) { pal->palPalEntry[i].peRed = r; pal->palPalEntry[i].peGreen = g; pal->palPalEntry[i].peBlue = b; i++; } + } + } #endif @@ -223,9 +230,10 @@ ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]) /* FIXME: check size! */ int y; - for (y = 0; y < im->ysize; y++) + for (y = 0; y < im->ysize; y++) { dib->pack(dib->bits + dib->linesize*(dib->ysize-(xy[1]+y)-1) + xy[0]*dib->pixelsize, im->image[y], im->xsize); + } } @@ -234,8 +242,9 @@ ImagingExposeDIB(ImagingDIB dib, void *dc) { /* Copy bitmap to display */ - if (dib->palette != 0) + if (dib->palette != 0) { SelectPalette((HDC) dc, dib->palette, FALSE); + } BitBlt((HDC) dc, 0, 0, dib->xsize, dib->ysize, dib->dc, 0, 0, SRCCOPY); } @@ -251,8 +260,9 @@ ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]) dib->info, DIB_RGB_COLORS, SRCCOPY); } else { /* stretchblt (displays) */ - if (dib->palette != 0) + if (dib->palette != 0) { SelectPalette((HDC) dc, dib->palette, FALSE); + } StretchBlt((HDC) dc, dst[0], dst[1], dst[2]-dst[0], dst[3]-dst[1], dib->dc, src[0], src[1], src[2]-src[0], src[3]-src[1], SRCCOPY); @@ -275,8 +285,9 @@ ImagingQueryPaletteDIB(ImagingDIB dib, void *dc) /* Restore palette */ SelectPalette((HDC) dc, now, FALSE); - } else + } else { n = 0; + } return n; /* number of colours that was changed */ } @@ -286,14 +297,16 @@ ImagingDeleteDIB(ImagingDIB dib) { /* Clean up */ - if (dib->palette) + if (dib->palette) { DeleteObject(dib->palette); + } if (dib->bitmap) { SelectObject(dib->dc, dib->old_bitmap); DeleteObject(dib->bitmap); } - if (dib->dc) + if (dib->dc) { DeleteDC(dib->dc); + } free(dib->info); } diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index dee7c524d..a2b2b10f3 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -35,6 +35,7 @@ #include "Imaging.h" #include +#include #define CEIL(v) (int) ceil(v) #define FLOOR(v) ((v) >= 0.0 ? (int) (v) : (int) floor(v)) @@ -42,7 +43,7 @@ #define INK8(ink) (*(UINT8*)ink) /* - * Rounds around zero (up=away from zero, down=torwards zero) + * Rounds around zero (up=away from zero, down=towards zero) * This guarantees that ROUND_UP|DOWN(f) == -ROUND_UP|DOWN(-f) */ #define ROUND_UP(f) ((int) ((f) >= 0.0 ? floor((f) + 0.5F) : -floor(fabs(f) + 0.5F))) @@ -79,8 +80,9 @@ point8(Imaging im, int x, int y, int ink) static inline void point32(Imaging im, int x, int y, int ink) { - if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) + if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { im->image32[y][x] = ink; + } } static inline void @@ -103,16 +105,19 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) int tmp, pixelwidth; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) + if (x0 > x1) { tmp = x0, x0 = x1, x1 = tmp; - if (x0 < 0) + } + if (x0 < 0) { x0 = 0; - else if (x0 >= im->xsize) + } else if (x0 >= im->xsize) { return; - if (x1 < 0) + } + if (x1 < 0) { return; - else if (x1 >= im->xsize) + } else if (x1 >= im->xsize) { x1 = im->xsize-1; + } if (x0 <= x1) { pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1; memset(im->image8[y0] + x0 * pixelwidth, (UINT8) ink, @@ -128,19 +133,23 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) INT32* p; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) + if (x0 > x1) { tmp = x0, x0 = x1, x1 = tmp; - if (x0 < 0) + } + if (x0 < 0) { x0 = 0; - else if (x0 >= im->xsize) + } else if (x0 >= im->xsize) { return; - if (x1 < 0) + } + if (x1 < 0) { return; - else if (x1 >= im->xsize) + } else if (x1 >= im->xsize) { x1 = im->xsize-1; + } p = im->image32[y0]; - while (x0 <= x1) + while (x0 <= x1) { p[x0++] = ink; + } } } @@ -151,16 +160,19 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) unsigned int tmp1; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) + if (x0 > x1) { tmp = x0, x0 = x1, x1 = tmp; - if (x0 < 0) + } + if (x0 < 0) { x0 = 0; - else if (x0 >= im->xsize) + } else if (x0 >= im->xsize) { return; - if (x1 < 0) + } + if (x1 < 0) { return; - else if (x1 >= im->xsize) + } else if (x1 >= im->xsize) { x1 = im->xsize-1; + } if (x0 <= x1) { UINT8* out = (UINT8*) im->image[y0]+x0*4; UINT8* in = (UINT8*) &ink; @@ -183,19 +195,21 @@ line8(Imaging im, int x0, int y0, int x1, int y1, int ink) /* normalize coordinates */ dx = x1-x0; - if (dx < 0) + if (dx < 0) { dx = -dx, xs = -1; - else + } else { xs = 1; + } dy = y1-y0; - if (dy < 0) + if (dy < 0) { dy = -dy, ys = -1; - else + } else { ys = 1; + } n = (dx > dy) ? dx : dy; - if (dx == 0) + if (dx == 0) { /* vertical */ for (i = 0; i < dy; i++) { @@ -203,7 +217,7 @@ line8(Imaging im, int x0, int y0, int x1, int y1, int ink) y0 += ys; } - else if (dy == 0) + } else if (dy == 0) { /* horizontal */ for (i = 0; i < dx; i++) { @@ -211,7 +225,7 @@ line8(Imaging im, int x0, int y0, int x1, int y1, int ink) x0 += xs; } - else if (dx > dy) { + } else if (dx > dy) { /* bresenham, horizontal slope */ n = dx; @@ -259,19 +273,21 @@ line32(Imaging im, int x0, int y0, int x1, int y1, int ink) /* normalize coordinates */ dx = x1-x0; - if (dx < 0) + if (dx < 0) { dx = -dx, xs = -1; - else + } else { xs = 1; + } dy = y1-y0; - if (dy < 0) + if (dy < 0) { dy = -dy, ys = -1; - else + } else { ys = 1; + } n = (dx > dy) ? dx : dy; - if (dx == 0) + if (dx == 0) { /* vertical */ for (i = 0; i < dy; i++) { @@ -279,7 +295,7 @@ line32(Imaging im, int x0, int y0, int x1, int y1, int ink) y0 += ys; } - else if (dy == 0) + } else if (dy == 0) { /* horizontal */ for (i = 0; i < dx; i++) { @@ -287,7 +303,7 @@ line32(Imaging im, int x0, int y0, int x1, int y1, int ink) x0 += xs; } - else if (dx > dy) { + } else if (dx > dy) { /* bresenham, horizontal slope */ n = dx; @@ -335,19 +351,21 @@ line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink) /* normalize coordinates */ dx = x1-x0; - if (dx < 0) + if (dx < 0) { dx = -dx, xs = -1; - else + } else { xs = 1; + } dy = y1-y0; - if (dy < 0) + if (dy < 0) { dy = -dy, ys = -1; - else + } else { ys = 1; + } n = (dx > dy) ? dx : dy; - if (dx == 0) + if (dx == 0) { /* vertical */ for (i = 0; i < dy; i++) { @@ -355,7 +373,7 @@ line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink) y0 += ys; } - else if (dy == 0) + } else if (dy == 0) { /* horizontal */ for (i = 0; i < dx; i++) { @@ -363,7 +381,7 @@ line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink) x0 += xs; } - else if (dx > dy) { + } else if (dx > dy) { /* bresenham, horizontal slope */ n = dx; @@ -406,15 +424,45 @@ static int x_cmp(const void *x0, const void *x1) { float diff = *((float*)x0) - *((float*)x1); - if (diff < 0) + if (diff < 0) { return -1; - else if (diff > 0) + } else if (diff > 0) { return 1; - else + } else { return 0; + } } +static void +draw_horizontal_lines(Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline) +{ + int i; + for (i = 0; i < n; i++) { + if (e[i].ymin == y && e[i].ymin == e[i].ymax) { + int xmax; + int xmin = e[i].xmin; + if (*x_pos < xmin) { + // Line would be after the current position + continue; + } + + xmax = e[i].xmax; + if (*x_pos > xmin) { + // Line would be partway through x_pos, so increase the starting point + xmin = *x_pos; + if (xmax < xmin) { + // Line would now end before it started + continue; + } + } + + (*hline)(im, xmin, e[i].ymin, xmax, ink); + *x_pos = xmax+1; + } + } +} + /* * Filled polygon draw function using scan line algorithm. */ @@ -442,18 +490,15 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, } for (i = 0; i < n; i++) { - /* This causes the pixels of horizontal edges to be drawn twice :( - * but without it there are inconsistencies in ellipses */ - if (e[i].ymin == e[i].ymax) { - (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink); - continue; - } if (ymin > e[i].ymin) { ymin = e[i].ymin; } if (ymax < e[i].ymax) { ymax = e[i].ymax; } + if (e[i].ymin == e[i].ymax) { + continue; + } edge_table[edge_count++] = (e + i); } if (ymin < 0) { @@ -472,6 +517,7 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, } for (; ymin <= ymax; ymin++) { int j = 0; + int x_pos = 0; for (i = 0; i < edge_count; i++) { Edge* current = edge_table[i]; if (ymin >= current->ymin && ymin <= current->ymax) { @@ -485,8 +531,30 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, } qsort(xx, j, sizeof(float), x_cmp); for (i = 1; i < j; i += 2) { - (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink); + int x_end = ROUND_DOWN(xx[i]); + if (x_end < x_pos) { + // Line would be before the current position + continue; + } + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + if (x_end < x_pos) { + // Line would be before the current position + continue; + } + + int x_start = ROUND_UP(xx[i-1]); + if (x_pos > x_start) { + // Line would be partway through x_pos, so increase the starting point + x_start = x_pos; + if (x_end < x_start) { + // Line would now end before it started + continue; + } + } + (*hline)(im, x_start, ymin, x_end, ink); + x_pos = x_end+1; } + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); } free(xx); @@ -517,25 +585,28 @@ add_edge(Edge *e, int x0, int y0, int x1, int y1) { /* printf("edge %d %d %d %d\n", x0, y0, x1, y1); */ - if (x0 <= x1) + if (x0 <= x1) { e->xmin = x0, e->xmax = x1; - else + } else { e->xmin = x1, e->xmax = x0; + } - if (y0 <= y1) + if (y0 <= y1) { e->ymin = y0, e->ymax = y1; - else + } else { e->ymin = y1, e->ymax = y0; + } if (y0 == y1) { e->d = 0; e->dx = 0.0; } else { e->dx = ((float)(x1-x0)) / (y1-y0); - if (y0 == e->ymin) + if (y0 == e->ymin) { e->d = 1; - else + } else { e->d = -1; + } } e->x0 = x0; @@ -613,7 +684,7 @@ ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1, return 0; } - big_hypotenuse = sqrt((double) (dx*dx + dy*dy)); + big_hypotenuse = hypot(dx, dy); small_hypotenuse = (width - 1) / 2.0; ratio_max = ROUND_UP(small_hypotenuse) / big_hypotenuse; ratio_min = ROUND_DOWN(small_hypotenuse) / big_hypotenuse; @@ -652,23 +723,27 @@ ImagingDrawRectangle(Imaging im, int x0, int y0, int x1, int y1, DRAWINIT(); - if (y0 > y1) + if (y0 > y1) { tmp = y0, y0 = y1, y1 = tmp; + } if (fill) { - if (y0 < 0) + if (y0 < 0) { y0 = 0; - else if (y0 >= im->ysize) + } else if (y0 >= im->ysize) { return 0; + } - if (y1 < 0) + if (y1 < 0) { return 0; - else if (y1 > im->ysize) + } else if (y1 > im->ysize) { y1 = im->ysize; + } - for (y = y0; y <= y1; y++) + for (y = y0; y <= y1; y++) { draw->hline(im, x0, y, x1, ink); + } } else { /* outline */ @@ -694,8 +769,9 @@ ImagingDrawPolygon(Imaging im, int count, int* xy, const void* ink_, DRAW* draw; INT32 ink; - if (count <= 0) + if (count <= 0) { return 0; + } DRAWINIT(); @@ -708,18 +784,21 @@ ImagingDrawPolygon(Imaging im, int count, int* xy, const void* ink_, (void) ImagingError_MemoryError(); return -1; } - for (i = n = 0; i < count-1; i++) + for (i = n = 0; i < count-1; i++) { add_edge(&e[n++], xy[i+i], xy[i+i+1], xy[i+i+2], xy[i+i+3]); - if (xy[i+i] != xy[0] || xy[i+i+1] != xy[1]) + } + if (xy[i+i] != xy[0] || xy[i+i+1] != xy[1]) { add_edge(&e[n++], xy[i+i], xy[i+i+1], xy[0], xy[1]); + } draw->polygon(im, n, e, ink, 0); free(e); } else { /* Outline */ - for (i = 0; i < count-1; i++) + for (i = 0; i < count-1; i++) { draw->line(im, xy[i+i], xy[i+i+1], xy[i+i+2], xy[i+i+3], ink); + } draw->line(im, xy[i+i], xy[i+i+1], xy[0], xy[1], ink); } @@ -740,216 +819,751 @@ ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void* ink, /* -------------------------------------------------------------------- */ /* standard shapes */ -#define ARC 0 -#define CHORD 1 -#define PIESLICE 2 +// Imagine 2D plane and ellipse with center in (0, 0) and semi-major axes a and b. +// Then quarter_* stuff approximates its top right quarter (x, y >= 0) with integer +// points from set {(2x+x0, 2y+y0) | x,y in Z} where x0, y0 are from {0, 1} and +// are such that point (a, b) is in the set. -static void -ellipsePoint(int cx, int cy, int w, int h, - float i, int *x, int *y) -{ - float i_cos, i_sin; - float x_f, y_f; - double modf_int; - i_cos = cos(i*M_PI/180); - i_sin = sin(i*M_PI/180); - x_f = (i_cos * w/2) + cx; - y_f = (i_sin * h/2) + cy; - if (modf(x_f, &modf_int) == 0.5) { - *x = i_cos > 0 ? FLOOR(x_f) : CEIL(x_f); +typedef struct { + int32_t a, b, cx, cy, ex, ey; + int64_t a2, b2, a2b2; + int8_t finished; +} quarter_state; + +void quarter_init(quarter_state* s, int32_t a, int32_t b) { + if (a < 0 || b < 0) { + s->finished = 1; } else { - *x = FLOOR(x_f + 0.5); - } - if (modf(y_f, &modf_int) == 0.5) { - *y = i_sin > 0 ? FLOOR(y_f) : CEIL(y_f); - } else { - *y = FLOOR(y_f + 0.5); + s->a = a; + s->b = b; + s->cx = a; + s->cy = b % 2; + s->ex = a % 2; + s->ey = b; + s->a2 = a * a; + s->b2 = b * b; + s->a2b2 = s->a2 * s->b2; + s->finished = 0; } } -static int -ellipse(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink_, int fill, - int width, int mode, int op) -{ - float i; - int inner; - int n; - int maxEdgeCount; - int w, h; - int x, y; - int cx, cy; - int lx = 0, ly = 0; - int sx = 0, sy = 0; - int lx_inner = 0, ly_inner = 0; - int sx_inner = 0, sy_inner = 0; - DRAW* draw; - INT32 ink; - Edge* e; +// deviation of the point from ellipse curve, basically a substitution +// of the point into the ellipse equation +int64_t quarter_delta(quarter_state* s, int64_t x, int64_t y) { + return llabs(s->a2 * y * y + s->b2 * x * x - s->a2b2); +} - DRAWINIT(); - - while (end < start) - end += 360; - - if (end - start > 360) { - // no need to go in loops - end = start + 361; +int8_t quarter_next(quarter_state* s, int32_t* ret_x, int32_t* ret_y) { + if (s->finished) { + return -1; } - - w = x1 - x0; - h = y1 - y0; - if (w <= 0 || h <= 0) - return 0; - - cx = (x0 + x1) / 2; - cy = (y0 + y1) / 2; - - if (!fill && width <= 1) { - for (i = start; i < end+1; i++) { - if (i > end) { - i = end; - } - ellipsePoint(cx, cy, w, h, i, &x, &y); - if (i != start) - draw->line(im, lx, ly, x, y, ink); - else - sx = x, sy = y; - lx = x, ly = y; - } - - if (i != start) { - if (mode == PIESLICE) { - if (x != cx || y != cy) { - draw->line(im, x, y, cx, cy, ink); - draw->line(im, cx, cy, sx, sy, ink); - } - } else if (mode == CHORD) { - if (x != sx || y != sy) - draw->line(im, x, y, sx, sy, ink); - } - } + *ret_x = s->cx; + *ret_y = s->cy; + if (s->cx == s->ex && s->cy == s->ey) { + s->finished = 1; } else { - inner = (mode == ARC || !fill) ? 1 : 0; - - // Build edge list - // malloc check UNDONE, FLOAT? - maxEdgeCount = ceil(end - start); - if (inner) { - maxEdgeCount *= 2; - } - maxEdgeCount += 3; - e = calloc(maxEdgeCount, sizeof(Edge)); - if (!e) { - ImagingError_MemoryError(); - return -1; - } - - // Outer circle - n = 0; - for (i = start; i < end+1; i++) { - if (i > end) { - i = end; + // Bresenham's algorithm, possible optimization: only consider 2 of 3 + // next points depending on current slope + int32_t nx = s->cx; + int32_t ny = s->cy + 2; + int64_t ndelta = quarter_delta(s, nx, ny); + if (nx > 1) { + int64_t newdelta = quarter_delta(s, s->cx - 2, s->cy + 2); + if (ndelta > newdelta) { + nx = s->cx - 2; + ny = s->cy + 2; + ndelta = newdelta; } - ellipsePoint(cx, cy, w, h, i, &x, &y); - if (i == start) { - sx = x, sy = y; - } else { - add_edge(&e[n++], lx, ly, x, y); - } - lx = x, ly = y; - } - if (n == 0) - return 0; - - if (inner) { - // Inner circle - x0 += width - 1; - y0 += width - 1; - x1 -= width - 1; - y1 -= width - 1; - - w = x1 - x0; - h = y1 - y0; - if (w <= 0 || h <= 0) { - // ARC with no gap in the middle is a PIESLICE - mode = PIESLICE; - inner = 0; - } else { - for (i = start; i < end+1; i++) { - if (i > end) { - i = end; - } - ellipsePoint(cx, cy, w, h, i, &x, &y); - if (i == start) - sx_inner = x, sy_inner = y; - else - add_edge(&e[n++], lx_inner, ly_inner, x, y); - lx_inner = x, ly_inner = y; - } + newdelta = quarter_delta(s, s->cx - 2, s->cy); + if (ndelta > newdelta) { + nx = s->cx - 2; + ny = s->cy; } } - - if (end - start < 360) { - // Close polygon - if (mode == PIESLICE) { - if (x != cx || y != cy) { - add_edge(&e[n++], sx, sy, cx, cy); - add_edge(&e[n++], cx, cy, lx, ly); - if (inner) { - ImagingDrawWideLine(im, sx, sy, cx, cy, &ink, width, op); - ImagingDrawWideLine(im, cx, cy, lx, ly, &ink, width, op); - } - } - } else if (mode == CHORD) { - add_edge(&e[n++], sx, sy, lx, ly); - if (inner) { - add_edge(&e[n++], sx_inner, sy_inner, lx_inner, ly_inner); - } - } else if (mode == ARC) { - add_edge(&e[n++], sx, sy, sx_inner, sy_inner); - add_edge(&e[n++], lx, ly, lx_inner, ly_inner); - } - } - - draw->polygon(im, n, e, ink, 0); - - free(e); + s->cx = nx; + s->cy = ny; } - return 0; } -int -ImagingDrawArc(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink, int width, int op) -{ - return ellipse(im, x0, y0, x1, y1, start, end, ink, 0, width, ARC, op); +// quarter_* stuff can "draw" a quarter of an ellipse with thickness 1, great. +// Now we use ellipse_* stuff to join all four quarters of two different sized +// ellipses and receive horizontal segments of a complete ellipse with +// specified thickness. +// +// Still using integer grid with step 2 at this point (like in quarter_*) +// to ease angle clipping in future. + +typedef struct { + quarter_state st_o, st_i; + int32_t py, pl, pr; + int32_t cy[4], cl[4], cr[4]; + int8_t bufcnt; + int8_t finished; + int8_t leftmost; +} ellipse_state; + +void ellipse_init(ellipse_state* s, int32_t a, int32_t b, int32_t w) { + s->bufcnt = 0; + s->leftmost = a % 2; + quarter_init(&s->st_o, a, b); + if (w < 1 || quarter_next(&s->st_o, &s->pr, &s->py) == -1) { + s->finished = 1; + } else { + s->finished = 0; + quarter_init(&s->st_i, a - 2 * (w - 1), b - 2 * (w - 1)); + s->pl = s->leftmost; + } } -int -ImagingDrawChord(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink, int fill, - int width, int op) +int8_t ellipse_next(ellipse_state* s, int32_t* ret_x0, int32_t* ret_y, int32_t* ret_x1) { + if (s->bufcnt == 0) { + if (s->finished) { + return -1; + } + int32_t y = s->py; + int32_t l = s->pl; + int32_t r = s->pr; + int32_t cx = 0, cy = 0; + int8_t next_ret; + while ((next_ret = quarter_next(&s->st_o, &cx, &cy)) != -1 && cy <= y) { + } + if (next_ret == -1) { + s->finished = 1; + } else { + s->pr = cx; + s->py = cy; + } + while ((next_ret = quarter_next(&s->st_i, &cx, &cy)) != -1 && cy <= y) { + l = cx; + } + s->pl = next_ret == -1 ? s->leftmost : cx; + + if ((l > 0 || l < r) && y > 0) { + s->cl[s->bufcnt] = l == 0 ? 2 : l; + s->cy[s->bufcnt] = y; + s->cr[s->bufcnt] = r; + ++s->bufcnt; + } + if (y > 0) { + s->cl[s->bufcnt] = -r; + s->cy[s->bufcnt] = y; + s->cr[s->bufcnt] = -l; + ++s->bufcnt; + } + if (l > 0 || l < r) { + s->cl[s->bufcnt] = l == 0 ? 2 : l; + s->cy[s->bufcnt] = -y; + s->cr[s->bufcnt] = r; + ++s->bufcnt; + } + s->cl[s->bufcnt] = -r; + s->cy[s->bufcnt] = -y; + s->cr[s->bufcnt] = -l; + ++s->bufcnt; + } + --s->bufcnt; + *ret_x0 = s->cl[s->bufcnt]; + *ret_y = s->cy[s->bufcnt]; + *ret_x1 = s->cr[s->bufcnt]; + return 0; +} + +// Clipping tree consists of half-plane clipping nodes and combining nodes. +// We can throw a horizontal segment in such a tree and collect an ordered set +// of resulting disjoint clipped segments organized into a sorted linked list +// of their end points. +typedef enum { + CT_AND, // intersection + CT_OR, // union + CT_CLIP // half-plane clipping +} clip_type; + +typedef struct clip_node { + clip_type type; + double a, b, c; // half-plane coeffs, only used in clipping nodes + struct clip_node* l; // child pointers, are only non-NULL in combining nodes + struct clip_node* r; +} clip_node; + +// Linked list for the ends of the clipped horizontal segments. +// Since the segment is always horizontal, we don't need to store Y coordinate. +typedef struct event_list { + int32_t x; + int8_t type; // used internally, 1 for the left end (smaller X), -1 for the + // right end; pointless in output since the output segments + // are disjoint, therefore the types would always come in pairs + // and interchange (1 -1 1 -1 ...) + struct event_list* next; +} event_list; + +// Mirrors all the clipping nodes of the tree relative to the y = x line. +void clip_tree_transpose(clip_node* root) { + if (root != NULL) { + if (root->type == CT_CLIP) { + double t = root->a; + root->a = root->b; + root->b = t; + } + clip_tree_transpose(root->l); + clip_tree_transpose(root->r); + } +} + +// Outputs a sequence of open-close events (types -1 and 1) for +// non-intersecting segments sorted by X coordinate. +// Combining nodes (AND, OR) may also accept sequences for intersecting +// segments, i.e. something like correct bracket sequences. +int clip_tree_do_clip(clip_node* root, int32_t x0, int32_t y, int32_t x1, event_list** ret) { + if (root == NULL) { + event_list* start = malloc(sizeof(event_list)); + if (!start) { + ImagingError_MemoryError(); + return -1; + } + event_list* end = malloc(sizeof(event_list)); + if (!end) { + free(start); + ImagingError_MemoryError(); + return -1; + } + start->x = x0; + start->type = 1; + start->next = end; + end->x = x1; + end->type = -1; + end->next = NULL; + *ret = start; + return 0; + } + if (root->type == CT_CLIP) { + double eps = 1e-9; + double A = root->a; + double B = root->b; + double C = root->c; + if (fabs(A) < eps) { + if (B * y + C < -eps) { + x0 = 1; + x1 = 0; + } + } else { + // X of intersection + double ix = - (B * y + C) / A; + if (A * x0 + B * y + C < eps) { + x0 = lround(fmax(x0, ix)); + } + if (A * x1 + B * y + C < eps) { + x1 = lround(fmin(x1, ix)); + } + } + if (x0 <= x1) { + event_list* start = malloc(sizeof(event_list)); + if (!start) { + ImagingError_MemoryError(); + return -1; + } + event_list* end = malloc(sizeof(event_list)); + if (!end) { + free(start); + ImagingError_MemoryError(); + return -1; + } + start->x = x0; + start->type = 1; + start->next = end; + end->x = x1; + end->type = -1; + end->next = NULL; + *ret = start; + } else { + *ret = NULL; + } + return 0; + } + if (root->type == CT_OR || root->type == CT_AND) { + event_list* l1; + event_list* l2; + if (clip_tree_do_clip(root->l, x0, y, x1, &l1) < 0) { + return -1; + } + if (clip_tree_do_clip(root->r, x0, y, x1, &l2) < 0) { + while (l1) { + l2 = l1->next; + free(l1); + l1 = l2; + } + return -1; + } + *ret = NULL; + event_list* tail = NULL; + int32_t k1 = 0; + int32_t k2 = 0; + while (l1 != NULL || l2 != NULL) { + event_list* t; + if (l2 == NULL || (l1 != NULL && (l1->x < l2->x || (l1->x == l2->x && l1->type > l2->type)))) { + t = l1; + k1 += t->type; + assert(k1 >= 0); + l1 = l1->next; + } else { + t = l2; + k2 += t->type; + assert(k2 >= 0); + l2 = l2->next; + } + t->next = NULL; + if ((root->type == CT_OR && ( + (t->type == 1 && (tail == NULL || tail->type == -1)) || + (t->type == -1 && k1 == 0 && k2 == 0) + )) || + (root->type == CT_AND && ( + (t->type == 1 && (tail == NULL || tail->type == -1) && k1 > 0 && k2 > 0) || + (t->type == -1 && tail != NULL && tail->type == 1 && (k1 == 0 || k2 == 0)) + ))) { + if (tail == NULL) { + *ret = t; + } else { + tail->next = t; + } + tail = t; + } else { + free(t); + } + } + return 0; + } + *ret = NULL; + return 0; +} + +// One more layer of processing on top of the regular ellipse. +// Uses the clipping tree. +// Used for producing ellipse derivatives such as arc, chord, pie, etc. +typedef struct { + ellipse_state st; + clip_node* root; + clip_node nodes[7]; + int32_t node_count; + event_list* head; + int32_t y; +} clip_ellipse_state; + +typedef void (*clip_ellipse_init)(clip_ellipse_state*, int32_t, int32_t, int32_t, float, float); + +void debug_clip_tree(clip_node* root, int space) { + if (root == NULL) { + return; + } + if (root->type == CT_CLIP) { + int t = space; + while (t--) { + fputc(' ', stderr); + } + fprintf(stderr, "clip %+fx%+fy%+f > 0\n", root->a, root->b, root->c); + } else { + debug_clip_tree(root->l, space + 2); + int t = space; + while (t--) { + fputc(' ', stderr); + } + fprintf(stderr, "%s\n", root->type == CT_AND ? "and" : "or"); + debug_clip_tree(root->r, space + 2); + } + if (space == 0) { + fputc('\n', stderr); + } +} + +// Resulting angles will satisfy 0 <= al < 360, al <= ar <= al + 360 +void normalize_angles(float* al, float* ar) { + if (*ar - *al >= 360) { + *al = 0; + *ar = 360; + } else { + *al = fmod(*al < 0 ? 360 - (fmod(-*al, 360)) : *al, 360); + *ar = *al + fmod(*ar < *al ? 360 - fmod(*al - *ar, 360) : *ar - *al, 360); + } +} + +// An arc with caps orthogonal to the ellipse curve. +void arc_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float ar) { + if (a < b) { + // transpose the coordinate system + arc_init(s, b, a, w, 90 - ar, 90 - al); + ellipse_init(&s->st, a, b, w); + clip_tree_transpose(s->root); + } else { + // a >= b, based on "wide" ellipse + ellipse_init(&s->st, a, b, w); + + s->head = NULL; + s->node_count = 0; + normalize_angles(&al, &ar); + + // building clipping tree, a lot of different cases + if (ar == al + 360) { + s->root = NULL; + } else { + clip_node* lc = s->nodes + s->node_count++; + clip_node* rc = s->nodes + s->node_count++; + lc->l = lc->r = rc->l = rc->r = NULL; + lc->type = rc->type = CT_CLIP; + lc->a = -a * sin(al * M_PI / 180.0); + lc->b = b * cos(al * M_PI / 180.0); + lc->c = (a * a - b * b) * sin(al * M_PI / 90.0) / 2.0; + rc->a = a * sin(ar * M_PI / 180.0); + rc->b = -b * cos(ar * M_PI / 180.0); + rc->c = (b * b - a * a) * sin(ar * M_PI / 90.0) / 2.0; + if (fmod(al, 180) == 0 || fmod(ar, 180) == 0) { + s->root = s->nodes + s->node_count++; + s->root->l = lc; + s->root->r = rc; + s->root->type = ar - al < 180 ? CT_AND : CT_OR; + } else if (((int)(al / 180) + (int)(ar / 180)) % 2 == 1) { + s->root = s->nodes + s->node_count++; + s->root->l = s->nodes + s->node_count++; + s->root->l->l = s->nodes + s->node_count++; + s->root->l->r = lc; + s->root->r = s->nodes + s->node_count++; + s->root->r->l = s->nodes + s->node_count++; + s->root->r->r = rc; + s->root->type = CT_OR; + s->root->l->type = CT_AND; + s->root->r->type = CT_AND; + s->root->l->l->type = CT_CLIP; + s->root->r->l->type = CT_CLIP; + s->root->l->l->l = s->root->l->l->r = NULL; + s->root->r->l->l = s->root->r->l->r = NULL; + s->root->l->l->a = s->root->l->l->c = 0; + s->root->r->l->a = s->root->r->l->c = 0; + s->root->l->l->b = (int)(al / 180) % 2 == 0 ? 1 : -1; + s->root->r->l->b = (int)(ar / 180) % 2 == 0 ? 1 : -1; + } else { + s->root = s->nodes + s->node_count++; + s->root->l = s->nodes + s->node_count++; + s->root->r = s->nodes + s->node_count++; + s->root->type = s->root->l->type = ar - al < 180 ? CT_AND : CT_OR; + s->root->l->l = lc; + s->root->l->r = rc; + s->root->r->type = CT_CLIP; + s->root->r->l = s->root->r->r = NULL; + s->root->r->a = s->root->r->c = 0; + s->root->r->b = ar < 180 || ar > 540 ? 1 : -1; + } + } + } +} + +// A chord line. +void chord_line_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float ar) { + ellipse_init(&s->st, a, b, a + b + 1); + + s->head = NULL; + s->node_count = 0; + + // line equation for chord + double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); + s->root = s->nodes + s->node_count++; + s->root->l = s->nodes + s->node_count++; + s->root->r = s->nodes + s->node_count++; + s->root->type = CT_AND; + s->root->l->type = s->root->r->type = CT_CLIP; + s->root->l->l = s->root->l->r = s->root->r->l = s->root->r->r = NULL; + s->root->l->a = yr - yl; + s->root->l->b = xl - xr; + s->root->l->c = -(s->root->l->a * xl + s->root->l->b * yl); + s->root->r->a = -s->root->l->a; + s->root->r->b = -s->root->l->b; + s->root->r->c = 2 * w * sqrt(pow(s->root->l->a, 2.0) + pow(s->root->l->b, 2.0)) - s->root->l->c; +} + +// Pie side. +void pie_side_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float _) { + ellipse_init(&s->st, a, b, a + b + 1); + + s->head = NULL; + s->node_count = 0; + + double xl = a * cos(al * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0); + double a1 = -yl; + double b1 = xl; + double c1 = w * sqrt(a1 * a1 + b1 * b1); + + s->root = s->nodes + s->node_count++; + s->root->type = CT_AND; + s->root->l = s->nodes + s->node_count++; + s->root->l->type = CT_AND; + + clip_node* cnode; + cnode = s->nodes + s->node_count++; + cnode->l = cnode->r = NULL; + cnode->type = CT_CLIP; + cnode->a = a1; + cnode->b = b1; + cnode->c = c1; + s->root->l->l = cnode; + cnode = s->nodes + s->node_count++; + cnode->l = cnode->r = NULL; + cnode->type = CT_CLIP; + cnode->a = -a1; + cnode->b = -b1; + cnode->c = c1; + s->root->l->r = cnode; + cnode = s->nodes + s->node_count++; + cnode->l = cnode->r = NULL; + cnode->type = CT_CLIP; + cnode->a = b1; + cnode->b = -a1; + cnode->c = 0; + s->root->r = cnode; +} + +// A chord. +void chord_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float ar) { + ellipse_init(&s->st, a, b, w); + + s->head = NULL; + s->node_count = 0; + + // line equation for chord + double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); + s->root = s->nodes + s->node_count++; + s->root->l = s->root->r = NULL; + s->root->type = CT_CLIP; + s->root->a = yr - yl; + s->root->b = xl - xr; + s->root->c = -(s->root->a * xl + s->root->b * yl); +} + +// A pie. Can also be used to draw an arc with ugly sharp caps. +void pie_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float ar) { + ellipse_init(&s->st, a, b, w); + + s->head = NULL; + s->node_count = 0; + + // line equations for pie sides + double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); + + clip_node* lc = s->nodes + s->node_count++; + clip_node* rc = s->nodes + s->node_count++; + lc->l = lc->r = rc->l = rc->r = NULL; + lc->type = rc->type = CT_CLIP; + lc->a = -yl; + lc->b = xl; + lc->c = 0; + rc->a = yr; + rc->b = -xr; + rc->c = 0; + + s->root = s->nodes + s->node_count++; + s->root->l = lc; + s->root->r = rc; + s->root->type = ar - al < 180 ? CT_AND : CT_OR; +} + +void clip_ellipse_free(clip_ellipse_state* s) { + while (s->head != NULL) { + event_list* t = s->head; + s->head = s->head->next; + free(t); + } +} + +int8_t clip_ellipse_next(clip_ellipse_state* s, int32_t* ret_x0, int32_t* ret_y, int32_t* ret_x1) { + int32_t x0, y, x1; + while (s->head == NULL && ellipse_next(&s->st, &x0, &y, &x1) >= 0) { + if (clip_tree_do_clip(s->root, x0, y, x1, &s->head) < 0) { + return -2; + } + s->y = y; + } + if (s->head != NULL) { + *ret_y = s->y; + event_list* t = s->head; + s->head = s->head->next; + *ret_x0 = t->x; + free(t); + t = s->head; + assert(t != NULL); + s->head = s->head->next; + *ret_x1 = t->x; + free(t); + return 0; + } + return -1; +} + +static int +ellipseNew(Imaging im, int x0, int y0, int x1, int y1, + const void* ink_, int fill, + int width, int op) { - return ellipse(im, x0, y0, x1, y1, start, end, ink, fill, width, CHORD, op); + DRAW* draw; + INT32 ink; + DRAWINIT(); + + int a = x1 - x0; + int b = y1 - y0; + if (a < 0 || b < 0) { + return 0; + } + if (fill) { + width = a + b; + } + + ellipse_state st; + ellipse_init(&st, a, b, width); + int32_t X0, Y, X1; + while (ellipse_next(&st, &X0, &Y, &X1) != -1) { + draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + } + return 0; +} + +static int +clipEllipseNew(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, + const void* ink_, int width, int op, clip_ellipse_init init) +{ + DRAW* draw; + INT32 ink; + DRAWINIT(); + + int a = x1 - x0; + int b = y1 - y0; + if (a < 0 || b < 0) { + return 0; + } + + clip_ellipse_state st; + init(&st, a, b, width, start, end); + // debug_clip_tree(st.root, 0); + int32_t X0, Y, X1; + int next_code; + while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) { + draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + } + clip_ellipse_free(&st); + return next_code == -1 ? 0 : -1; +} +static int +arcNew(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, + const void* ink_, int width, int op) +{ + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, arc_init); +} + +static int +chordNew(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, + const void* ink_, int width, int op) +{ + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, chord_init); +} + +static int +chordLineNew(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, + const void* ink_, int width, int op) +{ + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, chord_line_init); +} + +static int +pieNew(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, + const void* ink_, int width, int op) +{ + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, pie_init); +} + +static int +pieSideNew(Imaging im, int x0, int y0, int x1, int y1, + float start, + const void* ink_, int width, int op) +{ + return clipEllipseNew(im, x0, y0, x1, y1, start, 0, ink_, width, op, pie_side_init); } int ImagingDrawEllipse(Imaging im, int x0, int y0, int x1, int y1, const void* ink, int fill, int width, int op) { - return ellipse(im, x0, y0, x1, y1, 0, 360, ink, fill, width, CHORD, op); + return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); } +int +ImagingDrawArc(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, const void* ink, int width, int op) +{ + normalize_angles(&start, &end); + if (start + 360 == end) { + return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, 0, width, op); + } + if (start == end) { + return 0; + } + return arcNew(im, x0, y0, x1, y1, start, end, ink, width, op); +} + + +int +ImagingDrawChord(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, const void* ink, int fill, + int width, int op) +{ + normalize_angles(&start, &end); + if (start + 360 == end) { + return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, fill, width, op); + } + if (start == end) { + return 0; + } + if (fill) { + return chordNew(im, x0, y0, x1, y1, start, end, ink, x1 - x0 + y1 - y0 + 1, op); + } else { + if (chordLineNew(im, x0, y0, x1, y1, start, end, ink, width, op)) { + return -1; + } + return chordNew(im, x0, y0, x1, y1, start, end, ink, width, op); + } +} + + int ImagingDrawPieslice(Imaging im, int x0, int y0, int x1, int y1, float start, float end, const void* ink, int fill, int width, int op) { - return ellipse(im, x0, y0, x1, y1, start, end, ink, fill, width, PIESLICE, op); + normalize_angles(&start, &end); + if (start + 360 == end) { + return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); + } + if (start == end) { + return 0; + } + if (fill) { + return pieNew(im, x0, y0, x1, y1, start, end, ink, x1 + y1 - x0 - y0, op); + } else { + if (pieSideNew(im, x0, y0, x1, y1, start, ink, width, op)) { + return -1; + } + if (pieSideNew(im, x0, y0, x1, y1, end, ink, width, op)) { + return -1; + } + int xc = lround((x0 + x1 - width) / 2.0), yc = lround((y0 + y1 - width) / 2.0); + ellipseNew(im, xc, yc, xc + width - 1, yc + width - 1, ink, 1, 0, op); + return pieNew(im, x0, y0, x1, y1, start, end, ink, width, op); + } } + /* -------------------------------------------------------------------- */ /* experimental level 2 ("arrow") graphics stuff. this implements @@ -977,8 +1591,9 @@ ImagingOutlineNew(void) ImagingOutline outline; outline = calloc(1, sizeof(struct ImagingOutlineInstance)); - if (!outline) + if (!outline) { return (ImagingOutline) ImagingError_MemoryError(); + } outline->edges = NULL; outline->count = outline->size = 0; @@ -991,11 +1606,13 @@ ImagingOutlineNew(void) void ImagingOutlineDelete(ImagingOutline outline) { - if (!outline) + if (!outline) { return; + } - if (outline->edges) + if (outline->edges) { free(outline->edges); + } free(outline); } @@ -1019,8 +1636,9 @@ allocate(ImagingOutline outline, int extra) /* malloc check ok, overflow checked above */ e = realloc(outline->edges, outline->size * sizeof(Edge)); } - if (!e) + if (!e) { return NULL; + } outline->edges = e; } @@ -1046,8 +1664,9 @@ ImagingOutlineLine(ImagingOutline outline, float x1, float y1) Edge* e; e = allocate(outline, 1); - if (!e) + if (!e) { return -1; /* out of memory */ + } add_edge(e, (int) outline->x, (int) outline->y, (int) x1, (int) y1); @@ -1068,8 +1687,9 @@ ImagingOutlineCurve(ImagingOutline outline, float x1, float y1, #define STEPS 32 e = allocate(outline, STEPS); - if (!e) + if (!e) { return -1; /* out of memory */ + } xo = outline->x; yo = outline->y; @@ -1104,8 +1724,9 @@ ImagingOutlineCurve(ImagingOutline outline, float x1, float y1, int ImagingOutlineClose(ImagingOutline outline) { - if (outline->x == outline->x0 && outline->y == outline->y0) + if (outline->x == outline->x0 && outline->y == outline->y0) { return 0; + } return ImagingOutlineLine(outline, outline->x0, outline->y0); } @@ -1142,14 +1763,16 @@ ImagingOutlineTransform(ImagingOutline outline, double a[6]) y0 = eIn->y0; /* FIXME: ouch! */ - if (eIn->x0 == eIn->xmin) + if (eIn->x0 == eIn->xmin) { x1 = eIn->xmax; - else + } else { x1 = eIn->xmin; - if (eIn->y0 == eIn->ymin) + } + if (eIn->y0 == eIn->ymin) { y1 = eIn->ymax; - else + } else { y1 = eIn->ymin; + } /* full moon tonight! if this doesn't work, you may need to upgrade your compiler (make sure you have the right service diff --git a/src/libImaging/Effects.c b/src/libImaging/Effects.c index 7b4ff0b43..e0f1d0339 100644 --- a/src/libImaging/Effects.c +++ b/src/libImaging/Effects.c @@ -34,12 +34,14 @@ ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) /* Check arguments */ width = extent[2] - extent[0]; height = extent[3] - extent[1]; - if (width < 0.0 || height < 0.0 || quality < 2) + if (width < 0.0 || height < 0.0 || quality < 2) { return (Imaging) ImagingError_ValueError(NULL); + } im = ImagingNewDirty("L", xsize, ysize); - if (!im) + if (!im) { return NULL; + } dr = width/(xsize-1); di = height/(ysize-1); @@ -82,8 +84,9 @@ ImagingEffectNoise(int xsize, int ysize, float sigma) double this, next; imOut = ImagingNewDirty("L", xsize, ysize); - if (!imOut) + if (!imOut) { return NULL; + } next = 0.0; nextok = 0; @@ -123,20 +126,31 @@ ImagingEffectSpread(Imaging imIn, int distance) imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) + if (!imOut) { return NULL; + } #define SPREAD(type, image)\ - for (y = 0; y < imOut->ysize; y++)\ - for (x = 0; x < imOut->xsize; x++) {\ - int xx = x + (rand() % distance) - distance/2;\ - int yy = y + (rand() % distance) - distance/2;\ - if (xx >= 0 && xx < imIn->xsize && yy >= 0 && yy < imIn->ysize) {\ - imOut->image[yy][xx] = imIn->image[y][x];\ - imOut->image[y][x] = imIn->image[yy][xx];\ - } else\ - imOut->image[y][x] = imIn->image[y][x];\ - } + if (distance == 0) {\ + for (y = 0; y < imOut->ysize; y++) {\ + for (x = 0; x < imOut->xsize; x++) {\ + imOut->image[y][x] = imIn->image[y][x];\ + }\ + }\ + } else {\ + for (y = 0; y < imOut->ysize; y++) {\ + for (x = 0; x < imOut->xsize; x++) {\ + int xx = x + (rand() % distance) - distance/2;\ + int yy = y + (rand() % distance) - distance/2;\ + if (xx >= 0 && xx < imIn->xsize && yy >= 0 && yy < imIn->ysize) {\ + imOut->image[yy][xx] = imIn->image[y][x];\ + imOut->image[y][x] = imIn->image[yy][xx];\ + } else {\ + imOut->image[y][x] = imIn->image[y][x];\ + }\ + }\ + }\ + } if (imIn->image8) { SPREAD(UINT8, image8); diff --git a/src/libImaging/EpsEncode.c b/src/libImaging/EpsEncode.c index 45fab0a6e..ac8a4059c 100644 --- a/src/libImaging/EpsEncode.c +++ b/src/libImaging/EpsEncode.c @@ -5,11 +5,11 @@ * encoder for EPS hex data * * history: - * 96-04-19 fl created - * 96-06-27 fl don't drop last block of encoded data + * 96-04-19 fl created + * 96-06-27 fl don't drop last block of encoded data * * notes: - * FIXME: rename to HexEncode.c ?? + * FIXME: rename to HexEncode.c ?? * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -31,47 +31,50 @@ ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) UINT8* in, i; if (!state->state) { - state->state = HEXBYTE; - state->xsize *= im->pixelsize; /* Hack! */ + state->state = HEXBYTE; + state->xsize *= im->pixelsize; /* Hack! */ } in = (UINT8*) im->image[state->y]; for (;;) { - if (state->state == NEWLINE) { - if (bytes < 1) - break; - *ptr++ = '\n'; - bytes--; - state->state = HEXBYTE; - } + if (state->state == NEWLINE) { + if (bytes < 1) { + break; + } + *ptr++ = '\n'; + bytes--; + state->state = HEXBYTE; + } - if (bytes < 2) - break; + if (bytes < 2) { + break; + } - i = in[state->x++]; - *ptr++ = hex[(i>>4)&15]; - *ptr++ = hex[i&15]; - bytes -= 2; + i = in[state->x++]; + *ptr++ = hex[(i>>4)&15]; + *ptr++ = hex[i&15]; + bytes -= 2; - /* Skip junk bytes */ - if (im->bands == 3 && (state->x & 3) == 3) - state->x++; + /* Skip junk bytes */ + if (im->bands == 3 && (state->x & 3) == 3) { + state->x++; + } - if (++state->count >= 79/2) { - state->state = NEWLINE; - state->count = 0; - } + if (++state->count >= 79/2) { + state->state = NEWLINE; + state->count = 0; + } - if (state->x >= state->xsize) { - state->x = 0; - if (++state->y >= state->ysize) { - state->errcode = IMAGING_CODEC_END; - break; - } - in = (UINT8*) im->image[state->y]; - } + if (state->x >= state->xsize) { + state->x = 0; + if (++state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + break; + } + in = (UINT8*) im->image[state->y]; + } } diff --git a/src/libImaging/Except.c b/src/libImaging/Except.c index 515d85d1f..3903b5bb8 100644 --- a/src/libImaging/Except.c +++ b/src/libImaging/Except.c @@ -26,7 +26,7 @@ /* exception state */ void * -ImagingError_IOError(void) +ImagingError_OSError(void) { fprintf(stderr, "*** exception: file access error\n"); return NULL; @@ -43,21 +43,20 @@ void * ImagingError_ModeError(void) { return ImagingError_ValueError("bad image mode"); - return NULL; } void * ImagingError_Mismatch(void) { return ImagingError_ValueError("images don't match"); - return NULL; } void * ImagingError_ValueError(const char *message) { - if (!message) - message = "exception: bad argument to function"; + if (!message) { + message = "exception: bad argument to function"; + } fprintf(stderr, "*** %s\n", message); return NULL; } diff --git a/src/libImaging/File.c b/src/libImaging/File.c index 6f014c1f8..14688d661 100644 --- a/src/libImaging/File.c +++ b/src/libImaging/File.c @@ -31,15 +31,18 @@ ImagingSaveRaw(Imaging im, FILE* fp) /* @PIL227: FIXME: for mode "1", map != 0 to 255 */ /* PGM "L" */ - for (y = 0; y < im->ysize; y++) + for (y = 0; y < im->ysize; y++) { fwrite(im->image[y], 1, im->xsize, fp); + } } else { /* PPM "RGB" or other internal format */ - for (y = 0; y < im->ysize; y++) - for (x = i = 0; x < im->xsize; x++, i += im->pixelsize) + for (y = 0; y < im->ysize; y++) { + for (x = i = 0; x < im->xsize; x++, i += im->pixelsize) { fwrite(im->image[y]+i, 1, im->bands, fp); + } + } } @@ -59,7 +62,7 @@ ImagingSavePPM(Imaging im, const char* outfile) fp = fopen(outfile, "wb"); if (!fp) { - (void) ImagingError_IOError(); + (void) ImagingError_OSError(); return 0; } diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index d641a5996..da143b4f9 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -5,9 +5,9 @@ * fill image with constant pixel value * * history: - * 95-11-26 fl moved from Imaging.c - * 96-05-17 fl added radial fill, renamed wedge to linear - * 98-06-23 fl changed ImageFill signature + * 95-11-26 fl moved from Imaging.c + * 96-05-17 fl added radial fill, renamed wedge to linear + * 98-06-23 fl changed ImageFill signature * * Copyright (c) Secret Labs AB 1997-98. All rights reserved. * Copyright (c) Fredrik Lundh 1995-96. @@ -30,27 +30,33 @@ ImagingFill(Imaging im, const void* colour) /* use generic API */ ImagingAccess access = ImagingAccessNew(im); if (access) { - for (y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++) + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { access->put_pixel(im, x, y, colour); + } + } ImagingAccessDelete(im, access); } else { /* wipe the image */ - for (y = 0; y < im->ysize; y++) + for (y = 0; y < im->ysize; y++) { memset(im->image[y], 0, im->linesize); + } } } else { INT32 c = 0L; ImagingSectionEnter(&cookie); memcpy(&c, colour, im->pixelsize); if (im->image32 && c != 0L) { - for (y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++) + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { im->image32[y][x] = c; + } + } } else { unsigned char cc = (unsigned char) *(UINT8*) colour; - for (y = 0; y < im->ysize; y++) + for (y = 0; y < im->ysize; y++) { memset(im->image[y], cc, im->linesize); + } } ImagingSectionLeave(&cookie); } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index b033abf66..0897ddbfe 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -29,10 +29,12 @@ static inline UINT8 clip8(float in) { - if (in <= 0.0) + if (in <= 0.0) { return 0; - if (in >= 255.0) + } + if (in >= 255.0) { return 255; + } return (UINT8) in; } @@ -43,32 +45,40 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) int x, y; ImagingSectionCookie cookie; - if (xmargin < 0 && ymargin < 0) + if (xmargin < 0 && ymargin < 0) { return (Imaging) ImagingError_ValueError("bad kernel size"); + } imOut = ImagingNewDirty( imIn->mode, imIn->xsize+2*xmargin, imIn->ysize+2*ymargin); - if (!imOut) + if (!imOut) { return NULL; + } #define EXPAND_LINE(type, image, yin, yout) {\ - for (x = 0; x < xmargin; x++)\ + for (x = 0; x < xmargin; x++) {\ imOut->image[yout][x] = imIn->image[yin][0];\ - for (x = 0; x < imIn->xsize; x++)\ + }\ + for (x = 0; x < imIn->xsize; x++) {\ imOut->image[yout][x+xmargin] = imIn->image[yin][x];\ - for (x = 0; x < xmargin; x++)\ + }\ + for (x = 0; x < xmargin; x++) {\ imOut->image[yout][xmargin+imIn->xsize+x] =\ imIn->image[yin][imIn->xsize-1];\ - } + }\ +} #define EXPAND(type, image) {\ - for (y = 0; y < ymargin; y++)\ + for (y = 0; y < ymargin; y++) {\ EXPAND_LINE(type, image, 0, y);\ - for (y = 0; y < imIn->ysize; y++)\ + }\ + for (y = 0; y < imIn->ysize; y++) {\ EXPAND_LINE(type, image, y, y+ymargin);\ - for (y = 0; y < ymargin; y++)\ + }\ + for (y = 0; y < ymargin; y++) {\ EXPAND_LINE(type, image, imIn->ysize-1, ymargin+imIn->ysize+y);\ - } + }\ +} ImagingSectionEnter(&cookie); if (imIn->image8) { @@ -330,18 +340,22 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32* kernel, Imaging imOut; ImagingSectionCookie cookie; - if ( ! im || im->type != IMAGING_TYPE_UINT8) + if ( ! im || im->type != IMAGING_TYPE_UINT8) { return (Imaging) ImagingError_ModeError(); + } - if (im->xsize < xsize || im->ysize < ysize) + if (im->xsize < xsize || im->ysize < ysize) { return ImagingCopy(im); + } - if ((xsize != 3 && xsize != 5) || xsize != ysize) + if ((xsize != 3 && xsize != 5) || xsize != ysize) { return (Imaging) ImagingError_ValueError("bad kernel size"); + } imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); - if (!imOut) + if (!imOut) { return NULL; + } ImagingSectionEnter(&cookie); if (xsize == 3) { diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index 5f4485f89..84508013d 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -5,8 +5,8 @@ * decoder for Autodesk Animator FLI/FLC animations * * history: - * 97-01-03 fl Created - * 97-01-17 fl Added SS2 support (FLC) + * 97-01-03 fl Created + * 97-01-17 fl Added SS2 support (FLC) * * Copyright (c) Fredrik Lundh 1997. * Copyright (c) Secret Labs AB 1997. @@ -18,12 +18,17 @@ #include "Imaging.h" -#define I16(ptr)\ +#define I16(ptr)\ ((ptr)[0] + ((ptr)[1] << 8)) -#define I32(ptr)\ +#define I32(ptr)\ ((ptr)[0] + ((ptr)[1] << 8) + ((ptr)[2] << 16) + ((ptr)[3] << 24)) +#define ERR_IF_DATA_OOB(offset) \ + if ((data + (offset)) > ptr + bytes) {\ + state->errcode = IMAGING_CODEC_OVERRUN; \ + return -1; \ + } int ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) @@ -36,25 +41,30 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt /* If not even the chunk size is present, we'd better leave */ - if (bytes < 4) - return 0; + if (bytes < 4) { + return 0; + } /* We don't decode anything unless we have a full chunk in the - input buffer (on the other hand, the Python part of the driver - makes sure this is always the case) */ + input buffer */ ptr = buf; framesize = I32(ptr); - if (framesize < I32(ptr)) - return 0; + if (framesize < I32(ptr)) { + return 0; + } /* Make sure this is a frame chunk. The Python driver takes case of other chunk types. */ + if (bytes < 8) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } if (I16(ptr+4) != 0xF1FA) { - state->errcode = IMAGING_CODEC_UNKNOWN; - return -1; + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; } chunks = I16(ptr+6); @@ -63,150 +73,181 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt /* Process subchunks */ for (c = 0; c < chunks; c++) { - UINT8* data; - if (bytes < 10) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - data = ptr + 6; - switch (I16(ptr+4)) { - case 4: case 11: - /* FLI COLOR chunk */ - break; /* ignored; handled by Python code */ - case 7: - /* FLI SS2 chunk (word delta) */ - lines = I16(data); data += 2; - for (l = y = 0; l < lines && y < state->ysize; l++, y++) { - UINT8* buf = (UINT8*) im->image[y]; - int p, packets; - packets = I16(data); data += 2; - while (packets & 0x8000) { - /* flag word */ - if (packets & 0x4000) { - y += 65536 - packets; /* skip lines */ - if (y >= state->ysize) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - buf = (UINT8*) im->image[y]; - } else { - /* store last byte (used if line width is odd) */ - buf[state->xsize-1] = (UINT8) packets; - } - packets = I16(data); data += 2; - } - for (p = x = 0; p < packets; p++) { - x += data[0]; /* pixel skip */ - if (data[1] >= 128) { - i = 256-data[1]; /* run */ - if (x + i + i > state->xsize) - break; - for (j = 0; j < i; j++) { - buf[x++] = data[2]; - buf[x++] = data[3]; - } - data += 2 + 2; - } else { - i = 2 * (int) data[1]; /* chunk */ - if (x + i > state->xsize) - break; - memcpy(buf + x, data + 2, i); - data += 2 + i; - x += i; - } - } - if (p < packets) - break; /* didn't process all packets */ - } - if (l < lines) { - /* didn't process all lines */ - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - break; - case 12: - /* FLI LC chunk (byte delta) */ - y = I16(data); ymax = y + I16(data+2); data += 4; - for (; y < ymax && y < state->ysize; y++) { - UINT8* out = (UINT8*) im->image[y]; - int p, packets = *data++; - for (p = x = 0; p < packets; p++, x += i) { - x += data[0]; /* skip pixels */ - if (data[1] & 0x80) { - i = 256-data[1]; /* run */ - if (x + i > state->xsize) - break; - memset(out + x, data[2], i); - data += 3; - } else { - i = data[1]; /* chunk */ - if (x + i > state->xsize) - break; - memcpy(out + x, data + 2, i); - data += i + 2; - } - } - if (p < packets) - break; /* didn't process all packets */ - } - if (y < ymax) { - /* didn't process all lines */ - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - break; - case 13: - /* FLI BLACK chunk */ - for (y = 0; y < state->ysize; y++) - memset(im->image[y], 0, state->xsize); - break; - case 15: - /* FLI BRUN chunk */ - for (y = 0; y < state->ysize; y++) { - UINT8* out = (UINT8*) im->image[y]; - data += 1; /* ignore packetcount byte */ - for (x = 0; x < state->xsize; x += i) { - if (data[0] & 0x80) { - i = 256 - data[0]; - if (x + i > state->xsize) - break; /* safety first */ - memcpy(out + x, data + 1, i); - data += i + 1; - } else { - i = data[0]; - if (x + i > state->xsize) - break; /* safety first */ - memset(out + x, data[1], i); - data += 2; - } - } - if (x != state->xsize) { - /* didn't unpack whole line */ - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - } - break; - case 16: - /* COPY chunk */ - for (y = 0; y < state->ysize; y++) { - UINT8* buf = (UINT8*) im->image[y]; - memcpy(buf, data, state->xsize); - data += state->xsize; - } - break; - case 18: - /* PSTAMP chunk */ - break; /* ignored */ - default: - /* unknown chunk */ - /* printf("unknown FLI/FLC chunk: %d\n", I16(ptr+4)); */ - state->errcode = IMAGING_CODEC_UNKNOWN; - return -1; - } - advance = I32(ptr); - ptr += advance; - bytes -= advance; + UINT8* data; + if (bytes < 10) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + data = ptr + 6; + switch (I16(ptr+4)) { + case 4: case 11: + /* FLI COLOR chunk */ + break; /* ignored; handled by Python code */ + case 7: + /* FLI SS2 chunk (word delta) */ + /* OOB ok, we've got 4 bytes min on entry */ + lines = I16(data); data += 2; + for (l = y = 0; l < lines && y < state->ysize; l++, y++) { + UINT8* local_buf = (UINT8*) im->image[y]; + int p, packets; + ERR_IF_DATA_OOB(2) + packets = I16(data); data += 2; + while (packets & 0x8000) { + /* flag word */ + if (packets & 0x4000) { + y += 65536 - packets; /* skip lines */ + if (y >= state->ysize) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + local_buf = (UINT8*) im->image[y]; + } else { + /* store last byte (used if line width is odd) */ + local_buf[state->xsize-1] = (UINT8) packets; + } + ERR_IF_DATA_OOB(2) + packets = I16(data); data += 2; + } + for (p = x = 0; p < packets; p++) { + ERR_IF_DATA_OOB(2) + x += data[0]; /* pixel skip */ + if (data[1] >= 128) { + ERR_IF_DATA_OOB(4) + i = 256-data[1]; /* run */ + if (x + i + i > state->xsize) { + break; + } + for (j = 0; j < i; j++) { + local_buf[x++] = data[2]; + local_buf[x++] = data[3]; + } + data += 2 + 2; + } else { + i = 2 * (int) data[1]; /* chunk */ + if (x + i > state->xsize) { + break; + } + ERR_IF_DATA_OOB(2+i) + memcpy(local_buf + x, data + 2, i); + data += 2 + i; + x += i; + } + } + if (p < packets) { + break; /* didn't process all packets */ + } + } + if (l < lines) { + /* didn't process all lines */ + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + break; + case 12: + /* FLI LC chunk (byte delta) */ + /* OOB Check ok, we have 4 bytes min here */ + y = I16(data); ymax = y + I16(data+2); data += 4; + for (; y < ymax && y < state->ysize; y++) { + UINT8* out = (UINT8*) im->image[y]; + ERR_IF_DATA_OOB(1) + int p, packets = *data++; + for (p = x = 0; p < packets; p++, x += i) { + ERR_IF_DATA_OOB(2) + x += data[0]; /* skip pixels */ + if (data[1] & 0x80) { + i = 256-data[1]; /* run */ + if (x + i > state->xsize) { + break; + } + ERR_IF_DATA_OOB(3) + memset(out + x, data[2], i); + data += 3; + } else { + i = data[1]; /* chunk */ + if (x + i > state->xsize) { + break; + } + ERR_IF_DATA_OOB(2+i) + memcpy(out + x, data + 2, i); + data += i + 2; + } + } + if (p < packets) { + break; /* didn't process all packets */ + } + } + if (y < ymax) { + /* didn't process all lines */ + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + break; + case 13: + /* FLI BLACK chunk */ + for (y = 0; y < state->ysize; y++) { + memset(im->image[y], 0, state->xsize); + } + break; + case 15: + /* FLI BRUN chunk */ + /* OOB, ok, we've got 4 bytes min on entry */ + for (y = 0; y < state->ysize; y++) { + UINT8* out = (UINT8*) im->image[y]; + data += 1; /* ignore packetcount byte */ + for (x = 0; x < state->xsize; x += i) { + ERR_IF_DATA_OOB(2) + if (data[0] & 0x80) { + i = 256 - data[0]; + if (x + i > state->xsize) { + break; /* safety first */ + } + ERR_IF_DATA_OOB(i+1) + memcpy(out + x, data + 1, i); + data += i + 1; + } else { + i = data[0]; + if (x + i > state->xsize) { + break; /* safety first */ + } + memset(out + x, data[1], i); + data += 2; + } + } + if (x != state->xsize) { + /* didn't unpack whole line */ + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + } + break; + case 16: + /* COPY chunk */ + if (state->xsize > bytes/state->ysize) { + /* not enough data for frame */ + return ptr - buf; /* bytes consumed */ + } + for (y = 0; y < state->ysize; y++) { + UINT8* local_buf = (UINT8*) im->image[y]; + memcpy(local_buf, data, state->xsize); + data += state->xsize; + } + break; + case 18: + /* PSTAMP chunk */ + break; /* ignored */ + default: + /* unknown chunk */ + /* printf("unknown FLI/FLC chunk: %d\n", I16(ptr+4)); */ + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } + advance = I32(ptr); + if (advance < 0 || advance > bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + ptr += advance; + bytes -= advance; } return -1; /* end of frame */ diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index fd5e25958..06d0cf24d 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -20,10 +20,12 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) ImagingSectionCookie cookie; int x, y, xr; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) + } + if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { return (Imaging) ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); @@ -32,8 +34,9 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) INT* in = (INT *)imIn->image[y]; \ INT* out = (INT *)imOut->image[y]; \ xr = imIn->xsize-1; \ - for (x = 0; x < imIn->xsize; x++, xr--) \ + for (x = 0; x < imIn->xsize; x++, xr--) { \ out[xr] = in[x]; \ + } \ } ImagingSectionEnter(&cookie); @@ -62,18 +65,21 @@ ImagingFlipTopBottom(Imaging imOut, Imaging imIn) ImagingSectionCookie cookie; int y, yr; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) + } + if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { return (Imaging) ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); ImagingSectionEnter(&cookie); yr = imIn->ysize - 1; - for (y = 0; y < imIn->ysize; y++, yr--) + for (y = 0; y < imIn->ysize; y++, yr--) { memcpy(imOut->image[yr], imIn->image[y], imIn->linesize); + } ImagingSectionLeave(&cookie); @@ -88,10 +94,12 @@ ImagingRotate90(Imaging imOut, Imaging imIn) int x, y, xx, yy, xr, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) + } + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { return (Imaging) ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); @@ -144,10 +152,12 @@ ImagingTranspose(Imaging imOut, Imaging imIn) int x, y, xx, yy, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) + } + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { return (Imaging) ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); @@ -199,10 +209,12 @@ ImagingTransverse(Imaging imOut, Imaging imIn) int x, y, xr, yr, xx, yy, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) + } + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { return (Imaging) ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); @@ -255,10 +267,12 @@ ImagingRotate180(Imaging imOut, Imaging imIn) ImagingSectionCookie cookie; int x, y, xr, yr; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) + } + if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { return (Imaging) ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); @@ -267,8 +281,9 @@ ImagingRotate180(Imaging imOut, Imaging imIn) INT* in = (INT *)imIn->image[y]; \ INT* out = (INT *)imOut->image[yr]; \ xr = imIn->xsize-1; \ - for (x = 0; x < imIn->xsize; x++, xr--) \ + for (x = 0; x < imIn->xsize; x++, xr--) { \ out[xr] = in[x]; \ + } \ } ImagingSectionEnter(&cookie); @@ -299,10 +314,12 @@ ImagingRotate270(Imaging imOut, Imaging imIn) int x, y, xx, yy, yr, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) + } + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { return (Imaging) ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); @@ -415,8 +432,9 @@ nearest_filter8(void* out, Imaging im, double xin, double yin) { int x = COORD(xin); int y = COORD(yin); - if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { return 0; + } ((UINT8*)out)[0] = im->image8[y][x]; return 1; } @@ -426,8 +444,9 @@ nearest_filter16(void* out, Imaging im, double xin, double yin) { int x = COORD(xin); int y = COORD(yin); - if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { return 0; + } memcpy(out, im->image8[y] + x * sizeof(INT16), sizeof(INT16)); return 1; } @@ -437,8 +456,9 @@ nearest_filter32(void* out, Imaging im, double xin, double yin) { int x = COORD(xin); int y = COORD(yin); - if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { return 0; + } memcpy(out, &im->image32[y][x], sizeof(INT32)); return 1; } @@ -455,8 +475,9 @@ nearest_filter32(void* out, Imaging im, double xin, double yin) double v1, v2;\ double dx, dy;\ type* in;\ - if (xin < 0.0 || xin >= im->xsize || yin < 0.0 || yin >= im->ysize)\ + if (xin < 0.0 || xin >= im->xsize || yin < 0.0 || yin >= im->ysize) {\ return 0;\ + }\ xin -= 0.5;\ yin -= 0.5;\ x = FLOOR(xin);\ @@ -472,8 +493,9 @@ nearest_filter32(void* out, Imaging im, double xin, double yin) if (y+1 >= 0 && y+1 < im->ysize) {\ in = (type*) ((image)[y+1] + offset);\ BILINEAR(v2, in[x0], in[x1], dx);\ - } else\ + } else {\ v2 = v1;\ + }\ BILINEAR(v1, v1, v2, dy);\ } @@ -552,8 +574,9 @@ bilinear_filter32RGB(void* out, Imaging im, double xin, double yin) double v1, v2, v3, v4;\ double dx, dy;\ type* in;\ - if (xin < 0.0 || xin >= im->xsize || yin < 0.0 || yin >= im->ysize)\ + if (xin < 0.0 || xin >= im->xsize || yin < 0.0 || yin >= im->ysize) {\ return 0;\ + }\ xin -= 0.5;\ yin -= 0.5;\ x = FLOOR(xin);\ @@ -572,18 +595,21 @@ bilinear_filter32RGB(void* out, Imaging im, double xin, double yin) if (y+1 >= 0 && y+1 < im->ysize) {\ in = (type*) ((image)[y+1] + offset);\ BICUBIC(v2, in[x0], in[x1], in[x2], in[x3], dx);\ - } else\ + } else {\ v2 = v1;\ + }\ if (y+2 >= 0 && y+2 < im->ysize) {\ in = (type*) ((image)[y+2] + offset);\ BICUBIC(v3, in[x0], in[x1], in[x2], in[x3], dx);\ - } else\ + } else {\ v3 = v2;\ + }\ if (y+3 >= 0 && y+3 < im->ysize) {\ in = (type*) ((image)[y+3] + offset);\ BICUBIC(v4, in[x0], in[x1], in[x2], in[x3], dx);\ - } else\ + } else {\ v4 = v3;\ + }\ BICUBIC(v1, v1, v2, v3, v4, dy);\ } @@ -593,12 +619,13 @@ bicubic_filter8(void* out, Imaging im, double xin, double yin) { BICUBIC_HEAD(UINT8); BICUBIC_BODY(UINT8, im->image8, 1, 0); - if (v1 <= 0.0) + if (v1 <= 0.0) { ((UINT8*)out)[0] = 0; - else if (v1 >= 255.0) + } else if (v1 >= 255.0) { ((UINT8*)out)[0] = 255; - else + } else { ((UINT8*)out)[0] = (UINT8) v1; + } return 1; } @@ -643,12 +670,13 @@ bicubic_filter32LA(void* out, Imaging im, double xin, double yin) ((UINT8*)out)[2] = (UINT8) v1; } BICUBIC_BODY(UINT8, im->image, 4, 3); - if (v1 <= 0.0) + if (v1 <= 0.0) { ((UINT8*)out)[3] = 0; - else if (v1 >= 255.0) + } else if (v1 >= 255.0) { ((UINT8*)out)[3] = 255; - else + } else { ((UINT8*)out)[3] = (UINT8) v1; + } return 1; } @@ -659,12 +687,13 @@ bicubic_filter32RGB(void* out, Imaging im, double xin, double yin) BICUBIC_HEAD(UINT8); for (b = 0; b < im->bands; b++) { BICUBIC_BODY(UINT8, im->image, 4, b); - if (v1 <= 0.0) + if (v1 <= 0.0) { ((UINT8*)out)[b] = 0; - else if (v1 >= 255.0) + } else if (v1 >= 255.0) { ((UINT8*)out)[b] = 255; - else + } else { ((UINT8*)out)[b] = (UINT8) v1; + } } return 1; } @@ -678,7 +707,7 @@ getfilter(Imaging im, int filterid) { switch (filterid) { case IMAGING_TRANSFORM_NEAREST: - if (im->image8) + if (im->image8) { switch (im->type) { case IMAGING_TYPE_UINT8: return nearest_filter8; @@ -692,19 +721,21 @@ getfilter(Imaging im, int filterid) return nearest_filter32; } } - else + } else { return nearest_filter32; + } break; case IMAGING_TRANSFORM_BILINEAR: - if (im->image8) + if (im->image8) { return bilinear_filter8; - else if (im->image32) { + } else if (im->image32) { switch (im->type) { case IMAGING_TYPE_UINT8: - if (im->bands == 2) + if (im->bands == 2) { return bilinear_filter32LA; - else + } else { return bilinear_filter32RGB; + } case IMAGING_TYPE_INT32: return bilinear_filter32I; case IMAGING_TYPE_FLOAT32: @@ -713,15 +744,16 @@ getfilter(Imaging im, int filterid) } break; case IMAGING_TRANSFORM_BICUBIC: - if (im->image8) + if (im->image8) { return bicubic_filter8; - else if (im->image32) { + } else if (im->image32) { switch (im->type) { case IMAGING_TYPE_UINT8: - if (im->bands == 2) + if (im->bands == 2) { return bicubic_filter32LA; - else + } else { return bicubic_filter32RGB; + } case IMAGING_TYPE_INT32: return bicubic_filter32I; case IMAGING_TYPE_FLOAT32: @@ -751,32 +783,39 @@ ImagingGenericTransform( double xx, yy; ImagingTransformFilter filter = getfilter(imIn, filterid); - if (!filter) + if (!filter) { return (Imaging) ImagingError_ValueError("bad filter number"); + } - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging) ImagingError_ModeError(); + } ImagingCopyPalette(imOut, imIn); ImagingSectionEnter(&cookie); - if (x0 < 0) + if (x0 < 0) { x0 = 0; - if (y0 < 0) + } + if (y0 < 0) { y0 = 0; - if (x1 > imOut->xsize) + } + if (x1 > imOut->xsize) { x1 = imOut->xsize; - if (y1 > imOut->ysize) + } + if (y1 > imOut->ysize) { y1 = imOut->ysize; + } for (y = y0; y < y1; y++) { out = imOut->image[y] + x0*imOut->pixelsize; for (x = x0; x < x1; x++) { if ( ! transform(&xx, &yy, x-x0, y-y0, transform_data) || ! filter(out, imIn, xx, yy)) { - if (fill) + if (fill) { memset(out, 0, imOut->pixelsize); + } } out += imOut->pixelsize; } @@ -801,19 +840,24 @@ ImagingScaleAffine(Imaging imOut, Imaging imIn, int xmin, xmax; int *xintab; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging) ImagingError_ModeError(); + } ImagingCopyPalette(imOut, imIn); - if (x0 < 0) + if (x0 < 0) { x0 = 0; - if (y0 < 0) + } + if (y0 < 0) { y0 = 0; - if (x1 > imOut->xsize) + } + if (x1 > imOut->xsize) { x1 = imOut->xsize; - if (y1 > imOut->ysize) + } + if (y1 > imOut->ysize) { y1 = imOut->ysize; + } /* malloc check ok, uses calloc for overflow */ xintab = (int*) calloc(imOut->xsize, sizeof(int)); @@ -833,8 +877,9 @@ ImagingScaleAffine(Imaging imOut, Imaging imIn, xin = COORD(xo); if (xin >= 0 && xin < (int) imIn->xsize) { xmax = x+1; - if (x < xmin) + if (x < xmin) { xmin = x; + } xintab[x] = xin; } xo += a[0]; @@ -845,12 +890,14 @@ ImagingScaleAffine(Imaging imOut, Imaging imIn, int yi = COORD(yo);\ pixel *in, *out;\ out = imOut->image[y];\ - if (fill && x1 > x0)\ + if (fill && x1 > x0) {\ memset(out+x0, 0, (x1-x0)*sizeof(pixel));\ + }\ if (yi >= 0 && yi < imIn->ysize) {\ in = imIn->image[yi];\ - for (x = xmin; x < xmax; x++)\ + for (x = xmin; x < xmax; x++) {\ out[x] = in[xintab[x]];\ + }\ }\ yo += a[4];\ } @@ -915,14 +962,16 @@ affine_fixed(Imaging imOut, Imaging imIn, xx = a2;\ yy = a5;\ out = imOut->image[y];\ - if (fill && x1 > x0)\ + if (fill && x1 > x0) {\ memset(out+x0, 0, (x1-x0)*sizeof(pixel));\ + }\ for (x = x0; x < x1; x++, out++) {\ xin = xx >> 16;\ if (xin >= 0 && xin < xsize) {\ yin = yy >> 16;\ - if (yin >= 0 && yin < ysize)\ + if (yin >= 0 && yin < ysize) {\ *out = imIn->image[yin][xin];\ + }\ }\ xx += a0;\ yy += a3;\ @@ -933,10 +982,11 @@ affine_fixed(Imaging imOut, Imaging imIn, ImagingSectionEnter(&cookie); - if (imIn->image8) + if (imIn->image8) { AFFINE_TRANSFORM_FIXED(UINT8, image8) - else + } else { AFFINE_TRANSFORM_FIXED(INT32, image32) + } ImagingSectionLeave(&cookie); @@ -973,24 +1023,30 @@ ImagingTransformAffine(Imaging imOut, Imaging imIn, return ImagingScaleAffine(imOut, imIn, x0, y0, x1, y1, a, fill); } - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging) ImagingError_ModeError(); + } - if (x0 < 0) + if (x0 < 0) { x0 = 0; - if (y0 < 0) + } + if (y0 < 0) { y0 = 0; - if (x1 > imOut->xsize) + } + if (x1 > imOut->xsize) { x1 = imOut->xsize; - if (y1 > imOut->ysize) + } + if (y1 > imOut->ysize) { y1 = imOut->ysize; + } /* translate all four corners to check if they are within the range that can be represented by the fixed point arithmetics */ if (check_fixed(a, 0, 0) && check_fixed(a, x1-x0, y1-y0) && - check_fixed(a, 0, y1-y0) && check_fixed(a, x1-x0, 0)) + check_fixed(a, 0, y1-y0) && check_fixed(a, x1-x0, 0)) { return affine_fixed(imOut, imIn, x0, y0, x1, y1, a, filterid, fill); + } /* FIXME: cannot really think of any reasonable case when the following code is used. maybe we should fall back on the slow @@ -1010,14 +1066,16 @@ ImagingTransformAffine(Imaging imOut, Imaging imIn, xx = xo;\ yy = yo;\ out = imOut->image[y];\ - if (fill && x1 > x0)\ + if (fill && x1 > x0) {\ memset(out+x0, 0, (x1-x0)*sizeof(pixel));\ + }\ for (x = x0; x < x1; x++, out++) {\ xin = COORD(xx);\ if (xin >= 0 && xin < xsize) {\ yin = COORD(yy);\ - if (yin >= 0 && yin < ysize)\ + if (yin >= 0 && yin < ysize) {\ *out = imIn->image[yin][xin];\ + }\ }\ xx += a[0];\ yy += a[3];\ @@ -1028,10 +1086,11 @@ ImagingTransformAffine(Imaging imOut, Imaging imIn, ImagingSectionEnter(&cookie); - if (imIn->image8) + if (imIn->image8) { AFFINE_TRANSFORM(UINT8, image8) - else + } else { AFFINE_TRANSFORM(INT32, image32) + } ImagingSectionLeave(&cookie); diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index b63888f87..9a8ae1f32 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -33,36 +33,52 @@ ImagingGetBBox(Imaging im, int bbox[4]) bbox[1] = -1; bbox[2] = bbox[3] = 0; -#define GETBBOX(image, mask)\ +#define GETBBOX(image, mask)\ for (y = 0; y < im->ysize; y++) {\ - has_data = 0;\ - for (x = 0; x < im->xsize; x++)\ - if (im->image[y][x] & mask) {\ - has_data = 1;\ - if (x < bbox[0])\ - bbox[0] = x;\ - if (x >= bbox[2])\ - bbox[2] = x+1;\ - }\ - if (has_data) {\ - if (bbox[1] < 0)\ - bbox[1] = y;\ - bbox[3] = y+1;\ - }\ + has_data = 0;\ + for (x = 0; x < im->xsize; x++) {\ + if (im->image[y][x] & mask) {\ + has_data = 1;\ + if (x < bbox[0]) {\ + bbox[0] = x;\ + }\ + if (x >= bbox[2]) {\ + bbox[2] = x+1;\ + }\ + }\ + }\ + if (has_data) {\ + if (bbox[1] < 0) {\ + bbox[1] = y;\ + }\ + bbox[3] = y+1;\ + }\ } if (im->image8) { - GETBBOX(image8, 0xff); + GETBBOX(image8, 0xff); } else { - INT32 mask = 0xffffffff; - if (im->bands == 3) - ((UINT8*) &mask)[3] = 0; - GETBBOX(image32, mask); + INT32 mask = 0xffffffff; + if (im->bands == 3) { + ((UINT8*) &mask)[3] = 0; + } else if (strcmp(im->mode, "RGBa") == 0 || + strcmp(im->mode, "RGBA") == 0 || + strcmp(im->mode, "La") == 0 || + strcmp(im->mode, "LA") == 0 || + strcmp(im->mode, "PA") == 0) { +#ifdef WORDS_BIGENDIAN + mask = 0x000000ff; +#else + mask = 0xff000000; +#endif + } + GETBBOX(image32, mask); } /* Check that we got a box */ - if (bbox[1] < 0) - return 0; /* no data */ + if (bbox[1] < 0) { + return 0; /* no data */ + } return 1; /* ok */ } @@ -80,25 +96,28 @@ ImagingGetProjection(Imaging im, UINT8* xproj, UINT8* yproj) memset(xproj, 0, im->xsize); memset(yproj, 0, im->ysize); -#define GETPROJ(image, mask)\ - for (y = 0; y < im->ysize; y++) {\ - has_data = 0;\ - for (x = 0; x < im->xsize; x++)\ - if (im->image[y][x] & mask) {\ - has_data = 1;\ - xproj[x] = 1;\ - }\ - if (has_data)\ - yproj[y] = 1;\ - } + #define GETPROJ(image, mask)\ + for (y = 0; y < im->ysize; y++) {\ + has_data = 0;\ + for (x = 0; x < im->xsize; x++) {\ + if (im->image[y][x] & mask) {\ + has_data = 1;\ + xproj[x] = 1;\ + }\ + }\ + if (has_data) {\ + yproj[y] = 1;\ + }\ + } if (im->image8) { - GETPROJ(image8, 0xff); + GETPROJ(image8, 0xff); } else { - INT32 mask = 0xffffffff; - if (im->bands == 3) - ((UINT8*) &mask)[3] = 0; - GETPROJ(image32, mask); + INT32 mask = 0xffffffff; + if (im->bands == 3) { + ((UINT8*) &mask)[3] = 0; + } + GETPROJ(image32, mask); } return 1; /* ok */ @@ -113,12 +132,13 @@ ImagingGetExtrema(Imaging im, void *extrema) FLOAT32 fmin, fmax; if (im->bands != 1) { - (void) ImagingError_ModeError(); + (void) ImagingError_ModeError(); return -1; /* mismatch */ } - if (!im->xsize || !im->ysize) + if (!im->xsize || !im->ysize) { return 0; /* zero size */ + } switch (im->type) { case IMAGING_TYPE_UINT8: @@ -126,10 +146,11 @@ ImagingGetExtrema(Imaging im, void *extrema) for (y = 0; y < im->ysize; y++) { UINT8* in = im->image8[y]; for (x = 0; x < im->xsize; x++) { - if (imin > in[x]) + if (imin > in[x]) { imin = in[x]; - else if (imax < in[x]) + } else if (imax < in[x]) { imax = in[x]; + } } } ((UINT8*) extrema)[0] = (UINT8) imin; @@ -140,10 +161,11 @@ ImagingGetExtrema(Imaging im, void *extrema) for (y = 0; y < im->ysize; y++) { INT32* in = im->image32[y]; for (x = 0; x < im->xsize; x++) { - if (imin > in[x]) + if (imin > in[x]) { imin = in[x]; - else if (imax < in[x]) + } else if (imax < in[x]) { imax = in[x]; + } } } memcpy(extrema, &imin, sizeof(imin)); @@ -154,10 +176,11 @@ ImagingGetExtrema(Imaging im, void *extrema) for (y = 0; y < im->ysize; y++) { FLOAT32* in = (FLOAT32*) im->image32[y]; for (x = 0; x < im->xsize; x++) { - if (fmin > in[x]) + if (fmin > in[x]) { fmin = in[x]; - else if (fmax < in[x]) + } else if (fmax < in[x]) { fmax = in[x]; + } } } memcpy(extrema, &fmin, sizeof(fmin)); @@ -166,26 +189,37 @@ ImagingGetExtrema(Imaging im, void *extrema) case IMAGING_TYPE_SPECIAL: if (strcmp(im->mode, "I;16") == 0) { UINT16 v; - memcpy(&v, *im->image8, sizeof(v)); + UINT8* pixel = *im->image8; +#ifdef WORDS_BIGENDIAN + v = pixel[0] + (pixel[1] << 8); +#else + memcpy(&v, pixel, sizeof(v)); +#endif imin = imax = v; for (y = 0; y < im->ysize; y++) { for (x = 0; x < im->xsize; x++) { - memcpy(&v, im->image[y] + x * sizeof(v), sizeof(v)); - if (imin > v) + pixel = im->image[y] + x * sizeof(v); +#ifdef WORDS_BIGENDIAN + v = pixel[0] + (pixel[1] << 8); +#else + memcpy(&v, pixel, sizeof(v)); +#endif + if (imin > v) { imin = v; - else if (imax < v) + } else if (imax < v) { imax = v; + } } } v = (UINT16) imin; memcpy(extrema, &v, sizeof(v)); v = (UINT16) imax; memcpy(((char*)extrema) + sizeof(v), &v, sizeof(v)); - break; + break; } /* FALL THROUGH */ default: - (void) ImagingError_ModeError(); + (void) ImagingError_ModeError(); return -1; } return 1; /* ok */ @@ -243,19 +277,23 @@ getcolors32(Imaging im, int maxcolors, int* size) /* printf("code_size=%d\n", code_size); */ /* printf("code_poly=%d\n", code_poly); */ - if (!code_size) - return ImagingError_MemoryError(); /* just give up */ + if (!code_size) { + return ImagingError_MemoryError(); /* just give up */ + } - if (!im->image32) - return ImagingError_ModeError(); + if (!im->image32) { + return ImagingError_ModeError(); + } table = calloc(code_size + 1, sizeof(ImagingColorItem)); - if (!table) - return ImagingError_MemoryError(); + if (!table) { + return ImagingError_MemoryError(); + } pixel_mask = 0xffffffff; - if (im->bands == 3) + if (im->bands == 3) { ((UINT8*) &pixel_mask)[3] = 0; + } colors = 0; @@ -268,8 +306,9 @@ getcolors32(Imaging im, int maxcolors, int* size) v = &table[i]; if (!v->count) { /* add to table */ - if (colors++ == maxcolors) + if (colors++ == maxcolors) { goto overflow; + } v->x = x; v->y = y; v->pixel = pixel; v->count = 1; @@ -279,15 +318,17 @@ getcolors32(Imaging im, int maxcolors, int* size) continue; } incr = (h ^ (h >> 3)) & code_mask; - if (!incr) + if (!incr) { incr = code_mask; + } for (;;) { i = (i + incr) & code_mask; v = &table[i]; if (!v->count) { /* add to table */ - if (colors++ == maxcolors) + if (colors++ == maxcolors) { goto overflow; + } v->x = x; v->y = y; v->pixel = pixel; v->count = 1; @@ -297,8 +338,9 @@ getcolors32(Imaging im, int maxcolors, int* size) break; } incr = incr << 1; - if (incr > code_mask) + if (incr > code_mask) { incr = incr ^ code_poly; + } } } } @@ -308,8 +350,9 @@ overflow: /* pack the table */ for (x = y = 0; x < (int) code_size; x++) if (table[x].count) { - if (x != y) + if (x != y) { table[y] = table[x]; + } y++; } table[y].count = 0; /* mark end of table */ diff --git a/src/libImaging/Gif.h b/src/libImaging/Gif.h index 2cb95efd2..bb118396c 100644 --- a/src/libImaging/Gif.h +++ b/src/libImaging/Gif.h @@ -10,10 +10,10 @@ /* Max size for a LZW code word. */ -#define GIFBITS 12 +#define GIFBITS 12 -#define GIFTABLE (1< -#include /* memcpy() */ +#include /* memcpy() */ #include "Gif.h" @@ -34,26 +34,27 @@ state->x = 0;\ state->y += context->step;\ while (state->y >= state->ysize)\ - switch (context->interlace) {\ - case 1:\ - context->repeat = state->y = 4;\ - context->interlace = 2;\ - break;\ - case 2:\ - context->step = 4;\ - context->repeat = state->y = 2;\ - context->interlace = 3;\ - break;\ - case 3:\ - context->step = 2;\ - context->repeat = state->y = 1;\ - context->interlace = 0;\ - break;\ - default:\ - return -1;\ + switch (context->interlace) {\ + case 1:\ + context->repeat = state->y = 4;\ + context->interlace = 2;\ + break;\ + case 2:\ + context->step = 4;\ + context->repeat = state->y = 2;\ + context->interlace = 3;\ + break;\ + case 3:\ + context->step = 2;\ + context->repeat = state->y = 1;\ + context->interlace = 0;\ + break;\ + default:\ + return -1;\ }\ - if (state->y < state->ysize)\ + if (state->y < state->ysize) {\ out = im->image8[state->y + state->yoff] + state->xoff;\ + }\ } @@ -70,227 +71,232 @@ ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ssize_t if (!state->state) { - /* Initialise state */ - if (context->bits < 0 || context->bits > 12) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } + /* Initialise state */ + if (context->bits < 0 || context->bits > 12) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } - /* Clear code */ - context->clear = 1 << context->bits; + /* Clear code */ + context->clear = 1 << context->bits; - /* End code */ - context->end = context->clear + 1; + /* End code */ + context->end = context->clear + 1; - /* Interlace */ - if (context->interlace) { - context->interlace = 1; - context->step = context->repeat = 8; - } else - context->step = 1; + /* Interlace */ + if (context->interlace) { + context->interlace = 1; + context->step = context->repeat = 8; + } else { + context->step = 1; + } - state->state = 1; + state->state = 1; } out = im->image8[state->y + state->yoff] + state->xoff + state->x; for (;;) { - if (state->state == 1) { + if (state->state == 1) { - /* First free entry in table */ - context->next = context->clear + 2; + /* First free entry in table */ + context->next = context->clear + 2; - /* Initial code size */ - context->codesize = context->bits + 1; - context->codemask = (1 << context->codesize) - 1; + /* Initial code size */ + context->codesize = context->bits + 1; + context->codemask = (1 << context->codesize) - 1; - /* Buffer pointer. We fill the buffer from right, which - allows us to return all of it in one operation. */ - context->bufferindex = GIFBUFFER; + /* Buffer pointer. We fill the buffer from right, which + allows us to return all of it in one operation. */ + context->bufferindex = GIFBUFFER; - state->state = 2; - } + state->state = 2; + } - if (context->bufferindex < GIFBUFFER) { + if (context->bufferindex < GIFBUFFER) { - /* Return whole buffer in one chunk */ - i = GIFBUFFER - context->bufferindex; - p = &context->buffer[context->bufferindex]; + /* Return whole buffer in one chunk */ + i = GIFBUFFER - context->bufferindex; + p = &context->buffer[context->bufferindex]; - context->bufferindex = GIFBUFFER; + context->bufferindex = GIFBUFFER; - } else { + } else { - /* Get current symbol */ + /* Get current symbol */ - while (context->bitcount < context->codesize) { + while (context->bitcount < context->codesize) { - if (context->blocksize > 0) { + if (context->blocksize > 0) { - /* Read next byte */ - c = *ptr++; bytes--; + /* Read next byte */ + c = *ptr++; bytes--; - context->blocksize--; + context->blocksize--; - /* New bits are shifted in from from the left. */ - context->bitbuffer |= (INT32) c << context->bitcount; - context->bitcount += 8; + /* New bits are shifted in from from the left. */ + context->bitbuffer |= (INT32) c << context->bitcount; + context->bitcount += 8; - } else { + } else { - /* New GIF block */ + /* New GIF block */ - /* We don't start decoding unless we have a full block */ - if (bytes < 1) - return ptr - buffer; - c = *ptr; - if (bytes < c+1) - return ptr - buffer; + /* We don't start decoding unless we have a full block */ + if (bytes < 1) { + return ptr - buffer; + } + c = *ptr; + if (bytes < c+1) { + return ptr - buffer; + } - context->blocksize = c; + context->blocksize = c; - ptr++; bytes--; + ptr++; bytes--; - } - } + } + } - /* Extract current symbol from bit buffer. */ - c = (int) context->bitbuffer & context->codemask; + /* Extract current symbol from bit buffer. */ + c = (int) context->bitbuffer & context->codemask; - /* Adjust buffer */ - context->bitbuffer >>= context->codesize; - context->bitcount -= context->codesize; + /* Adjust buffer */ + context->bitbuffer >>= context->codesize; + context->bitcount -= context->codesize; - /* If c is less than "clear", it's a data byte. Otherwise, - it's either clear/end or a code symbol which should be - expanded. */ + /* If c is less than "clear", it's a data byte. Otherwise, + it's either clear/end or a code symbol which should be + expanded. */ - if (c == context->clear) { - if (state->state != 2) - state->state = 1; - continue; - } + if (c == context->clear) { + if (state->state != 2) { + state->state = 1; + } + continue; + } - if (c == context->end) - break; + if (c == context->end) { + break; + } - i = 1; - p = &context->lastdata; + i = 1; + p = &context->lastdata; - if (state->state == 2) { + if (state->state == 2) { - /* First valid symbol after clear; use as is */ - if (c > context->clear) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } + /* First valid symbol after clear; use as is */ + if (c > context->clear) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } - context->lastdata = context->lastcode = c; - state->state = 3; + context->lastdata = context->lastcode = c; + state->state = 3; - } else { + } else { - thiscode = c; + thiscode = c; - if (c > context->next) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } + if (c > context->next) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } - if (c == context->next) { + if (c == context->next) { - /* c == next is allowed. not sure why. */ + /* c == next is allowed. not sure why. */ - if (context->bufferindex <= 0) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } + if (context->bufferindex <= 0) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } - context->buffer[--context->bufferindex] = - context->lastdata; + context->buffer[--context->bufferindex] = + context->lastdata; - c = context->lastcode; + c = context->lastcode; - } + } - while (c >= context->clear) { + while (c >= context->clear) { - /* Copy data string to buffer (beginning from right) */ + /* Copy data string to buffer (beginning from right) */ - if (context->bufferindex <= 0 || c >= GIFTABLE) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } + if (context->bufferindex <= 0 || c >= GIFTABLE) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } - context->buffer[--context->bufferindex] = - context->data[c]; + context->buffer[--context->bufferindex] = + context->data[c]; - c = context->link[c]; - } + c = context->link[c]; + } - context->lastdata = c; + context->lastdata = c; - if (context->next < GIFTABLE) { + if (context->next < GIFTABLE) { - /* We'll only add this symbol if we have room - for it (take advise, Netscape!) */ - context->data[context->next] = c; - context->link[context->next] = context->lastcode; + /* We'll only add this symbol if we have room + for it (take advise, Netscape!) */ + context->data[context->next] = c; + context->link[context->next] = context->lastcode; - if (context->next == context->codemask && - context->codesize < GIFBITS) { + if (context->next == context->codemask && + context->codesize < GIFBITS) { - /* Expand code size */ - context->codesize++; - context->codemask = (1 << context->codesize) - 1; - } + /* Expand code size */ + context->codesize++; + context->codemask = (1 << context->codesize) - 1; + } - context->next++; + context->next++; - } + } - context->lastcode = thiscode; + context->lastcode = thiscode; - } - } + } + } - /* Copy the bytes into the image */ - if (state->y >= state->ysize) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } + /* Copy the bytes into the image */ + if (state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } - /* To squeeze some extra pixels out of this loop, we test for - some common cases and handle them separately. */ + /* To squeeze some extra pixels out of this loop, we test for + some common cases and handle them separately. */ - /* FIXME: should we handle the transparency index in here??? */ + /* FIXME: should we handle the transparency index in here??? */ - if (i == 1) { - if (state->x < state->xsize-1) { - /* Single pixel, not at the end of the line. */ - *out++ = p[0]; - state->x++; - continue; - } - } else if (state->x + i <= state->xsize) { - /* This string fits into current line. */ - memcpy(out, p, i); - out += i; - state->x += i; - if (state->x == state->xsize) { - NEWLINE(state, context); - } - continue; - } + if (i == 1) { + if (state->x < state->xsize-1) { + /* Single pixel, not at the end of the line. */ + *out++ = p[0]; + state->x++; + continue; + } + } else if (state->x + i <= state->xsize) { + /* This string fits into current line. */ + memcpy(out, p, i); + out += i; + state->x += i; + if (state->x == state->xsize) { + NEWLINE(state, context); + } + continue; + } - /* No shortcut, copy pixel by pixel */ - for (c = 0; c < i; c++) { - *out++ = p[c]; - if (++state->x >= state->xsize) { - NEWLINE(state, context); - } - } + /* No shortcut, copy pixel by pixel */ + for (c = 0; c < i; c++) { + *out++ = p[c]; + if (++state->x >= state->xsize) { + NEWLINE(state, context); + } + } } return ptr - buffer; diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index f211814ed..e9c6c3149 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -5,11 +5,11 @@ * encoder for uncompressed GIF data * * history: - * 97-01-05 fl created (writes uncompressed data) - * 97-08-27 fl fixed off-by-one error in buffer size test - * 98-07-09 fl added interlace write support - * 99-02-07 fl rewritten, now uses a run-length encoding strategy - * 99-02-08 fl improved run-length encoding for long runs + * 97-01-05 fl created (writes uncompressed data) + * 97-08-27 fl fixed off-by-one error in buffer size test + * 98-07-09 fl added interlace write support + * 99-02-07 fl rewritten, now uses a run-length encoding strategy + * 99-02-08 fl improved run-length encoding for long runs * * Copyright (c) Secret Labs AB 1997-99. * Copyright (c) Fredrik Lundh 1997. @@ -48,12 +48,14 @@ emit(GIFENCODERSTATE *context, int byte) /* add current block to end of flush queue */ if (context->block) { block = context->flush; - while (block && block->next) + while (block && block->next) { block = block->next; - if (block) + } + if (block) { block->next = context->block; - else + } else { context->flush = context->block; + } } /* get a new block */ @@ -63,8 +65,9 @@ emit(GIFENCODERSTATE *context, int byte) } else { /* malloc check ok, small constant allocation */ block = malloc(sizeof(GIFENCODERBLOCK)); - if (!block) + if (!block) { return 0; + } } block->size = 0; @@ -145,23 +148,25 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) if (!state->state) { - /* place a clear code in the output buffer */ - context->bitbuffer = CLEAR_CODE; - context->bitcount = 9; + /* place a clear code in the output buffer */ + context->bitbuffer = CLEAR_CODE; + context->bitcount = 9; - state->count = FIRST_CODE; + state->count = FIRST_CODE; - if (context->interlace) { - context->interlace = 1; - context->step = 8; - } else - context->step = 1; + if (context->interlace) { + context->interlace = 1; + context->step = 8; + } else { + context->step = 1; + } context->last = -1; /* sanity check */ - if (state->xsize <= 0 || state->ysize <= 0) + if (state->xsize <= 0 || state->ysize <= 0) { state->state = ENCODE_EOF; + } } @@ -169,152 +174,157 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) for (;;) - switch (state->state) { + switch (state->state) { - case INIT: - case ENCODE: + case INIT: + case ENCODE: - /* identify and store a run of pixels */ + /* identify and store a run of pixels */ - if (state->x == 0 || state->x >= state->xsize) { + if (state->x == 0 || state->x >= state->xsize) { - if (!context->interlace && state->y >= state->ysize) { - state->state = ENCODE_EOF; - break; - } - - if (context->flush) { - state->state = FLUSH; - break; - } - - /* get another line of data */ - state->shuffle( - state->buffer, - (UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->xsize - ); - - state->x = 0; - - if (state->state == INIT) { - /* preload the run-length buffer and get going */ - context->last = state->buffer[0]; - context->count = state->x = 1; - state->state = ENCODE; - } - - /* step forward, according to the interlace settings */ - state->y += context->step; - while (context->interlace && state->y >= state->ysize) - switch (context->interlace) { - case 1: - state->y = 4; - context->interlace = 2; + if (!context->interlace && state->y >= state->ysize) { + state->state = ENCODE_EOF; break; - case 2: - context->step = 4; - state->y = 2; - context->interlace = 3; - break; - case 3: - context->step = 2; - state->y = 1; - context->interlace = 0; - break; - default: - /* just make sure we don't loop forever */ - context->interlace = 0; } - } + if (context->flush) { + state->state = FLUSH; + break; + } - this = state->buffer[state->x++]; + /* get another line of data */ + state->shuffle( + state->buffer, + (UINT8*) im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, state->xsize + ); - if (this == context->last) - context->count++; - else { - EMIT_RUN(label1); - context->last = this; - context->count = 1; - } - break; + state->x = 0; + if (state->state == INIT) { + /* preload the run-length buffer and get going */ + context->last = state->buffer[0]; + context->count = state->x = 1; + state->state = ENCODE; + } - case ENCODE_EOF: + /* step forward, according to the interlace settings */ + state->y += context->step; + while (context->interlace && state->y >= state->ysize) + switch (context->interlace) { + case 1: + state->y = 4; + context->interlace = 2; + break; + case 2: + context->step = 4; + state->y = 2; + context->interlace = 3; + break; + case 3: + context->step = 2; + state->y = 1; + context->interlace = 0; + break; + default: + /* just make sure we don't loop forever */ + context->interlace = 0; + } - /* write the final run */ - EMIT_RUN(label2); - - /* write an end of image marker */ - EMIT(EOF_CODE); - - /* empty the bit buffer */ - while (context->bitcount > 0) { - if (!emit(context, (UINT8) context->bitbuffer)) { - state->errcode = IMAGING_CODEC_MEMORY; - return 0; } - context->bitbuffer >>= 8; - context->bitcount -= 8; - } - /* flush the last block, and exit */ - if (context->block) { - GIFENCODERBLOCK* block; - block = context->flush; - while (block && block->next) - block = block->next; - if (block) - block->next = context->block; - else - context->flush = context->block; - context->block = NULL; - } + this = state->buffer[state->x++]; - state->state = EXIT; + if (this == context->last) { + context->count++; + } else { + EMIT_RUN(label1); + context->last = this; + context->count = 1; + } + break; - /* fall through... */ - case EXIT: - case FLUSH: + case ENCODE_EOF: - while (context->flush) { + /* write the final run */ + EMIT_RUN(label2); - /* get a block from the flush queue */ - block = context->flush; + /* write an end of image marker */ + EMIT(EOF_CODE); - if (block->size > 0) { + /* empty the bit buffer */ + while (context->bitcount > 0) { + if (!emit(context, (UINT8) context->bitbuffer)) { + state->errcode = IMAGING_CODEC_MEMORY; + return 0; + } + context->bitbuffer >>= 8; + context->bitcount -= 8; + } - /* make sure it fits into the output buffer */ - if (bytes < block->size+1) + /* flush the last block, and exit */ + if (context->block) { + GIFENCODERBLOCK* block; + block = context->flush; + while (block && block->next) { + block = block->next; + } + if (block) { + block->next = context->block; + } else { + context->flush = context->block; + } + context->block = NULL; + } + + state->state = EXIT; + + /* fall through... */ + + case EXIT: + case FLUSH: + + while (context->flush) { + + /* get a block from the flush queue */ + block = context->flush; + + if (block->size > 0) { + + /* make sure it fits into the output buffer */ + if (bytes < block->size+1) { + return ptr - buf; + } + + ptr[0] = block->size; + memcpy(ptr+1, block->data, block->size); + + ptr += block->size+1; + bytes -= block->size+1; + + } + + context->flush = block->next; + + if (context->free) { + free(context->free); + } + context->free = block; + + } + + if (state->state == EXIT) { + /* this was the last block! */ + if (context->free) { + free(context->free); + } + state->errcode = IMAGING_CODEC_END; return ptr - buf; + } - ptr[0] = block->size; - memcpy(ptr+1, block->data, block->size); - - ptr += block->size+1; - bytes -= block->size+1; - - } - - context->flush = block->next; - - if (context->free) - free(context->free); - context->free = block; - - } - - if (state->state == EXIT) { - /* this was the last block! */ - if (context->free) - free(context->free); - state->errcode = IMAGING_CODEC_END; - return ptr - buf; - } - - state->state = ENCODE; - break; - } + state->state = ENCODE; + break; + } } diff --git a/src/libImaging/HexDecode.c b/src/libImaging/HexDecode.c index 8bd9bf67f..1def8766f 100644 --- a/src/libImaging/HexDecode.c +++ b/src/libImaging/HexDecode.c @@ -5,7 +5,7 @@ * decoder for hex encoded image data * * history: - * 96-05-16 fl Created + * 96-05-16 fl Created * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -16,9 +16,9 @@ #include "Imaging.h" -#define HEX(v) ((v >= '0' && v <= '9') ? v - '0' :\ - (v >= 'a' && v <= 'f') ? v - 'a' + 10 :\ - (v >= 'A' && v <= 'F') ? v - 'A' + 10 : -1) +#define HEX(v) ((v >= '0' && v <= '9') ? v - '0' :\ + (v >= 'a' && v <= 'f') ? v - 'a' + 10 :\ + (v >= 'A' && v <= 'F') ? v - 'A' + 10 : -1) int ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) @@ -30,38 +30,39 @@ ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt for (;;) { - if (bytes < 2) - return ptr - buf; + if (bytes < 2) { + return ptr - buf; + } - a = HEX(ptr[0]); - b = HEX(ptr[1]); + a = HEX(ptr[0]); + b = HEX(ptr[1]); - if (a < 0 || b < 0) { + if (a < 0 || b < 0) { - ptr++; - bytes--; + ptr++; + bytes--; - } else { + } else { - ptr += 2; - bytes -= 2; + ptr += 2; + bytes -= 2; - state->buffer[state->x] = (a<<4) + b; + state->buffer[state->x] = (a<<4) + b; - if (++state->x >= state->bytes) { + if (++state->x >= state->bytes) { - /* Got a full line, unpack it */ - state->shuffle((UINT8*) im->image[state->y], state->buffer, - state->xsize); + /* Got a full line, unpack it */ + state->shuffle((UINT8*) im->image[state->y], state->buffer, + state->xsize); - state->x = 0; + state->x = 0; - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } - } + } } } diff --git a/src/libImaging/Histo.c b/src/libImaging/Histo.c index 5c2824ab0..050c2840f 100644 --- a/src/libImaging/Histo.c +++ b/src/libImaging/Histo.c @@ -29,8 +29,9 @@ void ImagingHistogramDelete(ImagingHistogram h) { - if (h->histogram) - free(h->histogram); + if (h->histogram) { + free(h->histogram); + } free(h); } @@ -59,116 +60,132 @@ ImagingGetHistogram(Imaging im, Imaging imMask, void* minmax) INT32 imin, imax; FLOAT32 fmin, fmax, scale; - if (!im) - return ImagingError_ModeError(); + if (!im) { + return ImagingError_ModeError(); + } if (imMask) { - /* Validate mask */ - if (im->xsize != imMask->xsize || im->ysize != imMask->ysize) - return ImagingError_Mismatch(); - if (strcmp(imMask->mode, "1") != 0 && strcmp(imMask->mode, "L") != 0) - return ImagingError_ValueError("bad transparency mask"); + /* Validate mask */ + if (im->xsize != imMask->xsize || im->ysize != imMask->ysize) { + return ImagingError_Mismatch(); + } + if (strcmp(imMask->mode, "1") != 0 && strcmp(imMask->mode, "L") != 0) { + return ImagingError_ValueError("bad transparency mask"); + } } h = ImagingHistogramNew(im); if (imMask) { - /* mask */ - if (im->image8) { + /* mask */ + if (im->image8) { ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++) - if (imMask->image8[y][x] != 0) - h->histogram[im->image8[y][x]]++; + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { + if (imMask->image8[y][x] != 0) { + h->histogram[im->image8[y][x]]++; + } + } + } ImagingSectionLeave(&cookie); - } else { /* yes, we need the braces. C isn't Python! */ + } else { /* yes, we need the braces. C isn't Python! */ if (im->type != IMAGING_TYPE_UINT8) { ImagingHistogramDelete(h); return ImagingError_ModeError(); } ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) { - UINT8* in = (UINT8*) im->image32[y]; - for (x = 0; x < im->xsize; x++) - if (imMask->image8[y][x] != 0) { - h->histogram[(*in++)]++; - h->histogram[(*in++)+256]++; - h->histogram[(*in++)+512]++; - h->histogram[(*in++)+768]++; - } else - in += 4; - } - ImagingSectionLeave(&cookie); - } - } else { - /* mask not given; process pixels in image */ - if (im->image8) { - ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++) - h->histogram[im->image8[y][x]]++; - ImagingSectionLeave(&cookie); - } else { - switch (im->type) { - case IMAGING_TYPE_UINT8: - ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) { - UINT8* in = (UINT8*) im->image[y]; - for (x = 0; x < im->xsize; x++) { + for (y = 0; y < im->ysize; y++) { + UINT8* in = (UINT8*) im->image32[y]; + for (x = 0; x < im->xsize; x++) { + if (imMask->image8[y][x] != 0) { h->histogram[(*in++)]++; h->histogram[(*in++)+256]++; h->histogram[(*in++)+512]++; h->histogram[(*in++)+768]++; + } else { + in += 4; } } - ImagingSectionLeave(&cookie); - break; - case IMAGING_TYPE_INT32: - if (!minmax) { - ImagingHistogramDelete(h); - return ImagingError_ValueError("min/max not given"); + } + ImagingSectionLeave(&cookie); + } + } else { + /* mask not given; process pixels in image */ + if (im->image8) { + ImagingSectionEnter(&cookie); + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { + h->histogram[im->image8[y][x]]++; } - if (!im->xsize || !im->ysize) - break; - memcpy(&imin, minmax, sizeof(imin)); - memcpy(&imax, ((char*)minmax) + sizeof(imin), sizeof(imax)); - if (imin >= imax) - break; - ImagingSectionEnter(&cookie); - scale = 255.0F / (imax - imin); - for (y = 0; y < im->ysize; y++) { - INT32* in = im->image32[y]; - for (x = 0; x < im->xsize; x++) { - i = (int) (((*in++)-imin)*scale); - if (i >= 0 && i < 256) - h->histogram[i]++; + } + ImagingSectionLeave(&cookie); + } else { + switch (im->type) { + case IMAGING_TYPE_UINT8: + ImagingSectionEnter(&cookie); + for (y = 0; y < im->ysize; y++) { + UINT8* in = (UINT8*) im->image[y]; + for (x = 0; x < im->xsize; x++) { + h->histogram[(*in++)]++; + h->histogram[(*in++)+256]++; + h->histogram[(*in++)+512]++; + h->histogram[(*in++)+768]++; + } } - } - ImagingSectionLeave(&cookie); - break; - case IMAGING_TYPE_FLOAT32: - if (!minmax) { - ImagingHistogramDelete(h); - return ImagingError_ValueError("min/max not given"); - } - if (!im->xsize || !im->ysize) + ImagingSectionLeave(&cookie); break; - memcpy(&fmin, minmax, sizeof(fmin)); - memcpy(&fmax, ((char*)minmax) + sizeof(fmin), sizeof(fmax)); - if (fmin >= fmax) - break; - ImagingSectionEnter(&cookie); - scale = 255.0F / (fmax - fmin); - for (y = 0; y < im->ysize; y++) { - FLOAT32* in = (FLOAT32*) im->image32[y]; - for (x = 0; x < im->xsize; x++) { - i = (int) (((*in++)-fmin)*scale); - if (i >= 0 && i < 256) - h->histogram[i]++; + case IMAGING_TYPE_INT32: + if (!minmax) { + ImagingHistogramDelete(h); + return ImagingError_ValueError("min/max not given"); } - } - ImagingSectionLeave(&cookie); - break; + if (!im->xsize || !im->ysize) { + break; + } + memcpy(&imin, minmax, sizeof(imin)); + memcpy(&imax, ((char*)minmax) + sizeof(imin), sizeof(imax)); + if (imin >= imax) { + break; + } + ImagingSectionEnter(&cookie); + scale = 255.0F / (imax - imin); + for (y = 0; y < im->ysize; y++) { + INT32* in = im->image32[y]; + for (x = 0; x < im->xsize; x++) { + i = (int) (((*in++)-imin)*scale); + if (i >= 0 && i < 256) { + h->histogram[i]++; + } + } + } + ImagingSectionLeave(&cookie); + break; + case IMAGING_TYPE_FLOAT32: + if (!minmax) { + ImagingHistogramDelete(h); + return ImagingError_ValueError("min/max not given"); + } + if (!im->xsize || !im->ysize) { + break; + } + memcpy(&fmin, minmax, sizeof(fmin)); + memcpy(&fmax, ((char*)minmax) + sizeof(fmin), sizeof(fmax)); + if (fmin >= fmax) { + break; + } + ImagingSectionEnter(&cookie); + scale = 255.0F / (fmax - fmin); + for (y = 0; y < im->ysize; y++) { + FLOAT32* in = (FLOAT32*) im->image32[y]; + for (x = 0; x < im->xsize; x++) { + i = (int) (((*in++)-fmin)*scale); + if (i >= 0 && i < 256) { + h->histogram[i]++; + } + } + } + ImagingSectionLeave(&cookie); + break; } } } diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index b2d4db785..576ceaa58 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -39,41 +39,41 @@ /* For System that are not Windows, we'll need to define these. */ #if SIZEOF_SHORT == 2 -#define INT16 short +#define INT16 short #elif SIZEOF_INT == 2 -#define INT16 int +#define INT16 int #else -#define INT16 short /* most things works just fine anyway... */ +#define INT16 short /* most things works just fine anyway... */ #endif #if SIZEOF_SHORT == 4 -#define INT32 short +#define INT32 short #elif SIZEOF_INT == 4 -#define INT32 int +#define INT32 int #elif SIZEOF_LONG == 4 -#define INT32 long +#define INT32 long #else #error Cannot find required 32-bit integer type #endif #if SIZEOF_LONG == 8 -#define INT64 long +#define INT64 long #elif SIZEOF_LONG_LONG == 8 -#define INT64 long +#define INT64 long #endif -#define INT8 signed char -#define UINT8 unsigned char +#define INT8 signed char +#define UINT8 unsigned char -#define UINT16 unsigned INT16 -#define UINT32 unsigned INT32 +#define UINT16 unsigned INT16 +#define UINT32 unsigned INT32 #endif /* assume IEEE; tweak if necessary (patches are welcome) */ -#define FLOAT16 UINT16 -#define FLOAT32 float -#define FLOAT64 double +#define FLOAT16 UINT16 +#define FLOAT32 float +#define FLOAT64 double #ifdef _MSC_VER typedef signed __int64 int64_t; diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 25c15e758..d7dbe0325 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -185,8 +185,6 @@ extern Imaging ImagingNew2Dirty(const char* mode, Imaging imOut, Imaging imIn); extern void ImagingDelete(Imaging im); extern Imaging ImagingNewBlock(const char* mode, int xsize, int ysize); -extern Imaging ImagingNewMap(const char* filename, int readonly, - const char* mode, int xsize, int ysize); extern Imaging ImagingNewPrologue(const char *mode, int xsize, int ysize); @@ -229,7 +227,7 @@ extern void ImagingSectionLeave(ImagingSectionCookie* cookie); /* Exceptions */ /* ---------- */ -extern void* ImagingError_IOError(void); +extern void* ImagingError_OSError(void); extern void* ImagingError_MemoryError(void); extern void* ImagingError_ModeError(void); /* maps to ValueError by default */ extern void* ImagingError_Mismatch(void); /* maps to ValueError by default */ @@ -313,6 +311,7 @@ extern Imaging ImagingRotate270(Imaging imOut, Imaging imIn); extern Imaging ImagingTranspose(Imaging imOut, Imaging imIn); extern Imaging ImagingTransverse(Imaging imOut, Imaging imIn); extern Imaging ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]); +extern Imaging ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]); extern Imaging ImagingTransform( Imaging imOut, Imaging imIn, int method, int x0, int y0, int x1, int y1, double *a, int filter, int fill); @@ -338,15 +337,15 @@ extern Imaging ImagingChopSubtract( Imaging imIn1, Imaging imIn2, float scale, int offset); extern Imaging ImagingChopAddModulo(Imaging imIn1, Imaging imIn2); extern Imaging ImagingChopSubtractModulo(Imaging imIn1, Imaging imIn2); +extern Imaging ImagingChopSoftLight(Imaging imIn1, Imaging imIn2); +extern Imaging ImagingChopHardLight(Imaging imIn1, Imaging imIn2); +extern Imaging ImagingOverlay(Imaging imIn1, Imaging imIn2); /* "1" images only */ extern Imaging ImagingChopAnd(Imaging imIn1, Imaging imIn2); extern Imaging ImagingChopOr(Imaging imIn1, Imaging imIn2); extern Imaging ImagingChopXor(Imaging imIn1, Imaging imIn2); -/* Image measurement */ -extern void ImagingCrack(Imaging im, int x0, int y0); - /* Graphics */ extern int ImagingDrawArc(Imaging im, int x0, int y0, int x1, int y1, float start, float end, const void* ink, int width, @@ -392,10 +391,6 @@ extern Imaging ImagingEffectNoise(int xsize, int ysize, float sigma); extern Imaging ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality); -/* Obsolete */ -extern int ImagingToString(Imaging im, int orientation, char *buffer); -extern int ImagingFromString(Imaging im, int orientation, char *buffer); - /* File I/O */ /* -------- */ @@ -404,9 +399,6 @@ extern int ImagingFromString(Imaging im, int orientation, char *buffer); extern Imaging ImagingOpenPPM(const char* filename); extern int ImagingSavePPM(Imaging im, const char* filename); -/* Utility functions */ -extern UINT32 ImagingCRC32(UINT32 crc, UINT8* buffer, int bytes); - /* Codecs */ typedef struct ImagingCodecStateInstance *ImagingCodecState; typedef int (*ImagingCodec)(Imaging im, ImagingCodecState state, @@ -469,7 +461,6 @@ extern int ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); extern int ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ssize_t bytes); -extern int ImagingSgiRleDecodeCleanup(ImagingCodecState state); extern int ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ssize_t bytes); extern int ImagingTgaRleDecode(Imaging im, ImagingCodecState state, diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index d25da80ae..21c2688d8 100644 --- a/src/libImaging/ImagingUtils.h +++ b/src/libImaging/ImagingUtils.h @@ -1,11 +1,11 @@ #ifdef WORDS_BIGENDIAN - #define MAKE_UINT32(u0, u1, u2, u3) (u3 | (u2<<8) | (u1<<16) | (u0<<24)) + #define MAKE_UINT32(u0, u1, u2, u3) ((UINT32)(u3) | ((UINT32)(u2)<<8) | ((UINT32)(u1)<<16) | ((UINT32)(u0)<<24)) #define MASK_UINT32_CHANNEL_0 0xff000000 #define MASK_UINT32_CHANNEL_1 0x00ff0000 #define MASK_UINT32_CHANNEL_2 0x0000ff00 #define MASK_UINT32_CHANNEL_3 0x000000ff #else - #define MAKE_UINT32(u0, u1, u2, u3) (u0 | (u1<<8) | (u2<<16) | (u3<<24)) + #define MAKE_UINT32(u0, u1, u2, u3) ((UINT32)(u0) | ((UINT32)(u1)<<8) | ((UINT32)(u2)<<16) | ((UINT32)(u3)<<24)) #define MASK_UINT32_CHANNEL_0 0x000000ff #define MASK_UINT32_CHANNEL_1 0x0000ff00 #define MASK_UINT32_CHANNEL_2 0x00ff0000 diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 82e1b449f..280b6d638 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -14,13 +14,13 @@ typedef struct { - struct jpeg_error_mgr pub; /* "public" fields */ - jmp_buf setjmp_buffer; /* for return to caller */ + struct jpeg_error_mgr pub; /* "public" fields */ + jmp_buf setjmp_buffer; /* for return to caller */ } JPEGERROR; /* -------------------------------------------------------------------- */ -/* Decoder */ +/* Decoder */ typedef struct { struct jpeg_source_mgr pub; @@ -56,7 +56,7 @@ typedef struct { /* -------------------------------------------------------------------- */ -/* Encoder */ +/* Encoder */ typedef struct { struct jpeg_destination_mgr pub; @@ -67,7 +67,7 @@ typedef struct { /* CONFIGURATION */ - /* Quality (1-100, 0 means default) */ + /* Quality (0-100, -1 means default) */ int quality; /* Progressive mode */ @@ -110,7 +110,7 @@ typedef struct { int extra_offset; - int rawExifLen; /* EXIF data length */ + size_t rawExifLen; /* EXIF data length */ char* rawExif; /* EXIF buffer pointer */ } JPEGENCODERSTATE; diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index 7bb14eb2c..7645b9326 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -14,7 +14,7 @@ #define BUFFER_SIZE OPJ_J2K_STREAM_CHUNK_SIZE /* -------------------------------------------------------------------- */ -/* Decoder */ +/* Decoder */ /* -------------------------------------------------------------------- */ typedef struct { @@ -44,7 +44,7 @@ typedef struct { } JPEG2KDECODESTATE; /* -------------------------------------------------------------------- */ -/* Encoder */ +/* Encoder */ /* -------------------------------------------------------------------- */ typedef struct { diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index f2e437dda..b08e607a7 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -84,10 +84,11 @@ struct j2k_decode_unpacker { static inline unsigned j2ku_shift(unsigned x, int n) { - if (n < 0) + if (n < 0) { return x >> -n; - else + } else { return x << n; + } } static void @@ -104,35 +105,41 @@ j2ku_gray_l(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, unsigned x, y; - if (csiz == 3) + if (csiz == 3) { csiz = 4; + } - if (shift < 0) + if (shift < 0) { offset += 1 << (-shift - 1); + } + /* csiz*h*w + offset = tileinfo.datasize */ switch (csiz) { case 1: for (y = 0; y < h; ++y) { const UINT8 *data = &tiledata[y * w]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) + for (x = 0; x < w; ++x) { *row++ = j2ku_shift(offset + *data++, shift); + } } break; case 2: for (y = 0; y < h; ++y) { const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) + for (x = 0; x < w; ++x) { *row++ = j2ku_shift(offset + *data++, shift); + } } break; case 4: for (y = 0; y < h; ++y) { const UINT32 *data = (const UINT32 *)&tiledata[4 * y * w]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) + for (x = 0; x < w; ++x) { *row++ = j2ku_shift(offset + *data++, shift); + } } break; } @@ -153,35 +160,40 @@ j2ku_gray_i(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, unsigned x, y; - if (csiz == 3) + if (csiz == 3) { csiz = 4; + } - if (shift < 0) + if (shift < 0) { offset += 1 << (-shift - 1); + } switch (csiz) { case 1: for (y = 0; y < h; ++y) { const UINT8 *data = &tiledata[y * w]; UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) + for (x = 0; x < w; ++x) { *row++ = j2ku_shift(offset + *data++, shift); + } } break; case 2: for (y = 0; y < h; ++y) { const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) + for (x = 0; x < w; ++x) { *row++ = j2ku_shift(offset + *data++, shift); + } } break; case 4: for (y = 0; y < h; ++y) { const UINT32 *data = (const UINT32 *)&tiledata[4 * y * w]; UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) + for (x = 0; x < w; ++x) { *row++ = j2ku_shift(offset + *data++, shift); + } } break; } @@ -202,11 +214,13 @@ j2ku_gray_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, unsigned x, y; - if (shift < 0) + if (shift < 0) { offset += 1 << (-shift - 1); + } - if (csiz == 3) + if (csiz == 3) { csiz = 4; + } switch (csiz) { case 1: @@ -266,15 +280,19 @@ j2ku_graya_la(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, unsigned x, y; - if (csiz == 3) + if (csiz == 3) { csiz = 4; - if (acsiz == 3) + } + if (acsiz == 3) { acsiz = 4; + } - if (shift < 0) + if (shift < 0) { offset += 1 << (-shift - 1); - if (ashift < 0) + } + if (ashift < 0) { aoffset += 1 << (-ashift - 1); + } atiledata = tiledata + csiz * w * h; @@ -324,11 +342,13 @@ j2ku_srgb_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; - if (csiz[n] == 3) + if (csiz[n] == 3) { csiz[n] = 4; + } - if (shifts[n] < 0) + if (shifts[n] < 0) { offsets[n] += 1 << (-shifts[n] - 1); + } cptr += csiz[n] * w * h; } @@ -336,8 +356,9 @@ j2ku_srgb_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, for (y = 0; y < h; ++y) { const UINT8 *data[3]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; - for (n = 0; n < 3; ++n) + for (n = 0; n < 3; ++n) { data[n] = &cdata[n][csiz[n] * y * w]; + } for (x = 0; x < w; ++x) { for (n = 0; n < 3; ++n) { @@ -376,11 +397,13 @@ j2ku_sycc_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; - if (csiz[n] == 3) + if (csiz[n] == 3) { csiz[n] = 4; + } - if (shifts[n] < 0) + if (shifts[n] < 0) { offsets[n] += 1 << (-shifts[n] - 1); + } cptr += csiz[n] * w * h; } @@ -389,8 +412,9 @@ j2ku_sycc_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, const UINT8 *data[3]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; UINT8 *row_start = row; - for (n = 0; n < 3; ++n) + for (n = 0; n < 3; ++n) { data[n] = &cdata[n][csiz[n] * y * w]; + } for (x = 0; x < w; ++x) { for (n = 0; n < 3; ++n) { @@ -431,11 +455,13 @@ j2ku_srgba_rgba(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; - if (csiz[n] == 3) + if (csiz[n] == 3) { csiz[n] = 4; + } - if (shifts[n] < 0) + if (shifts[n] < 0) { offsets[n] += 1 << (-shifts[n] - 1); + } cptr += csiz[n] * w * h; } @@ -443,8 +469,9 @@ j2ku_srgba_rgba(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, for (y = 0; y < h; ++y) { const UINT8 *data[4]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; - for (n = 0; n < 4; ++n) + for (n = 0; n < 4; ++n) { data[n] = &cdata[n][csiz[n] * y * w]; + } for (x = 0; x < w; ++x) { for (n = 0; n < 4; ++n) { @@ -482,11 +509,13 @@ j2ku_sycca_rgba(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; - if (csiz[n] == 3) + if (csiz[n] == 3) { csiz[n] = 4; + } - if (shifts[n] < 0) + if (shifts[n] < 0) { offsets[n] += 1 << (-shifts[n] - 1); + } cptr += csiz[n] * w * h; } @@ -495,8 +524,9 @@ j2ku_sycca_rgba(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, const UINT8 *data[4]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; UINT8 *row_start = row; - for (n = 0; n < 4; ++n) + for (n = 0; n < 4; ++n) { data[n] = &cdata[n][csiz[n] * y * w]; + } for (x = 0; x < w; ++x) { for (n = 0; n < 4; ++n) { @@ -557,8 +587,10 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) opj_dparameters_t params; OPJ_COLOR_SPACE color_space; j2k_unpacker_t unpack = NULL; - size_t buffer_size = 0; - unsigned n; + size_t buffer_size = 0, tile_bytes = 0; + unsigned n, tile_height, tile_width; + int components; + stream = opj_stream_create(BUFFER_SIZE, OPJ_TRUE); @@ -581,10 +613,11 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) possibly support is 4GB. We can't go larger than this, because OpenJPEG truncates this value for the final box in the file, and the box lengths in OpenJPEG are currently 32 bit. */ - if (context->length < 0) + if (context->length < 0) { opj_stream_set_user_data_length(stream, 0xffffffff); - else + } else { opj_stream_set_user_data_length(stream, context->length); + } #endif /* Setup decompression context */ @@ -693,8 +726,9 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) goto quick_exit; } - if (!should_continue) + if (!should_continue) { break; + } /* Adjust the tile co-ordinates based on the reduction (OpenJPEG doesn't do this for us) */ @@ -703,8 +737,44 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) tile_info.x1 = (tile_info.x1 + correction) >> context->reduce; tile_info.y1 = (tile_info.y1 + correction) >> context->reduce; + /* Check the tile bounds; if the tile is outside the image area, + or if it has a negative width or height (i.e. the coordinates are + swapped), bail. */ + if (tile_info.x0 >= tile_info.x1 + || tile_info.y0 >= tile_info.y1 + || tile_info.x0 < (OPJ_INT32)image->x0 + || tile_info.y0 < (OPJ_INT32)image->y0 + || tile_info.x1 - image->x0 > im->xsize + || tile_info.y1 - image->y0 > im->ysize) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + /* Sometimes the tile_info.datasize we get back from openjpeg + is less than numcomps*w*h, and we overflow in the + shuffle stage */ + + tile_width = tile_info.x1 - tile_info.x0; + tile_height = tile_info.y1 - tile_info.y0; + components = tile_info.nb_comps == 3 ? 4 : tile_info.nb_comps; + if (( tile_width > UINT_MAX / components ) || + ( tile_height > UINT_MAX / components ) || + ( tile_width > UINT_MAX / (tile_height * components )) || + ( tile_height > UINT_MAX / (tile_width * components ))) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + tile_bytes = tile_width * tile_height * components; + + if (tile_bytes > tile_info.data_size) { + tile_info.data_size = tile_bytes; + } + if (buffer_size < tile_info.data_size) { - /* malloc check ok, tile_info.data_size from openjpeg */ + /* malloc check ok, overflow and tile size sanity check above */ UINT8 *new = realloc (state->buffer, tile_info.data_size); if (!new) { state->errcode = IMAGING_CODEC_MEMORY; @@ -715,6 +785,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) buffer_size = tile_info.data_size; } + if (!opj_decode_tile_data(codec, tile_info.tile_index, (OPJ_BYTE *)state->buffer, @@ -725,20 +796,6 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) goto quick_exit; } - /* Check the tile bounds; if the tile is outside the image area, - or if it has a negative width or height (i.e. the coordinates are - swapped), bail. */ - if (tile_info.x0 >= tile_info.x1 - || tile_info.y0 >= tile_info.y1 - || tile_info.x0 < image->x0 - || tile_info.y0 < image->y0 - || tile_info.x1 - image->x0 > im->xsize - || tile_info.y1 - image->y0 > im->ysize) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - unpack(image, &tile_info, state->buffer, im); } @@ -758,12 +815,15 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) } quick_exit: - if (codec) + if (codec) { opj_destroy_codec(codec); - if (image) + } + if (image) { opj_image_destroy(image); - if (stream) + } + if (stream) { opj_stream_destroy(stream); + } return -1; } @@ -778,8 +838,9 @@ ImagingJpeg2KDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t return -1; } - if (state->state == J2K_STATE_DONE || state->state == J2K_STATE_FAILED) + if (state->state == J2K_STATE_DONE || state->state == J2K_STATE_FAILED) { return -1; + } if (state->state == J2K_STATE_START) { state->state = J2K_STATE_DECODING; diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index aef1a4b94..5b18e472c 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -50,7 +50,7 @@ static OPJ_SIZE_T j2k_write(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) { ImagingCodecState state = (ImagingCodecState)p_user_data; - int result; + unsigned int result; result = _imaging_write_pyFd(state->fd, p_buffer, p_nb_bytes); @@ -106,8 +106,9 @@ j2k_pack_l(Imaging im, UINT8 *buf, unsigned x,y; for (y = 0; y < h; ++y) { UINT8 *data = (UINT8 *)(im->image[y + y0] + x0); - for (x = 0; x < w; ++x) + for (x = 0; x < w; ++x) { *ptr++ = *data++; + } } } @@ -240,8 +241,9 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) } else { rate = ((float)(components * im->xsize * im->ysize * 8) / (params->tcp_rates[n] * 8)); - if (rate > CINEMA_24_CS_LENGTH) + if (rate > CINEMA_24_CS_LENGTH) { params->tcp_rates[n] = max_rate; + } } } @@ -257,8 +259,9 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) } else { rate = ((float)(components * im->xsize * im->ysize * 8) / (params->tcp_rates[n] * 8)); - if (rate > CINEMA_48_CS_LENGTH) + if (rate > CINEMA_48_CS_LENGTH) { params->tcp_rates[n] = max_rate; + } } } @@ -396,9 +399,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) Py_ssize_t n; float *pq; - if (len) { - if (len > sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0])) + if (len > 0) { + if ((unsigned)len > sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0])) { len = sizeof(params.tcp_rates)/sizeof(params.tcp_rates[0]); + } params.tcp_numlayers = (int)len; @@ -423,8 +427,9 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) params.cp_disto_alloc = 1; } - if (context->num_resolutions) + if (context->num_resolutions) { params.numresolution = context->num_resolutions; + } if (context->cblk_width >= 4 && context->cblk_width <= 1024 && context->cblk_height >= 4 && context->cblk_height <= 1024 @@ -455,18 +460,21 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) case OPJ_CINEMA2K_24: case OPJ_CINEMA2K_48: params.cp_rsiz = OPJ_CINEMA2K; - if (params.numresolution > 6) + if (params.numresolution > 6) { params.numresolution = 6; + } break; case OPJ_CINEMA4K_24: params.cp_rsiz = OPJ_CINEMA4K; - if (params.numresolution > 7) + if (params.numresolution > 7) { params.numresolution = 7; + } break; } - if (context->cinema_mode != OPJ_OFF) + if (context->cinema_mode != OPJ_OFF) { j2k_set_cinema_params(im, components, ¶ms); + } /* Set up the reference grid in the image */ image->x0 = params.image_offset_x0; @@ -526,10 +534,12 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) unsigned ty1 = ty0 + tile_height; unsigned pixy, pixh; - if (ty0 < params.image_offset_y0) + if (ty0 < params.image_offset_y0) { ty0 = params.image_offset_y0; - if (ty1 > ysiz) + } + if (ty1 > ysiz) { ty1 = ysiz; + } pixy = ty0 - params.image_offset_y0; pixh = ty1 - ty0; @@ -540,10 +550,12 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) unsigned pixx, pixw; unsigned data_size; - if (tx0 < params.image_offset_x0) + if (tx0 < params.image_offset_x0) { tx0 = params.image_offset_x0; - if (tx1 > xsiz) + } + if (tx1 > xsiz) { tx1 = xsiz; + } pixx = tx0 - params.image_offset_x0; pixw = tx1 - tx0; @@ -572,12 +584,15 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) ret = -1; quick_exit: - if (codec) + if (codec) { opj_destroy_codec(codec); - if (image) + } + if (image) { opj_image_destroy(image); - if (stream) + } + if (stream) { opj_stream_destroy(stream); + } return ret; } @@ -585,8 +600,9 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) int ImagingJpeg2KEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - if (state->state == J2K_STATE_FAILED) + if (state->state == J2K_STATE_FAILED) { return -1; + } if (state->state == J2K_STATE_START) { @@ -611,8 +627,9 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { context->quality_layers = NULL; } - if (context->error_msg) + if (context->error_msg) { free ((void *)context->error_msg); + } context->error_msg = NULL; diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index 39d96de53..fb6112044 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -176,8 +176,9 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t by if (context->source.skip > 0) { skip_input_data(&context->cinfo, context->source.skip); - if (context->source.skip > 0) + if (context->source.skip > 0) { return context->source.pub.next_input_byte - buf; + } } switch (state->state) { @@ -193,43 +194,46 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t by } while (ok == JPEG_HEADER_TABLES_ONLY); - if (ok == JPEG_SUSPENDED) + if (ok == JPEG_SUSPENDED) { break; + } /* Decoder settings */ /* jpegmode indicates whats in the file; if not set, we'll trust the decoder */ - if (strcmp(context->jpegmode, "L") == 0) + if (strcmp(context->jpegmode, "L") == 0) { context->cinfo.jpeg_color_space = JCS_GRAYSCALE; - else if (strcmp(context->jpegmode, "RGB") == 0) + } else if (strcmp(context->jpegmode, "RGB") == 0) { context->cinfo.jpeg_color_space = JCS_RGB; - else if (strcmp(context->jpegmode, "CMYK") == 0) + } else if (strcmp(context->jpegmode, "CMYK") == 0) { context->cinfo.jpeg_color_space = JCS_CMYK; - else if (strcmp(context->jpegmode, "YCbCr") == 0) + } else if (strcmp(context->jpegmode, "YCbCr") == 0) { context->cinfo.jpeg_color_space = JCS_YCbCr; - else if (strcmp(context->jpegmode, "YCbCrK") == 0) { + } else if (strcmp(context->jpegmode, "YCbCrK") == 0) { context->cinfo.jpeg_color_space = JCS_YCCK; } /* rawmode indicates what we want from the decoder. if not set, conversions are disabled */ - if (strcmp(context->rawmode, "L") == 0) + if (strcmp(context->rawmode, "L") == 0) { context->cinfo.out_color_space = JCS_GRAYSCALE; - else if (strcmp(context->rawmode, "RGB") == 0) + } else if (strcmp(context->rawmode, "RGB") == 0) { context->cinfo.out_color_space = JCS_RGB; + } #ifdef JCS_EXTENSIONS - else if (strcmp(context->rawmode, "RGBX") == 0) + else if (strcmp(context->rawmode, "RGBX") == 0) { context->cinfo.out_color_space = JCS_EXT_RGBX; + } #endif else if (strcmp(context->rawmode, "CMYK") == 0 || - strcmp(context->rawmode, "CMYK;I") == 0) + strcmp(context->rawmode, "CMYK;I") == 0) { context->cinfo.out_color_space = JCS_CMYK; - else if (strcmp(context->rawmode, "YCbCr") == 0) + } else if (strcmp(context->rawmode, "YCbCr") == 0) { context->cinfo.out_color_space = JCS_YCbCr; - else if (strcmp(context->rawmode, "YCbCrK") == 0) + } else if (strcmp(context->rawmode, "YCbCrK") == 0) { context->cinfo.out_color_space = JCS_YCCK; - else { + } else { /* Disable decoder conversions */ context->cinfo.jpeg_color_space = JCS_UNKNOWN; context->cinfo.out_color_space = JCS_UNKNOWN; @@ -251,8 +255,9 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t by /* Set things up for decompression (this processes the entire file if necessary to return data line by line) */ - if (!jpeg_start_decompress(&context->cinfo)) + if (!jpeg_start_decompress(&context->cinfo)) { break; + } state->state++; /* fall through */ @@ -263,15 +268,17 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t by ok = 1; while (state->y < state->ysize) { ok = jpeg_read_scanlines(&context->cinfo, &state->buffer, 1); - if (ok != 1) + if (ok != 1) { break; + } state->shuffle((UINT8*) im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer, state->xsize); state->y++; } - if (ok != 1) + if (ok != 1) { break; + } state->state++; /* fall through */ @@ -280,8 +287,9 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t by /* Finish decompression */ if (!jpeg_finish_decompress(&context->cinfo)) { /* FIXME: add strictness mode test */ - if (state->y < state->ysize) + if (state->y < state->ysize) { break; + } } /* Clean up */ @@ -301,14 +309,14 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t by /* -------------------------------------------------------------------- */ int ImagingJpegDecodeCleanup(ImagingCodecState state){ - /* called to free the decompression engine when the decode terminates - due to a corrupt or truncated image - */ + /* called to free the decompression engine when the decode terminates + due to a corrupt or truncated image + */ JPEGSTATE* context = (JPEGSTATE*) state->context; - /* Clean up */ - jpeg_destroy_decompress(&context->cinfo); - return -1; + /* Clean up */ + jpeg_destroy_decompress(&context->cinfo); + return -1; } #endif diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 10ad886e0..ab730d92d 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -78,7 +78,7 @@ error(j_common_ptr cinfo) /* -------------------------------------------------------------------- */ -/* Encoder */ +/* Encoder */ /* -------------------------------------------------------------------- */ int @@ -88,24 +88,24 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) int ok; if (setjmp(context->error.setjmp_buffer)) { - /* JPEG error handler */ - jpeg_destroy_compress(&context->cinfo); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; + /* JPEG error handler */ + jpeg_destroy_compress(&context->cinfo); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; } if (!state->state) { - /* Setup compression context (very similar to the decoder) */ - context->cinfo.err = jpeg_std_error(&context->error.pub); - context->error.pub.error_exit = error; - jpeg_create_compress(&context->cinfo); - jpeg_buffer_dest(&context->cinfo, &context->destination); + /* Setup compression context (very similar to the decoder) */ + context->cinfo.err = jpeg_std_error(&context->error.pub); + context->error.pub.error_exit = error; + jpeg_create_compress(&context->cinfo); + jpeg_buffer_dest(&context->cinfo, &context->destination); context->extra_offset = 0; - /* Ready to encode */ - state->state = 1; + /* Ready to encode */ + state->state = 1; } @@ -115,212 +115,221 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) switch (state->state) { - case 1: + case 1: - context->cinfo.image_width = state->xsize; - context->cinfo.image_height = state->ysize; + context->cinfo.image_width = state->xsize; + context->cinfo.image_height = state->ysize; - switch (state->bits) { - case 8: - context->cinfo.input_components = 1; - context->cinfo.in_color_space = JCS_GRAYSCALE; - break; - case 24: - context->cinfo.input_components = 3; - if (strcmp(im->mode, "YCbCr") == 0) - context->cinfo.in_color_space = JCS_YCbCr; - else - context->cinfo.in_color_space = JCS_RGB; - break; - case 32: - context->cinfo.input_components = 4; - context->cinfo.in_color_space = JCS_CMYK; - #ifdef JCS_EXTENSIONS - if (strcmp(context->rawmode, "RGBX") == 0) - context->cinfo.in_color_space = JCS_EXT_RGBX; - #endif - break; - default: - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } + switch (state->bits) { + case 8: + context->cinfo.input_components = 1; + context->cinfo.in_color_space = JCS_GRAYSCALE; + break; + case 24: + context->cinfo.input_components = 3; + if (strcmp(im->mode, "YCbCr") == 0) { + context->cinfo.in_color_space = JCS_YCbCr; + } else { + context->cinfo.in_color_space = JCS_RGB; + } + break; + case 32: + context->cinfo.input_components = 4; + context->cinfo.in_color_space = JCS_CMYK; + #ifdef JCS_EXTENSIONS + if (strcmp(context->rawmode, "RGBX") == 0) { + context->cinfo.in_color_space = JCS_EXT_RGBX; + } + #endif + break; + default: + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } - /* Compressor configuration */ - jpeg_set_defaults(&context->cinfo); + /* Compressor configuration */ + jpeg_set_defaults(&context->cinfo); - /* Use custom quantization tables */ - if (context->qtables) { - int i; - int quality = 100; - int last_q = 0; - if (context->quality > 0) { - quality = context->quality; - } - for (i = 0; i < context->qtablesLen; i++) { - // TODO: Should add support for none baseline - jpeg_add_quant_table(&context->cinfo, i, &context->qtables[i * DCTSIZE2], - quality, TRUE); - context->cinfo.comp_info[i].quant_tbl_no = i; - last_q = i; - } - if (context->qtablesLen == 1) { - // jpeg_set_defaults created two qtables internally, but we only wanted one. - jpeg_add_quant_table(&context->cinfo, 1, &context->qtables[0], - quality, TRUE); - } - for (i = last_q; i < context->cinfo.num_components; i++) { - context->cinfo.comp_info[i].quant_tbl_no = last_q; - } - } else if (context->quality > 0) { - jpeg_set_quality(&context->cinfo, context->quality, 1); - } + /* Use custom quantization tables */ + if (context->qtables) { + int i; + int quality = 100; + int last_q = 0; + if (context->quality != -1) { + quality = context->quality; + } + for (i = 0; i < context->qtablesLen; i++) { + jpeg_add_quant_table(&context->cinfo, i, &context->qtables[i * DCTSIZE2], + quality, FALSE); + context->cinfo.comp_info[i].quant_tbl_no = i; + last_q = i; + } + if (context->qtablesLen == 1) { + // jpeg_set_defaults created two qtables internally, but we only wanted one. + jpeg_add_quant_table(&context->cinfo, 1, &context->qtables[0], + quality, FALSE); + } + for (i = last_q; i < context->cinfo.num_components; i++) { + context->cinfo.comp_info[i].quant_tbl_no = last_q; + } + } else if (context->quality != -1) { + jpeg_set_quality(&context->cinfo, context->quality, TRUE); + } - /* Set subsampling options */ - switch (context->subsampling) - { - case 0: /* 1x1 1x1 1x1 (4:4:4) : None */ - { - context->cinfo.comp_info[0].h_samp_factor = 1; - context->cinfo.comp_info[0].v_samp_factor = 1; - context->cinfo.comp_info[1].h_samp_factor = 1; - context->cinfo.comp_info[1].v_samp_factor = 1; - context->cinfo.comp_info[2].h_samp_factor = 1; - context->cinfo.comp_info[2].v_samp_factor = 1; - break; - } - case 1: /* 2x1, 1x1, 1x1 (4:2:2) : Medium */ - { - context->cinfo.comp_info[0].h_samp_factor = 2; - context->cinfo.comp_info[0].v_samp_factor = 1; - context->cinfo.comp_info[1].h_samp_factor = 1; - context->cinfo.comp_info[1].v_samp_factor = 1; - context->cinfo.comp_info[2].h_samp_factor = 1; - context->cinfo.comp_info[2].v_samp_factor = 1; - break; - } - case 2: /* 2x2, 1x1, 1x1 (4:2:0) : High */ - { - context->cinfo.comp_info[0].h_samp_factor = 2; - context->cinfo.comp_info[0].v_samp_factor = 2; - context->cinfo.comp_info[1].h_samp_factor = 1; - context->cinfo.comp_info[1].v_samp_factor = 1; - context->cinfo.comp_info[2].h_samp_factor = 1; - context->cinfo.comp_info[2].v_samp_factor = 1; - break; - } - default: - { - /* Use the lib's default */ - break; - } - } - if (context->progressive) - jpeg_simple_progression(&context->cinfo); - context->cinfo.smoothing_factor = context->smooth; - context->cinfo.optimize_coding = (boolean) context->optimize; - if (context->xdpi > 0 && context->ydpi > 0) { - context->cinfo.density_unit = 1; /* dots per inch */ - context->cinfo.X_density = context->xdpi; - context->cinfo.Y_density = context->ydpi; - } - switch (context->streamtype) { - case 1: - /* tables only -- not yet implemented */ - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - case 2: - /* image only */ - jpeg_suppress_tables(&context->cinfo, TRUE); - jpeg_start_compress(&context->cinfo, FALSE); - /* suppress extra section */ - context->extra_offset = context->extra_size; - break; - default: - /* interchange stream */ - jpeg_start_compress(&context->cinfo, TRUE); - break; - } - state->state++; - /* fall through */ + /* Set subsampling options */ + switch (context->subsampling) + { + case 0: /* 1x1 1x1 1x1 (4:4:4) : None */ + { + context->cinfo.comp_info[0].h_samp_factor = 1; + context->cinfo.comp_info[0].v_samp_factor = 1; + context->cinfo.comp_info[1].h_samp_factor = 1; + context->cinfo.comp_info[1].v_samp_factor = 1; + context->cinfo.comp_info[2].h_samp_factor = 1; + context->cinfo.comp_info[2].v_samp_factor = 1; + break; + } + case 1: /* 2x1, 1x1, 1x1 (4:2:2) : Medium */ + { + context->cinfo.comp_info[0].h_samp_factor = 2; + context->cinfo.comp_info[0].v_samp_factor = 1; + context->cinfo.comp_info[1].h_samp_factor = 1; + context->cinfo.comp_info[1].v_samp_factor = 1; + context->cinfo.comp_info[2].h_samp_factor = 1; + context->cinfo.comp_info[2].v_samp_factor = 1; + break; + } + case 2: /* 2x2, 1x1, 1x1 (4:2:0) : High */ + { + context->cinfo.comp_info[0].h_samp_factor = 2; + context->cinfo.comp_info[0].v_samp_factor = 2; + context->cinfo.comp_info[1].h_samp_factor = 1; + context->cinfo.comp_info[1].v_samp_factor = 1; + context->cinfo.comp_info[2].h_samp_factor = 1; + context->cinfo.comp_info[2].v_samp_factor = 1; + break; + } + default: + { + /* Use the lib's default */ + break; + } + } + if (context->progressive) { + jpeg_simple_progression(&context->cinfo); + } + context->cinfo.smoothing_factor = context->smooth; + context->cinfo.optimize_coding = (boolean) context->optimize; + if (context->xdpi > 0 && context->ydpi > 0) { + context->cinfo.write_JFIF_header = TRUE; + context->cinfo.density_unit = 1; /* dots per inch */ + context->cinfo.X_density = context->xdpi; + context->cinfo.Y_density = context->ydpi; + } + switch (context->streamtype) { + case 1: + /* tables only -- not yet implemented */ + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + case 2: + /* image only */ + jpeg_suppress_tables(&context->cinfo, TRUE); + jpeg_start_compress(&context->cinfo, FALSE); + /* suppress extra section */ + context->extra_offset = context->extra_size; + break; + default: + /* interchange stream */ + jpeg_start_compress(&context->cinfo, TRUE); + break; + } + state->state++; + /* fall through */ - case 2: - // check for exif len + 'APP1' header bytes - if (context->rawExifLen + 5 > context->destination.pub.free_in_buffer){ - break; - } - //add exif header - if (context->rawExifLen > 0){ - jpeg_write_marker(&context->cinfo, JPEG_APP0+1, - (unsigned char*)context->rawExif, context->rawExifLen); - } - - state->state++; - /* fall through */ - case 3: - - if (context->extra) { - /* copy extra buffer to output buffer */ - unsigned int n = context->extra_size - context->extra_offset; - if (n > context->destination.pub.free_in_buffer) - n = context->destination.pub.free_in_buffer; - memcpy(context->destination.pub.next_output_byte, - context->extra + context->extra_offset, n); - context->destination.pub.next_output_byte += n; - context->destination.pub.free_in_buffer -= n; - context->extra_offset += n; - if (context->extra_offset >= context->extra_size) - state->state++; - else + case 2: + // check for exif len + 'APP1' header bytes + if (context->rawExifLen + 5 > context->destination.pub.free_in_buffer){ break; - } else - state->state++; + } + //add exif header + if (context->rawExifLen > 0){ + jpeg_write_marker(&context->cinfo, JPEG_APP0+1, + (unsigned char*)context->rawExif, context->rawExifLen); + } - case 4: - if (1024 > context->destination.pub.free_in_buffer){ + state->state++; + /* fall through */ + case 3: + + if (context->extra) { + /* copy extra buffer to output buffer */ + unsigned int n = context->extra_size - context->extra_offset; + if (n > context->destination.pub.free_in_buffer) { + n = context->destination.pub.free_in_buffer; + } + memcpy(context->destination.pub.next_output_byte, + context->extra + context->extra_offset, n); + context->destination.pub.next_output_byte += n; + context->destination.pub.free_in_buffer -= n; + context->extra_offset += n; + if (context->extra_offset >= context->extra_size) { + state->state++; + } else { + break; + } + } else { + state->state++; + } + + case 4: + if (1024 > context->destination.pub.free_in_buffer){ + break; + } + + ok = 1; + while (state->y < state->ysize) { + state->shuffle(state->buffer, + (UINT8*) im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, state->xsize); + ok = jpeg_write_scanlines(&context->cinfo, &state->buffer, 1); + if (ok != 1) { + break; + } + state->y++; + } + + if (ok != 1) { + break; + } + state->state++; + /* fall through */ + + case 5: + + /* Finish compression */ + if (context->destination.pub.free_in_buffer < 100) { + break; + } + jpeg_finish_compress(&context->cinfo); + + /* Clean up */ + if (context->extra) { + free(context->extra); + context->extra = NULL; + } + if (context->rawExif) { + free(context->rawExif); + context->rawExif = NULL; + } + if (context->qtables) { + free(context->qtables); + context->qtables = NULL; + } + + jpeg_destroy_compress(&context->cinfo); + /* if (jerr.pub.num_warnings) return BROKEN; */ + state->errcode = IMAGING_CODEC_END; break; - } - - ok = 1; - while (state->y < state->ysize) { - state->shuffle(state->buffer, - (UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->xsize); - ok = jpeg_write_scanlines(&context->cinfo, &state->buffer, 1); - if (ok != 1) - break; - state->y++; - } - - if (ok != 1) - break; - state->state++; - /* fall through */ - - case 5: - - /* Finish compression */ - if (context->destination.pub.free_in_buffer < 100) - break; - jpeg_finish_compress(&context->cinfo); - - /* Clean up */ - if (context->extra) { - free(context->extra); - context->extra = NULL; - } - if (context->rawExif) { - free(context->rawExif); - context->rawExif = NULL; - } - if (context->qtables) { - free(context->qtables); - context->qtables = NULL; - } - - jpeg_destroy_compress(&context->cinfo); - /* if (jerr.pub.num_warnings) return BROKEN; */ - state->errcode = IMAGING_CODEC_END; - break; } diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index 5cc7795a4..e02b7b312 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -17,7 +17,7 @@ #include "Imaging.h" -#define CLIPF(v) ((v <= 0.0) ? 0 : (v >= 255.0F) ? 255 : (UINT8) v) +#define CLIPF(v) ((v <= 0.0) ? 0 : (v >= 255.0F) ? 255 : (UINT8) v) Imaging @@ -27,48 +27,52 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) int x, y; /* Assume there's enough data in the buffer */ - if (!im) - return (Imaging) ImagingError_ModeError(); + if (!im) { + return (Imaging) ImagingError_ModeError(); + } if (strcmp(mode, "L") == 0 && im->bands == 3) { - imOut = ImagingNewDirty("L", im->xsize, im->ysize); - if (!imOut) - return NULL; + imOut = ImagingNewDirty("L", im->xsize, im->ysize); + if (!imOut) { + return NULL; + } - for (y = 0; y < im->ysize; y++) { - UINT8* in = (UINT8*) im->image[y]; - UINT8* out = (UINT8*) imOut->image[y]; + for (y = 0; y < im->ysize; y++) { + UINT8* in = (UINT8*) im->image[y]; + UINT8* out = (UINT8*) imOut->image[y]; - for (x = 0; x < im->xsize; x++) { - float v = m[0]*in[0] + m[1]*in[1] + m[2]*in[2] + m[3] + 0.5; - out[x] = CLIPF(v); - in += 4; - } - } + for (x = 0; x < im->xsize; x++) { + float v = m[0]*in[0] + m[1]*in[1] + m[2]*in[2] + m[3] + 0.5; + out[x] = CLIPF(v); + in += 4; + } + } } else if (strlen(mode) == 3 && im->bands == 3) { - imOut = ImagingNewDirty(mode, im->xsize, im->ysize); - if (!imOut) - return NULL; + imOut = ImagingNewDirty(mode, im->xsize, im->ysize); + if (!imOut) { + return NULL; + } - for (y = 0; y < im->ysize; y++) { - UINT8* in = (UINT8*) im->image[y]; - UINT8* out = (UINT8*) imOut->image[y]; + for (y = 0; y < im->ysize; y++) { + UINT8* in = (UINT8*) im->image[y]; + UINT8* out = (UINT8*) imOut->image[y]; - for (x = 0; x < im->xsize; x++) { - float v0 = m[0]*in[0] + m[1]*in[1] + m[2]*in[2] + m[3] + 0.5; - float v1 = m[4]*in[0] + m[5]*in[1] + m[6]*in[2] + m[7] + 0.5; - float v2 = m[8]*in[0] + m[9]*in[1] + m[10]*in[2] + m[11] + 0.5; - out[0] = CLIPF(v0); - out[1] = CLIPF(v1); - out[2] = CLIPF(v2); - in += 4; out += 4; - } - } - } else - return (Imaging) ImagingError_ModeError(); + for (x = 0; x < im->xsize; x++) { + float v0 = m[0]*in[0] + m[1]*in[1] + m[2]*in[2] + m[3] + 0.5; + float v1 = m[4]*in[0] + m[5]*in[1] + m[6]*in[2] + m[7] + 0.5; + float v2 = m[8]*in[0] + m[9]*in[1] + m[10]*in[2] + m[11] + 0.5; + out[0] = CLIPF(v0); + out[1] = CLIPF(v1); + out[2] = CLIPF(v2); + in += 4; out += 4; + } + } + } else { + return (Imaging) ImagingError_ModeError(); + } return imOut; } diff --git a/src/libImaging/ModeFilter.c b/src/libImaging/ModeFilter.c index 5237d0732..b1cf7442c 100644 --- a/src/libImaging/ModeFilter.c +++ b/src/libImaging/ModeFilter.c @@ -25,12 +25,14 @@ ImagingModeFilter(Imaging im, int size) UINT8 maxpixel; int histogram[256]; - if (!im || im->bands != 1 || im->type != IMAGING_TYPE_UINT8) + if (!im || im->bands != 1 || im->type != IMAGING_TYPE_UINT8) { return (Imaging) ImagingError_ModeError(); + } imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); - if (!imOut) + if (!imOut) { return NULL; + } size = size / 2; @@ -46,27 +48,32 @@ ImagingModeFilter(Imaging im, int size) the added complexity... */ memset(histogram, 0, sizeof(histogram)); - for (yy = y - size; yy <= y + size; yy++) + for (yy = y - size; yy <= y + size; yy++) { if (yy >= 0 && yy < imOut->ysize) { UINT8* in = &IMAGING_PIXEL_L(im, 0, yy); - for (xx = x - size; xx <= x + size; xx++) - if (xx >= 0 && xx < imOut->xsize) + for (xx = x - size; xx <= x + size; xx++) { + if (xx >= 0 && xx < imOut->xsize) { histogram[in[xx]]++; + } + } } + } /* find most frequent pixel value in this region */ maxpixel = 0; maxcount = histogram[maxpixel]; - for (i = 1; i < 256; i++) + for (i = 1; i < 256; i++) { if (histogram[i] > maxcount) { maxcount = histogram[i]; maxpixel = (UINT8) i; } + } - if (maxcount > 2) + if (maxcount > 2) { out[x] = maxpixel; - else + } else { out[x] = IMAGING_PIXEL_L(im, x, y); + } } diff --git a/src/libImaging/Negative.c b/src/libImaging/Negative.c index 4dedcb245..09c946bbe 100644 --- a/src/libImaging/Negative.c +++ b/src/libImaging/Negative.c @@ -8,7 +8,7 @@ * FIXME: Maybe this should be implemented using ImagingPoint() * * history: - * 95-11-27 fl: Created + * 95-11-27 fl: Created * * Copyright (c) Fredrik Lundh 1995. * Copyright (c) Secret Labs AB 1997. @@ -26,16 +26,20 @@ ImagingNegative(Imaging im) Imaging imOut; int x, y; - if (!im) - return (Imaging) ImagingError_ModeError(); + if (!im) { + return (Imaging) ImagingError_ModeError(); + } imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); - if (!imOut) - return NULL; + if (!imOut) { + return NULL; + } - for (y = 0; y < im->ysize; y++) - for (x = 0; x < im->linesize; x++) - imOut->image[y][x] = ~im->image[y][x]; + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->linesize; x++) { + imOut->image[y][x] = ~im->image[y][x]; + } + } return imOut; } diff --git a/src/libImaging/Offset.c b/src/libImaging/Offset.c index b3d9425fb..29f038153 100644 --- a/src/libImaging/Offset.c +++ b/src/libImaging/Offset.c @@ -5,7 +5,7 @@ * offset an image in x and y directions * * history: - * 96-07-22 fl: Created + * 96-07-22 fl: Created * 98-11-01 cgw@pgt.com: Fixed negative-array index bug * * Copyright (c) Fredrik Lundh 1996. @@ -24,38 +24,44 @@ ImagingOffset(Imaging im, int xoffset, int yoffset) int x, y; Imaging imOut; - if (!im) - return (Imaging) ImagingError_ModeError(); + if (!im) { + return (Imaging) ImagingError_ModeError(); + } imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); - if (!imOut) - return NULL; + if (!imOut) { + return NULL; + } ImagingCopyPalette(imOut, im); /* make offsets positive to avoid negative coordinates */ xoffset %= im->xsize; xoffset = im->xsize - xoffset; - if (xoffset < 0) - xoffset += im->xsize; + if (xoffset < 0) { + xoffset += im->xsize; + } yoffset %= im->ysize; yoffset = im->ysize - yoffset; - if (yoffset < 0) - yoffset += im->ysize; + if (yoffset < 0) { + yoffset += im->ysize; + } -#define OFFSET(image)\ - for (y = 0; y < im->ysize; y++)\ - for (x = 0; x < im->xsize; x++) {\ - int yi = (y + yoffset) % im->ysize;\ - int xi = (x + xoffset) % im->xsize;\ - imOut->image[y][x] = im->image[yi][xi];\ - } +#define OFFSET(image)\ + for (y = 0; y < im->ysize; y++) {\ + for (x = 0; x < im->xsize; x++) {\ + int yi = (y + yoffset) % im->ysize;\ + int xi = (x + xoffset) % im->xsize;\ + imOut->image[y][x] = im->image[yi][xi];\ + }\ + } - if (im->image8) - OFFSET(image8) - else - OFFSET(image32) + if (im->image8) { + OFFSET(image8) + } else { + OFFSET(image32) + } return imOut; } diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index eaa276af4..0be7ad8c9 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -80,16 +80,18 @@ pack1(UINT8* out, const UINT8* in, int pixels) /* bilevel (black is 0) */ b = 0; m = 128; for (i = 0; i < pixels; i++) { - if (in[i] != 0) + if (in[i] != 0) { b |= m; + } m >>= 1; if (m == 0) { *out++ = b; b = 0; m = 128; } } - if (m != 128) + if (m != 128) { *out++ = b; + } } static void @@ -99,16 +101,18 @@ pack1I(UINT8* out, const UINT8* in, int pixels) /* bilevel (black is 1) */ b = 0; m = 128; for (i = 0; i < pixels; i++) { - if (in[i] == 0) + if (in[i] == 0) { b |= m; + } m >>= 1; if (m == 0) { *out++ = b; b = 0; m = 128; } } - if (m != 128) + if (m != 128) { *out++ = b; + } } static void @@ -118,16 +122,18 @@ pack1R(UINT8* out, const UINT8* in, int pixels) /* bilevel, lsb first (black is 0) */ b = 0; m = 1; for (i = 0; i < pixels; i++) { - if (in[i] != 0) + if (in[i] != 0) { b |= m; + } m <<= 1; if (m == 256){ *out++ = b; b = 0; m = 1; } } - if (m != 1) + if (m != 1) { *out++ = b; + } } static void @@ -137,16 +143,18 @@ pack1IR(UINT8* out, const UINT8* in, int pixels) /* bilevel, lsb first (black is 1) */ b = 0; m = 1; for (i = 0; i < pixels; i++) { - if (in[i] == 0) + if (in[i] == 0) { b |= m; + } m <<= 1; if (m == 256){ *out++ = b; b = 0; m = 1; } } - if (m != 1) + if (m != 1) { *out++ = b; + } } static void @@ -154,8 +162,9 @@ pack1L(UINT8* out, const UINT8* in, int pixels) { int i; /* bilevel, stored as bytes */ - for (i = 0; i < pixels; i++) + for (i = 0; i < pixels; i++) { out[i] = (in[i] != 0) ? 255 : 0; + } } static void @@ -167,8 +176,9 @@ packP4(UINT8* out, const UINT8* in, int pixels) in += 2; pixels -= 2; } - if (pixels) + if (pixels) { out[0] = (in[0] << 4); + } } static void @@ -407,12 +417,13 @@ packI16B(UINT8* out, const UINT8* in_, int pixels) for (i = 0; i < pixels; i++) { INT32 in; memcpy(&in, in_, sizeof(in)); - if (in <= 0) + if (in <= 0) { tmp_ = 0; - else if (in > 65535) + } else if (in > 65535) { tmp_ = 65535; - else + } else { tmp_ = in; + } C16B; out += 2; in_ += sizeof(in); } @@ -496,40 +507,45 @@ copy4I(UINT8* out, const UINT8* in, int pixels) { /* RGBA, CMYK quadruples, inverted */ int i; - for (i = 0; i < pixels*4; i++) + for (i = 0; i < pixels*4; i++) { out[i] = ~in[i]; + } } static void band0(UINT8* out, const UINT8* in, int pixels) { int i; - for (i = 0; i < pixels; i++, in += 4) + for (i = 0; i < pixels; i++, in += 4) { out[i] = in[0]; + } } static void band1(UINT8* out, const UINT8* in, int pixels) { int i; - for (i = 0; i < pixels; i++, in += 4) + for (i = 0; i < pixels; i++, in += 4) { out[i] = in[1]; + } } static void band2(UINT8* out, const UINT8* in, int pixels) { int i; - for (i = 0; i < pixels; i++, in += 4) + for (i = 0; i < pixels; i++, in += 4) { out[i] = in[2]; + } } static void band3(UINT8* out, const UINT8* in, int pixels) { int i; - for (i = 0; i < pixels; i++, in += 4) + for (i = 0; i < pixels; i++, in += 4) { out[i] = in[3]; + } } static struct { @@ -555,6 +571,9 @@ static struct { {"LA", "LA", 16, packLA}, {"LA", "LA;L", 16, packLAL}, + /* greyscale w. alpha premultiplied */ + {"La", "La", 16, packLA}, + /* palette */ {"P", "P;1", 1, pack1}, {"P", "P;2", 2, packP2}, @@ -670,12 +689,14 @@ ImagingFindPacker(const char* mode, const char* rawmode, int* bits_out) int i; /* find a suitable pixel packer */ - for (i = 0; packers[i].rawmode; i++) + for (i = 0; packers[i].rawmode; i++) { if (strcmp(packers[i].mode, mode) == 0 && strcmp(packers[i].rawmode, rawmode) == 0) { - if (bits_out) + if (bits_out) { *bits_out = packers[i].bits; + } return packers[i].pack; } + } return NULL; } diff --git a/src/libImaging/PackDecode.c b/src/libImaging/PackDecode.c index ef54f3c9a..34671828a 100644 --- a/src/libImaging/PackDecode.c +++ b/src/libImaging/PackDecode.c @@ -5,7 +5,7 @@ * decoder for PackBits image data. * * history: - * 96-04-19 fl Created + * 96-04-19 fl Created * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -18,7 +18,7 @@ int ImagingPackbitsDecode(Imaging im, ImagingCodecState state, - UINT8* buf, Py_ssize_t bytes) + UINT8* buf, Py_ssize_t bytes) { UINT8 n; UINT8* ptr; @@ -28,65 +28,68 @@ ImagingPackbitsDecode(Imaging im, ImagingCodecState state, for (;;) { - if (bytes < 1) - return ptr - buf; + if (bytes < 1) { + return ptr - buf; + } - if (ptr[0] & 0x80) { + if (ptr[0] & 0x80) { - if (ptr[0] == 0x80) { - /* Nop */ - ptr++; bytes--; - continue; - } + if (ptr[0] == 0x80) { + /* Nop */ + ptr++; bytes--; + continue; + } - /* Run */ - if (bytes < 2) - return ptr - buf; + /* Run */ + if (bytes < 2) { + return ptr - buf; + } - for (n = 257 - ptr[0]; n > 0; n--) { - if (state->x >= state->bytes) { - /* state->errcode = IMAGING_CODEC_OVERRUN; */ - break; - } - state->buffer[state->x++] = ptr[1]; - } + for (n = 257 - ptr[0]; n > 0; n--) { + if (state->x >= state->bytes) { + /* state->errcode = IMAGING_CODEC_OVERRUN; */ + break; + } + state->buffer[state->x++] = ptr[1]; + } - ptr += 2; bytes -= 2; + ptr += 2; bytes -= 2; - } else { + } else { - /* Literal */ - n = ptr[0]+2; + /* Literal */ + n = ptr[0]+2; - if (bytes < n) - return ptr - buf; + if (bytes < n) { + return ptr - buf; + } - for (i = 1; i < n; i++) { - if (state->x >= state->bytes) { - /* state->errcode = IMAGING_CODEC_OVERRUN; */ - break; - } - state->buffer[state->x++] = ptr[i]; - } + for (i = 1; i < n; i++) { + if (state->x >= state->bytes) { + /* state->errcode = IMAGING_CODEC_OVERRUN; */ + break; + } + state->buffer[state->x++] = ptr[i]; + } - ptr += n; bytes -= n; + ptr += n; bytes -= n; - } + } - if (state->x >= state->bytes) { + if (state->x >= state->bytes) { - /* Got a full line, unpack it */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->buffer, - state->xsize); + /* Got a full line, unpack it */ + state->shuffle((UINT8*) im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, state->buffer, + state->xsize); - state->x = 0; + state->x = 0; - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } } } diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index 7aee6e8ee..ac548f50c 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -30,12 +30,14 @@ ImagingPaletteNew(const char* mode) int i; ImagingPalette palette; - if (strcmp(mode, "RGB") && strcmp(mode, "RGBA")) + if (strcmp(mode, "RGB") && strcmp(mode, "RGBA")) { return (ImagingPalette) ImagingError_ModeError(); + } palette = calloc(1, sizeof(struct ImagingPaletteInstance)); - if (!palette) + if (!palette) { return (ImagingPalette) ImagingError_MemoryError(); + } strncpy(palette->mode, mode, IMAGING_MODE_LENGTH-1); palette->mode[IMAGING_MODE_LENGTH-1] = 0; @@ -60,8 +62,9 @@ ImagingPaletteNewBrowser(void) ImagingPalette palette; palette = ImagingPaletteNew("RGB"); - if (!palette) + if (!palette) { return NULL; + } /* Blank out unused entries */ /* FIXME: Add 10-level windows palette here? */ @@ -74,14 +77,16 @@ ImagingPaletteNewBrowser(void) /* Simple 6x6x6 colour cube */ - for (b = 0; b < 256; b += 51) - for (g = 0; g < 256; g += 51) + for (b = 0; b < 256; b += 51) { + for (g = 0; g < 256; g += 51) { for (r = 0; r < 256; r += 51) { palette->palette[i*4+0] = r; palette->palette[i*4+1] = g; palette->palette[i*4+2] = b; i++; } + } + } /* Blank out unused entries */ /* FIXME: add 30-level greyscale wedge here? */ @@ -102,12 +107,14 @@ ImagingPaletteDuplicate(ImagingPalette palette) ImagingPalette new_palette; - if (!palette) + if (!palette) { return NULL; + } /* malloc check ok, small constant allocation */ new_palette = malloc(sizeof(struct ImagingPaletteInstance)); - if (!new_palette) + if (!new_palette) { return (ImagingPalette) ImagingError_MemoryError(); + } memcpy(new_palette, palette, sizeof(struct ImagingPaletteInstance)); @@ -123,8 +130,9 @@ ImagingPaletteDelete(ImagingPalette palette) /* Destroy palette object */ if (palette) { - if (palette->cache) + if (palette->cache) { free(palette->cache); + } free(palette); } } @@ -209,8 +217,9 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) tmax += (b <= bc) ? BDIST(b, b1) : BDIST(b, b0); dmin[i] = tmin; - if (tmax < dmax) + if (tmax < dmax) { dmax = tmax; /* keep the smallest max distance only */ + } } @@ -220,10 +229,11 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) * all slots in that box. We only check boxes for which the min * distance is less than or equal the smallest max distance */ - for (i = 0; i < BOXVOLUME; i++) + for (i = 0; i < BOXVOLUME; i++) { d[i] = (unsigned int) ~0; + } - for (i = 0; i < 256; i++) + for (i = 0; i < 256; i++) { if (dmin[i] <= dmax) { @@ -262,6 +272,7 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) rx += 2 * RSTEP * RSTEP; } } + } /* Step 3 -- Update cache */ @@ -269,10 +280,13 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) * cache slot in the box. Update the cache. */ j = 0; - for (r = r0; r < r1; r+=4) - for (g = g0; g < g1; g+=4) - for (b = b0; b < b1; b+=4) + for (r = r0; r < r1; r+=4) { + for (g = g0; g < g1; g+=4) { + for (b = b0; b < b1; b+=4) { ImagingPaletteCache(palette, r, g, b) = c[j++]; + } + } + } } @@ -297,8 +311,9 @@ ImagingPaletteCachePrepare(ImagingPalette palette) } /* Mark all entries as empty */ - for (i = 0; i < entries; i++) + for (i = 0; i < entries; i++) { palette->cache[i] = 0x100; + } } diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 0bda25739..b89534423 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -37,8 +37,9 @@ paste(Imaging imOut, Imaging imIn, int dx, int dy, int sx, int sy, xsize *= pixelsize; - for (y = 0; y < ysize; y++) + for (y = 0; y < ysize; y++) { memcpy(imOut->image[y+dy]+dx, imIn->image[y+sy]+sx, xsize); + } } static inline void @@ -57,8 +58,9 @@ paste_mask_1(Imaging imOut, Imaging imIn, Imaging imMask, UINT8* in = imIn->image8[y+sy]+sx; UINT8* mask = imMask->image8[y+sy]+sx; for (x = 0; x < xsize; x++) { - if (*mask++) + if (*mask++) { *out = *in; + } out++, in++; } } @@ -70,8 +72,9 @@ paste_mask_1(Imaging imOut, Imaging imIn, Imaging imMask, INT32* in = imIn->image32[y+sy]+sx; UINT8* mask = imMask->image8[y+sy]+sx; for (x = 0; x < xsize; x++) { - if (*mask++) + if (*mask++) { *out = *in; + } out++, in++; } } @@ -231,17 +234,22 @@ ImagingPaste(Imaging imOut, Imaging imIn, Imaging imMask, /* Determine which region to copy */ sx0 = sy0 = 0; - if (dx0 < 0) + if (dx0 < 0) { xsize += dx0, sx0 = -dx0, dx0 = 0; - if (dx0 + xsize > imOut->xsize) + } + if (dx0 + xsize > imOut->xsize) { xsize = imOut->xsize - dx0; - if (dy0 < 0) + } + if (dy0 < 0) { ysize += dy0, sy0 = -dy0, dy0 = 0; - if (dy0 + ysize > imOut->ysize) + } + if (dy0 + ysize > imOut->ysize) { ysize = imOut->ysize - dy0; + } - if (xsize <= 0 || ysize <= 0) + if (xsize <= 0 || ysize <= 0) { return 0; + } if (!imMask) { ImagingSectionEnter(&cookie); @@ -297,15 +305,17 @@ fill(Imaging imOut, const void* ink_, int dx, int dy, dx *= pixelsize; xsize *= pixelsize; - for (y = 0; y < ysize; y++) + for (y = 0; y < ysize; y++) { memset(imOut->image[y+dy]+dx, ink8, xsize); + } } else { for (y = 0; y < ysize; y++) { INT32* out = imOut->image32[y+dy]+dx; - for (x = 0; x < xsize; x++) + for (x = 0; x < xsize; x++) { out[x] = ink32; + } } } @@ -331,8 +341,9 @@ fill_mask_1(Imaging imOut, const void* ink_, Imaging imMask, UINT8* out = imOut->image8[y+dy]+dx; UINT8* mask = imMask->image8[y+sy]+sx; for (x = 0; x < xsize; x++) { - if (*mask++) + if (*mask++) { *out = ink8; + } out++; } } @@ -343,8 +354,9 @@ fill_mask_1(Imaging imOut, const void* ink_, Imaging imMask, INT32* out = imOut->image32[y+dy]+dx; UINT8* mask = imMask->image8[y+sy]+sx; for (x = 0; x < xsize; x++) { - if (*mask++) + if (*mask++) { *out = ink32; + } out++; } } @@ -379,9 +391,19 @@ fill_mask_L(Imaging imOut, const UINT8* ink, Imaging imMask, UINT8* mask = (UINT8*) imMask->image[y+sy]+sx; for (x = 0; x < xsize; x++) { for (i = 0; i < pixelsize; i++) { - *out = BLEND(*mask, *out, ink[i], tmp1); - out++; + UINT8 channel_mask = *mask; + if (( + strcmp(imOut->mode, "RGBa") == 0 || + strcmp(imOut->mode, "RGBA") == 0 || + strcmp(imOut->mode, "La") == 0 || + strcmp(imOut->mode, "LA") == 0 || + strcmp(imOut->mode, "PA") == 0 + ) && i != 3) { + channel_mask = 255 - (255 - channel_mask) * (1 - (255 - out[3]) / 255); + } + out[i] = BLEND(channel_mask, out[i], ink[i], tmp1); } + out += pixelsize; mask++; } } @@ -494,17 +516,22 @@ ImagingFill2(Imaging imOut, const void* ink, Imaging imMask, /* Determine which region to fill */ sx0 = sy0 = 0; - if (dx0 < 0) + if (dx0 < 0) { xsize += dx0, sx0 = -dx0, dx0 = 0; - if (dx0 + xsize > imOut->xsize) + } + if (dx0 + xsize > imOut->xsize) { xsize = imOut->xsize - dx0; - if (dy0 < 0) + } + if (dy0 < 0) { ysize += dy0, sy0 = -dy0, dy0 = 0; - if (dy0 + ysize > imOut->ysize) + } + if (dy0 + ysize > imOut->ysize) { ysize = imOut->ysize - dy0; + } - if (xsize <= 0 || ysize <= 0) + if (xsize <= 0 || ysize <= 0) { return 0; + } if (!imMask) { ImagingSectionEnter(&cookie); diff --git a/src/libImaging/PcdDecode.c b/src/libImaging/PcdDecode.c index 8ff264edf..ff192a174 100644 --- a/src/libImaging/PcdDecode.c +++ b/src/libImaging/PcdDecode.c @@ -5,13 +5,13 @@ * decoder for uncompressed PCD image data. * * history: - * 96-05-10 fl Created - * 96-05-18 fl New tables - * 97-01-25 fl Use PhotoYCC unpacker + * 96-05-10 fl Created + * 96-05-18 fl New tables + * 97-01-25 fl Use PhotoYCC unpacker * * notes: - * This driver supports uncompressed PCD modes only - * (resolutions up to 768x512). + * This driver supports uncompressed PCD modes only + * (resolutions up to 768x512). * * Copyright (c) Fredrik Lundh 1996-97. * Copyright (c) Secret Labs AB 1997. @@ -37,42 +37,45 @@ ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt for (;;) { - /* We need data for two full lines before we can do anything */ - if (bytes < chunk) - return ptr - buf; + /* We need data for two full lines before we can do anything */ + if (bytes < chunk) { + return ptr - buf; + } - /* Unpack first line */ - out = state->buffer; - for (x = 0; x < state->xsize; x++) { - out[0] = ptr[x]; - out[1] = ptr[(x+4*state->xsize)/2]; - out[2] = ptr[(x+5*state->xsize)/2]; - out += 3; - } + /* Unpack first line */ + out = state->buffer; + for (x = 0; x < state->xsize; x++) { + out[0] = ptr[x]; + out[1] = ptr[(x+4*state->xsize)/2]; + out[2] = ptr[(x+5*state->xsize)/2]; + out += 3; + } - state->shuffle((UINT8*) im->image[state->y], - state->buffer, state->xsize); + state->shuffle((UINT8*) im->image[state->y], + state->buffer, state->xsize); - if (++state->y >= state->ysize) - return -1; /* This can hardly happen */ + if (++state->y >= state->ysize) { + return -1; /* This can hardly happen */ + } - /* Unpack second line */ - out = state->buffer; - for (x = 0; x < state->xsize; x++) { - out[0] = ptr[x+state->xsize]; - out[1] = ptr[(x+4*state->xsize)/2]; - out[2] = ptr[(x+5*state->xsize)/2]; - out += 3; - } + /* Unpack second line */ + out = state->buffer; + for (x = 0; x < state->xsize; x++) { + out[0] = ptr[x+state->xsize]; + out[1] = ptr[(x+4*state->xsize)/2]; + out[2] = ptr[(x+5*state->xsize)/2]; + out += 3; + } - state->shuffle((UINT8*) im->image[state->y], - state->buffer, state->xsize); + state->shuffle((UINT8*) im->image[state->y], + state->buffer, state->xsize); - if (++state->y >= state->ysize) - return -1; + if (++state->y >= state->ysize) { + return -1; + } - ptr += chunk; - bytes -= chunk; + ptr += chunk; + bytes -= chunk; } } diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index 67dcc1e08..eb0fecc83 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -5,7 +5,7 @@ * decoder for PCX image data. * * history: - * 95-09-14 fl Created + * 95-09-14 fl Created * * Copyright (c) Fredrik Lundh 1995. * Copyright (c) Secret Labs AB 1997. @@ -22,7 +22,7 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt UINT8 n; UINT8* ptr; - if (strcmp(im->mode, "1") == 0 && state->xsize > state->bytes * 8) { + if ((state->xsize * state->bits + 7) / 8 > state->bytes) { state->errcode = IMAGING_CODEC_OVERRUN; return -1; } @@ -31,59 +31,61 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt for (;;) { - if (bytes < 1) - return ptr - buf; + if (bytes < 1) { + return ptr - buf; + } - if ((*ptr & 0xC0) == 0xC0) { + if ((*ptr & 0xC0) == 0xC0) { - /* Run */ - if (bytes < 2) - return ptr - buf; + /* Run */ + if (bytes < 2) { + return ptr - buf; + } - n = ptr[0] & 0x3F; + n = ptr[0] & 0x3F; - while (n > 0) { - if (state->x >= state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - break; - } - state->buffer[state->x++] = ptr[1]; - n--; - } + while (n > 0) { + if (state->x >= state->bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + break; + } + state->buffer[state->x++] = ptr[1]; + n--; + } - ptr += 2; bytes -= 2; + ptr += 2; bytes -= 2; - } else { + } else { - /* Literal */ - state->buffer[state->x++] = ptr[0]; - ptr++; bytes--; + /* Literal */ + state->buffer[state->x++] = ptr[0]; + ptr++; bytes--; - } + } - if (state->x >= state->bytes) { - if (state->bytes % state->xsize && state->bytes > state->xsize) { - int bands = state->bytes / state->xsize; - int stride = state->bytes / bands; - int i; - for (i=1; i< bands; i++) { // note -- skipping first band - memmove(&state->buffer[i*state->xsize], - &state->buffer[i*stride], - state->xsize); + if (state->x >= state->bytes) { + if (state->bytes % state->xsize && state->bytes > state->xsize) { + int bands = state->bytes / state->xsize; + int stride = state->bytes / bands; + int i; + for (i=1; i< bands; i++) { // note -- skipping first band + memmove(&state->buffer[i*state->xsize], + &state->buffer[i*stride], + state->xsize); + } + } + /* Got a full line, unpack it */ + state->shuffle((UINT8*) im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, state->buffer, + state->xsize); + + state->x = 0; + + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; } } - /* Got a full line, unpack it */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->buffer, - state->xsize); - - state->x = 0; - - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } } } diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index 9b4bf6b75..b70840b07 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -35,8 +35,9 @@ im_point_8_8(Imaging imOut, Imaging imIn, im_point_context* context) for (y = 0; y < imIn->ysize; y++) { UINT8* in = imIn->image8[y]; UINT8* out = imOut->image8[y]; - for (x = 0; x < imIn->xsize; x++) + for (x = 0; x < imIn->xsize; x++) { out[x] = table[in[x]]; + } } } @@ -103,8 +104,9 @@ im_point_8_32(Imaging imOut, Imaging imIn, im_point_context* context) for (y = 0; y < imIn->ysize; y++) { UINT8* in = imIn->image8[y]; INT32* out = imOut->image32[y]; - for (x = 0; x < imIn->xsize; x++) + for (x = 0; x < imIn->xsize; x++) { memcpy(out + x, table + in[x] * sizeof(INT32), sizeof(INT32)); + } } } @@ -119,10 +121,11 @@ im_point_32_8(Imaging imOut, Imaging imIn, im_point_context* context) UINT8* out = imOut->image8[y]; for (x = 0; x < imIn->xsize; x++) { int v = in[x]; - if (v < 0) + if (v < 0) { v = 0; - else if (v > 65535) + } else if (v > 65535) { v = 65535; + } out[x] = table[v]; } } @@ -138,21 +141,26 @@ ImagingPoint(Imaging imIn, const char* mode, const void* table) im_point_context context; void (*point)(Imaging imIn, Imaging imOut, im_point_context* context); - if (!imIn) - return (Imaging) ImagingError_ModeError(); + if (!imIn) { + return (Imaging) ImagingError_ModeError(); + } - if (!mode) + if (!mode) { mode = imIn->mode; + } if (imIn->type != IMAGING_TYPE_UINT8) { - if (imIn->type != IMAGING_TYPE_INT32 || strcmp(mode, "L") != 0) + if (imIn->type != IMAGING_TYPE_INT32 || strcmp(mode, "L") != 0) { goto mode_mismatch; - } else if (!imIn->image8 && strcmp(imIn->mode, mode) != 0) + } + } else if (!imIn->image8 && strcmp(imIn->mode, mode) != 0) { goto mode_mismatch; + } imOut = ImagingNew(mode, imIn->xsize, imIn->ysize); - if (!imOut) - return NULL; + if (!imOut) { + return NULL; + } /* find appropriate handler */ if (imIn->type == IMAGING_TYPE_UINT8) { @@ -175,10 +183,12 @@ ImagingPoint(Imaging imIn, const char* mode, const void* table) point = im_point_8_8; break; } - } else + } else { point = im_point_8_32; - } else + } + } else { point = im_point_32_8; + } ImagingCopyPalette(imOut, imIn); @@ -209,12 +219,14 @@ ImagingPointTransform(Imaging imIn, double scale, double offset) if (!imIn || (strcmp(imIn->mode, "I") != 0 && strcmp(imIn->mode, "I;16") != 0 && - strcmp(imIn->mode, "F") != 0)) - return (Imaging) ImagingError_ModeError(); + strcmp(imIn->mode, "F") != 0)) { + return (Imaging) ImagingError_ModeError(); + } imOut = ImagingNew(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) - return NULL; + if (!imOut) { + return NULL; + } switch (imIn->type) { case IMAGING_TYPE_INT32: @@ -223,8 +235,9 @@ ImagingPointTransform(Imaging imIn, double scale, double offset) INT32* in = imIn->image32[y]; INT32* out = imOut->image32[y]; /* FIXME: add clipping? */ - for (x = 0; x < imIn->xsize; x++) + for (x = 0; x < imIn->xsize; x++) { out[x] = in[x] * scale + offset; + } } ImagingSectionLeave(&cookie); break; @@ -233,8 +246,9 @@ ImagingPointTransform(Imaging imIn, double scale, double offset) for (y = 0; y < imIn->ysize; y++) { FLOAT32* in = (FLOAT32*) imIn->image32[y]; FLOAT32* out = (FLOAT32*) imOut->image32[y]; - for (x = 0; x < imIn->xsize; x++) + for (x = 0; x < imIn->xsize; x++) { out[x] = in[x] * scale + offset; + } } ImagingSectionLeave(&cookie); break; @@ -254,7 +268,7 @@ ImagingPointTransform(Imaging imIn, double scale, double offset) } ImagingSectionLeave(&cookie); break; - } + } /* FALL THROUGH */ default: ImagingDelete(imOut); diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index b94dc6e1d..6c9f8d9b7 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -157,7 +157,9 @@ create_pixel_hash(Pixel *pixelData,uint32_t nPixels) /* malloc check ok, small constant allocation */ d=malloc(sizeof(PixelHashData)); - if (!d) return NULL; + if (!d) { + return NULL; + } hash=hashtable_new(pixel_hash,pixel_cmp); hashtable_set_user_data(hash,d); d->scale=0; @@ -197,7 +199,9 @@ static void destroy_pixel_hash(HashTable *hash) { PixelHashData *d=(PixelHashData *)hashtable_get_user_data(hash); - if (d) free(d); + if (d) { + free(d); + } hashtable_free(hash); } @@ -214,7 +218,9 @@ static int compute_box_volume(BoxNode *b) { unsigned char rl,rh,gl,gh,bl,bh; - if (b->volume>=0) return b->volume; + if (b->volume>=0) { + return b->volume; + } if (!b->head[0]) { b->volume=0; } else { @@ -242,7 +248,9 @@ hash_to_list(const HashTable *h, const Pixel pixel, const uint32_t count, void * /* malloc check ok, small constant allocation */ p=malloc(sizeof(PixelList)); - if (!p) return; + if (!p) { + return; + } p->flag=0; p->p=q; @@ -250,7 +258,9 @@ hash_to_list(const HashTable *h, const Pixel pixel, const uint32_t count, void * for (i=0;i<3;i++) { p->next[i]=pl[i]; p->prev[i]=NULL; - if (pl[i]) pl[i]->prev[i]=p; + if (pl[i]) { + pl[i]->prev[i]=p; + } pl[i]=p; } } @@ -268,7 +278,9 @@ mergesort_pixels(PixelList *head, int i) } for (c=t=head;c&&t;c=c->next[i],t=(t->next[i])?t->next[i]->next[i]:NULL); if (c) { - if (c->prev[i]) c->prev[i]->next[i]=NULL; + if (c->prev[i]) { + c->prev[i]->next[i]=NULL; + } c->prev[i]=NULL; } a=mergesort_pixels(head,i); @@ -285,9 +297,13 @@ mergesort_pixels(PixelList *head, int i) } c->prev[i]=p; c->next[i]=NULL; - if (p) p->next[i]=c; + if (p) { + p->next[i]=c; + } p=c; - if (!head) head=c; + if (!head) { + head=c; + } } if (a) { c->next[i]=a; @@ -442,17 +458,29 @@ splitlists(PixelList *h[3], for (c=h[i];c;c=n) { n=c->next[i]; if (c->flag) { /* move pixel to right list*/ - if (r) r->next[i]=c; else nh[1][i]=c; + if (r) { + r->next[i]=c; + } else { + nh[1][i]=c; + } c->prev[i]=r; r=c; } else { /* move pixel to left list */ - if (l) l->next[i]=c; else nh[0][i]=c; + if (l) { + l->next[i]=c; + } else { + nh[0][i]=c; + } c->prev[i]=l; l=c; } } - if (l) l->next[i]=NULL; - if (r) r->next[i]=NULL; + if (l) { + l->next[i]=NULL; + } + if (r) { + r->next[i]=NULL; + } nt[0][i]=l; nt[1][i]=r; } @@ -661,8 +689,12 @@ static void free_box_tree(BoxNode *n) { PixelList *p,*pp; - if (n->l) free_box_tree(n->l); - if (n->r) free_box_tree(n->r); + if (n->l) { + free_box_tree(n->l); + } + if (n->r) { + free_box_tree(n->r); + } for (p=n->head[0];p;p=pp) { pp=p->next[0]; free(p); @@ -720,7 +752,9 @@ annotate_hash_table(BoxNode *n,HashTable *h,uint32_t *box) return 0; } } - if (n->head[0]) (*box)++; + if (n->head[0]) { + (*box)++; + } return 1; } @@ -756,7 +790,9 @@ resort_distance_tables(uint32_t *avgDist, for (k=j;k&&(*(skRow[k-1])>*(skRow[k]));k--) { skRow[k]=skRow[k-1]; } - if (k!=j) skRow[k]=skElt; + if (k!=j) { + skRow[k]=skElt; + } } } return 1; @@ -976,7 +1012,9 @@ compute_palette_from_median_cut( /* malloc check ok, using calloc */ if (!(avg[i]=calloc(nPaletteEntries, sizeof(uint32_t)))) { for(i=0;i<3;i++) { - if (avg[i]) free (avg[i]); + if (avg[i]) { + free (avg[i]); + } } free(count); return 0; @@ -987,7 +1025,9 @@ compute_palette_from_median_cut( if (!(i%100)) { printf ("%05d\r",i); fflush(stdout); } if (checkContained(root,pixelData+i)>1) { printf ("pixel in two boxes\n"); - for(i=0;i<3;i++) free (avg[i]); + for(i=0;i<3;i++) { + free (avg[i]); + } free(count); return 0; } @@ -996,7 +1036,9 @@ compute_palette_from_median_cut( #ifndef NO_OUTPUT printf ("pixel lookup failed\n"); #endif - for(i=0;i<3;i++) free (avg[i]); + for(i=0;i<3;i++) { + free (avg[i]); + } free(count); return 0; } @@ -1004,7 +1046,9 @@ compute_palette_from_median_cut( #ifndef NO_OUTPUT printf ("panic - paletteEntry>=nPaletteEntries (%d>=%d)\n",(int)paletteEntry,(int)nPaletteEntries); #endif - for(i=0;i<3;i++) free (avg[i]); + for(i=0;i<3;i++) { + free (avg[i]); + } free(count); return 0; } @@ -1016,7 +1060,9 @@ compute_palette_from_median_cut( /* malloc check ok, using calloc */ p=calloc(nPaletteEntries, sizeof(Pixel)); if (!p) { - for(i=0;i<3;i++) free (avg[i]); + for(i=0;i<3;i++) { + free (avg[i]); + } free(count); return 0; } @@ -1026,7 +1072,9 @@ compute_palette_from_median_cut( p[i].c.b=(int)(.5+(double)avg[2][i]/(double)count[i]); } *palette=p; - for(i=0;i<3;i++) free (avg[i]); + for(i=0;i<3;i++) { + free (avg[i]); + } free(count); return 1; } @@ -1156,24 +1204,46 @@ k_means(Pixel *pixelData, #ifndef NO_OUTPUT printf (".(%d)",changes);fflush(stdout); #endif - if (changes<=threshold) break; + if (changes<=threshold) { + break; + } } #ifndef NO_OUTPUT printf("]\n"); #endif - if (avgDistSortKey) free(avgDistSortKey); - if (avgDist) free(avgDist); - for(i=0;i<3;i++) if (avg[i]) free (avg[i]); - if (count) free(count); + if (avgDistSortKey) { + free(avgDistSortKey); + } + if (avgDist) { + free(avgDist); + } + for(i=0;i<3;i++) { + if (avg[i]) { + free (avg[i]); + } + } + if (count) { + free(count); + } return 1; error_3: - if (avgDistSortKey) free(avgDistSortKey); + if (avgDistSortKey) { + free(avgDistSortKey); + } error_2: - if (avgDist) free(avgDist); + if (avgDist) { + free(avgDist); + } error_1: - for(i=0;i<3;i++) if (avg[i]) free (avg[i]); - if (count) free(count); + for(i=0;i<3;i++) { + if (avg[i]) { + free (avg[i]); + } + } + if (count) { + free(count); + } return 0; } @@ -1345,7 +1415,9 @@ quantize(Pixel *pixelData, #ifndef NO_OUTPUT printf ("k means...\n"); fflush(stdout); timer=clock(); #endif - if (kmeans) k_means(pixelData,nPixels,p,nPaletteEntries,qp,kmeans-1); + if (kmeans) { + k_means(pixelData,nPixels,p,nPaletteEntries,qp,kmeans-1); + } #ifndef NO_OUTPUT printf ("done (%f)\n",(clock()-timer)/(double)CLOCKS_PER_SEC); #endif @@ -1357,8 +1429,12 @@ quantize(Pixel *pixelData, #ifndef NO_OUTPUT printf ("cleanup..."); fflush(stdout); timer=clock(); #endif - if (avgDist) free(avgDist); - if (avgDistSortKey) free(avgDistSortKey); + if (avgDist) { + free(avgDist); + } + if (avgDistSortKey) { + free(avgDistSortKey); + } destroy_pixel_hash(h); #ifndef NO_OUTPUT printf ("done (%f)\n",(clock()-timer)/(double)CLOCKS_PER_SEC); @@ -1367,15 +1443,25 @@ quantize(Pixel *pixelData, return 1; error_7: - if (avgDistSortKey) free(avgDistSortKey); + if (avgDistSortKey) { + free(avgDistSortKey); + } error_6: - if (avgDist) free(avgDist); + if (avgDist) { + free(avgDist); + } error_5: - if (qp) free(qp); + if (qp) { + free(qp); + } error_4: - if (p) free(p); + if (p) { + free(p); + } error_3: - if (root) free_box_tree(root); + if (root) { + free_box_tree(root); + } error_1: destroy_pixel_hash(h); error_0: @@ -1430,7 +1516,9 @@ quantize2(Pixel *pixelData, /* malloc check ok, using calloc */ p=calloc(nQuantPixels, sizeof(Pixel)); - if (!p) return 0; + if (!p) { + return 0; + } mean[0]=mean[1]=mean[2]=0; h=hashtable_new(unshifted_pixel_hash,unshifted_pixel_cmp); for (i=0;i 256) + } + if (colors < 1 || colors > 256) { /* FIXME: for colors > 256, consider returning an RGB image instead (see @PIL205) */ return (Imaging) ImagingError_ValueError("bad number of colors"); + } if (strcmp(im->mode, "L") != 0 && strcmp(im->mode, "P") != 0 && - strcmp(im->mode, "RGB") != 0 && strcmp(im->mode, "RGBA") !=0) + strcmp(im->mode, "RGB") != 0 && strcmp(im->mode, "RGBA") !=0) { return ImagingError_ModeError(); + } /* only octree and imagequant supports RGBA */ - if (!strcmp(im->mode, "RGBA") && mode != 2 && mode != 3) + if (!strcmp(im->mode, "RGBA") && mode != 2 && mode != 3) { return ImagingError_ModeError(); + } if (im->xsize > INT_MAX / im->ysize) { return ImagingError_MemoryError(); } /* malloc check ok, using calloc for final overflow, x*y above */ p = calloc(im->xsize * im->ysize, sizeof(Pixel)); - if (!p) + if (!p) { return ImagingError_MemoryError(); + } /* collect statistics */ @@ -1543,18 +1638,19 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) /* FIXME: converting a "L" image to "P" with 256 colors should be done by a simple copy... */ - for (i = y = 0; y < im->ysize; y++) + for (i = y = 0; y < im->ysize; y++) { for (x = 0; x < im->xsize; x++, i++) { p[i].c.r = p[i].c.g = p[i].c.b = im->image8[y][x]; p[i].c.a = 255; } + } } else if (!strcmp(im->mode, "P")) { /* palette */ pp = im->palette->palette; - for (i = y = 0; y < im->ysize; y++) + for (i = y = 0; y < im->ysize; y++) { for (x = 0; x < im->xsize; x++, i++) { v = im->image8[y][x]; p[i].c.r = pp[v*4+0]; @@ -1562,13 +1658,16 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) p[i].c.b = pp[v*4+2]; p[i].c.a = pp[v*4+3]; } + } } else if (!strcmp(im->mode, "RGB") || !strcmp(im->mode, "RGBA")) { /* true colour */ - for (i = y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++, i++) + for (i = y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++, i++) { p[i].v = im->image32[y][x]; + } + } } else { free(p); @@ -1647,9 +1746,11 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) imOut = ImagingNewDirty("P", im->xsize, im->ysize); ImagingSectionEnter(&cookie); - for (i = y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++) + for (i = y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { imOut->image8[y][x] = (unsigned char) newData[i++]; + } + } free(newData); diff --git a/src/libImaging/QuantHash.c b/src/libImaging/QuantHash.c index 3fcbf3c02..6ff95d885 100644 --- a/src/libImaging/QuantHash.c +++ b/src/libImaging/QuantHash.c @@ -67,7 +67,9 @@ static uint32_t _findPrime(uint32_t start,int dir) { continue; } for (t=2;t=sqrt((double)start)) { break; @@ -144,7 +146,9 @@ static int _hashtable_insert_node(HashTable *h,HashNode *node,int resize,int upd node->next=*n; *n=node; h->count++; - if (resize) _hashtable_resize(h); + if (resize) { + _hashtable_resize(h); + } return 1; } else { return 0; @@ -169,13 +173,17 @@ static int _hashtable_insert(HashTable *h,HashKey_t key,HashVal_t val,int resize } if (!update) { t=malloc(sizeof(HashNode)); - if (!t) return 0; + if (!t) { + return 0; + } t->next=*n; *n=t; t->key=key; t->value=val; h->count++; - if (resize) _hashtable_resize(h); + if (resize) { + _hashtable_resize(h); + } return 1; } else { return 0; @@ -206,7 +214,9 @@ int hashtable_insert_or_update_computed(HashTable *h, } } t=malloc(sizeof(HashNode)); - if (!t) return 0; + if (!t) { + return 0; + } t->key=key; t->next=*n; *n=t; diff --git a/src/libImaging/QuantHeap.c b/src/libImaging/QuantHeap.c index 121b87275..6877e34a3 100644 --- a/src/libImaging/QuantHeap.c +++ b/src/libImaging/QuantHeap.c @@ -26,8 +26,8 @@ struct _Heap { void **heap; - int heapsize; - int heapcount; + unsigned int heapsize; + unsigned int heapcount; HeapCmpFunc cf; }; @@ -44,17 +44,23 @@ void ImagingQuantHeapFree(Heap *h) { free(h); } -static int _heap_grow(Heap *h,int newsize) { +static int _heap_grow(Heap *h,unsigned int newsize) { void *newheap; - if (!newsize) newsize=h->heapsize<<1; - if (newsizeheapsize) return 0; + if (!newsize) { + newsize=h->heapsize<<1; + } + if (newsizeheapsize) { + return 0; + } if (newsize > INT_MAX / sizeof(void *)){ return 0; } /* malloc check ok, using calloc for overflow, also checking above due to memcpy below*/ newheap=calloc(newsize, sizeof(void *)); - if (!newheap) return 0; + if (!newheap) { + return 0; + } memcpy(newheap,h->heap,sizeof(void *)*h->heapsize); free(h->heap); h->heap=newheap; @@ -64,7 +70,7 @@ static int _heap_grow(Heap *h,int newsize) { #ifdef DEBUG static int _heap_test(Heap *h) { - int k; + unsigned int k; for (k=1;k*2<=h->heapcount;k++) { if (h->cf(h,h->heap[k],h->heap[k*2])<0) { printf ("heap is bad\n"); @@ -80,7 +86,7 @@ static int _heap_test(Heap *h) { #endif int ImagingQuantHeapRemove(Heap* h,void **r) { - int k,l; + unsigned int k,l; void *v; if (!h->heapcount) { @@ -140,7 +146,9 @@ Heap *ImagingQuantHeapNew(HeapCmpFunc cf) { /* malloc check ok, small constant allocation */ h=malloc(sizeof(Heap)); - if (!h) return NULL; + if (!h) { + return NULL; + } h->heapsize=INITIAL_SIZE; /* malloc check ok, using calloc for overflow */ h->heap=calloc(h->heapsize, sizeof(void *)); diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index 6c0f605c9..e1205acc3 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -28,6 +28,7 @@ #include #include +#include "ImagingUtils.h" #include "QuantOctree.h" typedef struct _ColorBucket{ @@ -44,7 +45,7 @@ typedef struct _ColorCube{ unsigned int rWidth, gWidth, bWidth, aWidth; unsigned int rOffset, gOffset, bOffset, aOffset; - long size; + unsigned long size; ColorBucket buckets; } *ColorCube; @@ -56,7 +57,9 @@ new_color_cube(int r, int g, int b, int a) { /* malloc check ok, small constant allocation */ cube = malloc(sizeof(struct _ColorCube)); - if (!cube) return NULL; + if (!cube) { + return NULL; + } cube->rBits = MAX(r, 0); cube->gBits = MAX(g, 0); @@ -134,10 +137,10 @@ add_color_to_color_cube(const ColorCube cube, const Pixel *p) { bucket->a += p->c.a; } -static long +static unsigned long count_used_color_buckets(const ColorCube cube) { - long usedBuckets = 0; - long i; + unsigned long usedBuckets = 0; + unsigned long i; for (i=0; i < cube->size; i++) { if (cube->buckets[i].count > 0) { usedBuckets += 1; @@ -150,10 +153,10 @@ static void avg_color_from_color_bucket(const ColorBucket bucket, Pixel *dst) { float count = bucket->count; if (count != 0) { - dst->c.r = (int)(bucket->r / count); - dst->c.g = (int)(bucket->g / count); - dst->c.b = (int)(bucket->b / count); - dst->c.a = (int)(bucket->a / count); + dst->c.r = CLIP8((int)(bucket->r / count)); + dst->c.g = CLIP8((int)(bucket->g / count)); + dst->c.b = CLIP8((int)(bucket->b / count)); + dst->c.a = CLIP8((int)(bucket->a / count)); } else { dst->c.r = 0; dst->c.g = 0; @@ -175,7 +178,9 @@ create_sorted_color_palette(const ColorCube cube) { } /* malloc check ok, calloc + overflow check above for memcpy */ buckets = calloc(cube->size, sizeof(struct _ColorBucket)); - if (!buckets) return NULL; + if (!buckets) { + return NULL; + } memcpy(buckets, cube->buckets, sizeof(struct _ColorBucket)*cube->size); qsort(buckets, cube->size, sizeof(struct _ColorBucket), @@ -194,7 +199,7 @@ void add_bucket_values(ColorBucket src, ColorBucket dst) { /* expand or shrink a given cube to level */ static ColorCube copy_color_cube(const ColorCube cube, - int rBits, int gBits, int bBits, int aBits) + unsigned int rBits, unsigned int gBits, unsigned int bBits, unsigned int aBits) { unsigned int r, g, b, a; long src_pos, dst_pos; @@ -203,7 +208,9 @@ static ColorCube copy_color_cube(const ColorCube cube, ColorCube result; result = new_color_cube(rBits, gBits, bBits, aBits); - if (!result) return NULL; + if (!result) { + return NULL; + } if (cube->rBits > rBits) { dst_reduce[0] = cube->rBits - result->rBits; @@ -268,7 +275,9 @@ subtract_color_buckets(ColorCube cube, ColorBucket buckets, long nBuckets) { subtrahend = &buckets[i]; // If the subtrahend contains no buckets, there is nothing to subtract. - if (subtrahend->count == 0) continue; + if (subtrahend->count == 0) { + continue; + } avg_color_from_color_bucket(subtrahend, &p); minuend = color_bucket_from_cube(cube, &p); @@ -302,7 +311,7 @@ void add_lookup_buckets(ColorCube cube, ColorBucket palette, long nColors, long } ColorBucket -combined_palette(ColorBucket bucketsA, long nBucketsA, ColorBucket bucketsB, long nBucketsB) { +combined_palette(ColorBucket bucketsA, unsigned long nBucketsA, ColorBucket bucketsB, unsigned long nBucketsB) { ColorBucket result; if (nBucketsA > LONG_MAX - nBucketsB || (nBucketsA+nBucketsB) > LONG_MAX / sizeof(struct _ColorBucket)) { @@ -325,7 +334,9 @@ create_palette_array(const ColorBucket palette, unsigned int paletteLength) { /* malloc check ok, calloc for overflow */ paletteArray = calloc(paletteLength, sizeof(Pixel)); - if (!paletteArray) return NULL; + if (!paletteArray) { + return NULL; + } for (i=0; i nQuantPixels) + if (nCoarseColors > nQuantPixels) { nCoarseColors = nQuantPixels; + } /* how many space do we have in our palette for fine colors? */ nFineColors = nQuantPixels - nCoarseColors; /* create fine color palette */ paletteBucketsFine = create_sorted_color_palette(fineCube); - if (!paletteBucketsFine) goto error; + if (!paletteBucketsFine) { + goto error; + } /* remove the used fine colors from the coarse cube */ subtract_color_buckets(coarseCube, paletteBucketsFine, nFineColors); @@ -430,7 +448,9 @@ int quantize_octree(Pixel *pixelData, /* create our palette buckets with fine and coarse combined */ paletteBucketsCoarse = create_sorted_color_palette(coarseCube); - if (!paletteBucketsCoarse) goto error; + if (!paletteBucketsCoarse) { + goto error; + } paletteBuckets = combined_palette(paletteBucketsCoarse, nCoarseColors, paletteBucketsFine, nFineColors); @@ -438,19 +458,25 @@ int quantize_octree(Pixel *pixelData, paletteBucketsFine = NULL; free(paletteBucketsCoarse); paletteBucketsCoarse = NULL; - if (!paletteBuckets) goto error; + if (!paletteBuckets) { + goto error; + } /* add all coarse colors to our coarse lookup cube. */ coarseLookupCube = new_color_cube(cubeBits[4], cubeBits[5], cubeBits[6], cubeBits[7]); - if (!coarseLookupCube) goto error; + if (!coarseLookupCube) { + goto error; + } add_lookup_buckets(coarseLookupCube, paletteBuckets, nCoarseColors, 0); /* expand coarse cube (64) to larger fine cube (4k). the value of each coarse bucket is then present in the according 64 fine buckets. */ lookupCube = copy_color_cube(coarseLookupCube, cubeBits[0], cubeBits[1], cubeBits[2], cubeBits[3]); - if (!lookupCube) goto error; + if (!lookupCube) { + goto error; + } /* add fine colors to the lookup cube */ add_lookup_buckets(lookupCube, paletteBuckets, nFineColors, nCoarseColors); @@ -458,12 +484,16 @@ int quantize_octree(Pixel *pixelData, /* create result pixels and map palette indices */ /* malloc check ok, calloc for overflow */ qp = calloc(nPixels, sizeof(Pixel)); - if (!qp) goto error; + if (!qp) { + goto error; + } map_image_pixels(pixelData, nPixels, lookupCube, qp); /* convert palette buckets to RGB pixel palette */ *palette = create_palette_array(paletteBuckets, nQuantPixels); - if (!(*palette)) goto error; + if (!(*palette)) { + goto error; + } *quantizedPixels = qp; *paletteLength = nQuantPixels; diff --git a/src/libImaging/QuantPngQuant.c b/src/libImaging/QuantPngQuant.c index a9a547540..7a23ec8c5 100644 --- a/src/libImaging/QuantPngQuant.c +++ b/src/libImaging/QuantPngQuant.c @@ -20,8 +20,8 @@ int quantize_pngquant( Pixel *pixelData, - int width, - int height, + unsigned int width, + unsigned int height, uint32_t quantPixels, Pixel **palette, uint32_t *paletteLength, @@ -95,9 +95,15 @@ quantize_pngquant( result = 1; err: - if (attr) liq_attr_destroy(attr); - if (image) liq_image_destroy(image); - if (remap) liq_result_destroy(remap); + if (attr) { + liq_attr_destroy(attr); + } + if (image) { + liq_image_destroy(image); + } + if (remap) { + liq_result_destroy(remap); + } free(charMatrix); free(charMatrixRows); if (!result) { @@ -107,4 +113,13 @@ err: return result; } +const char* +ImagingImageQuantVersion(void) +{ + static char version[20]; + int number = liq_version(); + sprintf(version, "%d.%d.%d", number / 10000, (number / 100) % 100, number % 100); + return version; +} + #endif diff --git a/src/libImaging/QuantPngQuant.h b/src/libImaging/QuantPngQuant.h index d539a7a0d..fb0b4cc03 100644 --- a/src/libImaging/QuantPngQuant.h +++ b/src/libImaging/QuantPngQuant.h @@ -4,8 +4,8 @@ #include "QuantTypes.h" int quantize_pngquant(Pixel *, - int, - int, + unsigned int, + unsigned int, uint32_t, Pixel **, uint32_t *, diff --git a/src/libImaging/RankFilter.c b/src/libImaging/RankFilter.c index 0164861bb..e4f2679b2 100644 --- a/src/libImaging/RankFilter.c +++ b/src/libImaging/RankFilter.c @@ -30,15 +30,23 @@ static type Rank##type(type a[], int n, int k)\ i = l;\ j = m;\ do {\ - while (a[i] < x) i++;\ - while (x < a[j]) j--;\ + while (a[i] < x) {\ + i++;\ + }\ + while (x < a[j]) {\ + j--;\ + }\ if (i <= j) {\ SWAP(type, a[i], a[j]);\ i++; j--;\ }\ } while (i <= j);\ - if (j < k) l = i;\ - if (k < i) m = j;\ + if (j < k) {\ + l = i;\ + }\ + if (k < i) {\ + m = j;\ + }\ }\ return a[k];\ } @@ -54,11 +62,13 @@ ImagingRankFilter(Imaging im, int size, int rank) int x, y; int i, margin, size2; - if (!im || im->bands != 1 || im->type == IMAGING_TYPE_SPECIAL) + if (!im || im->bands != 1 || im->type == IMAGING_TYPE_SPECIAL) { return (Imaging) ImagingError_ModeError(); + } - if (!(size & 1)) + if (!(size & 1)) { return (Imaging) ImagingError_ValueError("bad filter size"); + } /* malloc check ok, for overflow in the define below */ if (size > INT_MAX / size || @@ -69,35 +79,40 @@ ImagingRankFilter(Imaging im, int size, int rank) size2 = size * size; margin = (size-1) / 2; - if (rank < 0 || rank >= size2) + if (rank < 0 || rank >= size2) { return (Imaging) ImagingError_ValueError("bad rank value"); + } imOut = ImagingNew(im->mode, im->xsize - 2*margin, im->ysize - 2*margin); - if (!imOut) + if (!imOut) { return NULL; + } /* malloc check ok, checked above */ #define RANK_BODY(type) do {\ type* buf = malloc(size2 * sizeof(type));\ - if (!buf)\ + if (!buf) {\ goto nomemory;\ - for (y = 0; y < imOut->ysize; y++)\ + }\ + for (y = 0; y < imOut->ysize; y++) {\ for (x = 0; x < imOut->xsize; x++) {\ - for (i = 0; i < size; i++)\ + for (i = 0; i < size; i++) {\ memcpy(buf + i*size, &IMAGING_PIXEL_##type(im, x, y+i),\ size * sizeof(type));\ + }\ IMAGING_PIXEL_##type(imOut, x, y) = Rank##type(buf, size2, rank);\ }\ + }\ free(buf); \ } while (0) - if (im->image8) + if (im->image8) { RANK_BODY(UINT8); - else if (im->type == IMAGING_TYPE_INT32) + } else if (im->type == IMAGING_TYPE_INT32) { RANK_BODY(INT32); - else if (im->type == IMAGING_TYPE_FLOAT32) + } else if (im->type == IMAGING_TYPE_FLOAT32) { RANK_BODY(FLOAT32); - else { + } else { /* safety net (we shouldn't end up here) */ ImagingDelete(imOut); return (Imaging) ImagingError_ModeError(); diff --git a/src/libImaging/RawDecode.c b/src/libImaging/RawDecode.c index c069bdb88..ca3d37149 100644 --- a/src/libImaging/RawDecode.c +++ b/src/libImaging/RawDecode.c @@ -5,7 +5,7 @@ * decoder for raw (uncompressed) image data * * history: - * 96-03-07 fl rewritten + * 96-03-07 fl rewritten * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -29,28 +29,29 @@ ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt if (state->state == 0) { - /* Initialize context variables */ + /* Initialize context variables */ - /* get size of image data and padding */ - state->bytes = (state->xsize * state->bits + 7) / 8; - if (rawstate->stride) { - rawstate->skip = rawstate->stride - state->bytes; - if (rawstate->skip < 0) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - } else { - rawstate->skip = 0; - } + /* get size of image data and padding */ + state->bytes = (state->xsize * state->bits + 7) / 8; + if (rawstate->stride) { + rawstate->skip = rawstate->stride - state->bytes; + if (rawstate->skip < 0) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + } else { + rawstate->skip = 0; + } - /* check image orientation */ - if (state->ystep < 0) { - state->y = state->ysize-1; - state->ystep = -1; - } else - state->ystep = 1; + /* check image orientation */ + if (state->ystep < 0) { + state->y = state->ysize-1; + state->ystep = -1; + } else { + state->ystep = 1; + } - state->state = LINE; + state->state = LINE; } @@ -58,38 +59,40 @@ ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt for (;;) { - if (state->state == SKIP) { + if (state->state == SKIP) { - /* Skip padding between lines */ + /* Skip padding between lines */ - if (bytes < rawstate->skip) - return ptr - buf; + if (bytes < rawstate->skip) { + return ptr - buf; + } - ptr += rawstate->skip; - bytes -= rawstate->skip; + ptr += rawstate->skip; + bytes -= rawstate->skip; - state->state = LINE; + state->state = LINE; - } + } - if (bytes < state->bytes) - return ptr - buf; + if (bytes < state->bytes) { + return ptr - buf; + } - /* Unpack data */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, ptr, state->xsize); + /* Unpack data */ + state->shuffle((UINT8*) im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, ptr, state->xsize); - ptr += state->bytes; - bytes -= state->bytes; + ptr += state->bytes; + bytes -= state->bytes; - state->y += state->ystep; + state->y += state->ystep; - if (state->y < 0 || state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } + if (state->y < 0 || state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } - state->state = SKIP; + state->state = SKIP; } diff --git a/src/libImaging/RawEncode.c b/src/libImaging/RawEncode.c index a3b74b8cf..fb4ab3346 100644 --- a/src/libImaging/RawEncode.c +++ b/src/libImaging/RawEncode.c @@ -9,8 +9,8 @@ * in ImageFile.py, but it should be solved here instead. * * history: - * 96-04-30 fl created - * 97-01-03 fl fixed padding + * 96-04-30 fl created + * 97-01-03 fl fixed padding * * Copyright (c) Fredrik Lundh 1996-97. * Copyright (c) Secret Labs AB 1997. @@ -27,60 +27,63 @@ ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) if (!state->state) { - /* The "count" field holds the stride, if specified. Fix - things up so "bytes" is the full size, and "count" the - packed size */ + /* The "count" field holds the stride, if specified. Fix + things up so "bytes" is the full size, and "count" the + packed size */ - if (state->count > 0) { - int bytes = state->count; + if (state->count > 0) { + int bytes = state->count; - /* stride must not be less than real size */ - if (state->count < state->bytes) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - state->count = state->bytes; - state->bytes = bytes; - } else - state->count = state->bytes; + /* stride must not be less than real size */ + if (state->count < state->bytes) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + state->count = state->bytes; + state->bytes = bytes; + } else { + state->count = state->bytes; + } - /* The "ystep" field specifies the orientation */ + /* The "ystep" field specifies the orientation */ - if (state->ystep < 0) { - state->y = state->ysize-1; - state->ystep = -1; - } else - state->ystep = 1; + if (state->ystep < 0) { + state->y = state->ysize-1; + state->ystep = -1; + } else { + state->ystep = 1; + } - state->state = 1; + state->state = 1; } if (bytes < state->bytes) { - state->errcode = IMAGING_CODEC_CONFIG; - return 0; + state->errcode = IMAGING_CODEC_CONFIG; + return 0; } ptr = buf; while (bytes >= state->bytes) { - state->shuffle(ptr, (UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->xsize); + state->shuffle(ptr, (UINT8*) im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, state->xsize); - if (state->bytes > state->count) - /* zero-pad the buffer, if necessary */ - memset(ptr + state->count, 0, state->bytes - state->count); + if (state->bytes > state->count) { + /* zero-pad the buffer, if necessary */ + memset(ptr + state->count, 0, state->bytes - state->count); + } - ptr += state->bytes; - bytes -= state->bytes; + ptr += state->bytes; + bytes -= state->bytes; - state->y += state->ystep; + state->y += state->ystep; - if (state->y < 0 || state->y >= state->ysize) { - state->errcode = IMAGING_CODEC_END; - break; - } + if (state->y < 0 || state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + break; + } } diff --git a/src/libImaging/Reduce.c b/src/libImaging/Reduce.c new file mode 100644 index 000000000..f6488deb2 --- /dev/null +++ b/src/libImaging/Reduce.c @@ -0,0 +1,1440 @@ +#include "Imaging.h" + +#include + +#define ROUND_UP(f) ((int) ((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) + + +UINT32 +division_UINT32(int divider, int result_bits) +{ + UINT32 max_dividend = (1 << result_bits) * divider; + float max_int = (1 << 30) * 4.0; + return (UINT32) (max_int / max_dividend); +} + + +void +ImagingReduceNxN(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) +{ + /* The most general implementation for any xscale and yscale + */ + int x, y, xx, yy; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y*yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + UINT32 ss = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image8[yy]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line0[xx + 0] + line0[xx + 1] + + line1[xx + 0] + line1[xx + 1]; + } + if (xscale & 0x01) { + ss += line0[xx + 0] + line1[xx + 0]; + } + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line[xx + 0] + line[xx + 1]; + } + if (xscale & 0x01) { + ss += line[xx + 0]; + } + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y*yscale; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + UINT32 v; + UINT32 ss0 = amend, ss3 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line0[xx*4 + 0] + line0[xx*4 + 4] + + line1[xx*4 + 0] + line1[xx*4 + 4]; + ss3 += line0[xx*4 + 3] + line0[xx*4 + 7] + + line1[xx*4 + 3] + line1[xx*4 + 7]; + } + if (xscale & 0x01) { + ss0 += line0[xx*4 + 0] + line1[xx*4 + 0]; + ss3 += line0[xx*4 + 3] + line1[xx*4 + 3]; + } + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx*4 + 0] + line[xx*4 + 4]; + ss3 += line[xx*4 + 3] + line[xx*4 + 7]; + } + if (xscale & 0x01) { + ss0 += line[xx*4 + 0]; + ss3 += line[xx*4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, 0, + 0, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line0[xx*4 + 0] + line0[xx*4 + 4] + + line1[xx*4 + 0] + line1[xx*4 + 4]; + ss1 += line0[xx*4 + 1] + line0[xx*4 + 5] + + line1[xx*4 + 1] + line1[xx*4 + 5]; + ss2 += line0[xx*4 + 2] + line0[xx*4 + 6] + + line1[xx*4 + 2] + line1[xx*4 + 6]; + } + if (xscale & 0x01) { + ss0 += line0[xx*4 + 0] + line1[xx*4 + 0]; + ss1 += line0[xx*4 + 1] + line1[xx*4 + 1]; + ss2 += line0[xx*4 + 2] + line1[xx*4 + 2]; + } + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx*4 + 0] + line[xx*4 + 4]; + ss1 += line[xx*4 + 1] + line[xx*4 + 5]; + ss2 += line[xx*4 + 2] + line[xx*4 + 6]; + } + if (xscale & 0x01) { + ss0 += line[xx*4 + 0]; + ss1 += line[xx*4 + 1]; + ss2 += line[xx*4 + 2]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line0[xx*4 + 0] + line0[xx*4 + 4] + + line1[xx*4 + 0] + line1[xx*4 + 4]; + ss1 += line0[xx*4 + 1] + line0[xx*4 + 5] + + line1[xx*4 + 1] + line1[xx*4 + 5]; + ss2 += line0[xx*4 + 2] + line0[xx*4 + 6] + + line1[xx*4 + 2] + line1[xx*4 + 6]; + ss3 += line0[xx*4 + 3] + line0[xx*4 + 7] + + line1[xx*4 + 3] + line1[xx*4 + 7]; + } + if (xscale & 0x01) { + ss0 += line0[xx*4 + 0] + line1[xx*4 + 0]; + ss1 += line0[xx*4 + 1] + line1[xx*4 + 1]; + ss2 += line0[xx*4 + 2] + line1[xx*4 + 2]; + ss3 += line0[xx*4 + 3] + line1[xx*4 + 3]; + } + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx*4 + 0] + line[xx*4 + 4]; + ss1 += line[xx*4 + 1] + line[xx*4 + 5]; + ss2 += line[xx*4 + 2] + line[xx*4 + 6]; + ss3 += line[xx*4 + 3] + line[xx*4 + 7]; + } + if (xscale & 0x01) { + ss0 += line[xx*4 + 0]; + ss1 += line[xx*4 + 1]; + ss2 += line[xx*4 + 2]; + ss3 += line[xx*4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + + +void +ImagingReduce1xN(Imaging imOut, Imaging imIn, int box[4], int yscale) +{ + /* Optimized implementation for xscale = 1. + */ + int x, y, yy; + int xscale = 1; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y*yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 ss = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image8[yy]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + ss += line0[xx + 0] + line1[xx + 0]; + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + ss += line[xx + 0]; + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y*yscale; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + UINT32 ss0 = amend, ss3 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + ss0 += line0[xx*4 + 0] + line1[xx*4 + 0]; + ss3 += line0[xx*4 + 3] + line1[xx*4 + 3]; + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + ss0 += line[xx*4 + 0]; + ss3 += line[xx*4 + 3]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, 0, + 0, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + ss0 += line0[xx*4 + 0] + line1[xx*4 + 0]; + ss1 += line0[xx*4 + 1] + line1[xx*4 + 1]; + ss2 += line0[xx*4 + 2] + line1[xx*4 + 2]; + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + ss0 += line[xx*4 + 0]; + ss1 += line[xx*4 + 1]; + ss2 += line[xx*4 + 2]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + ss0 += line0[xx*4 + 0] + line1[xx*4 + 0]; + ss1 += line0[xx*4 + 1] + line1[xx*4 + 1]; + ss2 += line0[xx*4 + 2] + line1[xx*4 + 2]; + ss3 += line0[xx*4 + 3] + line1[xx*4 + 3]; + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + ss0 += line[xx*4 + 0]; + ss1 += line[xx*4 + 1]; + ss2 += line[xx*4 + 2]; + ss3 += line[xx*4 + 3]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + + +void +ImagingReduceNx1(Imaging imOut, Imaging imIn, int box[4], int xscale) +{ + /* Optimized implementation for yscale = 1. + */ + int x, y, xx; + int yscale = 1; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + UINT32 ss = amend; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line[xx + 0] + line[xx + 1]; + } + if (xscale & 0x01) { + ss += line[xx + 0]; + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line = (UINT8 *)imIn->image[yy]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + UINT32 v; + UINT32 ss0 = amend, ss3 = amend; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx*4 + 0] + line[xx*4 + 4]; + ss3 += line[xx*4 + 3] + line[xx*4 + 7]; + } + if (xscale & 0x01) { + ss0 += line[xx*4 + 0]; + ss3 += line[xx*4 + 3]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, 0, + 0, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx*4 + 0] + line[xx*4 + 4]; + ss1 += line[xx*4 + 1] + line[xx*4 + 5]; + ss2 += line[xx*4 + 2] + line[xx*4 + 6]; + } + if (xscale & 0x01) { + ss0 += line[xx*4 + 0]; + ss1 += line[xx*4 + 1]; + ss2 += line[xx*4 + 2]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx*4 + 0] + line[xx*4 + 4]; + ss1 += line[xx*4 + 1] + line[xx*4 + 5]; + ss2 += line[xx*4 + 2] + line[xx*4 + 6]; + ss3 += line[xx*4 + 3] + line[xx*4 + 7]; + } + if (xscale & 0x01) { + ss0 += line[xx*4 + 0]; + ss1 += line[xx*4 + 1]; + ss2 += line[xx*4 + 2]; + ss3 += line[xx*4 + 3]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce1x2(Imaging imOut, Imaging imIn, int box[4]) +{ + /* Optimized implementation for xscale = 1 and yscale = 2. + */ + int xscale = 1, yscale = 2; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + ss0 = line0[xx + 0] + + line1[xx + 0]; + imOut->image8[y][x] = (ss0 + amend) >> 1; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + + line1[xx*4 + 0]; + ss3 = line0[xx*4 + 3] + + line1[xx*4 + 3]; + v = MAKE_UINT32((ss0 + amend) >> 1, 0, + 0, (ss3 + amend) >> 1); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + + line1[xx*4 + 0]; + ss1 = line0[xx*4 + 1] + + line1[xx*4 + 1]; + ss2 = line0[xx*4 + 2] + + line1[xx*4 + 2]; + v = MAKE_UINT32((ss0 + amend) >> 1, (ss1 + amend) >> 1, + (ss2 + amend) >> 1, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + + line1[xx*4 + 0]; + ss1 = line0[xx*4 + 1] + + line1[xx*4 + 1]; + ss2 = line0[xx*4 + 2] + + line1[xx*4 + 2]; + ss3 = line0[xx*4 + 3] + + line1[xx*4 + 3]; + v = MAKE_UINT32((ss0 + amend) >> 1, (ss1 + amend) >> 1, + (ss2 + amend) >> 1, (ss3 + amend) >> 1); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + + +void +ImagingReduce2x1(Imaging imOut, Imaging imIn, int box[4]) +{ + /* Optimized implementation for xscale = 2 and yscale = 1. + */ + int xscale = 2, yscale = 1; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + ss0 = line0[xx + 0] + line0[xx + 1]; + imOut->image8[y][x] = (ss0 + amend) >> 1; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4]; + ss3 = line0[xx*4 + 3] + line0[xx*4 + 7]; + v = MAKE_UINT32((ss0 + amend) >> 1, 0, + 0, (ss3 + amend) >> 1); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4]; + ss1 = line0[xx*4 + 1] + line0[xx*4 + 5]; + ss2 = line0[xx*4 + 2] + line0[xx*4 + 6]; + v = MAKE_UINT32((ss0 + amend) >> 1, (ss1 + amend) >> 1, + (ss2 + amend) >> 1, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4]; + ss1 = line0[xx*4 + 1] + line0[xx*4 + 5]; + ss2 = line0[xx*4 + 2] + line0[xx*4 + 6]; + ss3 = line0[xx*4 + 3] + line0[xx*4 + 7]; + v = MAKE_UINT32((ss0 + amend) >> 1, (ss1 + amend) >> 1, + (ss2 + amend) >> 1, (ss3 + amend) >> 1); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + + +void +ImagingReduce2x2(Imaging imOut, Imaging imIn, int box[4]) +{ + /* Optimized implementation for xscale = 2 and yscale = 2. + */ + int xscale = 2, yscale = 2; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + + line1[xx + 0] + line1[xx + 1]; + imOut->image8[y][x] = (ss0 + amend) >> 2; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + + line1[xx*4 + 0] + line1[xx*4 + 4]; + ss3 = line0[xx*4 + 3] + line0[xx*4 + 7] + + line1[xx*4 + 3] + line1[xx*4 + 7]; + v = MAKE_UINT32((ss0 + amend) >> 2, 0, + 0, (ss3 + amend) >> 2); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + + line1[xx*4 + 0] + line1[xx*4 + 4]; + ss1 = line0[xx*4 + 1] + line0[xx*4 + 5] + + line1[xx*4 + 1] + line1[xx*4 + 5]; + ss2 = line0[xx*4 + 2] + line0[xx*4 + 6] + + line1[xx*4 + 2] + line1[xx*4 + 6]; + v = MAKE_UINT32((ss0 + amend) >> 2, (ss1 + amend) >> 2, + (ss2 + amend) >> 2, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + + line1[xx*4 + 0] + line1[xx*4 + 4]; + ss1 = line0[xx*4 + 1] + line0[xx*4 + 5] + + line1[xx*4 + 1] + line1[xx*4 + 5]; + ss2 = line0[xx*4 + 2] + line0[xx*4 + 6] + + line1[xx*4 + 2] + line1[xx*4 + 6]; + ss3 = line0[xx*4 + 3] + line0[xx*4 + 7] + + line1[xx*4 + 3] + line1[xx*4 + 7]; + v = MAKE_UINT32((ss0 + amend) >> 2, (ss1 + amend) >> 2, + (ss2 + amend) >> 2, (ss3 + amend) >> 2); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + + +void +ImagingReduce1x3(Imaging imOut, Imaging imIn, int box[4]) +{ + /* Optimized implementation for xscale = 1 and yscale = 3. + */ + int xscale = 1, yscale = 3; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + ss0 = line0[xx + 0] + + line1[xx + 0] + + line2[xx + 0]; + imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + + line1[xx*4 + 0] + + line2[xx*4 + 0]; + ss3 = line0[xx*4 + 3] + + line1[xx*4 + 3] + + line2[xx*4 + 3]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, 0, + 0, ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + + line1[xx*4 + 0] + + line2[xx*4 + 0]; + ss1 = line0[xx*4 + 1] + + line1[xx*4 + 1] + + line2[xx*4 + 1]; + ss2 = line0[xx*4 + 2] + + line1[xx*4 + 2] + + line2[xx*4 + 2]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + + line1[xx*4 + 0] + + line2[xx*4 + 0]; + ss1 = line0[xx*4 + 1] + + line1[xx*4 + 1] + + line2[xx*4 + 1]; + ss2 = line0[xx*4 + 2] + + line1[xx*4 + 2] + + line2[xx*4 + 2]; + ss3 = line0[xx*4 + 3] + + line1[xx*4 + 3] + + line2[xx*4 + 3]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + + +void +ImagingReduce3x1(Imaging imOut, Imaging imIn, int box[4]) +{ + /* Optimized implementation for xscale = 3 and yscale = 1. + */ + int xscale = 3, yscale = 1; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2]; + imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + line0[xx*4 + 8]; + ss3 = line0[xx*4 + 3] + line0[xx*4 + 7] + line0[xx*4 + 11]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, 0, + 0, ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + line0[xx*4 + 8]; + ss1 = line0[xx*4 + 1] + line0[xx*4 + 5] + line0[xx*4 + 9]; + ss2 = line0[xx*4 + 2] + line0[xx*4 + 6] + line0[xx*4 + 10]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + line0[xx*4 + 8]; + ss1 = line0[xx*4 + 1] + line0[xx*4 + 5] + line0[xx*4 + 9]; + ss2 = line0[xx*4 + 2] + line0[xx*4 + 6] + line0[xx*4 + 10]; + ss3 = line0[xx*4 + 3] + line0[xx*4 + 7] + line0[xx*4 + 11]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + + +void +ImagingReduce3x3(Imaging imOut, Imaging imIn, int box[4]) +{ + /* Optimized implementation for xscale = 3 and yscale = 3. + */ + int xscale = 3, yscale = 3; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2] + + line1[xx + 0] + line1[xx + 1] + line1[xx + 2] + + line2[xx + 0] + line2[xx + 1] + line2[xx + 2]; + imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + line0[xx*4 + 8] + + line1[xx*4 + 0] + line1[xx*4 + 4] + line1[xx*4 + 8] + + line2[xx*4 + 0] + line2[xx*4 + 4] + line2[xx*4 + 8]; + ss3 = line0[xx*4 + 3] + line0[xx*4 + 7] + line0[xx*4 + 11] + + line1[xx*4 + 3] + line1[xx*4 + 7] + line1[xx*4 + 11] + + line2[xx*4 + 3] + line2[xx*4 + 7] + line2[xx*4 + 11]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, 0, + 0, ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + line0[xx*4 + 8] + + line1[xx*4 + 0] + line1[xx*4 + 4] + line1[xx*4 + 8] + + line2[xx*4 + 0] + line2[xx*4 + 4] + line2[xx*4 + 8]; + ss1 = line0[xx*4 + 1] + line0[xx*4 + 5] + line0[xx*4 + 9] + + line1[xx*4 + 1] + line1[xx*4 + 5] + line1[xx*4 + 9] + + line2[xx*4 + 1] + line2[xx*4 + 5] + line2[xx*4 + 9]; + ss2 = line0[xx*4 + 2] + line0[xx*4 + 6] + line0[xx*4 + 10] + + line1[xx*4 + 2] + line1[xx*4 + 6] + line1[xx*4 + 10] + + line2[xx*4 + 2] + line2[xx*4 + 6] + line2[xx*4 + 10]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + line0[xx*4 + 8] + + line1[xx*4 + 0] + line1[xx*4 + 4] + line1[xx*4 + 8] + + line2[xx*4 + 0] + line2[xx*4 + 4] + line2[xx*4 + 8]; + ss1 = line0[xx*4 + 1] + line0[xx*4 + 5] + line0[xx*4 + 9] + + line1[xx*4 + 1] + line1[xx*4 + 5] + line1[xx*4 + 9] + + line2[xx*4 + 1] + line2[xx*4 + 5] + line2[xx*4 + 9]; + ss2 = line0[xx*4 + 2] + line0[xx*4 + 6] + line0[xx*4 + 10] + + line1[xx*4 + 2] + line1[xx*4 + 6] + line1[xx*4 + 10] + + line2[xx*4 + 2] + line2[xx*4 + 6] + line2[xx*4 + 10]; + ss3 = line0[xx*4 + 3] + line0[xx*4 + 7] + line0[xx*4 + 11] + + line1[xx*4 + 3] + line1[xx*4 + 7] + line1[xx*4 + 11] + + line2[xx*4 + 3] + line2[xx*4 + 7] + line2[xx*4 + 11]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce4x4(Imaging imOut, Imaging imIn, int box[4]) +{ + /* Optimized implementation for xscale = 4 and yscale = 4. + */ + int xscale = 4, yscale = 4; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; + UINT8 *line3 = (UINT8 *)imIn->image8[yy + 3]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2] + line0[xx + 3] + + line1[xx + 0] + line1[xx + 1] + line1[xx + 2] + line1[xx + 3] + + line2[xx + 0] + line2[xx + 1] + line2[xx + 2] + line2[xx + 3] + + line3[xx + 0] + line3[xx + 1] + line3[xx + 2] + line3[xx + 3]; + imOut->image8[y][x] = (ss0 + amend) >> 4; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; + UINT8 *line3 = (UINT8 *)imIn->image[yy + 3]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + line0[xx*4 + 8] + line0[xx*4 + 12] + + line1[xx*4 + 0] + line1[xx*4 + 4] + line1[xx*4 + 8] + line1[xx*4 + 12] + + line2[xx*4 + 0] + line2[xx*4 + 4] + line2[xx*4 + 8] + line2[xx*4 + 12] + + line3[xx*4 + 0] + line3[xx*4 + 4] + line3[xx*4 + 8] + line3[xx*4 + 12]; + ss3 = line0[xx*4 + 3] + line0[xx*4 + 7] + line0[xx*4 + 11] + line0[xx*4 + 15] + + line1[xx*4 + 3] + line1[xx*4 + 7] + line1[xx*4 + 11] + line1[xx*4 + 15] + + line2[xx*4 + 3] + line2[xx*4 + 7] + line2[xx*4 + 11] + line2[xx*4 + 15] + + line3[xx*4 + 3] + line3[xx*4 + 7] + line3[xx*4 + 11] + line3[xx*4 + 15]; + v = MAKE_UINT32((ss0 + amend) >> 4, 0, + 0, (ss3 + amend) >> 4); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + line0[xx*4 + 8] + line0[xx*4 + 12] + + line1[xx*4 + 0] + line1[xx*4 + 4] + line1[xx*4 + 8] + line1[xx*4 + 12] + + line2[xx*4 + 0] + line2[xx*4 + 4] + line2[xx*4 + 8] + line2[xx*4 + 12] + + line3[xx*4 + 0] + line3[xx*4 + 4] + line3[xx*4 + 8] + line3[xx*4 + 12]; + ss1 = line0[xx*4 + 1] + line0[xx*4 + 5] + line0[xx*4 + 9] + line0[xx*4 + 13] + + line1[xx*4 + 1] + line1[xx*4 + 5] + line1[xx*4 + 9] + line1[xx*4 + 13] + + line2[xx*4 + 1] + line2[xx*4 + 5] + line2[xx*4 + 9] + line2[xx*4 + 13] + + line3[xx*4 + 1] + line3[xx*4 + 5] + line3[xx*4 + 9] + line3[xx*4 + 13]; + ss2 = line0[xx*4 + 2] + line0[xx*4 + 6] + line0[xx*4 + 10] + line0[xx*4 + 14] + + line1[xx*4 + 2] + line1[xx*4 + 6] + line1[xx*4 + 10] + line1[xx*4 + 14] + + line2[xx*4 + 2] + line2[xx*4 + 6] + line2[xx*4 + 10] + line2[xx*4 + 14] + + line3[xx*4 + 2] + line3[xx*4 + 6] + line3[xx*4 + 10] + line3[xx*4 + 14]; + v = MAKE_UINT32((ss0 + amend) >> 4, (ss1 + amend) >> 4, + (ss2 + amend) >> 4, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + line0[xx*4 + 8] + line0[xx*4 + 12] + + line1[xx*4 + 0] + line1[xx*4 + 4] + line1[xx*4 + 8] + line1[xx*4 + 12] + + line2[xx*4 + 0] + line2[xx*4 + 4] + line2[xx*4 + 8] + line2[xx*4 + 12] + + line3[xx*4 + 0] + line3[xx*4 + 4] + line3[xx*4 + 8] + line3[xx*4 + 12]; + ss1 = line0[xx*4 + 1] + line0[xx*4 + 5] + line0[xx*4 + 9] + line0[xx*4 + 13] + + line1[xx*4 + 1] + line1[xx*4 + 5] + line1[xx*4 + 9] + line1[xx*4 + 13] + + line2[xx*4 + 1] + line2[xx*4 + 5] + line2[xx*4 + 9] + line2[xx*4 + 13] + + line3[xx*4 + 1] + line3[xx*4 + 5] + line3[xx*4 + 9] + line3[xx*4 + 13]; + ss2 = line0[xx*4 + 2] + line0[xx*4 + 6] + line0[xx*4 + 10] + line0[xx*4 + 14] + + line1[xx*4 + 2] + line1[xx*4 + 6] + line1[xx*4 + 10] + line1[xx*4 + 14] + + line2[xx*4 + 2] + line2[xx*4 + 6] + line2[xx*4 + 10] + line2[xx*4 + 14] + + line3[xx*4 + 2] + line3[xx*4 + 6] + line3[xx*4 + 10] + line3[xx*4 + 14]; + ss3 = line0[xx*4 + 3] + line0[xx*4 + 7] + line0[xx*4 + 11] + line0[xx*4 + 15] + + line1[xx*4 + 3] + line1[xx*4 + 7] + line1[xx*4 + 11] + line1[xx*4 + 15] + + line2[xx*4 + 3] + line2[xx*4 + 7] + line2[xx*4 + 11] + line2[xx*4 + 15] + + line3[xx*4 + 3] + line3[xx*4 + 7] + line3[xx*4 + 11] + line3[xx*4 + 15]; + v = MAKE_UINT32((ss0 + amend) >> 4, (ss1 + amend) >> 4, + (ss2 + amend) >> 4, (ss3 + amend) >> 4); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + + +void +ImagingReduce5x5(Imaging imOut, Imaging imIn, int box[4]) +{ + /* Fast special case for xscale = 5 and yscale = 5. + */ + int xscale = 5, yscale = 5; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; + UINT8 *line3 = (UINT8 *)imIn->image8[yy + 3]; + UINT8 *line4 = (UINT8 *)imIn->image8[yy + 4]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2] + line0[xx + 3] + line0[xx + 4] + + line1[xx + 0] + line1[xx + 1] + line1[xx + 2] + line1[xx + 3] + line1[xx + 4] + + line2[xx + 0] + line2[xx + 1] + line2[xx + 2] + line2[xx + 3] + line2[xx + 4] + + line3[xx + 0] + line3[xx + 1] + line3[xx + 2] + line3[xx + 3] + line3[xx + 4] + + line4[xx + 0] + line4[xx + 1] + line4[xx + 2] + line4[xx + 3] + line4[xx + 4]; + imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y*yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; + UINT8 *line3 = (UINT8 *)imIn->image[yy + 3]; + UINT8 *line4 = (UINT8 *)imIn->image[yy + 4]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + line0[xx*4 + 8] + line0[xx*4 + 12] + line0[xx*4 + 16] + + line1[xx*4 + 0] + line1[xx*4 + 4] + line1[xx*4 + 8] + line1[xx*4 + 12] + line1[xx*4 + 16] + + line2[xx*4 + 0] + line2[xx*4 + 4] + line2[xx*4 + 8] + line2[xx*4 + 12] + line2[xx*4 + 16] + + line3[xx*4 + 0] + line3[xx*4 + 4] + line3[xx*4 + 8] + line3[xx*4 + 12] + line3[xx*4 + 16] + + line4[xx*4 + 0] + line4[xx*4 + 4] + line4[xx*4 + 8] + line4[xx*4 + 12] + line4[xx*4 + 16]; + ss3 = line0[xx*4 + 3] + line0[xx*4 + 7] + line0[xx*4 + 11] + line0[xx*4 + 15] + line0[xx*4 + 19] + + line1[xx*4 + 3] + line1[xx*4 + 7] + line1[xx*4 + 11] + line1[xx*4 + 15] + line1[xx*4 + 19] + + line2[xx*4 + 3] + line2[xx*4 + 7] + line2[xx*4 + 11] + line2[xx*4 + 15] + line2[xx*4 + 19] + + line3[xx*4 + 3] + line3[xx*4 + 7] + line3[xx*4 + 11] + line3[xx*4 + 15] + line3[xx*4 + 19] + + line4[xx*4 + 3] + line4[xx*4 + 7] + line4[xx*4 + 11] + line4[xx*4 + 15] + line4[xx*4 + 19]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, 0, + 0, ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + line0[xx*4 + 8] + line0[xx*4 + 12] + line0[xx*4 + 16] + + line1[xx*4 + 0] + line1[xx*4 + 4] + line1[xx*4 + 8] + line1[xx*4 + 12] + line1[xx*4 + 16] + + line2[xx*4 + 0] + line2[xx*4 + 4] + line2[xx*4 + 8] + line2[xx*4 + 12] + line2[xx*4 + 16] + + line3[xx*4 + 0] + line3[xx*4 + 4] + line3[xx*4 + 8] + line3[xx*4 + 12] + line3[xx*4 + 16] + + line4[xx*4 + 0] + line4[xx*4 + 4] + line4[xx*4 + 8] + line4[xx*4 + 12] + line4[xx*4 + 16]; + ss1 = line0[xx*4 + 1] + line0[xx*4 + 5] + line0[xx*4 + 9] + line0[xx*4 + 13] + line0[xx*4 + 17] + + line1[xx*4 + 1] + line1[xx*4 + 5] + line1[xx*4 + 9] + line1[xx*4 + 13] + line1[xx*4 + 17] + + line2[xx*4 + 1] + line2[xx*4 + 5] + line2[xx*4 + 9] + line2[xx*4 + 13] + line2[xx*4 + 17] + + line3[xx*4 + 1] + line3[xx*4 + 5] + line3[xx*4 + 9] + line3[xx*4 + 13] + line3[xx*4 + 17] + + line4[xx*4 + 1] + line4[xx*4 + 5] + line4[xx*4 + 9] + line4[xx*4 + 13] + line4[xx*4 + 17]; + ss2 = line0[xx*4 + 2] + line0[xx*4 + 6] + line0[xx*4 + 10] + line0[xx*4 + 14] + line0[xx*4 + 18] + + line1[xx*4 + 2] + line1[xx*4 + 6] + line1[xx*4 + 10] + line1[xx*4 + 14] + line1[xx*4 + 18] + + line2[xx*4 + 2] + line2[xx*4 + 6] + line2[xx*4 + 10] + line2[xx*4 + 14] + line2[xx*4 + 18] + + line3[xx*4 + 2] + line3[xx*4 + 6] + line3[xx*4 + 10] + line3[xx*4 + 14] + line3[xx*4 + 18] + + line4[xx*4 + 2] + line4[xx*4 + 6] + line4[xx*4 + 10] + line4[xx*4 + 14] + line4[xx*4 + 18]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x*xscale; + UINT32 v; + ss0 = line0[xx*4 + 0] + line0[xx*4 + 4] + line0[xx*4 + 8] + line0[xx*4 + 12] + line0[xx*4 + 16] + + line1[xx*4 + 0] + line1[xx*4 + 4] + line1[xx*4 + 8] + line1[xx*4 + 12] + line1[xx*4 + 16] + + line2[xx*4 + 0] + line2[xx*4 + 4] + line2[xx*4 + 8] + line2[xx*4 + 12] + line2[xx*4 + 16] + + line3[xx*4 + 0] + line3[xx*4 + 4] + line3[xx*4 + 8] + line3[xx*4 + 12] + line3[xx*4 + 16] + + line4[xx*4 + 0] + line4[xx*4 + 4] + line4[xx*4 + 8] + line4[xx*4 + 12] + line4[xx*4 + 16]; + ss1 = line0[xx*4 + 1] + line0[xx*4 + 5] + line0[xx*4 + 9] + line0[xx*4 + 13] + line0[xx*4 + 17] + + line1[xx*4 + 1] + line1[xx*4 + 5] + line1[xx*4 + 9] + line1[xx*4 + 13] + line1[xx*4 + 17] + + line2[xx*4 + 1] + line2[xx*4 + 5] + line2[xx*4 + 9] + line2[xx*4 + 13] + line2[xx*4 + 17] + + line3[xx*4 + 1] + line3[xx*4 + 5] + line3[xx*4 + 9] + line3[xx*4 + 13] + line3[xx*4 + 17] + + line4[xx*4 + 1] + line4[xx*4 + 5] + line4[xx*4 + 9] + line4[xx*4 + 13] + line4[xx*4 + 17]; + ss2 = line0[xx*4 + 2] + line0[xx*4 + 6] + line0[xx*4 + 10] + line0[xx*4 + 14] + line0[xx*4 + 18] + + line1[xx*4 + 2] + line1[xx*4 + 6] + line1[xx*4 + 10] + line1[xx*4 + 14] + line1[xx*4 + 18] + + line2[xx*4 + 2] + line2[xx*4 + 6] + line2[xx*4 + 10] + line2[xx*4 + 14] + line2[xx*4 + 18] + + line3[xx*4 + 2] + line3[xx*4 + 6] + line3[xx*4 + 10] + line3[xx*4 + 14] + line3[xx*4 + 18] + + line4[xx*4 + 2] + line4[xx*4 + 6] + line4[xx*4 + 10] + line4[xx*4 + 14] + line4[xx*4 + 18]; + ss3 = line0[xx*4 + 3] + line0[xx*4 + 7] + line0[xx*4 + 11] + line0[xx*4 + 15] + line0[xx*4 + 19] + + line1[xx*4 + 3] + line1[xx*4 + 7] + line1[xx*4 + 11] + line1[xx*4 + 15] + line1[xx*4 + 19] + + line2[xx*4 + 3] + line2[xx*4 + 7] + line2[xx*4 + 11] + line2[xx*4 + 15] + line2[xx*4 + 19] + + line3[xx*4 + 3] + line3[xx*4 + 7] + line3[xx*4 + 11] + line3[xx*4 + 15] + line3[xx*4 + 19] + + line4[xx*4 + 3] + line4[xx*4 + 7] + line4[xx*4 + 11] + line4[xx*4 + 15] + line4[xx*4 + 19]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + + +void +ImagingReduceCorners(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) +{ + /* Fill the last row and the last column for any xscale and yscale. + */ + int x, y, xx, yy; + + if (imIn->image8) { + if (box[2] % xscale) { + int scale = (box[2] % xscale) * yscale; + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y*yscale; + UINT32 ss = amend; + x = box[2] / xscale; + + for (yy = yy_from; yy < yy_from + yscale; yy++) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (xx = box[0] + x*xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + if (box[3] % yscale) { + int scale = xscale * (box[3] % yscale); + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + y = box[3] / yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + UINT32 ss = amend; + for (yy = box[1] + y*yscale; yy < box[1] + box[3]; yy++) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (xx = xx_from; xx < xx_from + xscale; xx++) { + ss += line[xx + 0]; + } + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + if (box[2] % xscale && box[3] % yscale) { + int scale = (box[2] % xscale) * (box[3] % yscale); + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + UINT32 ss = amend; + x = box[2] / xscale; + y = box[3] / yscale; + for (yy = box[1] + y*yscale; yy < box[1] + box[3]; yy++) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (xx = box[0] + x*xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } else { + if (box[2] % xscale) { + int scale = (box[2] % xscale) * yscale; + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y*yscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + x = box[2] / xscale; + + for (yy = yy_from; yy < yy_from + yscale; yy++) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = box[0] + x*xscale; xx < box[0] + box[2]; xx++) { + ss0 += line[xx*4 + 0]; + ss1 += line[xx*4 + 1]; + ss2 += line[xx*4 + 2]; + ss3 += line[xx*4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + if (box[3] % yscale) { + int scale = xscale * (box[3] % yscale); + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + y = box[3] / yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + for (yy = box[1] + y*yscale; yy < box[1] + box[3]; yy++) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = xx_from; xx < xx_from + xscale; xx++) { + ss0 += line[xx*4 + 0]; + ss1 += line[xx*4 + 1]; + ss2 += line[xx*4 + 2]; + ss3 += line[xx*4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + if (box[2] % xscale && box[3] % yscale) { + int scale = (box[2] % xscale) * (box[3] % yscale); + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + x = box[2] / xscale; + y = box[3] / yscale; + for (yy = box[1] + y*yscale; yy < box[1] + box[3]; yy++) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = box[0] + x*xscale; xx < box[0] + box[2]; xx++) { + ss0 += line[xx*4 + 0]; + ss1 += line[xx*4 + 1]; + ss2 += line[xx*4 + 2]; + ss3 += line[xx*4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } +} + + +void +ImagingReduceNxN_32bpc(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) +{ + /* The most general implementation for any xscale and yscale + */ + int x, y, xx, yy; + double multiplier = 1.0 / (yscale * xscale); + + switch(imIn->type) { + case IMAGING_TYPE_INT32: + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y*yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + double ss = 0; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + INT32 *line0 = (INT32 *)imIn->image32[yy]; + INT32 *line1 = (INT32 *)imIn->image32[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line0[xx + 0] + line0[xx + 1] + + line1[xx + 0] + line1[xx + 1]; + } + if (xscale & 0x01) { + ss += line0[xx + 0] + line1[xx + 0]; + } + } + if (yscale & 0x01) { + INT32 *line = (INT32 *)imIn->image32[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line[xx + 0] + line[xx + 1]; + } + if (xscale & 0x01) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); + } + } + break; + + case IMAGING_TYPE_FLOAT32: + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y*yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + double ss = 0; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + FLOAT32 *line0 = (FLOAT32 *)imIn->image32[yy]; + FLOAT32 *line1 = (FLOAT32 *)imIn->image32[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line0[xx + 0] + line0[xx + 1] + + line1[xx + 0] + line1[xx + 1]; + } + if (xscale & 0x01) { + ss += line0[xx + 0] + line1[xx + 0]; + } + } + if (yscale & 0x01) { + FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line[xx + 0] + line[xx + 1]; + } + if (xscale & 0x01) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; + } + } + break; + } +} + + +void +ImagingReduceCorners_32bpc(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) +{ + /* Fill the last row and the last column for any xscale and yscale. + */ + int x, y, xx, yy; + + switch(imIn->type) { + case IMAGING_TYPE_INT32: + if (box[2] % xscale) { + double multiplier = 1.0 / ((box[2] % xscale) * yscale); + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y*yscale; + double ss = 0; + x = box[2] / xscale; + for (yy = yy_from; yy < yy_from + yscale; yy++) { + INT32 *line = (INT32 *)imIn->image32[yy]; + for (xx = box[0] + x*xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); + } + } + if (box[3] % yscale) { + double multiplier = 1.0 / (xscale * (box[3] % yscale)); + y = box[3] / yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + double ss = 0; + for (yy = box[1] + y*yscale; yy < box[1] + box[3]; yy++) { + INT32 *line = (INT32 *)imIn->image32[yy]; + for (xx = xx_from; xx < xx_from + xscale; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); + } + } + if (box[2] % xscale && box[3] % yscale) { + double multiplier = 1.0 / ((box[2] % xscale) * (box[3] % yscale)); + double ss = 0; + x = box[2] / xscale; + y = box[3] / yscale; + for (yy = box[1] + y*yscale; yy < box[1] + box[3]; yy++) { + INT32 *line = (INT32 *)imIn->image32[yy]; + for (xx = box[0] + x*xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); + } + break; + + case IMAGING_TYPE_FLOAT32: + if (box[2] % xscale) { + double multiplier = 1.0 / ((box[2] % xscale) * yscale); + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y*yscale; + double ss = 0; + x = box[2] / xscale; + for (yy = yy_from; yy < yy_from + yscale; yy++) { + FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; + for (xx = box[0] + x*xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; + } + } + if (box[3] % yscale) { + double multiplier = 1.0 / (xscale * (box[3] % yscale)); + y = box[3] / yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x*xscale; + double ss = 0; + for (yy = box[1] + y*yscale; yy < box[1] + box[3]; yy++) { + FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; + for (xx = xx_from; xx < xx_from + xscale; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; + } + } + if (box[2] % xscale && box[3] % yscale) { + double multiplier = 1.0 / ((box[2] % xscale) * (box[3] % yscale)); + double ss = 0; + x = box[2] / xscale; + y = box[3] / yscale; + for (yy = box[1] + y*yscale; yy < box[1] + box[3]; yy++) { + FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; + for (xx = box[0] + x*xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; + } + break; + } +} + + +Imaging +ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]) +{ + ImagingSectionCookie cookie; + Imaging imOut = NULL; + + if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) { + return (Imaging) ImagingError_ModeError(); + } + + if (imIn->type == IMAGING_TYPE_SPECIAL) { + return (Imaging) ImagingError_ModeError(); + } + + imOut = ImagingNewDirty(imIn->mode, + (box[2] + xscale - 1) / xscale, + (box[3] + yscale - 1) / yscale); + if ( ! imOut) { + return NULL; + } + + ImagingSectionEnter(&cookie); + + switch(imIn->type) { + case IMAGING_TYPE_UINT8: + if (xscale == 1) { + if (yscale == 2) { + ImagingReduce1x2(imOut, imIn, box); + } else if (yscale == 3) { + ImagingReduce1x3(imOut, imIn, box); + } else { + ImagingReduce1xN(imOut, imIn, box, yscale); + } + } else if (yscale == 1) { + if (xscale == 2) { + ImagingReduce2x1(imOut, imIn, box); + } else if (xscale == 3) { + ImagingReduce3x1(imOut, imIn, box); + } else { + ImagingReduceNx1(imOut, imIn, box, xscale); + } + } else if (xscale == yscale && xscale <= 5) { + if (xscale == 2) { + ImagingReduce2x2(imOut, imIn, box); + } else if (xscale == 3) { + ImagingReduce3x3(imOut, imIn, box); + } else if (xscale == 4) { + ImagingReduce4x4(imOut, imIn, box); + } else { + ImagingReduce5x5(imOut, imIn, box); + } + } else { + ImagingReduceNxN(imOut, imIn, box, xscale, yscale); + } + + ImagingReduceCorners(imOut, imIn, box, xscale, yscale); + break; + + case IMAGING_TYPE_INT32: + case IMAGING_TYPE_FLOAT32: + ImagingReduceNxN_32bpc(imOut, imIn, box, xscale, yscale); + + ImagingReduceCorners_32bpc(imOut, imIn, box, xscale, yscale); + break; + } + + ImagingSectionLeave(&cookie); + + return imOut; +} diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index d1a89e2ce..ec35303d8 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -13,28 +13,34 @@ struct filter { static inline double box_filter(double x) { - if (x >= -0.5 && x < 0.5) + if (x > -0.5 && x <= 0.5) { return 1.0; + } return 0.0; } static inline double bilinear_filter(double x) { - if (x < 0.0) + if (x < 0.0) { x = -x; - if (x < 1.0) + } + if (x < 1.0) { return 1.0-x; + } return 0.0; } static inline double hamming_filter(double x) { - if (x < 0.0) + if (x < 0.0) { x = -x; - if (x == 0.0) + } + if (x == 0.0) { return 1.0; - if (x >= 1.0) + } + if (x >= 1.0) { return 0.0; + } x = x * M_PI; return sin(x) / x * (0.54f + 0.46f * cos(x)); } @@ -43,20 +49,24 @@ static inline double bicubic_filter(double x) { /* https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm */ #define a -0.5 - if (x < 0.0) + if (x < 0.0) { x = -x; - if (x < 1.0) + } + if (x < 1.0) { return ((a + 2.0) * x - (a + 3.0)) * x*x + 1; - if (x < 2.0) + } + if (x < 2.0) { return (((x - 5) * x + 8) * x - 4) * a; + } return 0.0; #undef a } static inline double sinc_filter(double x) { - if (x == 0.0) + if (x == 0.0) { return 1.0; + } x = x * M_PI; return sin(x) / x; } @@ -64,8 +74,9 @@ static inline double sinc_filter(double x) static inline double lanczos_filter(double x) { /* truncated sinc */ - if (-3.0 <= x && x < 3.0) + if (-3.0 <= x && x < 3.0) { return sinc_filter(x) * sinc_filter(x/3); + } return 0.0; } @@ -224,12 +235,14 @@ precompute_coeffs(int inSize, float in0, float in1, int outSize, ss = 1.0 / filterscale; // Round the value xmin = (int) (center - support + 0.5); - if (xmin < 0) + if (xmin < 0) { xmin = 0; + } // Round the value xmax = (int) (center + support + 0.5); - if (xmax > inSize) + if (xmax > inSize) { xmax = inSize; + } xmax -= xmin; k = &kk[xx * ksize]; for (x = 0; x < xmax; x++) { @@ -238,8 +251,9 @@ precompute_coeffs(int inSize, float in0, float in1, int outSize, ww += w; } for (x = 0; x < xmax; x++) { - if (ww != 0.0) + if (ww != 0.0) { k[x] /= ww; + } } // Remaining values should stay empty if they are used despite of xmax. for (; x < ksize; x++) { @@ -295,8 +309,9 @@ ImagingResampleHorizontal_8bpc(Imaging imOut, Imaging imIn, int offset, xmax = bounds[xx * 2 + 1]; k = &kk[xx * ksize]; ss0 = 1 << (PRECISION_BITS -1); - for (x = 0; x < xmax; x++) + for (x = 0; x < xmax; x++) { ss0 += ((UINT8) imIn->image8[yy + offset][x + xmin]) * k[x]; + } imOut->image8[yy][xx] = clip8(ss0); } } @@ -379,8 +394,9 @@ ImagingResampleVertical_8bpc(Imaging imOut, Imaging imIn, int offset, ymax = bounds[yy * 2 + 1]; for (xx = 0; xx < imOut->xsize; xx++) { ss0 = 1 << (PRECISION_BITS -1); - for (y = 0; y < ymax; y++) + for (y = 0; y < ymax; y++) { ss0 += ((UINT8) imIn->image8[y + ymin][xx]) * k[y]; + } imOut->image8[yy][xx] = clip8(ss0); } } @@ -460,8 +476,9 @@ ImagingResampleHorizontal_32bpc(Imaging imOut, Imaging imIn, int offset, xmax = bounds[xx * 2 + 1]; k = &kk[xx * ksize]; ss = 0.0; - for (x = 0; x < xmax; x++) + for (x = 0; x < xmax; x++) { ss += IMAGING_PIXEL_I(imIn, x + xmin, yy + offset) * k[x]; + } IMAGING_PIXEL_I(imOut, xx, yy) = ROUND_UP(ss); } } @@ -474,8 +491,9 @@ ImagingResampleHorizontal_32bpc(Imaging imOut, Imaging imIn, int offset, xmax = bounds[xx * 2 + 1]; k = &kk[xx * ksize]; ss = 0.0; - for (x = 0; x < xmax; x++) + for (x = 0; x < xmax; x++) { ss += IMAGING_PIXEL_F(imIn, x + xmin, yy + offset) * k[x]; + } IMAGING_PIXEL_F(imOut, xx, yy) = ss; } } @@ -503,8 +521,9 @@ ImagingResampleVertical_32bpc(Imaging imOut, Imaging imIn, int offset, k = &kk[yy * ksize]; for (xx = 0; xx < imOut->xsize; xx++) { ss = 0.0; - for (y = 0; y < ymax; y++) + for (y = 0; y < ymax; y++) { ss += IMAGING_PIXEL_I(imIn, xx, y + ymin) * k[y]; + } IMAGING_PIXEL_I(imOut, xx, yy) = ROUND_UP(ss); } } @@ -517,8 +536,9 @@ ImagingResampleVertical_32bpc(Imaging imOut, Imaging imIn, int offset, k = &kk[yy * ksize]; for (xx = 0; xx < imOut->xsize; xx++) { ss = 0.0; - for (y = 0; y < ymax; y++) + for (y = 0; y < ymax; y++) { ss += IMAGING_PIXEL_F(imIn, xx, y + ymin) * k[y]; + } IMAGING_PIXEL_F(imOut, xx, yy) = ss; } } @@ -546,8 +566,9 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) ResampleFunction ResampleHorizontal; ResampleFunction ResampleVertical; - if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) + if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) { return (Imaging) ImagingError_ModeError(); + } if (imIn->type == IMAGING_TYPE_SPECIAL) { return (Imaging) ImagingError_ModeError(); diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index 8a81ba8e6..a03ecd456 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -6,7 +6,7 @@ * * history: * 2017-07-28 mb fixed for images larger than 64KB - * 2017-07-20 mb created + * 2017-07-20 mb created * * Copyright (c) Mickael Bonfill 2017. * @@ -25,18 +25,25 @@ static void read4B(UINT32* dest, UINT8* buf) *dest = (UINT32)((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]); } -static int expandrow(UINT8* dest, UINT8* src, int n, int z) +static int expandrow(UINT8* dest, UINT8* src, int n, int z, int xsize) { UINT8 pixel, count; + int x = 0; for (;n > 0; n--) { pixel = *src++; - if (n == 1 && pixel != 0) + if (n == 1 && pixel != 0) { return n; + } count = pixel & RLE_MAX_RUN; - if (!count) + if (!count) { return count; + } + if (x + count > xsize) { + return -1; + } + x += count; if (pixel & RLE_COPY_FLAG) { while(count--) { *dest = *src++; @@ -56,20 +63,27 @@ static int expandrow(UINT8* dest, UINT8* src, int n, int z) return 0; } -static int expandrow2(UINT8* dest, const UINT8* src, int n, int z) +static int expandrow2(UINT8* dest, const UINT8* src, int n, int z, int xsize) { UINT8 pixel, count; + int x = 0; for (;n > 0; n--) { pixel = src[1]; src+=2; - if (n == 1 && pixel != 0) + if (n == 1 && pixel != 0) { return n; + } count = pixel & RLE_MAX_RUN; - if (!count) + if (!count) { return count; + } + if (x + count > xsize) { + return -1; + } + x += count; if (pixel & RLE_COPY_FLAG) { while(count--) { memcpy(dest, src, 2); @@ -91,11 +105,12 @@ static int expandrow2(UINT8* dest, const UINT8* src, int n, int z) int ImagingSgiRleDecode(Imaging im, ImagingCodecState state, - UINT8* buf, Py_ssize_t bytes) + UINT8* buf, Py_ssize_t bytes) { UINT8 *ptr; SGISTATE *c; int err = 0; + int status; /* Get all data from File descriptor */ c = (SGISTATE*)state->context; @@ -140,11 +155,13 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, goto sgi_finish_decode; } /* populate offsets table */ - for (c->tabindex = 0, c->bufindex = 0; c->tabindex < c->tablen; c->tabindex++, c->bufindex+=4) + for (c->tabindex = 0, c->bufindex = 0; c->tabindex < c->tablen; c->tabindex++, c->bufindex+=4) { read4B(&c->starttab[c->tabindex], &ptr[c->bufindex]); + } /* populate lengths table */ - for (c->tabindex = 0, c->bufindex = c->tablen * sizeof(UINT32); c->tabindex < c->tablen; c->tabindex++, c->bufindex+=4) + for (c->tabindex = 0, c->bufindex = c->tablen * sizeof(UINT32); c->tabindex < c->tablen; c->tabindex++, c->bufindex+=4) { read4B(&c->lengthtab[c->tabindex], &ptr[c->bufindex]); + } state->count += c->tablen * sizeof(UINT32) * 2; @@ -164,12 +181,16 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, /* row decompression */ if (c->bpc ==1) { - if(expandrow(&state->buffer[c->channo], &ptr[c->rleoffset], c->rlelength, im->bands)) - goto sgi_finish_decode; + status = expandrow(&state->buffer[c->channo], &ptr[c->rleoffset], c->rlelength, im->bands, im->xsize); } else { - if(expandrow2(&state->buffer[c->channo * 2], &ptr[c->rleoffset], c->rlelength, im->bands)) - goto sgi_finish_decode; + status = expandrow2(&state->buffer[c->channo * 2], &ptr[c->rleoffset], c->rlelength, im->bands, im->xsize); + } + if (status == -1) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } else if (status == 1) { + goto sgi_finish_decode; } state->count += c->rlelength; diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index ab476939a..c9a24e6aa 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -244,17 +244,21 @@ ImagingNewPrologue(const char *mode, int xsize, int ysize) void ImagingDelete(Imaging im) { - if (!im) + if (!im) { return; + } - if (im->palette) + if (im->palette) { ImagingPaletteDelete(im->palette); + } - if (im->destroy) + if (im->destroy) { im->destroy(im); + } - if (im->image) + if (im->image) { free(im->image); + } free(im); } @@ -399,8 +403,9 @@ ImagingAllocateArray(Imaging im, int dirty, int block_size) aligned_linesize = (im->linesize + arena->alignment - 1) & -arena->alignment; lines_per_block = (block_size - (arena->alignment - 1)) / aligned_linesize; - if (lines_per_block == 0) + if (lines_per_block == 0) { lines_per_block = 1; + } blocks_count = (im->ysize + lines_per_block - 1) / lines_per_block; // printf("NEW size: %dx%d, ls: %d, lpb: %d, blocks: %d\n", // im->xsize, im->ysize, aligned_linesize, lines_per_block, blocks_count); @@ -457,8 +462,9 @@ ImagingAllocateArray(Imaging im, int dirty, int block_size) static void ImagingDestroyBlock(Imaging im) { - if (im->block) + if (im->block) { free(im->block); + } } Imaging @@ -510,8 +516,9 @@ ImagingNewInternal(const char* mode, int xsize, int ysize, int dirty) } im = ImagingNewPrologue(mode, xsize, ysize); - if ( ! im) + if ( ! im) { return NULL; + } if (ImagingAllocateArray(im, dirty, ImagingDefaultArena.block_size)) { return im; @@ -550,8 +557,9 @@ ImagingNewBlock(const char* mode, int xsize, int ysize) } im = ImagingNewPrologue(mode, xsize, ysize); - if ( ! im) + if ( ! im) { return NULL; + } if (ImagingAllocateBlock(im)) { return im; @@ -576,8 +584,9 @@ ImagingNew2Dirty(const char* mode, Imaging imOut, Imaging imIn) } else { /* create new image */ imOut = ImagingNewDirty(mode, imIn->xsize, imIn->ysize); - if (!imOut) + if (!imOut) { return NULL; + } } return imOut; @@ -587,8 +596,9 @@ void ImagingCopyPalette(Imaging destination, Imaging source) { if (source->palette) { - if (destination->palette) + if (destination->palette) { ImagingPaletteDelete(destination->palette); + } destination->palette = ImagingPaletteDuplicate(source->palette); } } diff --git a/src/libImaging/SunRleDecode.c b/src/libImaging/SunRleDecode.c index e627c2c9a..acb39133a 100644 --- a/src/libImaging/SunRleDecode.c +++ b/src/libImaging/SunRleDecode.c @@ -31,13 +31,15 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t for (;;) { - if (bytes < 1) + if (bytes < 1) { return ptr - buf; + } if (ptr[0] == 0x80) { - if (bytes < 2) + if (bytes < 2) { break; + } n = ptr[1]; @@ -55,8 +57,9 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t } else { /* Run (3 bytes) */ - if (bytes < 3) + if (bytes < 3) { break; + } /* from (https://www.fileformat.info/format/sunraster/egff.htm) diff --git a/src/libImaging/TgaRleDecode.c b/src/libImaging/TgaRleDecode.c index d1971e546..b1364e004 100644 --- a/src/libImaging/TgaRleDecode.c +++ b/src/libImaging/TgaRleDecode.c @@ -5,8 +5,8 @@ * decoder for Targa RLE data. * * history: - * 97-01-04 fl created - * 98-09-11 fl don't one byte per pixel; take orientation into account + * 97-01-04 fl created + * 98-09-11 fl don't one byte per pixel; take orientation into account * * Copyright (c) Fredrik Lundh 1997. * Copyright (c) Secret Labs AB 1997-98. @@ -20,7 +20,7 @@ int ImagingTgaRleDecode(Imaging im, ImagingCodecState state, - UINT8* buf, Py_ssize_t bytes) + UINT8* buf, Py_ssize_t bytes) { int n, depth; UINT8* ptr; @@ -29,14 +29,15 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, if (state->state == 0) { - /* check image orientation */ - if (state->ystep < 0) { - state->y = state->ysize-1; - state->ystep = -1; - } else - state->ystep = 1; + /* check image orientation */ + if (state->ystep < 0) { + state->y = state->ysize-1; + state->ystep = -1; + } else { + state->ystep = 1; + } - state->state = 1; + state->state = 1; } @@ -44,64 +45,68 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, for (;;) { - if (bytes < 1) - return ptr - buf; + if (bytes < 1) { + return ptr - buf; + } - if (ptr[0] & 0x80) { + if (ptr[0] & 0x80) { - /* Run (1 + pixelsize bytes) */ + /* Run (1 + pixelsize bytes) */ - if (bytes < 1 + depth) - break; + if (bytes < 1 + depth) { + break; + } - n = depth * ((ptr[0] & 0x7f) + 1); + n = depth * ((ptr[0] & 0x7f) + 1); - if (state->x + n > state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } + if (state->x + n > state->bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } - if (depth == 1) + if (depth == 1) { memset(state->buffer + state->x, ptr[1], n); - else { + } else { int i; - for (i = 0; i < n; i += depth) + for (i = 0; i < n; i += depth) { memcpy(state->buffer + state->x + i, ptr+1, depth); + } } ptr += 1 + depth; - bytes -= 1 + depth; + bytes -= 1 + depth; - } else { + } else { - /* Literal (1+n+1 bytes block) */ - n = depth * (ptr[0] + 1); + /* Literal (1+n+1 bytes block) */ + n = depth * (ptr[0] + 1); - if (bytes < 1 + n) - break; + if (bytes < 1 + n) { + break; + } - if (state->x + n > state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } + if (state->x + n > state->bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } - memcpy(state->buffer + state->x, ptr + 1, n); + memcpy(state->buffer + state->x, ptr + 1, n); - ptr += 1 + n; - bytes -= 1 + n; + ptr += 1 + n; + bytes -= 1 + n; - } + } - state->x += n; + state->x += n; - if (state->x >= state->bytes) { + if (state->x >= state->bytes) { - /* Got a full line, unpack it */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->buffer, - state->xsize); + /* Got a full line, unpack it */ + state->shuffle((UINT8*) im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, state->buffer, + state->xsize); - state->x = 0; + state->x = 0; state->y += state->ystep; @@ -110,7 +115,7 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, return -1; } - } + } } diff --git a/src/libImaging/TgaRleEncode.c b/src/libImaging/TgaRleEncode.c index 2fb831e6b..c65dcf5ec 100644 --- a/src/libImaging/TgaRleEncode.c +++ b/src/libImaging/TgaRleEncode.c @@ -22,8 +22,9 @@ ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) if (state->ystep < 0) { state->ystep = -1; state->y = state->ysize - 1; - } else + } else { state->ystep = 1; + } state->state = 1; } @@ -46,8 +47,9 @@ ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) assert(state->x <= state->xsize); /* Make sure we have space for the descriptor. */ - if (bytes < 1) + if (bytes < 1) { break; + } if (state->x == state->xsize) { state->x = 0; @@ -59,12 +61,13 @@ ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) } } - if (state->x == 0) + if (state->x == 0) { state->shuffle( state->buffer, (UINT8*)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->xsize); + } row = state->buffer; @@ -87,28 +90,32 @@ ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) */ maxLookup = state->x + 126; /* A packet must not span multiple rows. */ - if (maxLookup > state->xsize - 1) + if (maxLookup > state->xsize - 1) { maxLookup = state->xsize - 1; + } if (isRaw) { - while (state->x < maxLookup) - if (!comparePixels(row, state->x, bytesPerPixel)) + while (state->x < maxLookup) { + if (!comparePixels(row, state->x, bytesPerPixel)) { ++state->x; - else { + } else { /* Two identical pixels will go to RLE packet. */ --state->x; break; } + } state->count += (state->x - startX) * bytesPerPixel; } else { descriptor |= 0x80; - while (state->x < maxLookup) - if (comparePixels(row, state->x, bytesPerPixel)) + while (state->x < maxLookup) { + if (comparePixels(row, state->x, bytesPerPixel)) { ++state->x; - else + } else { break; + } + } } } @@ -132,12 +139,14 @@ ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) assert(state->x > 0); assert(state->count <= state->x * bytesPerPixel); - if (bytes == 0) + if (bytes == 0) { break; + } flushCount = state->count; - if (flushCount > bytes) + if (flushCount > bytes) { flushCount = bytes; + } memcpy( dst, diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 7592f7f39..bdb524bec 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -20,6 +20,17 @@ #include "TiffDecode.h" +/* Convert C file descriptor to WinApi HFILE if LibTiff was compiled with tif_win32.c + * + * This cast is safe, as the top 32-bits of HFILE are guaranteed to be zero, + * see https://docs.microsoft.com/en-us/windows/win32/winprog64/interprocess-communication + */ +#ifndef USE_WIN32_FILEIO +#define fd_to_tiff_fd(fd) (fd) +#else +#define fd_to_tiff_fd(fd) ((int)_get_osfhandle(fd)) +#endif + void dump_state(const TIFFSTATE *state){ TRACE(("State: Location %u size %d eof %d data: %p ifd: %d\n", (uint)state->loc, (int)state->size, (uint)state->eof, state->data, state->ifd)); @@ -171,7 +182,7 @@ int ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset) { int ReadTile(TIFF* tiff, UINT32 col, UINT32 row, UINT32* buffer) { - uint16 photometric; + uint16 photometric = 0; TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); @@ -228,7 +239,7 @@ int ReadTile(TIFF* tiff, UINT32 col, UINT32 row, UINT32* buffer) { } int ReadStrip(TIFF* tiff, UINT32 row, UINT32* buffer) { - uint16 photometric; + uint16 photometric = 0; // init to not PHOTOMETRIC_YCBCR TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); // To avoid dealing with YCbCr subsampling, let libtiff handle it @@ -316,7 +327,7 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ if (clientstate->fp) { TRACE(("Opening using fd: %d\n",clientstate->fp)); lseek(clientstate->fp,0,SEEK_SET); // Sometimes, I get it set to the end. - tiff = TIFFFdOpen(clientstate->fp, filename, mode); + tiff = TIFFFdOpen(fd_to_tiff_fd(clientstate->fp), filename, mode); } else { TRACE(("Opening from string\n")); tiff = TIFFClientOpen(filename, mode, @@ -344,25 +355,41 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ } if (TIFFIsTiled(tiff)) { - UINT32 x, y, tile_y, row_byte_size; - UINT32 tile_width, tile_length, current_tile_width; + INT32 x, y, tile_y; + UINT32 tile_width, tile_length, current_tile_width, row_byte_size; UINT8 *new_data; TIFFGetField(tiff, TIFFTAG_TILEWIDTH, &tile_width); TIFFGetField(tiff, TIFFTAG_TILELENGTH, &tile_length); + /* overflow check for row_byte_size calculation */ + if ((UINT32) INT_MAX / state->bits < tile_width) { + state->errcode = IMAGING_CODEC_MEMORY; + TIFFClose(tiff); + return -1; + } + // We could use TIFFTileSize, but for YCbCr data it returns subsampled data size row_byte_size = (tile_width * state->bits + 7) / 8; + + /* overflow check for realloc */ + if (INT_MAX / row_byte_size < tile_length) { + state->errcode = IMAGING_CODEC_MEMORY; + TIFFClose(tiff); + return -1; + } + state->bytes = row_byte_size * tile_length; - /* overflow check for malloc */ - if (state->bytes > INT_MAX - 1) { + if (TIFFTileSize(tiff) > state->bytes) { + // If the strip size as expected by LibTiff isn't what we're expecting, abort. state->errcode = IMAGING_CODEC_MEMORY; TIFFClose(tiff); return -1; } /* realloc to fit whole tile */ + /* malloc check above */ new_data = realloc (state->buffer, state->bytes); if (!new_data) { state->errcode = IMAGING_CODEC_MEMORY; @@ -385,10 +412,10 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ TRACE(("Read tile at %dx%d; \n\n", x, y)); - current_tile_width = min(tile_width, state->xsize - x); + current_tile_width = min((INT32) tile_width, state->xsize - x); // iterate over each line in the tile and stuff data into image - for (tile_y = 0; tile_y < min(tile_length, state->ysize - y); tile_y++) { + for (tile_y = 0; tile_y < min((INT32) tile_length, state->ysize - y); tile_y++) { TRACE(("Writing tile data at %dx%d using tile_width: %d; \n", tile_y + y, x, current_tile_width)); // UINT8 * bbb = state->buffer + tile_y * row_byte_size; @@ -402,9 +429,9 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ } } } else { - UINT32 strip_row, row_byte_size; + INT32 strip_row; UINT8 *new_data; - UINT32 rows_per_strip; + UINT32 rows_per_strip, row_byte_size; int ret; ret = TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); @@ -415,11 +442,30 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ // We could use TIFFStripSize, but for YCbCr data it returns subsampled data size row_byte_size = (state->xsize * state->bits + 7) / 8; + + /* overflow check for realloc */ + if (INT_MAX / row_byte_size < rows_per_strip) { + state->errcode = IMAGING_CODEC_MEMORY; + TIFFClose(tiff); + return -1; + } + state->bytes = rows_per_strip * row_byte_size; TRACE(("StripSize: %d \n", state->bytes)); + if (TIFFStripSize(tiff) > state->bytes) { + // If the strip size as expected by LibTiff isn't what we're expecting, abort. + // man: TIFFStripSize returns the equivalent size for a strip of data as it would be returned in a + // call to TIFFReadEncodedStrip ... + + state->errcode = IMAGING_CODEC_MEMORY; + TIFFClose(tiff); + return -1; + } + /* realloc to fit whole strip */ + /* malloc check above */ new_data = realloc (state->buffer, state->bytes); if (!new_data) { state->errcode = IMAGING_CODEC_MEMORY; @@ -440,7 +486,7 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ TRACE(("Decoded strip for row %d \n", state->y)); // iterate over each row in the strip and stuff data into image - for (strip_row = 0; strip_row < min(rows_per_strip, state->ysize - state->y); strip_row++) { + for (strip_row = 0; strip_row < min((INT32) rows_per_strip, state->ysize - state->y); strip_row++) { TRACE(("Writing data into line %d ; \n", state->y + strip_row)); // UINT8 * bbb = state->buffer + strip_row * (state->bytes / rows_per_strip); @@ -493,7 +539,7 @@ int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { if (fp) { TRACE(("Opening using fd: %d for writing \n",clientstate->fp)); - clientstate->tiff = TIFFFdOpen(clientstate->fp, filename, mode); + clientstate->tiff = TIFFFdOpen(fd_to_tiff_fd(clientstate->fp), filename, mode); } else { // malloc a buffer to write the tif, we're going to need to realloc or something if we need bigger. TRACE(("Opening a buffer for writing \n")); diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index 08ef35cfd..3d27e65b6 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -28,17 +28,17 @@ #define _PIL_LIBTIFF_ typedef struct { - tdata_t data; /* tdata_t == void* */ - toff_t loc; /* toff_t == uint32 */ - tsize_t size; /* tsize_t == int32 */ - int fp; + tdata_t data; /* tdata_t == void* */ + toff_t loc; /* toff_t == uint32 */ + tsize_t size; /* tsize_t == int32 */ + int fp; uint32 ifd; /* offset of the ifd, used for multipage * Should be uint32 for libtiff 3.9.x * uint64 for libtiff 4.0.x */ - TIFF *tiff; /* Used in write */ - toff_t eof; - int flrealloc;/* may we realloc */ + TIFF *tiff; /* Used in write */ + toff_t eof; + int flrealloc;/* may we realloc */ } TIFFSTATE; @@ -55,7 +55,7 @@ extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); */ /* -#define VA_ARGS(...) __VA_ARGS__ +#define VA_ARGS(...) __VA_ARGS__ #define TRACE(args) fprintf(stderr, VA_ARGS args) */ diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index ab0c8dc60..917da6ab3 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -350,8 +350,9 @@ unpackLI(UINT8* out, const UINT8* in, int pixels) { /* negative */ int i; - for (i = 0; i < pixels; i++) + for (i = 0; i < pixels; i++) { out[i] = ~in[i]; + } } static void @@ -998,7 +999,7 @@ unpackI16N_I16B(UINT8* out, const UINT8* in, int pixels){ UINT8* tmp = (UINT8*) out; for (i = 0; i < pixels; i++) { C16B; - in += 2; tmp += 2; + in += 2; tmp += 2; } } @@ -1008,7 +1009,7 @@ unpackI16N_I16(UINT8* out, const UINT8* in, int pixels){ UINT8* tmp = (UINT8*) out; for (i = 0; i < pixels; i++) { C16L; - in += 2; tmp += 2; + in += 2; tmp += 2; } } @@ -1053,7 +1054,7 @@ unpackI12_I16(UINT8* out, const UINT8* in, int pixels){ memcpy(out, &pixel, sizeof(pixel)); #endif - in += 3; out+=2; + in += 3; out+=2; } if (i == pixels-1) { pixel = (((UINT16) in[0]) << 4 ) + (in[1] >>4); @@ -1115,8 +1116,9 @@ static void NAME(UINT8* out_, const UINT8* in, int pixels)\ {\ int i;\ OUTTYPE* out = (OUTTYPE*) out_;\ - for (i = 0; i < pixels; i++, in += sizeof(INTYPE))\ + for (i = 0; i < pixels; i++, in += sizeof(INTYPE)) {\ out[i] = (OUTTYPE) ((INTYPE) GET);\ + }\ } #define UNPACK(NAME, COPY, INTYPE, OUTTYPE)\ @@ -1307,6 +1309,9 @@ static struct { {"LA", "LA", 16, unpackLA}, {"LA", "LA;L", 16, unpackLAL}, + /* greyscale w. alpha premultiplied */ + {"La", "La", 16, unpackLA}, + /* palette */ {"P", "P;1", 1, unpackP1}, {"P", "P;2", 2, unpackP2}, @@ -1384,7 +1389,6 @@ static struct { {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, #endif - /* true colour w. alpha premultiplied */ {"RGBa", "RGBa", 32, copy4}, {"RGBa", "BGRa", 32, unpackBGRA}, @@ -1445,16 +1449,16 @@ static struct { {"YCbCr", "YCbCrK", 32, copy4}, /* LAB Color */ - {"LAB", "LAB", 24, ImagingUnpackLAB}, - {"LAB", "L", 8, band0}, - {"LAB", "A", 8, band1}, - {"LAB", "B", 8, band2}, + {"LAB", "LAB", 24, ImagingUnpackLAB}, + {"LAB", "L", 8, band0}, + {"LAB", "A", 8, band1}, + {"LAB", "B", 8, band2}, /* HSV Color */ - {"HSV", "HSV", 24, ImagingUnpackRGB}, - {"HSV", "H", 8, band0}, - {"HSV", "S", 8, band1}, - {"HSV", "V", 8, band2}, + {"HSV", "HSV", 24, ImagingUnpackRGB}, + {"HSV", "H", 8, band0}, + {"HSV", "S", 8, band1}, + {"HSV", "V", 8, band2}, /* integer variations */ {"I", "I", 32, copy4}, @@ -1503,11 +1507,11 @@ static struct { {"I;16B", "I;16B", 16, copy2}, {"I;16L", "I;16L", 16, copy2}, - {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. - {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. - {"I;16B", "I;16N", 16, unpackI16N_I16B}, + {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. + {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. + {"I;16B", "I;16N", 16, unpackI16N_I16B}, - {"I;16", "I;12", 12, unpackI12_I16}, // 12 bit Tiffs stored in 16bits. + {"I;16", "I;12", 12, unpackI12_I16}, // 12 bit Tiffs stored in 16bits. {NULL} /* sentinel */ }; @@ -1519,13 +1523,15 @@ ImagingFindUnpacker(const char* mode, const char* rawmode, int* bits_out) int i; /* find a suitable pixel unpacker */ - for (i = 0; unpackers[i].rawmode; i++) + for (i = 0; unpackers[i].rawmode; i++) { if (strcmp(unpackers[i].mode, mode) == 0 && strcmp(unpackers[i].rawmode, rawmode) == 0) { - if (bits_out) + if (bits_out) { *bits_out = unpackers[i].bits; + } return unpackers[i].unpack; } + } /* FIXME: configure a general unpacker based on the type codes... */ diff --git a/src/libImaging/UnpackYCC.c b/src/libImaging/UnpackYCC.c index 19da1f654..d6bce17ad 100644 --- a/src/libImaging/UnpackYCC.c +++ b/src/libImaging/UnpackYCC.c @@ -5,7 +5,7 @@ * code to convert and unpack PhotoYCC data * * history: - * 97-01-25 fl Moved from PcdDecode.c + * 97-01-25 fl Moved from PcdDecode.c * * Copyright (c) Fredrik Lundh 1996-97. * Copyright (c) Secret Labs AB 1997. @@ -116,12 +116,12 @@ static INT16 GR[] = { 127, 126, 125, 124, 123, 122, 121, 121, 120, 119, -85, -86, -87, -88, -89, -90, -91, -92, -93, -94, -94, -95, -96, -97, -98, -99, -100, -101, -102, -103, -104, -105, -106, -107, -107, -108 }; -#define R 0 -#define G 1 -#define B 2 -#define A 3 +#define R 0 +#define G 1 +#define B 2 +#define A 3 -#define YCC2RGB(rgb, y, cb, cr) {\ +#define YCC2RGB(rgb, y, cb, cr) {\ int l = L[y];\ int r = l + CR[cr];\ int g = l + GR[cr] + GB[cb];\ @@ -137,9 +137,9 @@ ImagingUnpackYCC(UINT8* out, const UINT8* in, int pixels) int i; /* PhotoYCC triplets */ for (i = 0; i < pixels; i++) { - YCC2RGB(out, in[0], in[1], in[2]); - out[A] = 255; - out += 4; in += 3; + YCC2RGB(out, in[0], in[1], in[2]); + out[A] = 255; + out += 4; in += 3; } } @@ -149,14 +149,14 @@ ImagingUnpackYCCA(UINT8* out, const UINT8* in, int pixels) int i; /* PhotoYCC triplets plus premultiplied alpha */ for (i = 0; i < pixels; i++) { - /* Divide by alpha */ - UINT8 rgb[3]; - rgb[0] = (in[3] == 0) ? 0 : (((int) in[0] * 255) / in[3]); - rgb[1] = (in[3] == 0) ? 0 : (((int) in[1] * 255) / in[3]); - rgb[2] = (in[3] == 0) ? 0 : (((int) in[2] * 255) / in[3]); - /* Convert non-multiplied data to RGB */ - YCC2RGB(out, rgb[0], rgb[1], rgb[2]); - out[A] = in[3]; - out += 4; in += 4; + /* Divide by alpha */ + UINT8 rgb[3]; + rgb[0] = (in[3] == 0) ? 0 : (((int) in[0] * 255) / in[3]); + rgb[1] = (in[3] == 0) ? 0 : (((int) in[1] * 255) / in[3]); + rgb[2] = (in[3] == 0) ? 0 : (((int) in[2] * 255) / in[3]); + /* Convert non-multiplied data to RGB */ + YCC2RGB(out, rgb[0], rgb[1], rgb[2]); + out[A] = in[3]; + out += 4; in += 4; } } diff --git a/src/libImaging/UnsharpMask.c b/src/libImaging/UnsharpMask.c index a034bebf2..59e595e82 100644 --- a/src/libImaging/UnsharpMask.c +++ b/src/libImaging/UnsharpMask.c @@ -14,10 +14,12 @@ typedef UINT8 pixel[4]; static inline UINT8 clip8(int in) { - if (in >= 255) + if (in >= 255) { return 255; - if (in <= 0) + } + if (in <= 0) { return 0; + } return (UINT8) in; } @@ -39,8 +41,9 @@ ImagingUnsharpMask(Imaging imOut, Imaging imIn, float radius, int percent, /* First, do a gaussian blur on the image, putting results in imOut temporarily. All format checks are in gaussian blur. */ result = ImagingGaussianBlur(imOut, imIn, radius, 3); - if (!result) + if (!result) { return NULL; + } /* Now, go through each pixel, compare "normal" pixel to blurred pixel. If the difference is more than threshold values, apply diff --git a/src/libImaging/XbmDecode.c b/src/libImaging/XbmDecode.c index 75b4961ab..607f1058a 100644 --- a/src/libImaging/XbmDecode.c +++ b/src/libImaging/XbmDecode.c @@ -5,7 +5,7 @@ * decoder for XBM hex image data * * history: - * 96-04-13 fl Created + * 96-04-13 fl Created * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -16,9 +16,9 @@ #include "Imaging.h" -#define HEX(v) ((v >= '0' && v <= '9') ? v - '0' :\ - (v >= 'a' && v <= 'f') ? v - 'a' + 10 :\ - (v >= 'A' && v <= 'F') ? v - 'A' + 10 : 0) +#define HEX(v) ((v >= '0' && v <= '9') ? v - '0' :\ + (v >= 'a' && v <= 'f') ? v - 'a' + 10 :\ + (v >= 'A' && v <= 'F') ? v - 'A' + 10 : 0) int ImagingXbmDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) @@ -27,54 +27,58 @@ ImagingXbmDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt UINT8* ptr; - if (!state->state) - state->state = SKIP; + if (!state->state) { + state->state = SKIP; + } ptr = buf; for (;;) { - if (state->state == SKIP) { + if (state->state == SKIP) { - /* Skip forward until next 'x' */ + /* Skip forward until next 'x' */ - while (bytes > 0) { - if (*ptr == 'x') - break; - ptr++; - bytes--; - } + while (bytes > 0) { + if (*ptr == 'x') { + break; + } + ptr++; + bytes--; + } - if (bytes == 0) - return ptr - buf; + if (bytes == 0) { + return ptr - buf; + } - state->state = BYTE; + state->state = BYTE; - } + } - if (bytes < 3) - return ptr - buf; + if (bytes < 3) { + return ptr - buf; + } - state->buffer[state->x] = (HEX(ptr[1])<<4) + HEX(ptr[2]); + state->buffer[state->x] = (HEX(ptr[1])<<4) + HEX(ptr[2]); - if (++state->x >= state->bytes) { + if (++state->x >= state->bytes) { - /* Got a full line, unpack it */ - state->shuffle((UINT8*) im->image[state->y], state->buffer, - state->xsize); + /* Got a full line, unpack it */ + state->shuffle((UINT8*) im->image[state->y], state->buffer, + state->xsize); - state->x = 0; + state->x = 0; - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } - ptr += 3; - bytes -= 3; + ptr += 3; + bytes -= 3; - state->state = SKIP; + state->state = SKIP; } diff --git a/src/libImaging/XbmEncode.c b/src/libImaging/XbmEncode.c index e066fd6b5..d1bc086db 100644 --- a/src/libImaging/XbmEncode.c +++ b/src/libImaging/XbmEncode.c @@ -5,7 +5,7 @@ * encoder for Xbm data * * history: - * 96-11-01 fl created + * 96-11-01 fl created * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -27,79 +27,80 @@ ImagingXbmEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) if (!state->state) { - /* 8 pixels are stored in no more than 6 bytes */ - state->bytes = 6*(state->xsize+7)/8; + /* 8 pixels are stored in no more than 6 bytes */ + state->bytes = 6*(state->xsize+7)/8; - state->state = 1; + state->state = 1; } if (bytes < state->bytes) { - state->errcode = IMAGING_CODEC_MEMORY; - return 0; + state->errcode = IMAGING_CODEC_MEMORY; + return 0; } ptr = buf; while (bytes >= state->bytes) { - state->shuffle(state->buffer, - (UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->xsize); + state->shuffle(state->buffer, + (UINT8*) im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, state->xsize); - if (state->y < state->ysize-1) { + if (state->y < state->ysize-1) { - /* any line but the last */ - for (n = 0; n < state->xsize; n += 8) { + /* any line but the last */ + for (n = 0; n < state->xsize; n += 8) { - i = state->buffer[n/8]; + i = state->buffer[n/8]; - *ptr++ = '0'; - *ptr++ = 'x'; - *ptr++ = hex[(i>>4)&15]; - *ptr++ = hex[i&15]; - *ptr++ = ','; - bytes -= 5; + *ptr++ = '0'; + *ptr++ = 'x'; + *ptr++ = hex[(i>>4)&15]; + *ptr++ = hex[i&15]; + *ptr++ = ','; + bytes -= 5; - if (++state->count >= 79/5) { - *ptr++ = '\n'; - bytes--; - state->count = 0; - } + if (++state->count >= 79/5) { + *ptr++ = '\n'; + bytes--; + state->count = 0; + } - } + } - state->y++; + state->y++; - } else { + } else { - /* last line */ - for (n = 0; n < state->xsize; n += 8) { + /* last line */ + for (n = 0; n < state->xsize; n += 8) { - i = state->buffer[n/8]; + i = state->buffer[n/8]; - *ptr++ = '0'; - *ptr++ = 'x'; - *ptr++ = hex[(i>>4)&15]; - *ptr++ = hex[i&15]; + *ptr++ = '0'; + *ptr++ = 'x'; + *ptr++ = hex[(i>>4)&15]; + *ptr++ = hex[i&15]; - if (n < state->xsize-8) { - *ptr++ = ','; - if (++state->count >= 79/5) { - *ptr++ = '\n'; - bytes--; - state->count = 0; - } - } else - *ptr++ = '\n'; + if (n < state->xsize-8) { + *ptr++ = ','; + if (++state->count >= 79/5) { + *ptr++ = '\n'; + bytes--; + state->count = 0; + } + } else { + *ptr++ = '\n'; + } - bytes -= 5; + bytes -= 5; - } + } - state->errcode = IMAGING_CODEC_END; - break; - } + state->errcode = IMAGING_CODEC_END; + break; + } } return ptr - buf; diff --git a/src/libImaging/Zip.h b/src/libImaging/Zip.h deleted file mode 100644 index 21a336f90..000000000 --- a/src/libImaging/Zip.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * declarations for the ZIP codecs - * - * Copyright (c) Fredrik Lundh 1996. - */ - - -#include "zlib.h" - - -/* modes */ -#define ZIP_PNG 0 /* continuous, filtered image data */ -#define ZIP_PNG_PALETTE 1 /* non-continuous data, disable filtering */ -#define ZIP_TIFF_PREDICTOR 2 /* TIFF, with predictor */ -#define ZIP_TIFF 3 /* TIFF, without predictor */ - - -typedef struct { - - /* CONFIGURATION */ - - /* Codec mode */ - int mode; - - /* Optimize (max compression) SLOW!!! */ - int optimize; - - /* 0 no compression, 9 best compression, -1 default compression */ - int compress_level; - /* compression strategy Z_XXX */ - int compress_type; - - /* Predefined dictionary (experimental) */ - char* dictionary; - int dictionary_size; - - /* PRIVATE CONTEXT (set by decoder/encoder) */ - - z_stream z_stream; /* (de)compression stream */ - - UINT8* previous; /* previous line (allocated) */ - - int last_output; /* # bytes last output by inflate */ - - /* Compressor specific stuff */ - UINT8* prior; /* filter storage (allocated) */ - UINT8* up; - UINT8* average; - UINT8* paeth; - - UINT8* output; /* output data */ - - int prefix; /* size of filter prefix (0 for TIFF data) */ - - int interlaced; /* is the image interlaced? (PNG) */ - - int pass; /* current pass of the interlaced image (PNG) */ - -} ZIPSTATE; diff --git a/src/libImaging/ZipCodecs.h b/src/libImaging/ZipCodecs.h new file mode 100644 index 000000000..b05c93bb4 --- /dev/null +++ b/src/libImaging/ZipCodecs.h @@ -0,0 +1,62 @@ +/* + * The Python Imaging Library. + * $Id$ + * + * declarations for the ZIP codecs + * + * Copyright (c) Fredrik Lundh 1996. + */ + + +#include "zlib.h" + + +/* modes */ +#define ZIP_PNG 0 /* continuous, filtered image data */ +#define ZIP_PNG_PALETTE 1 /* non-continuous data, disable filtering */ +#define ZIP_TIFF_PREDICTOR 2 /* TIFF, with predictor */ +#define ZIP_TIFF 3 /* TIFF, without predictor */ + + +typedef struct { + + /* CONFIGURATION */ + + /* Codec mode */ + int mode; + + /* Optimize (max compression) SLOW!!! */ + int optimize; + + /* 0 no compression, 9 best compression, -1 default compression */ + int compress_level; + /* compression strategy Z_XXX */ + int compress_type; + + /* Predefined dictionary (experimental) */ + char* dictionary; + int dictionary_size; + + /* PRIVATE CONTEXT (set by decoder/encoder) */ + + z_stream z_stream; /* (de)compression stream */ + + UINT8* previous; /* previous line (allocated) */ + + int last_output; /* # bytes last output by inflate */ + + /* Compressor specific stuff */ + UINT8* prior; /* filter storage (allocated) */ + UINT8* up; + UINT8* average; + UINT8* paeth; + + UINT8* output; /* output data */ + + int prefix; /* size of filter prefix (0 for TIFF data) */ + + int interlaced; /* is the image interlaced? (PNG) */ + + int pass; /* current pass of the interlaced image (PNG) */ + +} ZIPSTATE; diff --git a/src/libImaging/ZipDecode.c b/src/libImaging/ZipDecode.c index 43601c38e..a09ee82f7 100644 --- a/src/libImaging/ZipDecode.c +++ b/src/libImaging/ZipDecode.c @@ -20,7 +20,7 @@ #ifdef HAVE_LIBZ -#include "Zip.h" +#include "ZipCodecs.h" static const int OFFSET[] = { 7, 3, 3, 1, 1, 0, 0 }; static const int STARTING_COL[] = { 0, 4, 0, 2, 0, 1, 0 }; @@ -53,8 +53,9 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt if (!state->state) { /* Initialization */ - if (context->mode == ZIP_PNG || context->mode == ZIP_PNG_PALETTE) + if (context->mode == ZIP_PNG || context->mode == ZIP_PNG_PALETTE) { context->prefix = 1; /* PNG */ + } /* overflow check for malloc */ if (state->bytes > INT_MAX - 1) { @@ -121,12 +122,13 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt if (err < 0) { /* Something went wrong inside the compression library */ - if (err == Z_DATA_ERROR) + if (err == Z_DATA_ERROR) { state->errcode = IMAGING_CODEC_BROKEN; - else if (err == Z_MEM_ERROR) + } else if (err == Z_MEM_ERROR) { state->errcode = IMAGING_CODEC_MEMORY; - else + } else { state->errcode = IMAGING_CODEC_CONFIG; + } free(context->previous); context->previous = NULL; inflateEnd(&context->z_stream); @@ -149,28 +151,33 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt case 1: /* prior */ bpp = (state->bits + 7) / 8; - for (i = bpp+1; i <= row_len; i++) + for (i = bpp+1; i <= row_len; i++) { state->buffer[i] += state->buffer[i-bpp]; + } break; case 2: /* up */ - for (i = 1; i <= row_len; i++) + for (i = 1; i <= row_len; i++) { state->buffer[i] += context->previous[i]; + } break; case 3: /* average */ bpp = (state->bits + 7) / 8; - for (i = 1; i <= bpp; i++) + for (i = 1; i <= bpp; i++) { state->buffer[i] += context->previous[i]/2; - for (; i <= row_len; i++) + } + for (; i <= row_len; i++) { state->buffer[i] += (state->buffer[i-bpp] + context->previous[i])/2; + } break; case 4: /* paeth filtering */ bpp = (state->bits + 7) / 8; - for (i = 1; i <= bpp; i++) + for (i = 1; i <= bpp; i++) { state->buffer[i] += context->previous[i]; + } for (; i <= row_len; i++) { int a, b, c; int pa, pb, pc; @@ -201,8 +208,9 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt break; case ZIP_TIFF_PREDICTOR: bpp = (state->bits + 7) / 8; - for (i = bpp+1; i <= row_len; i++) + for (i = bpp+1; i <= row_len; i++) { state->buffer[i] += state->buffer[i-bpp]; + } break; } diff --git a/src/libImaging/ZipEncode.c b/src/libImaging/ZipEncode.c index fa1c4e728..4e862af57 100644 --- a/src/libImaging/ZipEncode.c +++ b/src/libImaging/ZipEncode.c @@ -19,7 +19,7 @@ #ifdef HAVE_LIBZ -#include "Zip.h" +#include "ZipCodecs.h" int ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) @@ -128,12 +128,13 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) if (err < 0) { /* Something went wrong inside the compression library */ - if (err == Z_DATA_ERROR) + if (err == Z_DATA_ERROR) { state->errcode = IMAGING_CODEC_BROKEN; - else if (err == Z_MEM_ERROR) + } else if (err == Z_MEM_ERROR) { state->errcode = IMAGING_CODEC_MEMORY; - else + } else { state->errcode = IMAGING_CODEC_CONFIG; + } free(context->paeth); free(context->average); free(context->up); @@ -282,12 +283,13 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) if (err < 0) { /* Something went wrong inside the compression library */ - if (err == Z_DATA_ERROR) + if (err == Z_DATA_ERROR) { state->errcode = IMAGING_CODEC_BROKEN; - else if (err == Z_MEM_ERROR) + } else if (err == Z_MEM_ERROR) { state->errcode = IMAGING_CODEC_MEMORY; - else + } else { state->errcode = IMAGING_CODEC_CONFIG; + } free(context->paeth); free(context->average); free(context->up); @@ -305,8 +307,9 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) } - if (context->z_stream.avail_out == 0) + if (context->z_stream.avail_out == 0) { break; /* Buffer full */ + } case 2: @@ -331,8 +334,9 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) break; } - if (context->z_stream.avail_out == 0) + if (context->z_stream.avail_out == 0) { break; /* Buffer full */ + } } @@ -369,7 +373,7 @@ ImagingZipEncodeCleanup(ImagingCodecState state) { const char* ImagingZipVersion(void) { - return ZLIB_VERSION; + return zlibVersion(); } #endif diff --git a/src/map.c b/src/map.c index a8fb69c6e..df2718ed7 100644 --- a/src/map.c +++ b/src/map.c @@ -20,7 +20,7 @@ #include "Python.h" -#include "Imaging.h" +#include "libImaging/Imaging.h" /* compatibility wrappers (defined in _imaging.c) */ extern int PyImaging_CheckBuffer(PyObject* buffer); @@ -47,12 +47,14 @@ PyImaging_MapperNew(const char* filename, int readonly) { ImagingMapperObject *mapper; - if (PyType_Ready(&ImagingMapperType) < 0) + if (PyType_Ready(&ImagingMapperType) < 0) { return NULL; + } mapper = PyObject_New(ImagingMapperObject, &ImagingMapperType); - if (mapper == NULL) + if (mapper == NULL) { return NULL; + } mapper->base = NULL; mapper->size = mapper->offset = 0; @@ -70,7 +72,7 @@ PyImaging_MapperNew(const char* filename, int readonly) FILE_ATTRIBUTE_NORMAL, NULL); if (mapper->hFile == (HANDLE)-1) { - PyErr_SetString(PyExc_IOError, "cannot open file"); + PyErr_SetString(PyExc_OSError, "cannot open file"); Py_DECREF(mapper); return NULL; } @@ -81,7 +83,7 @@ PyImaging_MapperNew(const char* filename, int readonly) 0, 0, NULL); if (mapper->hMap == (HANDLE)-1) { CloseHandle(mapper->hFile); - PyErr_SetString(PyExc_IOError, "cannot map file"); + PyErr_SetString(PyExc_OSError, "cannot map file"); Py_DECREF(mapper); return NULL; } @@ -101,12 +103,15 @@ static void mapping_dealloc(ImagingMapperObject* mapper) { #ifdef _WIN32 - if (mapper->base != 0) + if (mapper->base != 0) { UnmapViewOfFile(mapper->base); - if (mapper->hMap != (HANDLE)-1) + } + if (mapper->hMap != (HANDLE)-1) { CloseHandle(mapper->hMap); - if (mapper->hFile != (HANDLE)-1) + } + if (mapper->hFile != (HANDLE)-1) { CloseHandle(mapper->hFile); + } mapper->base = 0; mapper->hMap = mapper->hFile = (HANDLE)-1; #endif @@ -122,18 +127,22 @@ mapping_read(ImagingMapperObject* mapper, PyObject* args) PyObject* buf; int size = -1; - if (!PyArg_ParseTuple(args, "|i", &size)) + if (!PyArg_ParseTuple(args, "|i", &size)) { return NULL; + } /* check size */ - if (size < 0 || mapper->offset + size > mapper->size) + if (size < 0 || mapper->offset + size > mapper->size) { size = mapper->size - mapper->offset; - if (size < 0) + } + if (size < 0) { size = 0; + } buf = PyBytes_FromStringAndSize(NULL, size); - if (!buf) + if (!buf) { return NULL; + } if (size > 0) { memcpy(PyBytes_AsString(buf), mapper->base + mapper->offset, size); @@ -148,8 +157,9 @@ mapping_seek(ImagingMapperObject* mapper, PyObject* args) { int offset; int whence = 0; - if (!PyArg_ParseTuple(args, "i|i", &offset, &whence)) + if (!PyArg_ParseTuple(args, "i|i", &offset, &whence)) { return NULL; + } switch (whence) { case 0: /* SEEK_SET */ @@ -193,37 +203,43 @@ mapping_readimage(ImagingMapperObject* mapper, PyObject* args) int stride; int orientation; if (!PyArg_ParseTuple(args, "s(ii)ii", &mode, &xsize, &ysize, - &stride, &orientation)) + &stride, &orientation)) { return NULL; + } if (stride <= 0) { /* FIXME: maybe we should call ImagingNewPrologue instead */ - if (!strcmp(mode, "L") || !strcmp(mode, "P")) + if (!strcmp(mode, "L") || !strcmp(mode, "P")) { stride = xsize; - else if (!strcmp(mode, "I;16") || !strcmp(mode, "I;16B")) + } else if (!strcmp(mode, "I;16") || !strcmp(mode, "I;16B")) { stride = xsize * 2; - else + } else { stride = xsize * 4; + } } size = ysize * stride; if (mapper->offset + size > mapper->size) { - PyErr_SetString(PyExc_IOError, "image file truncated"); + PyErr_SetString(PyExc_OSError, "image file truncated"); return NULL; } im = ImagingNewPrologue(mode, xsize, ysize); - if (!im) + if (!im) { return NULL; + } /* setup file pointers */ - if (orientation > 0) - for (y = 0; y < ysize; y++) + if (orientation > 0) { + for (y = 0; y < ysize; y++) { im->image[y] = mapper->base + mapper->offset + y * stride; - else - for (y = 0; y < ysize; y++) + } + } else { + for (y = 0; y < ysize; y++) { im->image[ysize-y-1] = mapper->base + mapper->offset + y * stride; + } + } im->destroy = ImagingDestroyMap; @@ -279,8 +295,9 @@ PyObject* PyImaging_Mapper(PyObject* self, PyObject* args) { char* filename; - if (!PyArg_ParseTuple(args, "s", &filename)) + if (!PyArg_ParseTuple(args, "s", &filename)) { return NULL; + } return (PyObject*) PyImaging_MapperNew(filename, 1); } @@ -313,15 +330,15 @@ PyImaging_MapBuffer(PyObject* self, PyObject* args) Py_buffer view; char* mode; char* codec; - PyObject* bbox; Py_ssize_t offset; int xsize, ysize; int stride; int ystep; - if (!PyArg_ParseTuple(args, "O(ii)sOn(sii)", &target, &xsize, &ysize, - &codec, &bbox, &offset, &mode, &stride, &ystep)) + if (!PyArg_ParseTuple(args, "O(ii)sn(sii)", &target, &xsize, &ysize, + &codec, &offset, &mode, &stride, &ystep)) { return NULL; + } if (!PyImaging_CheckBuffer(target)) { PyErr_SetString(PyExc_TypeError, "expected string or buffer"); @@ -329,12 +346,13 @@ PyImaging_MapBuffer(PyObject* self, PyObject* args) } if (stride <= 0) { - if (!strcmp(mode, "L") || !strcmp(mode, "P")) + if (!strcmp(mode, "L") || !strcmp(mode, "P")) { stride = xsize; - else if (!strncmp(mode, "I;16", 4)) + } else if (!strncmp(mode, "I;16", 4)) { stride = xsize * 2; - else + } else { stride = xsize * 4; + } } if (stride > 0 && ysize > PY_SSIZE_T_MAX / stride) { @@ -350,30 +368,38 @@ PyImaging_MapBuffer(PyObject* self, PyObject* args) } /* check buffer size */ - if (PyImaging_GetBuffer(target, &view) < 0) + if (PyImaging_GetBuffer(target, &view) < 0) { return NULL; + } if (view.len < 0) { PyErr_SetString(PyExc_ValueError, "buffer has negative size"); + PyBuffer_Release(&view); return NULL; } if (offset + size > view.len) { PyErr_SetString(PyExc_ValueError, "buffer is not large enough"); + PyBuffer_Release(&view); return NULL; } im = ImagingNewPrologueSubtype( mode, xsize, ysize, sizeof(ImagingBufferInstance)); - if (!im) + if (!im) { + PyBuffer_Release(&view); return NULL; + } /* setup file pointers */ - if (ystep > 0) - for (y = 0; y < ysize; y++) + if (ystep > 0) { + for (y = 0; y < ysize; y++) { im->image[y] = (char*)view.buf + offset + y * stride; - else - for (y = 0; y < ysize; y++) + } + } else { + for (y = 0; y < ysize; y++) { im->image[ysize-y-1] = (char*)view.buf + offset + y * stride; + } + } im->destroy = mapping_destroy_buffer; diff --git a/src/outline.c b/src/outline.c index 25e63aeaf..a109189e1 100644 --- a/src/outline.c +++ b/src/outline.c @@ -19,11 +19,11 @@ #include "Python.h" -#include "Imaging.h" +#include "libImaging/Imaging.h" /* -------------------------------------------------------------------- */ -/* Class */ +/* Class */ typedef struct { PyObject_HEAD @@ -39,12 +39,14 @@ _outline_new(void) { OutlineObject *self; - if (PyType_Ready(&OutlineType) < 0) + if (PyType_Ready(&OutlineType) < 0) { return NULL; + } self = PyObject_New(OutlineObject, &OutlineType); - if (self == NULL) - return NULL; + if (self == NULL) { + return NULL; + } self->outline = ImagingOutlineNew(); @@ -61,35 +63,38 @@ _outline_dealloc(OutlineObject* self) ImagingOutline PyOutline_AsOutline(PyObject* outline) { - if (PyOutline_Check(outline)) + if (PyOutline_Check(outline)) { return ((OutlineObject*) outline)->outline; + } return NULL; } /* -------------------------------------------------------------------- */ -/* Factories */ +/* Factories */ PyObject* PyOutline_Create(PyObject* self, PyObject* args) { - if (!PyArg_ParseTuple(args, ":outline")) + if (!PyArg_ParseTuple(args, ":outline")) { return NULL; + } return (PyObject*) _outline_new(); } /* -------------------------------------------------------------------- */ -/* Methods */ +/* Methods */ static PyObject* _outline_move(OutlineObject* self, PyObject* args) { float x0, y0; - if (!PyArg_ParseTuple(args, "ff", &x0, &y0)) - return NULL; + if (!PyArg_ParseTuple(args, "ff", &x0, &y0)) { + return NULL; + } ImagingOutlineMove(self->outline, x0, y0); @@ -101,8 +106,9 @@ static PyObject* _outline_line(OutlineObject* self, PyObject* args) { float x1, y1; - if (!PyArg_ParseTuple(args, "ff", &x1, &y1)) - return NULL; + if (!PyArg_ParseTuple(args, "ff", &x1, &y1)) { + return NULL; + } ImagingOutlineLine(self->outline, x1, y1); @@ -114,8 +120,9 @@ static PyObject* _outline_curve(OutlineObject* self, PyObject* args) { float x1, y1, x2, y2, x3, y3; - if (!PyArg_ParseTuple(args, "ffffff", &x1, &y1, &x2, &y2, &x3, &y3)) - return NULL; + if (!PyArg_ParseTuple(args, "ffffff", &x1, &y1, &x2, &y2, &x3, &y3)) { + return NULL; + } ImagingOutlineCurve(self->outline, x1, y1, x2, y2, x3, y3); @@ -126,8 +133,9 @@ _outline_curve(OutlineObject* self, PyObject* args) static PyObject* _outline_close(OutlineObject* self, PyObject* args) { - if (!PyArg_ParseTuple(args, ":close")) + if (!PyArg_ParseTuple(args, ":close")) { return NULL; + } ImagingOutlineClose(self->outline); @@ -139,8 +147,9 @@ static PyObject* _outline_transform(OutlineObject* self, PyObject* args) { double a[6]; - if (!PyArg_ParseTuple(args, "(dddddd)", a+0, a+1, a+2, a+3, a+4, a+5)) + if (!PyArg_ParseTuple(args, "(dddddd)", a+0, a+1, a+2, a+3, a+4, a+5)) { return NULL; + } ImagingOutlineTransform(self->outline, a); @@ -158,35 +167,35 @@ static struct PyMethodDef _outline_methods[] = { }; static PyTypeObject OutlineType = { - PyVarObject_HEAD_INIT(NULL, 0) - "Outline", /*tp_name*/ - sizeof(OutlineObject), /*tp_size*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_outline_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _outline_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0) + "Outline", /*tp_name*/ + sizeof(OutlineObject), /*tp_size*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)_outline_dealloc,/*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + _outline_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ }; diff --git a/src/path.c b/src/path.c index f69755d16..8eba81aa4 100644 --- a/src/path.c +++ b/src/path.c @@ -27,7 +27,7 @@ #include "Python.h" -#include "Imaging.h" +#include "libImaging/Imaging.h" #include @@ -56,13 +56,14 @@ alloc_array(Py_ssize_t count) PyErr_NoMemory(); return NULL; } - if (count > (SIZE_MAX / (2 * sizeof(double))) - 1 ) { + if ((unsigned long long)count > (SIZE_MAX / (2 * sizeof(double))) - 1 ) { PyErr_NoMemory(); return NULL; } xy = malloc(2 * count * sizeof(double) + 1); - if (!xy) + if (!xy) { PyErr_NoMemory(); + } return xy; } @@ -74,8 +75,9 @@ path_new(Py_ssize_t count, double* xy, int duplicate) if (duplicate) { /* duplicate path */ double* p = alloc_array(count); - if (!p) + if (!p) { return NULL; + } memcpy(p, xy, count * 2 * sizeof(double)); xy = p; } @@ -120,8 +122,9 @@ PyPath_Flatten(PyObject* data, double **pxy) /* This was another path object. */ PyPathObject *path = (PyPathObject*) data; xy = alloc_array(path->count); - if (!xy) + if (!xy) { return -1; + } memcpy(xy, path->xy, 2 * path->count * sizeof(double)); *pxy = xy; return path->count; @@ -134,10 +137,12 @@ PyPath_Flatten(PyObject* data, double **pxy) float *ptr = (float*) buffer.buf; n = buffer.len / (2 * sizeof(float)); xy = alloc_array(n); - if (!xy) + if (!xy) { return -1; - for (i = 0; i < n+n; i++) + } + for (i = 0; i < n+n; i++) { xy[i] = ptr[i]; + } *pxy = xy; PyBuffer_Release(&buffer); return n; @@ -153,26 +158,28 @@ PyPath_Flatten(PyObject* data, double **pxy) j = 0; n = PyObject_Length(data); /* Just in case __len__ breaks (or doesn't exist) */ - if (PyErr_Occurred()) + if (PyErr_Occurred()) { return -1; + } /* Allocate for worst case */ xy = alloc_array(n); - if (!xy) + if (!xy) { return -1; + } /* Copy table to path array */ if (PyList_Check(data)) { for (i = 0; i < n; i++) { double x, y; PyObject *op = PyList_GET_ITEM(data, i); - if (PyFloat_Check(op)) + if (PyFloat_Check(op)) { xy[j++] = PyFloat_AS_DOUBLE(op); - else if (PyLong_Check(op)) + } else if (PyLong_Check(op)) { xy[j++] = (float) PyLong_AS_LONG(op); - else if (PyNumber_Check(op)) + } else if (PyNumber_Check(op)) { xy[j++] = PyFloat_AsDouble(op); - else if (PyArg_ParseTuple(op, "dd", &x, &y)) { + } else if (PyArg_ParseTuple(op, "dd", &x, &y)) { xy[j++] = x; xy[j++] = y; } else { @@ -184,13 +191,13 @@ PyPath_Flatten(PyObject* data, double **pxy) for (i = 0; i < n; i++) { double x, y; PyObject *op = PyTuple_GET_ITEM(data, i); - if (PyFloat_Check(op)) + if (PyFloat_Check(op)) { xy[j++] = PyFloat_AS_DOUBLE(op); - else if (PyLong_Check(op)) + } else if (PyLong_Check(op)) { xy[j++] = (float) PyLong_AS_LONG(op); - else if (PyNumber_Check(op)) + } else if (PyNumber_Check(op)) { xy[j++] = PyFloat_AsDouble(op); - else if (PyArg_ParseTuple(op, "dd", &x, &y)) { + } else if (PyArg_ParseTuple(op, "dd", &x, &y)) { xy[j++] = x; xy[j++] = y; } else { @@ -213,13 +220,13 @@ PyPath_Flatten(PyObject* data, double **pxy) return -1; } } - if (PyFloat_Check(op)) + if (PyFloat_Check(op)) { xy[j++] = PyFloat_AS_DOUBLE(op); - else if (PyLong_Check(op)) + } else if (PyLong_Check(op)) { xy[j++] = (float) PyLong_AS_LONG(op); - else if (PyNumber_Check(op)) + } else if (PyNumber_Check(op)) { xy[j++] = PyFloat_AsDouble(op); - else if (PyArg_ParseTuple(op, "dd", &x, &y)) { + } else if (PyArg_ParseTuple(op, "dd", &x, &y)) { xy[j++] = x; xy[j++] = y; } else { @@ -257,19 +264,22 @@ PyPath_Create(PyObject* self, PyObject* args) /* number of vertices */ xy = alloc_array(count); - if (!xy) + if (!xy) { return NULL; + } } else { /* sequence or other path */ PyErr_Clear(); - if (!PyArg_ParseTuple(args, "O", &data)) + if (!PyArg_ParseTuple(args, "O", &data)) { return NULL; + } count = PyPath_Flatten(data, &xy); - if (count < 0) + if (count < 0) { return NULL; + } } return (PyObject*) path_new(count, xy, 0); @@ -291,8 +301,9 @@ path_compact(PyPathObject* self, PyObject* args) double cityblock = 2.0; - if (!PyArg_ParseTuple(args, "|d:compact", &cityblock)) + if (!PyArg_ParseTuple(args, "|d:compact", &cityblock)) { return NULL; + } xy = self->xy; @@ -323,8 +334,9 @@ path_getbbox(PyPathObject* self, PyObject* args) double *xy; double x0, y0, x1, y1; - if (!PyArg_ParseTuple(args, ":getbbox")) + if (!PyArg_ParseTuple(args, ":getbbox")) { return NULL; + } xy = self->xy; @@ -332,14 +344,18 @@ path_getbbox(PyPathObject* self, PyObject* args) y0 = y1 = xy[1]; for (i = 1; i < self->count; i++) { - if (xy[i+i] < x0) + if (xy[i+i] < x0) { x0 = xy[i+i]; - if (xy[i+i] > x1) + } + if (xy[i+i] > x1) { x1 = xy[i+i]; - if (xy[i+i+1] < y0) + } + if (xy[i+i+1] < y0) { y0 = xy[i+i+1]; - if (xy[i+i+1] > y1) + } + if (xy[i+i+1] > y1) { y1 = xy[i+i+1]; + } } return Py_BuildValue("dddd", x0, y0, x1, y1); @@ -348,8 +364,9 @@ path_getbbox(PyPathObject* self, PyObject* args) static PyObject* path_getitem(PyPathObject* self, Py_ssize_t i) { - if (i < 0) + if (i < 0) { i = self->count + i; + } if (i < 0 || i >= self->count) { PyErr_SetString(PyExc_IndexError, "path index out of range"); return NULL; @@ -362,16 +379,19 @@ static PyObject* path_getslice(PyPathObject* self, Py_ssize_t ilow, Py_ssize_t ihigh) { /* adjust arguments */ - if (ilow < 0) + if (ilow < 0) { ilow = 0; - else if (ilow >= self->count) + } else if (ilow >= self->count) { ilow = self->count; - if (ihigh < 0) + } + if (ihigh < 0) { ihigh = 0; - if (ihigh < ilow) + } + if (ihigh < ilow) { ihigh = ilow; - else if (ihigh > self->count) + } else if (ihigh > self->count) { ihigh = self->count; + } return (PyObject*) path_new(ihigh - ilow, self->xy + ilow * 2, 1); } @@ -390,8 +410,9 @@ path_map(PyPathObject* self, PyObject* args) double *xy; PyObject* function; - if (!PyArg_ParseTuple(args, "O:map", &function)) + if (!PyArg_ParseTuple(args, "O:map", &function)) { return NULL; + } xy = self->xy; @@ -432,8 +453,9 @@ path_setitem(PyPathObject* self, Py_ssize_t i, PyObject* op) xy = &self->xy[i+i]; - if (!PyArg_ParseTuple(op, "dd", &xy[0], &xy[1])) + if (!PyArg_ParseTuple(op, "dd", &xy[0], &xy[1])) { return -1; + } return 0; } @@ -445,16 +467,18 @@ path_tolist(PyPathObject* self, PyObject* args) Py_ssize_t i; int flat = 0; - if (!PyArg_ParseTuple(args, "|i:tolist", &flat)) + if (!PyArg_ParseTuple(args, "|i:tolist", &flat)) { return NULL; + } if (flat) { list = PyList_New(self->count*2); for (i = 0; i < self->count*2; i++) { PyObject* item; item = PyFloat_FromDouble(self->xy[i]); - if (!item) + if (!item) { goto error; + } PyList_SetItem(list, i, item); } } else { @@ -462,8 +486,9 @@ path_tolist(PyPathObject* self, PyObject* args) for (i = 0; i < self->count; i++) { PyObject* item; item = Py_BuildValue("dd", self->xy[i+i], self->xy[i+i+1]); - if (!item) + if (!item) { goto error; + } PyList_SetItem(list, i, item); } } @@ -487,19 +512,20 @@ path_transform(PyPathObject* self, PyObject* args) if (!PyArg_ParseTuple(args, "(dddddd)|d:transform", &a, &b, &c, &d, &e, &f, - &wrap)) + &wrap)) { return NULL; + } xy = self->xy; /* transform the coordinate set */ - if (b == 0.0 && d == 0.0) + if (b == 0.0 && d == 0.0) { /* scaling */ for (i = 0; i < self->count; i++) { xy[i+i] = a*xy[i+i]+c; xy[i+i+1] = e*xy[i+i+1]+f; } - else + } else { /* affine transform */ for (i = 0; i < self->count; i++) { double x = xy[i+i]; @@ -507,11 +533,14 @@ path_transform(PyPathObject* self, PyObject* args) xy[i+i] = a*x+b*y+c; xy[i+i+1] = d*x+e*y+f; } + } /* special treatment of geographical map data */ - if (wrap != 0.0) - for (i = 0; i < self->count; i++) + if (wrap != 0.0) { + for (i = 0; i < self->count; i++) { xy[i+i] = fmod(xy[i+i], wrap); + } + } Py_INCREF(Py_None); return Py_None; @@ -542,16 +571,18 @@ path_subscript(PyPathObject* self, PyObject* item) { if (PyIndex_Check(item)) { Py_ssize_t i; i = PyNumber_AsSsize_t(item, PyExc_IndexError); - if (i == -1 && PyErr_Occurred()) + if (i == -1 && PyErr_Occurred()) { return NULL; + } return path_getitem(self, i); } if (PySlice_Check(item)) { int len = 4; Py_ssize_t start, stop, step, slicelength; - if (PySlice_GetIndicesEx(item, len, &start, &stop, &step, &slicelength) < 0) + if (PySlice_GetIndicesEx(item, len, &start, &stop, &step, &slicelength) < 0) { return NULL; + } if (slicelength <= 0) { double *xy = alloc_array(0); diff --git a/tox.ini b/tox.ini index 07d75be64..2557d5067 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,12 @@ # Tox (https://tox.readthedocs.io/en/latest/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. +# test suite on all supported python versions. To use it, +# "python3 -m pip install tox" and then run "tox" from this directory. [tox] envlist = lint - py{35,36,37,38,py3} + py{36,37,38,39,py3} minversion = 1.9 [testenv] @@ -14,7 +14,7 @@ commands = {envpython} setup.py clean {envpython} setup.py build_ext --inplace {envpython} selftest.py - {envpython} -m pytest {posargs} + {envpython} -m pytest -W always {posargs} deps = cffi numpy @@ -24,13 +24,10 @@ deps = [testenv:lint] commands = - black --target-version py35 --check --diff . - flake8 --statistics --count - isort --check-only --diff + pre-commit run --all-files --show-diff-on-failure check-manifest deps = - black + pre-commit check-manifest - flake8 - isort skip_install = true +passenv = PRE_COMMIT_COLOR diff --git a/winbuild/README.md b/winbuild/README.md index 471b61a57..611d1ed1a 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -1,18 +1,29 @@ -Quick README ------------- - -For more extensive info, see the [Windows build instructions](build.rst). - -* See https://github.com/python-pillow/Pillow/issues/553#issuecomment-37877416 and https://github.com/matplotlib/matplotlib/issues/1717#issuecomment-13343859 - -* Works best with Python 3.4, due to virtualenv and pip batteries included. Python3+ required for fetch command. -* Check config.py for virtual env paths, suffix for 64-bit releases. Defaults to `x64`, set `X64_EXT` to change. -* When running in CI with one Python per invocation, set the `PYTHON` env variable to the Python folder. (e.g. `PYTHON`=`c:\Python27\`) This overrides the matrix in config.py and will just build and test for the specific Python. -* `python get_pythons.py` downloads all the Python releases, and their signatures. (Manually) Install in `c:\PythonXX[x64]\`. -* `python build_dep.py` downloads and creates a build script for all the dependencies, in 32 and 64-bit versions, and with both compiler versions. -* (in powershell) `build_deps.cmd` invokes the dependency build. -* `python build.py --clean` makes Pillow for the matrix of Pythons. -* `python test.py` runs the tests on Pillow in all the virtual envs. -* Currently working with zlib, libjpeg, freetype, and libtiff on Python 2.7, and 3.4, both 32 and 64-bit, on a local win7 pro machine and appveyor.com -* WebP is built, not detected. -* LCMS, OpenJPEG and libimagequant are not building. +Quick README +------------ + +For more extensive info, see the [Windows build instructions](build.rst). + +* See [Current Windows Build/Testing process (Pillow#553)](https://github.com/python-pillow/Pillow/issues/553#issuecomment-37877416), + [Definitive docs for how to compile on Windows (matplotlib#1717)](https://github.com/matplotlib/matplotlib/issues/1717#issuecomment-13343859), + [Test Windows with GitHub Actions (Pillow#4084)](https://github.com/python-pillow/Pillow/pull/4084). + + +* Requires Microsoft Visual Studio 2017 or newer with C++ component. +* Requires NASM for libjpeg-turbo, a required dependency when using this script. +* Requires CMake 3.12 or newer (available as Visual Studio component). +* Tested on Windows Server 2016 with Visual Studio 2017 Community (AppVeyor). +* Tested on Windows Server 2019 with Visual Studio 2019 Enterprise (GitHub Actions). + +The following is a simplified version of the script used on AppVeyor: +``` +set PYTHON=C:\Python38\bin +cd /D C:\Pillow\winbuild +C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends +build\build_dep_all.cmd +build\build_pillow.cmd install +cd .. +path C:\Pillow\winbuild\build\bin;%PATH% +%PYTHON%\python.exe selftest.py +%PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests +build\build_pillow.cmd bdist_wheel +``` diff --git a/winbuild/appveyor_build_msys2.sh b/winbuild/appveyor_build_msys2.sh deleted file mode 100644 index 489f9411e..000000000 --- a/winbuild/appveyor_build_msys2.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -cd /c/pillow && /mingw32/$EXECUTABLE setup.py install diff --git a/winbuild/appveyor_install_msys2_deps.sh b/winbuild/appveyor_install_msys2_deps.sh deleted file mode 100644 index 4cc01082d..000000000 --- a/winbuild/appveyor_install_msys2_deps.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -mkdir /var/cache/pacman/pkg -pacman -S --noconfirm mingw32/mingw-w64-i686-python3-pip \ - mingw32/mingw-w64-i686-python3-setuptools \ - mingw32/mingw-w64-i686-python3-pytest \ - mingw32/mingw-w64-i686-python3-pytest-cov \ - mingw-w64-i686-libjpeg-turbo \ - mingw-w64-i686-libimagequant - -C:/msys64/mingw32/bin/python3 -m pip install --upgrade pip - -/mingw32/bin/pip install olefile -/mingw32/bin/pip3 install olefile diff --git a/winbuild/appveyor_install_pypy3.cmd b/winbuild/appveyor_install_pypy3.cmd deleted file mode 100644 index 3622ed6ec..000000000 --- a/winbuild/appveyor_install_pypy3.cmd +++ /dev/null @@ -1,3 +0,0 @@ -curl -fsSL -o pypy3.zip https://bitbucket.org/pypy/pypy/downloads/pypy3.6-v7.2.0-win32.zip -7z x pypy3.zip -oc:\ -c:\Python37\Scripts\virtualenv.exe -p c:\pypy3.6-v7.2.0-win32\pypy3.exe c:\vp\pypy3 diff --git a/winbuild/build.py b/winbuild/build.py deleted file mode 100755 index e565226bd..000000000 --- a/winbuild/build.py +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env python3 - -import getopt -import os -import shutil -import subprocess -import sys - -from config import ( - VIRT_BASE, - X64_EXT, - bit_from_env, - compiler_from_env, - compilers, - pythons, - pyversion_from_env, -) - - -def setup_vms(): - ret = [] - for py in pythons: - for arch in ("", X64_EXT): - ret.append( - "virtualenv -p c:/Python%s%s/python.exe --clear %s%s%s" - % (py, arch, VIRT_BASE, py, arch) - ) - ret.append( - r"%s%s%s\Scripts\pip.exe install pytest pytest-cov" - % (VIRT_BASE, py, arch) - ) - return "\n".join(ret) - - -def run_script(params): - (version, script) = params - try: - print("Running %s" % version) - filename = "build_pillow_%s.cmd" % version - with open(filename, "w") as f: - f.write(script) - - command = ["powershell", "./%s" % filename] - proc = subprocess.Popen( - command, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - (trace, stderr) = proc.communicate() - status = proc.returncode - print("-- stderr --") - print(stderr.decode()) - print("-- stdout --") - print(trace.decode()) - print("Done with {}: {}".format(version, status)) - return (version, status, trace, stderr) - except Exception as msg: - print("Error with {}: {}".format(version, str(msg))) - return (version, -1, "", str(msg)) - - -def header(op): - return r""" -setlocal -set MPLSRC=%%~dp0\.. -set INCLIB=%%~dp0\depends -set BLDOPT=%s -cd /D %%MPLSRC%% -""" % ( - op - ) - - -def footer(): - return """endlocal -exit -""" - - -def vc_setup(compiler, bit): - script = "" - if compiler["vc_version"] == "2015": - arch = "x86" if bit == 32 else "x86_amd64" - script = ( - r""" -call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %s -echo on""" - % arch - ) - return script - - -def build_one(py_ver, compiler, bit): - # UNDONE virtual envs if we're not running on AppVeyor - args = {} - args.update(compiler) - if "PYTHON" in os.environ: - args["python_path"] = "%PYTHON%" - else: - args["python_path"] = "{}{}\\Scripts".format(VIRT_BASE, py_ver) - - args["executable"] = "python.exe" - if "EXECUTABLE" in os.environ: - args["executable"] = "%EXECUTABLE%" - - args["py_ver"] = py_ver - args["tcl_ver"] = "86" - - if compiler["vc_version"] == "2015": - args["imaging_libs"] = " build_ext --add-imaging-libs=msvcrt" - else: - args["imaging_libs"] = "" - - args["vc_setup"] = vc_setup(compiler, bit) - - script = r""" -setlocal EnableDelayedExpansion -call "%%ProgramFiles%%\Microsoft SDKs\Windows\%(env_version)s\Bin\SetEnv.Cmd" /Release %(env_flags)s -set DISTUTILS_USE_SDK=1 -set LIB=%%LIB%%;%%INCLIB%%\%(inc_dir)s -set INCLUDE=%%INCLUDE%%;%%INCLIB%%\%(inc_dir)s;%%INCLIB%%\tcl%(tcl_ver)s\include - -setlocal -set LIB=%%LIB%%;C:\Python%(py_ver)s\tcl%(vc_setup)s -call %(python_path)s\%(executable)s setup.py %(imaging_libs)s %%BLDOPT%% -call %(python_path)s\%(executable)s -c "from PIL import _webp;import os, shutil;shutil.copy(r'%%INCLIB%%\freetype.dll', os.path.dirname(_webp.__file__));" -endlocal - -endlocal -""" # noqa: E501 - return script % args - - -def clean(): - try: - shutil.rmtree("../build") - except Exception: - # could already be removed - pass - run_script(("virtualenvs", setup_vms())) - - -def main(op): - scripts = [] - - for py_version, py_info in pythons.items(): - py_compilers = compilers[py_info["compiler"]][py_info["vc"]] - scripts.append( - ( - py_version, - "\n".join( - [header(op), build_one(py_version, py_compilers[32], 32), footer()] - ), - ) - ) - - scripts.append( - ( - "{}{}".format(py_version, X64_EXT), - "\n".join( - [ - header(op), - build_one("%sx64" % py_version, py_compilers[64], 64), - footer(), - ] - ), - ) - ) - - results = map(run_script, scripts) - - for (version, status, trace, err) in results: - print("Compiled {}: {}".format(version, status and "ERR" or "OK")) - - -def run_one(op): - - compiler = compiler_from_env() - py_version = pyversion_from_env() - bit = bit_from_env() - - run_script( - ( - py_version, - "\n".join([header(op), build_one(py_version, compiler, bit), footer()]), - ) - ) - - -if __name__ == "__main__": - opts, args = getopt.getopt(sys.argv[1:], "", ["clean", "wheel"]) - opts = dict(opts) - - if "--clean" in opts: - clean() - - op = "install" - if "--wheel" in opts: - op = "bdist_wheel" - - if "PYTHON" in os.environ: - run_one(op) - else: - main(op) diff --git a/winbuild/build.rst b/winbuild/build.rst index 1d2084044..cd4a45e87 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -5,89 +5,109 @@ Building Pillow on Windows <../docs/installation.rst#windows-installation>`_ should be sufficient. -This page will describe a build setup to build Pillow against the -supported Python versions in 32 and 64-bit modes, using freely -available Microsoft compilers. This has been developed and tested -against 64-bit Windows 7 Professional and Windows Server 2012 -64-bit version on Amazon EC2. +This page describes the steps necessary to build Pillow using the same +scripts used on GitHub Actions and AppVeyor CIs. Prerequisites ------------- -Extra Build Helpers -^^^^^^^^^^^^^^^^^^^ - -* Powershell (available by default on Windows Server) -* GitHub client (provides git+bash shell) - -Optional: -* GPG (for checking signatures) (UNDONE -- Python signature checking) - - -Pythons -^^^^^^^ - -The build routines expect Python to be installed at C:\PythonXX for -32-bit versions or C:\PythonXXx64 for the 64-bit versions. - -Download Python 3.4, install it, and add it to the path. This is the -Python that we will use to bootstrap the build process. (The download -routines are using 3 features, and installing 3.4 gives us pip and -virtualenv as well, reducing the number of packages that we need to -install.) - -Download the rest of the Pythons by opening a command window, changing -to the ``winbuild`` directory, and running ``python -get_pythons.py``. - -UNDONE -- gpg verify the signatures (note that we can download from -https) - -Run each installer and set the proper path to the installation. Don't -set any of them as the default Python, or add them to the path. - Compilers ^^^^^^^^^ Download and install: -* `Microsoft Windows SDK for Windows 7 and .NET Framework - 4 `_ +* `Microsoft Visual Studio 2017 or newer or Build Tools for Visual Studio 2017 or newer + `_ + (MSVC C++ build tools, and any Windows SDK version required) -* `CMake-2.8.10.2-win32-x86.exe - `_ +* `CMake 3.12 or newer `_ + (also available as Visual Studio component C++ CMake tools for Windows) -The samples and the .NET SDK portions aren't required, just the -compilers and other tools. UNDONE -- check exact wording. +* `NASM `_ + +Any version of Visual Studio 2017 or newer should be supported, +including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. + +Paths to CMake (if standalone) and NASM must be added to the ``PATH`` environment variable. +Visual Studio is found automatically with ``vswhere.exe``. + +Build configuration +------------------- + +The following environment variables, if set, will override the default +behaviour of ``build_prepare.py``: + +* ``PYTHON`` + ``EXECUTABLE`` point to the target version of Python. + If ``PYTHON`` is unset, the version of Python used to run + ``build_prepare.py`` will be used. If only ``PYTHON`` is set, + ``EXECUTABLE`` defaults to ``python.exe``. +* ``ARCHITECTURE`` is used to select a ``x86`` or ``x64`` build. By default, + uses same architecture as the version of Python used to run ``build_prepare.py``. + is used. +* ``PILLOW_BUILD`` can be used to override the ``winbuild\build`` directory + path, used to store generated build scripts and compiled libraries. + **Warning:** This directory is wiped when ``build_prepare.py`` is run. +* ``PILLOW_DEPS`` points to the directory used to store downloaded + dependencies. By default ``winbuild\depends`` is used. + +``build_prepare.py`` also supports the following command line parameters: + +* ``-v`` will print generated scripts. +* ``--no-imagequant`` will skip GPL-licensed ``libimagequant`` optional dependency +* ``--no-raqm`` will skip optional dependency Raqm (which itself depends on + LGPL-licensed ``fribidi``). +* ``--python=`` and ``--executable=`` override ``PYTHON`` and ``EXECUTABLE``. +* ``--architecture=`` overrides ``ARCHITECTURE``. +* ``--dir=`` and ``--depends=`` override ``PILLOW_BUILD`` + and ``PILLOW_DEPS``. Dependencies ------------ -The script 'build_dep.py' downloads and builds the dependencies. Open -a command window, change directory into ``winbuild`` and run ``python -build_dep.py``. +Dependencies will be automatically downloaded by ``build_prepare.py``. +By default, downloaded dependencies are stored in ``winbuild\depends``; +set the ``PILLOW_DEPS`` environment variable to override this location. -This will download libjpeg, libtiff, libz, and freetype. It will then -compile 32 and 64-bit versions of the libraries, with both versions of -the compilers. - -UNDONE -- lcms fails. -UNDONE -- webp, jpeg2k not recognized +To build all dependencies, run ``winbuild\build\build_dep_all.cmd``, +or run the individual scripts to build each dependency separately. Building Pillow --------------- -Once the dependencies are built, run ``python build.py --clean`` to -build and install Pillow in virtualenvs for each Python -build. ``build.py --wheel`` will build wheels instead of -installing into virtualenvs. +Once the dependencies are built, run +``winbuild\build\build_pillow.cmd install`` to build and install +Pillow for the selected version of Python. +``winbuild\build\build_pillow.cmd bdist_wheel`` will build wheels +instead of installing Pillow. -UNDONE -- suppressed output, what about failures. +You can also use ``winbuild\build\build_pillow.cmd --inplace develop`` to build +and install Pillow in develop mode (instead of ``python3 -m pip install --editable``). Testing Pillow -------------- -Build and install Pillow, then run ``python test.py`` from the -``winbuild`` directory. +Some binary dependencies (e.g. ``libraqm.dll``) will be stored in the +``winbuild\build\bin`` directory; this directory should be added to ``PATH`` +before running tests. +Build and install Pillow, then run ``python -m pytest Tests`` +from the root Pillow directory. + +Example +------- + +The following is a simplified version of the script used on AppVeyor: + +.. code-block:: + + set PYTHON=C:\Python38\bin + cd /D C:\Pillow\winbuild + C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends + build\build_dep_all.cmd + build\build_pillow.cmd install + cd .. + path C:\Pillow\winbuild\build\bin;%PATH% + %PYTHON%\python.exe selftest.py + %PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests + build\build_pillow.cmd bdist_wheel diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py deleted file mode 100644 index 5762de5e5..000000000 --- a/winbuild/build_dep.py +++ /dev/null @@ -1,328 +0,0 @@ -import os - -from build import vc_setup -from config import all_compilers, bit_from_env, compiler_from_env, compilers, libs -from fetch import fetch -from untar import untar -from unzip import unzip - - -def _relpath(*args): - return os.path.join(os.getcwd(), *args) - - -build_dir = _relpath("build") -inc_dir = _relpath("depends") - - -def check_sig(filename, signame): - # UNDONE -- need gpg - return filename - - -def mkdirs(): - try: - os.mkdir(build_dir) - except OSError: - pass - try: - os.mkdir(inc_dir) - except OSError: - pass - for compiler in all_compilers(): - try: - os.mkdir(os.path.join(inc_dir, compiler["inc_dir"])) - except OSError: - pass - - -def extract(src, dest): - if ".zip" in src: - return unzip(src, dest) - if ".tar.gz" in src or ".tgz" in src: - return untar(src, dest) - - -def extract_libs(): - for name, lib in libs.items(): - filename = fetch(lib["url"]) - if name == "openjpeg": - for compiler in all_compilers(): - if not os.path.exists( - os.path.join(build_dir, lib["dir"] + compiler["inc_dir"]) - ): - extract(filename, build_dir) - os.rename( - os.path.join(build_dir, lib["dir"]), - os.path.join(build_dir, lib["dir"] + compiler["inc_dir"]), - ) - else: - extract(filename, build_dir) - - -def extract_openjpeg(compiler): - return ( - r""" -rem build openjpeg -setlocal -cd %%BUILD%% -mkdir %%INCLIB%%\openjpeg-2.0 -copy /Y /B openjpeg-2.0.0-win32-x86\include\openjpeg-2.0 %%INCLIB%%\openjpeg-2.0 -copy /Y /B openjpeg-2.0.0-win32-x86\bin\ %%INCLIB%% -copy /Y /B openjpeg-2.0.0-win32-x86\lib\ %%INCLIB%% -endlocal -""" - % compiler - ) - - -def cp_tk(ver_85, ver_86): - versions = {"ver_85": ver_85, "ver_86": ver_86} - return ( - r""" -mkdir %%INCLIB%%\tcl85\include\X11 -copy /Y /B %%BUILD%%\tcl%(ver_85)s\generic\*.h %%INCLIB%%\tcl85\include\ -copy /Y /B %%BUILD%%\tk%(ver_85)s\generic\*.h %%INCLIB%%\tcl85\include\ -copy /Y /B %%BUILD%%\tk%(ver_85)s\xlib\X11\* %%INCLIB%%\tcl85\include\X11\ - -mkdir %%INCLIB%%\tcl86\include\X11 -copy /Y /B %%BUILD%%\tcl%(ver_86)s\generic\*.h %%INCLIB%%\tcl86\include\ -copy /Y /B %%BUILD%%\tk%(ver_86)s\generic\*.h %%INCLIB%%\tcl86\include\ -copy /Y /B %%BUILD%%\tk%(ver_86)s\xlib\X11\* %%INCLIB%%\tcl86\include\X11\ -""" - % versions - ) - - -def header(): - return r"""setlocal -set MSBUILD=C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe -set CMAKE="cmake.exe" -set INCLIB=%~dp0\depends -set BUILD=%~dp0\build -""" + "\n".join( - r"set {}=%BUILD%\{}".format(k.upper(), v["dir"]) - for (k, v) in libs.items() - if v["dir"] - ) - - -def setup_compiler(compiler): - return ( - r"""setlocal EnableDelayedExpansion -call "%%ProgramFiles%%\Microsoft SDKs\Windows\%(env_version)s\Bin\SetEnv.Cmd" /Release %(env_flags)s -echo on -set INCLIB=%%INCLIB%%\%(inc_dir)s -""" # noqa: E501 - % compiler - ) - - -def end_compiler(): - return """ -endlocal -""" - - -def nmake_openjpeg(compiler, bit): - if compiler["env_version"] == "v7.0": - return "" - - atts = {"op_ver": "2.3.1"} - atts.update(compiler) - return ( - r""" -rem build openjpeg -setlocal -""" - + vc_setup(compiler, bit) - + r""" -cd /D %%OPENJPEG%%%(inc_dir)s - -%%CMAKE%% -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF -DCMAKE_BUILD_TYPE=Release -G "NMake Makefiles" . -nmake -nologo -f Makefile clean -nmake -nologo -f Makefile -copy /Y /B bin\* %%INCLIB%% -mkdir %%INCLIB%%\openjpeg-%(op_ver)s -copy /Y /B src\lib\openjp2\*.h %%INCLIB%%\openjpeg-%(op_ver)s -endlocal -""" # noqa: E501 - % atts - ) - - -def nmake_libs(compiler, bit): - # undone -- pre, makes, headers, libs - script = ( - r""" -rem Build libjpeg -setlocal -""" - + vc_setup(compiler, bit) - + r""" -cd /D %%JPEG%% -nmake -nologo -f makefile.vc setup-vc6 -nmake -nologo -f makefile.vc clean -nmake -nologo -f makefile.vc nodebug=1 libjpeg.lib -copy /Y /B *.dll %%INCLIB%% -copy /Y /B *.lib %%INCLIB%% -copy /Y /B j*.h %%INCLIB%% -endlocal - -rem Build zlib -setlocal -cd /D %%ZLIB%% -nmake -nologo -f win32\Makefile.msc clean -nmake -nologo -f win32\Makefile.msc zlib.lib -copy /Y /B *.dll %%INCLIB%% -copy /Y /B *.lib %%INCLIB%% -copy /Y /B zlib.lib %%INCLIB%%\z.lib -copy /Y /B zlib.h %%INCLIB%% -copy /Y /B zconf.h %%INCLIB%% -endlocal - -rem Build webp -setlocal -""" - + vc_setup(compiler, bit) - + r""" -cd /D %%WEBP%% -rd /S /Q %%WEBP%%\output\release-static -nmake -nologo -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output all -copy /Y /B output\release-static\%(webp_platform)s\lib\* %%INCLIB%% -mkdir %%INCLIB%%\webp -copy /Y /B src\webp\*.h %%INCLIB%%\\webp -endlocal - -rem Build libtiff -setlocal -""" - + vc_setup(compiler, bit) - + r""" -rem do after building jpeg and zlib -copy %%~dp0\tiff.opt %%TIFF%%\nmake.opt - -cd /D %%TIFF%% -nmake -nologo -f makefile.vc clean -nmake -nologo -f makefile.vc lib -copy /Y /B libtiff\*.dll %%INCLIB%% -copy /Y /B libtiff\*.lib %%INCLIB%% -copy /Y /B libtiff\tiff*.h %%INCLIB%% -endlocal -""" - ) - return script % compiler - - -def msbuild_freetype(compiler, bit): - script = r""" -rem Build freetype -setlocal -rd /S /Q %%FREETYPE%%\objs -set DefaultPlatformToolset=v100 -""" - properties = r"""/p:Configuration="Release" /p:Platform=%(platform)s""" - if bit == 64: - script += ( - r"copy /Y /B " - + r'"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Lib\x64\*.Lib" ' - + r"%%FREETYPE%%\builds\windows\vc2010" - ) - properties += r" /p:_IsNativeEnvironment=false" - script += ( - r""" -%%MSBUILD%% %%FREETYPE%%\builds\windows\vc2010\freetype.sln /t:Clean;Build """ - + properties - + r""" /m -xcopy /Y /E /Q %%FREETYPE%%\include %%INCLIB%% -""" - ) - freetypeReleaseDir = r"%%FREETYPE%%\objs\%(platform)s\Release" - script += ( - r""" -copy /Y /B """ - + freetypeReleaseDir - + r"""\freetype.lib %%INCLIB%%\freetype.lib -copy /Y /B """ - + freetypeReleaseDir - + r"""\freetype.dll %%INCLIB%%\..\freetype.dll -endlocal -""" - ) - return script % compiler - - -def build_lcms2(compiler): - if compiler["env_version"] == "v7.1": - return build_lcms_71(compiler) - return build_lcms_70(compiler) - - -def build_lcms_70(compiler): - """Link error here on x64""" - if compiler["platform"] == "x64": - return "" - - """Build LCMS on VC2008. This version is only 32bit/Win32""" - return ( - r""" -rem Build lcms2 -setlocal -set LCMS=%%LCMS-2.7%% -rd /S /Q %%LCMS%%\Lib -rd /S /Q %%LCMS%%\Projects\VC%(vc_version)s\Release -%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:Clean /p:Configuration="Release" /p:Platform=Win32 /m -%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:lcms2_static /p:Configuration="Release" /p:Platform=Win32 /p:PlatformToolset=v90 /m -xcopy /Y /E /Q %%LCMS%%\include %%INCLIB%% -copy /Y /B %%LCMS%%\Lib\MS\*.lib %%INCLIB%% -endlocal -""" # noqa: E501 - % compiler - ) - - -def build_lcms_71(compiler): - return ( - r""" -rem Build lcms2 -setlocal -set LCMS=%%LCMS-2.8%% -rd /S /Q %%LCMS%%\Lib -rd /S /Q %%LCMS%%\Projects\VC%(vc_version)s\Release -powershell -Command "(gc Projects\VC2015\lcms2_static\lcms2_static.vcxproj) -replace 'MultiThreadedDLL', 'MultiThreaded' | Out-File -encoding ASCII Projects\VC2015\lcms2_static\lcms2_static.vcxproj" -%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:Clean /p:Configuration="Release" /p:Platform=%(platform)s /m -%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:lcms2_static /p:Configuration="Release" /p:Platform=%(platform)s /m -xcopy /Y /E /Q %%LCMS%%\include %%INCLIB%% -copy /Y /B %%LCMS%%\Lib\MS\*.lib %%INCLIB%% -endlocal -""" # noqa: E501 - % compiler - ) - - -def add_compiler(compiler, bit): - script.append(setup_compiler(compiler)) - script.append(nmake_libs(compiler, bit)) - - # script.append(extract_openjpeg(compiler)) - - script.append(msbuild_freetype(compiler, bit)) - script.append(build_lcms2(compiler)) - script.append(nmake_openjpeg(compiler, bit)) - script.append(end_compiler()) - - -mkdirs() -extract_libs() -script = [header(), cp_tk(libs["tk-8.5"]["version"], libs["tk-8.6"]["version"])] - - -if "PYTHON" in os.environ: - add_compiler(compiler_from_env(), bit_from_env()) -else: - # for compiler in all_compilers(): - # add_compiler(compiler) - add_compiler(compilers[7.0][2010][32], 32) - -with open("build_deps.cmd", "w") as f: - f.write("\n".join(script)) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py new file mode 100644 index 000000000..6c431f4d6 --- /dev/null +++ b/winbuild/build_prepare.py @@ -0,0 +1,596 @@ +import os +import shutil +import struct +import subprocess +import sys + + +def cmd_cd(path): + return f"cd /D {path}" + + +def cmd_set(name, value): + return f"set {name}={value}" + + +def cmd_append(name, value): + op = "path " if name == "PATH" else f"set {name}=" + return op + f"%{name}%;{value}" + + +def cmd_copy(src, tgt): + return f'copy /Y /B "{src}" "{tgt}"' + + +def cmd_xcopy(src, tgt): + return f'xcopy /Y /E "{src}" "{tgt}"' + + +def cmd_mkdir(path): + return f'mkdir "{path}"' + + +def cmd_rmdir(path): + return f'rmdir /S /Q "{path}"' + + +def cmd_nmake(makefile=None, target="", params=None): + if params is None: + params = "" + elif isinstance(params, list) or isinstance(params, tuple): + params = " ".join(params) + else: + params = str(params) + + return " ".join( + [ + "{nmake}", + "-nologo", + f'-f "{makefile}"' if makefile is not None else "", + f"{params}", + f'"{target}"', + ] + ) + + +def cmd_cmake(params=None, file="."): + if params is None: + params = "" + elif isinstance(params, list) or isinstance(params, tuple): + params = " ".join(params) + else: + params = str(params) + return " ".join( + [ + "{cmake}", + "-DCMAKE_VERBOSE_MAKEFILE=ON", + "-DCMAKE_RULE_MESSAGES:BOOL=OFF", + "-DCMAKE_BUILD_TYPE=Release", + f"{params}", + '-G "NMake Makefiles"', + f'"{file}"', + ] + ) + + +def cmd_msbuild( + file, configuration="Release", target="Build", platform="{msbuild_arch}" +): + return " ".join( + [ + "{msbuild}", + f"{file}", + f'/t:"{target}"', + f'/p:Configuration="{configuration}"', + f"/p:Platform={platform}", + "/m", + ] + ) + + +SF_MIRROR = "http://iweb.dl.sourceforge.net" + +architectures = { + "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, + "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, +} + +header = [ + cmd_set("INCLUDE", "{inc_dir}"), + cmd_set("INCLIB", "{lib_dir}"), + cmd_set("LIB", "{lib_dir}"), + cmd_append("PATH", "{bin_dir}"), +] + +# dependencies, listed in order of compilation +deps = { + "libjpeg": { + "url": SF_MIRROR + "/project/libjpeg-turbo/2.0.5/libjpeg-turbo-2.0.5.tar.gz", + "filename": "libjpeg-turbo-2.0.5.tar.gz", + "dir": "libjpeg-turbo-2.0.5", + "build": [ + cmd_cmake( + [ + "-DENABLE_SHARED:BOOL=FALSE", + "-DWITH_JPEG8:BOOL=TRUE", + "-DWITH_CRT_DLL:BOOL=TRUE", + ] + ), + cmd_nmake(target="clean"), + cmd_nmake(target="jpeg-static"), + cmd_copy("jpeg-static.lib", "libjpeg.lib"), + cmd_nmake(target="cjpeg-static"), + cmd_copy("cjpeg-static.exe", "cjpeg.exe"), + cmd_nmake(target="djpeg-static"), + cmd_copy("djpeg-static.exe", "djpeg.exe"), + ], + "headers": ["j*.h"], + "libs": ["libjpeg.lib"], + "bins": ["cjpeg.exe", "djpeg.exe"], + }, + "zlib": { + "url": "http://zlib.net/zlib1211.zip", + "filename": "zlib1211.zip", + "dir": "zlib-1.2.11", + "build": [ + cmd_nmake(r"win32\Makefile.msc", "clean"), + cmd_nmake(r"win32\Makefile.msc", "zlib.lib"), + cmd_copy("zlib.lib", "z.lib"), + ], + "headers": [r"z*.h"], + "libs": [r"*.lib"], + }, + "libtiff": { + "url": "https://download.osgeo.org/libtiff/tiff-4.1.0.tar.gz", + "filename": "tiff-4.1.0.tar.gz", + "dir": "tiff-4.1.0", + "build": [ + cmd_copy(r"{winbuild_dir}\tiff.opt", "nmake.opt"), + cmd_nmake("makefile.vc", "clean"), + cmd_nmake("makefile.vc", "lib"), + ], + "headers": [r"libtiff\tiff*.h"], + "libs": [r"libtiff\*.lib"], + # "bins": [r"libtiff\*.dll"], + }, + "libwebp": { + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.1.0.tar.gz", + "filename": "libwebp-1.1.0.tar.gz", + "dir": "libwebp-1.1.0", + "build": [ + cmd_rmdir(r"output\release-static"), # clean + cmd_nmake( + "Makefile.vc", + "all", + ["CFG=release-static", "OBJDIR=output", "ARCH={architecture}"], + ), + cmd_mkdir(r"{inc_dir}\webp"), + cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), + ], + "libs": [r"output\release-static\{architecture}\lib\*.lib"], + }, + "libpng": { + "url": SF_MIRROR + "/project/libpng/libpng16/1.6.37/lpng1637.zip", + "filename": "lpng1637.zip", + "dir": "lpng1637", + "build": [ + # lint: do not inline + cmd_cmake(("-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF")), + cmd_nmake(target="clean"), + cmd_nmake(), + cmd_copy("libpng16_static.lib", "libpng16.lib"), + ], + "headers": [r"png*.h"], + "libs": [r"libpng16.lib"], + }, + "freetype": { + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.10.4.tar.gz", # noqa: E501 + "filename": "freetype-2.10.4.tar.gz", + "dir": "freetype-2.10.4", + "patch": { + r"builds\windows\vc2010\freetype.vcxproj": { + # freetype setting is /MD for .dll and /MT for .lib, we need /MD + "MultiThreaded": "MultiThreadedDLL", # noqa: E501 + # freetype doesn't specify SDK version, MSBuild may guess incorrectly + '': '\n $(WindowsSDKVersion)', # noqa: E501 + }, + r"builds\windows\vc2010\freetype.user.props": { + "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ", # noqa: E501 + "": r"{dir_harfbuzz}\src;{inc_dir}", # noqa: E501 + "": "{lib_dir}", # noqa: E501 + "": "zlib.lib;libpng16.lib", # noqa: E501 + }, + r"src/autofit/afshaper.c": { + # link against harfbuzz.lib once it becomes available + "#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ": '#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ\n#pragma comment(lib, "harfbuzz.lib")', # noqa: E501 + }, + }, + "build": [ + cmd_rmdir("objs"), + cmd_msbuild( + r"builds\windows\vc2010\freetype.sln", "Release Static", "Clean" + ), + cmd_msbuild( + r"builds\windows\vc2010\freetype.sln", "Release Static", "Build" + ), + cmd_xcopy("include", "{inc_dir}"), + ], + "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], + # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], + }, + "lcms2": { + "url": SF_MIRROR + "/project/lcms/lcms/2.11/lcms2-2.11.tar.gz", + "filename": "lcms2-2.11.tar.gz", + "dir": "lcms2-2.11", + "patch": { + r"Projects\VC2017\lcms2_static\lcms2_static.vcxproj": { + # default is /MD for x86 and /MT for x64, we need /MD always + "MultiThreaded": "MultiThreadedDLL", # noqa: E501 + # retarget to default toolset (selected by vcvarsall.bat) + "v141": "$(DefaultPlatformToolset)", # noqa: E501 + # retarget to latest (selected by vcvarsall.bat) + "10.0.17134.0": "$(WindowsSDKVersion)", # noqa: E501 + } + }, + "build": [ + cmd_rmdir("Lib"), + cmd_rmdir(r"Projects\VC2017\Release"), + cmd_msbuild(r"Projects\VC2017\lcms2.sln", "Release", "Clean"), + cmd_msbuild(r"Projects\VC2017\lcms2.sln", "Release", "lcms2_static"), + cmd_xcopy("include", "{inc_dir}"), + ], + "libs": [r"Lib\MS\*.lib"], + }, + "openjpeg": { + "url": "https://github.com/uclouvain/openjpeg/archive/v2.3.1.tar.gz", + "filename": "openjpeg-2.3.1.tar.gz", + "dir": "openjpeg-2.3.1", + "build": [ + cmd_cmake(("-DBUILD_THIRDPARTY:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), + cmd_nmake(target="clean"), + cmd_nmake(target="openjp2"), + cmd_mkdir(r"{inc_dir}\openjpeg-2.3.1"), + cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.3.1"), + ], + "libs": [r"bin\*.lib"], + }, + "libimagequant": { + # e5d454b: Merge tag '2.12.6' into msvc + "url": "https://github.com/ImageOptim/libimagequant/archive/e5d454bc7f5eb63ee50c84a83a7fa5ac94f68ec4.zip", # noqa: E501 + "filename": "libimagequant-e5d454bc7f5eb63ee50c84a83a7fa5ac94f68ec4.zip", + "dir": "libimagequant-e5d454bc7f5eb63ee50c84a83a7fa5ac94f68ec4", + "patch": { + "CMakeLists.txt": { + "add_library": "add_compile_options(-openmp-)\r\nadd_library", + " SHARED": " STATIC", + } + }, + "build": [ + # lint: do not inline + cmd_cmake(), + cmd_nmake(target="clean"), + cmd_nmake(), + ], + "headers": [r"*.h"], + "libs": [r"*.lib"], + }, + "harfbuzz": { + "url": "https://github.com/harfbuzz/harfbuzz/archive/2.7.2.zip", + "filename": "harfbuzz-2.7.2.zip", + "dir": "harfbuzz-2.7.2", + "build": [ + cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), + cmd_nmake(target="clean"), + cmd_nmake(target="harfbuzz"), + ], + "headers": [r"src\*.h"], + "libs": [r"*.lib"], + }, + "fribidi": { + "url": "https://github.com/fribidi/fribidi/archive/v1.0.9.zip", + "filename": "fribidi-1.0.9.zip", + "dir": "fribidi-1.0.9", + "build": [ + cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), + cmd_cmake(), + cmd_nmake(target="clean"), + cmd_nmake(target="fribidi"), + ], + "headers": [r"lib\*.h"], + "libs": [r"*.lib"], + }, + "libraqm": { + "url": "https://github.com/HOST-Oman/libraqm/archive/v0.7.0.zip", + "filename": "libraqm-0.7.0.zip", + "dir": "libraqm-0.7.0", + "build": [ + cmd_copy(r"{winbuild_dir}\raqm.cmake", r"CMakeLists.txt"), + cmd_cmake(), + cmd_nmake(target="clean"), + cmd_nmake(target="libraqm"), + ], + "headers": [r"src\*.h"], + "bins": [r"libraqm.dll"], + }, +} + + +# based on distutils._msvccompiler from CPython 3.7.4 +def find_msvs(): + root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles") + if not root: + print("Program Files not found") + return None + + try: + vspath = ( + subprocess.check_output( + [ + os.path.join( + root, "Microsoft Visual Studio", "Installer", "vswhere.exe" + ), + "-latest", + "-prerelease", + "-requires", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "-property", + "installationPath", + "-products", + "*", + ] + ) + .decode(encoding="mbcs") + .strip() + ) + except (subprocess.CalledProcessError, OSError, UnicodeDecodeError): + print("vswhere not found") + return None + + if not os.path.isdir(os.path.join(vspath, "VC", "Auxiliary", "Build")): + print("Visual Studio seems to be missing C compiler") + return None + + vs = { + "header": [], + # nmake selected by vcvarsall + "nmake": "nmake.exe", + "vs_dir": vspath, + } + + # vs2017 + msbuild = os.path.join(vspath, "MSBuild", "15.0", "Bin", "MSBuild.exe") + if os.path.isfile(msbuild): + vs["msbuild"] = f'"{msbuild}"' + else: + # vs2019 + msbuild = os.path.join(vspath, "MSBuild", "Current", "Bin", "MSBuild.exe") + if os.path.isfile(msbuild): + vs["msbuild"] = f'"{msbuild}"' + else: + print("Visual Studio MSBuild not found") + return None + + vcvarsall = os.path.join(vspath, "VC", "Auxiliary", "Build", "vcvarsall.bat") + if not os.path.isfile(vcvarsall): + print("Visual Studio vcvarsall not found") + return None + vs["header"].append(f'call "{vcvarsall}" {{vcvars_arch}}') + + return vs + + +def extract_dep(url, filename): + import tarfile + import urllib.request + import zipfile + + file = os.path.join(depends_dir, filename) + if not os.path.exists(file): + ex = None + for i in range(3): + try: + print("Fetching %s (attempt %d)..." % (url, i + 1)) + content = urllib.request.urlopen(url).read() + with open(file, "wb") as f: + f.write(content) + break + except urllib.error.URLError as e: + ex = e + else: + raise RuntimeError(ex) + + print("Extracting " + filename) + if filename.endswith(".zip"): + with zipfile.ZipFile(file) as zf: + zf.extractall(sources_dir) + elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): + with tarfile.open(file, "r:gz") as tgz: + tgz.extractall(sources_dir) + else: + raise RuntimeError("Unknown archive type: " + filename) + + +def write_script(name, lines): + name = os.path.join(build_dir, name) + lines = [line.format(**prefs) for line in lines] + print("Writing " + name) + with open(name, "w") as f: + f.write("\n\r".join(lines)) + if verbose: + for line in lines: + print(" " + line) + + +def get_footer(dep): + lines = [] + for out in dep.get("headers", []): + lines.append(cmd_copy(out, "{inc_dir}")) + for out in dep.get("libs", []): + lines.append(cmd_copy(out, "{lib_dir}")) + for out in dep.get("bins", []): + lines.append(cmd_copy(out, "{bin_dir}")) + return lines + + +def build_dep(name): + dep = deps[name] + dir = dep["dir"] + file = f"build_dep_{name}.cmd" + + extract_dep(dep["url"], dep["filename"]) + + for patch_file, patch_list in dep.get("patch", {}).items(): + patch_file = os.path.join(sources_dir, dir, patch_file.format(**prefs)) + with open(patch_file) as f: + text = f.read() + for patch_from, patch_to in patch_list.items(): + patch_from = patch_from.format(**prefs) + patch_to = patch_to.format(**prefs) + assert patch_from in text + text = text.replace(patch_from, patch_to) + with open(patch_file, "w") as f: + f.write(text) + + banner = f"Building {name} ({dir})" + lines = [ + "@echo " + ("=" * 70), + f"@echo ==== {banner:<60} ====", + "@echo " + ("=" * 70), + "cd /D %s" % os.path.join(sources_dir, dir), + *prefs["header"], + *dep.get("build", []), + *get_footer(dep), + ] + + write_script(file, lines) + return file + + +def build_dep_all(): + lines = ["@echo on"] + for dep_name in deps: + if dep_name in disabled: + continue + script = build_dep(dep_name) + lines.append(fr'cmd.exe /c "{{build_dir}}\{script}"') + lines.append("if errorlevel 1 echo Build failed! && exit /B 1") + lines.append("@echo All Pillow dependencies built successfully!") + write_script("build_dep_all.cmd", lines) + + +def build_pillow(): + lines = [ + "@echo ---- Building Pillow (build_ext %*) ----", + cmd_cd("{pillow_dir}"), + *prefs["header"], + cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow + cmd_set("MSSdk", "1"), # for PyPy3.6 + cmd_set("py_vcruntime_redist", "true"), # use /MD, not /MT + r'"{python_dir}\{python_exe}" setup.py build_ext %*', + ] + + write_script("build_pillow.cmd", lines) + + +if __name__ == "__main__": + # winbuild directory + winbuild_dir = os.path.dirname(os.path.realpath(__file__)) + + verbose = False + disabled = [] + depends_dir = os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")) + python_dir = os.environ.get("PYTHON") + python_exe = os.environ.get("EXECUTABLE", "python.exe") + architecture = os.environ.get( + "ARCHITECTURE", "x86" if struct.calcsize("P") == 4 else "x64" + ) + build_dir = os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")) + sources_dir = "" + for arg in sys.argv[1:]: + if arg == "-v": + verbose = True + elif arg == "--no-imagequant": + disabled += ["libimagequant"] + elif arg == "--no-raqm": + disabled += ["fribidi", "libraqm"] + elif arg.startswith("--depends="): + depends_dir = arg[10:] + elif arg.startswith("--python="): + python_dir = arg[9:] + elif arg.startswith("--executable="): + python_exe = arg[13:] + elif arg.startswith("--architecture="): + architecture = arg[15:] + elif arg.startswith("--dir="): + build_dir = arg[6:] + elif arg == "--srcdir": + sources_dir = os.path.sep + "src" + else: + raise ValueError("Unknown parameter: " + arg) + + # dependency cache directory + os.makedirs(depends_dir, exist_ok=True) + print("Caching dependencies in:", depends_dir) + + if python_dir is None: + python_dir = os.path.dirname(os.path.realpath(sys.executable)) + python_exe = os.path.basename(sys.executable) + print("Target Python:", os.path.join(python_dir, python_exe)) + + arch_prefs = architectures[architecture] + print("Target Architecture:", architecture) + + msvs = find_msvs() + if msvs is None: + raise RuntimeError( + "Visual Studio not found. Please install Visual Studio 2017 or newer." + ) + print("Found Visual Studio at:", msvs["vs_dir"]) + + print("Using output directory:", build_dir) + + # build directory for *.h files + inc_dir = os.path.join(build_dir, "inc") + # build directory for *.lib files + lib_dir = os.path.join(build_dir, "lib") + # build directory for *.bin files + bin_dir = os.path.join(build_dir, "bin") + # directory for storing project files + sources_dir = build_dir + sources_dir + + shutil.rmtree(build_dir, ignore_errors=True) + os.makedirs(build_dir, exist_ok=False) + for path in [inc_dir, lib_dir, bin_dir, sources_dir]: + os.makedirs(path, exist_ok=True) + + prefs = { + # Python paths / preferences + "python_dir": python_dir, + "python_exe": python_exe, + "architecture": architecture, + **arch_prefs, + # Pillow paths + "pillow_dir": os.path.realpath(os.path.join(winbuild_dir, "..")), + "winbuild_dir": winbuild_dir, + # Build paths + "build_dir": build_dir, + "inc_dir": inc_dir, + "lib_dir": lib_dir, + "bin_dir": bin_dir, + "src_dir": sources_dir, + # Compilers / Tools + **msvs, + "cmake": "cmake.exe", # TODO find CMAKE automatically + # TODO find NASM automatically + # script header + "header": sum([header, msvs["header"], ["@echo on"]], []), + } + + for k, v in deps.items(): + prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) + + print() + + write_script(".gitignore", ["*"]) + build_dep_all() + build_pillow() diff --git a/winbuild/config.py b/winbuild/config.py deleted file mode 100644 index b68fb6936..000000000 --- a/winbuild/config.py +++ /dev/null @@ -1,199 +0,0 @@ -import os - -SF_MIRROR = "https://iweb.dl.sourceforge.net" - -pythons = { - "pypy3": {"compiler": 7.1, "vc": 2015}, - # for AppVeyor - "35": {"compiler": 7.1, "vc": 2015}, - "36": {"compiler": 7.1, "vc": 2015}, - "37": {"compiler": 7.1, "vc": 2015}, - "38": {"compiler": 7.1, "vc": 2015}, - # for GitHub Actions - "3.5": {"compiler": 7.1, "vc": 2015}, - "3.6": {"compiler": 7.1, "vc": 2015}, - "3.7": {"compiler": 7.1, "vc": 2015}, - "3.8": {"compiler": 7.1, "vc": 2015}, -} - -VIRT_BASE = "c:/vp/" -X64_EXT = os.environ.get("X64_EXT", "x64") - -libs = { - # 'openjpeg': { - # 'filename': 'openjpeg-2.0.0-win32-x86.zip', - # 'version': '2.0' - # }, - "zlib": { - "url": "http://zlib.net/zlib1211.zip", - "filename": "zlib1211.zip", - "dir": "zlib-1.2.11", - }, - "jpeg": { - "url": "http://www.ijg.org/files/jpegsr9c.zip", - "filename": "jpegsr9c.zip", - "dir": "jpeg-9c", - }, - "tiff": { - "url": "ftp://download.osgeo.org/libtiff/tiff-4.1.0.tar.gz", - "filename": "tiff-4.1.0.tar.gz", - "dir": "tiff-4.1.0", - }, - "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.10.1.tar.gz", # noqa: E501 - "filename": "freetype-2.10.1.tar.gz", - "dir": "freetype-2.10.1", - }, - "lcms-2.7": { - "url": SF_MIRROR + "/project/lcms/lcms/2.7/lcms2-2.7.zip", - "filename": "lcms2-2.7.zip", - "dir": "lcms2-2.7", - }, - "lcms-2.8": { - "url": SF_MIRROR + "/project/lcms/lcms/2.8/lcms2-2.8.zip", - "filename": "lcms2-2.8.zip", - "dir": "lcms2-2.8", - }, - "tcl-8.5": { - "url": SF_MIRROR + "/project/tcl/Tcl/8.5.19/tcl8519-src.zip", - "filename": "tcl8519-src.zip", - "dir": "", - }, - "tk-8.5": { - "url": SF_MIRROR + "/project/tcl/Tcl/8.5.19/tk8519-src.zip", - "filename": "tk8519-src.zip", - "dir": "", - "version": "8.5.19", - }, - "tcl-8.6": { - "url": SF_MIRROR + "/project/tcl/Tcl/8.6.10/tcl8610-src.zip", - "filename": "tcl8610-src.zip", - "dir": "", - }, - "tk-8.6": { - "url": SF_MIRROR + "/project/tcl/Tcl/8.6.10/tk8610-src.zip", - "filename": "tk8610-src.zip", - "dir": "", - "version": "8.6.10", - }, - "webp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.0.3.tar.gz", - "filename": "libwebp-1.0.3.tar.gz", - "dir": "libwebp-1.0.3", - }, - "openjpeg": { - "url": "https://github.com/uclouvain/openjpeg/archive/v2.3.1.tar.gz", - "filename": "openjpeg-2.3.1.tar.gz", - "dir": "openjpeg-2.3.1", - }, - "jpeg-turbo": { - "url": SF_MIRROR + "/project/libjpeg-turbo/2.0.3/libjpeg-turbo-2.0.3.tar.gz", - "filename": "libjpeg-turbo-2.0.3.tar.gz", - "dir": "libjpeg-turbo-2.0.3", - }, - # ba653c8: Merge tag '2.12.5' into msvc - "imagequant": { - "url": "https://github.com/ImageOptim/libimagequant/archive/ba653c8ccb34dde4e21c6076d85a72d21ed9d971.zip", # noqa: E501 - "filename": "libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971.zip", - "dir": "libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971", - }, - "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/2.6.1.zip", - "filename": "harfbuzz-2.6.1.zip", - "dir": "harfbuzz-2.6.1", - }, - "fribidi": { - "url": "https://github.com/fribidi/fribidi/archive/v1.0.7.zip", - "filename": "fribidi-1.0.7.zip", - "dir": "fribidi-1.0.7", - }, - "libraqm": { - "url": "https://github.com/HOST-Oman/libraqm/archive/v0.7.0.zip", - "filename": "libraqm-0.7.0.zip", - "dir": "libraqm-0.7.0", - }, -} - -compilers = { - 7: { - 2010: { - 64: { - "env_version": "v7.0", - "vc_version": "2010", - "env_flags": "/x64 /xp", - "inc_dir": "msvcr90-x64", - "platform": "x64", - "webp_platform": "x64", - }, - 32: { - "env_version": "v7.0", - "vc_version": "2010", - "env_flags": "/x86 /xp", - "inc_dir": "msvcr90-x32", - "platform": "Win32", - "webp_platform": "x86", - }, - } - }, - 7.1: { - 2015: { - 64: { - "env_version": "v7.1", - "vc_version": "2015", - "env_flags": "/x64 /vista", - "inc_dir": "msvcr10-x64", - "platform": "x64", - "webp_platform": "x64", - }, - 32: { - "env_version": "v7.1", - "vc_version": "2015", - "env_flags": "/x86 /vista", - "inc_dir": "msvcr10-x32", - "platform": "Win32", - "webp_platform": "x86", - }, - } - }, -} - - -def pyversion_from_env(): - py = os.environ["PYTHON"] - - py_version = "35" - for k in pythons: - if k in py: - py_version = k - break - - if "64" in py: - py_version = "{}{}".format(py_version, X64_EXT) - - return py_version - - -def compiler_from_env(): - py = os.environ["PYTHON"] - - for k, v in pythons.items(): - if k in py: - py_info = v - break - - bit = bit_from_env() - return compilers[py_info["compiler"]][py_info["vc"]][bit] - - -def bit_from_env(): - py = os.environ["PYTHON"] - - return 64 if "64" in py else 32 - - -def all_compilers(): - all = [] - for vc_compilers in compilers.values(): - for bit_compilers in vc_compilers.values(): - all += bit_compilers.values() - return all diff --git a/winbuild/fetch.py b/winbuild/fetch.py deleted file mode 100644 index adc45429a..000000000 --- a/winbuild/fetch.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import sys -import urllib.parse -import urllib.request - -from config import libs - - -def fetch(url): - depends_filename = None - for lib in libs.values(): - if lib["url"] == url: - depends_filename = lib["filename"] - break - if depends_filename and os.path.exists(depends_filename): - return depends_filename - name = urllib.parse.urlsplit(url)[2].split("/")[-1] - - if not os.path.exists(name): - - def retrieve(request_url): - print("Fetching", request_url) - try: - return urllib.request.urlopen(request_url) - except urllib.error.URLError: - return urllib.request.urlopen(request_url) - - try: - r = retrieve(url) - except urllib.error.HTTPError: - if depends_filename: - r = retrieve( - "https://github.com/python-pillow/pillow-depends/raw/master/" - + depends_filename - ) - name = depends_filename - content = r.read() - with open(name, "wb") as fd: - fd.write(content) - return name - - -if __name__ == "__main__": - fetch(sys.argv[1]) diff --git a/winbuild/fribidi.cmake b/winbuild/fribidi.cmake index 247e79e4c..112174731 100644 --- a/winbuild/fribidi.cmake +++ b/winbuild/fribidi.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.13) +cmake_minimum_required(VERSION 3.12) project(fribidi) diff --git a/winbuild/get_pythons.py b/winbuild/get_pythons.py deleted file mode 100644 index a853fc6f7..000000000 --- a/winbuild/get_pythons.py +++ /dev/null @@ -1,15 +0,0 @@ -import os - -from fetch import fetch - -if __name__ == "__main__": - for version in ["3.4.4"]: - for platform in ["", ".amd64"]: - for extension in ["", ".asc"]: - fetch( - "https://www.python.org/ftp/python/%s/python-%s%s.msi%s" - % (version, version, platform, extension) - ) - - # find pip, if it's not in the path! - os.system("pip install virtualenv") diff --git a/winbuild/lcms2_patch.ps1 b/winbuild/lcms2_patch.ps1 deleted file mode 100644 index 7fc48c034..000000000 --- a/winbuild/lcms2_patch.ps1 +++ /dev/null @@ -1,9 +0,0 @@ - -Get-ChildItem .\Projects\VC2015\ *.vcxproj -recurse | - Foreach-Object { - $c = ($_ | Get-Content) - $c = $c -replace 'MultiThreaded<','MultiThreadedDLL<' - $c = $c -replace '8.1','10' - $c = $c -replace 'v140','v142' - [IO.File]::WriteAllText($_.FullName, ($c -join "`r`n")) - } diff --git a/winbuild/raqm.cmake b/winbuild/raqm.cmake index 88eb7f284..e8e71800e 100644 --- a/winbuild/raqm.cmake +++ b/winbuild/raqm.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.13) +cmake_minimum_required(VERSION 3.12) project(libraqm) diff --git a/winbuild/test.py b/winbuild/test.py deleted file mode 100755 index a05a20b18..000000000 --- a/winbuild/test.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 - -import glob -import os -import subprocess -import sys - -from config import VIRT_BASE, X64_EXT, pythons - - -def test_one(params): - python, architecture = params - try: - print("Running: %s, %s" % params) - command = [ - r"{}\{}{}\Scripts\python.exe".format(VIRT_BASE, python, architecture), - "test-installed.py", - "--processes=-0", - "--process-timeout=30", - ] - command.extend(glob.glob("Tests/test*.py")) - proc = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE) - (trace, stderr) = proc.communicate() - status = proc.returncode - print("Done with {}, {} -- {}".format(python, architecture, status)) - return (python, architecture, status, trace) - except Exception as msg: - print("Error with {}, {}: {}".format(python, architecture, msg)) - return (python, architecture, -1, str(msg)) - - -if __name__ == "__main__": - - os.chdir("..") - matrix = [ - (python, architecture) for python in pythons for architecture in ("", X64_EXT) - ] - - results = map(test_one, matrix) - - for (python, architecture, status, trace) in results: - print("{}{}: {}".format(python, architecture, status and "ERR" or "PASS")) - - res = all(status for (python, architecture, status, trace) in results) - sys.exit(res) diff --git a/winbuild/untar.py b/winbuild/untar.py deleted file mode 100644 index f2713b2f2..000000000 --- a/winbuild/untar.py +++ /dev/null @@ -1,11 +0,0 @@ -import sys -import tarfile - - -def untar(src, dest): - with tarfile.open(src, "r:gz") as tgz: - tgz.extractall(dest) - - -if __name__ == "__main__": - untar(sys.argv[1], sys.argv[2]) diff --git a/winbuild/unzip.py b/winbuild/unzip.py deleted file mode 100644 index eb17a2e63..000000000 --- a/winbuild/unzip.py +++ /dev/null @@ -1,11 +0,0 @@ -import sys -import zipfile - - -def unzip(src, dest): - with zipfile.ZipFile(src) as zf: - zf.extractall(dest) - - -if __name__ == "__main__": - unzip(sys.argv[1], sys.argv[2])