diff --git a/.appveyor.yml b/.appveyor.yml index f299794b6a2..f6ac309c49a 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,16 +13,8 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/vp/pypy2 - EXECUTABLE: bin/pypy.exe - PIP_DIR: bin - VENV: YES - - PYTHON: C:/Python27-x64 - - PYTHON: C:/Python37 - - PYTHON: C:/Python27 - - PYTHON: C:/Python37-x64 - - PYTHON: C:/Python36 - - PYTHON: C:/Python36-x64 + - PYTHON: C:/Python38 + - PYTHON: C:/Python38-x64 - PYTHON: C:/Python35 - PYTHON: C:/Python35-x64 - PYTHON: C:/msys64/mingw32 @@ -44,11 +36,6 @@ install: - xcopy c:\pillow-depends\*.tar.gz c:\pillow\winbuild\ - xcopy /s c:\pillow-depends\test_images\* c:\pillow\tests\images - cd c:\pillow\winbuild\ -- ps: | - if ($env:PYTHON -eq "c:/vp/pypy2") - { - c:\pillow\winbuild\appveyor_install_pypy2.cmd - } - ps: | if ($env:PYTHON -eq "c:/vp/pypy3") { @@ -65,6 +52,9 @@ install: c:\pillow\winbuild\build_deps.cmd $host.SetShouldExit(0) } +- curl -fsSL -o gs952.exe https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs952/gs952w32.exe +- gs952.exe /S +- path %path%;C:\Program Files (x86)\gs\gs9.52\bin build_script: - ps: | @@ -85,12 +75,13 @@ build_script: test_script: - cd c:\pillow - '%PYTHON%\%PIP_DIR%\pip.exe install pytest pytest-cov' -- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests' +- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% +- '%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% +- codecov --file coverage.xml --name %PYTHON% --flags AppVeyor matrix: fast_finish: true diff --git a/.azure-pipelines/jobs/lint.yml b/.azure-pipelines/jobs/lint.yml deleted file mode 100644 index d017590f8f4..00000000000 --- 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 41dc2daece8..00000000000 --- 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 00000000000..dcf276daa56 --- /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.8" ]; then + # Coverage and quality reports on just the latest diff. + depends/diffcover-install.sh + depends/diffcover-run.sh +fi diff --git a/.ci/build.sh b/.ci/build.sh new file mode 100755 index 00000000000..a2e3041bd27 --- /dev/null +++ b/.ci/build.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +coverage erase +if [ $(uname) == "Darwin" ]; then + export CPPFLAGS="-I/usr/local/miniconda/include"; +fi +make clean +make install-coverage diff --git a/.travis/install.sh b/.ci/install.sh similarity index 60% rename from .travis/install.sh rename to .ci/install.sh index 72588093413..8e819631aa5 100755 --- a/.travis/install.sh +++ b/.ci/install.sh @@ -3,7 +3,7 @@ set -e sudo apt-get update -sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk python-qt4\ +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 @@ -15,9 +15,17 @@ pip install -U pytest-cov pip install pyroma pip install test-image-results pip install numpy - -# docs only on Python 2.7 -if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then pip install -r requirements.txt ; 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 pyqt5-dev-tools + pip install pyqt5!=5.14.1 + fi +fi + +# docs only on Python 3.8 +if [ "$TRAVIS_PYTHON_VERSION" == "3.8" ]; then pip install -r requirements.txt ; fi # webp pushd depends && ./install_webp.sh && popd diff --git a/.ci/test.sh b/.ci/test.sh new file mode 100755 index 00000000000..516581ff008 --- /dev/null +++ b/.ci/test.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +python -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests + +# Docs +if [ "$TRAVIS_PYTHON_VERSION" == "3.8" ] && [ "$TRAVIS_CPU_ARCH" == "amd64" ]; then + make doccheck +fi diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index 3e147d1511f..00000000000 --- a/.codecov.yml +++ /dev/null @@ -1,9 +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 - -comment: off diff --git a/.coveragerc b/.coveragerc index ea79190ae0e..f71b6b1a281 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 b3d45665969..3d27b5d88c6 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -9,14 +9,14 @@ 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 on Python 2.7 and 3.x. 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.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. - Create a pull request to pull the changes from your branch to the Pillow master. ### Guidelines - Separate code commits from reformatting commits. - Provide tests for any newly added code. -- Follow PEP8. +- 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. ## Reporting Issues diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ca04afe02ab..e0e6804bfe4 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -tidelift: pypi/pillow +tidelift: "pypi/Pillow" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4bd02b674d0..3cad8d4170b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,17 +8,36 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [3.7] + python-version: ["3.8"] - name: Python ${{ matrix.python }} + name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python }} + - name: pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: lint-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + lint-pip- + + - name: pre-commit cache + uses: actions/cache@v1 + with: + path: ~/.cache/pre-commit + key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} + restore-keys: | + lint-pre-commit- + + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: - python-version: ${{ matrix.python }} + python-version: ${{ matrix.python-version }} + + - name: Build system information + run: python .github/workflows/system-info.py - name: Install dependencies run: | @@ -27,3 +46,4 @@ jobs: - name: Lint run: tox -e lint + diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh new file mode 100755 index 00000000000..6cd9dadf3d1 --- /dev/null +++ b/.github/workflows/macos-install.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype + +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 + +# 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 00000000000..8e840319a7d --- /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 new file mode 100644 index 00000000000..c10f9af3e74 --- /dev/null +++ b/.github/workflows/test-docker.yml @@ -0,0 +1,66 @@ +name: Test Docker + +on: [push, pull_request] + +jobs: + build: + + 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, + 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, + ] + dockerTag: [master] + + name: ${{ matrix.docker }} + + steps: + - uses: actions/checkout@v1 + + - name: Build system information + run: python .github/workflows/system-info.py + + - name: Docker pull + run: | + docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + + - name: Docker build + run: | + # The Pillow user in the docker container is UID 1000 + sudo chown -R 1000 $GITHUB_WORKSPACE + docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + sudo chown -R runner $GITHUB_WORKSPACE + + - name: After success + if: 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 + if: success() + uses: codecov/codecov-action@v1 + with: + flags: GHA_Docker + name: ${{ matrix.docker }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml new file mode 100644 index 00000000000..705c61e50a0 --- /dev/null +++ b/.github/workflows/test-windows.yml @@ -0,0 +1,389 @@ +name: Test Windows + +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"] + architecture: ["x86", "x64"] + include: + - architecture: "x86" + platform-vcvars: "x86" + platform-msbuild: "Win32" + - architecture: "x64" + platform-vcvars: "x86_amd64" + platform-msbuild: "x64" + exclude: + # 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 + + - uses: actions/checkout@v1 + with: + repository: python-pillow/pillow-depends + ref: master + + - name: Cache + uses: actions/cache@v1 + with: + path: ~\AppData\Local\pip\Cache + key: + ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.architecture }}-${{ hashFiles('**/.github/workflows/test-windows.yml') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.architecture }}- + ${{ runner.os }}-${{ matrix.python-version }}- + + # sets env: pythonLocation + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + architecture: ${{ matrix.architecture }} + + - name: Build system information + run: python .github/workflows/system-info.py + + - name: pip install wheel pytest pytest-cov + run: | + "%pythonLocation%\python.exe" -m pip install wheel pytest pytest-cov + shell: cmd + + - 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" + + ..\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 + shell: pwsh + + - name: Build dependencies / libjpeg + if: false + run: | + REM FIXME uses /MT not /MD, see makefile.vc and win32.mak for more info + + 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-9d + 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: 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 + + - 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 + + - 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.1.0 + 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 + + - 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 + + - 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 + + - 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 + + # GPL licensed; skip if building wheels + - 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 e5d454b: Merge tag '2.12.6' into msvc + cd /D %BUILD%\libimagequant-e5d454bc7f5eb63ee50c84a83a7fa5ac94f68ec4 + 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 + + # for Raqm + - 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.4 + 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 + - 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.9 + 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 + + - 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% + 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 + + # 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 + + - 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 -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 + if: failure() + with: + name: errors + path: Tests/errors + + - name: After success + if: success() + run: | + .ci/after_success.sh + shell: pwsh + + - name: Upload coverage + if: success() + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml + flags: GHA_Windows + name: ${{ runner.os }} Python ${{ matrix.python-version }} + + - name: Build wheel + id: wheel + if: "github.event_name == 'push' && !contains(matrix.python-version, 'pypy')" + 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 + shell: cmd + + - uses: actions/upload-artifact@v1 + if: "github.event_name == 'push' && !contains(matrix.python-version, 'pypy')" + with: + name: ${{ steps.wheel.outputs.dist }} + path: dist diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000000..f1643edd615 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,107 @@ +name: Test + +on: [push, pull_request] + +jobs: + build: + + strategy: + fail-fast: false + matrix: + os: [ + "ubuntu-latest", + "macOS-latest", + ] + python-version: [ + "pypy3", + "3.8", + "3.7", + "3.6", + "3.5", + ] + include: + - python-version: "3.5" + env: PYTHONOPTIMIZE=2 + - python-version: "3.6" + env: PYTHONOPTIMIZE=1 + # 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@v2 + + - name: Ubuntu cache + uses: actions/cache@v1 + if: startsWith(matrix.os, 'ubuntu') + with: + path: ~/.cache/pip + key: + ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/.ci/*.sh') }} + restore-keys: | + ${{ matrix.os }}-${{ matrix.python-version }}- + + - name: macOS cache + uses: actions/cache@v1 + if: startsWith(matrix.os, 'macOS') + with: + path: ~/Library/Caches/pip + key: + ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/.ci/*.sh') }} + restore-keys: | + ${{ matrix.os }}-${{ matrix.python-version }}- + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Build system information + run: python .github/workflows/system-info.py + + - name: Install Linux dependencies + if: startsWith(matrix.os, 'ubuntu') + run: | + .ci/install.sh + + - name: Install macOS dependencies + if: startsWith(matrix.os, 'macOS') + run: | + .github/workflows/macos-install.sh + + - name: Build + run: | + .ci/build.sh + + - name: Test + run: | + .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 + if: failure() + with: + name: errors + path: Tests/errors + + - name: After success + if: success() + run: | + .ci/after_success.sh + + - name: Upload coverage + if: success() + run: bash <(curl -s https://codecov.io/bash) -F ${{ matrix.codecov-flag }} + env: + CODECOV_NAME: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000000..e30453c3ed2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +repos: + - repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black + args: ["--target-version", "py35"] + # Only .py files, until https://github.com/psf/black/issues/402 resolved + files: \.py$ + types: [] + + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.9 + hooks: + - id: flake8 + additional_dependencies: [flake8-2020, flake8-implicit-str-concat] + + - repo: https://github.com/timothycrosley/isort + rev: 4.3.21 + hooks: + - id: isort + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.5.1 + hooks: + - id: python-check-blanket-noqa + - id: rst-backticks + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.5.0 + hooks: + - id: check-merge-conflict + - id: check-yaml diff --git a/.travis.yml b/.travis.yml index 545cb0b50d9..e77102e44a2 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 them a headstart and reduce waiting time. -# Run latest 3.x and 2.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,66 +18,50 @@ matrix: - python: "3.6" name: "Lint" env: LINT="true" - - python: "pypy" - name: "PyPy2 Xenial" + + - python: "3.6" + arch: arm64 + - python: "3.7" + arch: ppc64le + - python: "3.5" + arch: s390x + - python: "pypy3" name: "PyPy3 Xenial" + - python: "3.8" + name: "3.8 Xenial" + services: xvfb - python: '3.7' name: "3.7 Xenial" - - python: '2.7' - name: "2.7 Xenial" - - python: "2.7_with_system_site_packages" # For PyQt4 - name: "2.7_with_system_site_packages Xenial" 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 - - python: "3.8-dev" - name: "3.8-dev Xenial" - - 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-29-amd64" DOCKER_TAG="master" - - env: DOCKER="fedora-30-amd64" DOCKER_TAG="master" - -services: - - docker - -before_install: - - if [ "$DOCKER" ]; then travis_retry docker pull pythonpillow/$DOCKER:$DOCKER_TAG; fi + services: xvfb install: - | if [ "$LINT" == "true" ]; then pip install tox - elif [ "$DOCKER" == "" ]; then - .travis/install.sh; + else + .ci/install.sh; fi script: - | if [ "$LINT" == "true" ]; then tox -e lint - elif [ "$DOCKER" == "" ]; then - .travis/script.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 1dca2ccb930..00000000000 --- a/.travis/after_success.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# gather the coverage data -sudo apt-get -qq install lcov -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 -if [[ $TRAVIS_PYTHON_VERSION != "2.7_with_system_site_packages" ]]; then - # Not working here. Just skip it, it's being removed soon. - pip install coveralls-merge - coveralls-merge coverage.c.json -fi -codecov - -if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ] && [ "$DOCKER" == "" ]; then - # Coverage and quality reports on just the latest diff. - # (Installation is very slow on Py3, so just do it for Py2.) - depends/diffcover-install.sh - depends/diffcover-run.sh -fi diff --git a/.travis/script.sh b/.travis/script.sh deleted file mode 100755 index af56cc6ab92..00000000000 --- a/.travis/script.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -e - -coverage erase -make clean -make install-coverage - -python -m pytest -v -x --cov PIL --cov-report term Tests - -# Docs -if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then make doccheck; fi diff --git a/CHANGES.rst b/CHANGES.rst index d9e3bac80e9..eec3c2fcf71 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,11 +2,203 @@ Changelog (Pillow) ================== -6.2.0 (2019-10-01) +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] + +- Remove deprecated __version__ from plugins #4197 + [hugovk, radarhere] + +- Fixed freeing unallocated pointer when resizing with height too large #4116 + [radarhere] + +- Copy info in Image.transform #4128 + [radarhere] + +- Corrected DdsImagePlugin setting info gamma #4171 + [radarhere] + +- 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 + [jdufresne] + +- Removed CI testing of Fedora 29 #4165 + [hugovk] + +- Added pypy3 to tox envlist #4137 + [jdufresne] + +- Drop support for EOL PyQt4 and PySide #4108 + [hugovk, radarhere] + +- Removed deprecated setting of TIFF image sizes #4114 + [radarhere] + +- Removed deprecated PILLOW_VERSION #4107 + [hugovk] + +- Changed default frombuffer raw decoder args #1730 + [radarhere] + +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] + +6.2.0 (2019-10-01) +------------------ + - Catch buffer overruns #4104 [radarhere] diff --git a/LICENSE b/LICENSE index c106eeb1aed..4aac532f486 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: -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 79f4e2adb37..a5f726b040a 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,12 @@ 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 1803e617d15..6e55e4c7a98 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ .DEFAULT_GOAL := release-test clean: - python setup.py clean + python3 setup.py clean rm src/PIL/*.so || true rm -r build || true find . -name __pycache__ | xargs rm -r || true @@ -15,8 +15,7 @@ co: done coverage: - python selftest.py - python setup.py test + pytest -qq rm -r htmlcov || true coverage report @@ -30,7 +29,7 @@ doccheck: $(MAKE) -C docs linkcheck || true docserve: - cd docs/_build/html && python -mSimpleHTTPServer 2> /dev/null& + cd docs/_build/html && python3 -mSimpleHTTPServer 2> /dev/null& help: @echo "Welcome to Pillow development. Please use \`make \` where is one of" @@ -50,22 +49,22 @@ help: @echo " upload-test build and upload sdists to test.pythonpackages.com" inplace: clean - python setup.py develop build_ext --inplace + python3 setup.py develop build_ext --inplace install: - python setup.py install - python selftest.py + python3 setup.py install + python3 selftest.py install-coverage: - CFLAGS="-coverage" python setup.py build_ext install - python selftest.py + CFLAGS="-coverage" python3 setup.py build_ext install + python3 selftest.py 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 # see any build failures. make clean > /dev/null - CFLAGS='-g -O0' python setup.py build_ext install > /dev/null + CFLAGS='-g -O0' python3 setup.py build_ext install > /dev/null install-req: pip install -r requirements.txt @@ -76,17 +75,17 @@ install-venv: release-test: $(MAKE) install-req - python setup.py develop - python selftest.py - python -m pytest Tests - python setup.py install - python -m pytest -qq + python3 setup.py develop + python3 selftest.py + python3 -m pytest Tests + python3 setup.py install + python3 -m pytest -qq check-manifest pyroma . viewdoc sdist: - python setup.py sdist --format=gztar + python3 setup.py sdist --format=gztar test: pytest -qq @@ -97,10 +96,10 @@ upload-test: # username: # password: # repository = http://test.pythonpackages.com - python setup.py sdist --format=gztar upload -r test + python3 setup.py sdist --format=gztar upload -r test upload: - python setup.py sdist --format=gztar upload + python3 setup.py sdist --format=gztar upload readme: viewdoc diff --git a/README.rst b/README.rst index 6b783a95a46..c1d5be57912 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ 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 `_. +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 @@ -14,7 +14,7 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors *' \ + wget -m -A 'Pillow--*' \ http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com ``` diff --git a/Tests/README.rst b/Tests/README.rst index da3297bcea7..55464578702 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 99c7006fac4..f196757dcc6 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -2,7 +2,7 @@ from PIL import PyAccess -from .helper import PillowTestCase, hopper, unittest +from .helper import hopper # Not running this test by default. No DOS against Travis CI. @@ -40,22 +40,17 @@ def timer(func, label, *args): ) -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 8a54ff9219c..00000000000 --- 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 00000000000..739ad224e7e --- /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 db6559f1eed..08a55d349d5 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -1,17 +1,10 @@ from PIL import Image -from .helper import PillowTestCase, unittest - 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_icns_dos.py b/Tests/check_icns_dos.py index 03eda2e3f81..3f4fb6518e2 100644 --- a/Tests/check_icns_dos.py +++ b/Tests/check_icns_dos.py @@ -4,9 +4,5 @@ from io import BytesIO from PIL import Image -from PIL._util import py3 -if py3: - Image.open(BytesIO(bytes("icns\x00\x00\x00\x10hang\x00\x00\x00\x00", "latin-1"))) -else: - Image.open(BytesIO(bytes("icns\x00\x00\x00\x10hang\x00\x00\x00\x00"))) +Image.open(BytesIO(b"icns\x00\x00\x00\x10hang\x00\x00\x00\x00")) diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 2b9a9605be4..db12d00e3f5 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,49 +1,44 @@ #!/usr/bin/env python - -from __future__ import division - -import sys - +import pytest from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import is_win32 min_iterations = 100 max_iterations = 10000 +pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") + + +def _get_mem_usage(): + from resource import getpagesize, getrusage, RUSAGE_SELF + + 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 = "memory usage limit exceeded after %d iterations" % (i + 1) + 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()) + -@unittest.skipIf(sys.platform.startswith("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)), - ) - - -if __name__ == "__main__": - unittest.main() +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_dos.py b/Tests/check_j2k_dos.py index 7d0e95a60bc..273c18585e8 100644 --- a/Tests/check_j2k_dos.py +++ b/Tests/check_j2k_dos.py @@ -4,19 +4,5 @@ from io import BytesIO from PIL import Image -from PIL._util import py3 -if py3: - Image.open( - BytesIO( - bytes( - "\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang", - "latin-1", - ) - ) - ) - -else: - Image.open( - BytesIO(bytes("\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang")) - ) +Image.open(BytesIO(b"\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang")) diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index 4614529ed11..5cef4b544ee 100755 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,46 +1,41 @@ -import sys from io import BytesIO +import pytest from PIL import Image -from .helper import PillowTestCase, unittest +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" +pytestmark = [ + pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"), + skip_unless_feature("jpg_2000"), +] -@unittest.skipIf(sys.platform.startswith("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 +def test_leak_load(): + 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() + 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() +def test_leak_save(): + from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK - -if __name__ == "__main__": - unittest.main() + 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 3e6cf8d34ef..f20ad674819 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,16 +1,9 @@ +import pytest from PIL import Image -from .helper import PillowTestCase, unittest - -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(IOError): + im.save(target) diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py new file mode 100755 index 00000000000..a7a343c98ec --- /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 2f758ba10c1..b63fa2a1e59 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,7 +1,8 @@ -import sys from io import BytesIO -from .helper import PillowTestCase, hopper, unittest +import pytest + +from .helper import hopper, is_win32 iterations = 5000 @@ -15,10 +16,9 @@ """ -@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") -class TestJpegLeaks(PillowTestCase): +pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") - """ +""" pre patch: MB @@ -74,49 +74,51 @@ class TestJpegLeaks(PillowTestCase): """ - 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 - ) - ] - - 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 - ) - ] - - qtables = [standard_l_qtable, standard_chrominance_qtable] - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", qtables=qtables) - - def test_exif_leak(self): - """ + +def test_qtables_leak(): + 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 + ) + ] + + 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 + ) + ] + + qtables = [standard_l_qtable, standard_chrominance_qtable] + + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", qtables=qtables) + + +def test_exif_leak(): + """ pre patch: MB @@ -171,15 +173,16 @@ def test_exif_leak(self): 0 11.33 """ - im = hopper("RGB") - exif = b"12345678" * 4096 + im = hopper("RGB") + exif = b"12345678" * 4096 - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", exif=exif) + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", exif=exif) - def test_base_save(self): - """ + +def test_base_save(): + """ base case: MB 20.99^ ::::: :::::::::::::::::::::::::::::::::::::::::::@::: @@ -205,12 +208,8 @@ def test_base_save(self): 0 +----------------------------------------------------------------------->Gi 0 7.882 """ - im = hopper("RGB") - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG") - + im = hopper("RGB") -if __name__ == "__main__": - unittest.main() + 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 5df476e0a5f..f44a5a5bb37 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,9 +1,8 @@ import sys +import pytest from PIL import Image -from .helper import PillowTestCase, unittest - # This test is not run automatically. # # It requires > 2gb memory for the >2 gigapixel image generated in the @@ -23,26 +22,26 @@ 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) +pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") + + +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(self): - """ succeeded prepatch""" - self._write_png(XDIM, YDIM) +def test_large(tmp_path): + """ succeeded prepatch""" + _write_png(tmp_path, 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) +def test_2gpx(tmp_path): + """failed prepatch""" + _write_png(tmp_path, XDIM, XDIM) -if __name__ == "__main__": - unittest.main() +@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 4653a601334..de6f4571cb8 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,9 +1,8 @@ import sys +import pytest from PIL import Image -from .helper import PillowTestCase, unittest - # This test is not run automatically. # # It requires > 2gb memory for the >2 gigapixel image generated in the @@ -13,32 +12,28 @@ # 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) +pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") + + +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(self): - """ succeeded prepatch""" - self._write_png(XDIM, YDIM) - def test_2gpx(self): - """failed prepatch""" - self._write_png(XDIM, XDIM) +def test_large(tmp_path): + """ succeeded prepatch""" + _write_png(tmp_path, XDIM, YDIM) -if __name__ == "__main__": - unittest.main() +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 ae9a46d1b6c..5187385d621 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -1,20 +1,14 @@ +import pytest from PIL import Image -from .helper import PillowTestCase, unittest - 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(IOError): + 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 5c78ce12281..86eb937e9ba 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -3,66 +3,59 @@ from PIL import Image, ImageFile, PngImagePlugin -from .helper import PillowTestCase, unittest - 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") + for s in im.info.values(): + assert 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 +def test_dos_text(): - for s in im.text.values(): - self.assertLess(len(s), 1024 * 1024, "Text chunk larger than 1M") + try: + im = Image.open(TEST_FILE) + im.load() + except ValueError as msg: + assert msg, "Decompressed Data Too Large" + return - def test_dos_total_memory(self): - im = Image.new("L", (1, 1)) - compressed_data = zlib.compress(b"a" * 1024 * 1023) + for s in im.text.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" - 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) +def test_dos_total_memory(): + im = Image.new("L", (1, 1)) + compressed_data = zlib.compress(b"a" * 1024 * 1023) - b = BytesIO() - im.save(b, "PNG", pnginfo=info) - b.seek(0) + info = PngImagePlugin.PngInfo() - try: - im2 = Image.open(b) - except ValueError as msg: - self.assertIn("Too much memory", msg) - return + 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) - 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" - ) + 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 -if __name__ == "__main__": - unittest.main() + 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/check_tiff_crashes.py b/Tests/check_tiff_crashes.py new file mode 100644 index 00000000000..f4eb0437514 --- /dev/null +++ b/Tests/check_tiff_crashes.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +# 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 python check_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. + + +from PIL import Image + +repro_read_strip = ( + "images/crash_1.tif", + "images/crash_2.tif", +) + +for path in repro_read_strip: + with Image.open(path) as im: + try: + im.load() + except Exception as msg: + print(msg) diff --git a/Tests/conftest.py b/Tests/conftest.py new file mode 100644 index 00000000000..624eab73c25 --- /dev/null +++ b/Tests/conftest.py @@ -0,0 +1,12 @@ +import io + + +def pytest_report_header(config): + try: + from PIL import features + + with io.StringIO() as out: + features.pilinfo(out=out, supported_formats=False) + return out.getvalue() + except Exception as e: + return "pytest_report_header failed: %s" % e diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index 4d189dbad90..c7055995e37 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -from __future__ import print_function - import base64 import os diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index 726d5d797fd..b9488cb94e1 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -4,6 +4,7 @@ NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/ 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/ 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. diff --git a/Tests/fonts/ter-x20b-cp1250.pbm b/Tests/fonts/ter-x20b-cp1250.pbm new file mode 100644 index 00000000000..fe7e2c4dc03 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 00000000000..4da49e5fd41 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 00000000000..ffd840ae94e 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 00000000000..14d6e8be7b2 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 00000000000..ad5b3af8d75 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 00000000000..14d6e8be7b2 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 00000000000..962bcca6af4 Binary files /dev/null and b/Tests/fonts/ter-x20b.pcf differ diff --git a/Tests/helper.py b/Tests/helper.py index 78a2f520f92..15a51ccd1e9 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -1,16 +1,16 @@ """ Helper functions. """ -from __future__ import print_function import logging import os +import shutil import sys import tempfile -import unittest +from io import BytesIO -from PIL import Image, ImageMath -from PIL._util import py3 +import pytest +from PIL import Image, ImageMath, features logger = logging.getLogger(__name__) @@ -22,12 +22,26 @@ HAS_UPLOADER = True class test_image_results: - @classmethod - def upload(self, a, b): + @staticmethod + def upload(a, b): a.show() b.show() +elif "GITHUB_ACTIONS" in os.environ: + HAS_UPLOADER = True + + class test_image_results: + @staticmethod + def upload(a, b): + dir_errors = os.path.join(os.path.dirname(__file__), "errors") + os.makedirs(dir_errors, exist_ok=True) + tmpdir = tempfile.mkdtemp(dir=dir_errors) + a.save(os.path.join(tmpdir, "a.png")) + b.save(os.path.join(tmpdir, "b.png")) + return tmpdir + + else: try: import test_image_results @@ -50,195 +64,119 @@ def convert_to_comparable(a, b): return new_a, new_b -class PillowTestCase(unittest.TestCase): - def __init__(self, *args, **kwargs): - unittest.TestCase.__init__(self, *args, **kwargs) - # holds last result object passed to run method: - self.currentResult = None - - def run(self, result=None): - self.currentResult = result # remember result for use later - unittest.TestCase.run(self, result) # call superclass run method +def assert_deep_equal(a, b, msg=None): + try: + assert len(a) == len(b), msg or "got length {}, expected {}".format( + len(a), len(b) + ) + except Exception: + assert a == b, msg - def delete_tempfile(self, path): - try: - ok = self.currentResult.wasSuccessful() - except AttributeError: # for pytest - ok = True - if ok: - # only clean out tempfiles if test passed - try: - os.remove(path) - except OSError: - pass # report? - else: - print("=== orphaned temp file: %s" % path) - - def assert_deep_equal(self, a, b, msg=None): - try: - self.assertEqual( - len(a), len(b), msg or "got length %s, expected %s" % (len(a), len(b)) - ) - self.assertTrue( - all(x == y for x, y in zip(a, b)), msg or "got %s, expected %s" % (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" % (im.mode, mode) - ) - - if size is not None: - self.assertEqual( - im.size, size, msg or "got size %r, expected %r" % (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" % (a.mode, b.mode) +def assert_image(im, mode, size, msg=None): + if mode is not None: + assert im.mode == mode, msg or "got mode {!r}, expected {!r}".format( + im.mode, mode ) - self.assertEqual( - a.size, b.size, msg or "got size %r, expected %r" % (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" % (a.mode, b.mode) - ) - self.assertEqual( - a.size, b.size, msg or "got size %r, expected %r" % (a.size, b.size) + + if size is not None: + assert im.size == size, msg or "got size {!r}, expected {!r}".format( + im.size, 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 = 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_image_equal(a, b, msg=None): + assert a.mode == b.mode, msg or "got mode {!r}, expected {!r}".format( + a.mode, b.mode + ) + assert 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 + + 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 "got mode {!r}, expected {!r}".format( + a.mode, b.mode + ) + assert a.size == b.size, msg or "got size {!r}, expected {!r}".format( + a.size, b.size + ) + + a, b = convert_to_comparable(a, b) - def assert_not_all_same(self, items, msg=None): - self.assertNotEqual(items.count(items[0]), len(items), msg) + 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 "" + ) + " 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(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_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 +def assert_not_all_same(items, msg=None): + assert items.count(items[0]) != len(items), msg - self.assertTrue(value, msg + ": " + repr(actuals) + " != " + repr(targets)) - def skipKnownBadTest(self, msg=None, platform=None, travis=None, interpreter=None): - # Skip if platform/travis matches, and - # 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 +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 - skip = True - if platform is not None: - skip = sys.platform.startswith(platform) - if travis is not None: - skip = skip and (travis == bool(os.environ.get("TRAVIS", False))) - if interpreter is not None: - skip = skip and ( - interpreter == "pypy" and hasattr(sys, "pypy_version_info") - ) - if skip: - self.skipTest(msg or "Known Bad Test") + assert value, msg + ": " + repr(actuals) + " != " + repr(targets) - 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 skip_known_bad_test(msg=None): + # Skip if PILLOW_RUN_KNOWN_BAD is not true in the environment. + if not os.environ.get("PILLOW_RUN_KNOWN_BAD", False): + pytest.skip(msg or "Known bad test") - def open_withImagemagick(self, f): - if not imagemagick_available(): - raise IOError() - outfile = self.tempfile("temp.png") - if command_succeeds([IMCONVERT, f, outfile]): - return Image.open(outfile) - raise IOError() +def skip_unless_feature(feature): + reason = "%s not available" % feature + return pytest.mark.skipif(not features.check(feature), reason=reason) -@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") -class PillowLeakTestCase(PillowTestCase): +@pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") +class PillowLeakTestCase: # requires unix/macOS iterations = 100 # count mem_limit = 512 # k @@ -272,26 +210,17 @@ def _test_leak(self, core): core() mem = self._get_mem_usage() - start_mem msg = "memory usage limit exceeded in iteration %d" % cycle - self.assertLess(mem, self.mem_limit, msg) + assert mem < self.mem_limit, msg # helpers -if not py3: - # Remove DeprecationWarning in Python 3 - PillowTestCase.assertRaisesRegex = PillowTestCase.assertRaisesRegexp - PillowTestCase.assertRegex = PillowTestCase.assertRegexpMatches - def fromstring(data): - from io import BytesIO - return Image.open(BytesIO(data)) def tostring(im, string_format, **options): - from io import BytesIO - out = BytesIO() im.save(out, string_format, **options) return out.getvalue() @@ -318,47 +247,49 @@ 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. - """ - import subprocess - - with open(os.devnull, "wb") as f: - try: - subprocess.call(cmd, stdout=f, 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(): return "APPVEYOR" in os.environ +def on_github_actions(): + return "GITHUB_ACTIONS" in os.environ + + def on_ci(): # Travis and AppVeyor have "CI" # Azure Pipelines has "TF_BUILD" - return "CI" in os.environ or "TF_BUILD" in os.environ + # GitHub Actions has "GITHUB_ACTIONS" + return ( + "CI" in os.environ or "TF_BUILD" in os.environ or "GITHUB_ACTIONS" in os.environ + ) + + +def is_big_endian(): + return sys.byteorder == "big" + + +def is_win32(): + return sys.platform.startswith("win32") + + +def is_pypy(): + return hasattr(sys, "pypy_translation_info") if sys.platform == "win32": @@ -369,15 +300,7 @@ def on_ci(): 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(object): +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 00000000000..28612238a9c 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 00000000000..f3f840a08e3 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 00000000000..f40777ac582 Binary files /dev/null and b/Tests/images/01r_00.pcx differ diff --git a/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds b/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds new file mode 100644 index 00000000000..9b4d8e21f64 Binary files /dev/null and b/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds differ diff --git a/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.png b/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.png new file mode 100644 index 00000000000..57177fe2bb8 Binary files /dev/null and b/Tests/images/DXGI_FORMAT_BC7_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 00000000000..3fe0f4ca789 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 00000000000..3ee5fe3bf27 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 00000000000..1af30f81f7e 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 00000000000..d90c54967b6 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 00000000000..0f290fd7fdb 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 00000000000..296a29d4c11 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 00000000000..213f8854969 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 00000000000..5b68c7b4409 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 00000000000..58ca904abdc 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 00000000000..af42766b5ed 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 00000000000..a5779855fc6 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 00000000000..64cceaae83a 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 00000000000..3f082665c99 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 00000000000..99d53b71812 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 00000000000..bad60c767fb 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 00000000000..a029a959b5d 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 00000000000..b63ebc0b35d 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 00000000000..427b829a025 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 00000000000..77694ff1d15 Binary files /dev/null and b/Tests/images/apng/dispose_op_background_final.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 00000000000..05948d44aed 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 00000000000..3094c1d23d6 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 00000000000..4e1dbf77e45 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 00000000000..1c15f132fe7 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 00000000000..858f6f0382b 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 00000000000..3f9b3cfae76 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 00000000000..f326afa5c27 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 00000000000..d0418ddd75f 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 00000000000..1210e373797 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 00000000000..29ed7d1ea12 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 00000000000..f9307f63504 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 00000000000..11ccfb6cba0 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 00000000000..e95425ac194 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 00000000000..f3c4c9f9e6d 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 00000000000..4d76802e4cc 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 00000000000..fb25394305f 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 00000000000..29ac75e1675 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 00000000000..25dd9bcd868 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 00000000000..dc78e9bb13b 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 00000000000..5d951ffe2a5 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 00000000000..d5cf83f9f98 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 00000000000..27d1d3eb5a4 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 00000000000..5e040743a1d 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 00000000000..0cd5bea856b 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 00000000000..db7581fbdfc 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 00000000000..2dc58b929a4 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 00000000000..14a76d9d618 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 00000000000..bba9cdfd580 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 00000000000..ca7b13ab8ab 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 00000000000..6f895f91d75 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 00000000000..0cb7ea36e3e 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 00000000000..89f2b75e257 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 00000000000..8341383a0e6 Binary files /dev/null and b/Tests/images/app13-multiple.jpg differ diff --git a/Tests/images/crash_1.tif b/Tests/images/crash_1.tif new file mode 100644 index 00000000000..230d4439aad Binary files /dev/null and b/Tests/images/crash_1.tif differ diff --git a/Tests/images/crash_2.tif b/Tests/images/crash_2.tif new file mode 100644 index 00000000000..26c00d0ff1a Binary files /dev/null and b/Tests/images/crash_2.tif differ diff --git a/Tests/images/drawing_wmf_ref_144.png b/Tests/images/drawing_wmf_ref_144.png new file mode 100644 index 00000000000..20ed9ce597b Binary files /dev/null and b/Tests/images/drawing_wmf_ref_144.png differ diff --git a/Tests/images/exif_imagemagick.png b/Tests/images/exif_imagemagick.png new file mode 100644 index 00000000000..6f59224c854 Binary files /dev/null and b/Tests/images/exif_imagemagick.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 00000000000..eac0e4304f2 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 00000000000..49f92b19bed --- /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 00000000000..3a5864c84c5 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 00000000000..2b3d15b55ae 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 00000000000..a631721321a 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 00000000000..4c17cbb3dee 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 00000000000..3286d94f1c7 --- /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 00000000000..7972880cecd 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 00000000000..d75605cea64 --- /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 00000000000..1102c69ca3b 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 00000000000..d30326fe0b0 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 00000000000..7f3db178e60 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 00000000000..f05375e843b 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 00000000000..03794432419 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 00000000000..1527cbf91a0 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 00000000000..c9dea41351d 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 00000000000..698101443c5 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 00000000000..12058480a44 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 00000000000..448b0a812d7 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 00000000000..db1b5fe5870 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 00000000000..145b8b074a2 --- /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 00000000000..c4e416f3903 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 00000000000..5a8659f7c0b 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 00000000000..7922e0ba895 --- /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 00000000000..af968970b65 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 00000000000..ae027fc1180 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 00000000000..ab92f4b6a83 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 00000000000..533ffa027e8 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 00000000000..b07ef6496af 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 00000000000..145b8b074a2 --- /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 00000000000..dff5a01e804 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 00000000000..bec9db779a7 --- /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 00000000000..1ad3444fec3 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 00000000000..cd6429884c4 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 00000000000..2a4be914cb8 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 00000000000..0b547c7e2ec 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 00000000000..0bf7752300e 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 00000000000..c35b8e232f9 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 00000000000..b99ce01b307 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 00000000000..145b8b074a2 --- /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 00000000000..9189d6ed03f 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 00000000000..397ad4748a3 --- /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 00000000000..24a99dacc4f 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 00000000000..02067a32c10 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 00000000000..649668c0ad9 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 00000000000..bff28ccfcea 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 00000000000..145b8b074a2 --- /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 00000000000..e074e4a76c0 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 00000000000..6cfd7f6478f 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 00000000000..ff5a6b63b19 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 00000000000..12c15b43ea7 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 00000000000..4afdb6f8909 Binary files /dev/null and b/Tests/images/fli_overrun2.bin differ diff --git a/Tests/images/hopper_long_name.im b/Tests/images/hopper_long_name.im new file mode 100644 index 00000000000..ff45b7c7539 Binary files /dev/null and b/Tests/images/hopper_long_name.im differ diff --git a/Tests/images/imagedraw_chord_zero_width.png b/Tests/images/imagedraw_chord_zero_width.png new file mode 100644 index 00000000000..c1c0058d766 Binary files /dev/null and b/Tests/images/imagedraw_chord_zero_width.png differ diff --git a/Tests/images/imagedraw_ellipse_translucent.png b/Tests/images/imagedraw_ellipse_translucent.png new file mode 100644 index 00000000000..964dce67889 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_translucent.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 00000000000..f14a279f2fe 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 4139e66d84e..daaf9422d30 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_pieslice_zero_width.png b/Tests/images/imagedraw_pieslice_zero_width.png new file mode 100644 index 00000000000..4ca05158327 Binary files /dev/null and b/Tests/images/imagedraw_pieslice_zero_width.png differ diff --git a/Tests/images/imagedraw_polygon_kite_L.png b/Tests/images/imagedraw_polygon_kite_L.png index 241d86bf40d..0d9a1c8f816 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 00000000000..989c9576196 Binary files /dev/null and b/Tests/images/imagedraw_rectangle_zero_width.png differ diff --git a/Tests/images/imagedraw_stroke_descender.png b/Tests/images/imagedraw_stroke_descender.png new file mode 100644 index 00000000000..93462334ae4 Binary files /dev/null and b/Tests/images/imagedraw_stroke_descender.png differ diff --git a/Tests/images/imageops_pad_h_0.jpg b/Tests/images/imageops_pad_h_0.jpg index f9fcb1cdb40..7afbbb96a6e 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 4b9b9ebc466..b9bf8a49a8d 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 2c822489253..7e0eb95994a 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 caf435796cf..73a96c86cac 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 4a6698e9154..04545f81742 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 792952bcd99..f3e399d7b18 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 00000000000..5fcb144aef1 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 00000000000..00f6bd2f305 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 00000000000..5f00b50595a 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 00000000000..a574872f267 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 00000000000..39a02fbbfdf 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 00000000000..9b138f6fe0a 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 00000000000..316d618818e 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 00000000000..f70e03a3960 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 00000000000..1907d5d3d47 Binary files /dev/null and b/Tests/images/sgi_overrun_expandrowF04.bin differ diff --git a/Tests/images/sugarshack_no_data.mpo b/Tests/images/sugarshack_no_data.mpo new file mode 100644 index 00000000000..d94bad53b1f Binary files /dev/null and b/Tests/images/sugarshack_no_data.mpo 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 00000000000..f1fa25b5539 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 00000000000..503337d2bfa Binary files /dev/null and b/Tests/images/test_draw_pbm_ter_pl_target.png 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 00000000000..979c7f17696 Binary files /dev/null and b/Tests/images/tiff_overflow_rows_per_strip.tif differ diff --git a/Tests/import_all.py b/Tests/import_all.py deleted file mode 100644 index 4dfacb2911e..00000000000 --- a/Tests/import_all.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import print_function - -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 bacb391faad..00000000000 --- a/Tests/make_hash.py +++ /dev/null @@ -1,68 +0,0 @@ -# brute-force search for access descriptor hash table - -from __future__ import print_function - -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 a6143f084cb..59fbac527ed 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 79d5d2bcb8e..4882e65e655 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(self): - self.assertEqual(_binary.i16le(b"\xff\xff\x00\x00"), 65535) - self.assertEqual(_binary.i32le(b"\xff\xff\x00\x00"), 65535) +def test_little_endian(): + assert _binary.i16le(b"\xff\xff\x00\x00") == 65535 + assert _binary.i32le(b"\xff\xff\x00\x00") == 65535 - self.assertEqual(_binary.o16le(65535), b"\xff\xff") - self.assertEqual(_binary.o32le(65535), b"\xff\xff\x00\x00") + assert _binary.o16le(65535) == b"\xff\xff" + assert _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) - self.assertEqual(_binary.o16be(65535), b"\xff\xff") - self.assertEqual(_binary.o32be(65535), b"\x00\x00\xff\xff") +def test_big_endian(): + assert _binary.i16be(b"\x00\x00\xff\xff") == 0 + assert _binary.i32be(b"\x00\x00\xff\xff") == 65535 + + 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 e6a75e2c370..ade2901b710 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,112 +1,110 @@ -from __future__ import print_function - 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: - im = Image.open(f) - im.load() - except Exception: # as msg: - pass - - # 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 test_bad(): + """ These shouldn't crash/dos, but they shouldn't return anything + either """ + for f in get_files("b"): + + def open(f): try: - im = Image.open(f) - im.load() - if os.path.basename(f) not in supported: - print("Please add %s to the partially supported bmp specs." % f) + with Image.open(f) as im: + im.load() except Exception: # as msg: - if os.path.basename(f) in supported: - raise - - def test_good(self): - """ 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", "%s.png" % name) - - for f in self.get_files("g"): - try: - im = Image.open(f) + pass + + # Assert that there is no unclosed file warning + pytest.warns(None, open, f) + + +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("Please add %s to the partially supported bmp specs." % f) + except Exception: # as msg: + if os.path.basename(f) in supported: + raise + + +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", "%s.png" % name) + + for f in get_files("g"): + try: + with Image.open(f) as im: im.load() - compare = Image.open(get_compare(f)) - 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 %s: %s" % (f, msg)) + 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, "Unsupported Image {}: {}".format(f, msg) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index c17e7996d09..44910b9ed5a 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -1,7 +1,6 @@ +import pytest from PIL import Image, ImageFilter -from .helper import PillowTestCase - sample = Image.new("L", (7, 5)) # fmt: off sample.putdata(sum([ @@ -14,234 +13,251 @@ # 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) - - -class TestBoxBlur(PillowTestCase): - def box_blur(self, 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 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 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_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_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_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_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_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_imageops_box_blur(): + i = sample.filter(ImageFilter.BoxBlur(1)) + assert i.mode == sample.mode + assert i.size == sample.size + assert isinstance(i, Image.Image) + + +def box_blur(image, radius=1, n=1): + return image._new(image.im.box_blur(radius, n)) + + +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 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_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(): + 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_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_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_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_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 ca82209c248..b34dbadb6bf 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,10 +1,9 @@ -from __future__ import division - from array import array +import pytest from PIL import Image, ImageFilter -from .helper import PillowTestCase, unittest +from .helper import assert_image_equal try: import numpy @@ -12,7 +11,7 @@ numpy = None -class TestColorLut3DCoreAPI(PillowTestCase): +class TestColorLut3DCoreAPI: def generate_identity_table(self, channels, size): if isinstance(size, tuple): size1D, size2D, size3D = size @@ -21,11 +20,11 @@ def generate_identity_table(self, channels, size): 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) @@ -42,43 +41,43 @@ def generate_identity_table(self, channels, size): 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): @@ -105,25 +104,25 @@ def test_correct_args(self): ) 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)) @@ -148,7 +147,7 @@ def test_identities(self): # 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( @@ -158,7 +157,7 @@ def test_identities(self): ) # Not so fast - self.assert_image_equal( + assert_image_equal( im, im._new( im.im.color_lut_3d( @@ -174,7 +173,7 @@ def test_identities_4_channels(self): ) # 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( @@ -195,7 +194,7 @@ def test_copy_alpha_channel(self): ], ) - self.assert_image_equal( + assert_image_equal( im, im._new( im.im.color_lut_3d( @@ -212,7 +211,7 @@ def test_channels_order(self): # 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, [ @@ -241,14 +240,14 @@ def test_overflow(self): -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, @@ -261,96 +260,96 @@ def test_overflow(self): -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( @@ -359,25 +358,25 @@ def test_numpy_formats(self): 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) @@ -387,7 +386,7 @@ def test_numpy_formats(self): 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), @@ -396,47 +395,48 @@ def test_repr(self): 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): @@ -446,23 +446,23 @@ def test_apply(self): 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): @@ -471,31 +471,29 @@ def test_target_mode(self): ) 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): @@ -505,13 +503,13 @@ def test_4_to_3_channels(self): 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): @@ -519,13 +517,13 @@ def test_4_to_4_channels(self): (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): @@ -535,13 +533,13 @@ def test_with_normals_3_channels(self): 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): @@ -552,11 +550,11 @@ def test_with_normals_4_channels(self): 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 eefb1a0efab..a8fe8bfebeb 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,41 +1,38 @@ -from __future__ import division, print_function - import sys +import pytest from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import is_pypy -is_pypy = hasattr(sys, "pypy_version_info") +def test_get_stats(): + # Create at least one image + Image.new("RGB", (10, 10)) -class TestCoreStats(PillowTestCase): - def test_get_stats(self): - # Create at least one image - Image.new("RGB", (10, 10)) + 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 - 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) +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(PillowTestCase): - def tearDown(self): +class TestCoreMemory: + def teardown_method(self): # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) @@ -45,38 +42,44 @@ def tearDown(self): 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() @@ -85,30 +88,32 @@ def test_set_block_size_stats(self): Image.new("RGB", (256, 256)) stats = Image.core.get_stats() - self.assertGreaterEqual(stats["new_count"], 1) - self.assertGreaterEqual(stats["allocated_blocks"], 64) - if not is_pypy: - self.assertGreaterEqual(stats["freed_blocks"], 64) + assert stats["new_count"] >= 1 + assert stats["allocated_blocks"] >= 64 + if not is_pypy(): + 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) @@ -117,13 +122,13 @@ def test_set_blocks_max_stats(self): 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() @@ -135,11 +140,11 @@ def test_clear_cache_stats(self): 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() @@ -149,16 +154,16 @@ def test_large_images(self): 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) - if not is_pypy: - self.assertGreaterEqual(stats["freed_blocks"], 16) + assert stats["new_count"] >= 1 + assert stats["allocated_blocks"] >= 16 + assert stats["reused_blocks"] >= 0 + assert stats["blocks_cached"] == 0 + if not is_pypy(): + 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) @@ -167,17 +172,17 @@ def tearDown(self): 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 7c18f85d245..1704400b489 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -1,69 +1,81 @@ +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): # Implicit assert: no warning. # A warning would cause a failure. - Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - Image.open(TEST_FILE) + with Image.open(TEST_FILE): + pass def test_no_warning_no_limit(self): # 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. # A warning would cause a failure. - Image.open(TEST_FILE) + with Image.open(TEST_FILE): + pass 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, Image.open, TEST_FILE) + 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 - self.assertRaises(Image.DecompressionBombError, lambda: Image.open(TEST_FILE)) + 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") -class TestDecompressionCrop(PillowTestCase): - def setUp(self): - self.src = hopper() - Image.MAX_IMAGE_PIXELS = self.src.height * self.src.width * 4 - 1 +class TestDecompressionCrop: + @classmethod + def setup_class(self): + width, height = 128, 128 + Image.MAX_IMAGE_PIXELS = height * width * 4 - 1 - def tearDown(self): + @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): @@ -76,11 +88,11 @@ def test_crop_decompression_checks(self): 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 64b0302caa5..7cfa08071ba 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,88 +1,102 @@ -from __future__ import unicode_literals - import io +import pytest from PIL import features -from .helper import PillowTestCase, unittest +from .helper import skip_unless_feature try: from PIL import _webp - - HAVE_WEBP = True except ImportError: - HAVE_WEBP = False - - -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)) - - @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) - - @unittest.skipUnless(HAVE_WEBP, "WebP not available") - def test_webp_anim(self): - self.assertEqual(features.check("webp_anim"), _webp.HAVE_WEBPANIM) - - 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]) - - 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) - - 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.assertEqual(lines[2], "-" * 68) - self.assertTrue(lines[3].startswith("Python modules loaded from ")) - self.assertTrue(lines[4].startswith("Binary modules loaded from ")) - self.assertEqual(lines[5], "-" * 68) - self.assertTrue(lines[6].startswith("Python ")) - jpeg = ( - "\n" - + "-" * 68 - + "\n" - + "JPEG image/jpeg\n" - + "Extensions: .jfif, .jpe, .jpeg, .jpg\n" - + "Features: open, save\n" - + "-" * 68 - + "\n" - ) - self.assertIn(jpeg, out) + pass + + +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) + + +@skip_unless_feature("webp") +def test_webp_transparency(): + assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() + assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY + + +@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 + + +def test_check_modules(): + for feature in features.modules: + assert features.check_module(feature) in [True, False] + 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) + + +def test_unsupported_module(): + # Arrange + module = "unsupported_module" + # Act / Assert + with pytest.raises(ValueError): + features.check_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 00000000000..deb043fdd4c --- /dev/null +++ b/Tests/test_file_apng.py @@ -0,0 +1,555 @@ +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_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("Tests/images/apng/{0}".format(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_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 59951a890a9..94c469c7f23 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): - im = Image.open("Tests/images/blp/blp2_raw.blp") - target = Image.open("Tests/images/blp/blp2_raw.png") - 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): - im = Image.open("Tests/images/blp/blp2_dxt1.blp") - target = Image.open("Tests/images/blp/blp2_dxt1.png") - 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 2180835ba52..8bb58794c00 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,124 +1,139 @@ 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() - im = hopper() + +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_dpi(): + dpi = (72, 72) + + output = io.BytesIO() + with hopper() as im: im.save(output, "BMP", dpi=dpi) - output.seek(0) - reloaded = Image.open(output) + output.seek(0) + with Image.open(output) as reloaded: + assert reloaded.info["dpi"] == dpi - self.assertEqual(reloaded.info["dpi"], dpi) - 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_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 - im = Image.open("Tests/images/hopper.bmp") - self.assertEqual(im.info["dpi"], (96, 96)) - # Round down - im = Image.open("Tests/images/hopper_roundDown.bmp") - 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)) - reloaded = Image.open(outfile) - 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)) - reloaded = Image.open(outfile) - self.assertEqual(reloaded.info["dpi"], (73, 73)) + 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") +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" - target = Image.open("Tests/images/clipboard_target.png") - self.assert_image_equal(im, target) + with Image.open("Tests/images/clipboard_target.png") as target: + assert_image_equal(im, target) - def test_save_dib(self): - outfile = self.tempfile("temp.dib") - 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 37573e3406c..ee6f3f2a4a2 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,42 +1,46 @@ +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 - im = Image.open(TEST_FILE) +def test_open(): + # Act + with Image.open(TEST_FILE) as im: # Assert - self.assertEqual(im.format, "BUFR") + assert im.format == "BUFR" # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + 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 - im = Image.open(TEST_FILE) + # Act / Assert + with pytest.raises(SyntaxError): + BufrStubImagePlugin.BufrStubImageFile(invalid_file) + + +def test_load(): + # Arrange + with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) + with pytest.raises(IOError): + im.load() + - def test_save(self): - # Arrange - im = hopper() - tmpfile = self.tempfile("temp.bufr") +def test_save(tmp_path): + # Arrange + im = hopper() + tmpfile = str(tmp_path / "temp.bufr") - # Act / Assert: stub cannot save without an implemented handler - self.assertRaises(IOError, im.save, tmpfile) + # Act / Assert: stub cannot save without an implemented handler + with pytest.raises(IOError): + im.save(tmpfile) diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 5f14001d9a5..b752e217faa 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): - im = hopper() + +def test_isatty(): + with hopper() as im: container = ContainerIO.ContainerIO(im, 0, 0) - self.assertFalse(container.isatty()) + assert container.isatty() is False - def test_seek_mode_0(self): - # Arrange - mode = 0 - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) - # Act - container.seek(33, mode) - container.seek(33, mode) +def test_seek_mode_0(): + # Arrange + mode = 0 + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - # Assert - self.assertEqual(container.tell(), 33) + # Act + container.seek(33, mode) + container.seek(33, mode) - def test_seek_mode_1(self): - # Arrange - mode = 1 - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) + # Assert + assert container.tell() == 33 - # Act - container.seek(33, mode) - container.seek(33, mode) - # Assert - self.assertEqual(container.tell(), 66) +def test_seek_mode_1(): + # Arrange + mode = 1 + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - def test_seek_mode_2(self): - # Arrange - mode = 2 - 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() == 66 - # Assert - self.assertEqual(container.tell(), 100) - 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 @@ def test_read_n0(self): 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 @@ def test_read_n(self): 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 @@ def test_read_eof(self): 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 @@ def test_readlines(self): "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 0b2f7a98ce3..3200fd8f63a 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -1,29 +1,29 @@ +import pytest from PIL import CurImagePlugin, Image -from .helper import PillowTestCase - 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" +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, CurImagePlugin.CurImageFile, invalid_file) + with pytest.raises(SyntaxError): + CurImagePlugin.CurImageFile(invalid_file) - no_cursors_file = "Tests/images/no_cursors.cur" + 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: - self.assertRaises(TypeError, cur._open) + 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 4d3690d30c5..bc76b4591d0 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,65 +1,92 @@ +import pytest from PIL import DcxImagePlugin, Image -from .helper import PillowTestCase, hopper +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 - im = Image.open(TEST_FILE) + # Act + with Image.open(TEST_FILE) as im: # Assert - self.assertEqual(im.size, (128, 128)) - self.assertIsInstance(im, DcxImagePlugin.DcxImageFile) + assert im.size == (128, 128) + assert isinstance(im, DcxImagePlugin.DcxImageFile) orig = hopper() - self.assert_image_equal(im, orig) + assert_image_equal(im, orig) - def test_unclosed_file(self): - def open(): - im = Image.open(TEST_FILE) - im.load() - self.assert_warning(None, open) +@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_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, DcxImagePlugin.DcxImageFile, fp) - def test_tell(self): - # Arrange +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 frame = im.tell() # Assert - self.assertEqual(frame, 0) + assert frame == 0 - def test_n_frames(self): - im = Image.open(TEST_FILE) - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) - def test_eoferror(self): - im = Image.open(TEST_FILE) +def test_n_frames(): + with Image.open(TEST_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated + + +def test_eoferror(): + 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) + 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_seek_too_far(self): - # Arrange - im = Image.open(TEST_FILE) + +def test_seek_too_far(): + # 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 498c64f2124..d157e15fd71 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,142 +1,161 @@ +"""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_UNCOMPRESSED_RGB = "Tests/images/uncompressed_rgb.dds" -class TestFileDds(PillowTestCase): - """Test DdsImagePlugin""" +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() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) + + assert_image_equal(im, target) - 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_dxt5(): + """Check DXT5 images can be opened""" + + 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.convert("RGBA"), im) + with Image.open(TEST_FILE_DXT5.replace(".dds", ".png")) as target: + assert_image_equal(target, im) - def test_sanity_dxt5(self): - """Check DXT5 images can be opened""" - target = Image.open(TEST_FILE_DXT5.replace(".dds", ".png")) +def test_sanity_dxt3(): + """Check DXT3 images can be opened""" - im = Image.open(TEST_FILE_DXT5) - im.load() + with Image.open(TEST_FILE_DXT3.replace(".dds", ".png")) as target: + with Image.open(TEST_FILE_DXT3) 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) + 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_dx10_bc7(): + """Check DX10 images can be opened""" - im = Image.open(TEST_FILE_DXT3) + with Image.open(TEST_FILE_DX10_BC7) 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_DX10_BC7.replace(".dds", ".png")) as target: + 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")) +def test_dx10_bc7_unorm_srgb(): + """Check DX10 unsigned normalized integer images can be opened""" - im = Image.open(TEST_FILE_DX10_BC7) + with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) 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 == (16, 16) + assert im.info["gamma"] == 1 / 2.2 - self.assert_image_equal(target, im) + with Image.open( + TEST_FILE_DX10_BC7_UNORM_SRGB.replace(".dds", ".png") + ) as target: + 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""" +def test_unimplemented_dxgi_format(): + with pytest.raises(NotImplementedError): + Image.open("Tests/images/unimplemented_dxgi_format.dds") - target = Image.open(TEST_FILE_UNCOMPRESSED_RGB.replace(".dds", ".png")) - im = Image.open(TEST_FILE_UNCOMPRESSED_RGB) +def test_uncompressed_rgb(): + """Check uncompressed RGB images can be opened""" + + with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: im.load() - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (800, 600)) + 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) + - self.assert_image_equal(target, im) +def test__validate_true(): + """Check valid prefix""" + # Arrange + prefix = b"DDS etc" - def test__validate_true(self): - """Check valid prefix""" - # Arrange - prefix = b"DDS etc" + # Act + output = DdsImagePlugin._validate(prefix) - # Act - output = DdsImagePlugin._validate(prefix) + # Assert + assert output - # Assert - self.assertTrue(output) - def test__validate_false(self): - """Check invalid prefix""" - # Arrange - prefix = b"something invalid" +def test__validate_false(): + """Check invalid prefix""" + # Arrange + prefix = b"something invalid" - # Act - output = DdsImagePlugin._validate(prefix) + # Act + output = DdsImagePlugin._validate(prefix) - # Assert - self.assertFalse(output) + # Assert + assert not 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])) +def test_short_header(): + """ Check a short header""" + with open(TEST_FILE_DXT5, "rb") as f: + img_file = f.read() - self.assertRaises(IOError, short_header) + def short_header(): + Image.open(BytesIO(img_file[:119])) - def test_short_file(self): - """ Check that the appropriate error is thrown for a short file""" + with pytest.raises(IOError): + short_header() - with open(TEST_FILE_DXT5, "rb") as f: - img_file = f.read() - def short_file(): - im = Image.open(BytesIO(img_file[:-100])) +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() - self.assertRaises(IOError, short_file) + with pytest.raises(IOError): + short_file() + - def test_unimplemented_pixel_format(self): - self.assertRaises( - NotImplementedError, - Image.open, - "Tests/images/unimplemented_pixel_format.dds", - ) +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 3459310dfad..504c09db1ab 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,254 +1,258 @@ import io -from PIL import EpsImagePlugin, Image +import pytest +from PIL import EpsImagePlugin, Image, features -from .helper import PillowTestCase, hopper, unittest +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 - image1 = Image.open(file1) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_sanity(): + # 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") + assert image1.mode == "RGB" + assert image1.size == (460, 352) + assert image1.format == "EPS" - image2 = Image.open(file2) + with Image.open(FILE2) as image2: image2.load() - self.assertEqual(image2.mode, "RGB") - self.assertEqual(image2.size, (360, 252)) - self.assertEqual(image2.format, "EPS") + assert image2.mode == "RGB" + assert image2.size == (360, 252) + assert image2.format == "EPS" - # Double scale - image1_scale2 = Image.open(file1) + # 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") + assert image1_scale2.mode == "RGB" + assert image1_scale2.size == (920, 704) + assert image1_scale2.format == "EPS" - image2_scale2 = Image.open(file2) + 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") + 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): - cmyk_image = Image.open("Tests/images/pil_sample_cmyk.eps") + 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") + +@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: + + assert cmyk_image.mode == "CMYK" + assert cmyk_image.size == (100, 100) + assert cmyk_image.format == "EPS" cmyk_image.load() - self.assertEqual(cmyk_image.mode, "RGB") - - if "jpeg_decoder" in dir(Image.core): - target = Image.open("Tests/images/pil_sample_rgb.jpg") - self.assert_image_similar(cmyk_image, target, 10) - - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_showpage(self): - # See https://github.com/python-pillow/Pillow/issues/2615 - plot_image = Image.open("Tests/images/reqd_showpage.eps") - target = Image.open("Tests/images/reqd_showpage.png") - - # should not crash/hang - plot_image.load() - # fonts could be slightly different - self.assert_image_similar(plot_image, target, 6) - - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_file_object(self): - # issue 479 - image1 = Image.open(file1) - with open(self.tempfile("temp_file.eps"), "wb") as fh: + assert cmyk_image.mode == "RGB" + + if features.check("jpg"): + with Image.open("Tests/images/pil_sample_rgb.jpg") as target: + assert_image_similar(cmyk_image, target, 10) + + +@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) + + +@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") - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_iobase_object(self): - # issue 479 - image1 = Image.open(file1) - with io.open(self.tempfile("temp_iobase.eps"), "wb") as fh: + +@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") - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_bytesio_object(self): - with open(file1, "rb") as f: - img_bytes = io.BytesIO(f.read()) - img = Image.open(img_bytes) +@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) - - 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") - - # Zero bounding box - image1_scale1 = Image.open(file1) + assert_image_similar(img, image1_scale1_compare, 5) + + +def test_image_mode_not_supported(tmp_path): + im = hopper("RGBA") + tmpfile = str(tmp_path / "temp.eps") + with pytest.raises(ValueError): + im.save(tmpfile) + + +@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 + + # Zero bounding box + with Image.open(FILE1) as image1_scale1: image1_scale1.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(image1_scale1, image1_scale1_compare, 5) + assert_image_similar(image1_scale1, image1_scale1_compare, 5) - # Non-Zero bounding box - image2_scale1 = Image.open(file2) + # Non-Zero bounding box + with Image.open(FILE2) as image2_scale1: image2_scale1.load() - image2_scale1_compare = Image.open(file2_compare).convert("RGB") + with Image.open(FILE2_COMPARE) as image2_scale1_compare: + image2_scale1_compare = image2_scale1_compare.convert("RGB") image2_scale1_compare.load() - self.assert_image_similar(image2_scale1, image2_scale1_compare, 10) + assert_image_similar(image2_scale1, image2_scale1_compare, 10) - @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 - image1_scale2 = Image.open(file1) +@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 + + # 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") + with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: + image1_scale2_compare = image1_scale2_compare.convert("RGB") image1_scale2_compare.load() - self.assert_image_similar(image1_scale2, image1_scale2_compare, 5) + assert_image_similar(image1_scale2, image1_scale2_compare, 5) - # Non-Zero bounding box - image2_scale2 = Image.open(file2) + # 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") + with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: + image2_scale2_compare = image2_scale2_compare.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): - # Arrange - image1 = Image.open(file1) - image2 = Image.open(file2) - image3 = Image.open("Tests/images/illu10_preview.eps") - new_size = (100, 100) - - # Act - image1 = image1.resize(new_size) - image2 = image2.resize(new_size) - image3 = image3.resize(new_size) - - # Assert - self.assertEqual(image1.size, new_size) - self.assertEqual(image2.size, new_size) - self.assertEqual(image3.size, new_size) - - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_thumbnail(self): - # Issue #619 - # Arrange - image1 = Image.open(file1) - image2 = Image.open(file2) - new_size = (100, 100) - - # Act - image1.thumbnail(new_size) - image2.thumbnail(new_size) - - # Assert - self.assertEqual(max(image1.size), max(new_size)) - self.assertEqual(max(image2.size), max(new_size)) - - def test_read_binary_preview(self): - # Issue 302 - # open image with binary preview - Image.open(file3) - - def _test_readline(self, t, ending): + assert_image_similar(image2_scale2, image2_scale2_compare, 10) + + +@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) - - 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) - 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", - ] - - # Act / Assert - for filename in FILES: - img = Image.open(filename) - self.assertEqual(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" - - image = Image.open(emptyline_file) + _test_readline(t, ending) + + for ending in line_endings: + s = ending.join(strings) + _test_readline_io_psfile(s, ending) + _test_readline_file_psfile(s, ending) + + +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: + assert img.mode == "RGB" + + +@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() - self.assertEqual(image.mode, "RGB") - self.assertEqual(image.size, (460, 352)) - self.assertEqual(image.format, "EPS") + 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 0221bab9934..f9f6c4ba9b4 100644 --- a/Tests/test_file_fitsstub.py +++ b/Tests/test_file_fitsstub.py @@ -1,46 +1,47 @@ +import pytest from PIL import FitsStubImagePlugin, Image -from .helper import PillowTestCase - TEST_FILE = "Tests/images/hopper.fits" -class TestFileFitsStub(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, "FITS") + assert im.format == "FITS" # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + assert im.mode == "F" + assert im.size == (1, 1) + - def test_invalid_file(self): - # Arrange - invalid_file = "Tests/images/flower.jpg" +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" - # Act / Assert - self.assertRaises( - SyntaxError, FitsStubImagePlugin.FITSStubImageFile, invalid_file - ) + # Act / Assert + with pytest.raises(SyntaxError): + FitsStubImagePlugin.FITSStubImageFile(invalid_file) - def test_load(self): - # Arrange - im = Image.open(TEST_FILE) + +def test_load(): + # Arrange + with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) + with pytest.raises(IOError): + im.load() + - def test_save(self): - # Arrange - im = Image.open(TEST_FILE) +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 - self.assertRaises(IOError, im.save, dummy_filename) - self.assertRaises( - IOError, FitsStubImagePlugin._save, im, dummy_fp, dummy_filename - ) + with pytest.raises(IOError): + im.save(dummy_filename) + with pytest.raises(IOError): + FitsStubImagePlugin._save(im, dummy_fp, dummy_filename) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index ad3e84a5bbe..726e16c1a16 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,6 +1,7 @@ +import pytest from PIL import FliImagePlugin, Image -from .helper import PillowTestCase +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. @@ -10,89 +11,115 @@ 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() - self.assertEqual(im.mode, "P") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "FLI") - self.assertFalse(im.is_animated) - - im = Image.open(animated_test_file) - 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) - - def test_unclosed_file(self): - def open(): - im = Image.open(static_test_file) - im.load() - self.assert_warning(None, open) + pytest.warns(ResourceWarning, open) + - def test_tell(self): - # Arrange +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() + + pytest.warns(None, open) + + +def test_tell(): + # Arrange + with Image.open(static_test_file) as im: # Act frame = im.tell() # Assert - self.assertEqual(frame, 0) + assert frame == 0 - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, FliImagePlugin.FliImageFile, invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + FliImagePlugin.FliImageFile(invalid_file) - def test_n_frames(self): - im = Image.open(static_test_file) - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) - im = Image.open(animated_test_file) - self.assertEqual(im.n_frames, 384) - self.assertTrue(im.is_animated) +def test_n_frames(): + with Image.open(static_test_file) as im: + assert im.n_frames == 1 + assert not im.is_animated - def test_eoferror(self): - im = Image.open(animated_test_file) + with Image.open(animated_test_file) as im: + assert im.n_frames == 384 + assert im.is_animated + + +def test_eoferror(): + with Image.open(animated_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) + 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_seek_tell(self): - im = Image.open(animated_test_file) + +def test_seek_tell(): + with Image.open(animated_test_file) as im: layer_number = im.tell() - self.assertEqual(layer_number, 0) + assert layer_number == 0 im.seek(0) layer_number = im.tell() - self.assertEqual(layer_number, 0) + assert layer_number == 0 im.seek(1) layer_number = im.tell() - self.assertEqual(layer_number, 1) + assert layer_number == 1 im.seek(2) layer_number = im.tell() - self.assertEqual(layer_number, 2) + assert layer_number == 2 im.seek(1) layer_number = im.tell() - self.assertEqual(layer_number, 1) + assert layer_number == 1 + - def test_seek(self): - im = Image.open(animated_test_file) +def test_seek(): + with Image.open(animated_test_file) as im: im.seek(50) - expected = Image.open("Tests/images/a_fli.png") - 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 68412c8caa0..ef8cdb5770b 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -1,20 +1,23 @@ -from .helper import PillowTestCase, unittest +import pytest +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(IOError, 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 7d30042ca0c..9b4375cd430 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 659179a4e12..f183390bccf 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -1,17 +1,17 @@ +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): - im = Image.open("Tests/images/gbr.gbr") - target = Image.open("Tests/images/gbr.png") - - 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) diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index 6467d1e9281..b6f8594bec7 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -1,20 +1,22 @@ -from PIL import GdImageFile - -from .helper import PillowTestCase +import pytest +from PIL import GdImageFile, UnidentifiedImageError TEST_GD_FILE = "Tests/images/hopper.gd" -class TestFileGd(PillowTestCase): - def test_sanity(self): - im = GdImageFile.open(TEST_GD_FILE) - 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(): + with pytest.raises(ValueError): + GdImageFile.open(TEST_GD_FILE, "bad mode") - 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_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - self.assertRaises(IOError, GdImageFile.open, invalid_file) + with pytest.raises(UnidentifiedImageError): + GdImageFile.open(invalid_file) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 4ff9727e1e9..455e30f71da 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,17 +1,15 @@ from io import BytesIO -from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette +import pytest +from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, features -from .helper import PillowTestCase, hopper, netpbm_available, unittest - -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" @@ -20,753 +18,811 @@ 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" + + +@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_sanity(self): + +def test_closed_file(): + def open(): im = Image.open(TEST_GIF) 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") - - def test_unclosed_file(self): - def open(): - im = Image.open(TEST_GIF) + im.close() + + pytest.warns(None, open) + + +def test_context_manager(): + def open(): + with Image.open(TEST_GIF) as im: im.load() - self.assert_warning(None, open) - - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - - self.assertRaises(SyntaxError, GifImagePlugin.GifImageFile, invalid_file) - - 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(bytearray(range(256 - colors, 256)) * colors), - ) - im = im.resize((size, size)) - outfile = BytesIO() - im.save(outfile, "GIF") - outfile.seek(0) - reloaded = Image.open(outfile) + pytest.warns(None, open) + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + GifImagePlugin.GifImageFile(invalid_file) + + +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_bilevel(optimize): + im = Image.new("1", (1, 1), 0) + test_file = BytesIO() + im.save(test_file, "GIF", optimize=optimize) + return len(test_file.getvalue()) + + assert test_grayscale(0) == 800 + assert test_grayscale(1) == 44 + assert test_bilevel(0) == 800 + assert test_bilevel(1) == 800 + + +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(bytearray(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) - reread = Image.open(out) +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") - im = Image.open(TEST_GIF) +def test_roundtrip(tmp_path): + out = str(tmp_path / "temp.gif") + im = hopper() + im.save(out) + with Image.open(out) as reread: + + assert_image_similar(reread.convert("RGB"), im, 50) + + +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) - reread = Image.open(out) + with Image.open(out) as reread: - self.assert_image_similar(reread.convert("RGB"), hopper(), 50) + assert_image_similar(reread.convert("RGB"), hopper(), 50) - def test_roundtrip_save_all(self): - # Single frame image - out = self.tempfile("temp.gif") - im = hopper() - im.save(out, save_all=True) - reread = Image.open(out) - self.assert_image_similar(reread.convert("RGB"), im, 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: - # Multiframe image - im = Image.open("Tests/images/dispose_bgnd.gif") + assert_image_similar(reread.convert("RGB"), im, 50) - out = self.tempfile("temp.gif") + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + out = str(tmp_path / "temp.gif") im.save(out, save_all=True) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 5) + with Image.open(out) as reread: + assert reread.n_frames == 5 + - def test_headers_saving_for_animated_gifs(self): - important_headers = ["background", "version", "duration", "loop"] - # Multiframe image - im = Image.open("Tests/images/dispose_bgnd.gif") +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: info = im.info.copy() - out = self.tempfile("temp.gif") + out = str(tmp_path / "temp.gif") im.save(out, save_all=True) - reread = Image.open(out) + with Image.open(out) as reread: for header in important_headers: - self.assertEqual(info[header], reread.info[header]) + assert info[header] == reread.info[header] - def test_palette_handling(self): - # see https://github.com/python-pillow/Pillow/issues/513 - im = Image.open(TEST_GIF) +def test_palette_handling(tmp_path): + # see https://github.com/python-pillow/Pillow/issues/513 + + 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) - f = self.tempfile("temp.gif") + f = str(tmp_path / "temp.gif") im2.save(f, optimize=True) - reloaded = Image.open(f) + with Image.open(f) as reloaded: - self.assert_image_similar(im, reloaded.convert("RGB"), 10) + assert_image_similar(im, reloaded.convert("RGB"), 10) - def test_palette_434(self): - # 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 test_palette_434(tmp_path): + # see https://github.com/python-pillow/Pillow/issues/434 + + 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" - im = Image.open(orig) + orig = "Tests/images/test.colors.gif" + with Image.open(orig) as im: - self.assert_image_similar(im, roundtrip(im), 1) - self.assert_image_similar(im, roundtrip(im, optimize=True), 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 - reloaded = roundtrip(im).convert("RGB") - self.assert_image_equal(im, reloaded) + 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): - img = Image.open(TEST_GIF).convert("RGB") - tempfile = self.tempfile("temp.gif") +@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") + + tempfile = str(tmp_path / "temp.gif") GifImagePlugin._save_netpbm(img, 0, tempfile) - self.assert_image_similar(img, Image.open(tempfile).convert("RGB"), 0) + with Image.open(tempfile) as reloaded: + assert_image_similar(img, reloaded.convert("RGB"), 0) + - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_l_mode(self): - img = Image.open(TEST_GIF).convert("L") +@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") - tempfile = self.tempfile("temp.gif") + tempfile = str(tmp_path / "temp.gif") GifImagePlugin._save_netpbm(img, 0, tempfile) - self.assert_image_similar(img, Image.open(tempfile).convert("L"), 0) + with Image.open(tempfile) as reloaded: + assert_image_similar(img, reloaded.convert("L"), 0) + - def test_seek(self): - img = Image.open("Tests/images/dispose_none.gif") - framecount = 0 +def test_seek(): + with Image.open("Tests/images/dispose_none.gif") as img: + frame_count = 0 try: while True: - framecount += 1 + frame_count += 1 img.seek(img.tell() + 1) except EOFError: - self.assertEqual(framecount, 5) + assert frame_count == 5 - def test_seek_info(self): - im = Image.open("Tests/images/iss634.gif") + +def test_seek_info(): + with Image.open("Tests/images/iss634.gif") as im: info = im.info.copy() im.seek(1) im.seek(0) - self.assertEqual(im.info, info) + assert im.info == info + - def test_seek_rewind(self): - im = Image.open("Tests/images/iss634.gif") +def test_seek_rewind(): + with Image.open("Tests/images/iss634.gif") as im: im.seek(2) im.seek(1) - expected = Image.open("Tests/images/iss634.gif") - expected.seek(1) - self.assert_image_equal(im, expected) + with Image.open("Tests/images/iss634.gif") as expected: + expected.seek(1) + 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 - im = Image.open(path) - self.assertEqual(im.is_animated, n_frames != 1) - # Test is_animated after n_frames - im = Image.open(path) - self.assertEqual(im.n_frames, n_frames) - self.assertEqual(im.is_animated, n_frames != 1) +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) - def test_eoferror(self): - im = Image.open(TEST_GIF) + # 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 - 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) - def test_dispose_none(self): - img = Image.open("Tests/images/dispose_none.gif") + +def test_dispose_none(): + with Image.open("Tests/images/dispose_none.gif") as img: try: while True: img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, 1) + assert img.disposal_method == 1 except EOFError: pass - def test_dispose_background(self): - img = Image.open("Tests/images/dispose_bgnd.gif") + +def test_dispose_background(): + with Image.open("Tests/images/dispose_bgnd.gif") as img: try: while True: img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, 2) + assert img.disposal_method == 2 except EOFError: pass - def test_dispose_previous(self): - img = Image.open("Tests/images/dispose_prev.gif") + +def test_dispose_previous(): + with Image.open("Tests/images/dispose_prev.gif") as img: try: while True: img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, 3) + assert 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 - ) - img = Image.open(out) + +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 _ in range(2): img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, method) + assert 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))), - ) + # Check per frame disposal + im_list[0].save( + out, + save_all=True, + append_images=im_list[1:], + disposal=tuple(range(len(im_list))), + ) - img = Image.open(out) + with Image.open(out) as img: for i in range(2): img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, i + 1) + assert img.disposal_method == i + 1 - 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_dispose2_palette(tmp_path): + out = str(tmp_path / "temp.gif") - im_list = [] - for circle in circles: - img = Image.new("RGB", (100, 100), (255, 0, 0)) + # 4 backgrounds: White, Grey, Black, Red + circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] - # Red circle in center of each frame - d = ImageDraw.Draw(img) - d.ellipse([(40, 40), (60, 60)], fill=circle) + im_list = [] + for circle in circles: + img = Image.new("RGB", (100, 100), (255, 0, 0)) - im_list.append(img) + # Red circle in center of each frame + d = ImageDraw.Draw(img) + d.ellipse([(40, 40), (60, 60)], fill=circle) - im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) + im_list.append(img) - img = Image.open(out) + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) + with Image.open(out) as img: for i, circle in enumerate(circles): img.seek(i) rgb_img = img.convert("RGB") # Check top left pixel matches background - self.assertEqual(rgb_img.getpixel((0, 0)), (255, 0, 0)) + assert rgb_img.getpixel((0, 0)) == (255, 0, 0) # Center remains red every frame - self.assertEqual(rgb_img.getpixel((50, 50)), circle) + assert rgb_img.getpixel((50, 50)) == circle - 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)), - ] +def test_dispose2_diff(tmp_path): + out = str(tmp_path / "temp.gif") - im_list = [] - for i in range(len(circles)): - # Transparent BG - img = Image.new("RGBA", (100, 100), (255, 255, 255, 0)) + # 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)), + ] - # 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 = [] + for i in range(len(circles)): + # Transparent BG + img = Image.new("RGBA", (100, 100), (255, 255, 255, 0)) - im_list.append(img) + # 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[0].save( - out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0 - ) + im_list.append(img) - img = Image.open(out) + 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]) + assert rgb_img.getpixel((20, 50)) == colours[0] # Check right circle is correct colour - self.assertEqual(rgb_img.getpixel((80, 50)), colours[1]) + assert rgb_img.getpixel((80, 50)) == colours[1] # Check BG is correct colour - self.assertEqual(rgb_img.getpixel((1, 1)), (255, 255, 255, 0)) + assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0) - def test_dispose2_background(self): - out = self.tempfile("temp.gif") - im_list = [] +def test_dispose2_background(tmp_path): + out = str(tmp_path / "temp.gif") - 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_list = [] - 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 = 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_list[0].save( - out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1 - ) + 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 + ) - im = Image.open(out) + with Image.open(out) as im: im.seek(1) - self.assertEqual(im.getpixel((0, 0)), 0) + assert im.getpixel((0, 0)) == 0 - def test_iss634(self): - img = Image.open("Tests/images/iss634.gif") - # seek to the second frame + +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 - self.assertEqual(img.histogram()[img.info["transparency"]], 0) + # All transparent pixels should be replaced with the color from the first frame + assert img.histogram()[img.info["transparency"]] == 0 - def test_duration(self): - duration = 1000 - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") +def test_duration(tmp_path): + duration = 1000 - # Check that the argument has priority over the info settings - im.info["duration"] = 100 - im.save(out, duration=duration) + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") - reread = Image.open(out) - self.assertEqual(reread.info["duration"], duration) + # Check that the argument has priority over the info settings + im.info["duration"] = 100 + im.save(out, duration=duration) - def test_multiple_duration(self): - duration_list = [1000, 2000, 3000] + with Image.open(out) as reread: + assert reread.info["duration"] == duration - 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"), - ] - # duration as list - im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=duration_list - ) - reread = Image.open(out) +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: - self.assertEqual(reread.info["duration"], duration) + 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) - ) - reread = Image.open(out) + # 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: - self.assertEqual(reread.info["duration"], duration) + assert reread.info["duration"] == duration try: reread.seek(reread.tell() + 1) except EOFError: pass - def test_identical_frames(self): - duration_list = [1000, 1500, 2000, 4000] - out = self.tempfile("temp.gif") +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 ) - reread = Image.open(out) - - # Assert that the first three frames were combined - self.assertEqual(reread.n_frames, 2) - - # Assert that the new duration is the total of the identical frames - self.assertEqual(reread.info["duration"], 4500) - - 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 - ) - reread = Image.open(out) - + with Image.open(out) as reread: # Assert that all frames were combined - self.assertEqual(reread.n_frames, 1) + assert reread.n_frames == 1 # Assert that the new duration is the total of the identical frames - self.assertEqual(reread.info["duration"], 8500) + assert reread.info["duration"] == 8500 - def test_number_of_loops(self): - number_of_loops = 2 - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") - im.save(out, loop=number_of_loops) - reread = Image.open(out) +def test_number_of_loops(tmp_path): + number_of_loops = 2 - self.assertEqual(reread.info["loop"], number_of_loops) + 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: + + assert reread.info["loop"] == number_of_loops - def test_background(self): - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") - im.info["background"] = 1 - im.save(out) - reread = Image.open(out) - self.assertEqual(reread.info["background"], im.info["background"]) +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: - if HAVE_WEBP and _webp.HAVE_WEBPANIM: - im = Image.open("Tests/images/hopper.webp") - self.assertIsInstance(im.info["background"], tuple) + 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(self): - im = Image.open(TEST_GIF) - self.assertEqual(im.info["comment"], b"File written by Adobe Photoshop\xa8 4.0") - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") - im.info["comment"] = b"Test comment text" - im.save(out) - reread = Image.open(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" - self.assertEqual(reread.info["comment"], im.info["comment"]) + 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"] - 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) - reread = Image.open(out) + im.info["comment"] = "Test comment text" + im.save(out) + with Image.open(out) as reread: + assert reread.info["comment"] == im.info["comment"].encode() - self.assertEqual(reread.info["comment"], comment) - def test_zero_comment_subblocks(self): - im = Image.open("Tests/images/hopper_zero_comment_subblocks.gif") - expected = Image.open(TEST_GIF) - self.assert_image_equal(im, expected) +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: - def test_version(self): - out = self.tempfile("temp.gif") + assert reread.info["comment"] == comment - def assertVersionAfterSave(im, version): - im.save(out) - reread = Image.open(out) - self.assertEqual(reread.info["version"], version) - # Test that GIF87a is used by default - im = Image.new("L", (100, 100), "#000") - assertVersionAfterSave(im, b"GIF87a") +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 - # Test setting the version to 89a - im = Image.new("L", (100, 100), "#000") - im.info["version"] = b"89a" - assertVersionAfterSave(im, b"GIF89a") + # Test that GIF87a is used by default + im = Image.new("L", (100, 100), "#000") + assertVersionAfterSave(im, b"GIF87a") - # Test that adding a GIF89a feature changes the version - im.info["transparency"] = 1 - assertVersionAfterSave(im, b"GIF89a") + # 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 GIF87a image is also saved in that format - im = Image.open("Tests/images/test.colors.gif") + # 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") # Test that a GIF89a image is also saved in that format im.info["version"] = b"GIF89a" assertVersionAfterSave(im, b"GIF87a") - def test_append_images(self): - out = self.tempfile("temp.gif") - # 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(tmp_path): + out = str(tmp_path / "temp.gif") - reread = Image.open(out) - self.assertEqual(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): - for im in ims: - yield im + with Image.open(out) as reread: + assert 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 - reread = Image.open(out) - self.assertEqual(reread.n_frames, 3) + im.save(out, save_all=True, append_images=imGenerator(ims)) - # Tests appending single and multiple frame images - im = Image.open("Tests/images/dispose_none.gif") - ims = [Image.open("Tests/images/dispose_prev.gif")] - im.save(out, save_all=True, append_images=ims) + with Image.open(out) as reread: + assert reread.n_frames == 3 - reread = Image.open(out) - self.assertEqual(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]) - 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. + with Image.open(out) as reread: + assert reread.n_frames == 10 - data = bytes(bytearray(range(1, 254))) - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - im = Image.new("L", (253, 1)) - im.frombytes(data) - im.putpalette(palette) +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. - out = self.tempfile("temp.gif") - im.save(out, transparency=253) - reloaded = Image.open(out) + data = bytes(range(1, 254)) + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - self.assertEqual(reloaded.info["transparency"], 253) + im = Image.new("L", (253, 1)) + im.frombytes(data) + im.putpalette(palette) - def test_rgb_transparency(self): - out = self.tempfile("temp.gif") + out = str(tmp_path / "temp.gif") + im.save(out, transparency=253) + with Image.open(out) as reloaded: - # Single frame - im = Image.new("RGB", (1, 1)) - im.info["transparency"] = (255, 0, 0) - self.assert_warning(UserWarning, im.save, out) + assert reloaded.info["transparency"] == 253 - reloaded = Image.open(out) - self.assertNotIn("transparency", reloaded.info) - # 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) +def test_rgb_transparency(tmp_path): + out = str(tmp_path / "temp.gif") - reloaded = Image.open(out) - self.assertNotIn("transparency", reloaded.info) + # Single frame + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = (255, 0, 0) + pytest.warns(UserWarning, im.save, out) - def test_bbox(self): - out = self.tempfile("temp.gif") + with Image.open(out) as reloaded: + assert "transparency" not in reloaded.info - im = Image.new("RGB", (100, 100), "#fff") - ims = [Image.new("RGB", (100, 100), "#000")] - im.save(out, save_all=True, append_images=ims) + # 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) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 2) + with Image.open(out) as reloaded: + assert "transparency" not in reloaded.info - 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(bytearray(im.getpalette())) +def test_bbox(tmp_path): + out = str(tmp_path / "temp.gif") - out = self.tempfile("temp.gif") - im_l.save(out, palette=palette) + im = Image.new("RGB", (100, 100), "#fff") + ims = [Image.new("RGB", (100, 100), "#000")] + im.save(out, save_all=True, append_images=ims) - reloaded = Image.open(out) + with Image.open(out) as reread: + assert reread.n_frames == 2 - self.assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) - 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. +def test_palette_save_L(tmp_path): + # Generate an L mode image with a separate palette - im = hopper("P") - palette = bytes(bytearray([255 - i // 3 for i in range(768)])) + im = hopper("P") + im_l = Image.frombytes("L", im.size, im.tobytes()) + palette = bytes(im.getpalette()) - out = self.tempfile("temp.gif") - im.save(out, palette=palette) + out = str(tmp_path / "temp.gif") + im_l.save(out, palette=palette) - reloaded = Image.open(out) + 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) - self.assert_image_equal(reloaded, im) + assert_image_equal(reloaded, im) - 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) +def test_palette_save_ImagePalette(tmp_path): + # Pass in a different palette, as an ImagePalette.ImagePalette + # effectively the same as test_palette_save_P - out = self.tempfile("temp.gif") - im.save(out, palette=palette) + im = hopper("P") + palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) - reloaded = Image.open(out) + out = str(tmp_path / "temp.gif") + im.save(out, palette=palette) + + with Image.open(out) as reloaded: im.putpalette(palette) - self.assert_image_equal(reloaded, im) + assert_image_equal(reloaded, im) - def test_save_I(self): - # Test saving something that would trigger the auto-convert to 'L' - im = hopper("I") +def test_save_I(tmp_path): + # Test saving something that would trigger the auto-convert to 'L' - out = self.tempfile("temp.gif") - im.save(out) + im = hopper("I") - reloaded = Image.open(out) - self.assert_image_equal(reloaded.convert("L"), im.convert("L")) + out = str(tmp_path / "temp.gif") + im.save(out) - 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} + with Image.open(out) as reloaded: + assert_image_equal(reloaded.convert("L"), im.convert("L")) - passed_palette = bytes(bytearray([255 - i // 3 for i in range(768)])) - GifImagePlugin._FORCE_OPTIMIZE = True - try: - h = GifImagePlugin.getheader(im, passed_palette) - d = GifImagePlugin.getdata(im) +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} - import pickle + passed_palette = bytes([255 - i // 3 for i in range(768)]) - # 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) + GifImagePlugin._FORCE_OPTIMIZE = True + try: + h = GifImagePlugin.getheader(im, passed_palette) + d = GifImagePlugin.getdata(im) - self.assertEqual(h, h_target) - self.assertEqual(d, d_target) - finally: - GifImagePlugin._FORCE_OPTIMIZE = False + import pickle - def test_lzw_bits(self): - # see https://github.com/python-pillow/Pillow/issues/2811 - im = Image.open("Tests/images/issue_2811.gif") + # 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(im.tile[0][3][0], 11) # LZW bits + assert h == h_target + assert d == d_target + finally: + GifImagePlugin._FORCE_OPTIMIZE = False + + +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_extents(self): - im = Image.open("Tests/images/test_extents.gif") - self.assertEqual(im.size, (100, 100)) + +def test_extents(): + with Image.open("Tests/images/test_extents.gif") as im: + assert im.size == (100, 100) im.seek(1) - self.assertEqual(im.size, (150, 150)) + assert im.size == (150, 150) diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index bafee79a3e5..3f056fdae1d 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -1,122 +1,124 @@ -from PIL import GimpGradientFile +from PIL import GimpGradientFile, ImagePalette -from .helper import PillowTestCase +def test_linear_pos_le_middle(): + # Arrange + middle = 0.5 + pos = 0.25 -class TestImage(PillowTestCase): - def test_linear_pos_le_middle(self): - # Arrange - middle = 0.5 - pos = 0.25 + # Act + ret = GimpGradientFile.linear(middle, pos) - # Act - ret = GimpGradientFile.linear(middle, pos) + # Assert + assert ret == 0.25 - # Assert - self.assertEqual(ret, 0.25) - def test_linear_pos_le_small_middle(self): - # Arrange - middle = 1e-11 - pos = 1e-12 +def test_linear_pos_le_small_middle(): + # Arrange + middle = 1e-11 + pos = 1e-12 - # Act - ret = GimpGradientFile.linear(middle, pos) + # Act + ret = GimpGradientFile.linear(middle, pos) - # Assert - self.assertEqual(ret, 0.0) + # Assert + assert ret == 0.0 - def test_linear_pos_gt_middle(self): - # Arrange - middle = 0.5 - pos = 0.75 - # Act - ret = GimpGradientFile.linear(middle, pos) +def test_linear_pos_gt_middle(): + # Arrange + middle = 0.5 + pos = 0.75 - # Assert - self.assertEqual(ret, 0.75) + # Act + ret = GimpGradientFile.linear(middle, pos) - def test_linear_pos_gt_small_middle(self): - # Arrange - middle = 1 - 1e-11 - pos = 1 - 1e-12 + # Assert + assert ret == 0.75 - # Act - ret = GimpGradientFile.linear(middle, pos) - # Assert - self.assertEqual(ret, 1.0) +def test_linear_pos_gt_small_middle(): + # Arrange + middle = 1 - 1e-11 + pos = 1 - 1e-12 - def test_curved(self): - # Arrange - middle = 0.5 - pos = 0.75 + # Act + ret = GimpGradientFile.linear(middle, pos) - # Act - ret = GimpGradientFile.curved(middle, pos) + # Assert + assert ret == 1.0 - # Assert - self.assertEqual(ret, 0.75) - def test_sine(self): - # Arrange - middle = 0.5 - pos = 0.75 +def test_curved(): + # Arrange + middle = 0.5 + pos = 0.75 - # Act - ret = GimpGradientFile.sine(middle, pos) + # Act + ret = GimpGradientFile.curved(middle, pos) - # Assert - self.assertEqual(ret, 0.8535533905932737) + # Assert + assert ret == 0.75 - def test_sphere_increasing(self): - # Arrange - middle = 0.5 - pos = 0.75 - # Act - ret = GimpGradientFile.sphere_increasing(middle, pos) +def test_sine(): + # Arrange + middle = 0.5 + pos = 0.75 - # Assert - self.assertAlmostEqual(ret, 0.9682458365518543) + # Act + ret = GimpGradientFile.sine(middle, pos) - def test_sphere_decreasing(self): - # Arrange - middle = 0.5 - pos = 0.75 + # Assert + assert ret == 0.8535533905932737 - # Act - ret = GimpGradientFile.sphere_decreasing(middle, pos) - # Assert - self.assertEqual(ret, 0.3385621722338523) +def test_sphere_increasing(): + # Arrange + middle = 0.5 + pos = 0.75 - def test_load_via_imagepalette(self): - # Arrange - from PIL import ImagePalette + # Act + ret = GimpGradientFile.sphere_increasing(middle, pos) - test_file = "Tests/images/gimp_gradient.ggr" + # Assert + assert round(abs(ret - 0.9682458365518543), 7) == 0 - # Act - palette = ImagePalette.load(test_file) - # Assert - # load returns raw palette information - self.assertEqual(len(palette[0]), 1024) - self.assertEqual(palette[1], "RGBA") +def test_sphere_decreasing(): + # Arrange + middle = 0.5 + pos = 0.75 - def test_load_1_3_via_imagepalette(self): - # Arrange - from PIL import ImagePalette + # Act + ret = GimpGradientFile.sphere_decreasing(middle, pos) - # GIMP 1.3 gradient files contain a name field - test_file = "Tests/images/gimp_gradient_with_name.ggr" + # Assert + assert ret == 0.3385621722338523 - # Act - palette = ImagePalette.load(test_file) - # 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 a1677f0cb17..a38c6320c8d 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,29 +1,31 @@ +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 d322e1c70fa..1cc1f47ac9e 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,42 +1,46 @@ +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 - im = Image.open(TEST_FILE) +def test_open(): + # Act + with Image.open(TEST_FILE) as im: # Assert - self.assertEqual(im.format, "GRIB") + assert im.format == "GRIB" # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + 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 - im = Image.open(TEST_FILE) + # Act / Assert + with pytest.raises(SyntaxError): + GribStubImagePlugin.GribStubImageFile(invalid_file) + + +def test_load(): + # Arrange + with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) + with pytest.raises(IOError): + im.load() + - def test_save(self): - # Arrange - im = hopper() - tmpfile = self.tempfile("temp.grib") +def test_save(tmp_path): + # Arrange + im = hopper() + tmpfile = str(tmp_path / "temp.grib") - # Act / Assert: stub cannot save without an implemented handler - self.assertRaises(IOError, im.save, tmpfile) + # Act / Assert: stub cannot save without an implemented handler + with pytest.raises(IOError): + im.save(tmpfile) diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index c300bae2066..526fd7c99ce 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,46 +1,47 @@ +import pytest from PIL import Hdf5StubImagePlugin, Image -from .helper import PillowTestCase - TEST_FILE = "Tests/images/hdf5.h5" -class TestFileHdf5Stub(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, "HDF5") + assert im.format == "HDF5" # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + assert im.mode == "F" + assert im.size == (1, 1) + - def test_invalid_file(self): - # Arrange - invalid_file = "Tests/images/flower.jpg" +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" - # Act / Assert - self.assertRaises( - SyntaxError, Hdf5StubImagePlugin.HDF5StubImageFile, invalid_file - ) + # Act / Assert + with pytest.raises(SyntaxError): + Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file) - def test_load(self): - # Arrange - im = Image.open(TEST_FILE) + +def test_load(): + # Arrange + with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) + with pytest.raises(IOError): + im.load() + - def test_save(self): - # Arrange - im = Image.open(TEST_FILE) +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 - self.assertRaises(IOError, im.save, dummy_filename) - self.assertRaises( - IOError, Hdf5StubImagePlugin._save, im, dummy_fp, dummy_filename - ) + with pytest.raises(IOError): + im.save(dummy_filename) + with pytest.raises(IOError): + Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 2e33e0ae52c..aeb146f7ec2 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,120 +1,127 @@ import io import sys +import pytest from PIL import IcnsImagePlugin, Image -from .helper import PillowTestCase, unittest +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 = hasattr(Image.core, "jp2klib_version") -class TestFileIcns(PillowTestCase): - def test_sanity(self): - # Loading this icon by default should result in the largest size - # (512x512@2x) being loaded - im = Image.open(TEST_FILE) +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) + 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) + + with Image.open(temp_file) as reread: + reread.size = (16, 16, 2) + reread.load() + assert_image_equal(reread, provided_im) - reread = Image.open(temp_file) - reread.size = (16, 16, 2) - reread.load() - self.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 - im = Image.open(TEST_FILE) +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() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (wr, hr)) + assert im.mode == "RGBA" + assert im.size == (wr, hr) # Check that we cannot load an incorrect size - with self.assertRaises(ValueError): + with pytest.raises(ValueError): im.size = (1, 1) - 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). - im = Image.open("Tests/images/pillow2.icns") + +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 - im2 = Image.open("Tests/images/pillow2.icns") - im2.size = (w, h, r) - im2.load() - self.assertEqual(im2.mode, "RGBA") - self.assertEqual(im2.size, (wr, hr)) + 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) - 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. - # (oldiconutil is here: https://github.com/uliwitness/oldiconutil) +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. - if not enable_jpeg2k: - return + # (oldiconutil is here: https://github.com/uliwitness/oldiconutil) - im = Image.open("Tests/images/pillow3.icns") + 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 - im2 = Image.open("Tests/images/pillow3.icns") - im2.size = (w, h, r) - im2.load() - self.assertEqual(im2.mode, "RGBA") - self.assertEqual(im2.size, (wr, hr)) - - 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)) - - 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) + 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 8a01e417f38..9ed1ffcb70e 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,103 +1,109 @@ 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): - im = Image.open(TEST_ICO_FILE) +def test_sanity(): + 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_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)]) - - # 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)) - - # the other one - output.seek(0) - reloaded = Image.open(output) + 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(): + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + IcoImagePlugin.IcoImageFile(fp) + + +def test_save_to_bytes(): + output = io.BytesIO() + im = hopper() + im.save(output, "ico", sizes=[(32, 32), (64, 64)]) + + # 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): - im = Image.open(TEST_ICO_FILE) - with self.assertRaises(ValueError): + +def test_incorrect_size(): + with Image.open(TEST_ICO_FILE) as im: + with pytest.raises(ValueError): im.size = (1, 1) - def test_save_256x256(self): - """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" - # Arrange - im = Image.open("Tests/images/hopper_256x256.ico") - outfile = self.tempfile("temp_saved_hopper_256x256.ico") + +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") # Act im.save(outfile) - im_saved = Image.open(outfile) + with Image.open(outfile) as im_saved: # Assert - self.assertEqual(im_saved.size, (256, 256)) + assert im_saved.size == (256, 256) - 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 - im = Image.open("Tests/images/python.ico") # 16x16, 32x32, 48x48 - outfile = self.tempfile("temp_saved_python.ico") +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) - im_saved = Image.open(outfile) + with Image.open(outfile) as im_saved: # Assert - self.assertEqual( - im_saved.info["sizes"], {(16, 16), (24, 24), (32, 32), (48, 48)} - ) - - def test_unexpected_size(self): - # This image has been manually hexedited to state that it is 16x32 - # while the image within is still 16x16 - im = self.assert_warning( - UserWarning, Image.open, "Tests/images/hopper_unexpected.ico" - ) - self.assertEqual(im.size, (16, 16)) - - def test_draw_reloaded(self): - im = Image.open(TEST_ICO_FILE) - outfile = self.tempfile("temp_saved_hopper_draw.ico") + assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} + + +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) - im = Image.open(outfile) + with Image.open(outfile) as im: im.save("Tests/images/hopper_draw.ico") - reloaded = Image.open("Tests/images/hopper_draw.ico") - self.assert_image_equal(im, reloaded) + 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 90e26efd519..30a9fd52a07 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,70 +1,110 @@ +import filecmp + +import pytest from PIL import Image, ImImagePlugin -from .helper import PillowTestCase, hopper +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() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "IM") - def test_unclosed_file(self): - def open(): - im = Image.open(TEST_IM) - im.load() + pytest.warns(ResourceWarning, open) - self.assert_warning(None, open) - def test_tell(self): - # Arrange +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() + + pytest.warns(None, open) + + +def test_tell(): + # Arrange + with Image.open(TEST_IM) as im: # Act frame = im.tell() - # Assert - self.assertEqual(frame, 0) + # Assert + assert frame == 0 - def test_n_frames(self): - im = Image.open(TEST_IM) - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) - def test_eoferror(self): - im = Image.open(TEST_IM) +def test_n_frames(): + with Image.open(TEST_IM) as im: + assert im.n_frames == 1 + assert not im.is_animated + + +def test_eoferror(): + with Image.open(TEST_IM) 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) - def test_roundtrip(self): - for mode in ["RGB", "P", "PA"]: - out = self.tempfile("temp.im") - im = hopper(mode) - im.save(out) - reread = Image.open(out) - self.assert_image_equal(reread, im) +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) + + for mode in ["RGB", "P", "PA"]: + roundtrip(mode) + + +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_save_unsupported_mode(self): - out = self.tempfile("temp.im") - im = hopper("HSV") - self.assertRaises(ValueError, im.save, out) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + ImImagePlugin.ImImageFile(invalid_file) - self.assertRaises(SyntaxError, ImImagePlugin.ImImageFile, invalid_file) - def test_number(self): - self.assertEqual(1.2, ImImagePlugin.number("1.2")) +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 800563af1bb..2d0e6977a70 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,71 +1,71 @@ +import sys +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 - im = hopper() +def test_getiptcinfo_jpg_none(): + # Arrange + with hopper() as im: # Act iptc = IptcImagePlugin.getiptcinfo(im) - # Assert - self.assertIsNone(iptc) + # Assert + assert iptc is None + - def test_getiptcinfo_jpg_found(self): - # Arrange - im = Image.open(TEST_FILE) +def test_getiptcinfo_jpg_found(): + # 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") + # Assert + assert isinstance(iptc, dict) + assert iptc[(2, 90)] == b"Budapest" + assert iptc[(2, 101)] == b"Hungary" + - def test_getiptcinfo_tiff_none(self): - # Arrange - im = Image.open("Tests/images/hopper.tif") +def test_getiptcinfo_tiff_none(): + # Arrange + with Image.open("Tests/images/hopper.tif") as im: # Act iptc = IptcImagePlugin.getiptcinfo(im) - # Assert - self.assertIsNone(iptc) + # Assert + assert iptc is None - def test_i(self): - # Arrange - c = b"a" - # Act - ret = IptcImagePlugin.i(c) +def test_i(): + # Arrange + c = b"a" - # Assert - self.assertEqual(ret, 97) + # Act + ret = IptcImagePlugin.i(c) - def test_dump(self): - # Arrange - c = b"abc" - # Temporarily redirect stdout - try: - from cStringIO import StringIO - except ImportError: - from io import StringIO - import sys + # Assert + assert ret == 97 - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() - # Act - IptcImagePlugin.dump(c) +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 + # Reset stdout + sys.stdout = old_stdout - # Assert - self.assertEqual(mystdout.getvalue(), "61 62 63 \n") + # Assert + assert mystdout.getvalue() == "61 62 63 \n" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 7f9bf7c1d6d..33045122891 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,21 +1,26 @@ import os -import sys +import re from io import BytesIO -from PIL import Image, ImageFile, JpegImagePlugin +import pytest +from PIL import ExifTags, Image, ImageFile, JpegImagePlugin -from .helper import PillowTestCase, cjpeg_available, djpeg_available, hopper, unittest - -codecs = dir(Image.core) +from .helper import ( + assert_image, + assert_image_equal, + assert_image_similar, + cjpeg_available, + djpeg_available, + hopper, + is_win32, + skip_unless_feature, +) 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) @@ -36,77 +41,82 @@ def gen_random_image(self, size, mode="RGB"): def test_sanity(self): # internal version number - self.assertRegex(Image.core.jpeglib_version, r"\d+\.\d+$") + assert re.search(r"\d+\.\d+$", Image.core.jpeglib_version) - 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) - im = Image.open(TEST_FILE) - self.assertEqual( - im.applist[0], ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") - ) - self.assertEqual( - im.applist[1], ("COM", b"File written by Adobe Photoshop\xa8 4.0\x00") - ) - self.assertEqual(len(im.applist), 2) + with Image.open(TEST_FILE) as im: + 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", + ) + 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): def test(xdpi, ydpi=None): - im = Image.open(TEST_FILE) - im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) + with Image.open(TEST_FILE) 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 - im1 = Image.open("Tests/images/rgb.jpg") - icc_profile = im1.info["icc_profile"] - self.assertEqual(len(icc_profile), 3144) - # Roundtrip via physical file. - f = self.tempfile("temp.jpg") - im1.save(f, icc_profile=icc_profile) - im2 = Image.open(f) - self.assertEqual(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")) + with Image.open("Tests/images/rgb.jpg") as im1: + icc_profile = im1.info["icc_profile"] + assert len(icc_profile) == 3144 + # Roundtrip via physical file. + f = str(tmp_path / "temp.jpg") + im1.save(f, icc_profile=icc_profile) + with Image.open(f) as im2: + assert im2.info.get("icc_profile") == icc_profile + # Roundtrip via memory buffer. + im1 = self.roundtrip(hopper()) + im2 = self.roundtrip(hopper(), icc_profile=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 @@ -115,9 +125,9 @@ def test(n): # 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) @@ -130,35 +140,35 @@ def test(n): 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 IOError 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) @@ -167,21 +177,21 @@ def test_progressive(self): 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) @@ -192,34 +202,34 @@ def test_progressive_cmyk_buffer(self): 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) def test_exif_typeerror(self): - im = Image.open("Tests/images/exif_typeerror.jpg") - # Should not raise a TypeError - im._getexif() + with Image.open("Tests/images/exif_typeerror.jpg") as im: + # Should not raise a TypeError + im._getexif() def test_exif_gps(self): # Arrange - im = Image.open("Tests/images/exif_gps.jpg") - 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", - } + 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() + # Act + exif = im._getexif() # Assert - self.assertEqual(exif[gps_index], expected_exif_gps) + assert exif[gps_index] == expected_exif_gps def test_exif_rollback(self): # rolling back exif support in 3.1 to pre-3.0 formatting. @@ -250,49 +260,53 @@ def test_exif_rollback(self): 33434: (4294967295, 1), } - im = Image.open("Tests/images/exif_gps.jpg") - exif = im._getexif() + 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): - im = Image.open("Tests/images/exif_gps_typeerror.jpg") + with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: - # Should not raise a TypeError - im._getexif() + # Should not raise a TypeError + im._getexif() 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): @@ -301,216 +315,219 @@ def getsampling(im): # 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): - im = Image.open("Tests/images/pil_sample_rgb.jpg") - info = im._getexif() - self.assertEqual(info[305], "Adobe Photoshop CS Macintosh") + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + info = im._getexif() + assert info[305] == "Adobe Photoshop CS Macintosh" def test_mp(self): - im = Image.open("Tests/images/pil_sample_rgb.jpg") - self.assertIsNone(im._getmp()) + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + 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 filename = "Tests/images/junk_jpeg_header.jpg" - Image.open(filename) + with Image.open(filename): + pass def test_ff00_jpeg_header(self): filename = "Tests/images/jpeg_ff00_header.jpg" - Image.open(filename) + with Image.open(filename): + pass 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): filename = "Tests/images/truncated_jpeg.jpg" - im = Image.open(filename) + with Image.open(filename) as im: + with pytest.raises(IOError): + im.load() + + # Test that the error is raised if loaded a second time + with pytest.raises(IOError): + im.load() + + 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 - with self.assertRaises(IOError): - im.load() - - # Test that the error is raised if loaded a second time - with self.assertRaises(IOError): - 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): - 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) - - # 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 + 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]) + + # 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 - self.assert_image_similar( - im, - self.roundtrip( - im, qtables={0: standard_l_qtable, 1: standard_chrominance_qtable} - ), - 30, - ) - - 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 - 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 - 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]]) - - @unittest.skipUnless(djpeg_available(), "djpeg not available") - def test_load_djpeg(self): - img = Image.open(TEST_FILE) - img.load_djpeg() - self.assert_image_similar(img, Image.open(TEST_FILE), 0) - @unittest.skipUnless(cjpeg_available(), "cjpeg not available") - def test_save_cjpeg(self): - img = Image.open(TEST_FILE) + # dict of qtable lists + assert_image_similar( + im, + self.roundtrip( + im, qtables={0: standard_l_qtable, 1: standard_chrominance_qtable} + ), + 30, + ) - 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) + _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") + + # 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]) + + # 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]]) + + @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() + assert_image_similar(img, Image.open(TEST_FILE), 0) + + @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 """ @@ -519,10 +536,10 @@ def test_bad_mpo_header(self): # Act # Shouldn't raise error fn = "Tests/images/sugarshack_bad_mpo_header.jpg" - im = self.assert_warning(UserWarning, Image.open, fn) + with pytest.warns(UserWarning, Image.open, fn) as im: - # Assert - self.assertEqual(im.format, "JPEG") + # Assert + assert im.format == "JPEG" def test_save_correct_modes(self): out = BytesIO() @@ -535,142 +552,159 @@ def test_save_wrong_modes(self): 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(IOError): + 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 - im = Image.open("Tests/images/iptc_roundUp.jpg") - self.assertEqual(im.info["dpi"], (44, 44)) + with Image.open("Tests/images/iptc_roundUp.jpg") as im: + assert im.info["dpi"] == (44, 44) # Round down - im = Image.open("Tests/images/iptc_roundDown.jpg") - self.assertEqual(im.info["dpi"], (2, 2)) + with Image.open("Tests/images/iptc_roundDown.jpg") as im: + assert im.info["dpi"] == (2, 2) + + 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)) - def test_save_dpi_rounding(self): - outfile = self.tempfile("temp.jpg") - im = Image.open("Tests/images/hopper.jpg") + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == (72, 72) - im.save(outfile, dpi=(72.2, 72.2)) - reloaded = Image.open(outfile) - self.assertEqual(reloaded.info["dpi"], (72, 72)) + im.save(outfile, dpi=(72.8, 72.8)) - im.save(outfile, dpi=(72.8, 72.8)) - reloaded = Image.open(outfile) - self.assertEqual(reloaded.info["dpi"], (73, 73)) + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == (73, 73) def test_dpi_tuple_from_exif(self): # Arrange # This Photoshop CC 2017 image has DPI in EXIF not metadata # EXIF XResolution is (2000000, 10000) - im = Image.open("Tests/images/photoshop-200dpi.jpg") + with Image.open("Tests/images/photoshop-200dpi.jpg") as im: - # Act / Assert - self.assertEqual(im.info.get("dpi"), (200, 200)) + # Act / Assert + assert im.info.get("dpi") == (200, 200) def test_dpi_int_from_exif(self): # Arrange # This image has DPI in EXIF not metadata # EXIF XResolution is 72 - im = Image.open("Tests/images/exif-72dpi-int.jpg") + with Image.open("Tests/images/exif-72dpi-int.jpg") as im: - # Act / Assert - self.assertEqual(im.info.get("dpi"), (72, 72)) + # Act / Assert + assert im.info.get("dpi") == (72, 72) def test_dpi_from_dpcm_exif(self): # Arrange # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg - im = Image.open("Tests/images/exif-200dpcm.jpg") + with Image.open("Tests/images/exif-200dpcm.jpg") as im: - # Act / Assert - self.assertEqual(im.info.get("dpi"), (508, 508)) + # Act / Assert + assert im.info.get("dpi") == (508, 508) def test_dpi_exif_zero_division(self): # Arrange # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg - im = Image.open("Tests/images/exif-dpi-zerodivision.jpg") + with Image.open("Tests/images/exif-dpi-zerodivision.jpg") as im: - # Act / Assert - # This should return the default, and not raise a ZeroDivisionError - self.assertEqual(im.info.get("dpi"), (72, 72)) + # Act / Assert + # This should return the default, and not raise a ZeroDivisionError + assert im.info.get("dpi") == (72, 72) def test_no_dpi_in_exif(self): # Arrange # This is photoshop-200dpi.jpg with resolution removed from EXIF: # exiftool "-*resolution*"= photoshop-200dpi.jpg - im = Image.open("Tests/images/no-dpi-in-exif.jpg") + with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: - # 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)) + # Act / Assert + # "When the image resolution is unknown, 72 [dpi] is designated." + # http://www.exiv2.org/tags.html + 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 # hexedited from MM * to FF FF FF FF - im = Image.open("Tests/images/invalid-exif.jpg") + with Image.open("Tests/images/invalid-exif.jpg") as im: - # This should return the default, and not a SyntaxError or - # OSError for unidentified image. - self.assertEqual(im.info.get("dpi"), (72, 72)) + # This should return the default, and not a SyntaxError or + # OSError for unidentified image. + assert im.info.get("dpi") == (72, 72) + + 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 # This image has been manually hexedited to have an IFD offset of 10, # in contrast to normal 8 - im = Image.open("Tests/images/exif-ifd-offset.jpg") + with Image.open("Tests/images/exif-ifd-offset.jpg") as im: - # Act / Assert - self.assertEqual(im._getexif()[306], "2017:03:13 23:03:09") + # Act / Assert + assert im._getexif()[306] == "2017:03:13 23:03:09" def test_photoshop(self): - im = Image.open("Tests/images/photoshop-200dpi.jpg") - self.assertEqual( - im.info["photoshop"][0x03ED], - { + with Image.open("Tests/images/photoshop-200dpi.jpg") as im: + 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 - im = Image.open("Tests/images/app13.jpg") - self.assertNotIn("photoshop", im.info) + with Image.open("Tests/images/app13.jpg") as im: + 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 -@unittest.skipUnless(sys.platform.startswith("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(WindowsError): + 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 72b374a0b68..72bc7df672f 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 PIL import Image, ImageFile, Jpeg2KImagePlugin -from .helper import PillowTestCase +from .helper import ( + assert_image_equal, + assert_image_similar, + is_big_endian, + on_ci, + skip_unless_feature, +) -codecs = dir(Image.core) +pytestmark = skip_unless_feature("jpg_2000") test_card = Image.open("Tests/images/test-card.png") test_card.load() @@ -14,199 +22,214 @@ # '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+$", Image.core.jp2klib_version) - 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") - - def test_jpf(self): - im = Image.open("Tests/images/balloon.jpf") - self.assertEqual(im.format, "JPEG2000") - self.assertEqual(im.get_format_mimetype(), "image/jpx") - - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - - 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) + 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(): + with Image.open("Tests/images/balloon.jpf") as im: + assert im.format == "JPEG2000" + assert im.get_format_mimetype() == "image/jpx" + + +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) - # 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") +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) - - 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_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_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_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_reduce(self): - im = Image.open("Tests/images/test-card-lossless.jp2") + assert_image_similar(im, test_card, 2.0) + + +def test_lossless_rt(): + im = roundtrip(test_card) + 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_rt(): + im = roundtrip(test_card, tile_size=(128, 128)) + 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) + im.reduce = 2 + assert im.reduce == 2 + im.load() - self.assertEqual(im.size, (160, 120)) + assert im.size == (160, 120) + + im.thumbnail((40, 40)) + assert im.size == (40, 30) + - def test_layers_type(self): - outfile = self.tempfile("temp_layers.jp2") - for quality_layers in [[100, 50, 10], (100, 50, 10), None]: +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 - j2k.load() - jp2.load() +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: - # Assert - self.assertEqual(j2k.mode, "RGBA") - self.assertEqual(jp2.mode, "RGBA") + # Act + j2k.load() + jp2.load() - def test_16bit_monochrome_has_correct_mode(self): + # Assert + assert j2k.mode == "RGBA" + assert jp2.mode == "RGBA" - j2k = Image.open("Tests/images/16bit.cropped.j2k") - jp2 = Image.open("Tests/images/16bit.cropped.jp2") +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" - self.assertEqual(j2k.mode, "I;16") - self.assertEqual(jp2.mode, "I;16") - def test_16bit_monochrome_jp2_like_tiff(self): +@pytest.mark.xfail(is_big_endian() and on_ci(), 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) - 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): +@pytest.mark.xfail(is_big_endian() and on_ci(), 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) - 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_16bit_j2k_roundtrips(): + with Image.open("Tests/images/16bit.cropped.j2k") as j2k: + im = roundtrip(j2k) + assert_image_equal(im, j2k) - 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_16bit_jp2_roundtrips(): + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + im = roundtrip(jp2) + assert_image_equal(im, jp2) - jp2 = Image.open("Tests/images/16bit.cropped.jp2") - im = self.roundtrip(jp2) - self.assert_image_equal(im, jp2) - 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_unbound_local(): + # prepatch, a malformed jp2 file could cause an UnboundLocalError exception. + with pytest.raises(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() +def test_parser_feed(): + # Arrange + with open("Tests/images/test-card-lossless.jp2", "rb") as f: + data = f.read() - # Act - p = ImageFile.Parser() - p.feed(data) + # 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 ea73a7ad50a..923bd610714 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,6 +1,4 @@ -from __future__ import print_function - -import distutils.version +import base64 import io import itertools import logging @@ -8,36 +6,40 @@ from collections import namedtuple from ctypes import c_float -from PIL import Image, TiffImagePlugin, TiffTags, features -from PIL._util import py3 +import pytest +from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags -from .helper import PillowTestCase, hopper +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) logger = logging.getLogger(__name__) -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() @@ -45,43 +47,40 @@ def _assert_noerr(self, im): class TestFileLibTiff(LibTiffTestCase): - def test_g4_tiff(self): + 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) - - self.assertEqual(im.size, (500, 500)) - self._assert_noerr(im) + with Image.open(test_file) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, 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) - - self.assertEqual(im.size, (500, 500)) - self._assert_noerr(im) + with Image.open(s) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, 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() @@ -89,69 +88,63 @@ def test_g4_non_disk_file_object(self): 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") - - self.assert_image_equal(g4, png) + 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) - 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) - - out = self.tempfile("temp.tif") - rot = orig.transpose(Image.ROTATE_90) - self.assertEqual(rot.size, (500, 500)) - rot.save(out) + 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) - 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") + 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" - self.assertEqual(reread.info["compression"], orig.info["compression"]) + assert 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) - - 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() + 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.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + 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]: - img = Image.open("Tests/images/hopper_g4.tif") - f = self.tempfile("temp.tiff") - - img.save(f, tiffinfo=img.tag) + f = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper_g4.tif") as img: + img.save(f, tiffinfo=img.tag) - if legacy_api: - original = img.tag.named() - else: - original = img.tag_v2.named() + if legacy_api: + original = img.tag.named() + else: + original = img.tag_v2.named() # PhotometricInterpretation is set from SAVE_INFO, # not the original image. @@ -162,37 +155,34 @@ def test_write_metadata(self): "PhotometricInterpretation", ] - loaded = Image.open(f) - if legacy_api: - reloaded = loaded.tag.named() - else: - reloaded = loaded.tag_v2.named() + with Image.open(f) as loaded: + if legacy_api: + reloaded = loaded.tag.named() + else: + reloaded = loaded.tag_v2.named() for tag, value in itertools.chain(reloaded.items(), original.items()): if tag not in ignored: val = original[tag] if tag.endswith("Resolution"): if legacy_api: - 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 + ), ("%s didn't roundtrip" % tag) 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, ( + "%s didn't roundtrip" % tag ) else: - self.assertEqual(val, value, msg="%s didn't roundtrip" % tag) + assert val == value, "%s didn't roundtrip" % tag # 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, "%s not in metadata" % field - 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. @@ -207,48 +197,48 @@ def test_additional_metadata(self): # 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 - - # 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)) - - # Extra samples really doesn't make sense in this application. - del new_ifd[338] - - out = self.tempfile("temp.tif") - TiffImagePlugin.WRITE_LIBTIFF = True - - im.save(out, tiffinfo=new_ifd) + with Image.open("Tests/images/hopper_g4.tif") as im: + for tag in im.tag_v2: + try: + del core_items[tag] + except KeyError: + pass + + # 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)) + + # Extra samples really doesn't make sense in this application. + del new_ifd[338] + + out = str(tmp_path / "temp.tif") + TiffImagePlugin.WRITE_LIBTIFF = True + + 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 @@ -263,7 +253,6 @@ def test_custom_metadata(self): tc(4.25, TiffTags.FLOAT, True), tc(4.25, TiffTags.DOUBLE, True), tc("custom tag value", TiffTags.ASCII, True), - tc(u"custom tag value", TiffTags.ASCII, True), tc(b"custom tag value", TiffTags.BYTE, True), tc((4, 5, 6), TiffTags.SHORT, True), tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True), @@ -284,12 +273,8 @@ def test_custom_metadata(self): ) } - 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: @@ -298,24 +283,26 @@ def test_custom_metadata(self): def check_tags(tiffinfo): im = hopper() - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") im.save(out, tiffinfo=tiffinfo) - reloaded = Image.open(out) - for tag, value in tiffinfo.items(): - reloaded_value = reloaded.tag_v2[tag] - if ( - isinstance(reloaded_value, TiffImagePlugin.IFDRational) - and libtiff - ): - # libtiff does not support real RATIONALS - self.assertAlmostEqual(float(reloaded_value), float(value)) - continue + with Image.open(out) as reloaded: + for tag, value in tiffinfo.items(): + reloaded_value = reloaded.tag_v2[tag] + if ( + isinstance(reloaded_value, TiffImagePlugin.IFDRational) + and libtiff + ): + # libtiff does not support real RATIONALS + assert ( + round(abs(float(reloaded_value) - float(value)), 7) == 0 + ) + continue - if libtiff and isinstance(value, bytes): - value = value.decode() + 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() @@ -335,176 +322,169 @@ def check_tags(tiffinfo): ) TiffImagePlugin.WRITE_LIBTIFF = False - def test_int_dpi(self): + 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 - reloaded = Image.open(out) - self.assertEqual(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") - - reread = Image.open(out) - self.assertEqual(reread.info["compression"], "group3") - self.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") - - b = im.tobytes() - # Bytes are in image native order (little endian) - if py3: - self.assertEqual(b[0], ord(b"\xe0")) - self.assertEqual(b[1], ord(b"\x01")) - else: - self.assertEqual(b[0], b"\xe0") - self.assertEqual(b[1], 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) + with Image.open(out) as reloaded: + assert reloaded.info["dpi"] == (72.0, 72.0) + + 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") + + with Image.open(out) as reread: + assert reread.info["compression"] == "group3" + assert_image_equal(reread, i) + + 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) + assert b[0] == ord(b"\xe0") + assert b[1] == ord(b"\x01") + + 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) - if py3: - self.assertEqual(b[0], ord(b"\x01")) - self.assertEqual(b[1], ord(b"\xe0")) - else: - self.assertEqual(b[0], b"\x01") - self.assertEqual(b[1], b"\xe0") - - out = self.tempfile("temp.tif") - im.save(out) - reread = Image.open(out) + # Bytes are in image native order (big endian) + assert b[0] == ord(b"\x01") + assert b[1] == ord(b"\xe0") - self.assertEqual(reread.info["compression"], im.info["compression"]) - self.assertEqual(reread.getpixel((0, 0)), 480) + 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 - 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) - - reread = Image.open(out) - self.assertEqual("temp.tif", reread.tag_v2[269]) - self.assertEqual("temp.tif", reread.tag[269][0]) + with Image.open(out) as reread: + 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? """ 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_quality(self, tmp_path): im = hopper("RGB") - out = self.tempfile("temp.tif") - - 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") + 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): + 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(IOError): + im.save(out, compression="tiff_ccitt") + with pytest.raises(IOError): + im.save(out, compression="group3") + with pytest.raises(IOError): + im.save(out, compression="group4") def test_fp_leak(self): im = Image.open("Tests/images/hopper_g4_500.tif") @@ -512,53 +492,56 @@ def test_fp_leak(self): 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 TiffImagePlugin.READ_LIBTIFF = True - im = Image.open("Tests/images/multipage.tiff") - # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue + with Image.open("Tests/images/multipage.tiff") as im: + # 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) + im.seek(0) + 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) + im.seek(1) + 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)) + im.seek(2) + assert not im.tag.next + assert im.size == (20, 20) + assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) TiffImagePlugin.READ_LIBTIFF = False def test_multipage_nframes(self): # issue #862 TiffImagePlugin.READ_LIBTIFF = True - im = Image.open("Tests/images/multipage.tiff") - frames = im.n_frames - self.assertEqual(frames, 3) - for _ in range(frames): - im.seek(0) - # Should not raise ValueError: I/O operation on closed file - im.load() + with Image.open("Tests/images/multipage.tiff") as im: + frames = im.n_frames + assert frames == 3 + for _ in range(frames): + im.seek(0) + # Should not raise ValueError: I/O operation on closed file + im.load() TiffImagePlugin.READ_LIBTIFF = False 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 @@ -567,13 +550,13 @@ def test_4bit(self): # 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 = ( @@ -598,15 +581,15 @@ def test_gray_semibyte_per_pixel(self): ) 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 @@ -624,8 +607,8 @@ def save_bytesio(compression=None): 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") @@ -635,34 +618,34 @@ def save_bytesio(compression=None): 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 infile = "Tests/images/total-pages-zero.tif" - im = Image.open(infile) - # Should not divide by zero - im.save(outfile) + with Image.open(infile) as im: + # 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()) @@ -676,184 +659,158 @@ def test_fd_duplication(self): 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_multipage_compression(self): - im = Image.open("Tests/images/compression.tif") + with Image.open("Tests/images/compression.tif") as im: - im.seek(0) - self.assertEqual(im._compression, "tiff_ccitt") - self.assertEqual(im.size, (10, 10)) + im.seek(0) + assert im._compression == "tiff_ccitt" + assert im.size == (10, 10) - im.seek(1) - self.assertEqual(im._compression, "packbits") - self.assertEqual(im.size, (10, 10)) - im.load() + im.seek(1) + 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)) - im.load() + im.seek(0) + 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") - - 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() + 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.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) - - 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() + 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.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" - im = Image.open(infile) - im.load() - self.assertEqual(im.size, (950, 975)) + with Image.open(infile) as im: + im.load() + assert im.size == (950, 975) def test_orientation(self): - base_im = Image.open("Tests/images/g4_orientation_1.tif") - - for i in range(2, 9): - im = Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") - im.load() + 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() - 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 # when saving to a new file. # Pillow 6.0 fails with "OSError: cannot identify image file". - import base64 - tiff = io.BytesIO( base64.b64decode( b"SUkqAAgAAAAPAP4ABAABAAAAAAAAAAABBAABAAAAAQAAAAEBBAABAAAAAQAA" @@ -870,3 +827,13 @@ def test_sampleformat_not_corrupted(self): 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(IOError) 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 0db37c7ea95..593a8eda83f 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,3 +1,5 @@ +from io import BytesIO + from PIL import Image from .test_file_libtiff import LibTiffTestCase @@ -13,35 +15,30 @@ class TestFileLibTiffSmall(LibTiffTestCase): 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) - - self.assertEqual(im.size, (128, 128)) - self._assert_noerr(im) + with Image.open(f) as im: + assert im.size == (128, 128) + self._assert_noerr(tmp_path, im) - def test_g4_hopper_bytesio(self): + def test_g4_hopper_bytesio(self, tmp_path): """Testing the bytesio loading code path""" - from io import BytesIO - 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 acc4ddb9191..516dbb20872 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -1,28 +1,30 @@ +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 5ec110c80b0..5003090c7d8 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -1,63 +1,62 @@ -from PIL import Image, ImagePalette, features +import pytest +from PIL import Image, ImagePalette -from .helper import PillowTestCase, hopper, unittest - -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): - im = Image.open(TEST_FILE) +def test_sanity(): + 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") + 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()]) im2 = hopper("RGBA") - self.assert_image_similar(im, im2, 10) + assert_image_similar(im, im2, 10) - def test_n_frames(self): - im = Image.open(TEST_FILE) - 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): - im = Image.open(TEST_FILE) - 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): - im = Image.open(TEST_FILE) - self.assertEqual(im.tell(), 0) +def test_tell(): + with Image.open(TEST_FILE) as im: + assert im.tell() == 0 - def test_seek(self): - im = Image.open(TEST_FILE) +def test_seek(): + with Image.open(TEST_FILE) as im: im.seek(0) - self.assertEqual(im.tell(), 0) + assert im.tell() == 0 + + with pytest.raises(EOFError): + im.seek(99) + assert im.tell() == 0 - self.assertRaises(EOFError, im.seek, 99) - self.assertEqual(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) +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" - self.assertRaises(SyntaxError, MicImagePlugin.MicImageFile, ole_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 82ecf64574f..893f9075d03 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,174 +1,217 @@ from io import BytesIO +import pytest from PIL import Image -from .helper import PillowTestCase +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 test_sanity(self): - for test_file in test_files: - im = Image.open(test_file) + +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(): + 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") + 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_unclosed_file(self): - def open(): - im = Image.open(test_files[0]) +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(None, open) + pytest.warns(None, open) - def test_app(self): - for test_file in test_files: - # Test APP/COM reader (@PIL135) - im = Image.open(test_file) - 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" + +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" ) - self.assertEqual(len(im.applist), 2) + assert len(im.applist) == 2 + - def test_exif(self): - for test_file in test_files: - im = Image.open(test_file) +def test_exif(): + 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) + assert info[272] == "Nintendo 3DS" + assert info[296] == 2 + assert 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 - im = Image.open("Tests/images/sugarshack_frame_size.mpo") - self.assertEqual(im.size, (640, 480)) +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) - self.assertEqual(im.size, (680, 480)) + assert im.size == (680, 480) - def test_parallax(self): - # Nintendo - im = Image.open("Tests/images/sugarshack.mpo") + +def test_parallax(): + # Nintendo + with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() - self.assertEqual(exif.get_ifd(0x927C)[0x1101]["Parallax"], -44.798187255859375) + assert exif.get_ifd(0x927C)[0x1101]["Parallax"] == -44.798187255859375 - # Fujifilm - im = Image.open("Tests/images/fujifilm.mpo") + # 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) + assert exif.get_ifd(0x927C)[0xB211] == -3.125 + - def test_mp(self): - for test_file in test_files: - im = Image.open(test_file) +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_offset(self): - # This image has been manually hexedited to have an IFD offset of 10 - # in APP2 data, in contrast to normal 8 - im = Image.open("Tests/images/sugarshack_ifd_offset.mpo") +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() - self.assertEqual(mpinfo[45056], b"0100") - self.assertEqual(mpinfo[45057], 2) + assert mpinfo[45056] == b"0100" + assert mpinfo[45057] == 2 + + +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) - def test_mp_attribute(self): - for test_file in test_files: - im = Image.open(test_file) + +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: - 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: - im = Image.open(test_file) - self.assertEqual(im.tell(), 0) + 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_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 - self.assertRaises(EOFError, im.seek, -1) - self.assertRaises(EOFError, im.seek, -523) + 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 - self.assertRaises(EOFError, im.seek, 2) - self.assertRaises(EOFError, im.seek, 523) + with pytest.raises(EOFError): + im.seek(2) + with pytest.raises(EOFError): + im.seek(523) # bad calls shouldn't change the frame - self.assertEqual(im.tell(), 0) + assert im.tell() == 0 # this one will work im.seek(1) - self.assertEqual(im.tell(), 1) + assert im.tell() == 1 # and this one, too im.seek(0) - self.assertEqual(im.tell(), 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_n_frames(self): - im = Image.open("Tests/images/sugarshack.mpo") - self.assertEqual(im.n_frames, 2) - self.assertTrue(im.is_animated) - def test_eoferror(self): - im = Image.open("Tests/images/sugarshack.mpo") +def test_eoferror(): + with Image.open("Tests/images/sugarshack.mpo") 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) - def test_image_grab(self): - for test_file in test_files: - im = Image.open(test_file) - self.assertEqual(im.tell(), 0) + +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) - self.assertEqual(im.tell(), 1) + assert im.tell() == 1 im1 = im.tobytes() im.seek(0) - self.assertEqual(im.tell(), 0) + assert 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: - im = Image.open(test_file) - self.assertEqual(im.tell(), 0) - jpg0 = self.frame_roundtrip(im) - self.assert_image_similar(im, jpg0, 30) + 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) - self.assertEqual(im.tell(), 1) - jpg1 = self.frame_roundtrip(im) - self.assert_image_similar(im, jpg1, 30) + 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 5d512047b93..8f261388ee7 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,78 +1,90 @@ import os +import pytest from PIL import Image, MspImagePlugin -from .helper import PillowTestCase, hopper, unittest +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) - - 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): - - 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" + assert_image_equal(im, hopper("1")) + assert isinstance(im, MspImagePlugin.MspImageFile) + + +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) + + +@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")) - - def test_cannot_save_wrong_mode(self): - # Arrange - im = hopper() - filename = self.tempfile("temp.msp") - - # Act/Assert - self.assertRaises(IOError, im.save, filename) + for path in files: + _assert_file_image_equal(path, path.replace(".msp", ".png")) + + +@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(IOError): + im.save(filename) diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index fbfd8966152..886332dea82 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,61 +1,90 @@ 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, + skip_known_bad_test, +) -class TestFilePalm(PillowTestCase): - _roundtrip = imagemagick_available() +_roundtrip = imagemagick_available() - def helper_save_as_palm(self, mode): - # Arrange - im = hopper(mode) - outfile = self.tempfile("temp_" + mode + ".palm") - # Act - im.save(outfile) +def helper_save_as_palm(tmp_path, mode): + # Arrange + im = hopper(mode) + outfile = str(tmp_path / ("temp_" + mode + ".palm")) - # Assert - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + # Act + im.save(outfile) - def roundtrip(self, mode): - if not self._roundtrip: - return + # Assert + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 - im = hopper(mode) - outfile = self.tempfile("temp.palm") - im.save(outfile) - converted = self.open_withImagemagick(outfile) - self.assert_image_equal(converted, im) +def open_with_imagemagick(tmp_path, f): + if not imagemagick_available(): + raise OSError() - def test_monochrome(self): - # Arrange - mode = "1" + 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) - # Act / Assert - self.helper_save_as_palm(mode) - self.roundtrip(mode) - def test_p_mode(self): - # Arrange - mode = "P" +def roundtrip(tmp_path, mode): + if not _roundtrip: + return - # Act / Assert - self.helper_save_as_palm(mode) - self.skipKnownBadTest("Palm P image is wrong") - self.roundtrip(mode) + im = hopper(mode) + outfile = str(tmp_path / "temp.palm") - def test_l_ioerror(self): - # Arrange - mode = "L" + im.save(outfile) + converted = open_with_imagemagick(tmp_path, outfile) + assert_image_equal(converted, im) - # Act / Assert - self.assertRaises(IOError, self.helper_save_as_palm, mode) - def test_rgb_ioerror(self): - # Arrange - mode = "RGB" +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_p_mode(tmp_path): + # Arrange + mode = "P" + + # Act / Assert + helper_save_as_palm(tmp_path, mode) + skip_known_bad_test("Palm P image is wrong") + roundtrip(tmp_path, mode) + + +def test_l_ioerror(tmp_path): + # Arrange + mode = "L" + + # Act / Assert + with pytest.raises(IOError): + helper_save_as_palm(tmp_path, mode) + + +def test_rgb_ioerror(tmp_path): + # Arrange + mode = "RGB" + + # Act / Assert + with pytest.raises(IOError): + helper_save_as_palm(tmp_path, mode) diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index b23328ba537..dc45a48c1cb 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 eb2c7d6112b..5af7469c7fd 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,129 +1,142 @@ +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") - im.save(f) - im2 = Image.open(f) +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)) - 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) + # Test an unsupported mode + f = str(tmp_path / "temp.pcx") + im = hopper("RGBA") + with pytest.raises(ValueError): + im.save(f) - def test_sanity(self): - for mode in ("1", "L", "P", "RGB"): - self._roundtrip(hopper(mode)) - # Test an unsupported mode - f = self.tempfile("temp.pcx") - im = hopper("RGBA") - self.assertRaises(ValueError, im.save, f) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + PcxImagePlugin.PcxImageFile(invalid_file) - self.assertRaises(SyntaxError, PcxImagePlugin.PcxImageFile, invalid_file) - 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_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_pil184(self): - # Check reading of files where xmin/xmax is not zero. - test_file = "Tests/images/pil184.pcx" - im = Image.open(test_file) +def test_pil184(): + # Check reading of files where xmin/xmax is not zero. - 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) - - 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() + assert im.histogram()[0] + im.histogram()[255] == 447 * 144 + + +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, y] = x % 128 + _test_buffer_overflow(tmp_path, 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_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(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_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, 0] = x // 67 * 67 - self._roundtrip(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(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_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_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) + 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 25c2f6bf6a6..ea3b6c1d9f3 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -4,276 +4,283 @@ 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 - im = Image.open("Tests/images/dispose_bgnd.gif") +def test_cmyk_mode(tmp_path): + # Arrange + mode = "CMYK" - outfile = self.tempfile("temp.pdf") + # Act / Assert + helper_save_as_pdf(tmp_path, mode) + + +def test_unsupported_mode(tmp_path): + im = hopper("LA") + outfile = str(tmp_path / "temp_LA.pdf") + + with pytest.raises(ValueError): + im.save(outfile) + + +def test_save_all(tmp_path): + # Single frame image + helper_save_as_pdf(tmp_path, "RGB", save_all=True) + + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + + outfile = str(tmp_path / "temp.pdf") im.save(outfile, save_all=True) - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + 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) - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 # Test appending using a generator def imGenerator(ims): - for im in ims: - yield im + yield from ims im.save(outfile, save_all=True, append_images=imGenerator(ims)) - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 - # Append JPEG images - jpeg = Image.open("Tests/images/flower.jpg") + # 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 - im = Image.open("Tests/images/dispose_bgnd.gif") +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: - outfile = self.tempfile("temp.pdf") + outfile = str(tmp_path / "temp.pdf") im.save(outfile) - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) - - def test_pdf_open(self): - # fail on a buffer full of null bytes - self.assertRaises( - PdfParser.PdfFormatError, PdfParser.PdfParser, buf=bytearray(65536) - ) - - # 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") - - # 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) - - # 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) - - # 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) - - def test_pdf_append_fails_on_nonexistent_file(self): - im = hopper("RGB") - temp_dir = tempfile.mkdtemp() - try: - self.assertRaises( - IOError, im.save, os.path.join(temp_dir, "nonexistent.pdf"), append=True - ) - finally: - os.rmdir(temp_dir) - - 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, []) - - 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) - - # append some info - pdf.info.Title = "abc" - pdf.info.Author = "def" - pdf.info.Subject = u"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) - - # 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, u"ghi\uABCD") - self.assertIn(b"CreationDate", pdf.info) - self.assertIn(b"ModDate", pdf.info) - self.check_pdf_pages_consistency(pdf) - - 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) 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) - - 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) + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + +def test_pdf_open(tmp_path): + # fail on a buffer full of null bytes + with pytest.raises(PdfParser.PdfFormatError): + PdfParser.PdfParser(buf=bytearray(65536)) + + # 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 + + # make a PDF file + pdf_filename = helper_save_as_pdf(tmp_path, "RGB") + + # 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 + + # 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 + + # 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_fails_on_nonexistent_file(): + im = hopper("RGB") + with tempfile.TemporaryDirectory() as temp_dir: + with pytest.raises(IOError): + im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) + + +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 == [] + + +def test_pdf_append(tmp_path): + # make a PDF file + pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser") + + # 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) + + # 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: + 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 c744932d437..5e83c610498 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -1,23 +1,25 @@ +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 6d76a6caa7c..b6da365309c 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,21 +1,20 @@ -import sys +import re import zlib from io import BytesIO +import pytest from PIL import Image, ImageFile, PngImagePlugin -from PIL._util import py3 - -from .helper import PillowLeakTestCase, PillowTestCase, hopper, unittest - -try: - from PIL import _webp - - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False - -codecs = dir(Image.core) +from .helper import ( + PillowLeakTestCase, + assert_image, + assert_image_equal, + hopper, + is_big_endian, + is_win32, + on_ci, + skip_unless_feature, +) # sample png stream @@ -53,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: @@ -73,272 +69,267 @@ def get_chunks(self, filename): png.crc(cid, s) return chunks - def test_sanity(self): + @pytest.mark.xfail(is_big_endian() and on_ci(), 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+)?$", Image.core.zlib_version) - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") hopper("RGB").save(test_file) - im = Image.open(test_file) - 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") + with Image.open(test_file) as im: + im.load() + 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(IOError): + 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) - - self.assert_image(im, "RGBA", (162, 150)) - self.assertTrue(im.info.get("interlace")) + with Image.open(test_file) as im: + assert_image(im, "RGBA", (162, 150)) + assert 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) - - # 'transparency' contains a byte string with the opacity for - # each palette entry - self.assertEqual(len(im.info["transparency"]), 256) + 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 - 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) - - # pixel value 164 is full transparent - self.assertEqual(im.info["transparency"], 164) - self.assertEqual(im.getpixel((31, 31)), 164) + with Image.open(in_file) as im: + # pixel value 164 is full transparent + assert im.info["transparency"] == 164 + assert 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))]) - - def test_save_greyscale_transparency(self): + 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, 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) - - # Assert that there is no unclosed file warning - self.assert_warning(None, im.verify) + with Image.open(TEST_PNG_FILE) as im: + # Assert that there is no unclosed file warning + pytest.warns(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) @@ -351,9 +342,10 @@ def test_verify_struct_error(self): 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((IOError, SyntaxError)): + im.verify() def test_verify_ignores_crc_error(self): # check ignores crc errors in ancillary chunks @@ -362,12 +354,13 @@ def test_verify_ignores_crc_error(self): 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 @@ -378,50 +371,46 @@ def test_verify_not_ignores_crc_error_in_required_chunk(self): 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 - im = Image.open(TEST_PNG_FILE) - self.assertEqual(im.info["dpi"], (96, 96)) + with Image.open(TEST_PNG_FILE) as im: + assert im.info["dpi"] == (96, 96) # Round down - im = Image.open("Tests/images/icc_profile_none.png") - self.assertEqual(im.info["dpi"], (72, 72)) + with Image.open("Tests/images/icc_profile_none.png") as im: + 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) - - info = PngImagePlugin.PngInfo() - info.add_text("TXT", "VALUE") - info.add_text("ZIP", "VALUE", zip=True) + with Image.open(TEST_PNG_FILE) as im: + 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 @@ -432,12 +421,12 @@ def test_roundtrip_itxt(self): 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 @@ -446,26 +435,23 @@ def test_nonunicode_text(self): 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 on Python 3 - # This cannot really be meaningfully tested on Python 2, - # since it didn't preserve charsets to begin with. + # Check preservation of non-ASCII characters def rt_text(value): im = Image.new("RGB", (32, 32)) info = PngImagePlugin.PngInfo() info.add_text("Text", value) im = roundtrip(im, pnginfo=info) - self.assertEqual(im.info, {"Text": value}) + assert im.info == {"Text": value} - if py3: - rt_text(" Aa" + chr(0xA0) + chr(0xC4) + chr(0xFF)) # Latin1 - rt_text(chr(0x400) + chr(0x472) + chr(0x4FF)) # Cyrillic - # CJK: - rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00)) - rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042)) # Combined + rt_text(" Aa" + chr(0xA0) + chr(0xC4) + chr(0xFF)) # Latin1 + rt_text(chr(0x400) + chr(0x472) + chr(0x4FF)) # Cyrillic + # CJK: + rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00)) + rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042)) # Combined def test_scary(self): # Check reading of evil PNG file. For information, see: @@ -477,188 +463,179 @@ def test_scary(self): data = b"\x89" + fd.read() pngfile = BytesIO(data) - self.assertRaises(IOError, Image.open, pngfile) + with pytest.raises(IOError): + 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" - im = Image.open(test_file) + with Image.open(test_file) as im: - self.assertEqual(im.info["transparency"], 0) + assert im.info["transparency"] == 0 def test_save_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 - with_icc = Image.open("Tests/images/icc_profile.png") - expected_icc = with_icc.info["icc_profile"] + 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) + im = roundtrip(im, 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_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(IOError): + 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(IOError): + 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"}) - - def test_exif(self): - im = Image.open("Tests/images/exif.png") - exif = im._getexif() - self.assertEqual(exif[274], 1) - - def test_exif_save(self): - im = Image.open("Tests/images/exif.png") - - test_file = self.tempfile("temp.png") - im.save(test_file) - - reloaded = Image.open(test_file) - exif = reloaded._getexif() - self.assertEqual(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) - - reloaded = Image.open(test_file) - exif = reloaded._getexif() - self.assertEqual(exif[305], "Adobe Photoshop CS Macintosh") + with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: + assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} + + @pytest.mark.parametrize( + "test_file", + [ + "Tests/images/exif.png", # With an EXIF chunk + "Tests/images/exif_imagemagick.png", # With an ImageMagick zTXt chunk + ], + ) + def test_exif(self, test_file): + with Image.open(test_file) as im: + exif = im._getexif() + assert exif[274] == 1 + + 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) - def test_exif_argument(self): - im = Image.open(TEST_PNG_FILE) + with Image.open(test_file) as reloaded: + exif = reloaded._getexif() + assert exif[274] == 1 - test_file = self.tempfile("temp.png") - im.save(test_file, exif=b"exifstring") + 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) - reloaded = Image.open(test_file) - self.assertEqual(reloaded.info["exif"], b"Exif\x00\x00exifstring") + with Image.open(test_file) as reloaded: + exif = reloaded._getexif() + assert exif[305] == "Adobe Photoshop CS Macintosh" - @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_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") - # 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) + with Image.open(test_file) as reloaded: + assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" -@unittest.skipIf(sys.platform.startswith("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 5d2a0bc69e8..6b91ba28adb 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,75 +1,82 @@ +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" + + with Image.open("Tests/images/16_bit_binary_pgm.png") as tgt: + assert_image_equal(im, tgt) - tgt = Image.open("Tests/images/16_bit_binary_pgm.png") - self.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) +def test_pnm(tmp_path): + with Image.open("Tests/images/hopper.pnm") as im: + assert_image_similar(im, hopper(), 0.0001) - f = self.tempfile("temp.pnm") + 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(tmp_path): + path = str(tmp_path / "temp.pgm") + with open(path, "w") as f: + f.write("P6") + + with pytest.raises(ValueError): + Image.open(path) - 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_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. - 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(IOError): + Image.open("Tests/images/negative_size.ppm") - with self.assertRaises(IOError): - Image.open("Tests/images/negative_size.ppm") - def test_mimetypes(self): - path = self.tempfile("temp.pgm") +def test_mimetypes(tmp_path): + path = str(tmp_path / "temp.pgm") - with open(path, "w") as f: - f.write("P4\n128 128\n255") - im = Image.open(path) - self.assertEqual(im.get_format_mimetype(), "image/x-portable-bitmap") + 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") - im = Image.open(path) - self.assertEqual(im.get_format_mimetype(), "image/x-portable-anymap") + 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 8381ceaefcd..4d8c6eba466 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,98 +1,129 @@ +import pytest from PIL import Image, PsdImagePlugin -from .helper import PillowTestCase, hopper +from .helper import assert_image_similar, hopper, is_pypy test_file = "Tests/images/hopper.psd" -class TestImagePsd(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, "PSD") + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "PSD" im2 = hopper() - self.assert_image_similar(im, im2, 4.8) + 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_unclosed_file(self): - def open(): - im = Image.open(test_file) +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.assert_warning(None, open) + pytest.warns(None, open) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, PsdImagePlugin.PsdImageFile, invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - def test_n_frames(self): - im = Image.open("Tests/images/hopper_merged.psd") - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) + with pytest.raises(SyntaxError): + PsdImagePlugin.PsdImageFile(invalid_file) - im = Image.open(test_file) - self.assertEqual(im.n_frames, 2) - self.assertTrue(im.is_animated) - def test_eoferror(self): - im = Image.open(test_file) +def test_n_frames(): + with Image.open("Tests/images/hopper_merged.psd") as im: + assert im.n_frames == 1 + assert not im.is_animated + + with Image.open(test_file) as im: + assert im.n_frames == 2 + assert im.is_animated + + +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 # 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) - def test_seek_tell(self): - im = Image.open(test_file) + +def test_seek_tell(): + with Image.open(test_file) as im: layer_number = im.tell() - self.assertEqual(layer_number, 1) + assert layer_number == 1 - self.assertRaises(EOFError, im.seek, 0) + with pytest.raises(EOFError): + im.seek(0) im.seek(1) layer_number = im.tell() - self.assertEqual(layer_number, 1) + assert layer_number == 1 im.seek(2) layer_number = im.tell() - self.assertEqual(layer_number, 2) + assert layer_number == 2 - def test_seek_eoferror(self): - im = Image.open(test_file) - self.assertRaises(EOFError, im.seek, -1) +def test_seek_eoferror(): + with Image.open(test_file) as im: - def test_open_after_exclusive_load(self): - im = Image.open(test_file) + with pytest.raises(EOFError): + im.seek(-1) + + +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): - im = Image.open(test_file) - self.assertIn("icc_profile", im.info) + +def test_icc_profile(): + with Image.open(test_file) as im: + assert "icc_profile" in im.info icc_profile = im.info["icc_profile"] - self.assertEqual(len(icc_profile), 3144) + assert len(icc_profile) == 3144 + - def test_no_icc_profile(self): - im = Image.open("Tests/images/hopper_merged.psd") +def test_no_icc_profile(): + with Image.open("Tests/images/hopper_merged.psd") as im: + assert "icc_profile" not in im.info - self.assertNotIn("icc_profile", im.info) - 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. +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 self.assertRaises(IOError): - Image.open("Tests/images/combined_larger_than_size.psd") + # 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(IOError): + Image.open("Tests/images/combined_larger_than_size.psd") diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index ff3aea1d57b..cb16276ce97 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,89 +1,100 @@ +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") +def test_unsupported_mode(tmp_path): + im = hopper("LA") + out = str(tmp_path / "temp.sgi") - self.assertRaises(ValueError, im.save, out, format="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 34020848629..c7446e161c9 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,131 +1,162 @@ import tempfile from io import BytesIO +import pytest from PIL import Image, ImageSequence, SpiderImagePlugin -from .helper import PillowTestCase, hopper +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() - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "SPIDER") + im.close() + + pytest.warns(None, open) + - def test_unclosed_file(self): - def open(): - im = Image.open(TEST_FILE) +def test_context_manager(): + def open(): + with Image.open(TEST_FILE) as im: im.load() - self.assert_warning(None, open) + pytest.warns(None, open) - def test_save(self): - # Arrange - temp = self.tempfile("temp.spider") - im = hopper() - # Act - im.save(temp, "SPIDER") +def test_save(tmp_path): + # Arrange + temp = str(tmp_path / "temp.spider") + im = hopper() - # Assert - im2 = Image.open(temp) - self.assertEqual(im2.mode, "F") - self.assertEqual(im2.size, (128, 128)) - self.assertEqual(im2.format, "SPIDER") + # Act + im.save(temp, "SPIDER") - def test_tempfile(self): - # Arrange - im = hopper() + # Assert + with Image.open(temp) as im2: + assert im2.mode == "F" + assert im2.size == (128, 128) + assert im2.format == "SPIDER" - # 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_tempfile(): + # Arrange + im = hopper() - def test_isSpiderImage(self): - self.assertTrue(SpiderImagePlugin.isSpiderImage(TEST_FILE)) + # Act + with tempfile.TemporaryFile() as fp: + im.save(fp, "SPIDER") - def test_tell(self): - # Arrange - im = Image.open(TEST_FILE) + # Assert + fp.seek(0) + with Image.open(fp) as reloaded: + assert reloaded.mode == "F" + assert reloaded.size == (128, 128) + assert reloaded.format == "SPIDER" + + +def test_is_spider_image(): + assert SpiderImagePlugin.isSpiderImage(TEST_FILE) + + +def test_tell(): + # Arrange + with Image.open(TEST_FILE) as im: # Act index = im.tell() # Assert - self.assertEqual(index, 0) + assert index == 0 - def test_n_frames(self): - im = Image.open(TEST_FILE) - 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"] +def test_n_frames(): + with Image.open(TEST_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated - # Act - img_list = SpiderImagePlugin.loadImageSeries(file_list) - # Assert - self.assertEqual(len(img_list), 1) - self.assertIsInstance(img_list[0], Image.Image) - self.assertEqual(img_list[0].size, (128, 128)) +def test_load_image_series(): + # Arrange + not_spider_file = "Tests/images/hopper.ppm" + file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] - def test_loadImageSeries_no_input(self): - # Arrange - file_list = None + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) - # Act - img_list = SpiderImagePlugin.loadImageSeries(file_list) + # Assert + assert len(img_list) == 1 + assert isinstance(img_list[0], Image.Image) + assert img_list[0].size == (128, 128) - # Assert - self.assertIsNone(img_list) - def test_isInt_not_a_number(self): - # Arrange - not_a_number = "a" +def test_load_image_series_no_input(): + # Arrange + file_list = None - # Act - ret = SpiderImagePlugin.isInt(not_a_number) + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) - # Assert - self.assertEqual(ret, 0) + # Assert + assert img_list is None - def test_invalid_file(self): - invalid_file = "Tests/images/invalid.spider" - self.assertRaises(IOError, Image.open, invalid_file) +def test_is_int_not_a_number(): + # Arrange + not_a_number = "a" - def test_nonstack_file(self): - im = Image.open(TEST_FILE) + # Act + ret = SpiderImagePlugin.isInt(not_a_number) - self.assertRaises(EOFError, im.seek, 0) + # Assert + assert ret == 0 - def test_nonstack_dos(self): - im = Image.open(TEST_FILE) + +def test_invalid_file(): + invalid_file = "Tests/images/invalid.spider" + + with pytest.raises(IOError): + 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)): - if i > 1: - self.fail("Non-stack DOS file test failed") - - # 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) + 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 84d59e0c737..03e26ef8b04 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,46 +1,51 @@ import os +import pytest from PIL import Image, SunImagePlugin -from .helper import PillowTestCase, hopper, unittest +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)) - - self.assert_image_similar(im, hopper(), 5) # visually verified - - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(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) + assert im.size == (128, 128) + + assert_image_similar(im, hopper(), 5) # visually verified + + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + SunImagePlugin.SunImageFile(invalid_file) + + +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 = "%s.png" % os.path.splitext(path)[0] + # 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 c4666a65ab7..3fe0cd04e1a 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,35 +1,45 @@ -from PIL import Image, TarIO +import pytest +from PIL import Image, TarIO, features -from .helper import PillowTestCase - -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: - tar = TarIO.TarIO(TEST_TAR_FILE, test_path) - im = Image.open(tar) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, format) - - def test_close(self): +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() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == format + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") + + pytest.warns(ResourceWarning, open) + + +def test_close(): + def open(): tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() - def test_contextmanager(self): + 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 abbebe0ebe5..4919ad76627 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -2,199 +2,205 @@ 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, "*x*_{}.png".format(mode.lower())) + ) + + 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) - - # Generate a new test name every time so the - # test will not fail with permission error - # on Windows. - out = self.tempfile("temp.tga") + assert_image_equal(original_im, reference_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() - ) + roundtrip(original_im) - self.assert_image_equal(saved_im, original_im) - def test_id_field(self): - # tga file with id field - test_file = "Tests/images/tga_id_field.tga" +def test_id_field(): + # tga file with id field + test_file = "Tests/images/tga_id_field.tga" - # Act - im = Image.open(test_file) + # Act + with Image.open(test_file) as im: # Assert - self.assertEqual(im.size, (100, 100)) + assert im.size == (100, 100) - def test_id_field_rle(self): - # tga file with id field - test_file = "Tests/images/rgb32rle.tga" - # Act - im = Image.open(test_file) +def test_id_field_rle(): + # tga file with id field + test_file = "Tests/images/rgb32rle.tga" + + # Act + with Image.open(test_file) as im: # Assert - self.assertEqual(im.size, (199, 199)) + 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) - test_im = Image.open(out) - self.assertEqual(test_im.size, (100, 100)) - self.assertEqual(test_im.info["id_section"], im.info["id_section"]) + with Image.open(out) as test_im: + assert test_im.size == (100, 100) + assert test_im.info["id_section"] == im.info["id_section"] # RGBA save im.convert("RGBA").save(out) - test_im = Image.open(out) - self.assertEqual(test_im.size, (100, 100)) + with Image.open(out) as test_im: + assert test_im.size == (100, 100) + + +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(self): - test_file = "Tests/images/rgb32rle.tga" - im = Image.open(test_file) - out = self.tempfile("temp.tga") +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) - test_im = Image.open(out) - 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") - test_im = Image.open(out) - 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)) - test_im = Image.open(out) - 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" - im = Image.open(test_file) + 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="") - test_im = Image.open(out) - self.assertNotIn("id_section", test_im.info) + 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) - test_im = Image.open(out) - 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) - test_im = Image.open(out) - 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) - test_im = Image.open(out) - 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) - test_im = Image.open(out) - 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") - test_im = Image.open(out) - 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 +def test_save_l_transparency(tmp_path): + # 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) + 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 = self.tempfile("temp.tga") + 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 2d15de2bd56..a996f0b0e7f 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,415 +1,428 @@ import logging -import sys +import os from io import BytesIO +import pytest from PIL import Image, TiffImagePlugin -from PIL._util import py3 from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION -from .helper import PillowTestCase, hopper, unittest +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_tofile, + hopper, + is_pypy, + is_win32, +) logger = logging.getLogger(__name__) -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) - im = Image.open(filename) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "TIFF") + with Image.open(filename) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "TIFF" hopper("1").save(filename) - Image.open(filename) + with Image.open(filename): + pass hopper("L").save(filename) - Image.open(filename) + with Image.open(filename): + pass hopper("P").save(filename) - Image.open(filename) + with Image.open(filename): + pass hopper("RGB").save(filename) - Image.open(filename) + with Image.open(filename): + pass hopper("I").save(filename) - Image.open(filename) + with Image.open(filename): + pass + @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(None, open) + pytest.warns(ResourceWarning, open) + + def test_closed_file(self): + def open(): + im = Image.open("Tests/images/multipage.tiff") + im.load() + im.close() + + pytest.warns(None, open) + + def test_context_manager(self): + def open(): + with Image.open("Tests/images/multipage.tiff") as im: + im.load() + + pytest.warns(None, open) def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - - 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() + 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.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") - - def test_size(self): - filename = "Tests/images/pil168.tif" - im = Image.open(filename) - - def set_size(): - im.size = (256, 256) - - self.assert_warning(DeprecationWarning, set_size) + assert str(e.value) == "Not allowing setting of legacy api" def test_xyres_tiff(self): filename = "Tests/images/pil168.tif" - im = Image.open(filename) + with Image.open(filename) as im: - # legacy api - self.assertIsInstance(im.tag[X_RESOLUTION][0], tuple) - self.assertIsInstance(im.tag[Y_RESOLUTION][0], tuple) + # legacy api + 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) + # v2 api + 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" - im = Image.open(filename) + 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]) + # v2 api + 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)) - # Fallback "inch". - self.assertEqual(im.info["dpi"], (100.0, 100.0)) + # Legacy. + assert im.info["resolution"] == (100.0, 100.0) + # Fallback "inch". + assert im.info["dpi"] == (100.0, 100.0) def test_int_resolution(self): filename = "Tests/images/pil168.tif" - im = Image.open(filename) + with Image.open(filename) as im: - # Try to read a file where X,Y_RESOLUTION are ints - im.tag_v2[X_RESOLUTION] = 71 - im.tag_v2[Y_RESOLUTION] = 71 - im._setup() - self.assertEqual(im.info["dpi"], (71.0, 71.0)) + # Try to read a file where X,Y_RESOLUTION are ints + im.tag_v2[X_RESOLUTION] = 71 + im.tag_v2[Y_RESOLUTION] = 71 + im._setup() + 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))): - im = Image.open( + with Image.open( "Tests/images/hopper_roundDown_" + str(resolutionUnit) + ".tif" - ) - self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit) - self.assertEqual(im.info["dpi"], (dpi[0], dpi[0])) + ) as im: + assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit + assert im.info["dpi"] == (dpi[0], dpi[0]) - im = Image.open( + with Image.open( "Tests/images/hopper_roundUp_" + str(resolutionUnit) + ".tif" - ) - self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit) - self.assertEqual(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") + ) as im: + assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit + assert im.info["dpi"] == (dpi[1], dpi[1]) - for dpi in (72.2, 72.8): - im.save(outfile, dpi=(dpi, dpi)) + 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)) - 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): - i = Image.open("Tests/images/hopper_bad_exif.jpg") - # Should not raise struct.error. - self.assert_warning(UserWarning, i._getexif) + with Image.open("Tests/images/hopper_bad_exif.jpg") as i: + # Should not raise struct.error. + 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(IOError): + 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) - if py3: - self.assertEqual(b[0], ord(b"\xe0")) - self.assertEqual(b[1], ord(b"\x01")) - else: - self.assertEqual(b[0], b"\xe0") - self.assertEqual(b[1], 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) - if py3: - self.assertEqual(b[0], ord(b"\x01")) - self.assertEqual(b[1], ord(b"\xe0")) - else: - self.assertEqual(b[0], b"\x01") - self.assertEqual(b[1], 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? """ - 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(IOError): + Image.open("Tests/images/hopper_unknown_pixel_mode.tif") def test_n_frames(self): for path, n_frames in [ ["Tests/images/multipage-lastframe.tif", 1], ["Tests/images/multipage.tiff", 3], ]: - im = Image.open(path) - self.assertEqual(im.n_frames, n_frames) - self.assertEqual(im.is_animated, n_frames != 1) + with Image.open(path) as im: + assert im.n_frames == n_frames + assert im.is_animated == (n_frames != 1) def test_eoferror(self): - im = Image.open("Tests/images/multipage-lastframe.tif") - n_frames = im.n_frames + 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) + # 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) + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) def test_multipage(self): # issue #862 - im = Image.open("Tests/images/multipage.tiff") - # file is a multipage tiff: 10x10 green, 10x10 red, 20x20 blue + with Image.open("Tests/images/multipage.tiff") as im: + # 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)) + im.seek(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)) + im.seek(1) + im.load() + 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)) + im.seek(0) + im.load() + 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)) + im.seek(2) + im.load() + 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" - im = Image.open(filename) + with Image.open(filename) as im: - # Act - ret = str(im.ifd) + # Act + ret = str(im.ifd) - # Assert - self.assertIsInstance(ret, str) + # Assert + assert isinstance(ret, str) def test_dict(self): # Arrange filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - - # v2 interface - v2_tags = { - 256: 55, - 257: 43, - 258: (8, 8, 8, 8), - 259: 1, - 262: 2, - 296: 2, - 273: (8,), - 338: (1,), - 277: 4, - 279: (9460,), - 282: 72.0, - 283: 72.0, - 284: 1, - } - self.assertEqual(dict(im.tag_v2), v2_tags) - - # legacy interface - legacy_tags = { - 256: (55,), - 257: (43,), - 258: (8, 8, 8, 8), - 259: (1,), - 262: (2,), - 296: (2,), - 273: (8,), - 338: (1,), - 277: (4,), - 279: (9460,), - 282: ((720000, 10000),), - 283: ((720000, 10000),), - 284: (1,), - } - self.assertEqual(dict(im.tag), legacy_tags) + with Image.open(filename) as im: + + # v2 interface + v2_tags = { + 256: 55, + 257: 43, + 258: (8, 8, 8, 8), + 259: 1, + 262: 2, + 296: 2, + 273: (8,), + 338: (1,), + 277: 4, + 279: (9460,), + 282: 72.0, + 283: 72.0, + 284: 1, + } + assert dict(im.tag_v2) == v2_tags + + # legacy interface + legacy_tags = { + 256: (55,), + 257: (43,), + 258: (8, 8, 8, 8), + 259: (1,), + 262: (2,), + 296: (2,), + 273: (8,), + 338: (1,), + 277: (4,), + 279: (9460,), + 282: ((720000, 10000),), + 283: ((720000, 10000),), + 284: (1,), + } + assert dict(im.tag) == legacy_tags def test__delitem__(self): filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - len_before = len(dict(im.ifd)) - del im.ifd[256] - len_after = len(dict(im.ifd)) - self.assertEqual(len_before, len_after + 1) + with Image.open(filename) as im: + len_before = len(dict(im.ifd)) + del im.ifd[256] + len_after = len(dict(im.ifd)) + assert len_before == len_after + 1 def test_load_byte(self): for legacy_api in [False, True]: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc" ret = ifd.load_byte(data, legacy_api) - 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_seek(self): filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - im.seek(0) - self.assertEqual(im.tell(), 0) + with Image.open(filename) as im: + im.seek(0) + assert im.tell() == 0 def test_seek_eof(self): filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - self.assertEqual(im.tell(), 0) - self.assertRaises(EOFError, im.seek, -1) - self.assertRaises(EOFError, im.seek, 1) + with Image.open(filename) as im: + 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 = ( @@ -434,119 +447,113 @@ def test_gray_semibyte_per_pixel(self): ) 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) - - def test_with_underscores(self): + 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, 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) - im = Image.open(filename) + 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) + # legacy interface + 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) + # v2 interface + 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) - - tmpfile = self.tempfile("temp.tif") - im.save(tmpfile) + with Image.open(infile) as im: + assert im.getpixel((0, 0)) == pixel_value - reloaded = Image.open(tmpfile) + tmpfile = str(tmp_path / "temp.tif") + im.save(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" - 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_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")) - def test_tiff_save_all(self): - import io - import os + for mode in ["P", "PA"]: + roundtrip(mode) - mp = io.BytesIO() + def test_tiff_save_all(self): + mp = BytesIO() with Image.open("Tests/images/multipage.tiff") as im: im.save(mp, format="tiff", save_all=True) 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 = io.BytesIO() + mp = BytesIO() im = Image.new("RGB", (100, 100), "#f00") ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] 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): - for im in ims: - yield im + yield from ims - mp = io.BytesIO() + mp = BytesIO() 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,27 +562,26 @@ def test_saving_icc_profile(self): 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") - reloaded = Image.open(tmpfile) - - self.assertEqual(b"Dummy value", reloaded.info["icc_profile"]) + with Image.open(tmpfile) as reloaded: + 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) @@ -583,21 +589,23 @@ def test_close_on_load_nonexclusive(self): 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") 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(sys.platform.startswith("win32"), "Windows only") -class TestFileTiffW32(PillowTestCase): - def test_fd_leak(self): - tmpfile = self.tempfile("temp.tif") - import os +@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: @@ -605,10 +613,11 @@ def test_fd_leak(self): im = Image.open(tmpfile) fp = im.fp - self.assertFalse(fp.closed) - self.assertRaises(WindowsError, os.remove, tmpfile) + assert not fp.closed + with pytest.raises(WindowsError): + 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 170cac71ed5..9fe601bd65b 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,258 +1,339 @@ import io import struct +import pytest from PIL import Image, TiffImagePlugin, TiffTags -from PIL.TiffImagePlugin import IFDRational, _limit_rational - -from .helper import PillowTestCase, hopper - -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 - """ - - 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. - - 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"] - - 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 - - f = self.tempfile("temp.tif") - - img.save(f, tiffinfo=info) - - loaded = Image.open(f) - - self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (len(bindata),)) - - self.assertEqual(loaded.tag[ImageJMetaData], bindata) - self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata) - - self.assertEqual(loaded.tag[ImageDescription], (reloaded_textdata,)) - self.assertEqual(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) - - # check with 2 element ImageJMetaDataByteCounts, issue #2006 - - info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8) - img.save(f, tiffinfo=info) - loaded = Image.open(f) - - self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (8, len(bindata) - 8)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (8, len(bindata) - 8)) - - def test_read_metadata(self): - img = Image.open("Tests/images/hopper_g4.tif") - - 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(), - ) - - 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(), - ) - - def test_write_metadata(self): - """ Test metadata writing through the python code """ - img = Image.open("Tests/images/hopper.tif") - - f = self.tempfile("temp.tiff") +from PIL.TiffImagePlugin import IFDRational + +from .helper import assert_deep_equal, hopper + +TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()} + + +def test_rt_metadata(tmp_path): + """ 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() + + # 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() + + 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[ImageDescription] = textdata + + f = str(tmp_path / "temp.tif") + + img.save(f, tiffinfo=info) + + with Image.open(f) as loaded: + + assert loaded.tag[ImageJMetaDataByteCounts] == (len(bindata),) + assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bindata),) + + assert loaded.tag[ImageJMetaData] == bindata + assert loaded.tag_v2[ImageJMetaData] == bindata + + assert loaded.tag[ImageDescription] == (reloaded_textdata,) + assert loaded.tag_v2[ImageDescription] == reloaded_textdata + + 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 + + info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8) + img.save(f, tiffinfo=info) + with Image.open(f) as loaded: + + assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) + assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) + + +def test_read_metadata(): + with Image.open("Tests/images/hopper_g4.tif") as img: + + 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() + + 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() + + +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) - loaded = Image.open(f) - original = img.tag_v2.named() - 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"] - - 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, - "%s didn't roundtrip, %s, %s" % (tag, original[tag], value), - ) - else: - self.assertEqual( - original[tag], - value, - "%s didn't roundtrip, %s, %s" % (tag, original[tag], value), - ) - - 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_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) - - 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") + with Image.open(f) as loaded: + reloaded = loaded.tag_v2.named() + 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, + "{} didn't roundtrip, {}, {}".format(tag, original[tag], value), + ) + else: + assert 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], "%s didn't roundtrip" % tag + + +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) - reloaded = Image.open(out) - 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. - - im = Image.open("Tests/images/hopper.iccprofile_binary.tif") - 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") + + with Image.open(out) as reloaded: + assert not isinstance(im.info["icc_profile"], tuple) + assert im.info["icc_profile"] == reloaded.info["icc_profile"] + + +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") - - reloaded = Image.open(out) - self.assertEqual(0, reloaded.tag_v2[41988].numerator) - self.assertEqual(0, reloaded.tag_v2[41988].denominator) - - 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) - - 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_exif_div_zero(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + info[41988] = TiffImagePlugin.IFDRational(0, 0) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert 0 == reloaded.tag_v2[41988].numerator + assert 0 == reloaded.tag_v2[41988].denominator + + +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]) == 1 + assert isinstance(im.tag_v2[34377][0], bytes) + out = str(tmp_path / "temp.tiff") im.save(out) - reloaded = Image.open(out) - 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]) == 1 + assert isinstance(reloaded.tag_v2[34377][0], bytes) + - def test_too_many_entries(self): - ifd = TiffImagePlugin.ImageFileDirectory_v2() +def test_too_many_entries(): + ifd = TiffImagePlugin.ImageFileDirectory_v2() - # 277: ("SamplesPerPixel", SHORT, 1), - ifd._tagdata[277] = struct.pack("hh", 4, 4) - ifd.tagtype[277] = TiffTags.SHORT + # 277: ("SamplesPerPixel", SHORT, 1), + ifd._tagdata[277] = struct.pack("hh", 4, 4) + ifd.tagtype[277] = TiffTags.SHORT - # Should not raise ValueError. - self.assert_warning(UserWarning, lambda: ifd[277]) + # 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 74c238fc1c6..60be1d5bcd5 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 4d44f47b65c..22957f06d9b 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,6 +1,12 @@ +import pytest from PIL import Image, WebPImagePlugin -from .helper import PillowTestCase, hopper, unittest +from .helper import ( + assert_image_similar, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) try: from PIL import _webp @@ -10,23 +16,21 @@ 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(IOError, 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): @@ -39,89 +43,83 @@ def test_read_rgb(self): Does it have the bits we expect? """ - image = Image.open("Tests/images/hopper.webp") - - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") - image.load() - image.getdata() + 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() - # 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 - ) + # 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) - def test_write_rgb(self): + def test_write_rgb(self, tmp_path): """ Can we write a RGB mode file to webp without error. Does it have the bits we expect? """ - temp_file = self.tempfile("temp.webp") + temp_file = str(tmp_path / "temp.webp") hopper(self.rgb_mode).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() - - # 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 - ) - - # 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): + 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() + + # 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. The old lena images for WebP are showing ~16 on + # Ubuntu, the jpegs are showing ~18. + target = hopper(self.rgb_mode) + assert_image_similar(image, target, 12.0) + + 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") + temp_file = str(tmp_path / "temp.webp") hopper("L").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") + 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() - target = hopper("L").convert(self.rgb_mode) + image.load() + image.getdata() + target = hopper("L").convert(self.rgb_mode) - self.assert_image_similar(image, target, 10.0) + 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") + temp_file = str(tmp_path / "temp.webp") hopper("P").save(temp_file) - image = Image.open(temp_file) + with Image.open(temp_file) as image: + assert image.mode == self.rgb_mode + assert image.size == (128, 128) + assert image.format == "WEBP" - 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) - image.load() - image.getdata() - target = hopper("P").convert(self.rgb_mode) - - self.assert_image_similar(image, target, 50.0) + assert_image_similar(image, target, 50.0) def test_WebPEncode_with_invalid_args(self): """ @@ -129,8 +127,10 @@ def test_WebPEncode_with_invalid_args(self): """ 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): """ @@ -138,15 +138,16 @@ def test_WebPDecode_with_invalid_args(self): """ 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" @@ -154,24 +155,23 @@ def test_file_pointer_could_be_reused(self): 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): - im = Image.open("Tests/images/chi.gif") - original_value = im.convert("RGB").getpixel((1, 1)) + @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") - im.save(out_webp, save_all=True) + # Save as 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) - reread = Image.open(out_gif) - reread_value = reread.convert("RGB").getpixel((1, 1)) + with Image.open(out_gif) as reread: + reread_value = reread.convert("RGB").getpixel((1, 1)) difference = sum( [abs(original_value[i] - reread_value[i]) for i in range(0, 3)] ) - self.assertLess(difference, 5) + assert difference < 5 diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index f2f10d7b72e..c624156df13 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,117 +1,115 @@ +import pytest from PIL import Image -from .helper import PillowTestCase, hopper, unittest +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? - """ +def test_write_rgba(tmp_path): + """ + Can we write a RGBA mode file to WebP without error. + Does it have the bits we expect? + """ - temp_file = self.tempfile("temp.webp") + temp_file = str(tmp_path / "temp.webp") - pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) - pil_image.save(temp_file) + pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) + pil_image.save(temp_file) - if _webp.WebPDecoderBuggyAlpha(self): - return + if _webp.WebPDecoderBuggyAlpha(): + return - image = Image.open(temp_file) + 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" - Image.open(file_path).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() - target = Image.open(file_path).convert("RGBA") + 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 dec74d0d044..a846a6db4fd 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,165 +1,166 @@ +import pytest from PIL import Image -from .helper import PillowTestCase +from .helper import ( + assert_image_equal, + assert_image_similar, + is_big_endian, + on_ci, + skip_unless_feature, +) -try: - from PIL import _webp +pytestmark = [ + skip_unless_feature("webp"), + skip_unless_feature("webp_anim"), +] - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False +def test_n_frames(): + """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" -class TestFileWebpAnimation(PillowTestCase): - def setUp(self): - if not HAVE_WEBP: - self.skipTest("WebP support not installed") - return + with Image.open("Tests/images/hopper.webp") as im: + assert im.n_frames == 1 + assert not im.is_animated - if not _webp.HAVE_WEBPANIM: - self.skipTest( - "WebP library does not contain animation support, " - "not testing animation" - ) + with Image.open("Tests/images/iss634.webp") as im: + assert im.n_frames == 42 + assert im.is_animated - def test_n_frames(self): - """ - Ensure that WebP format sets n_frames and is_animated - attributes correctly. - """ - im = Image.open("Tests/images/hopper.webp") - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) +@pytest.mark.xfail(is_big_endian() and on_ci(), 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. + """ - im = Image.open("Tests/images/iss634.webp") - self.assertEqual(im.n_frames, 42) - self.assertTrue(im.is_animated) + with Image.open("Tests/images/iss634.gif") as orig: + assert orig.n_frames > 1 - 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. - """ + 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 - orig = Image.open("Tests/images/iss634.gif") - self.assertGreater(orig.n_frames, 1) + # Compare first and last frames to the original animated GIF + orig.load() + im.load() + 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() + assert_image_similar(im, orig.convert("RGBA"), 25.0) - 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) - - # Compare first and last frames to the original animated GIF - orig.load() - im.load() - self.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) - - 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() and on_ci(), 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")) - - frame1 = Image.open("Tests/images/anim_frame1.webp") - frame2 = Image.open("Tests/images/anim_frame2.webp") - - 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): - for im in ims: - yield im - - 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. - """ - - 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, - ) - - im = Image.open(temp_file) - self.assertEqual(im.n_frames, 5) - self.assertTrue(im.is_animated) + assert_image_equal(im, frame2.convert("RGBA")) + + 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) + + # 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) + + +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 = 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, + ) + + 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, - ) - - im = Image.open(temp_file) - self.assertEqual(im.n_frames, 5) - self.assertTrue(im.is_animated) + +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. + """ + + 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 2eff4152911..4d06f53b1af 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,38 +1,27 @@ +import pytest from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import assert_image_equal, hopper -try: - from PIL import _webp +_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") +RGB_MODE = "RGB" - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False +def test_write_lossless_rgb(tmp_path): + if _webp.WebPDecoderVersion() < 0x0200: + pytest.skip("lossless not included") -class TestFileWebpLossless(PillowTestCase): - def setUp(self): - if not HAVE_WEBP: - self.skipTest("WebP support not installed") - return + temp_file = str(tmp_path / "temp.webp") - if _webp.WebPDecoderVersion() < 0x0200: - self.skipTest("lossless not included") + hopper(RGB_MODE).save(temp_file, lossless=True) - self.rgb_mode = "RGB" - - 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 ae528e3bf16..9fa20e403bf 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,139 +1,119 @@ -from PIL import Image - -from .helper import PillowTestCase +from io import BytesIO -try: - from PIL import _webp - - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False +from PIL import Image +from .helper import skip_unless_feature -class TestFileWebpMetadata(PillowTestCase): - def setUp(self): - if not HAVE_WEBP: - self.skipTest("WebP support not installed") - return +pytestmark = [ + skip_unless_feature("webp"), + skip_unless_feature("webp_mux"), +] - if not _webp.HAVE_WEBPMUX: - self.skipTest("WebPMux support not installed") - def test_read_exif_metadata(self): +def test_read_exif_metadata(): - file_path = "Tests/images/flower.webp" - image = Image.open(file_path) + file_path = "Tests/images/flower.webp" + with Image.open(file_path) as image: - self.assertEqual(image.format, "WEBP") + assert image.format == "WEBP" exif_data = image.info.get("exif", None) - self.assertTrue(exif_data) + assert exif_data exif = image._getexif() - # camera make - self.assertEqual(exif[271], "Canon") + # Camera make + assert exif[271] == "Canon" - jpeg_image = Image.open("Tests/images/flower.jpg") - expected_exif = jpeg_image.info["exif"] + with Image.open("Tests/images/flower.jpg") as jpeg_image: + expected_exif = jpeg_image.info["exif"] - self.assertEqual(exif_data, expected_exif) + assert exif_data == expected_exif - def test_write_exif_metadata(self): - from io import BytesIO - file_path = "Tests/images/flower.jpg" - image = Image.open(file_path) +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): +def test_read_icc_profile(): - file_path = "Tests/images/flower2.webp" - image = Image.open(file_path) + file_path = "Tests/images/flower2.webp" + with Image.open(file_path) as image: - self.assertEqual(image.format, "WEBP") - self.assertTrue(image.info.get("icc_profile", None)) + assert image.format == "WEBP" + assert image.info.get("icc_profile", None) icc = image.info["icc_profile"] - jpeg_image = Image.open("Tests/images/flower2.jpg") - expected_icc = jpeg_image.info["icc_profile"] + with Image.open("Tests/images/flower2.jpg") as jpeg_image: + expected_icc = jpeg_image.info["icc_profile"] - self.assertEqual(icc, expected_icc) + assert icc == expected_icc - def test_write_icc_metadata(self): - from io import BytesIO - file_path = "Tests/images/flower2.jpg" - image = Image.open(file_path) +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" - ) - - def test_read_no_exif(self): - from io import BytesIO + assert webp_icc_profile + if webp_icc_profile: + assert webp_icc_profile == expected_icc_profile, "Webp ICC didn't match" - 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) - - self.assertFalse(webp_image._getexif()) - - def test_write_animated_metadata(self): - if not _webp.HAVE_WEBPANIM: - self.skipTest("WebP animation support not available") - - iccp_data = "".encode("utf-8") - exif_data = "".encode("utf-8") - xmp_data = "".encode("utf-8") - - 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, - ) - - image = Image.open(temp_file) - 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)) + test_buffer.seek(0) + with Image.open(test_buffer) as webp_image: + assert not webp_image._getexif() + + +@skip_unless_feature("webp_anim") +def test_write_animated_metadata(tmp_path): + 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, + ) + + 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 cea0cec5bd8..03444eb9d3f 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,61 +1,78 @@ +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 - im = Image.open("Tests/images/drawing.emf") + # 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") - imref.load() - self.assert_image_similar(im, imref, 0) + with Image.open("Tests/images/drawing_emf_ref.png") as imref: + imref.load() + assert_image_similar(im, imref, 0) - # Test basic WMF open and rendering - im = Image.open("Tests/images/drawing.wmf") + # 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") - imref.load() - self.assert_image_similar(im, imref, 2.0) + with Image.open("Tests/images/drawing_wmf_ref.png") as imref: + imref.load() + 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 - im = Image.open("Tests/images/drawing.emf") - self.assertEqual(im.info["dpi"], 1424) + # Restore the state before this test + WmfImagePlugin.register_handler(original_handler) - # Round down - im = Image.open("Tests/images/drawing_roundDown.emf") - 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.0) + + +def test_save(tmp_path): + im = hopper() + + for ext in [".wmf", ".emf"]: + tmpfile = str(tmp_path / ("temp" + ext)) + with pytest.raises(IOError): + im.save(tmpfile) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 9693ba05a86..23a54056969 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,6 +1,9 @@ +from io import BytesIO + +import pytest from PIL import Image -from .helper import PillowTestCase +from .helper import hopper PIL151 = b""" #define basic_width 32 @@ -26,36 +29,53 @@ """ -class TestFileXbm(PillowTestCase): - def test_pil151(self): - from io import BytesIO - - 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 - im = Image.open(filename) +def test_open(): + # Arrange + # Created with `convert hopper.png hopper.xbm` + filename = "Tests/images/hopper.xbm" + + # Act + with Image.open(filename) as im: # Assert - self.assertEqual(im.mode, "1") - self.assertEqual(im.size, (128, 128)) + assert im.mode == "1" + assert im.size == (128, 128) + - def test_open_filename_with_underscore(self): - # Arrange - # Created with `convert hopper.png hopper_underscore.xbm` - filename = "Tests/images/hopper_underscore.xbm" +def test_open_filename_with_underscore(): + # Arrange + # Created with `convert hopper.png hopper_underscore.xbm` + filename = "Tests/images/hopper_underscore.xbm" - # Act - im = Image.open(filename) + # Act + with Image.open(filename) as im: # Assert - self.assertEqual(im.mode, "1") - self.assertEqual(im.size, (128, 128)) + 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 a49b7c8dd10..187440d4e2a 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,33 +1,36 @@ +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 - im = Image.open(TEST_FILE) + with pytest.raises(SyntaxError): + XpmImagePlugin.XpmImageFile(invalid_file) + + +def test_load_read(): + # Arrange + with Image.open(TEST_FILE) as im: dummy_bytes = 1 # Act data = im.load_read(dummy_bytes) - # Assert - self.assertEqual(len(data), 16384) + # Assert + assert len(data) == 16384 diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index f8b6d35314f..7c8c451139f 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -1,35 +1,37 @@ +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 9b3a342d02f..4be39c383de 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,19 +1,18 @@ +import pytest from PIL import BdfFontFile, FontFile -from .helper import PillowTestCase - 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 14b36858523..015210b4d4c 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,13 +1,8 @@ -from __future__ import division +from PIL import Image, ImageDraw, ImageFont -import sys +from .helper import PillowLeakTestCase, skip_unless_feature -from PIL import Image, ImageDraw, ImageFont, features -from .helper import PillowLeakTestCase, unittest - - -@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") class TestTTypeFontLeak(PillowLeakTestCase): # fails at iteration 3 in master iterations = 10 @@ -22,7 +17,7 @@ def _test_font(self, font): ) ) - @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 a2b4ef27e8d..afd0c38b2e6 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,79 +1,90 @@ -from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile -from PIL._util import py3 +import os -from .helper import PillowTestCase +import pytest +from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile -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") - - 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) - - with Image.open(tempname.replace(".pil", ".pbm")) as loaded: - with Image.open("Tests/fonts/10x20.pbm") as target: - self.assert_image_equal(loaded, target) - - 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 test_sanity(self): - self.save_font() - - def test_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, PcfFontFile.PcfFontFile, fp) - - 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) - - 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_high_characters(self): - message = "".join(chr(i + 1) for i in range(140, 232)) - self._test_high_characters(message) - # accept bytes instances in Py3. - if py3: - self._test_high_characters(message.encode("latin1")) +pytestmark = skip_unless_feature("zlib") + + +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 + + 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("Tests/fonts/10x20.pbm") as target: + assert_image_equal(loaded, target) + + 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_sanity(request, tmp_path): + save_font(request, tmp_path) + + +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 l in range(len(message)): + msg = message[: l + 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 00000000000..8621f18aee4 --- /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("Tests/fonts/ter-x20b-%s.pbm" % encoding) as target: + assert_image_equal(loaded, target) + + with open(tempname, "rb") as f_loaded: + with open("Tests/fonts/ter-x20b-%s.pil" % encoding, "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 l in range(len(message)): + msg = message[: l + 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 ce0524e1e22..d10b1acfd66 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -2,153 +2,135 @@ import itertools from PIL import Image -from PIL._util import py3 -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() - - r.paste(w, (0, 0)) - r.paste(w90, (px, 0)) - - g.paste(w90, (0, 0)) - g.paste(w, (2 * px, 0)) - - b.paste(w, (px, 0)) - b.paste(w90, (2 * px, 0)) - - img = Image.merge("RGB", (r, g, b)) - - return img - - def to_xxx_colorsys(self, im, func, mode): - # convert the hard way using the library colorsys routines. - - (r, g, b) = im.split() - - if py3: - conv_func = self.int_to_float - else: - conv_func = self.str_to_float - - if hasattr(itertools, "izip"): - iter_helper = itertools.izip - else: - iter_helper = itertools.zip_longest - - converted = [ - self.tuple_to_ints(func(conv_func(_r), conv_func(_g), conv_func(_b))) - for (_r, _g, _b) in iter_helper(r.tobytes(), g.tobytes(), b.tobytes()) - ] - - if py3: - new_bytes = b"".join( - bytes(chr(h) + chr(s) + chr(v), "latin-1") for (h, s, v) in converted - ) - else: - new_bytes = b"".join(chr(h) + chr(s) + chr(v) for (h, s, v) in converted) - - hsv = Image.frombytes(mode, r.size, new_bytes) - - return hsv - - def to_hsv_colorsys(self, im): - return self.to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") - - def to_rgb_colorsys(self, im): - return self.to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") - - def test_wedge(self): - src = self.wedge().resize((3 * 32, 32), Image.BILINEAR) - im = src.convert("HSV") - comparable = self.to_hsv_colorsys(src) - - 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" - ) - - comparable = src - im = im.convert("RGB") - - 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")) - - 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) - - 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_sanity(): + Image.new("HSV", (100, 100)) + + +def wedge(): + w = Image._wedge() + w90 = w.rotate(90) + + (px, h) = w.size + + r = Image.new("L", (px * 3, h)) + g = r.copy() + b = r.copy() + + r.paste(w, (0, 0)) + r.paste(w90, (px, 0)) + + g.paste(w90, (0, 0)) + g.paste(w, (2 * px, 0)) + + b.paste(w, (px, 0)) + b.paste(w90, (2 * px, 0)) + + img = Image.merge("RGB", (r, g, b)) + + return img + + +def to_xxx_colorsys(im, func, mode): + # convert the hard way using the library colorsys routines. + + (r, g, b) = im.split() + + conv_func = int_to_float + + 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()) + ] + + 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) + + return hsv + + +def to_hsv_colorsys(im): + return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") + + +def to_rgb_colorsys(im): + return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") + + +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 a98c2057901..41c8efdf316 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) + + assert list(L) == [255] * 100 + assert list(a) == [128] * 100 + assert list(b) == [128] * 100 - 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") +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 47196a1394a..3a0b7bd62d9 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,14 +1,22 @@ +import io import os import shutil -import sys +import tempfile -from PIL import Image -from PIL._util import py3 +import PIL +import pytest +from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError -from .helper import PillowTestCase, hopper, unittest +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_not_all_same, + hopper, + is_win32, +) -class TestImage(PillowTestCase): +class TestImage: def test_image_modes_success(self): for mode in [ "1", @@ -44,76 +52,82 @@ def test_image_modes_fail(self): "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): + assert issubclass(UnidentifiedImageError, IOError) 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"]: - im = Image.open(os.path.join("Tests/images", file)) + """ 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() + assert False + except OSError as e: + assert str(e) == "buffer overrun when reading image file" + + with Image.open("Tests/images/fli_overrun2.bin") as im: try: - im.load() - self.assertFail() - except IOError as e: - self.assertEqual(str(e), "buffer overrun when reading image file") + im.seek(1) + assert False + except OSError as e: + assert str(e) == "buffer overrun when reading image file" -class MockEncoder(object): +class MockEncoder: pass @@ -609,23 +675,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(IOError): + Image._getencoder("RGB", "DoesNotExist", ("args",), extra=("extra",)) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index b06814cb944..25cc9fef4e3 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,9 +1,13 @@ +import ctypes import os +import subprocess import sys +from distutils import ccompiler, sysconfig +import pytest from PIL import Image -from .helper import PillowTestCase, hopper, on_appveyor, unittest +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 @@ -17,17 +21,17 @@ 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 @@ -41,7 +45,7 @@ def test_sanity(self): 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 @@ -51,8 +55,8 @@ def test_sanity(self): 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) @@ -63,22 +67,22 @@ def test_sanity(self): 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 @@ -88,8 +92,8 @@ def test_sanity_negative_index(self): 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) @@ -100,7 +104,7 @@ def test_sanity_negative_index(self): 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): @@ -119,54 +123,45 @@ def check(self, mode, c=None): # 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 %s, color %s" % (mode, c), - ) + assert ( + im.getpixel((0, 0)) == c + ), "put/getpixel roundtrip failed for mode {}, color {}".format(mode, 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, ( + "put/getpixel roundtrip negative index failed for mode %s, color %s" + % (mode, 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 %s, color %s " % (mode, c), - ) + assert ( + im.getpixel((0, 0)) == c + ), "initial color failed for mode {}, color {} ".format(mode, 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 + ), "initial color failed with negative index for mode %s, color %s " % (mode, 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): @@ -201,20 +196,20 @@ def test_p_putpixel_rgb_rgba(self): for color in [(255, 0, 0), (255, 0, 0, 255)]: im = Image.new("P", (1, 1), 0) im.putpixel((0, 0), color) - 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 @@ -229,12 +224,11 @@ def _test_get_access(self, im): 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") @@ -276,11 +270,11 @@ def _test_set_access(self, im, color): 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): @@ -310,7 +304,7 @@ def test_set_vs_c(self): # 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): @@ -321,26 +315,23 @@ def test_reference_counting(self): 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( - not sys.platform.startswith("win32") or on_appveyor(), - "Failing on AppVeyor when run from subprocess, not from shell", +class TestEmbeddable: + @pytest.mark.skipif( + not is_win32() or on_ci(), + reason="Failing on AppVeyor / GitHub Actions when run from subprocess, " + "not from shell", ) def test_embeddable(self): - import subprocess - import ctypes - from distutils import ccompiler, sysconfig - with open("embed_pil.c", "w") as fh: fh.write( """ @@ -349,12 +340,8 @@ def test_embeddable(self): int main(int argc, char* argv[]) { char *home = "%s"; -#if PY_MAJOR_VERSION >= 3 wchar_t *whome = Py_DecodeLocale(home, NULL); Py_SetPythonHome(whome); -#else - Py_SetPythonHome(home); -#endif Py_InitializeEx(0); Py_DECREF(PyImport_ImportModule("PIL.Image")); @@ -364,9 +351,7 @@ def test_embeddable(self): Py_DECREF(PyImport_ImportModule("PIL.Image")); Py_Finalize(); -#if PY_MAJOR_VERSION >= 3 PyMem_RawFree(whome); -#endif return 0; } @@ -393,4 +378,4 @@ def test_embeddable(self): 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 02e5c80f2a0..bf6d88a97e5 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,54 +1,60 @@ +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"]) - - # self.assertEqual(test("1"), (3, (100, 128), '|b1', 1600)) - self.assertEqual(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)) - - 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)) - - def test_fromarray(self): - class Wrapper(object): - """ Class with API matching Image.fromarray """ - - def __init__(self, img, arr_params): - self.img = img - self.__array_interface__ = arr_params - - def tobytes(self): - return self.img.tobytes() - - 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()) - - # 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_toarray(): + def test(mode): + ai = im.convert(mode).__array_interface__ + return ai["version"], ai["shape"], ai["typestr"], len(ai["data"]) + + # assert test("1") == (3, (100, 128), '|b1', 1600)) + assert test("L") == (3, (100, 128), "|u1", 12800) + + # FIXME: wrong? + assert test("I") == (3, (100, 128), Image._ENDIAN + "i4", 51200) + # FIXME: wrong? + assert test("F") == (3, (100, 128), Image._ENDIAN + "f4", 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(): + class Wrapper: + """ Class with API matching Image.fromarray """ + + def __init__(self, img, arr_params): + self.img = img + self.__array_interface__ = arr_params + + def tobytes(self): + return self.img.tobytes() + + 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 abbd2a45f1d..cf83922b6f7 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,241 +1,261 @@ +import pytest from PIL import Image -from .helper import PillowTestCase, 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) - - modes = ( - "1", - "L", - "LA", - "P", - "PA", - "I", - "F", - "RGB", - "RGBA", - "RGBX", - "CMYK", - "YCbCr", - "HSV", - ) +from .helper import assert_image, assert_image_equal, assert_image_similar, hopper + + +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", + ) + + for mode in modes: + im = hopper(mode) + for mode in modes: + convert(im, mode) + # Check 0 + im = Image.new(mode, (0, 0)) 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) - def test_default(self): +def test_default(): - 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) + 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) - # ref https://github.com/python-pillow/Pillow/issues/274 - def _test_float_conversion(self, im): - orig = im.getpixel((5, 5)) - converted = im.convert("F").getpixel((5, 5)) - self.assertEqual(orig, converted) +# ref https://github.com/python-pillow/Pillow/issues/274 - def test_8bit(self): - im = Image.open("Tests/images/hopper.jpg") - self._test_float_conversion(im.convert("L")) - def test_16bit(self): - im = Image.open("Tests/images/16bit.cropped.tif") - self._test_float_conversion(im) +def _test_float_conversion(im): + orig = im.getpixel((5, 5)) + converted = im.convert("F").getpixel((5, 5)) + assert orig == converted - def test_16bit_workaround(self): - im = Image.open("Tests/images/16bit.cropped.tif") - self._test_float_conversion(im.convert("I")) - def test_rgba_p(self): - im = hopper("RGBA") - im.putalpha(hopper("L")) +def test_8bit(): + with Image.open("Tests/images/hopper.jpg") as im: + _test_float_conversion(im.convert("L")) - converted = im.convert("P") - comparable = converted.convert("RGBA") - self.assert_image_similar(im, comparable, 20) +def test_16bit(): + with Image.open("Tests/images/16bit.cropped.tif") as im: + _test_float_conversion(im) - def test_trns_p(self): - im = hopper("P") - im.info["transparency"] = 0 - f = self.tempfile("temp.png") +def test_16bit_workaround(): + with Image.open("Tests/images/16bit.cropped.tif") as im: + _test_float_conversion(im.convert("I")) - im_l = im.convert("L") - self.assertEqual(im_l.info["transparency"], 0) # undone - im_l.save(f) - im_rgb = im.convert("RGB") - self.assertEqual(im_rgb.info["transparency"], (0, 0, 0)) # undone - im_rgb.save(f) +def test_rgba_p(): + im = hopper("RGBA") + im.putalpha(hopper("L")) - # ref https://github.com/python-pillow/Pillow/issues/664 + converted = im.convert("P") + comparable = converted.convert("RGBA") - def test_trns_p_rgba(self): - # Arrange - im = hopper("P") - im.info["transparency"] = 128 + assert_image_similar(im, comparable, 20) - # Act - im_rgba = im.convert("RGBA") - # Assert - self.assertNotIn("transparency", im_rgba.info) - # https://github.com/python-pillow/Pillow/issues/2702 - self.assertIsNone(im_rgba.palette) +def test_trns_p(tmp_path): + im = hopper("P") + im.info["transparency"] = 0 - def test_trns_l(self): - im = hopper("L") - im.info["transparency"] = 128 + f = str(tmp_path / "temp.png") - f = self.tempfile("temp.png") + im_l = im.convert("L") + assert im_l.info["transparency"] == 0 # undone + im_l.save(f) - im_rgb = im.convert("RGB") - self.assertEqual(im_rgb.info["transparency"], (128, 128, 128)) # undone - im_rgb.save(f) + im_rgb = im.convert("RGB") + assert im_rgb.info["transparency"] == (0, 0, 0) # 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) +# ref https://github.com/python-pillow/Pillow/issues/664 - def test_trns_RGB(self): - im = hopper("RGB") - im.info["transparency"] = im.getpixel((0, 0)) - f = self.tempfile("temp.png") +def test_trns_p_rgba(): + # Arrange + im = hopper("P") + im.info["transparency"] = 128 - im_l = im.convert("L") - self.assertEqual(im_l.info["transparency"], im_l.getpixel((0, 0))) # undone - im_l.save(f) + # Act + im_rgba = im.convert("RGBA") - im_p = im.convert("P") - self.assertIn("transparency", im_p.info) - im_p.save(f) + # 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_rgba = im.convert("RGBA") - self.assertNotIn("transparency", im_rgba.info) - im_rgba.save(f) + im_p = im.convert("P") + assert "transparency" in 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) + 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(self): - # See https://github.com/python-pillow/Pillow/issues/2433 - im = Image.open("Tests/images/hopper.gif") + +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() - self.assertEqual(im.palette.mode, "RGBA") + assert im.palette.mode == "RGBA" im_p = im.convert("P") - # Should not raise ValueError: unrecognized raw mode - im_p.load() + # 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") +def test_p_la(): + im = hopper("RGBA") + alpha = hopper("L") + im.putalpha(alpha) - self.assert_image_similar(alpha, comparable, 5) + comparable = im.convert("P").convert("LA").getchannel("A") - 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") + 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 - self.assertRaises(ValueError, im.convert, mode="CMYK", matrix=matrix) + # Act / Assert + with pytest.raises(ValueError): + im.convert(mode="CMYK", matrix=matrix) - def test_matrix_wrong_mode(self): + +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("L") + 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, "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") - if converted_im.mode == "RGB": - self.assert_image_similar(converted_im, target, 3) - self.assertEqual(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) - - 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") + assert im.mode == "RGB" # Act - # Convert with an identity matrix - converted_im = im.convert(mode="RGB", matrix=identity_matrix) + # Convert an RGB image to the CIE XYZ colour space + converted_im = im.convert(mode=mode, matrix=matrix) # Assert - # No change - self.assert_image_equal(converted_im, im) + assert converted_im.mode == mode + assert converted_im.size == im.size + with Image.open("Tests/images/hopper-XYZ.png") as target: + if converted_im.mode == "RGB": + assert_image_similar(converted_im, target, 3) + assert converted_im.info["transparency"] == (105, 54, 4) + else: + assert_image_similar(converted_im, target.getchannel(0), 1) + assert converted_im.info["transparency"] == 105 + + matrix_convert("RGB") + matrix_convert("L") + + +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" + + # 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 653159e149f..ad0391dbe5e 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -2,40 +2,40 @@ from PIL import Image -from .helper import PillowTestCase, 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)) +from .helper import hopper + + +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 6a604f49462..3a2ce150d9e 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -1,103 +1,109 @@ +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): +def test_crop_zero(): - im = Image.new("RGB", (0, 0), "white") + im = Image.new("RGB", (0, 0), "white") - cropped = im.crop((0, 0, 0, 0)) - self.assertEqual(cropped.size, (0, 0)) + cropped = im.crop((0, 0, 0, 0)) + assert cropped.size == (0, 0) - cropped = im.crop((10, 10, 20, 20)) - self.assertEqual(cropped.size, (10, 10)) - self.assertEqual(cropped.getdata()[0], (0, 0, 0)) + cropped = im.crop((10, 10, 20, 20)) + assert cropped.size == (10, 10) + assert cropped.getdata()[0] == (0, 0, 0) - im = Image.new("RGB", (0, 0)) + im = Image.new("RGB", (0, 0)) - cropped = im.crop((10, 10, 20, 20)) - self.assertEqual(cropped.size, (10, 10)) - self.assertEqual(cropped.getdata()[2], (0, 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 5d92ee79792..8b4b447688f 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(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 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 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_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_several_drafts(self): - im = self.draft_roundtrip("L", (128, 128), None, (64, 64)) - im.draft(None, (64, 64)) +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 bc792bca6c4..876d676fe39 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 bbd10e6e59d..42f4f448d01 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,143 +1,155 @@ +import pytest from PIL import Image, ImageFilter -from .helper import PillowTestCase, 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) - - 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)) - - self.assertRaises(TypeError, filter, "hello") - - def test_crash(self): - - # crashes on small images - im = Image.new("RGB", (1, 1)) - im.filter(ImageFilter.SMOOTH) - - im = Image.new("RGB", (2, 2)) - im.filter(ImageFilter.SMOOTH) - - im = Image.new("RGB", (3, 3)) - 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 - - 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 - - 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)) - - def test_rankfilter_properties(self): - rankfilter = ImageFilter.RankFilter(1, 2) - - self.assertEqual(rankfilter.size, 1) - self.assertEqual(rankfilter.rank, 2) - - def test_builtinfilter_p(self): - builtinFilter = ImageFilter.BuiltinFilter() - - 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_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 - - 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)]), +from .helper import assert_image_equal, hopper + + +def test_sanity(): + def apply_filter(filter_to_apply): + for mode in ["L", "RGB", "CMYK"]: + im = hopper(mode) + out = im.filter(filter_to_apply) + assert out.mode == im.mode + assert out.size == im.size + + 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)) + + with pytest.raises(TypeError): + apply_filter("hello") + + +def test_crash(): + + # crashes on small images + im = Image.new("RGB", (1, 1)) + im.filter(ImageFilter.SMOOTH) + + im = Image.new("RGB", (2, 2)) + im.filter(ImageFilter.SMOOTH) + + im = Image.new("RGB", (3, 3)) + im.filter(ImageFilter.SMOOTH) + + +def test_modefilter(): + def modefilter(mode): + im = Image.new(mode, (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) + im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 + mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) + return mod, mod2 + + assert modefilter("1") == (4, 0) + assert modefilter("L") == (4, 0) + assert modefilter("P") == (4, 0) + assert modefilter("RGB") == ((4, 0, 0), (0, 0, 0)) + + +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 + + 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) + + +def test_rankfilter_properties(): + rankfilter = ImageFilter.RankFilter(1, 2) + + assert rankfilter.size == 1 + assert rankfilter.rank == 2 + + +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( # noqa: E127 + (3, 3), + # fmt: off + (-1, -1, 0, + -1, 0, 1, + 0, 1, 1), + # fmt: on + 0.3, ) - - 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"]: - self.assert_image_equal( - Image.merge(mode, source[: len(mode)]).filter(kernel), - Image.merge(mode, reference[: len(mode)]), + 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)]), + ) + + +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( # 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)]), + ) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 21d466d028f..faf94ac7794 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,14 +1,16 @@ +import pytest 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) + assert_image_equal(im1, im2) - def test_not_implemented(self): - self.assertRaises(NotImplementedError, Image.fromstring) + +def test_not_implemented(): + with pytest.raises(NotImplementedError): + Image.fromstring() diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index d7556a68002..170d49ae1b1 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -1,44 +1,59 @@ +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): - files_to_test = [ +@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 785b2ae4204..08fc12c1cf4 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 2df9f20f1b0..c86e33eb2fb 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 f1abf028725..e5b6a772462 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -1,67 +1,68 @@ -from .helper import PillowTestCase, 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 - - 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")) - - self.assertIsNone(getcolors("L", 128)) - self.assertEqual(getcolors("L", 1024), 255) - - self.assertIsNone(getcolors("RGB", 8192)) - self.assertEqual(getcolors("RGB", 16384), 10100) - self.assertEqual(getcolors("RGB", 100000), 10100) - - self.assertEqual(getcolors("RGBA", 16384), 10100) - self.assertEqual(getcolors("CMYK", 16384), 10100) - self.assertEqual(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)), - ] - - A = im.getcolors(maxcolors=2) - self.assertIsNone(A) - - A = im.getcolors(maxcolors=3) - A.sort() - self.assertEqual(A, expected) - - A = im.getcolors(maxcolors=4) - A.sort() - self.assertEqual(A, expected) - - A = im.getcolors(maxcolors=8) - A.sort() - self.assertEqual(A, expected) - - A = im.getcolors(maxcolors=16) - A.sort() - self.assertEqual(A, expected) +from .helper import hopper + + +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 + + 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 + + assert getcolors("L", 128) is None + assert getcolors("L", 1024) == 255 + + assert getcolors("RGB", 8192) is None + assert getcolors("RGB", 16384) == 10100 + assert getcolors("RGB", 100000) == 10100 + + assert getcolors("RGBA", 16384) == 10100 + assert getcolors("CMYK", 16384) == 10100 + assert getcolors("YCbCr", 16384) == 9329 + + +# -------------------------------------------------------------------- + + +def test_pack(): + # 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)), + ] + + A = im.getcolors(maxcolors=2) + assert A is None + + A = im.getcolors(maxcolors=3) + A.sort() + assert 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 d9bfcc7ddef..159efd78aa2 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): - data = hopper().getdata() +def test_sanity(): + data = hopper().getdata() - len(data) - list(data) + len(data) + list(data) - self.assertEqual(data[0], (20, 20, 70)) + assert 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)) - 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)) +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)) + + 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 1944b041c04..710794da426 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 3f0c46c46d3..746e63b1551 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,14 +1,9 @@ -from PIL._util import py3 +from .helper import hopper -from .helper import PillowTestCase, hopper +def test_sanity(): + im = hopper() + type_repr = repr(type(im.getim())) -class TestImageGetIm(PillowTestCase): - def test_sanity(self): - im = hopper() - type_repr = repr(type(im.getim())) - - if py3: - 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 7beeeff58ff..1818adca234 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 3b8bca64f0e..f65d40708b1 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 8d34658b85e..91e02973d04 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 2770126c41a..efb9a1452d8 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,36 +1,40 @@ 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") - im.close() - self.assertRaises(ValueError, im.load) - self.assertRaises(ValueError, im.getpixel, (0, 0)) - def test_contextmanager(self): - fn = None - with Image.open("Tests/images/hopper.gif") as im: - fn = im.fp.fileno() - os.fstat(fn) +def test_contextmanager(): + fn = None + with Image.open("Tests/images/hopper.gif") as im: + fn = im.fp.fileno() + os.fstat(fn) - self.assertRaises(OSError, os.fstat, fn) + with pytest.raises(OSError): + os.fstat(fn) - def test_contextmanager_non_exclusive_fp(self): - with open("Tests/images/hopper.gif", "rb") as fp: - with Image.open(fp): - pass - 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 e2395791674..7f92c226416 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(): - im = hopper() + 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 3139db664ed..1d3ca813550 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 @@ def assert_9points_image(self, im, expected): 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 @@ def test_image_solid(self): 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 @@ def test_color_solid(self): 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"): diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 56ed4648872..fe868b7c2fe 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() - self.assertRaises(ValueError, im.point, list(range(256))) - im.point(list(range(256)) * 3) - im.point(lambda x: x) +def test_sanity(): + im = hopper() - 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) + with pytest.raises(ValueError): + im.point(list(range(256))) + im.point(list(range(256)) * 3) + im.point(lambda x: x) - 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") + 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_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)] - out = im.point(lut, "F") +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") - int_lut = [x // 2 for x in range(256)] - self.assert_image_equal(out.convert("L"), im.point(int_lut, "L")) - def test_f_mode(self): - im = hopper("F") - self.assertRaises(ValueError, im.point, None) +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)] + + 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 6dc802598f7..e2dcead34c5 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) -class TestImagePutAlpha(PillowTestCase): - def test_interface(self): + im = Image.new("RGBA", (1, 1), (1, 2, 3)) + assert im.getpixel((0, 0)) == (1, 2, 3, 255) - im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 0)) + im.putalpha(Image.new("L", im.size, 4)) + assert im.getpixel((0, 0)) == (1, 2, 3, 4) - im = Image.new("RGBA", (1, 1), (1, 2, 3)) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 255)) + im.putalpha(5) + assert im.getpixel((0, 0)) == (1, 2, 3, 5) - im.putalpha(Image.new("L", im.size, 4)) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) - im.putalpha(5) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 5)) +def test_promote(): + im = Image.new("L", (1, 1), 1) + assert im.getpixel((0, 0)) == 1 - def test_promote(self): + im.putalpha(2) + assert im.mode == "LA" + assert im.getpixel((0, 0)) == (1, 2) - im = Image.new("L", (1, 1), 1) - self.assertEqual(im.getpixel((0, 0)), 1) + im = Image.new("P", (1, 1), 1) + assert im.getpixel((0, 0)) == 1 - im.putalpha(2) - self.assertEqual(im.mode, "LA") - self.assertEqual(im.getpixel((0, 0)), (1, 2)) + im.putalpha(2) + assert im.mode == "PA" + assert im.getpixel((0, 0)) == (1, 2) - im = Image.new("P", (1, 1), 1) - self.assertEqual(im.getpixel((0, 0)), 1) + im = Image.new("RGB", (1, 1), (1, 2, 3)) + assert im.getpixel((0, 0)) == (1, 2, 3) - im.putalpha(2) - self.assertEqual(im.mode, "PA") - self.assertEqual(im.getpixel((0, 0)), (1, 2)) + im.putalpha(4) + assert im.mode == "RGBA" + assert im.getpixel((0, 0)) == (1, 2, 3, 4) - 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(): + im = Image.new("RGB", (1, 1), (1, 2, 3)) + im.readonly = 1 - 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 a213fbf8841..54712fd6c9d 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -3,83 +3,87 @@ 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 68cfc4efeb6..7b05e88b6b8 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,33 +1,40 @@ +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 2be5b74fc39..96fa143a9e1 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,64 +1,74 @@ +import pytest from PIL import Image -from .helper import PillowTestCase, 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) - - image = hopper() - converted = image.quantize(palette=hopper("P")) - self.assert_image(converted, "P", converted.size) - self.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_rgba_quantize(self): - image = hopper("RGBA") - self.assertRaises(ValueError, image.quantize, method=0) - - self.assertEqual(image.quantize().convert().mode, "RGBA") - - 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") - - converted = image.quantize(dither=0, palette=palette) - self.assert_image(converted, "P", converted.size) - - 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) - - self.assertNotEqual(dither.tobytes(), nodither.tobytes()) +from .helper import assert_image, assert_image_similar, hopper + + +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")) + assert_image(converted, "P", converted.size) + assert_image_similar(converted.convert("RGB"), image, 60) + + +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_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_rgba_quantize(): + image = hopper("RGBA") + with pytest.raises(ValueError): + image.quantize(method=0) + + assert image.quantize().convert().mode == "RGBA" + + +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) + + +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 00000000000..729645a0b2b --- /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 + +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, "got mode %r, expected %r" % (a.mode, b.mode) + assert a.size == b.size, "got size %r, expected %r" % (a.size, 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 = "average pixel value difference {:.4f} > expected {:.4f} " + "for '{}' band".format(average_diff, max_average_diff, 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 + ), "max pixel value difference {} > expected {} for '{}' band".format( + last_diff, max_diff, 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) + + +@pytest.mark.skipif( + "jpeg2k_decoder" not in codecs, reason="JPEG 2000 support not available" +) +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 7d1dc009db2..764a3ca4907 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,21 +1,24 @@ -from __future__ import division, print_function - from contextlib import contextmanager +import pytest from PIL import Image, ImageDraw -from .helper import PillowTestCase, hopper, unittest +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") - xsize = 0x100000008 // 4 - ysize = 1000 # unimportant - with self.assertRaises(MemoryError): - # any resampling filter will do here - im.im.resize((xsize, ysize), Image.BILINEAR) + size_too_large = 0x100000008 // 4 + size_normal = 1000 # unimportant + for xsize, ysize in ( + (size_too_large, size_normal), + (size_normal, size_too_large), + ): + with pytest.raises(MemoryError): + # any resampling filter will do here + im.im.resize((xsize, ysize), Image.BILINEAR) def test_invalid_size(self): im = hopper() @@ -23,10 +26,10 @@ def test_invalid_size(self): # 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): @@ -36,10 +39,10 @@ def test_modify_after_resizing(self): # 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,7 +84,7 @@ def check_case(self, case, sample): message = "\nHave: \n{}\n\nExpected: \n{}".format( self.serialize_image(case), 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() @@ -209,8 +212,13 @@ def test_enlarge_lanczos(self): 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 CoreResampleConsistencyTest: def make_case(self, mode, fill): im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.LANCZOS), im.load()[0, 0] @@ -222,7 +230,7 @@ def run_case(self, case): 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) + assert px[x, y] == color, message def test_8u(self): im, color = self.make_case("RGB", (0, 64, 255)) @@ -245,7 +253,7 @@ def test_32f(self): self.run_case(self.make_case("F", 1.192093e-07)) -class CoreResampleAlphaCorrectTest(PillowTestCase): +class CoreResampleAlphaCorrectTest: def make_levels_case(self, mode): i = Image.new(mode, (256, 16)) px = i.load() @@ -260,14 +268,13 @@ def run_levels_case(self, i): 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 present in resized image. Only {} on {} line.".format( + len(used_colors), y ) - @unittest.skip("current implementation isn't precise enough") + @pytest.mark.skip("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)) @@ -276,7 +283,7 @@ def test_levels_rgba(self): 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.skip("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,7 +310,7 @@ def run_dirty_case(self, i, clean_pixel): message = "pixel at ({}, {}) is differ:\n{}\n{}".format( x, y, px[x, y], 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)) @@ -322,12 +329,12 @@ def test_dirty_pixels_la(self): self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255,)) -class CoreResamplePassesTest(PillowTestCase): +class CoreResamplePassesTest: @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") @@ -352,7 +359,7 @@ def test_box_horizontal(self): 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") @@ -362,10 +369,10 @@ def test_box_vertical(self): 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 CoreResampleCoefficientsTest: def test_reduce(self): test_color = 254 @@ -376,7 +383,7 @@ def test_reduce(self): 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 @@ -385,16 +392,16 @@ def test_nonzero_coefficients(self): 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 CoreResampleBoxTest: def test_wrong_arguments(self): im = hopper() for resample in ( @@ -410,24 +417,24 @@ def test_wrong_arguments(self): 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): @@ -447,33 +454,33 @@ def split_range(size, tiles): 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]: @@ -482,7 +489,7 @@ def test_formats(self): 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 @@ -494,13 +501,9 @@ def test_passthrough(self): ((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), ">>> {} {}".format(size, box)) def test_no_passthrough(self): # When resize is required @@ -512,15 +515,13 @@ def test_no_passthrough(self): ((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, ">>> {} {}".format(size, box) + ) def test_skip_horizontal(self): # Can skip resize for one dimension @@ -533,14 +534,15 @@ def test_skip_horizontal(self): ((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, + ">>> {} {} {}".format(size, box, flt), + ) def test_skip_vertical(self): # Can skip resize for one dimension @@ -553,11 +555,12 @@ def test_skip_vertical(self): ((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, + ">>> {} {} {}".format(size, box, flt), + ) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 7c35be570b7..ad4be135a00 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -3,12 +3,13 @@ """ 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 +30,23 @@ def test_nearest_mode(self): ]: # 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 +58,8 @@ def test_reduce_filters(self): 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 +71,8 @@ def test_enlarge_filters(self): 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 +114,7 @@ def test_endianness(self): 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,25 +126,126 @@ def test_enlarge_zero(self): 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)) resize(mode, (188, 214)) # Test unknown resampling filter - im = hopper() - self.assertRaises(ValueError, im.resize, (10, 10), "unknown") + with hopper() as im: + 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 9c62e7362cb..a41d850bb01 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 - - -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 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_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') - - target = Image.open("Tests/images/hopper_45.png") +from .helper import assert_image_equal, assert_image_similar, hopper + + +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(): + for mode in ("1", "P", "L", "RGB", "I", "F"): + im = hopper(mode) + rotate(im, mode, 45) + + +def test_angle(): + for angle in (0, 90, 180, 270): + with Image.open("Tests/images/test-card.png") as im: + rotate(im, im.mode, angle) + + +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 @@ def test_resample(self): ): 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(): + im = hopper() + im = im.rotate(45, center=(14, 14), resample=Image.BICUBIC) - def test_center_14(self): - im = hopper() - target = Image.open("Tests/images/hopper_45.png") + 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) - - self.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_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_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_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)) + im = im.rotate(45, translate=(5, 5), resample=Image.BICUBIC) + + assert_image_similar(im, target, 1) + + +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_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_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_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 a19878aaeb1..fbed276b8b7 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,64 +1,63 @@ -from PIL import Image - -from .helper import PillowTestCase, 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] - - 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)] - ) - - 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_open(self): - codecs = dir(Image.core) - - 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) +from PIL import Image, features + +from .helper import assert_image_equal, hopper + + +def test_split(): + def split(mode): + layers = hopper(mode).split() + return [(i.mode, i.size[0], i.size[1]) for i in layers] + + 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(): + def split_merge(mode): + return Image.merge(mode, hopper(mode).split()) + + assert_image_equal(hopper("1"), split_merge("1")) + assert_image_equal(hopper("L"), split_merge("L")) + assert_image_equal(hopper("I"), split_merge("I")) + assert_image_equal(hopper("F"), split_merge("F")) + assert_image_equal(hopper("P"), split_merge("P")) + assert_image_equal(hopper("RGB"), split_merge("RGB")) + assert_image_equal(hopper("RGBA"), split_merge("RGBA")) + assert_image_equal(hopper("CMYK"), split_merge("CMYK")) + assert_image_equal(hopper("YCbCr"), split_merge("YCbCr")) + + +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 bd7c98c2831..f4ed8e746ff 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,49 +1,124 @@ +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 - def test_no_resize(self): - # Check that draft() can resize the image to the destination size - im = Image.open("Tests/images/hopper.jpg") + 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_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)) - self.assertEqual(im.size, (64, 64)) + assert im.size == (64, 64) - # Test thumbnail(), where only draft() is necessary to resize the image - im = Image.open("Tests/images/hopper.jpg") + # 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 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) + + with Image.open("Tests/images/hopper.jpg") as im: + im.thumbnail((18, 18), Image.BICUBIC, reducing_gap=3.0) + + assert_image_equal(ref, im) diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index d7c879a25ba..178cfcef359 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): - self.assertRaises(ValueError, lambda: hopper().tobitmap()) +def test_sanity(): - im1 = hopper().convert("1") + with pytest.raises(ValueError): + hopper().tobitmap() - bitmap = im1.tobitmap() + im1 = hopper().convert("1") - self.assertIsInstance(bitmap, bytes) - self.assert_image_equal(im1, fromstring(bitmap)) + bitmap = im1.tobitmap() + + 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 d21ef7f6f98..31e1c0080c6 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 a0e54176a7b..3409d86f08d 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,14 +1,13 @@ import math -from PIL import Image +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): - from PIL import ImageTransform - im = Image.new("L", (100, 100)) seq = tuple(range(10)) @@ -22,6 +21,16 @@ def test_sanity(self): transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) im.transform((100, 100), transform) + def test_info(self): + comment = b"File written by Adobe Photoshop\xa8 4.0" + + 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) + assert new_im.info["comment"] == comment + def test_extent(self): im = hopper("RGB") (w, h) = im.size @@ -35,7 +44,7 @@ def test_extent(self): 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 @@ -53,7 +62,7 @@ def test_quad(self): (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 [ @@ -71,7 +80,7 @@ def test_fill(self): 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 @@ -96,13 +105,13 @@ def test_mesh(self): 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, @@ -118,7 +127,7 @@ def _test_alpha_premult(self, op): 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): @@ -156,24 +165,19 @@ def test_blank_fill(self): self.test_mesh() def test_missing_method_data(self): - im = hopper() - self.assertRaises(ValueError, im.transform, (100, 100), None) + with hopper() as im: + with pytest.raises(ValueError): + im.transform((100, 100), None) def test_unknown_resampling_filter(self): - im = hopper() - (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 hopper() as im: + (w, h) = im.size + for resample in (Image.BOX, "unknown"): + 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): @@ -206,7 +210,7 @@ def _test_rotate(self, deg, transpose): 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) @@ -236,7 +240,7 @@ def _test_resize(self, scale, epsilonscale): 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) @@ -269,7 +273,7 @@ def _test_translate(self, x, y, epsilonscale): 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 f5e8746ee40..a004434dae7 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -9,151 +9,154 @@ ) from . import helper -from .helper import PillowTestCase - - -class TestImageTranspose(PillowTestCase): - - hopper = { - mode: helper.hopper(mode).crop((0, 0, 121, 127)).copy() - for mode in ["L", "RGB", "I;16", "I;16L", "I;16B"] - } - - 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) - - 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(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 - 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 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]) - - 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))) - - for mode in self.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))) - - for mode in self.hopper: - transpose(mode) - - 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]) - - 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_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 - 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 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]) - - 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))) - - for mode in self.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) - - 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) - ) +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"] +} + + +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 + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2)) + + for mode in HOPPER: + transpose(mode) + + +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 + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, y - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1)) + + for mode in HOPPER: + transpose(mode) + + +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 + assert im.getpixel((1, 1)) == out.getpixel((1, x - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1)) + + for mode in HOPPER: + transpose(mode) + + +def test_rotate_180(): + def transpose(mode): + im = HOPPER[mode] + out = im.transpose(ROTATE_180) + assert out.mode == mode + assert out.size == im.size + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) + + for mode in HOPPER: + transpose(mode) + + +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] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2)) + + for mode in HOPPER: + transpose(mode) + + +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 + assert im.getpixel((1, 1)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2)) + + for mode in HOPPER: + transpose(mode) + + +def test_tranverse(): + def transpose(mode): + im = HOPPER[mode] + out = im.transpose(TRANSVERSE) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) + + for mode in HOPPER: + transpose(mode) + + +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 6f42a28dfd7..7d042cb9feb 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 @@ 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.darker(im1, 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 - # Assert - self.assert_image_equal(new, im2) - def test_darker_pixel(self): - # Arrange - im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") +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.assertEqual(new.getpixel((50, 50)), (240, 166, 0)) + # Assert + assert new.getpixel((50, 50)) == (240, 166, 0) - 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") - # Act - new = ImageChops.difference(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.getbbox(), (25, 25, 76, 76)) + # Act + new = ImageChops.difference(im1, im2) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) - def test_difference_pixel(self): - # Arrange - im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") + +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.getpixel((50, 50)), (240, 166, 128)) + # Assert + assert new.getpixel((50, 50)) == (240, 166, 128) - def test_duplicate(self): - # Arrange - im = hopper() - # Act - new = ImageChops.duplicate(im) +def test_duplicate(): + # Arrange + im = hopper() - # Assert - self.assert_image_equal(new, im) + # Act + new = ImageChops.duplicate(im) + + # Assert + 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") - # Act - new = ImageChops.lighter(im1, im2) +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 - self.assert_image_equal(new, im1) + assert_image_equal(new, im1) + - def test_lighter_pixel(self): - # Arrange - im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") +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.assertEqual(new.getpixel((50, 50)), (255, 255, 127)) + # Assert + assert new.getpixel((50, 50)) == (255, 255, 127) - 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") - # Act - new = ImageChops.multiply(im1, black) +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") + + # Act + new = ImageChops.multiply(im1, black) + + # Assert + assert_image_equal(new, 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 - # 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) +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: - 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) - # Act - new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) + # Assert + assert new.getbbox() == (25, 50, 76, 76) + assert new.getpixel((50, 50)) == GREEN + assert new.getpixel((50, 51)) == BLACK - # Assert - self.assertEqual(new.getbbox(), (0, 0, 100, 100)) - self.assertEqual(new.getpixel((50, 50)), (100, 202, 100)) - def test_subtract_clip(self): - # Arrange - im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") +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.getpixel((50, 50)), (0, 0, 127)) + # Assert + assert 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") - # Act - new = ImageChops.subtract_modulo(im1, im2) +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: + + # Act + new = ImageChops.subtract_modulo(im1, im2) + + # Assert + assert new.getbbox() == (25, 50, 76, 76) + assert new.getpixel((50, 50)) == GREEN + assert new.getpixel((50, 51)) == BLACK - # Assert - self.assertEqual(new.getbbox(), (25, 50, 76, 76)) - self.assertEqual(new.getpixel((50, 50)), GREEN) - self.assertEqual(new.getpixel((50, 51)), BLACK) - def test_subtract_modulo_no_clip(self): - # Arrange - im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_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.getpixel((50, 50)), (241, 167, 127)) - - 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) - - 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)) - - 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)) + # Assert + assert new.getpixel((50, 50)) == (241, 167, 127) + + +def test_soft_light(): + # Arrange + im1 = Image.open("Tests/images/hopper.png") + im2 = Image.open("Tests/images/hopper-XYZ.png") + + # Act + new = ImageChops.soft_light(im1, im2) + + # Assert + assert new.getpixel((64, 64)) == (163, 54, 32) + assert new.getpixel((15, 100)) == (1, 1, 3) + + +def test_hard_light(): + # Arrange + im1 = Image.open("Tests/images/hopper.png") + im2 = Image.open("Tests/images/hopper-XYZ.png") + + # 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 10465e73977..ac34a81064e 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,10 +1,12 @@ import datetime import os +import re from io import BytesIO +import pytest from PIL import Image, ImageMode -from .helper import PillowTestCase, hopper +from .helper import assert_image, assert_image_equal, assert_image_similar, hopper try: from PIL import ImageCms @@ -12,7 +14,7 @@ ImageCms.core.profile_open except ImportError: - # Skipped via setUp() + # Skipped via setup_module() pass @@ -20,589 +22,587 @@ 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+$", ImageCms.core.littlecms_version) - 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)) - t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") - i = ImageCms.applyTransform(hopper(), t) - self.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)) - i = hopper() + 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) - self.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) - - 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_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_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_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_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, - ) + 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)) + + 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)) + + # test PointTransform convenience API + hopper().point(t) + + +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(): + skip_missing() + assert ImageCms.getProfileInfo(SRGB).splitlines() == [ + "sRGB IEC61966-2-1 black scaled", + "", + "Copyright International Color Consortium, 2009", + "", + ] + + +def test_copyright(): + skip_missing() + assert ( + ImageCms.getProfileCopyright(SRGB).strip() + == "Copyright International Color Consortium, 2009" + ) + + +def test_manufacturer(): + skip_missing() + assert ImageCms.getProfileManufacturer(SRGB).strip() == "" - def test_extensions(self): - # extensions - i = Image.open("Tests/images/rgb.jpg") +def test_model(): + skip_missing() + assert ( + ImageCms.getProfileModel(SRGB).strip() + == "IEC 61966-2-1 Default RGB Colour Space - sRGB" + ) + + +def test_description(): + skip_missing() + assert ( + ImageCms.getProfileDescription(SRGB).strip() == "sRGB IEC61966-2-1 black scaled" + ) + + +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_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_extensions(): + # extensions + + 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", - ) + assert ( + ImageCms.getProfileName(p).strip() + == "IEC 61966-2.1 Default RGB colour space - sRGB" + ) + + +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")) + + # 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) + + +def test_display_profile(): + # try fetching the profile for the current display device + ImageCms.get_display_profile() - 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")) - # the procedural pyCMS API uses PyCMSError for all sorts of errors - self.assertRaises( - ImageCms.PyCMSError, ImageCms.profileToProfile, hopper(), "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_lab_color_profile(): + ImageCms.createProfile("LAB", 5000) + ImageCms.createProfile("LAB", 6500) - def test_display_profile(self): - # try fetching the profile for the current display device - ImageCms.get_display_profile() - def test_lab_color_profile(self): - ImageCms.createProfile("LAB", 5000) - ImageCms.createProfile("LAB", 6500) +def test_unsupported_color_space(): + with pytest.raises(ImageCms.PyCMSError): + ImageCms.createProfile("unsupported") - def test_unsupported_color_space(self): - self.assertRaises(ImageCms.PyCMSError, ImageCms.createProfile, "unsupported") - def test_invalid_color_temperature(self): - self.assertRaises(ImageCms.PyCMSError, ImageCms.createProfile, "LAB", "invalid") +def test_invalid_color_temperature(): + with pytest.raises(ImageCms.PyCMSError): + ImageCms.createProfile("LAB", "invalid") - def test_simple_lab(self): - i = Image.new("RGB", (10, 10), (128, 128, 128)) - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") +def test_simple_lab(): + i = Image.new("RGB", (10, 10), (128, 128, 128)) - i_lab = ImageCms.applyTransform(i, t) + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") - self.assertEqual(i_lab.mode, "LAB") + i_lab = ImageCms.applyTransform(i, t) - k = i_lab.getpixel((0, 0)) - # not a linear luminance map. so L != 128: - self.assertEqual(k, (137, 128, 128)) + assert i_lab.mode == "LAB" - l_data = i_lab.getdata(0) - a_data = i_lab.getdata(1) - b_data = i_lab.getdata(2) + k = i_lab.getpixel((0, 0)) + # not a linear luminance map. so L != 128: + assert k == (137, 128, 128) - self.assertEqual(list(l_data), [137] * 100) - self.assertEqual(list(a_data), [128] * 100) - self.assertEqual(list(b_data), [128] * 100) + l_data = i_lab.getdata(0) + a_data = i_lab.getdata(1) + b_data = i_lab.getdata(2) - def test_lab_color(self): - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") + assert list(l_data) == [137] * 100 + assert list(a_data) == [128] * 100 + assert list(b_data) == [128] * 100 - # 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)) - # i.save('temp.lab.tif') # visually verified vs PS. +def test_lab_color(): + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") - target = Image.open("Tests/images/hopper.Lab.tif") + # 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)) - self.assert_image_similar(i, target, 3.5) + # i.save('temp.lab.tif') # visually verified vs PS. - def test_lab_srgb(self): - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") + with Image.open("Tests/images/hopper.Lab.tif") as target: + assert_image_similar(i, target, 3.5) - img = Image.open("Tests/images/hopper.Lab.tif") +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): - i = Image.open("Tests/images/rgb.jpg") - 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_extended_information(self): - self.skip_missing() - o = ImageCms.getOpenProfile(SRGB) - p = o.profile - - 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 - ) - self.assertEqual(truncate_tuple(tup1), truncate_tuple(tup2)) +def test_profile_tobytes(): + with Image.open("Tests/images/rgb.jpg") as i: + p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) - self.assertEqual(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, - ( - ( - (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), - }, - ) - - 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, + p2 = ImageCms.getOpenProfile(BytesIO(p.tobytes())) + + # 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 test_extended_information(): + skip_missing() + o = ImageCms.getOpenProfile(SRGB) + p = o.profile + + 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.012054443359375, 0.0124969482421875, 0.01031494140625), - (0.34573304157549234, 0.35842450765864337, 0.0124969482421875), + (1.04791259765625, 0.0229339599609375, -0.050201416015625), + (0.02960205078125, 0.9904632568359375, -0.0170745849609375), + (-0.009246826171875, 0.0150604248046875, 0.7517852783203125), ), - ) - assert_truncated_tuple_equal( - p.media_white_point, ( - (0.964202880859375, 1.0, 0.8249053955078125), - (0.3457029219802284, 0.3585375327567059, 1.0), + (1.0267159024652783, 0.022470062342089134, 0.0229339599609375), + (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), + (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875), ), - ) - assert_truncated_tuple_equal( - (p.media_white_point_temperature,), (5000.722328847392,) - ) - self.assertEqual(p.model, "IEC 61966-2-1 Default RGB Colour Space - sRGB") - - self.assertIsNone(p.perceptual_rendering_intent_gamut) - - 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 ") - - 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) - - # p.color_space - helper_deprecated("color_space", "RGB") - - # p.pcs - helper_deprecated("pcs", "XYZ") - - # p.product_copyright - helper_deprecated( - "product_copyright", "Copyright International Color Consortium, 2009" - ) - - # p.product_desc - helper_deprecated("product_desc", "sRGB IEC61966-2-1 black scaled") - - # p.product_description - helper_deprecated("product_description", "sRGB IEC61966-2-1 black scaled") - - # 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")), + ), + ) + assert p.chromaticity is None + assert p.clut == { + 0: (False, False, True), + 1: (False, False, True), + 2: (False, False, True), + 3: (False, False, True), + } + + 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" + + assert p.perceptual_rendering_intent_gamut is None + + 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(): + skip_missing() + o = ImageCms.getOpenProfile(SRGB) + p = o.profile + + def helper_deprecated(attr, expected): + result = pytest.warns(DeprecationWarning, getattr, p, attr) + assert result == expected + + # p.color_space + helper_deprecated("color_space", "RGB") + + # p.pcs + helper_deprecated("pcs", "XYZ") + + # p.product_copyright + helper_deprecated( + "product_copyright", "Copyright International Color Consortium, 2009" + ) + + # p.product_desc + helper_deprecated("product_desc", "sRGB IEC61966-2-1 black scaled") + + # p.product_description + helper_deprecated("product_description", "sRGB IEC61966-2-1 black scaled") + + # 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(): + """ Profile init type safety + + prepatch, these would segfault, postpatch they should emit a typeerror + """ + + with pytest.raises(TypeError): + ImageCms.ImageCmsProfile(0).tobytes() + with pytest.raises(TypeError): + ImageCms.ImageCmsProfile(1).tobytes() + + +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 = [ # noqa: E131 + (-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 - - # 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], - ) + # 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 / 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 - ) - - # 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 + # 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 ) - self.assert_image_equal( - test_image.convert(dst_format[2]), reference_image - ) + # 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 + ) + + assert_image_equal(test_image.convert(dst_format[2]), reference_image) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index e4a7b7dfe00..d2fd07c8132 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -1,184 +1,192 @@ +import pytest from PIL import Image, ImageColor -from .helper import PillowTestCase - - -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")) - - # 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")) - - # 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")) - - # 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")) - - # 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") - - # 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 ") - - 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")) - - self.assertRaises(ValueError, ImageColor.getrgb, "black ") - - 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)")) - - # 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%)")) - - # 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)")) - - 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%)")) - - 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%)")) - - # alternate format - self.assertEqual( - ImageColor.getrgb("hsb(0,100%,50%)"), ImageColor.getrgb("hsv(0,100%,50%)") - ) - - # 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%)")) - - 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%)")) - - # 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%)") - ) - - # 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)") - - 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%)") - - self.assertRaises(ValueError, ImageColor.getrgb, "rgba(255,0,0)") - self.assertRaises(ValueError, ImageColor.getrgb, "rgba(255,0,0,0,0)") - - 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)") - - 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)") - - # 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") + +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 ") + + +def test_colormap(): + assert (0, 0, 0) == ImageColor.getrgb("black") + assert (255, 255, 255) == ImageColor.getrgb("white") + assert (255, 255, 255) == ImageColor.getrgb("WHITE") + + with pytest.raises(ValueError): + ImageColor.getrgb("black ") + + +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)") + + # 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%)") + + # 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)") + + 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%)") + + 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%)") + + # alternate format + assert ImageColor.getrgb("hsb(0,100%,50%)") == ImageColor.getrgb("hsv(0,100%,50%)") + + # 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%)") + + assert (253, 2, 2) == ImageColor.getrgb("hsv(0.1,99.2%,99.3%)") + assert (255, 0, 0) == ImageColor.getrgb("hsv(360.,100.0%,100%)") + + # 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%)") + + # 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% )") + + # wrong number of components + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(255,0)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(255,0,0,0)") + + 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%)") + + with pytest.raises(ValueError): + ImageColor.getrgb("rgba(255,0,0)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgba(255,0,0,0,0)") + + 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)") + + 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)") + + +# 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 + + assert (0, 255, 115) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGB") + Image.new("RGB", (1, 1), "white") + + 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") + + 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") + + 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") + + 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 bfc2c3c9cf4..f6eabb21a66 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,8 +1,14 @@ import os.path -from PIL import Image, ImageColor, ImageDraw, ImageFont, features +import pytest +from PIL import Image, ImageColor, ImageDraw, ImageFont -from .helper import PillowTestCase, hopper, unittest +from .helper import ( + assert_image_equal, + assert_image_similar, + hopper, + skip_unless_feature, +) BLACK = (0, 0, 0) WHITE = (255, 255, 255) @@ -29,862 +35,983 @@ KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] -HAS_FREETYPE = features.check("freetype2") +def test_sanity(): + im = hopper("RGB").copy() -class TestImageDraw(PillowTestCase): - def test_sanity(self): - im = hopper("RGB").copy() + draw = ImageDraw.ImageDraw(im) + draw = ImageDraw.Draw(im) - 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))) - draw.ellipse(list(range(4))) - draw.line(list(range(10))) - draw.polygon(list(range(100))) - draw.rectangle(list(range(4))) - def test_valueerror(self): - im = Image.open("Tests/images/chi.gif") +def test_valueerror(): + with Image.open("Tests/images/chi.gif") as im: draw = ImageDraw.Draw(im) draw.line((0, 0), fill=(0, 0, 0)) - def test_mode_mismatch(self): - im = hopper("RGB").copy() - self.assertRaises(ValueError, ImageDraw.ImageDraw, im, mode="L") +def test_mode_mismatch(): + im = hopper("RGB").copy() - def helper_arc(self, bbox, start, end): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) + with pytest.raises(ValueError): + ImageDraw.ImageDraw(im, mode="L") - # Act - draw.arc(bbox, start, end) - # Assert - self.assert_image_similar(im, Image.open("Tests/images/imagedraw_arc.png"), 1) +def helper_arc(bbox, start, end): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - def test_arc1(self): - self.helper_arc(BBOX1, 0, 180) - self.helper_arc(BBOX1, 0.5, 180.4) + # Act + draw.arc(bbox, start, end) - def test_arc2(self): - self.helper_arc(BBOX2, 0, 180) - self.helper_arc(BBOX2, 0.5, 180.4) + # Assert + assert_image_similar(im, Image.open("Tests/images/imagedraw_arc.png"), 1) - def test_arc_end_le_start(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - start = 270.5 - end = 0 - # Act - draw.arc(BBOX1, start=start, end=end) +def test_arc1(): + helper_arc(BBOX1, 0, 180) + helper_arc(BBOX1, 0.5, 180.4) - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_arc_end_le_start.png") - ) - 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 +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_similar( - im, Image.open("Tests/images/imagedraw_arc_no_loops.png"), 1 - ) +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_width(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_arc_width.png" + # Act + draw.arc(BBOX1, start=start, end=end) - # Act - draw.arc(BBOX1, 10, 260, width=5) + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_arc_end_le_start.png")) - # Assert - self.assert_image_similar(im, Image.open(expected), 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" +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, fill="yellow", width=100) + # Act + draw.arc(BBOX1, start=start, end=end) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + # Assert + assert_image_similar(im, Image.open("Tests/images/imagedraw_arc_no_loops.png"), 1) - 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" - # Act - draw.arc(BBOX1, 10, 260, fill="yellow", width=5) +def test_arc_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_arc_width.png" - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + # Act + draw.arc(BBOX1, 10, 260, width=5) - 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" + # Assert + assert_image_similar(im, Image.open(expected), 1) - # Act - draw.arc(BBOX1, 10, 259.5, 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) + expected = "Tests/images/imagedraw_arc_width_pieslice.png" - 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) + # Act + draw.arc(BBOX1, 10, 260, fill="yellow", width=100) + + # Assert + assert_image_similar(im, Image.open(expected), 1) + + +def test_arc_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_arc_width_fill.png" + + # Act + draw.arc(BBOX1, 10, 260, fill="yellow", width=5) + + # Assert + assert_image_similar(im, Image.open(expected), 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(im, Image.open(expected), 1) + + +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)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_chord_{}.png".format(mode) - # Act - draw.chord(bbox, start, end, fill="red", outline="yellow") +def helper_chord(mode, bbox, start, end): + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_chord_{}.png".format(mode) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + # Act + draw.chord(bbox, start, end, fill="red", outline="yellow") - 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) + # Assert + assert_image_similar(im, Image.open(expected), 1) - 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" +def test_chord1(): + for mode in ["RGB", "L"]: + helper_chord(mode, BBOX1, 0, 180) + helper_chord(mode, BBOX1, 0.5, 180.4) - # Act - draw.chord(BBOX1, 10, 260, outline="yellow", width=5) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) +def test_chord2(): + for mode in ["RGB", "L"]: + helper_chord(mode, BBOX2, 0, 180) + helper_chord(mode, BBOX2, 0.5, 180.4) - 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) +def test_chord_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_chord_width.png" - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + # Act + draw.chord(BBOX1, 10, 260, outline="yellow", width=5) - 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) + # Assert + assert_image_similar(im, Image.open(expected), 1) - # Act - draw.ellipse(bbox, fill="green", outline="blue") - # Assert - self.assert_image_similar(im, Image.open(expected), 1) +def test_chord_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_chord_width_fill.png" - def test_ellipse1(self): - for mode in ["RGB", "L"]: - self.helper_ellipse(mode, BBOX1) + # Act + draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5) - def test_ellipse2(self): - for mode in ["RGB", "L"]: - self.helper_ellipse(mode, BBOX2) + # Assert + assert_image_similar(im, Image.open(expected), 1) - def test_ellipse_edge(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - # Act - draw.ellipse(((0, 0), (W - 1, H)), fill="white") +def test_chord_zero_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1 - ) + # Act + draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=0) - 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)) + # Assert + with Image.open("Tests/images/imagedraw_chord_zero_width.png") as expected: + assert_image_equal(im, expected) - def test_ellipse_width(self): - # 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) +def helper_ellipse(mode, bbox): + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_ellipse_{}.png".format(mode) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + # Act + draw.ellipse(bbox, fill="green", outline="blue") - 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" + # Assert + assert_image_similar(im, Image.open(expected), 1) - # Act - draw.ellipse((25, 25, 475, 475), outline="blue", width=75) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) +def test_ellipse1(): + for mode in ["RGB", "L"]: + helper_ellipse(mode, BBOX1) - 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) +def test_ellipse2(): + for mode in ["RGB", "L"]: + helper_ellipse(mode, BBOX2) - # 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) +def test_ellipse_translucent(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") - # Act - draw.line(points, fill="yellow", width=2) + # Act + draw.ellipse(BBOX1, fill=(0, 255, 0, 127)) - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_line.png")) + # Assert + expected = Image.open("Tests/images/imagedraw_ellipse_translucent.png") + assert_image_similar(im, expected, 1) - def test_line1(self): - self.helper_line(POINTS1) - def test_line2(self): - self.helper_line(POINTS2) +def test_ellipse_edge(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - def test_shape1(self): - # Arrange - im = Image.new("RGB", (100, 100), "white") + # Act + draw.ellipse(((0, 0), (W - 1, H)), fill="white") + + # Assert + assert_image_similar(im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) + + +def test_ellipse_symmetric(): + for bbox in [(25, 25, 76, 76), (25, 25, 75, 75)]: + im = Image.new("RGB", (101, 101)) draw = ImageDraw.Draw(im) - x0, y0 = 5, 5 - x1, y1 = 5, 50 - x2, y2 = 95, 50 - x3, y3 = 95, 5 + draw.ellipse(bbox, fill="green", outline="blue") + assert_image_equal(im, im.transpose(Image.FLIP_LEFT_RIGHT)) - # 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) +def test_ellipse_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_ellipse_width.png" - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_shape1.png")) + # Act + draw.ellipse(BBOX1, outline="blue", width=5) - 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 + # Assert + assert_image_similar(im, Image.open(expected), 1) - # 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") +def test_ellipse_width_large(): + # Arrange + im = Image.new("RGB", (500, 500)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_ellipse_width_large.png" - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_shape2.png")) + # Act + draw.ellipse((25, 25, 475, 475), outline="blue", width=75) - def helper_pieslice(self, bbox, start, end): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) + # Assert + assert_image_similar(im, Image.open(expected), 1) - # 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_ellipse_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_ellipse_width_fill.png" - def test_pieslice1(self): - self.helper_pieslice(BBOX1, -90, 45) - self.helper_pieslice(BBOX1, -90.5, 45.4) + # Act + draw.ellipse(BBOX1, fill="green", outline="blue", width=5) - def test_pieslice2(self): - self.helper_pieslice(BBOX2, -90, 45) - self.helper_pieslice(BBOX2, -90.5, 45.4) + # Assert + assert_image_similar(im, Image.open(expected), 1) - 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) +def test_ellipse_zero_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + # Act + draw.ellipse(BBOX1, fill="green", outline="blue", width=0) - 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" + # Assert + with Image.open("Tests/images/imagedraw_ellipse_zero_width.png") as expected: + assert_image_equal(im, expected) - # Act - draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) +def helper_line(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - def helper_point(self, points): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) + # Act + draw.line(points, fill="yellow", width=2) - # Act - draw.point(points, fill="yellow") + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_line.png")) - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_point.png")) - def test_point1(self): - self.helper_point(POINTS1) +def test_line1(): + helper_line(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) +def test_line2(): + helper_line(POINTS2) - # Act - draw.polygon(points, fill="red", outline="blue") - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_polygon.png")) +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 - def test_polygon1(self): - self.helper_polygon(POINTS1) + # Act + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) - def test_polygon2(self): - self.helper_polygon(POINTS2) + draw.shape(s, fill=1) - 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) + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_shape1.png")) - # Act - draw.polygon(KITE_POINTS, fill="blue", outline="yellow") - # Assert - self.assert_image_equal(im, Image.open(expected)) +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 - def helper_rectangle(self, bbox): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) + # Act + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) - # Act - draw.rectangle(bbox, fill="black", outline="green") + draw.shape(s, outline="blue") - # Assert - self.assert_image_equal(im, Image.open("Tests/images/imagedraw_rectangle.png")) + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_shape2.png")) - def test_rectangle1(self): - self.helper_rectangle(BBOX1) - def test_rectangle2(self): - self.helper_rectangle(BBOX2) +def helper_pieslice(bbox, start, end): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - 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.pieslice(bbox, start, end, fill="white", outline="blue") - # Act - draw.rectangle(bbox, fill="orange") + # Assert + assert_image_similar(im, Image.open("Tests/images/imagedraw_pieslice.png"), 1) - # 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" +def test_pieslice1(): + helper_pieslice(BBOX1, -90, 45) + helper_pieslice(BBOX1, -90.5, 45.4) - # Act - draw.rectangle(BBOX1, outline="green", width=5) - # Assert - self.assert_image_equal(im, Image.open(expected)) +def test_pieslice2(): + helper_pieslice(BBOX2, -90, 45) + helper_pieslice(BBOX2, -90.5, 45.4) - 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) +def test_pieslice_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_pieslice_width.png" - # Assert - self.assert_image_equal(im, Image.open(expected)) + # Act + draw.pieslice(BBOX1, 10, 260, outline="blue", width=5) + + # Assert + assert_image_similar(im, Image.open(expected), 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(im, Image.open(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 helper_point(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - def test_rectangle_I16(self): + # 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("I;16", (W, H)) + im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_polygon_kite_{}.png".format(mode) # Act - draw.rectangle(BBOX1, fill="black", outline="green") + draw.polygon(KITE_POINTS, fill="blue", outline="yellow") # Assert - self.assert_image_equal( - im.convert("I"), Image.open("Tests/images/imagedraw_rectangle_I.png") - ) + 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) + expected = "Tests/images/imagedraw_big_rectangle.png" + + # Act + draw.rectangle(bbox, fill="orange") + + # Assert + assert_image_similar(im, Image.open(expected), 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) - def test_floodfill(self): - red = ImageColor.getrgb("red") + # Assert + assert_image_equal(im, Image.open(expected)) - 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) +def test_rectangle_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_rectangle_width_fill.png" - # Assert - expected = "Tests/images/imagedraw_floodfill_" + mode + ".png" - im_floodfill = Image.open(expected) - self.assert_image_equal(im, im_floodfill) + # Act + draw.rectangle(BBOX1, fill="blue", outline="green", width=5) - # Test that using the same colour does not change the image - ImageDraw.floodfill(im, centre_point, red) - self.assert_image_equal(im, im_floodfill) + # Assert + assert_image_equal(im, Image.open(expected)) - # 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_rectangle_zero_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - def test_floodfill_border(self): - # floodfill() is experimental + # 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)) + 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 + assert_image_equal(im, Image.open("Tests/images/imagedraw_floodfill2.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) - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_floodfill_not_negative.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_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" - ) - - 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) - - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - - def test_line_joint(self): - im = Image.new("RGB", (500, 325)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_line_joint_curve.png" - - # Act - xy = [ - (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), - ] - draw.line(xy, GRAY, 50, "curve") - - # Assert - self.assert_image_similar(im, Image.open(expected), 3) - - def test_textsize_empty_string(self): - # 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") - - @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)) + assert_image_equal(img, expected, "line oblique 45 inverted 3px wide B failed") + + +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) + expected = "Tests/images/imagedraw_wide_line_dot.png" + + # Act + draw.line([(50, 50), (50, 50)], width=3) + + # Assert + assert_image_similar(im, Image.open(expected), 1) + + +def test_line_joint(): + im = Image.new("RGB", (500, 325)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_line_joint_curve.png" + + # Act + xy = [ + (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), + ] + draw.line(xy, GRAY, 50, "curve") + + # Assert + assert_image_similar(im, Image.open(expected), 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", (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" - ) - - # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_stroke_multiline.png"), 3.3 - ) - - 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) - - # 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 = "Tests/images/imagedraw_outline_{}_{}.png".format( - operation, mode - ) - self.assert_image_similar(im, Image.open(expected), 1) + draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill) + + # Assert + assert_image_similar( + im, Image.open("Tests/images/imagedraw_stroke_" + suffix + ".png"), 3.1 + ) + + +@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) + + # Act + draw.text((10, 0), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0") + + # Assert + assert_image_similar( + im, Image.open("Tests/images/imagedraw_stroke_descender.png"), 6.76 + ) + + +@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( + (10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0" + ) + + # Assert + assert_image_similar( + im, Image.open("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 = "Tests/images/imagedraw_outline_{}_{}.png".format( + operation, mode + ) + assert_image_similar(im, Image.open(expected), 1) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 9ce472dd07a..72cbb79b8e5 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -1,8 +1,13 @@ import os.path -from PIL import Image, ImageDraw2, features +from PIL import Image, ImageDraw, ImageDraw2 -from .helper import PillowTestCase, hopper, unittest +from .helper import ( + assert_image_equal, + assert_image_similar, + hopper, + skip_unless_feature, +) BLACK = (0, 0, 0) WHITE = (255, 255, 255) @@ -29,194 +34,207 @@ 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 = "Tests/images/imagedraw_ellipse_{}.png".format(mode) - # 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)), 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 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 - # Pass in the pen as the brush parameter - draw.line(POINTS1, pen, brush) - - # 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") - - # Act - draw.polygon(points, pen, brush) - - # 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 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 - draw.rectangle(bbox, pen, brush) - - # 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)] - brush = ImageDraw2.Brush("orange") - draw = ImageDraw2.Draw(im) - expected = "Tests/images/imagedraw_big_rectangle.png" - - # Act - draw.rectangle(bbox, brush) - - # Assert - self.assert_image_similar(im, Image.open(expected), 1) - - @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) - - # 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) - - # Act - size = draw.textsize("ImageDraw2", font) - - # Assert - self.assertEqual(size[1], 12) - - @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) - - @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() - - # Assert - self.assert_image_equal(im, im2) +def helper_line(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("yellow", width=2) + + # Act + draw.line(points, pen) + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_line.png")) + + +def test_line1_pen(): + helper_line(POINTS1) + + +def test_line2_pen(): + helper_line(POINTS2) + + +def test_line_pen_as_brush(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = None + brush = ImageDraw2.Pen("yellow", width=2) + + # 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")) + + +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") + + # Act + draw.polygon(points, pen, brush) + + # 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 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 + draw.rectangle(bbox, pen, brush) + + # 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)] + 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 b2235853a63..32ab05f1772 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 - - -class TestImageEnhance(PillowTestCase): - def test_sanity(self): - - # 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): - - # 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") - - 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 - - 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 %s: %s" % (op, amount), - ) - - def test_alpha(self): - # Issue https://github.com/python-pillow/Pillow/issues/899 - # Is alpha preserved through image enhancement? - - 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, - ) +from .helper import assert_image_equal, hopper + + +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) + + +def test_crash(): + # crashes on small images + im = Image.new("RGB", (1, 1)) + ImageEnhance.Sharpness(im).enhance(0.5) + + +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) + + return im + + +def _check_alpha(im, original, op, amount): + assert im.getbands() == original.getbands() + assert_image_equal( + im.getchannel("A"), + original.getchannel("A"), + "Diff on {}: {}".format(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 a367f62dfae..883e3f5668e 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,29 +1,28 @@ from io import BytesIO -from PIL import EpsImagePlugin, Image, ImageFile - -from .helper import PillowTestCase, fromstring, hopper, tostring, unittest - -try: - from PIL import _webp - - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False - - -codecs = dir(Image.core) +import pytest +from PIL import EpsImagePlugin, Image, ImageFile, features + +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") @@ -39,23 +38,23 @@ def roundtrip(format): 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") @@ -66,25 +65,24 @@ def roundtrip(format): # 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(IOError): + 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: @@ -93,13 +91,14 @@ def test_safeblock(self): 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): + ImageFile.raise_ioerror(1) def test_raise_typeerror(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): parser = ImageFile.Parser() parser.feed(1) @@ -108,52 +107,42 @@ def test_negative_stride(self): input = f.read() p = ImageFile.Parser() p.feed(input) - with self.assertRaises(IOError): + with pytest.raises(IOError): 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") - - im = Image.open("Tests/images/truncated_image.png") - with self.assertRaises(IOError): - im.load() + with Image.open("Tests/images/truncated_image.png") as im: + with pytest.raises(IOError): + im.load() - # Test that the error is raised if loaded a second time - with self.assertRaises(IOError): - im.load() + # Test that the error is raised if loaded a second time + with pytest.raises(IOError): + 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(IOError): + 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): @@ -173,7 +162,7 @@ def _open(self): self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] -class TestPyDecoder(PillowTestCase): +class TestPyDecoder: def get_decoder(self): decoder = MockPyDecoder(None) @@ -192,12 +181,13 @@ def test_setimage(self): 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) @@ -208,10 +198,10 @@ def test_extents_none(self): 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) @@ -220,10 +210,12 @@ def test_negsize(self): 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) @@ -232,114 +224,107 @@ def test_oversize(self): 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()) - - 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) - reloaded = Image.open(out) - 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) - reloaded = Image.open(out) - 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(): - reloaded = Image.open(out) + assert im.format is None + assert im.get_format_mimetype() is None + + def test_exif_jpeg(self, tmp_path): + with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian + exif = im.getexif() + assert 258 not in exif + assert 40960 in exif + assert exif[40963] == 450 + assert exif[11] == "gThumb 3.0.1" + + out = str(tmp_path / "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() + assert reloaded_exif[258] == 8 + assert 40960 not in exif + assert reloaded_exif[40963] == 455 + assert exif[11] == "Pillow test" + + with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: # Big endian + exif = im.getexif() + assert 258 not in exif + assert 40962 in exif + assert exif[40963] == 200 + assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" + + out = str(tmp_path / "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() + assert reloaded_exif[258] == 8 + assert 40960 not in exif + assert reloaded_exif[40963] == 455 + assert exif[305] == "Pillow test" + + @skip_unless_feature("webp") + @skip_unless_feature("webp_anim") + def test_exif_webp(self, tmp_path): + with Image.open("Tests/images/hopper.webp") as im: + exif = im.getexif() + assert exif == {} + + out = str(tmp_path / "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() + assert reloaded_exif[258] == 8 + assert reloaded_exif[40963] == 455 + assert 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, tmp_path): + with Image.open("Tests/images/exif.png") as im: + exif = im.getexif() + assert exif == {274: 1} + + out = str(tmp_path / "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) - 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) - - reloaded = Image.open(out) - reloaded_exif = reloaded.getexif() - self.assertEqual(reloaded_exif, {258: 8, 40963: 455, 305: "Pillow test"}) + assert reloaded_exif == {258: 8, 40963: 455, 305: "Pillow test"} def test_exif_interop(self): - im = Image.open("Tests/images/flower.jpg") - exif = im.getexif() - self.assertEqual( - exif.get_ifd(0xA005), {1: "R98", 2: b"0100", 4097: 2272, 4098: 1704} - ) - - def test_exif_shared(self): - im = Image.open("Tests/images/exif.png") - exif = im.getexif() - self.assertIs(im.getexif(), exif) - - def test_exif_str(self): - im = Image.open("Tests/images/exif.png") - exif = im.getexif() - self.assertEqual(str(exif), "{274: 1}") + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + assert exif.get_ifd(0xA005) == { + 1: "R98", + 2: b"0100", + 4097: 2272, + 4098: 1704, + } diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 6a2d572a954..e93aff4b22d 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import copy import distutils.version import os @@ -7,47 +6,28 @@ import sys from io import BytesIO -from PIL import Image, ImageDraw, ImageFont, features +import pytest +from PIL import Image, ImageDraw, ImageFont -from .helper import PillowTestCase, unittest +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_image_similar_tofile, + is_pypy, + is_win32, + skip_unless_feature, +) 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") - - -class SimplePatcher(object): - 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): + +pytestmark = skip_unless_feature("freetype2") + + +class TestImageFont: LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC # Freetype has different metrics depending on the version. @@ -58,7 +38,8 @@ class TestImageFont(PillowTestCase): "Default": {"multiline": 0.5, "textsize": 0.5, "getters": (12, 16)}, } - def setUp(self): + @classmethod + def setup_class(self): freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) self.metrics = self.METRICS["Default"] @@ -86,23 +67,23 @@ def get_font(self): ) def test_sanity(self): - self.assertRegex(ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") + assert re.search(r"\d+\.\d+\.\d+$", ImageFont.core.freetype2_version) 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 +102,19 @@ def test_font_with_filelike(self): # 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_unicode_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("Unicode path could not be created") ImageFont.truetype(tempfile, FONT_SIZE) @@ -147,7 +129,7 @@ def test_unavailable_layout_engine(self): 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 +148,7 @@ def test_render_equal(self): 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_textsize_equal(self): im = Image.new(mode="RGB", size=(300, 100)) @@ -179,10 +161,10 @@ def test_textsize_equal(self): 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"]) def test_render_multiline(self): im = Image.new(mode="RGB", size=(300, 100)) @@ -196,12 +178,12 @@ def test_render_multiline(self): 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 +195,10 @@ def test_render_multiline_text(self): 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 +214,10 @@ def test_render_multiline_text(self): 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 +225,8 @@ def test_unknown_align(self): 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 +241,13 @@ def test_multiline_size(self): 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 +259,9 @@ def test_multiline_width(self): 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 +272,10 @@ def test_multiline_spacing(self): 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 +295,8 @@ def test_rotated_transposed_font(self): 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 +316,7 @@ def test_unrotated_transposed_font(self): 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 +329,7 @@ def test_rotated_transposed_font_get_mask(self): mask = transposed_font.getmask(text) # Assert - self.assertEqual(mask.size, (13, 108)) + assert mask.size == (13, 108) def test_unrotated_transposed_font_get_mask(self): # Arrange @@ -367,7 +342,7 @@ def test_unrotated_transposed_font_get_mask(self): mask = transposed_font.getmask(text) # Assert - self.assertEqual(mask.size, (108, 13)) + assert mask.size == (108, 13) def test_free_type_font_get_name(self): # Arrange @@ -377,7 +352,7 @@ def test_free_type_font_get_name(self): name = font.getname() # Assert - self.assertEqual(("FreeMono", "Regular"), name) + assert ("FreeMono", "Regular") == name def test_free_type_font_get_metrics(self): # Arrange @@ -387,9 +362,9 @@ def test_free_type_font_get_metrics(self): 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 +375,7 @@ def test_free_type_font_get_offset(self): 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 +386,22 @@ def test_free_type_font_get_mask(self): mask = font.getmask(text) # Assert - self.assertEqual(mask.size, (108, 13)) + assert mask.size == (108, 13) 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(IOError): + ImageFont.load_path(filename) + with pytest.raises(IOError): + 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(IOError): + ImageFont.truetype(f) def test_default_font(self): # Arrange @@ -432,20 +410,20 @@ def test_default_font(self): 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,22 +433,19 @@ def test_render_empty(self): 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): - font.getsize(u"’") + with pytest.raises(UnicodeEncodeError): + font.getsize("’") - @unittest.skipIf( - sys.version.startswith("2") or hasattr(sys, "pypy_translation_info"), - "requires CPython 3.3+", - ) + @pytest.mark.skipif(is_pypy(), reason="failing on PyPy") def test_unicode_extended(self): # issue #3777 - text = u"A\u278A\U0001F12B" + text = "A\u278A\U0001F12B" target = "Tests/images/unicode_extended.png" ttf = ImageFont.truetype( @@ -482,12 +457,13 @@ def test_unicode_extended(self): d = ImageDraw.Draw(img) d.text((10, 10), text, font=ttf) - self.assert_image_similar_tofile(img, target, self.metrics["multiline"]) + assert_image_similar_tofile(img, target, self.metrics["multiline"]) - 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: @@ -498,112 +474,108 @@ def loadable_font(filepath, size, index, encoding, *args, **kwargs): 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(sys.platform.startswith("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): - - 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" + 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"], ) - self._test_fake_loading_font(font_directory + "/Arial.ttf", "Arial") + ] + return [(path, [], ["some_random_font.ttf"])] + + 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 non-ttf fonts can be found without the - # extension - self._test_fake_loading_font( - font_directory + "/Single.otf", "Single" - ) + # Test that non-ttf fonts can be found without the + # extension + self._test_fake_loading_font( + monkeypatch, font_directory + "/Single.otf", "Single" + ) - # Test that ttf fonts are preferred if the extension is - # not specified - self._test_fake_loading_font( - font_directory + "/Duplicate.ttf", "Duplicate" - ) + # Test that ttf fonts are preferred if the extension is + # not specified + self._test_fake_loading_font( + monkeypatch, font_directory + "/Duplicate.ttf", "Duplicate" + ) - @unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") - def test_find_macos_font(self): + @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"): - - 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(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"])] + + 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 @@ -611,13 +583,13 @@ def test_getsize_stroke(self): # 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): @@ -625,89 +597,88 @@ def test_complex_font_settings(self): 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) + 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(IOError): + font.get_variation_names() + with pytest.raises(IOError): + 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 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") + with pytest.raises(NotImplementedError): + font.set_variation_by_name("Bold") return - self.assertRaises(IOError, font.set_variation_by_name, "Bold") + with pytest.raises(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 Image.open(path) as expected: + assert_image_similar(im, expected, epsilon) font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) _check_text(font, "Tests/images/variation_adobe.png", 11) @@ -726,18 +697,20 @@ def test_variation_set_by_axes(self): freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) if freetype < "2.9.1": - self.assertRaises(NotImplementedError, font.set_variation_by_axes, [100]) + with pytest.raises(NotImplementedError): + font.set_variation_by_axes([100]) return - self.assertRaises(IOError, font.set_variation_by_axes, [500, 50]) + with pytest.raises(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 Image.open(path) as expected: + assert_image_similar(im, expected, epsilon) font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) font.set_variation_by_axes([500, 50]) @@ -748,6 +721,6 @@ def _check_text(font, path, epsilon): _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) -@unittest.skipUnless(HAS_RAQM, "Raqm not Available") +@skip_unless_feature("raqm") class TestImageFont_RaqmLayout(TestImageFont): LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py index b7be8f72348..c4032d55dfc 100644 --- a/Tests/test_imagefont_bitmap.py +++ b/Tests/test_imagefont_bitmap.py @@ -1,6 +1,7 @@ +import pytest from PIL import Image, ImageDraw, ImageFont -from .helper import PillowTestCase, unittest +from .helper import assert_image_similar image_font_installed = True try: @@ -9,35 +10,29 @@ 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) +@pytest.mark.skipif(not image_font_installed, reason="Image font not installed") +def test_similar(): + 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) + # 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, + ) + assert_image_similar(im_bitmap, im_outline, 20) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 5b88f94cced..6d7a9c2f485 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,208 +1,207 @@ -# -*- coding: utf-8 -*- -from PIL import Image, ImageDraw, ImageFont, features +import pytest +from PIL import Image, ImageDraw, ImageFont -from .helper import PillowTestCase, unittest +from .helper import assert_image_similar, skip_unless_feature 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.3) - 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, 1.15) - 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) + 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": + pytest.skip("libraqm 0.7 or greater not available") - self.assert_image_similar(im, target_img, 12.4) + target = "Tests/images/test_direction_ttb_stroke.png" + with Image.open(target) as target_img: + 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) +def test_ligature_features(): + 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), "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) - liga_size = ttf.getsize("fi", features=["-liga"]) - self.assertEqual(liga_size, (13, 19)) + liga_size = ttf.getsize("fi", features=["-liga"]) + assert 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"]) +def test_kerning_features(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - target = "Tests/images/test_kerning_features.png" - target_img = Image.open(target) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "TeToAV", font=ttf, fill=500, features=["-kern"]) - self.assert_image_similar(im, target_img, 0.5) + target = "Tests/images/test_kerning_features.png" + with Image.open(target) as target_img: + 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) - draw.text( - (0, 0), - "اللغة العربية", - font=ttf, - fill=500, - features=["-fina", "-init", "-medi"], - ) +def test_arabictext_features(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - target = "Tests/images/test_arabictext_features.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, + features=["-fina", "-init", "-medi"], + ) - self.assert_image_similar(im, target_img, 0.5) + 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(self): - 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) +def test_x_max_and_y_offset(): + ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) - target = "Tests/images/test_x_max_and_y_offset.png" - target_img = Image.open(target) + im = Image.new(mode="RGB", size=(50, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "لح", font=ttf, fill=500) - self.assert_image_similar(im, target_img, 0.5) + 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(self): - 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") +def test_language(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - target = "Tests/images/test_language.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, language="sr") - self.assert_image_similar(im, target_img, 0.5) + target = "Tests/images/test_language.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index bea7f68b31c..7908477344c 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,62 +1,73 @@ import subprocess import sys -from .helper import PillowTestCase - -try: - from PIL import 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) - - 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") -[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") -$bmp = New-Object Drawing.Bitmap 200, 200 -[Windows.Forms.Clipboard]::SetImage($bmp)""" - ) - p.communicate() +import pytest +from PIL import Image, ImageGrab - im = ImageGrab.grabclipboard() - self.assert_image(im, im.mode, im.size) +from .helper import assert_image -except ImportError: +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) - class TestImageGrab(PillowTestCase): - def test_skip(self): - self.skipTest("ImportError") + im = ImageGrab.grab(bbox=(10, 20, 50, 80)) + assert_image(im, im.mode, (40, 60)) + @pytest.mark.skipif(not Image.core.HAVE_XCB, reason="requires XCB") + def test_grab_x11(self): + try: + if sys.platform not in ("win32", "darwin"): + im = ImageGrab.grab() + assert_image(im, im.mode, im.size) -class TestImageGrabImport(PillowTestCase): - def test_import(self): - # Arrange - exception = None + im2 = ImageGrab.grab(xdisplay="") + assert_image(im2, im2.mode, im2.size) + except IOError as e: + pytest.skip(str(e)) - # Act - try: - from PIL import ImageGrab + @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(IOError) as e: + ImageGrab.grab() + assert str(e.value).startswith("Pillow was built without XCB support") - ImageGrab.__name__ # dummy to prevent Pyflakes warning - except Exception as e: - exception = e + with pytest.raises(IOError) as e: + ImageGrab.grab(xdisplay="") + assert str(e.value).startswith("Pillow was built without XCB support") - # Assert - if sys.platform in ["win32", "darwin"]: - self.assertIsNone(exception) + @pytest.mark.skipif(not Image.core.HAVE_XCB, reason="requires XCB") + def test_grab_invalid_xdisplay(self): + with pytest.raises(IOError) 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() 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) diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index da41b3a1274..bc4f1af28ba 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,13 +1,9 @@ -from __future__ import print_function - from PIL import Image, ImageMath -from .helper import PillowTestCase - def pixel(im): if hasattr(im, "im"): - return "%s %r" % (im.mode, im.getpixel((0, 0))) + return "{} {!r}".format(im.mode, im.getpixel((0, 0))) else: if isinstance(im, int): return int(im) # hack to deal with booleans @@ -26,153 +22,168 @@ def pixel(im): 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_ops(self): - - self.assertEqual(pixel(ImageMath.eval("-A", images)), "I -1") - self.assertEqual(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") - - 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" - ) - - 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_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_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_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_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_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_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_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_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_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(): + assert pixel(ImageMath.eval("-A", images)) == "I -1" + assert pixel(ImageMath.eval("+B", images)) == "L 2" + + 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" + + 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(): + assert pixel(ImageMath.eval("not A", images)) == 0 + assert pixel(ImageMath.eval("A and B", images)) == "L 2" + assert pixel(ImageMath.eval("A or B", images)) == "L 1" + + +def test_convert(): + assert pixel(ImageMath.eval("convert(A+B, 'L')", images)) == "L 3" + assert pixel(ImageMath.eval("convert(A+B, '1')", images)) == "1 0" + assert pixel(ImageMath.eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" + + +def test_compare(): + assert pixel(ImageMath.eval("min(A, B)", images)) == "I 1" + assert pixel(ImageMath.eval("max(A, B)", images)) == "I 2" + assert pixel(ImageMath.eval("A == 1", images)) == "I 1" + assert pixel(ImageMath.eval("A == 2", images)) == "I 0" + + +def test_one_image_larger(): + assert pixel(ImageMath.eval("A+B", A=A2, B=B)) == "I 3" + assert pixel(ImageMath.eval("A+B", A=A, B=B2)) == "I 3" + + +def test_abs(): + assert pixel(ImageMath.eval("abs(A)", A=A)) == "I 1" + assert pixel(ImageMath.eval("abs(B)", B=B)) == "I 2" + + +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_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_bitwise_and(): + assert pixel(ImageMath.eval("Z&Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.eval("Z&A", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.eval("A&Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.eval("A&A", A=A, Z=Z)) == "I 1" + + +def test_bitwise_or(): + assert pixel(ImageMath.eval("Z|Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.eval("Z|A", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.eval("A|Z", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.eval("A|A", A=A, Z=Z)) == "I 1" + + +def test_bitwise_xor(): + 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 1492872b694..62119e4b3bc 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,328 +1,341 @@ # Test the ImageMorphology functionality +import pytest from PIL import Image, ImageMorph, _imagingmorph -from .helper import PillowTestCase, hopper - - -class MorphTests(PillowTestCase): - def setUp(self): - self.A = self.string_to_img( - """ - ....... - ....... - ..111.. - ..111.. - ..111.. - ....... - ....... - """ - ) - - 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) - ) - - 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 - - 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 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 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()) - - 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") - - # 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_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") +from .helper import assert_image_equal, hopper + + +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) + + return im + + +A = string_to_img( + """ + ....... + ....... + ..111.. + ..111.. + ..111.. + ....... + ....... + """ +) + + +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 img_string_normalize(im): + return img_to_string(string_to_img(im)) + + +def assert_img_equal(A, B): + assert img_to_string(A) == img_to_string(B) + + +def assert_img_equal_img_string(A, Bstring): + assert img_to_string(A) == img_string_normalize(Bstring) + + +def test_str_to_img(): + with Image.open("Tests/images/morph_a.png") as im: + assert_image_equal(A, im) + + +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("Tests/images/%s.lut" % op, "wb") as f: + f.write(lut) + - # Act - mop.set_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 + + lut = lb.build_lut() + with open("Tests/images/%s.lut" % op, "rb") as f: + assert lut == bytearray(f.read()) + + +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" + + +# 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 - # Assert - self.assertEqual(mop.lut, lut) - 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)) +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 self.assertRaises(RuntimeError): - _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) + with pytest.raises(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) + with pytest.raises(RuntimeError): + _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) - with self.assertRaises(RuntimeError): - _imagingmorph.match(bytes(lut), imrgb.im.id) + with pytest.raises(RuntimeError): + _imagingmorph.match(bytes(lut), imrgb.im.id) - # Should not raise - _imagingmorph.match(bytes(lut), iml.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 2cdbbe02f85..3d0afba9c58 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,287 +1,302 @@ -from PIL import Image, ImageOps +import pytest +from PIL import Image, ImageOps, features -from .helper import PillowTestCase, hopper +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_tuple_approx_equal, + hopper, +) -try: - from PIL import _webp - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False +class Deformer: + def getmesh(self, im): + x, y = im.size + return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] -class TestImageOps(PillowTestCase): - class Deformer(object): - def getmesh(self, im): - x, y = im.size - return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] +deformer = Deformer() - deformer = Deformer() - def test_sanity(self): +def test_sanity(): - ImageOps.autocontrast(hopper("L")) - ImageOps.autocontrast(hopper("RGB")) + ImageOps.autocontrast(hopper("L")) + ImageOps.autocontrast(hopper("RGB")) - ImageOps.autocontrast(hopper("L"), cutoff=10) - ImageOps.autocontrast(hopper("L"), ignore=[0, 255]) + ImageOps.autocontrast(hopper("L"), cutoff=10) + ImageOps.autocontrast(hopper("L"), ignore=[0, 255]) - ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) - ImageOps.colorize(hopper("L"), "black", "white") + ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) + ImageOps.colorize(hopper("L"), "black", "white") - ImageOps.pad(hopper("L"), (128, 128)) - ImageOps.pad(hopper("RGB"), (128, 128)) + ImageOps.pad(hopper("L"), (128, 128)) + ImageOps.pad(hopper("RGB"), (128, 128)) - ImageOps.crop(hopper("L"), 1) - ImageOps.crop(hopper("RGB"), 1) + ImageOps.crop(hopper("L"), 1) + ImageOps.crop(hopper("RGB"), 1) - ImageOps.deform(hopper("L"), self.deformer) - ImageOps.deform(hopper("RGB"), self.deformer) + ImageOps.deform(hopper("L"), deformer) + ImageOps.deform(hopper("RGB"), deformer) - ImageOps.equalize(hopper("L")) - ImageOps.equalize(hopper("RGB")) + ImageOps.equalize(hopper("L")) + ImageOps.equalize(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.expand(hopper("L"), 1) + ImageOps.expand(hopper("RGB"), 1) + ImageOps.expand(hopper("L"), 2, "blue") + ImageOps.expand(hopper("RGB"), 2, "blue") - ImageOps.fit(hopper("L"), (128, 128)) - ImageOps.fit(hopper("RGB"), (128, 128)) + ImageOps.fit(hopper("L"), (128, 128)) + ImageOps.fit(hopper("RGB"), (128, 128)) - ImageOps.flip(hopper("L")) - ImageOps.flip(hopper("RGB")) + ImageOps.flip(hopper("L")) + ImageOps.flip(hopper("RGB")) - ImageOps.grayscale(hopper("L")) - ImageOps.grayscale(hopper("RGB")) + ImageOps.grayscale(hopper("L")) + ImageOps.grayscale(hopper("RGB")) - ImageOps.invert(hopper("L")) - ImageOps.invert(hopper("RGB")) + ImageOps.invert(hopper("L")) + ImageOps.invert(hopper("RGB")) - ImageOps.mirror(hopper("L")) - ImageOps.mirror(hopper("RGB")) + ImageOps.mirror(hopper("L")) + ImageOps.mirror(hopper("RGB")) - ImageOps.posterize(hopper("L"), 4) - ImageOps.posterize(hopper("RGB"), 4) + ImageOps.posterize(hopper("L"), 4) + ImageOps.posterize(hopper("RGB"), 4) - ImageOps.solarize(hopper("L")) - ImageOps.solarize(hopper("RGB")) + ImageOps.solarize(hopper("L")) + ImageOps.solarize(hopper("RGB")) - ImageOps.exif_transpose(hopper("L")) - ImageOps.exif_transpose(hopper("RGB")) + ImageOps.exif_transpose(hopper("L")) + ImageOps.exif_transpose(hopper("RGB")) - 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)) - newimg = ImageOps.fit(hopper("RGB").resize((1, 100)), (35, 35)) - self.assertEqual(newimg.size, (35, 35)) +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) - newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) - self.assertEqual(newimg.size, (35, 35)) + newimg = ImageOps.fit(hopper("RGB").resize((1, 100)), (35, 35)) + assert 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)) + newimg = ImageOps.fit(hopper("RGB").resize((100, 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) - 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) +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) - 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_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 - i = hopper("RGB").resize((15, 16)) + 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 - ImageOps.equalize(i.convert("L")) - ImageOps.equalize(i.convert("P")) - ImageOps.equalize(i.convert("RGB")) + with Image.open( + "Tests/images/imageops_pad_" + label + "_" + str(i) + ".jpg" + ) as target: + assert_image_similar(new_im, target, 6) - def test_scale(self): - # Test the scaling function - i = hopper("L").resize((50, 50)) - with self.assertRaises(ValueError): - ImageOps.scale(i, -1) +def test_pil163(): + # Division by zero in equalize if < 255 pixels in image (@PIL163) - newimg = ImageOps.scale(i, 1) - self.assertEqual(newimg.size, (50, 50)) + i = hopper("RGB").resize((15, 16)) - newimg = ImageOps.scale(i, 2) - self.assertEqual(newimg.size, (100, 100)) + ImageOps.equalize(i.convert("L")) + ImageOps.equalize(i.convert("P")) + ImageOps.equalize(i.convert("RGB")) - newimg = ImageOps.scale(i, 0.5) - self.assertEqual(newimg.size, (25, 25)) - def test_colorize_2color(self): - # Test the colorizing function with 2-color functionality +def test_scale(): + # Test the scaling function + i = hopper("L").resize((50, 50)) - # Open test image (256px by 10px, black to white) - im = Image.open("Tests/images/bw_gradient.png") + 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") - - # 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", - ) - - 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") + # 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) + 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(): + # 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 - ) - - # 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", - ) - - 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") + # 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) + 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(): + # 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, - ) - - # 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", - ) - - 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) + # 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) + 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(): + 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) diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 8340c5f0dec..61f8dc2baee 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,85 +1,110 @@ +import pytest from PIL import Image, ImageFilter -from .helper import PillowTestCase - -im = Image.open("Tests/images/hopper.ppm") -snakes = Image.open("Tests/images/color_snakes.png") - - -class TestImageOpsUsm(PillowTestCase): - def test_filter_api(self): - - test_filter = ImageFilter.GaussianBlur(2.0) - i = 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) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - - def test_usm_formats(self): - - usm = ImageFilter.UnsharpMask - self.assertRaises(ValueError, im.convert("1").filter, usm) - im.convert("L").filter(usm) - self.assertRaises(ValueError, im.convert("I").filter, usm) - self.assertRaises(ValueError, im.convert("F").filter, usm) - im.convert("RGB").filter(usm) - im.convert("RGBA").filter(usm) - im.convert("CMYK").filter(usm) - self.assertRaises(ValueError, im.convert("YCbCr").filter, usm) - - def test_blur_formats(self): - - blur = ImageFilter.GaussianBlur - self.assertRaises(ValueError, im.convert("1").filter, blur) - blur(im.convert("L")) - self.assertRaises(ValueError, im.convert("I").filter, blur) - self.assertRaises(ValueError, im.convert("F").filter, blur) - im.convert("RGB").filter(blur) - im.convert("RGBA").filter(blur) - im.convert("CMYK").filter(blur) - self.assertRaises(ValueError, im.convert("YCbCr").filter, blur) - - def test_usm_accuracy(self): - - 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. - self.assertEqual(i.tobytes(), src.tobytes()) - - def test_blur_accuracy(self): - - 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), - ]: - self.assertGreaterEqual(i.im.getpixel((x, y))[c], 250) - # Fuzzy match. - - 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) + +@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() + + +def test_filter_api(test_images): + im = test_images["im"] + + test_filter = ImageFilter.GaussianBlur(2.0) + i = im.filter(test_filter) + assert i.mode == "RGB" + assert 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) + + +def test_usm_formats(test_images): + im = test_images["im"] + + 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(test_images): + im = test_images["im"] + + 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) + + +def test_usm_accuracy(test_images): + snakes = test_images["snakes"] + + 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 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 1297712ef50..29771cf0301 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,136 +1,149 @@ +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") - - p = ImagePalette.raw(p[1], p[0]) - self.assertIsInstance(p, ImagePalette.ImagePalette) - self.assertEqual(p.palette, palette.tobytes()) - - def test_make_linear_lut(self): - # Arrange - black = 0 - white = 255 - - # 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_make_linear_lut_not_yet_implemented(self): - # Update after FIXME - # Arrange - black = 1 - white = 255 - - # Act - self.assertRaises( - NotImplementedError, ImagePalette.make_linear_lut, black, white - ) - - def test_make_gamma_lut(self): - # Arrange - exp = 5 - - # Act - lut = ImagePalette.make_gamma_lut(exp) - - # 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) - - def test_rawmode_valueerrors(self): - # Arrange - palette = ImagePalette.raw("RGB", list(range(256)) * 3) - - # 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_getdata(): + # Arrange + data_in = list(range(256)) * 3 + palette = ImagePalette.ImagePalette("RGB", data_in) + + # Act + mode, data_out = palette.getdata() + + # Assert + assert mode == "RGB;L" + + +def test_rawmode_getdata(): + # Arrange + data_in = list(range(256)) * 3 + palette = ImagePalette.raw("RGB", data_in) + + # Act + rawmode, data_out = palette.getdata() + + # Assert + assert rawmode == "RGB" + assert data_in == data_out + + +def test_2bit_palette(tmp_path): + # issue #2258, 2 bit palettes are corrupted. + outfile = str(tmp_path / "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") + + with Image.open(outfile) as reloaded: + assert_image_equal(img, reloaded) + + +def test_invalid_palette(): + with pytest.raises(IOError): + ImagePalette.load("Tests/images/hopper.jpg") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index ed65d47c1ce..52af164551f 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,84 +1,79 @@ import array import struct +import pytest from PIL import Image, ImagePath -from PIL._util import py3 -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() # This fails due to the invalid malloc above, # and segfaults for i in range(200000): - if py3: - x[i] = b"0" * 16 - else: - x[i] = "0" * 16 + x[i] = b"0" * 16 class evil: diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index dc43e6bc7dc..d723690ef24 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,95 +1,60 @@ -import sys -import warnings - +import pytest from PIL import ImageQt -from .helper import PillowTestCase, hopper - -if sys.version_info.major >= 3: - from importlib import reload +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(object): - def setUp(self): - skip_if_qt_is_not_installed(self) - - def tearDown(self): - pass - -class PillowQPixmapTestCase(PillowQtTestCase): - def setUp(self): - PillowQtTestCase.setUp(self) +@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 == "4": - from PyQt4.QtGui import QGuiApplication - elif ImageQt.qt_version == "side": - from PySide.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): - PillowQtTestCase.tearDown(self) + @classmethod + def teardown_class(self): self.app.quit() - - -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 == "4": - from PyQt4.QtGui import qRgb - elif ImageQt.qt_version == "side": - from PySide.QtGui import qRgb - elif ImageQt.qt_version == "side2": - from PySide2.QtGui import qRgb - - self.assertEqual(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) - - 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)) - - def test_deprecated(self): - with warnings.catch_warnings(record=True) as w: - reload(ImageQt) - if ImageQt.qt_version in ["4", "side"]: - self.assertEqual(len(w), 1) - self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - else: - # No warning. - self.assertEqual(w, []) + self.app = None + + +@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 + + 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 + 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) + + +@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 1eea839da6c..b3fe9df97e1 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,98 +1,105 @@ +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): - im = Image.open("Tests/images/multipage.tiff") + +def test_iterator(): + 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) + assert i[index] == next(i) + with pytest.raises(IndexError): + i[index + 1] + with pytest.raises(StopIteration): + next(i) + - def test_iterator_min_frame(self): - im = Image.open("Tests/images/hopper.psd") +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): - self.assertEqual(i[index], next(i)) + assert i[index] == next(i) + - def _test_multipage_tiff(self): - im = Image.open("Tests/images/multipage.tiff") +def _test_multipage_tiff(): + with Image.open("Tests/images/multipage.tiff") as im: for index, frame in enumerate(ImageSequence.Iterator(im)): frame.load() - self.assertEqual(index, im.tell()) + assert index == im.tell() frame.convert("RGB") - def test_tiff(self): - self._test_multipage_tiff() - def test_libtiff(self): - codecs = dir(Image.core) +def test_tiff(): + _test_multipage_tiff() - if "libtiff_encoder" not in codecs or "libtiff_decoder" not in codecs: - self.skipTest("tiff support not available") - TiffImagePlugin.READ_LIBTIFF = True - self._test_multipage_tiff() - TiffImagePlugin.READ_LIBTIFF = False +@skip_unless_feature("libtiff") +def test_libtiff(): + TiffImagePlugin.READ_LIBTIFF = True + _test_multipage_tiff() + TiffImagePlugin.READ_LIBTIFF = False - def test_consecutive(self): - im = Image.open("Tests/images/multipage.tiff") + +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): - self.assert_image_equal(frame, firstFrame) + assert_image_equal(frame, firstFrame) break - def test_palette_mmap(self): - # Using mmap in ImageFile can require to reload the palette. - im = Image.open("Tests/images/multipage-mmap.tiff") + +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] - self.assertEqual(color1, color2) + assert color1 == color2 + - def test_all_frames(self): - # Test a single image - im = Image.open("Tests/images/iss634.gif") +def test_all_frames(): + # Test a single image + with Image.open("Tests/images/iss634.gif") as im: ims = ImageSequence.all_frames(im) - self.assertEqual(len(ims), 42) + assert len(ims) == 42 for i, im_frame in enumerate(ims): - self.assertFalse(im_frame is im) + assert im_frame is not im im.seek(i) - self.assert_image_equal(im, im_frame) + assert_image_equal(im, im_frame) # Test a series of images ims = ImageSequence.all_frames([im, hopper(), im]) - self.assertEqual(len(ims), 85) + 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) - self.assert_image_equal(im.rotate(90), im_frame) + assert_image_equal(im.rotate(90), im_frame) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 2f2620b7408..0d513a47dc4 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,52 +1,61 @@ +import pytest from PIL import Image, ImageShow -from .helper import PillowTestCase, hopper, on_ci, unittest +from .helper import hopper, is_win32, on_ci, on_github_actions -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) +def test_viewer_show(): + class TestViewer(ImageShow.Viewer): + methodCalled = False - for mode in ("1", "I;16", "LA", "RGB", "RGBA"): - im = hopper(mode) - self.assertTrue(ImageShow.show(im)) - self.assertTrue(viewer.methodCalled) + def show_image(self, image, **options): + self.methodCalled = True + return True - # Restore original state - ImageShow._viewers.pop(0) + viewer = TestViewer() + ImageShow.register(viewer, -1) - @unittest.skipUnless(on_ci(), "Only run on CIs") - def test_show(self): - for mode in ("1", "I;16", "LA", "RGB", "RGBA"): - im = hopper(mode) - self.assertTrue(ImageShow.show(im)) + for mode in ("1", "I;16", "LA", "RGB", "RGBA"): + with hopper() as im: + assert ImageShow.show(im) + assert viewer.methodCalled - def test_viewer(self): - viewer = ImageShow.Viewer() + # Restore original state + ImageShow._viewers.pop(0) - self.assertIsNone(viewer.get_format(None)) - self.assertRaises(NotImplementedError, viewer.get_command, None) +@pytest.mark.skipif( + not on_ci() or (is_win32() and on_github_actions()), + reason="Only run on CIs; hangs on Windows on GitHub Actions", +) +def test_show(): + for mode in ("1", "I;16", "LA", "RGB", "RGBA"): + im = hopper(mode) + assert ImageShow.show(im) - 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 d6c6a7a5594..6c70193ce67 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,55 +1,59 @@ +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 c397c84beb9..d13920c16d4 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,88 +1,91 @@ +import pytest from PIL import Image -from PIL._util import py3 -from .helper import PillowTestCase, hopper, unittest +from .helper import assert_image_equal, hopper try: from PIL import ImageTk - if py3: - import tkinter as tk - else: - import Tkinter as tk + import tkinter as tk + 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("TCL Error: %s" % 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)) +def test_photoimage(): + for mode in TK_MODES: + # test as image: + im = hopper(mode) - self.assertEqual(im_tk.width(), 100) - self.assertEqual(im_tk.height(), 100) + # this should not crash + im_tk = ImageTk.PhotoImage(im) - # reloaded = ImageTk.getimage(im_tk) - # self.assert_image_equal(reloaded, im) + assert im_tk.width() == im.width + assert im_tk.height() == im.height - def test_bitmapimage(self): - im = hopper("1") + reloaded = ImageTk.getimage(im_tk) + assert_image_equal(reloaded, im.convert("RGBA")) - # this should not crash - im_tk = ImageTk.BitmapImage(im) - self.assertEqual(im_tk.width(), im.width) - self.assertEqual(im_tk.height(), im.height) +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 92fcdc28d60..b1ddc75e95a 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,11 +1,10 @@ -import sys - +import pytest from PIL import ImageWin -from .helper import PillowTestCase, hopper, unittest +from .helper import hopper, is_win32 -class TestImageWin(PillowTestCase): +class TestImageWin: def test_sanity(self): dir(ImageWin) @@ -18,7 +17,7 @@ def test_hdc(self): dc2 = int(hdc) # Assert - self.assertEqual(dc2, 50) + assert dc2 == 50 def test_hwnd(self): # Arrange @@ -29,11 +28,11 @@ def test_hwnd(self): wnd2 = int(hwnd) # Assert - self.assertEqual(wnd2, 50) + assert wnd2 == 50 -@unittest.skipUnless(sys.platform.startswith("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 +41,7 @@ def test_dib_image(self): 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 +52,7 @@ def test_dib_mode_string(self): 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 +66,7 @@ def test_dib_paste(self): dib.paste(im) # Assert - self.assertEqual(dib.size, (128, 128)) + assert dib.size == (128, 128) def test_dib_paste_bbox(self): # Arrange @@ -82,7 +81,7 @@ def test_dib_paste_bbox(self): 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 +94,7 @@ def test_dib_frombytes_tobytes_roundtrip(self): 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 +103,4 @@ def test_dib_frombytes_tobytes_roundtrip(self): # 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 efa3753b951..a5cac96e42c 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,14 +1,13 @@ import ctypes -import sys from io import BytesIO from PIL import Image, ImageWin -from .helper import PillowTestCase, hopper +from .helper import hopper, is_win32 # see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652 -if sys.platform.startswith("win32"): +if is_win32(): import ctypes.wintypes class BITMAPFILEHEADER(ctypes.Structure): @@ -82,34 +81,33 @@ def serialize_dib(bi, pixels): 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) - - 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) - - imdib.expose(hdc) - bitmap = serialize_dib(hdr, pixels) - DeleteObject(dib) - DeleteDC(hdc) - - Image.open(BytesIO(bitmap)).save(opath) + 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 + + 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) + + Image.open(BytesIO(bitmap)).save(opath) diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 68e72bc4e5c..7115e62add6 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,36 +1,32 @@ +import pytest from PIL import Image -from .helper import PillowTestCase, unittest +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.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("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") - 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)) + 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) - 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("L") + with pytest.raises(ValueError): + im.im.setmode("RGBABCDE") diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 6178184bc1d..8e3c1fda925 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,13 +1,12 @@ import sys +import pytest from PIL import Image -from .helper import PillowTestCase - 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. @@ -18,9 +17,9 @@ def assert_pack(self, mode, rawmode, data, *pixels): if isinstance(data, int): data_len = data * len(pixels) - data = bytes(bytearray(range(1, data_len + 1))) + 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 +43,9 @@ def test_LA(self): 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,19 +221,19 @@ def test_F_float(self): ) -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. """ if isinstance(data, int): data_len = data * len(pixels) - data = bytes(bytearray(range(1, data_len + 1))) + data = bytes(range(1, data_len + 1)) 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 +271,9 @@ def test_LA(self): 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 +376,7 @@ def test_RGBA(self): 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 +391,7 @@ def test_RGBA(self): 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 +718,9 @@ def test_CMYK16(self): 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 cbec8b965ea..c5e54883d90 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,11 +1,8 @@ -from __future__ import print_function - import locale +import pytest from PIL import Image -from .helper import PillowTestCase, unittest - # ref https://github.com/python-pillow/Pillow/issues/272 # on windows, in polish locale: @@ -24,15 +21,16 @@ path = "Tests/images/hopper.jpg" -class TestLocale(PillowTestCase): - def test_sanity(self): - Image.open(path) - try: - locale.setlocale(locale.LC_ALL, "polish") - except locale.Error: - unittest.skip("Polish locale not available") +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: - Image.open(path) - finally: - locale.setlocale(locale.LC_ALL, (None, None)) + try: + with Image.open(path): + pass + finally: + locale.setlocale(locale.LC_ALL, (None, None)) diff --git a/Tests/test_main.py b/Tests/test_main.py index 847def83423..46ff63c4e97 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -1,33 +1,32 @@ -from __future__ import unicode_literals - import os import subprocess import sys -from unittest import TestCase -class TestMain(TestCase): - 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.assertEqual(lines[2], "-" * 68) - self.assertTrue(lines[3].startswith("Python modules loaded from ")) - self.assertTrue(lines[4].startswith("Binary modules loaded from ")) - self.assertEqual(lines[5], "-" * 68) - self.assertTrue(lines[6].startswith("Python ")) - 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) +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 + ) + assert jpeg in out diff --git a/Tests/test_map.py b/Tests/test_map.py index 3fc42651b30..b2f3ff2271b 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,37 +1,35 @@ import sys +import pytest from PIL import Image -from .helper import PillowTestCase, unittest +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(sys.platform.startswith("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. - im = Image.open("Tests/images/l2rgb_read.bmp") - with self.assertRaises((ValueError, MemoryError, IOError)): + # This image hits the offset test. + with Image.open("Tests/images/l2rgb_read.bmp") as im: + with pytest.raises((ValueError, MemoryError, IOError)): 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 b1cf2a23395..19e16f2c44b 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,107 +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): - original = hopper().resize((32, 32)).convert("I") +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, "got {!r} from mode {} at {}, expected {!r}".format( + p1, im1.mode, xy, p2 + ) - 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 %s at %s, expected %r" % (p1, im1.mode, xy, p2)), - ) - 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 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 basic(mode): + def basic(mode): - imIn = self.original.convert(mode) - self.verify(imIn) + imIn = original.convert(mode) + verify(imIn) - w, h = imIn.size + w, h = imIn.size - imOut = imIn.copy() - self.verify(imOut) # copy + imOut = imIn.copy() + verify(imOut) # copy - imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) - self.verify(imOut) # transform + imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) + verify(imOut) # transform - filename = self.tempfile("temp.im") - imIn.save(filename) + filename = str(tmp_path / "temp.im") + imIn.save(filename) - imOut = Image.open(filename) + with Image.open(filename) as imOut: - self.verify(imIn) - self.verify(imOut) + verify(imIn) + verify(imOut) - imOut = imIn.crop((0, 0, w, h)) - self.verify(imOut) + imOut = imIn.crop((0, 0, w, h)) + 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)) - self.verify(imIn) - self.verify(imOut) + verify(imIn) + verify(imOut) - imIn = Image.new(mode, (1, 1), 1) - self.assertEqual(imIn.getpixel((0, 0)), 1) + imIn = Image.new(mode, (1, 1), 1) + assert imIn.getpixel((0, 0)) == 1 - imIn.putpixel((0, 0), 2) - self.assertEqual(imIn.getpixel((0, 0)), 2) + imIn.putpixel((0, 0), 2) + assert 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) - self.assertEqual(imIn.getpixel((0, 0)), min(256, maximum)) + imIn = Image.new(mode, (1, 1), 256) + assert imIn.getpixel((0, 0)) == min(256, maximum) - imIn.putpixel((0, 0), 512) - self.assertEqual(imIn.getpixel((0, 0)), min(512, maximum)) + imIn.putpixel((0, 0), 512) + assert 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() - order = 1 if Image._ENDIAN == "<" else -1 +def test_tobytes(): + def tobytes(mode): + return Image.new(mode, (1, 1), 1).tobytes() - 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]) + order = 1 if Image._ENDIAN == "<" else -1 - def test_convert(self): + 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] - im = self.original.copy() - self.verify(im.convert("I;16")) - self.verify(im.convert("I;16").convert("L")) - self.verify(im.convert("I;16").convert("I")) +def test_convert(): - self.verify(im.convert("I;16B")) - self.verify(im.convert("I;16B").convert("L")) - self.verify(im.convert("I;16B").convert("I")) + im = original.copy() + + 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 872ecdbb600..30ab5132a31 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,223 +1,231 @@ -from __future__ import print_function - +import pytest from PIL import Image -from .helper import PillowTestCase, hopper, unittest - -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) - - self.assert_image(to_image(numpy.int16), "I", 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 64-bit integer formats - self.assertRaises(TypeError, to_image, numpy.uint64) - self.assertRaises(TypeError, to_image, numpy.int64) - - # 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) - - 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)) - - # 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) - - 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]) - - def test_16bit(self): - img = Image.open("Tests/images/16bit.cropped.tif") + 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 + 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 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 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 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) + + assert_image(to_image(numpy.int16), "I", 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) + + # Check 64-bit integer formats + with pytest.raises(TypeError): + to_image(numpy.uint64) + with pytest.raises(TypeError): + to_image(numpy.int64) + + # 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_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) + _test_img_equals_nparray(img, np_img) + assert 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_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_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(): + 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 42f47f169ab..ba48689883c 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,13 +1,12 @@ -from PIL import Image +import pickle -from .helper import PillowTestCase +from PIL import Image -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=0, mode=None): + # Arrange + with Image.open("Tests/images/hopper.jpg") as im: + filename = str(tmp_path / "temp.pkl") if mode: im = im.convert(mode) @@ -18,12 +17,13 @@ def helper_pickle_file(self, pickle, protocol=0, mode=None): 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=0, test_file="Tests/images/hopper.jpg", mode=None +): + with Image.open(test_file) as im: if mode: im = im.convert(mode) @@ -32,94 +32,59 @@ def helper_pickle_string( loaded_im = pickle.loads(dumped_string) # Assert - self.assertEqual(im, loaded_im) + assert im == loaded_im + + +def test_pickle_image(tmp_path): + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + helper_pickle_string(pickle, protocol) + helper_pickle_file(tmp_path, pickle, protocol) + + +def test_pickle_p_mode(): + # 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): + helper_pickle_string(pickle, protocol=protocol, test_file=test_file) - 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) - - 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_pa_mode(tmp_path): + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + helper_pickle_string(pickle, protocol, mode="PA") + helper_pickle_file(tmp_path, 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_l_mode(tmp_path): + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + helper_pickle_string(pickle, protocol, mode="L") + helper_pickle_file(tmp_path, 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) - - im.mode = "PA" - self.assertEqual(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") + # 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" + assert im == loaded_im diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 561df4ee602..31f0f493b25 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -1,63 +1,58 @@ import os import sys +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): +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 - # Based on Pillow tutorial, but there is no textsize: - # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript + # Arrange + tempfile = str(tmp_path / "temp.ps") + with open(tempfile, "wb") as fp: + # Act + ps = PSDraw.PSDraw(fp) + _create_document(ps) - # Arrange - tempfile = self.tempfile("temp.ps") - with open(tempfile, "wb") as fp: - # Act - ps = PSDraw.PSDraw(fp) - self._create_document(ps) + # Assert + # Check non-zero file was created + assert os.path.isfile(tempfile) + assert os.path.getsize(tempfile) > 0 - # Assert - # Check non-zero file was created - self.assertTrue(os.path.isfile(tempfile)) - self.assertGreater(os.path.getsize(tempfile), 0) - def test_stdout(self): - # Temporarily redirect stdout - try: - from cStringIO import StringIO - except ImportError: - from io import StringIO - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() +def test_stdout(): + # Temporarily redirect stdout + old_stdout = sys.stdout + sys.stdout = mystdout = StringIO() - ps = PSDraw.PSDraw() - self._create_document(ps) + ps = PSDraw.PSDraw() + _create_document(ps) - # Reset stdout - sys.stdout = old_stdout + # Reset stdout + sys.stdout = old_stdout - self.assertNotEqual(mystdout.getvalue(), "") + assert mystdout.getvalue() != "" diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 3455a502bc7..f4302350d70 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,30 +1,24 @@ +import pytest from PIL import __version__ -from .helper import PillowTestCase, unittest +pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") -try: - import pyroma -except ImportError: - pyroma = None +def test_pyroma(): + # Arrange + data = pyroma.projectdata.get_data(".") -@unittest.skipIf(pyroma is None, "Pyroma is not installed") -class TestPyroma(PillowTestCase): - def test_pyroma(self): - # 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. + assert 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. - self.assertEqual( - 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 1cff26d88db..cb1b385ec8b 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 d0c223b1ad3..4c98bf0b498 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,7 +1,11 @@ +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 @@ -9,75 +13,55 @@ try: from PyQt5 import QtGui from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication - - QT_VERSION = 5 except (ImportError, RuntimeError): - try: - from PySide2 import QtGui - from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication - - QT_VERSION = 5 - except (ImportError, RuntimeError): - try: - from PyQt4 import QtGui - from PyQt4.QtGui import QWidget, QHBoxLayout, QLabel, QApplication - - QT_VERSION = 4 - except (ImportError, RuntimeError): - from PySide import QtGui - from PySide.QtGui import QWidget, QHBoxLayout, QLabel, QApplication - - QT_VERSION = 4 - - -class TestToQImage(PillowQtTestCase, PillowTestCase): - def test_sanity(self): - for mode in ("RGB", "RGBA", "L", "P", "1"): - src = hopper(mode) - data = ImageQt.toqimage(src) - - self.assertIsInstance(data, QImage) - self.assertFalse(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) - - 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) - - # Check that it actually worked. - reloaded = Image.open(tempfile) - # Gray images appear to come back in palette mode. - # They're roughly equivalent - if QT_VERSION == 4 and mode == "L": - src = src.convert("P") - self.assert_image_equal(reloaded, src) - - def test_segfault(self): - app = QApplication([]) - ex = Example() - assert app # Silence warning - assert ex # Silence warning + from PySide2 import QtGui + from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication + + +def test_sanity(tmp_path): + for mode in ("RGB", "RGBA", "L", "P", "1"): + src = hopper(mode) + data = ImageQt.toqimage(src) + + assert isinstance(data, QImage) + assert not data.isNull() + + # reload directly from the qimage + rt = ImageQt.fromqimage(data) + if mode in ("L", "P", "1"): + assert_image_equal(rt, src.convert("RGB")) + else: + assert_image_equal(rt, src) + + 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 = str(tmp_path / "temp_{}.png".format(mode)) + data.save(tempfile) + + # Check that it actually worked. + with Image.open(tempfile) as reloaded: + assert_image_equal(reloaded, src) + + +def test_segfault(): + app = QApplication([]) + ex = Example() + assert app # Silence warning + assert ex # Silence warning if ImageQt.qt_is_installed: class Example(QWidget): def __init__(self): - super(Example, self).__init__() + super().__init__() img = hopper().resize((1000, 1000)) diff --git a/Tests/test_qt_image_toqpixmap.py b/Tests/test_qt_image_toqpixmap.py index 2c07f1bf53c..af281da697d 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 / "temp_{}.png".format(mode)) data.save(tempfile) diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py new file mode 100644 index 00000000000..6f3fc6f5d13 --- /dev/null +++ b/Tests/test_sgi_crash.py @@ -0,0 +1,14 @@ +#!/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(IOError): + im.load() diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 35a3dcfcd86..45c60fa107d 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,15 +1,9 @@ import shutil -import sys +import pytest from PIL import GifImagePlugin, Image, JpegImagePlugin -from .helper import ( - PillowTestCase, - cjpeg_available, - djpeg_available, - 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,35 +11,38 @@ test_filenames = ("temp_';", 'temp_";', "temp_'\"|", "temp_'\"||", "temp_'\"&&") -@unittest.skipIf(sys.platform.startswith("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 - Image.open(dest_file).load() + 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) - im = Image.open(src_file) - 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) - - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_filename_bmp_mode(self): - im = Image.open(TEST_GIF).convert("RGB") - self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) - - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_filename_l_mode(self): - im = Image.open(TEST_GIF).convert("L") - self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) + with Image.open(src_file) as im: + im.load_djpeg() + + @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) + + @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(tmp_path, im, GifImagePlugin._save_netpbm) + + @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") + def test_save_netpbm_filename_l_mode(self, tmp_path): + with Image.open(TEST_GIF) as im: + im = im.convert("L") + self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index f210c87377c..707284d7b4f 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,58 +1,60 @@ 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_nonetype(): + # Fails if the _delegate function doesn't return a valid function - def test_ifd_rational_save(self): - methods = (True, False) - if "libtiff_encoder" not in dir(Image.core): - methods = (False,) + 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 - for libtiff in methods: - TiffImagePlugin.WRITE_LIBTIFF = libtiff + assert xres and 1 + assert xres and yres - im = hopper() - out = self.tempfile("temp.tiff") - res = IFDRational(301, 1) - im.save(out, dpi=(res, res), compression="raw") - reloaded = Image.open(out) - self.assertEqual(float(IFDRational(301, 1)), float(reloaded.tag_v2[282])) +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 46dbd824aa9..720926e5377 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 5ec21a77cbc..3d88b947216 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,88 +1,72 @@ +import pytest from PIL import _util -from .helper import PillowTestCase, unittest +def test_is_path(): + # Arrange + fp = "filename.ext" -class TestUtil(PillowTestCase): - def test_is_string_type(self): - # Arrange - color = "red" + # Act + it_is = _util.isPath(fp) - # Act - it_is = _util.isStringType(color) + # Assert + assert it_is - # Assert - self.assertTrue(it_is) - def test_is_not_string_type(self): - # Arrange - color = (255, 0, 0) +@pytest.mark.skipif(not _util.py36, reason="os.path support for Paths added in 3.6") +def test_path_obj_is_path(): + # Arrange + from pathlib import Path - # Act - it_is_not = _util.isStringType(color) + test_path = Path("filename.ext") - # Assert - self.assertFalse(it_is_not) + # Act + it_is = _util.isPath(test_path) - def test_is_path(self): - # Arrange - fp = "filename.ext" + # Assert + assert it_is - # Act - it_is = _util.isPath(fp) - # Assert - self.assertTrue(it_is) +def test_is_not_path(tmp_path): + # Arrange + with (tmp_path / "temp.ext").open("w") as fp: + pass - @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 + # Act + it_is_not = _util.isPath(fp) - test_path = Path("filename.ext") + # Assert + assert not it_is_not - # Act - it_is = _util.isPath(test_path) - # Assert - self.assertTrue(it_is) +def test_is_directory(): + # Arrange + directory = "Tests" - def test_is_not_path(self): - # Arrange - filename = self.tempfile("temp.ext") - fp = open(filename, "w").close() + # Act + it_is = _util.isDirectory(directory) - # Act - it_is_not = _util.isPath(fp) + # Assert + assert it_is - # Assert - self.assertFalse(it_is_not) - def test_is_directory(self): - # Arrange - directory = "Tests" +def test_is_not_directory(): + # Arrange + text = "abc" - # Act - it_is = _util.isDirectory(directory) + # Act + it_is_not = _util.isDirectory(text) - # Assert - self.assertTrue(it_is) + # Assert + assert not it_is_not - def test_is_not_directory(self): - # Arrange - text = "abc" - # Act - it_is_not = _util.isDirectory(text) +def test_deferred_error(): + # Arrange - # Assert - self.assertFalse(it_is_not) + # Act + thing = _util.deferred_error(ValueError("Some error text")) - def test_deferred_error(self): - # Arrange - - # Act - thing = _util.deferred_error(ValueError("Some error text")) - - # Assert - self.assertRaises(ValueError, lambda: thing.some_attr) + # Assert + with pytest.raises(ValueError): + thing.some_attr diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 93a6c2db0cc..34197c14f80 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -1,13 +1,13 @@ from io import BytesIO -from PIL import Image, features +from PIL import Image -from .helper import PillowLeakTestCase, unittest +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 11eb8677929..00000000000 --- a/Tests/threaded_save.py +++ /dev/null @@ -1,59 +0,0 @@ -from __future__ import print_function - -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 1ac226c9d15..00000000000 --- a/Tests/versions.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import print_function - -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 f26f2c03775..00000000000 --- 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/languages/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-29-amd64' - name: 'fedora_29_amd64' - -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'fedora-30-amd64' - name: 'fedora_30_amd64' diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000000..f3afccc1caf --- /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 00000000000..e123cca80fe --- /dev/null +++ b/conftest.py @@ -0,0 +1 @@ +pytest_plugins = ["Tests.helper"] diff --git a/depends/README.rst b/depends/README.rst index 069d2b81f5b..ce88fa47ba2 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/alpine_Dockerfile b/depends/alpine_Dockerfile deleted file mode 100644 index 69bdf84f6cc..00000000000 --- a/depends/alpine_Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -# This is a sample Dockerfile to build Pillow on Alpine Linux -# with all/most of the dependencies working. -# -# Tcl/Tk isn't detecting -# Freetype has different metrics so tests are failing. -# sudo and bash are required for the webp build script. - -FROM alpine -USER root - -RUN apk --no-cache add python \ - build-base \ - python-dev \ - py-pip \ - # Pillow dependencies - jpeg-dev \ - zlib-dev \ - freetype-dev \ - lcms2-dev \ - openjpeg-dev \ - tiff-dev \ - tk-dev \ - tcl-dev - -# install from pip, without webp -#RUN LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "pip install Pillow" - -# install from git, run tests, including webp -RUN apk --no-cache add git \ - bash \ - sudo - -RUN git clone https://github.com/python-pillow/Pillow.git /Pillow -RUN pip install virtualenv && virtualenv /vpy && source /vpy/bin/activate && pip install nose - -RUN echo "#!/bin/bash" >> /test && \ - echo "source /vpy/bin/activate && cd /Pillow " >> test && \ - echo "pushd depends && ./install_webp.sh && ./install_imagequant.sh && popd" >> test && \ - echo "LIBRARY_PATH=/lib:/usr/lib make install && make test" >> test - -RUN chmod +x /test - -CMD ["/test"] diff --git a/depends/debian_8.2.sh b/depends/debian_8.2.sh deleted file mode 100755 index c4f72bf8ee5..00000000000 --- a/depends/debian_8.2.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Debian 8.2 -# for both system Pythons 2.7 and 3.4 -# -# Also works for Raspbian Jessie -# - -sudo apt-get -y install python-dev python-setuptools \ - python3-dev python-virtualenv cmake -sudo apt-get -y install libtiff5-dev libjpeg62-turbo-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ - python-tk python3-tk libharfbuzz-dev libfribidi-dev - -./install_openjpeg.sh -./install_imagequant.sh -./install_raqm.sh diff --git a/depends/fedora_23.sh b/depends/fedora_23.sh deleted file mode 100755 index 5bdcf7f174a..00000000000 --- a/depends/fedora_23.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Fedora 23 -# for both system Pythons 2.7 and 3.4 -# -# note that Fedora does ship packages for Pillow as python-pillow - -# this is a workaround for -# "gcc: error: /usr/lib/rpm/redhat/redhat-hardened-cc1: No such file or directory" -# errors when compiling. -sudo dnf install redhat-rpm-config - -sudo dnf install python-devel python3-devel python-virtualenv make gcc - -sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ - lcms2-devel libwebp-devel openjpeg2-devel tkinter python3-tkinter \ - tcl-devel tk-devel harfbuzz-devel fribidi-devel libraqm-devel \ No newline at end of file diff --git a/depends/freebsd_10.sh b/depends/freebsd_10.sh deleted file mode 100755 index 36d9c106913..00000000000 --- a/depends/freebsd_10.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Freebsd 10.x -# for both system Pythons 2.7 and 3.4 -# -sudo pkg install python2 python3 py27-pip py27-virtualenv wget cmake - -# Openjpeg fails badly using the openjpeg package. -# I can't find a python3.4 version of tkinter -sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 harfbuzz fribidi py27-tkinter - -./install_raqm_cmake.sh diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 0a98fc9d93d..36af34b54c3 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,19 +1,15 @@ #!/bin/bash # install extra test images -rm -rf test_images - # Use SVN to just fetch a single Git subdirectory -svn_checkout() +svn_export() { if [ ! -z $1 ]; then echo "" - echo "Retrying svn checkout..." + echo "Retrying svn export..." echo "" fi - svn checkout https://github.com/python-pillow/pillow-depends/trunk/test_images + svn export --force https://github.com/python-pillow/pillow-depends/trunk/test_images ../Tests/images } -svn_checkout || svn_checkout retry || svn_checkout retry || svn_checkout retry - -cp -r test_images/* ../Tests/images +svn_export || svn_export retry || svn_export retry || svn_export retry diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 0120dbc0b4a..1f2b677fde9 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.12.6 ./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 7ccd0930147..9b1882c43d5 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 f117790c5a0..1acc09c4463 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/depends/ubuntu_12.04.sh b/depends/ubuntu_12.04.sh deleted file mode 100755 index 9bfae43b0bc..00000000000 --- a/depends/ubuntu_12.04.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Ubuntu 12.04 -# for both system Pythons 2.7 and 3.2 -# - -sudo apt-get -y install python-dev python-setuptools \ - python3-dev python-virtualenv cmake -sudo apt-get install libtiff4-dev libjpeg8-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev tcl8.5-dev \ - tk8.5-dev python-tk python3-tk - - -./install_openjpeg.sh -./install_webp.sh -./install_imagequant.sh diff --git a/depends/ubuntu_14.04.sh b/depends/ubuntu_14.04.sh deleted file mode 100755 index f7d28fba71b..00000000000 --- a/depends/ubuntu_14.04.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Ubuntu 14.04 -# for both system Pythons 2.7 and 3.4 -# -sudo apt-get update -sudo apt-get -y install python-dev python-setuptools \ - python3-dev python-virtualenv cmake -sudo apt-get -y install libtiff5-dev libjpeg8-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ - python-tk python3-tk libharfbuzz-dev libfribidi-dev - -./install_openjpeg.sh -./install_imagequant.sh -./install_raqm.sh diff --git a/docs/COPYING b/docs/COPYING index a1e258129b6..ec2a5d8cbf3 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 1a912039ec9..d7c20bca4c1 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/about.rst b/docs/about.rst index 323593a3684..ce6537e148a 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -6,12 +6,13 @@ Goals The fork author's goal is to foster and support active development of PIL through: -- Continuous integration testing via `Travis CI`_ and `AppVeyor`_ +- Continuous integration testing via `Travis CI`_, `AppVeyor`_ and `GitHub Actions`_ - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ .. _Travis CI: https://travis-ci.org/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 .. _Python Package Index: https://pypi.org/project/Pillow/ diff --git a/docs/conf.py b/docs/conf.py index a9ca91de71c..d3431810020 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Pillow (PIL Fork) documentation build configuration file, created by # sphinx-quickstart on Sat Apr 4 07:54:11 2015. @@ -42,9 +41,9 @@ master_doc = "index" # General information about the project. -project = u"Pillow (PIL Fork)" -copyright = u"1995-2011 Fredrik Lundh, 2010-2019 Alex Clark and Contributors" -author = u"Fredrik Lundh, Alex Clark and Contributors" +project = "Pillow (PIL Fork)" +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 # |version| and |release|, also used in various other places throughout the @@ -130,7 +129,7 @@ # 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, @@ -220,8 +219,8 @@ ( master_doc, "PillowPILFork.tex", - u"Pillow (PIL Fork) Documentation", - u"Alex Clark", + "Pillow (PIL Fork) Documentation", + "Alex Clark", "manual", ) ] @@ -252,7 +251,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, "pillowpilfork", u"Pillow (PIL Fork) Documentation", [author], 1) + (master_doc, "pillowpilfork", "Pillow (PIL Fork) Documentation", [author], 1) ] # If true, show URL addresses after external links. @@ -268,7 +267,7 @@ ( master_doc, "PillowPILFork", - u"Pillow (PIL Fork) Documentation", + "Pillow (PIL Fork) Documentation", author, "PillowPILFork", "Pillow is the friendly PIL fork by Alex Clark and Contributors.", diff --git a/docs/deprecations.rst b/docs/deprecations.rst index f00f3e31fa5..227a5bc823e 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,16 +12,61 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. +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. + +ImageCms.CmsProfile attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 3.2.0 + +Some attributes in ``ImageCms.CmsProfile`` are deprecated. From 6.0.0, they issue a +``DeprecationWarning``: + +======================== =============================== +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`` +======================== =============================== + +Removed features +---------------- + +Deprecated features are only removed in major releases after an appropriate +period of deprecation has passed. + +Python 2.7 +~~~~~~~~~~ + +*Removed in version 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__ ~~~~~~~~~~~~~ -.. deprecated:: 6.1.0 +*Removed in version 7.0.0.* -Implicitly closing the image's underlying file in ``Image.__del__`` has been deprecated. +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. -Deprecated: +Previous method: .. code-block:: python @@ -35,37 +80,16 @@ Use instead: with Image.open("hopper.png") as im: im.save("out.jpg") -Python 2.7 -~~~~~~~~~~ - -.. deprecated:: 6.0.0 - -Python 2.7 reaches end-of-life on 2020-01-01. - -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. - -PyQt4 and PySide -~~~~~~~~~~~~~~~~ - -.. deprecated:: 6.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. - -Support for PyQt4 and PySide has been deprecated from ``ImageQt`` and will be removed in -a future version. Please upgrade to PyQt5 or PySide2. - PIL.*ImagePlugin.__version__ attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 6.0.0 +*Removed in version 7.0.0.* -The version constants of individual plugins have been deprecated and will be removed in -a future version. Use ``PIL.__version__`` instead. +The version constants of individual plugins have been removed. Use ``PIL.__version__`` +instead. =============================== ================================= ================================== -Deprecated Deprecated Deprecated +Removed Removed Removed =============================== ================================= ================================== ``BmpImagePlugin.__version__`` ``Jpeg2KImagePlugin.__version__`` ``PngImagePlugin.__version__`` ``CurImagePlugin.__version__`` ``JpegImagePlugin.__version__`` ``PpmImagePlugin.__version__`` @@ -81,52 +105,24 @@ Deprecated Deprecated Deprecated ``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` =============================== ================================= ================================== -Setting the size of TIFF images -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.3.0 - -Setting the image size of a TIFF image (eg. ``im.size = (256, 256)``) issues -a ``DeprecationWarning``: - -.. code-block:: none - - Setting the size of a TIFF image directly is deprecated, and will - be removed in a future version. Use the resize method instead. - -PILLOW_VERSION constant -~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.2.0 - -``PILLOW_VERSION`` has been deprecated and will be removed in 7.0.0. Use ``__version__`` -instead. +PyQt4 and PySide +~~~~~~~~~~~~~~~~ -ImageCms.CmsProfile attributes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*Removed in version 7.0.0.* -.. deprecated:: 3.2.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. -Some attributes in ``ImageCms.CmsProfile`` are deprecated. From 6.0.0, they issue a -``DeprecationWarning``: +Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 +or PySide2. -======================== =============================== -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`` -======================== =============================== +Setting the size of TIFF images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Removed features ----------------- +*Removed in version 7.0.0.* -Deprecated features are only removed in major releases after an appropriate -period of deprecation has passed. +Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws +an error. Use ``Image.resize`` instead. VERSION constant ~~~~~~~~~~~~~~~~ @@ -162,4 +158,4 @@ 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`` 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 be493a3160a..45f63f1647d 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. +.. _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. + +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 ^^^ @@ -813,8 +937,9 @@ 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`, 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, the +following options are available when the ``save_all`` argument is present and +true. **append_images** A list of images to append as additional frames. Each of the @@ -1018,6 +1143,43 @@ 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 ^^^ @@ -1175,35 +1337,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/tutorial.rst b/docs/handbook/tutorial.rst index 16090b040c2..86840261578 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -18,7 +18,6 @@ in the :py:mod:`~PIL.Image` module:: If successful, this function returns an :py:class:`~PIL.Image.Image` object. You can now use instance attributes to examine the file contents:: - >>> from __future__ import print_function >>> print(im.format, im.size, im.mode) PPM (512, 512) RGB @@ -67,7 +66,6 @@ Convert files to JPEG :: - from __future__ import print_function import os, sys from PIL import Image @@ -76,7 +74,8 @@ Convert files to JPEG outfile = f + ".jpg" if infile != outfile: try: - Image.open(infile).save(outfile) + with Image.open(infile) as im: + im.save(outfile) except IOError: print("cannot convert", infile) @@ -89,7 +88,6 @@ Create JPEG thumbnails :: - from __future__ import print_function import os, sys from PIL import Image @@ -99,9 +97,9 @@ Create JPEG thumbnails outfile = os.path.splitext(infile)[0] + ".thumbnail" if infile != outfile: try: - im = Image.open(infile) - im.thumbnail(size) - im.save(outfile, "JPEG") + with Image.open(infile) as im: + im.thumbnail(size) + im.save(outfile, "JPEG") except IOError: print("cannot create thumbnail for", infile) @@ -120,7 +118,6 @@ Identify Image Files :: - from __future__ import print_function import sys from PIL import Image @@ -267,7 +264,8 @@ Converting between modes :: from PIL import Image - im = Image.open("hopper.ppm").convert("L") + with Image.open("hopper.ppm") as im: + im = im.convert("L") The library supports transformations between each supported mode and the “L” and “RGB” modes. To convert between other modes, you may have to use an @@ -383,15 +381,15 @@ Reading sequences from PIL import Image - im = Image.open("animation.gif") - im.seek(1) # skip to the second frame + with Image.open("animation.gif") as im: + im.seek(1) # skip to the second frame - try: - while 1: - im.seek(im.tell()+1) - # do something to im - except EOFError: - pass # end of sequence + try: + while 1: + im.seek(im.tell()+1) + # do something to im + except EOFError: + pass # end of sequence As seen in this example, you’ll get an :py:exc:`EOFError` exception when the sequence ends. @@ -422,32 +420,34 @@ Drawing Postscript from PIL import Image from PIL import PSDraw - im = Image.open("hopper.ppm") - title = "hopper" - box = (1*72, 2*72, 7*72, 10*72) # in points + with Image.open("hopper.ppm") as im: + title = "hopper" + box = (1*72, 2*72, 7*72, 10*72) # in points - ps = PSDraw.PSDraw() # default is sys.stdout - ps.begin_document(title) + ps = PSDraw.PSDraw() # default is sys.stdout + ps.begin_document(title) - # draw the image (75 dpi) - ps.image(box, im, 75) - ps.rectangle(box) + # draw the image (75 dpi) + ps.image(box, im, 75) + ps.rectangle(box) - # draw title - ps.setfont("HelveticaNarrow-Bold", 36) - ps.text((3*72, 4*72), title) + # draw title + ps.setfont("HelveticaNarrow-Bold", 36) + ps.text((3*72, 4*72), title) - ps.end_document() + ps.end_document() 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:: +simply pass it the filename as an argument. ``Image.open()`` can be used as a +context manager:: from PIL import Image - im = Image.open("hopper.ppm") + with Image.open("hopper.ppm") as im: + ... If everything goes well, the result is an :py:class:`PIL.Image.Image` object. Otherwise, an :exc:`IOError` exception is raised. @@ -465,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 @@ -513,12 +513,12 @@ This is only available for JPEG and MPO files. :: from PIL import Image - from __future__ import print_function - im = Image.open(file) - print("original =", im.mode, im.size) - im.draft("L", (100, 100)) - print("draft =", im.mode, im.size) + with Image.open(file) as im: + print("original =", im.mode, im.size) + + im.draft("L", (100, 100)) + print("draft =", im.mode, im.size) This prints something like:: diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst index 0763109ab4f..58e2bccc51c 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-file-decoder.rst @@ -52,7 +52,6 @@ true color. **SpamImagePlugin.py**:: from PIL import Image, ImageFile - import string class SpamImageFile(ImageFile.ImageFile): @@ -63,10 +62,10 @@ true color. # check header header = self.fp.read(128) - if header[:4] != "SPAM": - raise SyntaxError, "not a SPAM file" + if header[:4] != b"SPAM": + raise SyntaxError("not a SPAM file") - header = string.split(header) + header = header.split() # size in pixels (width, height) self._size = int(header[1]), int(header[2]) @@ -80,7 +79,7 @@ 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 = [ diff --git a/docs/index.rst b/docs/index.rst index 034da6eed45..4f16ba960bb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,10 +3,7 @@ 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 @@ -20,10 +17,38 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more = 7.0.0 | | | | | | | | Yes | Yes | Yes | -+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ + ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|**Python** |**3.8**|**3.7**|**3.6**|**3.5**|**3.4**|**3.3**|**3.2**|**2.7**|**2.6**|**2.5**|**2.4**| ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow >= 7 | 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 ------------------ @@ -44,18 +47,20 @@ 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 macOS Installation @@ -63,10 +68,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 ^^^^^^^^^^^^^^^^^^ @@ -76,7 +82,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 @@ -89,18 +96,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 @@ -123,16 +129,15 @@ External Libraries .. note:: - There are scripts to install the dependencies for some operating - systems included in the ``depends`` directory. Also see the - Dockerfiles in our `docker images repo - `_. + There are Dockerfiles in our `Docker images repo + `_ to install the + dependencies for some operating systems. 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. @@ -144,7 +149,7 @@ 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 @@ -169,12 +174,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.12.6** * 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. @@ -188,11 +191,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 @@ -202,7 +208,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 @@ -222,14 +228,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. @@ -246,11 +252,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 @@ -266,21 +272,22 @@ 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 ^^^^^^^^^^^^^^^^^^^ @@ -294,17 +301,13 @@ 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. @@ -317,35 +320,27 @@ 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:: - $ 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:: - $ 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 exact distribution. @@ -360,8 +355,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. @@ -380,40 +375,44 @@ 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 29 | 2.7, 3.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Fedora 30 | 2.7, 3.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| macOS 10.13 High Sierra* | 2.7, 3.5, 3.6, 3.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Ubuntu Linux 16.04 LTS | 2.7, 3.5, 3.6, 3.7, |x86-64 | -| | PyPy, PyPy3 | | -+----------------------------------+-------------------------------+-----------------------+ -| Windows Server 2012 R2 | 2.7, 3.5, 3.6, 3.7 |x86, x86-64 | -| +-------------------------------+-----------------------+ -| | PyPy, 3.7/MinGW |x86 | -+----------------------------------+-------------------------------+-----------------------+ - -\* macOS CI is not run for every commit, but is run for every release. ++----------------------------------+--------------------------+-----------------------+ +|**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 9 Stretch | 3.5 |x86 | ++----------------------------------+--------------------------+-----------------------+ +| Debian 10 Buster | 3.7 |x86 | ++----------------------------------+--------------------------+-----------------------+ +| Fedora 30 | 3.7 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Fedora 31 | 3.7 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| macOS 10.15 Catalina | 3.5, 3.6, 3.7, 3.8, PyPy3|x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Ubuntu Linux 16.04 LTS | 3.5, 3.6, 3.7, 3.8, PyPy3|x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Windows Server 2012 R2 | 3.5, 3.8 |x86, x86-64 | +| +--------------------------+-----------------------+ +| | PyPy3, 3.7/MinGW |x86 | ++----------------------------------+--------------------------+-----------------------+ +| Windows Server 2019 | 3.5, 3.6, 3.7, 3.8 |x86, x86-64 | +| +--------------------------+-----------------------+ +| | PyPy3 |x86 | ++----------------------------------+--------------------------+-----------------------+ + Other Platforms ^^^^^^^^^^^^^^^ @@ -428,6 +427,8 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+------------------------------+--------------------------------+-----------------------+ |**Operating system** |**Tested Python versions** |**Latest tested Pillow version**|**Tested processors** | +----------------------------------+------------------------------+--------------------------------+-----------------------+ +| macOS 10.15 Catalina | 3.5, 3.6, 3.7, 3.8 | 7.0.0 |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ | macOS 10.14 Mojave | 2.7, 3.5, 3.6, 3.7 | 6.0.0 |x86-64 | | +------------------------------+--------------------------------+ + | | 3.4 | 5.4.1 | | diff --git a/docs/porting.rst b/docs/porting.rst index 50b713fac3f..3b14cde9d7d 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 diff --git a/docs/reference/ImageChops.rst b/docs/reference/ImageChops.rst index 6c8f11253ac..fb742254903 100644 --- a/docs/reference/ImageChops.rst +++ b/docs/reference/ImageChops.rst @@ -36,6 +36,9 @@ operations in this module). .. autofunction:: PIL.ImageChops.logical_or .. autofunction:: PIL.ImageChops.logical_xor .. autofunction:: PIL.ImageChops.multiply +.. autofunction:: PIL.ImageChops.soft_light +.. autofunction:: PIL.ImageChops.hard_light +.. autofunction:: PIL.ImageChops.overlay .. py:method:: PIL.ImageChops.offset(image, xoffset, yoffset=None) Returns a copy of the image where data has been offset by the given diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 51eaf925ec6..d888913c34a 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -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 @@ -154,7 +154,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:: PIL.ImageDraw.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 +168,7 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.ellipse(xy, fill=None, outline=None, width=0) +.. py:method:: PIL.ImageDraw.ImageDraw.ellipse(xy, fill=None, outline=None, width=1) Draws an ellipse inside the given bounding box. @@ -198,7 +198,7 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=0) +.. py:method:: PIL.ImageDraw.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. @@ -236,7 +236,7 @@ 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:: PIL.ImageDraw.ImageDraw.rectangle(xy, fill=None, outline=None, width=1) Draws a rectangle. @@ -255,7 +255,7 @@ Methods 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:: PIL.ImageDraw.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) Draws the string at the given position. @@ -306,7 +306,7 @@ 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) +.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None) Draws the string at the given position. diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index e94e21cb9df..ddd5bbbb516 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -11,13 +11,13 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 -.. py:function:: PIL.ImageGrab.grab(bbox=None, include_layered_windows=False, all_screens=False) +.. py:function:: PIL.ImageGrab.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. 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,6 +27,11 @@ 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 ``None`` to grab the default system screen. + Pass ``""`` to grab the default X11 screen on Windows or macOS. + + .. versionadded:: 7.1.0 :return: An image .. py:function:: PIL.ImageGrab.grabclipboard() diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index 50cea90ca98..1c86d168ff0 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -8,13 +8,13 @@ The :py:mod:`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. -Only bug fixes have been added since the Pillow fork. - .. versionadded:: 1.1.3 .. autofunction:: autocontrast .. autofunction:: colorize +.. autofunction:: pad .. autofunction:: crop +.. autofunction:: scale .. autofunction:: deform .. autofunction:: equalize .. autofunction:: expand @@ -25,3 +25,4 @@ Only bug fixes have been added since the Pillow fork. .. autofunction:: mirror .. autofunction:: posterize .. autofunction:: solarize +.. autofunction:: exif_transpose diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 5128f28fb23..7dd7084dbfa 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -4,14 +4,8 @@ :py:mod:`ImageQt` Module ======================== -The :py:mod:`ImageQt` module contains support for creating PyQt4, PyQt5, PySide or -PySide2 QImage objects from PIL images. - -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. - -Support for PyQt4 and PySide is deprecated since Pillow 6.0.0 and will be removed in a -future version. Please upgrade to PyQt5 or PySide2. +The :py:mod:`ImageQt` module contains support for creating PyQt5 or PySide2 QImage +objects from PIL images. .. versionadded:: 1.1.6 @@ -20,7 +14,7 @@ future version. Please upgrade to PyQt5 or PySide2. Creates an :py:class:`~PIL.ImageQt.ImageQt` object from a PIL :py:class:`~PIL.Image.Image` object. This class is a subclass of QtGui.QImage, which means that you can pass the resulting objects directly - to PyQt4/PyQt5/PySide API functions and methods. + to PyQt5/PySide2 API functions and methods. This operation is currently supported for mode 1, L, P, RGB, and RGBA images. To handle other modes, you need to convert the image first. diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index 251ea3a93fd..353e8099ef4 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -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("frame%d.png" % index) + index += 1 The :py:class:`~PIL.ImageSequence.Iterator` class ------------------------------------------------- diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 8a856992209..f28e58f8625 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 6a492cd86c3..e00741c4323 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -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/open_files.rst b/docs/reference/open_files.rst index e26d9e63992..ed0ab1a0ca6 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -8,27 +8,25 @@ object, or a file-like object. Pillow uses the filename or ``Path`` to open a file, so for the rest of this article, they will all be treated as a file-like object. -The first four of these items are equivalent, the last is dangerous -and may fail:: +The following are all equivalent:: from PIL import Image import io import pathlib - im = Image.open('test.jpg') + with Image.open('test.jpg') as im: + ... - im2 = Image.open(pathlib.Path('test.jpg')) - - f = open('test.jpg', 'rb') - im3 = Image.open(f) + with Image.open(pathlib.Path('test.jpg')) as im2: + ... with open('test.jpg', 'rb') as f: - im4 = Image.open(io.BytesIO(f.read())) + im3 = Image.open(f) + ... - # Dangerous FAIL: with open('test.jpg', 'rb') as f: - im5 = Image.open(f) - im5.load() # FAILS, closed file + im4 = Image.open(io.BytesIO(f.read())) + ... If a filename or a path-like object is passed to Pillow, then the resulting file object opened by Pillow may also be closed by Pillow after the @@ -38,13 +36,6 @@ have multiple frames. Pillow cannot in general close and reopen a file, so any access to that file needs to be prior to the close. -Issues ------- - -* Using the file context manager to provide a file-like object to - Pillow is dangerous unless the context of the image is limited to - the context of the file. - Image Lifecycle --------------- @@ -70,9 +61,9 @@ Image Lifecycle ... # image operations here. -The lifecycle of a single-frame image is relatively simple. The file -must remain open until the ``load()`` or ``close()`` function is -called. +The lifecycle of a single-frame image is relatively simple. The file must +remain open until the ``load()`` or ``close()`` function is called or the +context manager exits. Multi-frame images are more complicated. The ``load()`` method is not a terminal method, so it should not close the underlying file. In general, @@ -87,14 +78,16 @@ Complications libtiff (if working on an actual file). Since libtiff closes the file descriptor internally, it is duplicated prior to passing it into libtiff. -* I don't think that there's any way to make this safe without - changing the lazy loading:: +* After a file has been closed, operations that require file access will fail:: - # Dangerous FAIL: with open('test.jpg', 'rb') as f: im5 = Image.open(f) im5.load() # FAILS, closed file + with Image.open('test.jpg') as im6: + pass + im6.load() # FAILS, closed file + Proposed File Handling ---------------------- @@ -104,5 +97,6 @@ Proposed File Handling * ``Image.Image.seek()`` should never close the image file. -* Users of the library should call ``Image.Image.close()`` on any - multi-frame image to ensure that the underlying file is closed. +* Users of the library should use a context manager or call + ``Image.Image.close()`` on any image opened with a filename or ``Path`` + object to ensure that the underlying file is closed. diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 0c3ce77e2e9..faf84e6bdc8 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -47,7 +47,7 @@ creates the following image: ImageGrab on multi-monitor Windows ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -An `all_screens` argument has been added to ``ImageGrab.grab``. If ``True``, +An ``all_screens`` argument has been added to ``ImageGrab.grab``. If ``True``, all monitors will be included in the created image. API Changes @@ -62,14 +62,6 @@ shared instance of ``Image.Exif``. Deprecations ^^^^^^^^^^^^ -Python 2.7 -~~~~~~~~~~ - -Python 2.7 reaches end-of-life on 2020-01-01. - -Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python -2.7, making Pillow 6.2.x the last release series to support Python 2. - Image.frombuffer ~~~~~~~~~~~~~~~~ @@ -77,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.1.rst b/docs/releasenotes/6.2.1.rst new file mode 100644 index 00000000000..ca298fa702c --- /dev/null +++ b/docs/releasenotes/6.2.1.rst @@ -0,0 +1,26 @@ +6.2.1 +----- + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +Python 2.7 +~~~~~~~~~~ + +Python 2.7 reaches end-of-life on 2020-01-01. + +Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python +2.7, making Pillow 6.2.x the last release series to support Python 2. + +Other Changes +============= + + + +Support added for Python 3.8 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 6.2.1 supports Python 3.8. diff --git a/docs/releasenotes/6.2.2.rst b/docs/releasenotes/6.2.2.rst new file mode 100644 index 00000000000..a138c7d607f --- /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 new file mode 100644 index 00000000000..e0e76434249 --- /dev/null +++ b/docs/releasenotes/7.0.0.rst @@ -0,0 +1,161 @@ +7.0.0 +----- + +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 +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +PIL.*ImagePlugin.__version__ attributes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The version constants of individual plugins have been removed. Use ``PIL.__version__`` +instead. + +=============================== ================================= ================================== +Removed Removed Removed +=============================== ================================= ================================== +``BmpImagePlugin.__version__`` ``Jpeg2KImagePlugin.__version__`` ``PngImagePlugin.__version__`` +``CurImagePlugin.__version__`` ``JpegImagePlugin.__version__`` ``PpmImagePlugin.__version__`` +``DcxImagePlugin.__version__`` ``McIdasImagePlugin.__version__`` ``PsdImagePlugin.__version__`` +``EpsImagePlugin.__version__`` ``MicImagePlugin.__version__`` ``SgiImagePlugin.__version__`` +``FliImagePlugin.__version__`` ``MpegImagePlugin.__version__`` ``SunImagePlugin.__version__`` +``FpxImagePlugin.__version__`` ``MpoImagePlugin.__version__`` ``TgaImagePlugin.__version__`` +``GdImageFile.__version__`` ``MspImagePlugin.__version__`` ``TiffImagePlugin.__version__`` +``GifImagePlugin.__version__`` ``PalmImagePlugin.__version__`` ``WmfImagePlugin.__version__`` +``IcoImagePlugin.__version__`` ``PcdImagePlugin.__version__`` ``XbmImagePlugin.__version__`` +``ImImagePlugin.__version__`` ``PcxImagePlugin.__version__`` ``XpmImagePlugin.__version__`` +``ImtImagePlugin.__version__`` ``PdfImagePlugin.__version__`` ``XVThumbImagePlugin.__version__`` +``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` +=============================== ================================= ================================== + +PyQt4 and PySide +^^^^^^^^^^^^^^^^ + +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. + +Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 +or PySide2. + +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 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +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. + +Image.draft() return value +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If the :py:meth:`~PIL.Image.Image.draft` method has no effect, it returns ``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. + +.. _chain methods: https://en.wikipedia.org/wiki/Method_chaining + + +API Additions +============= + +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``. + +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 ``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 +============= + +Image.__del__ +^^^^^^^^^^^^^ + +Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. +Use a context manager or call :py:meth:`~PIL.Image.Image.close` instead to close +the file in a deterministic way. + +Previous method: + +.. code-block:: python + + im = Image.open("hopper.png") + im.save("out.jpg") + +Use instead: + +.. code-block:: python + + 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 00000000000..346b9b49099 --- /dev/null +++ b/docs/releasenotes/7.1.0.rst @@ -0,0 +1,92 @@ +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 ``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. + + +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/index.rst b/docs/releasenotes/index.rst index 76c0321e73a..1838803ded3 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,6 +6,10 @@ Release Notes .. toctree:: :maxdepth: 2 + 7.1.0 + 7.0.0 + 6.2.2 + 6.2.1 6.2.0 6.1.0 6.0.0 diff --git a/docs/resources/favicon.ico b/docs/resources/favicon.ico new file mode 100644 index 00000000000..78eef9ae3e0 Binary files /dev/null and b/docs/resources/favicon.ico differ diff --git a/mp_compile.py b/mp_compile.py deleted file mode 100644 index ec73e927e28..00000000000 --- a/mp_compile.py +++ /dev/null @@ -1,93 +0,0 @@ -# A monkey patch of the base distutils.ccompiler to use parallel builds -# Tested on 2.7, looks to be identical to 3.3. -# Only applied on Python 2.7 because otherwise, it conflicts with Python's -# own newly-added support for parallel builds. - -from __future__ import print_function - -import os -import sys -from distutils.ccompiler import CCompiler -from multiprocessing import Pool, cpu_count - -try: - MAX_PROCS = int(os.environ.get("MAX_CONCURRENCY", min(4, cpu_count()))) -except NotImplementedError: - MAX_PROCS = None - - -# hideous monkeypatching. but. but. but. -def _mp_compile_one(tp): - (self, obj, build, cc_args, extra_postargs, pp_opts) = tp - try: - src, ext = build[obj] - except KeyError: - return - self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) - return - - -def _mp_compile( - self, - sources, - output_dir=None, - macros=None, - include_dirs=None, - debug=0, - extra_preargs=None, - extra_postargs=None, - depends=None, -): - """Compile one or more source files. - - see distutils.ccompiler.CCompiler.compile for comments. - """ - # A concrete compiler class can either override this method - # entirely or implement _compile(). - - macros, objects, extra_postargs, pp_opts, build = self._setup_compile( - output_dir, macros, include_dirs, sources, depends, extra_postargs - ) - cc_args = self._get_cc_args(pp_opts, debug, extra_preargs) - - pool = Pool(MAX_PROCS) - try: - print("Building using %d processes" % pool._processes) - except Exception: - pass - arr = [(self, obj, build, cc_args, extra_postargs, pp_opts) for obj in objects] - pool.map_async(_mp_compile_one, arr) - pool.close() - pool.join() - # Return *all* object filenames, not just the ones we just built. - return objects - - -def install(): - - fl_win = sys.platform.startswith("win") - fl_cygwin = sys.platform.startswith("cygwin") - - if fl_win or fl_cygwin: - # Windows barfs on multiprocessing installs - print("Single threaded build for Windows") - return - - if MAX_PROCS != 1: - # explicitly don't enable if environment says 1 processor - try: - # bug, only enable if we can make a Pool. see issue #790 and - # https://stackoverflow.com/questions/6033599/oserror-38-errno-38-with-multiprocessing - Pool(2) - CCompiler.compile = _mp_compile - except Exception as msg: - print("Exception installing mp_compile, proceeding without: %s" % msg) - else: - print( - "Single threaded build, not installing mp_compile: %s processes" % MAX_PROCS - ) - - -# We monkeypatch Python 2.7 -if sys.version_info.major < 3: - install() diff --git a/requirements.txt b/requirements.txt index 082d8e51649..14f934c9cc9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ black; python_version >= '3.6' check-manifest coverage -coveralls jarn.viewdoc olefile pycodestyle diff --git a/selftest.py b/selftest.py index dcac54a5a71..ea52256f79a 100755 --- a/selftest.py +++ b/selftest.py @@ -1,8 +1,6 @@ #!/usr/bin/env python # minimal sanity check -from __future__ import print_function -import os import sys from PIL import Image, features @@ -42,8 +40,8 @@ def testimage(): Or open existing files: - >>> im = Image.open("Tests/images/hopper.gif") - >>> _info(im) + >>> with Image.open("Tests/images/hopper.gif") as im: + ... _info(im) ('GIF', 'P', (128, 128)) >>> _info(Image.open("Tests/images/hopper.ppm")) ('PPM', 'RGB', (128, 128)) @@ -162,32 +160,7 @@ def testimage(): exit_status = 0 - print("-" * 68) - print("Pillow", Image.__version__, "TEST SUMMARY ") - print("-" * 68) - print("Python modules loaded from", os.path.dirname(Image.__file__)) - print("Binary modules loaded from", os.path.dirname(Image.core.__file__)) - print("-" * 68) - for name, feature in [ - ("pil", "PIL CORE"), - ("tkinter", "TKINTER"), - ("freetype2", "FREETYPE2"), - ("littlecms2", "LITTLECMS2"), - ("webp", "WEBP"), - ("transp_webp", "WEBP Transparency"), - ("webp_mux", "WEBPMUX"), - ("webp_anim", "WEBP Animation"), - ("jpg", "JPEG"), - ("jpg_2000", "OPENJPEG (JPEG2000)"), - ("zlib", "ZLIB (PNG/ZIP)"), - ("libtiff", "LIBTIFF"), - ("raqm", "RAQM (Bidirectional Text)"), - ]: - if features.check(name): - print("---", feature, "support ok") - else: - print("***", feature, "support not installed") - print("-" * 68) + features.pilinfo(sys.stdout, False) # use doctest to make sure the test program behaves as documented! import doctest diff --git a/setup.cfg b/setup.cfg index 1c6ebc84c46..17e85bd216e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[aliases] -test=pytest - [flake8] extend-ignore = E203, W503 max-line-length = 88 diff --git a/setup.py b/setup.py index 76bdfb159fb..3e1a812b6a3 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,6 @@ # Final rating: 10/10 # Your cheese is so fresh most people think it's a cream: Mascarpone # ------------------------------ -from __future__ import print_function import os import re @@ -20,15 +19,31 @@ from setuptools import Extension, setup -# monkey patch import hook. Even though flake8 says it's not used, it is. -# comment this out to disable multi threaded builds. -import mp_compile -if sys.platform == "win32" and sys.version_info >= (3, 8): +def get_version(): + version_file = "src/PIL/_version.py" + with open(version_file, "r") as f: + exec(compile(f.read(), version_file, "exec")) + return locals()["__version__"] + + +NAME = "Pillow" +PILLOW_VERSION = get_version() +FREETYPE_ROOT = None +IMAGEQUANT_ROOT = None +JPEG2K_ROOT = None +JPEG_ROOT = None +LCMS_ROOT = None +TIFF_ROOT = None +ZLIB_ROOT = None + + +if sys.platform == "win32" and sys.version_info >= (3, 9): warnings.warn( - "Pillow does not yet support Python {}.{} and does not yet provide " - "prebuilt Windows binaries. We do not recommend building from " - "source on Windows.".format(sys.version_info.major, sys.version_info.minor), + "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 + ), RuntimeWarning, ) @@ -39,6 +54,7 @@ "Access", "AlphaComposite", "Resample", + "Reduce", "Bands", "BcnDecode", "BitDecode", @@ -169,10 +185,10 @@ def _find_library_dirs_ldconfig(): expr = r".* => (.*)" env = {} - null = open(os.devnull, "wb") try: - with null: - p = subprocess.Popen(args, stderr=null, stdout=subprocess.PIPE, env=env) + p = subprocess.Popen( + args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env + ) except OSError: # E.g. command not found return [] [data, _] = p.communicate() @@ -233,24 +249,6 @@ def _read(file): return fp.read() -def get_version(): - version_file = "src/PIL/_version.py" - with open(version_file, "r") as f: - exec(compile(f.read(), version_file, "exec")) - return locals()["__version__"] - - -NAME = "Pillow" -PILLOW_VERSION = get_version() -JPEG_ROOT = None -JPEG2K_ROOT = None -ZLIB_ROOT = None -IMAGEQUANT_ROOT = None -TIFF_ROOT = None -FREETYPE_ROOT = None -LCMS_ROOT = None - - def _pkg_config(name): try: command = os.environ.get("PKG_CONFIG", "pkg-config") @@ -288,6 +286,7 @@ class feature: "webpmux", "jpeg2000", "imagequant", + "xcb", ] required = {"jpeg", "zlib"} @@ -303,8 +302,7 @@ def want(self, feat): return getattr(self, feat) is None def __iter__(self): - for x in self.features: - yield x + yield from self.features feature = feature() @@ -332,12 +330,15 @@ def finalize_options(self): if self.debug: global DEBUG DEBUG = True - if sys.version_info.major >= 3 and not self.parallel: - # For Python 2.7, we monkeypatch distutils to have parallel - # builds. If --parallel (or -j) wasn't specified, we want to - # reproduce the same behavior as before, that is, auto-detect the - # number of jobs. - self.parallel = mp_compile.MAX_PROCS + if not self.parallel: + # If --parallel (or -j) wasn't specified, we want to reproduce the same + # behavior as before, that is, auto-detect the number of jobs. + try: + self.parallel = int( + os.environ.get("MAX_CONCURRENCY", min(4, os.cpu_count())) + ) + except TypeError: + self.parallel = None for x in self.feature: if getattr(self, "disable_%s" % x): setattr(self.feature, x, False) @@ -345,7 +346,7 @@ def finalize_options(self): _dbg("Disabling %s", x) if getattr(self, "enable_%s" % x): raise ValueError( - "Conflicting options: --enable-%s and --disable-%s" % (x, x) + "Conflicting options: --enable-{} and --disable-{}".format(x, x) ) if getattr(self, "enable_%s" % x): _dbg("Requiring %s", x) @@ -681,6 +682,12 @@ def build_extensions(self): ): 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"): @@ -715,9 +722,12 @@ def build_extensions(self): if feature.tiff: libs.append(feature.tiff) defs.append(("HAVE_LIBTIFF", 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", "\0\1".encode("ascii"))[0] == 1: + 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): @@ -798,7 +808,7 @@ def summary_report(self, feature): print("-" * 68) print("version Pillow %s" % PILLOW_VERSION) v = sys.version.split("[") - print("platform %s %s" % (sys.platform, v[0].strip())) + print("platform {} {}".format(sys.platform, v[0].strip())) for v in v[1:]: print(" [%s" % v.strip()) print("-" * 68) @@ -813,6 +823,7 @@ def summary_report(self, feature): (feature.lcms, "LITTLECMS2"), (feature.webp, "WEBP"), (feature.webpmux, "WEBPMUX"), + (feature.xcb, "XCB (X protocol)"), ] all = 1 @@ -821,7 +832,7 @@ def summary_report(self, feature): version = "" if len(option) >= 3 and option[2]: version = " (%s)" % option[2] - print("--- %s support available%s" % (option[1], version)) + print("--- {} support available{}".format(option[1], version)) else: print("*** %s support not available" % option[1]) all = 0 @@ -845,9 +856,6 @@ def debug_build(): return hasattr(sys, "gettotalrefcount") -needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv) -pytest_runner = ["pytest-runner"] if needs_pytest else [] - try: setup( name=NAME, @@ -857,16 +865,22 @@ def debug_build(): license="HPND", author="Alex Clark (PIL Fork Author)", author_email="aclark@python-pillow.org", - url="http://python-pillow.org", + url="https://python-pillow.org", + project_urls={ + "Documentation": "https://pillow.readthedocs.io", + "Source": "https://github.com/python-pillow/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 :: 2", - "Programming Language :: Python :: 2.7", "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 :: Only", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Multimedia :: Graphics", @@ -875,12 +889,10 @@ def debug_build(): "Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Multimedia :: Graphics :: Viewers", ], - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", + python_requires=">=3.5", cmdclass={"build_ext": pil_build_ext}, ext_modules=[Extension("PIL._imaging", ["_imaging.c"])], include_package_data=True, - setup_requires=pytest_runner, - tests_require=["pytest"], packages=["PIL"], package_dir={"": "src"}, keywords=["Imaging"], diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index fdf2c097e1e..7a485cf8009 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -17,7 +17,6 @@ # See the README file for information on usage and redistribution. # -from __future__ import print_function from . import FontFile, Image @@ -85,8 +84,7 @@ def bdf_char(f): class BdfFontFile(FontFile.FontFile): def __init__(self, fp): - - FontFile.FontFile.__init__(self) + super().__init__() s = fp.readline() if s[:13] != b"STARTFONT 2.1": diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 7b97964a852..5ccba37dbd8 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -119,7 +119,7 @@ def decode_dxt3(data): bits = struct.unpack_from("<8B", block) color0, color1 = struct.unpack_from("= 16 @@ -163,12 +159,12 @@ def _bitmap(self, header=0, offset=0): # ------------------------------- Check abnormal values for DOS attacks if file_info["width"] * file_info["height"] > 2 ** 31: - raise IOError("Unsupported BMP Size: (%dx%d)" % self.size) + 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 IOError("Unsupported BMP pixel depth (%d)" % file_info["bits"]) + raise OSError("Unsupported BMP pixel depth (%d)" % file_info["bits"]) # ---------------- Process BMP with Bitfields compression (not palette) if file_info["compression"] == self.BITFIELDS: @@ -206,21 +202,21 @@ def _bitmap(self, header=0, offset=0): ): raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] else: - raise IOError("Unsupported BMP bitfields layout") + raise OSError("Unsupported BMP bitfields layout") else: - raise IOError("Unsupported BMP bitfields layout") + raise OSError("Unsupported BMP bitfields layout") elif file_info["compression"] == self.RAW: if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset raw_mode, self.mode = "BGRA", "RGBA" else: - raise IOError("Unsupported BMP compression (%d)" % file_info["compression"]) + raise OSError("Unsupported BMP compression (%d)" % 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 IOError("Unsupported BMP Palette size (%d)" % file_info["colors"]) + raise OSError("Unsupported BMP Palette size (%d)" % file_info["colors"]) else: padding = file_info["palette_padding"] palette = read(padding * file_info["colors"]) @@ -309,7 +305,7 @@ def _save(im, fp, filename, bitmap_header=True): try: rawmode, bits, colors = SAVE[im.mode] except KeyError: - raise IOError("cannot write mode %s as BMP" % im.mode) + raise OSError("cannot write mode %s as BMP" % im.mode) info = im.encoderinfo @@ -325,12 +321,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/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 56cac3bb144..48f21e1b3b4 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -60,7 +60,7 @@ def _load(self): def _save(im, fp, filename): if _handler is None or not hasattr("_handler", "save"): - raise IOError("BUFR save handler not installed") + raise OSError("BUFR save handler not installed") _handler.save(im, fp, filename) diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 3cf9d82d2c7..5bb0086f6e7 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -21,7 +21,7 @@ import io -class ContainerIO(object): +class ContainerIO: def __init__(self, file, offset, length): """ Create file object. @@ -82,7 +82,7 @@ def read(self, n=0): 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 +92,14 @@ def readline(self): :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 9e2d8c96f7c..3a1b6d2e533 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -15,16 +15,9 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - from . import BmpImagePlugin, Image from ._binary import i8, i16le as i16, i32le as i32 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - # # -------------------------------------------------------------------- diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index 57c321417b4..7d2aff325aa 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -25,10 +25,6 @@ from ._binary import i32le as i32 from .PcxImagePlugin import PcxImageFile -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.2" - MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index b2d508942d8..9ba6e0ff842 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -106,10 +106,10 @@ class DdsImageFile(ImageFile.ImageFile): def _open(self): magic, header_size = struct.unpack(" 2: - fp = io.TextIOWrapper(fp, encoding="latin-1") - wrapped_fp = True + fp = io.TextIOWrapper(fp, encoding="latin-1") + wrapped_fp = True try: if eps: diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 47a981e0f45..cecc3f24602 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -152,6 +152,12 @@ 0x9290: "SubsecTime", 0x9291: "SubsecTimeOriginal", 0x9292: "SubsecTimeDigitized", + 0x9400: "AmbientTemperature", + 0x9401: "Humidity", + 0x9402: "Pressure", + 0x9403: "WaterDepth", + 0x9404: "Acceleration", + 0x9405: "CameraElevationAngle", 0x9C9B: "XPTitle", 0x9C9C: "XPComment", 0x9C9D: "XPAuthor", diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index 7e6d35ee567..c2ce8651c8a 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -63,7 +63,7 @@ def _load(self): def _save(im, fp, filename): if _handler is None or not hasattr("_handler", "save"): - raise IOError("FITS save handler not installed") + raise OSError("FITS save handler not installed") _handler.save(im, fp, filename) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 82015e2fc17..9bf7d74d6e4 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -19,11 +19,6 @@ from . import Image, ImageFile, ImagePalette from ._binary import i8, i16le as i16, i32le as i32, o8 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.2" - - # # decoder diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index e57c2f3fd32..979a1e33c81 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -14,7 +14,6 @@ # See the README file for information on usage and redistribution. # -from __future__ import print_function import os @@ -35,7 +34,7 @@ def puti16(fp, values): # Base class for raster font file handlers. -class FontFile(object): +class FontFile: bitmap = None diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 15ebe0e3b00..8d252c79cf5 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -14,18 +14,11 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - import olefile from . import Image, ImageFile from ._binary import i8, i32le as i32 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - # we map from colour field tuples to (mode, rawmode) descriptors MODES = { # opacity @@ -66,7 +59,7 @@ def _open(self): try: self.ole = olefile.OleFileIO(self.fp) - except IOError: + except OSError: raise SyntaxError("not an FPX file; invalid OLE file") if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": @@ -104,7 +97,10 @@ def _open_index(self, index=1): s = prop[0x2000002 | id] colors = [] - for i in range(i32(s, 4)): + bands = i32(s, 4) + if bands > 4: + raise IOError("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) @@ -145,7 +141,7 @@ def _open_subimage(self, index=1, subimage=0): length = i32(s, 32) if size != self.size: - raise IOError("subimage mismatch") + raise OSError("subimage mismatch") # get tile descriptors fp.seek(28 + offset) @@ -218,7 +214,7 @@ def _open_subimage(self, index=1, subimage=0): self.tile_prefix = self.jpeg[jpeg_tables] else: - raise IOError("unknown/invalid compression") + raise OSError("unknown/invalid compression") x = x + xtile if x >= xsize: diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 06f4a72d03c..096ccacac69 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -79,7 +79,7 @@ def _open(self): format, where = struct.unpack("<2i", self.fp.read(8)) self.fp.seek(where) - mipmap_size, = struct.unpack("Image.open function. To use @@ -87,4 +82,4 @@ def open(fp, mode="r"): try: return GdImageFile(fp) except SyntaxError: - raise IOError("cannot identify this image file") + raise UnidentifiedImageError("cannot identify this image file") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 9d8e96feeeb..1d94fc7c7c7 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -25,15 +25,13 @@ # 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 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.9" - - # -------------------------------------------------------------------- # Identify/read GIF files @@ -572,8 +570,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: @@ -617,42 +618,44 @@ def _save_netpbm(im, fp, filename): # If you need real GIF compression and/or RGB quantization, you # can use the external NETPBM/PBMPLUS utilities. See comments # below for information on how to enable this. - - import os - from subprocess import Popen, check_call, PIPE, CalledProcessError - tempfile = im._dump() - with open(filename, "wb") as f: - if im.mode != "RGB": - with open(os.devnull, "wb") as devnull: - check_call(["ppmtogif", tempfile], stdout=f, stderr=devnull) - else: - # Pipe ppmquant output into ppmtogif - # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) - quant_cmd = ["ppmquant", "256", tempfile] - togif_cmd = ["ppmtogif"] - with open(os.devnull, "wb") as devnull: - quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=devnull) - togif_proc = Popen( - togif_cmd, stdin=quant_proc.stdout, stdout=f, stderr=devnull + try: + 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 CalledProcessError(retcode, quant_cmd) + # Allow ppmquant to receive SIGPIPE if ppmtogif exits + quant_proc.stdout.close() - retcode = togif_proc.wait() - if retcode: - raise CalledProcessError(retcode, togif_cmd) + retcode = quant_proc.wait() + if retcode: + raise subprocess.CalledProcessError(retcode, quant_cmd) - try: - os.unlink(tempfile) - except OSError: - pass + 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 @@ -699,14 +702,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): @@ -853,7 +854,7 @@ def getdata(im, offset=(0, 0), **params): """ - class Collector(object): + class Collector: data = [] def write(self, data): diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index f48e7f76ef3..1cacf5718dc 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -60,7 +60,7 @@ def sphere_decreasing(middle, pos): SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] -class GradientFile(object): +class GradientFile: gradient = None @@ -73,7 +73,7 @@ def getpalette(self, entries=256): for i in range(entries): - x = i / float(entries - 1) + x = i / (entries - 1) while x1 < x: ix += 1 @@ -132,7 +132,7 @@ def __init__(self, fp): cspace = int(s[12]) if cspace != 0: - raise IOError("cannot handle HSV colour space") + raise OSError("cannot handle HSV colour space") gradient.append((x0, x1, xm, rgb0, rgb1, segment)) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 2994bbeab84..e3060ab8a8f 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -22,7 +22,7 @@ # File handler for GIMP's palette format. -class GimpPaletteFile(object): +class GimpPaletteFile: rawmode = "RGB" diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 8a24a9829f4..515c272f727 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -61,7 +61,7 @@ def _load(self): def _save(im, fp, filename): if _handler is None or not hasattr("_handler", "save"): - raise IOError("GRIB save handler not installed") + raise OSError("GRIB save handler not installed") _handler.save(im, fp, filename) diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index a3ea12f999c..362f2d3994d 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -60,7 +60,7 @@ def _load(self): def _save(im, fp, filename): if _handler is None or not hasattr("_handler", "save"): - raise IOError("HDF5 save handler not installed") + raise OSError("HDF5 save handler not installed") _handler.save(im, fp, filename) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 75ea18b6b5f..c003926154e 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -19,6 +19,7 @@ import os import shutil import struct +import subprocess import sys import tempfile @@ -128,7 +129,7 @@ def read_png_or_jpeg2000(fobj, start_length, size): raise ValueError("Unsupported icon subimage format") -class IcnsFile(object): +class IcnsFile: SIZES = { (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], @@ -313,41 +314,40 @@ def _save(im, fp, filename): fp.flush() # create the temporary set of pngs - iconset = tempfile.mkdtemp(".iconset") - provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])} - last_w = None - second_path = None - for w in [16, 32, 128, 256, 512]: - prefix = "icon_{}x{}".format(w, w) - - first_path = os.path.join(iconset, prefix + ".png") - if last_w == w: - shutil.copyfile(second_path, first_path) - else: - im_w = provided_images.get(w, im.resize((w, w), Image.LANCZOS)) - im_w.save(first_path) - - second_path = os.path.join(iconset, prefix + "@2x.png") - im_w2 = provided_images.get(w * 2, im.resize((w * 2, w * 2), Image.LANCZOS)) - im_w2.save(second_path) - last_w = w * 2 - - # iconutil -c icns -o {} {} - from subprocess import Popen, PIPE, CalledProcessError - - convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] - with open(os.devnull, "wb") as devnull: - convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=devnull) - - convert_proc.stdout.close() + with tempfile.TemporaryDirectory(".iconset") as iconset: + provided_images = { + im.width: im for im in im.encoderinfo.get("append_images", []) + } + last_w = None + second_path = None + for w in [16, 32, 128, 256, 512]: + prefix = "icon_{}x{}".format(w, w) + + first_path = os.path.join(iconset, prefix + ".png") + if last_w == w: + shutil.copyfile(second_path, first_path) + else: + im_w = provided_images.get(w, im.resize((w, w), Image.LANCZOS)) + im_w.save(first_path) + + second_path = os.path.join(iconset, prefix + "@2x.png") + im_w2 = provided_images.get(w * 2, im.resize((w * 2, w * 2), Image.LANCZOS)) + im_w2.save(second_path) + last_w = w * 2 + + # iconutil -c icns -o {} {} + + convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] + convert_proc = subprocess.Popen( + convert_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) - retcode = convert_proc.wait() + convert_proc.stdout.close() - # remove the temporary files - shutil.rmtree(iconset) + retcode = convert_proc.wait() - if retcode: - raise CalledProcessError(retcode, convert_cmd) + if retcode: + raise subprocess.CalledProcessError(retcode, convert_cmd) Image.register_open(IcnsImageFile.format, IcnsImageFile, lambda x: x[:4] == b"icns") @@ -365,13 +365,12 @@ def _save(im, fp, filename): 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 148e604f895..e4a74321b6e 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -30,10 +30,6 @@ from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from ._binary import i8, i16le as i16, i32le as i32 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - # # -------------------------------------------------------------------- @@ -67,8 +63,9 @@ def _save(im, fp, filename): fp.write(struct.pack("= (3, 7): - builtins = __builtin__ + def __getattr__(name): + if name == "PILLOW_VERSION": + _raise_version_warning() + return __version__ + raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name)) -try: - # Python 3 - from collections.abc import Callable, MutableMapping -except ImportError: - # Python 2.7 - from collections import Callable, MutableMapping +else: + + from . import PILLOW_VERSION + # Silence warning + assert PILLOW_VERSION -# Silence warning -assert PILLOW_VERSION logger = logging.getLogger(__name__) @@ -71,12 +80,6 @@ class DecompressionBombError(Exception): pass -class _imaging_not_installed(object): - # module placeholder - def __getattr__(self, id): - raise ImportError("The _imaging C module is not installed") - - # Limit to around a quarter gigabyte for a 24 bit (3 bpp) image MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3) @@ -97,7 +100,7 @@ def __getattr__(self, id): ) except ImportError as v: - core = _imaging_not_installed() + core = deferred_error(ImportError("The _imaging C module is not installed.")) # Explanations for ways that we know we might have an import error if str(v).startswith("Module use of python"): # The _imaging C module is present, but not compiled for @@ -109,22 +112,6 @@ def __getattr__(self, id): ) elif str(v).startswith("The _imaging extension"): warnings.warn(str(v), RuntimeWarning) - elif "Symbol not found: _PyUnicodeUCS2_" in str(v): - # should match _PyUnicodeUCS2_FromString and - # _PyUnicodeUCS2_AsLatin1String - warnings.warn( - "The _imaging extension was built for Python with UCS2 support; " - "recompile Pillow or build Python --without-wide-unicode. ", - RuntimeWarning, - ) - elif "Symbol not found: _PyUnicodeUCS4_" in str(v): - # should match _PyUnicodeUCS4_FromString and - # _PyUnicodeUCS4_AsLatin1String - warnings.warn( - "The _imaging extension was built for Python with UCS4 support; " - "recompile Pillow or build Python --with-wide-unicode. ", - RuntimeWarning, - ) # Fail here anyway. Don't let people run with a mostly broken Pillow. # see docs/porting.rst raise @@ -137,18 +124,6 @@ def __getattr__(self, id): except ImportError: cffi = None -try: - from pathlib import Path - - HAS_PATHLIB = True -except ImportError: - try: - from pathlib2 import Path - - HAS_PATHLIB = True - except ImportError: - HAS_PATHLIB = False - def isImageType(t): """ @@ -193,6 +168,9 @@ def isImageType(t): 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 @@ -447,15 +425,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 IOError("decoder %s not available" % decoder_name) + raise OSError("decoder %s not available" % decoder_name) + return decoder(mode, *args + extra) def _getencoder(mode, encoder_name, args, extra=()): @@ -468,15 +448,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 IOError("encoder %s not available" % encoder_name) + raise OSError("encoder %s not available" % encoder_name) + return encoder(mode, *args + extra) # -------------------------------------------------------------------- @@ -487,7 +469,7 @@ def coerce_e(value): return value if isinstance(value, _E) else _E(value) -class _E(object): +class _E: def __init__(self, data): self.data = data @@ -528,7 +510,7 @@ def _getscaleoffset(expr): # Implementation wrapper -class Image(object): +class Image: """ This class represents an image object. To create :py:class:`~PIL.Image.Image` objects, use the appropriate factory @@ -624,11 +606,6 @@ def close(self): # object is gone. self.im = deferred_error(ValueError("Operation on closed image")) - if sys.version_info.major >= 3: - - def __del__(self): - self.__exit__() - def _copy(self): self.load() self.im = self.im.copy() @@ -642,8 +619,6 @@ def _ensure_mutable(self): self.load() def _dump(self, file=None, format=None, **options): - import tempfile - suffix = "" if format: suffix = "." + format @@ -677,10 +652,6 @@ def __eq__(self, other): and self.tobytes() == other.tobytes() ) - def __ne__(self, other): - eq = self == other - return not eq - def __repr__(self): return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( self.__class__.__module__, @@ -1186,16 +1157,18 @@ def draft(self, mode, size): """ 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. @@ -1350,10 +1323,7 @@ def getpalette(self): self.load() try: - if py3: - return list(self.im.getpalette()) - else: - return [i8(c) for c in self.im.getpalette()] + return list(self.im.getpalette()) except ValueError: return None # no palette @@ -1504,7 +1474,7 @@ def paste(self, im, box=None, mask=None): raise ValueError("cannot determine region size; use 4-item box") box += (box[0] + size[0], box[1] + size[1]) - if isStringType(im): + if isinstance(im, str): from . import ImageColor im = ImageColor.getcolor(im, self.mode) @@ -1704,10 +1674,7 @@ def putpalette(self, data, rawmode="RGB"): palette = ImagePalette.raw(data.rawmode, data.palette) else: if not isinstance(data, bytes): - if py3: - data = bytes(data) - else: - data = "".join(chr(x) for x in data) + data = bytes(data) palette = ImagePalette.raw(rawmode, data) self.mode = "PA" if "A" in self.mode else "P" self.palette = palette @@ -1827,7 +1794,24 @@ def remap_palette(self, dest_map, source_palette=None): 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. @@ -1837,13 +1821,26 @@ def resize(self, size, resample=NEAREST, box=None): 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`. + Default filter is :py:attr:`PIL.Image.BICUBIC`. + If the image has mode "1" or "P", it is + always set to :py:attr:`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. """ @@ -1865,6 +1862,9 @@ def resize(self, size, resample=NEAREST, box=None): 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: @@ -1885,8 +1885,58 @@ def resize(self, size, resample=NEAREST, box=None): 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 by `factor` times. + If the size of the image is not dividable by the `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, @@ -1908,7 +1958,7 @@ def rotate( environment), or :py:attr:`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:attr:`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 @@ -1993,8 +2043,8 @@ def transform(x, y, matrix): 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 @@ -2039,7 +2089,7 @@ def save(self, fp, format=None, **params): if isPath(fp): filename = fp open_fp = True - elif HAS_PATHLIB and isinstance(fp, Path): + elif isinstance(fp, Path): filename = str(fp) open_fp = True if not filename and hasattr(fp, "name") and isPath(fp.name): @@ -2161,7 +2211,7 @@ def getchannel(self, channel): """ self.load() - if isStringType(channel): + if isinstance(channel, str): try: channel = self.getbands().index(channel) except ValueError: @@ -2177,7 +2227,7 @@ def tell(self): """ 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 @@ -2196,27 +2246,47 @@ def thumbnail(self, size, resample=BICUBIC): 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) + (was :py:attr:`PIL.Image.NEAREST` prior to version 2.5.0). + :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: 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 @@ -2246,12 +2316,14 @@ def transform( It may also be an :py:class:`~PIL.Image.ImageTransformHandler` object:: + class Example(Image.ImageTransformHandler): def transform(size, method, 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): def getdata(self): method = Image.EXTENT @@ -2297,6 +2369,7 @@ def getdata(self): raise ValueError("missing method data") im = new(self.mode, size, fillcolor) + im.info = self.info.copy() if method == MESH: # list of quads for box, quad in data: @@ -2318,8 +2391,8 @@ def __transformer(self, box, image, method, data, resample=NEAREST, fill=1): 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) @@ -2425,12 +2498,12 @@ def toqpixmap(self): # Abstract handlers. -class ImagePointHandler(object): +class ImagePointHandler: # used as a mixin by point transforms (for use with im.point) pass -class ImageTransformHandler(object): +class ImageTransformHandler: # used as a mixin by geometry transforms (for use with im.transform) pass @@ -2488,7 +2561,7 @@ def new(mode, size, color=0): # don't initialize return Image()._new(core.new(mode, size)) - if isStringType(color): + if isinstance(color, str): # css3-style specifier from . import ImageColor @@ -2592,17 +2665,10 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): if decoder_name == "raw": if args == (): - warnings.warn( - "the frombuffer defaults will change in Pillow 7.0.0; " - "for portability, change the call to read:\n" - " frombuffer(mode, size, data, 'raw', mode, 0, 1)", - RuntimeWarning, - stacklevel=2, - ) - args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 + 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 @@ -2642,9 +2708,12 @@ def fromarray(obj, mode=None): if mode is None: try: typekey = (1, 1) + shape[2:], arr["typestr"] - mode, rawmode = _fromarray_typemap[typekey] except KeyError: raise TypeError("Cannot handle this data type") + try: + mode, rawmode = _fromarray_typemap[typekey] + except KeyError: + raise TypeError("Cannot handle this data type: %s, %s" % typekey) else: rawmode = mode if mode in ["1", "L", "I", "P", "F"]: @@ -2748,16 +2817,24 @@ def open(fp, mode="r"): and be opened in binary mode. :param mode: The mode. If given, this argument must be "r". :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``. """ if mode != "r": raise ValueError("bad mode %r" % mode) + elif isinstance(fp, io.StringIO): + raise ValueError( + "StringIO cannot be used to open an image. " + "Binary data must be used instead." + ) exclusive_fp = False filename = "" - if HAS_PATHLIB and isinstance(fp, Path): + if isinstance(fp, Path): filename = str(fp.resolve()) elif isPath(fp): filename = fp @@ -2815,7 +2892,9 @@ def _open_core(fp, filename, prefix): fp.close() for message in accept_warnings: warnings.warn(message) - raise IOError("cannot identify image file %r" % (filename if filename else fp)) + raise UnidentifiedImageError( + "cannot identify image file %r" % (filename if filename else fp) + ) # @@ -3238,7 +3317,7 @@ def get_ifd(self, tag): continue size = count * unit_size if size > 4: - offset, = struct.unpack("L", data) + (offset,) = struct.unpack(">L", data) self.fp.seek(offset) camerainfo = {"ModelID": self.fp.read(4)} @@ -3321,11 +3400,6 @@ def __getitem__(self, tag): def __contains__(self, tag): return tag in self._data or (self._info is not None and tag in self._info) - if not py3: - - def has_key(self, tag): - return tag in self - def __setitem__(self, tag, value): if self._info is not None and tag in self._info: del self._info[tag] diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index b1f71b5e71e..2d13b529fef 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -139,6 +139,42 @@ 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 diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index ed4eefc0d0e..661c3f33be4 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -15,12 +15,9 @@ # See the README file for information on usage and redistribution. See # below for the original description. -from __future__ import print_function - import sys from PIL import Image -from PIL._util import isStringType try: from PIL import _imagingcms @@ -152,7 +149,7 @@ # Profile. -class ImageCmsProfile(object): +class ImageCmsProfile: def __init__(self, profile): """ :param profile: Either a string representing a filename, @@ -161,7 +158,7 @@ def __init__(self, profile): """ - if isStringType(profile): + if isinstance(profile, str): self._set(core.profile_open(profile), profile) elif hasattr(profile, "read"): self._set(core.profile_frombytes(profile.read())) @@ -257,20 +254,17 @@ def get_display_profile(handle=None): :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) @@ -374,7 +368,7 @@ def profileToProfile( imOut = None else: imOut = transform.apply(im) - except (IOError, TypeError, ValueError) as v: + except (OSError, TypeError, ValueError) as v: raise PyCMSError(v) return imOut @@ -398,7 +392,7 @@ def getOpenProfile(profileFilename): try: return ImageCmsProfile(profileFilename) - except (IOError, TypeError, ValueError) as v: + except (OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -479,7 +473,7 @@ def buildTransform( return ImageCmsTransform( inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags ) - except (IOError, TypeError, ValueError) as v: + except (OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -590,7 +584,7 @@ def buildProofTransform( proofRenderingIntent, flags, ) - except (IOError, TypeError, ValueError) as v: + except (OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -733,9 +727,9 @@ def getProfileName(profile): return (profile.profile.profile_description or "") + "\n" if not manufacturer or len(model) > 30: return model + "\n" - return "%s - %s\n" % (model, manufacturer) + return "{} - {}\n".format(model, manufacturer) - except (AttributeError, IOError, TypeError, ValueError) as v: + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -775,7 +769,7 @@ def getProfileInfo(profile): arr.append(elt) return "\r\n\r\n".join(arr) + "\r\n\r\n" - except (AttributeError, IOError, TypeError, ValueError) as v: + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -803,7 +797,7 @@ def getProfileCopyright(profile): if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) return (profile.profile.copyright or "") + "\n" - except (AttributeError, IOError, TypeError, ValueError) as v: + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -831,7 +825,7 @@ def getProfileManufacturer(profile): if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) return (profile.profile.manufacturer or "") + "\n" - except (AttributeError, IOError, TypeError, ValueError) as v: + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -860,7 +854,7 @@ def getProfileModel(profile): if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) return (profile.profile.model or "") + "\n" - except (AttributeError, IOError, TypeError, ValueError) as v: + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -889,7 +883,7 @@ def getProfileDescription(profile): if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) return (profile.profile.profile_description or "") + "\n" - except (AttributeError, IOError, TypeError, ValueError) as v: + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -928,7 +922,7 @@ def getDefaultIntent(profile): if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) return profile.profile.rendering_intent - except (AttributeError, IOError, TypeError, ValueError) as v: + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -979,7 +973,7 @@ def isIntentSupported(profile, intent, direction): return 1 else: return -1 - except (AttributeError, IOError, TypeError, ValueError) as v: + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index 692d7d2c3c0..9cf7a991274 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -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 ed3383f0531..7abd459f97d 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,7 +34,6 @@ import numbers from . import Image, ImageColor -from ._util import isStringType """ @@ -45,7 +44,7 @@ """ -class ImageDraw(object): +class ImageDraw: def __init__(self, im, mode=None): """ Create a drawing instance. @@ -107,13 +106,13 @@ def _getink(self, ink, fill=None): ink = self.ink else: if ink is not None: - if isStringType(ink): + if isinstance(ink, str): ink = ImageColor.getcolor(ink, self.mode) if self.palette and not isinstance(ink, numbers.Number): ink = self.palette.getcolor(ink) ink = self.draw.draw_ink(ink) if fill is not None: - if isStringType(fill): + if isinstance(fill, str): fill = ImageColor.getcolor(fill, self.mode) if self.palette and not isinstance(fill, numbers.Number): fill = self.palette.getcolor(fill) @@ -135,20 +134,20 @@ def bitmap(self, xy, bitmap, fill=None): 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): @@ -220,12 +219,12 @@ def shape(self, shape, fill=None, outline=None): 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): @@ -242,12 +241,12 @@ def polygon(self, xy, fill=None, outline=None): 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 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): @@ -314,7 +313,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None): language=language, stroke_width=stroke_width, *args, - **kwargs + **kwargs, ) coord = coord[0] + offset[0], coord[1] + offset[1] except AttributeError: @@ -327,7 +326,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None): language, stroke_width, *args, - **kwargs + **kwargs, ) except TypeError: mask = font.getmask(text) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 324d869f032..20b5fe4c499 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -19,25 +19,25 @@ from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath -class Pen(object): +class Pen: def __init__(self, color, width=1, opacity=255): self.color = ImageColor.getrgb(color) self.width = width -class Brush(object): +class Brush: def __init__(self, color, opacity=255): self.color = ImageColor.getrgb(color) -class Font(object): +class Font: def __init__(self, color, file, size=12): # FIXME: add support for bitmap fonts self.color = ImageColor.getrgb(color) self.font = ImageFont.truetype(file, size) -class Draw(object): +class Draw: def __init__(self, image, size=None, color=None): if not hasattr(image, "im"): image = Image.new(image, size, color) diff --git a/src/PIL/ImageEnhance.py b/src/PIL/ImageEnhance.py index 534eb4f16a0..3b79d5c46a1 100644 --- a/src/PIL/ImageEnhance.py +++ b/src/PIL/ImageEnhance.py @@ -21,7 +21,7 @@ from . import Image, ImageFilter, ImageStat -class _Enhance(object): +class _Enhance: def enhance(self, factor): """ Returns an enhanced image. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 836e6318c5f..6287968652e 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -56,7 +56,7 @@ def raise_ioerror(error): message = ERRORS.get(error) if not message: message = "decoder error %d" % error - raise IOError(message + " when reading image file") + raise OSError(message + " when reading image file") # @@ -78,7 +78,7 @@ class ImageFile(Image.Image): "Base class for image file format handlers." def __init__(self, fp=None, filename=None): - Image.Image.__init__(self) + super().__init__() self._min_frame = 0 @@ -103,26 +103,24 @@ def __init__(self, fp=None, filename=None): self._exclusive_fp = None try: - self._open() - except ( - IndexError, # end of data - TypeError, # end of data (ord) - KeyError, # unsupported mode - EOFError, # got header but not the first frame - struct.error, - ) as v: + try: + self._open() + except ( + IndexError, # end of data + TypeError, # end of data (ord) + KeyError, # unsupported mode + EOFError, # got header but not the first frame + struct.error, + ) as v: + raise SyntaxError(v) + + if not self.mode or self.size[0] <= 0: + raise SyntaxError("not identified by this driver") + except BaseException: # close the file only if we have opened it this constructor if self._exclusive_fp: self.fp.close() - raise SyntaxError(v) - - if not self.mode or self.size[0] <= 0: - raise SyntaxError("not identified by this driver") - - def draft(self, mode, size): - """Set draft mode""" - - pass + raise def get_format_mimetype(self): if self.custom_mimetype: @@ -145,7 +143,7 @@ def load(self): pixel = Image.Image.load(self) if self.tile is None: - raise IOError("cannot load this image") + raise OSError("cannot load this image") if not self.tile: return pixel @@ -196,14 +194,14 @@ def load(self): 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, # we might need to reload the palette data. if self.palette: self.palette.dirty = 1 - except (AttributeError, EnvironmentError, ImportError): + except (AttributeError, OSError, ImportError): self.map = None self.load_prepare() @@ -238,13 +236,13 @@ def load(self): if LOAD_TRUNCATED_IMAGES: break else: - raise IOError("image file is truncated") + raise OSError("image file is truncated") if not s: # truncated jpeg if LOAD_TRUNCATED_IMAGES: break else: - raise IOError( + raise OSError( "image file is truncated " "(%d bytes not processed)" % len(b) ) @@ -322,7 +320,7 @@ def _open(self): def load(self): loader = self._load() if loader is None: - raise IOError("cannot find loader for this %s file" % self.format) + raise OSError("cannot find loader for this %s file" % self.format) image = loader.load(self) assert image is not None # become the other object (!) @@ -334,7 +332,7 @@ def _load(self): raise NotImplementedError("StubImageFile subclass must implement _load") -class Parser(object): +class Parser: """ Incremental image parser. This class implements the standard feed/close consumer interface. @@ -411,7 +409,7 @@ def feed(self, data): try: with io.BytesIO(self.data) as fp: im = Image.open(fp) - except IOError: + except OSError: # traceback.print_exc() pass # not enough data else: @@ -456,9 +454,9 @@ def close(self): self.feed(b"") self.data = self.decoder = None if not self.finished: - raise IOError("image was incomplete") + raise OSError("image was incomplete") if not self.image: - raise IOError("cannot parse this image") + raise OSError("cannot parse this image") if self.data: # incremental parsing not possible; reopen the file # not that we have all data @@ -514,7 +512,7 @@ def _save(im, fp, tile, bufsize=0): if s: break if s < 0: - raise IOError("encoder error %d when writing image file" % s) + raise OSError("encoder error %d when writing image file" % s) e.cleanup() else: # slight speedup: compress to real file object @@ -529,7 +527,7 @@ def _save(im, fp, tile, bufsize=0): else: s = e.encode_to_file(fh, bufsize) if s < 0: - raise IOError("encoder error %d when writing image file" % s) + raise OSError("encoder error %d when writing image file" % s) e.cleanup() if hasattr(fp, "flush"): fp.flush() @@ -559,7 +557,7 @@ def _safe_read(fp, size): return b"".join(data) -class PyCodecState(object): +class PyCodecState: def __init__(self): self.xsize = 0 self.ysize = 0 @@ -570,7 +568,7 @@ def extents(self): return (self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize) -class PyDecoder(object): +class PyDecoder: """ Python implementation of a format decoder. Override this class and add the decoding logic in the `decode` method. diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index fa4162b6114..6b0f5eb376b 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -14,9 +14,6 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import division - import functools try: @@ -25,7 +22,7 @@ numpy = None -class Filter(object): +class Filter: pass @@ -498,7 +495,7 @@ def transform(self, callback, with_normals=False, channels=None, target_mode=Non r / (size1D - 1), g / (size2D - 1), b / (size3D - 1), - *values + *values, ) else: values = callback(*values) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 5cce9af8e43..027e4c42e6c 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -25,17 +25,19 @@ # See the README file for information on usage and redistribution. # +import base64 import os import sys +from io import BytesIO from . import Image -from ._util import isDirectory, isPath, py3 +from ._util import isDirectory, isPath LAYOUT_BASIC = 0 LAYOUT_RAQM = 1 -class _imagingft_not_installed(object): +class _imagingft_not_installed: # module placeholder def __getattr__(self, id): raise ImportError("The _imagingft C module is not installed") @@ -63,13 +65,16 @@ def __getattr__(self, id): # -------------------------------------------------------------------- -class ImageFont(object): +class ImageFont: "PIL font wrapper" 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) @@ -79,11 +84,14 @@ def _load_pilfont(self, filename): if image and image.mode in ("1", "L"): break else: - raise IOError("cannot find glyph data file") + 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): @@ -145,7 +153,7 @@ def getmask(self, text, mode="", *args, **kwargs): # truetype factory function to create font objects. -class FreeTypeFont(object): +class FreeTypeFont: "FreeType font wrapper (requires _imagingft service)" def __init__(self, font=None, size=10, index=0, encoding="", layout_engine=None): @@ -542,7 +550,7 @@ def set_variation_by_axes(self, axes): raise NotImplementedError("FreeType 2.9.1 or greater is required") -class TransposedFont(object): +class TransposedFont: "Wrapper for writing rotated or mirrored text" def __init__(self, font, orientation=None): @@ -638,7 +646,7 @@ def freetype(font): try: return freetype(font) - except IOError: + except OSError: if not isPath(font): raise ttf_filename = os.path.basename(font) @@ -695,15 +703,12 @@ def load_path(filename): for directory in sys.path: if isDirectory(directory): if not isinstance(filename, str): - if py3: - filename = filename.decode("utf-8") - else: - filename = filename.encode("utf-8") + filename = filename.decode("utf-8") try: return load(os.path.join(directory, filename)) - except IOError: + except OSError: pass - raise IOError("cannot find font file") + raise OSError("cannot find font file") def load_default(): @@ -713,9 +718,6 @@ def load_default(): :return: A font object. """ - from io import BytesIO - import base64 - f = ImageFont() f._load_pilfont_data( # courB08 diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 9b4413536ea..66e2e856009 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -19,42 +19,52 @@ from . import Image -if sys.platform == "win32": - grabber = Image.core.grabscreen -elif sys.platform == "darwin": +if sys.platform == "darwin": import os import tempfile import subprocess -else: - raise ImportError("ImageGrab is macOS and Windows only") -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 = grabber(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 IOError("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 @@ -82,11 +92,13 @@ def grabclipboard(): im.load() os.unlink(filepath) return im - else: - data = Image.core.grabclipboard() + elif sys.platform == "win32": + data = Image.core.grabclipboard_win32() if isinstance(data, bytes): from . import BmpImagePlugin import io return BmpImagePlugin.DibImageFile(io.BytesIO(data)) return data + else: + raise NotImplementedError("ImageGrab.grabclipboard() is macOS and Windows only") diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 392151c10f8..adbb940000e 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -15,15 +15,9 @@ # See the README file for information on usage and redistribution. # -from . import Image, _imagingmath -from ._util import py3 - -try: - import builtins -except ImportError: - import __builtin__ +import builtins - builtins = __builtin__ +from . import Image, _imagingmath VERBOSE = 0 @@ -32,7 +26,7 @@ def _isconstant(v): return isinstance(v, (int, float)) -class _Operand(object): +class _Operand: """Wraps an image operand, providing standard operators""" def __init__(self, im): @@ -101,11 +95,6 @@ def __bool__(self): # an image is "true" if it contains at least one non-zero pixel return self.im.getbbox() is not None - if not py3: - # Provide __nonzero__ for pre-Py3k - __nonzero__ = __bool__ - del __bool__ - def __abs__(self): return self.apply("abs", self) @@ -152,13 +141,6 @@ def __pow__(self, other): def __rpow__(self, other): return self.apply("pow", other, self) - if not py3: - # Provide __div__ and __rdiv__ for pre-Py3k - __div__ = __truediv__ - __rdiv__ = __rtruediv__ - del __truediv__ - del __rtruediv__ - # bitwise def __invert__(self): return self.apply("invert", self) diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 596be7b9d7e..988288329b8 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -17,7 +17,7 @@ _modes = None -class ModeDescriptor(object): +class ModeDescriptor: """Wrapper for mode strings.""" def __init__(self, mode, bands, basemode, basetype): diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 61199234bc5..d1ec09eace4 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -5,8 +5,6 @@ # # Copyright (c) 2014 Dov Grobgeld -from __future__ import print_function - import re from . import Image, _imagingmorph @@ -27,7 +25,7 @@ # fmt: on -class LutBuilder(object): +class LutBuilder: """A class for building a MorphLut from a descriptive language The input patterns is a list of a strings sequences like these:: @@ -178,7 +176,7 @@ def build_lut(self): return self.lut -class MorphOp(object): +class MorphOp: """A class for binary morphological operators""" def __init__(self, lut=None, op_name=None, patterns=None): diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 5052cb74d4b..e4e0840b8a9 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,7 +21,6 @@ import operator from . import Image -from ._util import isStringType # # helpers @@ -39,7 +38,7 @@ def _border(border): def _color(color, mode): - if isStringType(color): + if isinstance(color, str): from . import ImageColor color = ImageColor.getcolor(color, mode) @@ -55,7 +54,7 @@ def _lut(image, lut): lut = lut + lut + lut return image.point(lut) else: - raise IOError("not supported for this image mode") + raise OSError("not supported for this image mode") # @@ -222,7 +221,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. @@ -231,10 +230,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 @@ -243,7 +243,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) @@ -281,7 +281,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 @@ -289,8 +289,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: @@ -298,7 +298,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) @@ -364,7 +364,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. @@ -375,7 +375,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). @@ -420,10 +420,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 2d4f5cb6b50..e0d439c9841 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -21,7 +21,7 @@ from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile -class ImagePalette(object): +class ImagePalette: """ Color palette for palette mapped images @@ -216,6 +216,6 @@ def load(filename): # traceback.print_exc() pass else: - raise IOError("cannot load palette") + raise OSError("cannot load palette") return lut # data, rawmode diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 2edb0a12bae..dfe2f80bd81 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -17,18 +17,12 @@ # import sys -import warnings from io import BytesIO from . import Image -from ._util import isPath, py3 +from ._util import isPath -qt_versions = [["5", "PyQt5"], ["side2", "PySide2"], ["4", "PyQt4"], ["side", "PySide"]] - -WARNING_TEXT = ( - "Support for EOL {} is deprecated and will be removed in a future version. " - "Please upgrade to PyQt5 or PySide2." -) +qt_versions = [["5", "PyQt5"], ["side2", "PySide2"]] # If a version has already been imported, attempt it first qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True) @@ -40,16 +34,6 @@ elif qt_module == "PySide2": from PySide2.QtGui import QImage, qRgba, QPixmap from PySide2.QtCore import QBuffer, QIODevice - elif qt_module == "PyQt4": - from PyQt4.QtGui import QImage, qRgba, QPixmap - from PyQt4.QtCore import QBuffer, QIODevice - - warnings.warn(WARNING_TEXT.format(qt_module), DeprecationWarning) - elif qt_module == "PySide": - from PySide.QtGui import QImage, qRgba, QPixmap - from PySide.QtCore import QBuffer, QIODevice - - warnings.warn(WARNING_TEXT.format(qt_module), DeprecationWarning) except (ImportError, RuntimeError): continue qt_is_installed = True @@ -81,11 +65,7 @@ def fromqimage(im): im.save(buffer, "ppm") b = BytesIO() - try: - b.write(buffer.data()) - except TypeError: - # workaround for Python 2 - b.write(str(buffer.data())) + b.write(buffer.data()) buffer.close() b.seek(0) @@ -141,10 +121,7 @@ def _toqclass_helper(im): # handle filename, if given instead of image name if hasattr(im, "toUtf8"): # FIXME - is this really the best way to do this? - if py3: - im = str(im.toUtf8(), "utf-8") - else: - im = unicode(im.toUtf8(), "utf-8") # noqa: F821 + im = str(im.toUtf8(), "utf-8") if isPath(im): im = Image.open(im) @@ -196,8 +173,7 @@ def __init__(self, im): # buffer, so this buffer has to hang on for the life of the image. # Fixes https://github.com/python-pillow/Pillow/issues/1370 self.__data = im_data["data"] - QImage.__init__( - self, + super().__init__( self.__data, im_data["im"].size[0], im_data["im"].size[1], diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index f9be92d4838..4e9f5c210b7 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -16,7 +16,7 @@ ## -class Iterator(object): +class Iterator: """ This class implements an iterator object that can be used to loop over an image sequence. @@ -52,9 +52,6 @@ def __next__(self): except EOFError: raise StopIteration - def next(self): - return self.__next__() - def all_frames(im, func=None): """ diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index ca622c52506..fc50894236b 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -11,21 +11,15 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - import os +import shutil import subprocess import sys import tempfile +from shlex import quote from PIL import Image -if sys.version_info.major >= 3: - from shlex import quote -else: - from pipes import quote - _viewers = [] @@ -56,7 +50,7 @@ def show(image, title=None, **options): return 0 -class Viewer(object): +class Viewer: """Base class for viewers.""" # main api @@ -127,10 +121,8 @@ 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 = "(%s %s; sleep 20; rm -f %s)&" % ( - command, - quote(file), - quote(file), + command = "({} {}; sleep 20; rm -f {})&".format( + command, quote(file), quote(file) ) return command @@ -154,23 +146,13 @@ def show_file(self, file, **options): # unixoids - def which(executable): - path = os.environ.get("PATH") - if not path: - return None - for dirname in path.split(os.pathsep): - filename = os.path.join(dirname, executable) - if os.path.isfile(filename) and os.access(filename, os.X_OK): - return filename - return None - class UnixViewer(Viewer): format = "PNG" options = {"compress_level": 1} def get_command(self, file, **options): command = self.get_command_ex(file, **options)[0] - return "(%s %s; rm -f %s)&" % (command, quote(file), quote(file)) + return "({} {}; rm -f {})&".format(command, quote(file), quote(file)) def show_file(self, file, **options): """Display given file""" @@ -192,7 +174,7 @@ def get_command_ex(self, file, **options): command = executable = "display" return command, executable - if which("display"): + if shutil.which("display"): register(DisplayViewer) class EogViewer(UnixViewer): @@ -200,7 +182,7 @@ def get_command_ex(self, file, **options): command = executable = "eog" return command, executable - if which("eog"): + if shutil.which("eog"): register(EogViewer) class XVViewer(UnixViewer): @@ -212,7 +194,7 @@ def get_command_ex(self, file, title=None, **options): command += " -name %s" % quote(title) return command, executable - if which("xv"): + if shutil.which("xv"): register(XVViewer) if __name__ == "__main__": @@ -221,4 +203,5 @@ def get_command_ex(self, file, title=None, **options): 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/ImageStat.py b/src/PIL/ImageStat.py index 9ba16fd851d..50bafc97294 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -26,7 +26,7 @@ import operator -class Stat(object): +class Stat: def __init__(self, image_or_list, mask=None): try: if mask: diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index fd480007ad4..ee707cffb5b 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -25,17 +25,11 @@ # See the README file for information on usage and redistribution. # -import sys +import tkinter from io import BytesIO from . import Image -if sys.version_info.major > 2: - import tkinter -else: - import Tkinter as tkinter - - # -------------------------------------------------------------------- # Check for Tkinter interface hooks @@ -68,7 +62,7 @@ def _get_image_from_kw(kw): # PhotoImage -class PhotoImage(object): +class PhotoImage: """ A Tkinter-compatible photo image. This can be used everywhere Tkinter expects an image object. If the image is an RGBA @@ -209,7 +203,7 @@ def paste(self, im, box=None): # BitmapImage -class BitmapImage(object): +class BitmapImage: """ A Tkinter-compatible bitmap image. This can be used everywhere Tkinter expects an image object. @@ -296,10 +290,10 @@ def __init__(self, master, im): self.image = BitmapImage(im, foreground="white", master=master) else: self.image = PhotoImage(im, master=master) - tkinter.Label.__init__(self, master, image=self.image, bg="black", bd=0) + super().__init__(master, image=self.image, bg="black", bd=0) if not tkinter._default_root: - raise IOError("tkinter not initialized") + raise OSError("tkinter not initialized") top = tkinter.Toplevel() if title: top.title(title) diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index ed2c18ec425..927b1694b3e 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -20,7 +20,7 @@ from . import Image -class HDC(object): +class HDC: """ Wraps an HDC integer. The resulting object can be passed to the :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` @@ -34,7 +34,7 @@ def __int__(self): return self.dc -class HWND(object): +class HWND: """ Wraps an HWND integer. The resulting object can be passed to the :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` @@ -48,7 +48,7 @@ def __int__(self): return self.wnd -class Dib(object): +class Dib: """ A Windows bitmap with the given mode and size. The mode can be one of "1", "L", "P", or "RGB". @@ -186,7 +186,7 @@ def tobytes(self): return self.image.tobytes() -class Window(object): +class Window: """Create a Window with the given title size.""" def __init__(self, title="PIL", width=None, height=None): @@ -224,7 +224,7 @@ def __init__(self, image, title="PIL"): image = Dib(image) self.image = image width, height = image.size - Window.__init__(self, title, width=width, height=height) + super().__init__(title, width=width, height=height) def ui_handle_repair(self, dc, x0, y0, x1, y1): self.image.draw(dc, (x0, y0, x1, y1)) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index a9e991fbe55..21ffd747584 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -19,11 +19,6 @@ from . import Image, ImageFile -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.2" - - # # -------------------------------------------------------------------- diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index aedf2e48ccb..b2f976dda0d 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -14,19 +14,12 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - import os import tempfile from . import Image, ImageFile from ._binary import i8, i16be as i16, i32be as i32, o8 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.3" - COMPRESSION = {1: "raw", 5: "jpeg"} PAD = o8(0) * 4 @@ -75,7 +68,7 @@ def field(self): # field size size = i8(s[3]) if size > 132: - raise IOError("illegal field length in IPTC/NAA file") + raise OSError("illegal field length in IPTC/NAA file") elif size == 128: size = 0 elif size > 128: @@ -126,7 +119,7 @@ def _open(self): try: compression = COMPRESSION[self.getint((3, 120))] except KeyError: - raise IOError("Unknown IPTC image compression") + raise OSError("Unknown IPTC image compression") # tile if tag == (8, 10): @@ -165,9 +158,9 @@ def load(self): 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) @@ -215,7 +208,7 @@ def getiptcinfo(im): return None # no properties # create an IptcImagePlugin object without initializing it - class FakeImage(object): + class FakeImage: pass im = FakeImage() diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 37f111778c6..0b0d433db41 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -18,10 +18,6 @@ from . import Image, ImageFile -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - def _parse_codestream(fp): """Parse the JPEG 2000 codestream to extract the size and component @@ -180,7 +176,7 @@ def _open(self): 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 @@ -204,23 +200,33 @@ def _open(self): "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 020b952192f..2aa029efbff 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -31,24 +31,18 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - import array import io +import os import struct +import subprocess +import tempfile import warnings from . import Image, ImageFile, TiffImagePlugin from ._binary import i8, i16be as i16, i32be as i32, o8 -from ._util import isStringType from .JpegPresets import presets -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.6" - - # # Parser @@ -106,27 +100,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, @@ -135,10 +127,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 @@ -169,10 +162,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 @@ -182,6 +176,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)) @@ -415,7 +410,8 @@ def draft(self, mode, size): 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 @@ -438,16 +434,13 @@ def draft(self, mode, size): 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): # ALTERNATIVE: handle JPEGs via the IJG command line utilities - import subprocess - import tempfile - import os - f, path = tempfile.mkstemp() os.close(f) if os.path.exists(self.filename): @@ -456,9 +449,9 @@ def load_djpeg(self): 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) @@ -618,23 +611,23 @@ def _save(im, fp, filename): try: rawmode = RAWMODE[im.mode] except KeyError: - raise IOError("cannot write mode %s as JPEG" % im.mode) + raise OSError("cannot write mode %s as JPEG" % im.mode) 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): @@ -642,7 +635,7 @@ def _save(im, fp, filename): else: if subsampling in presets: subsampling = presets[subsampling].get("subsampling", -1) - if isStringType(qtables) and qtables in presets: + if isinstance(qtables, str) and qtables in presets: qtables = presets[qtables].get("quantization") if subsampling == "4:4:4": @@ -663,7 +656,7 @@ def _save(im, fp, filename): def validate_qtables(qtables): if qtables is None: return qtables - if isStringType(qtables): + if isinstance(qtables, str): try: lines = [ int(num) @@ -757,8 +750,8 @@ def validate_qtables(qtables): # 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] @@ -772,9 +765,6 @@ def validate_qtables(qtables): def _save_cjpeg(im, fp, filename): # ALTERNATIVE: handle JPEGs via the IJG command line utilities. - import os - import subprocess - tempfile = im._dump() subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) try: diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 387844f8e8c..012bf81b01c 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -33,7 +33,10 @@ 4:2:0. You can get the subsampling of a JPEG with the -`JpegImagePlugin.get_subsampling(im)` function. +`JpegImagePlugin.get_sampling(im)` function. + +In JPEG compressed data a JPEG marker is used instead of an EXIF tag. +(ref.: https://www.exiv2.org/tags.html) Quantization tables diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index bddd33abb7d..cd047fe9d9d 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -20,10 +20,6 @@ from . import Image, ImageFile -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.2" - def _accept(s): return s[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index b48905bda00..8610988fcd2 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -21,11 +21,6 @@ from . import Image, TiffImagePlugin -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - - # # -------------------------------------------------------------------- @@ -51,7 +46,7 @@ def _open(self): try: self.ole = olefile.OleFileIO(self.fp) - except IOError: + except OSError: raise SyntaxError("not an MIC file; invalid OLE file") # find ACI subfiles with Image members (maybe not the diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 9c662fcc288..a358dfdce62 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -17,16 +17,11 @@ from . import Image, ImageFile from ._binary import i8 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - - # # Bitstream parser -class BitStream(object): +class BitStream: def __init__(self, fp): self.fp = fp self.bits = 0 diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 938f2a5a646..e97176d572b 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -21,10 +21,6 @@ from . import Image, ImageFile, JpegImagePlugin from ._binary import i16be as i16 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - def _accept(prefix): return JpegImagePlugin._accept(prefix) @@ -86,7 +82,10 @@ def seek(self, frame): 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 7315ab66ed6..2b2937ecfcf 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -29,11 +29,6 @@ from . import Image, ImageFile from ._binary import i8, i16le as i16, o16le as o16 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - - # # read MSP files @@ -122,7 +117,7 @@ def decode(self, buffer): "<%dH" % (self.state.ysize), self.fd.read(self.state.ysize * 2) ) except struct.error: - raise IOError("Truncated MSP file in row map") + raise OSError("Truncated MSP file in row map") for x, rowlen in enumerate(rowmap): try: @@ -131,7 +126,7 @@ def decode(self, buffer): continue row = self.fd.read(rowlen) if len(row) != rowlen: - raise IOError( + raise OSError( "Truncated MSP file, expected %d bytes on row %s", (rowlen, x) ) idx = 0 @@ -148,7 +143,7 @@ def decode(self, buffer): idx += runcount except struct.error: - raise IOError("Corrupted MSP file in row %d" % x) + raise OSError("Corrupted MSP file in row %d" % x) self.set_as_raw(img.getvalue(), ("1", 0, 1)) @@ -165,7 +160,7 @@ def decode(self, buffer): def _save(im, fp, filename): if im.mode != "1": - raise IOError("cannot write mode %s as MSP" % im.mode) + raise OSError("cannot write mode %s as MSP" % im.mode) # create MSP header header = [0] * 16 diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index f37701ce979..762d31e887a 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -18,13 +18,12 @@ import sys from . import EpsImagePlugin -from ._util import py3 ## # Simple Postscript graphics interface. -class PSDraw(object): +class PSDraw: """ Sets up printing to the given file. If **fp** is omitted, :py:attr:`sys.stdout` is assumed. @@ -36,7 +35,7 @@ def __init__(self, fp=None): self.fp = fp def _fp_write(self, to_write): - if not py3 or self.fp == sys.stdout: + if self.fp == sys.stdout: self.fp.write(to_write) else: self.fp.write(bytes(to_write, "UTF-8")) @@ -72,7 +71,7 @@ def setfont(self, font, size): """ if font not in self.isofont: # reencode font - self._fp_write("/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font, font)) + self._fp_write("/PSDraw-{} ISOLatin1Encoding /{} E\n".format(font, font)) self.isofont[font] = 1 # rough self._fp_write("/F0 %d /PSDraw-%s F\n" % (size, font)) @@ -120,8 +119,8 @@ def image(self, box, im, dpi=None): 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]) @@ -133,12 +132,12 @@ def image(self, box, im, dpi=None): y = ymax dx = (xmax - x) / 2 + box[0] dy = (ymax - y) / 2 + box[1] - self._fp_write("gsave\n%f %f translate\n" % (dx, dy)) + self._fp_write("gsave\n{:f} {:f} translate\n".format(dx, dy)) 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" % (sx, sy)) + self._fp_write("{:f} {:f} scale\n".format(sx, sy)) EpsImagePlugin._save(im, self.fp, None, 0) self._fp_write("\ngrestore\n") diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index ab22d5f0c1f..73f1b4b27c4 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -19,7 +19,7 @@ # File handler for Teragon-style palette files. -class PaletteFile(object): +class PaletteFile: rawmode = "RGB" diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index dd068d79412..804ece34a6e 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -10,10 +10,6 @@ from . import Image, ImageFile from ._binary import o8, o16be as o16b -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "1.0" - # fmt: off _Palm8BitColormapValues = ( # noqa: E131 (255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255), @@ -141,7 +137,7 @@ def _save(im, fp, filename): bpp = im.info["bpp"] im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval)) else: - raise IOError("cannot write mode %s as Palm" % im.mode) + raise OSError("cannot write mode %s as Palm" % im.mode) # we ignore the palette here im.mode = "P" @@ -157,7 +153,7 @@ def _save(im, fp, filename): else: - raise IOError("cannot write mode %s as Palm" % im.mode) + raise OSError("cannot write mode %s as Palm" % im.mode) # # make sure image data is available diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 6f01845ece9..625f5564666 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -18,11 +18,6 @@ from . import Image, ImageFile from ._binary import i8 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - - ## # Image plugin for PhotoCD images. This plugin only reads the 768x512 # image from the file; higher resolutions are encoded in a proprietary diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 074124612d2..c463533cd01 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -56,13 +56,15 @@ class PcfFontFile(FontFile.FontFile): 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: raise SyntaxError("not a PCF file") - FontFile.FontFile.__init__(self) + super().__init__() count = l32(fp.read(4)) self.toc = {} @@ -184,7 +186,7 @@ def _load_bitmaps(self, metrics): nbitmaps = i32(fp.read(4)) if nbitmaps != len(metrics): - raise IOError("Wrong number of bitmaps") + raise OSError("Wrong number of bitmaps") offsets = [] for i in range(nbitmaps): @@ -229,12 +231,17 @@ def _load_encoding(self): 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 397af8c1067..6cf10deb3f7 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -33,10 +33,6 @@ logger = logging.getLogger(__name__) -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.6" - def _accept(prefix): return i8(prefix[0]) == 10 and i8(prefix[1]) in [0, 2, 3, 5] @@ -107,7 +103,7 @@ def _open(self): rawmode = "RGB;L" else: - raise IOError("unknown PCX mode") + raise OSError("unknown PCX mode") self.mode = mode self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 1fd40f5bad9..47500baf7d0 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -24,12 +24,7 @@ import os import time -from . import Image, ImageFile, ImageSequence, PdfParser - -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.5" - +from . import Image, ImageFile, ImageSequence, PdfParser, __version__ # # -------------------------------------------------------------------- @@ -82,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 PIL PDF driver " + __version__) + existing_pdf.write_comment("created by Pillow {} PDF driver".format(__version__)) # # pages @@ -219,9 +214,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 0ec6bba14d6..fdb35eded56 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -7,25 +7,6 @@ import time import zlib -from ._util import py3 - -try: - from UserDict import UserDict # Python 2.x -except ImportError: - UserDict = collections.UserDict # Python 3.x - - -if py3: # Python 3.x - - def make_bytes(s): - return s.encode("us-ascii") - - -else: # Python 2.x - - def make_bytes(s): # pragma: no cover - return s # pragma: no cover - # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set # on page 656 @@ -34,57 +15,55 @@ def encode_text(s): PDFDocEncoding = { - 0x16: u"\u0017", - 0x18: u"\u02D8", - 0x19: u"\u02C7", - 0x1A: u"\u02C6", - 0x1B: u"\u02D9", - 0x1C: u"\u02DD", - 0x1D: u"\u02DB", - 0x1E: u"\u02DA", - 0x1F: u"\u02DC", - 0x80: u"\u2022", - 0x81: u"\u2020", - 0x82: u"\u2021", - 0x83: u"\u2026", - 0x84: u"\u2014", - 0x85: u"\u2013", - 0x86: u"\u0192", - 0x87: u"\u2044", - 0x88: u"\u2039", - 0x89: u"\u203A", - 0x8A: u"\u2212", - 0x8B: u"\u2030", - 0x8C: u"\u201E", - 0x8D: u"\u201C", - 0x8E: u"\u201D", - 0x8F: u"\u2018", - 0x90: u"\u2019", - 0x91: u"\u201A", - 0x92: u"\u2122", - 0x93: u"\uFB01", - 0x94: u"\uFB02", - 0x95: u"\u0141", - 0x96: u"\u0152", - 0x97: u"\u0160", - 0x98: u"\u0178", - 0x99: u"\u017D", - 0x9A: u"\u0131", - 0x9B: u"\u0142", - 0x9C: u"\u0153", - 0x9D: u"\u0161", - 0x9E: u"\u017E", - 0xA0: u"\u20AC", + 0x16: "\u0017", + 0x18: "\u02D8", + 0x19: "\u02C7", + 0x1A: "\u02C6", + 0x1B: "\u02D9", + 0x1C: "\u02DD", + 0x1D: "\u02DB", + 0x1E: "\u02DA", + 0x1F: "\u02DC", + 0x80: "\u2022", + 0x81: "\u2020", + 0x82: "\u2021", + 0x83: "\u2026", + 0x84: "\u2014", + 0x85: "\u2013", + 0x86: "\u0192", + 0x87: "\u2044", + 0x88: "\u2039", + 0x89: "\u203A", + 0x8A: "\u2212", + 0x8B: "\u2030", + 0x8C: "\u201E", + 0x8D: "\u201C", + 0x8E: "\u201D", + 0x8F: "\u2018", + 0x90: "\u2019", + 0x91: "\u201A", + 0x92: "\u2122", + 0x93: "\uFB01", + 0x94: "\uFB02", + 0x95: "\u0141", + 0x96: "\u0152", + 0x97: "\u0160", + 0x98: "\u0178", + 0x99: "\u017D", + 0x9A: "\u0131", + 0x9B: "\u0142", + 0x9C: "\u0153", + 0x9D: "\u0161", + 0x9E: "\u017E", + 0xA0: "\u20AC", } def decode_text(b): if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE: return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be") - elif py3: # Python 3.x + else: return "".join(PDFDocEncoding.get(byte, chr(byte)) for byte in b) - else: # Python 2.x - return u"".join(PDFDocEncoding.get(ord(byte), byte) for byte in b) class PdfFormatError(RuntimeError): @@ -196,10 +175,10 @@ def write(self, f): 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( @@ -212,10 +191,8 @@ def write(self, f): 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 @@ -247,40 +224,27 @@ def __repr__(self): def from_pdf_stream(cls, data): return cls(PdfParser.interpret_name(data)) - allowed_chars = set(range(33, 127)) - set(ord(c) for c in "#%/()<>[]{}") + allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} def __bytes__(self): result = bytearray(b"/") for b in self.name: - if py3: # Python 3.x - if b in self.allowed_chars: - result.append(b) - else: - result.extend(make_bytes("#%02X" % b)) - else: # Python 2.x - if ord(b) in self.allowed_chars: - result.append(b) - else: - result.extend(b"#%02X" % ord(b)) + if b in self.allowed_chars: + result.append(b) + else: + 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(UserDict): +class PdfDict(collections.UserDict): def __setattr__(self, key, value): if key == "data": - if hasattr(UserDict, "__setattr__"): - UserDict.__setattr__(self, key, value) - else: - self.__dict__[key] = value + collections.UserDict.__setattr__(self, key, value) else: self[key.encode("us-ascii")] = value @@ -324,23 +288,13 @@ def __bytes__(self): out.extend(b"\n>>") return bytes(out) - if not py3: - __str__ = __bytes__ - class PdfBinary: def __init__(self, data): self.data = data - if py3: # Python 3.x - - def __bytes__(self): - return make_bytes("<%s>" % "".join("%02X" % b for b in self.data)) - - else: # Python 2.x - - def __str__(self): - return "<%s>" % "".join("%02X" % ord(b) for b in self.data) + def __bytes__(self): + return b"<%s>" % b"".join(b"%02X" % b for b in self.data) class PdfStream: @@ -382,9 +336,7 @@ def pdf_repr(x): return bytes(PdfDict(x)) elif isinstance(x, list): return bytes(PdfArray(x)) - elif (py3 and isinstance(x, str)) or ( - not py3 and isinstance(x, unicode) # noqa: F821 - ): + elif isinstance(x, str): return pdf_repr(encode_text(x)) elif isinstance(x, bytes): # XXX escape more chars? handle binary garbage @@ -471,7 +423,7 @@ def write_header(self): self.f.write(b"%PDF-1.4\n") def write_comment(self, s): - self.f.write(("%% %s\n" % (s,)).encode("utf-8")) + self.f.write(("% {}\n".format(s)).encode("utf-8")) def write_catalog(self): self.del_root() @@ -533,7 +485,7 @@ def write_xref_and_trailer(self, new_root_ref=None): 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): diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index dc71ca17a88..5ea32ba89be 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -22,11 +22,6 @@ from . import Image, ImageFile from ._binary import i16le as i16 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - - # # helpers diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index be237b3eeb3..81a9e36af5e 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -31,18 +31,15 @@ # 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 ._util import py3 - -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.9" +from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence +from ._binary import i8, i16be as i16, i32be as i32, o8, o16be as o16, o32be as o32 logger = logging.getLogger(__name__) @@ -86,6 +83,16 @@ MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK +# APNG frame disposal modes +APNG_DISPOSE_OP_NONE = 0 +APNG_DISPOSE_OP_BACKGROUND = 1 +APNG_DISPOSE_OP_PREVIOUS = 2 + +# APNG frame blend modes +APNG_BLEND_OP_SOURCE = 0 +APNG_BLEND_OP_OVER = 1 + + def _safe_zlib_decompress(s): dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) @@ -102,7 +109,7 @@ def _crc32(data, seed=0): # Support classes. Suitable for PNG and related formats like MNG etc. -class ChunkStream(object): +class ChunkStream: def __init__(self, fp): self.fp = fp @@ -180,7 +187,7 @@ def verify(self, endchunk=b"IEND"): try: cid, pos, length = self.read() except struct.error: - raise IOError("truncated PNG file") + raise OSError("truncated PNG file") if cid == endchunk: break @@ -212,7 +219,7 @@ def __new__(cls, text, lang=None, tkey=None): return self -class PngInfo(object): +class PngInfo: """ PNG chunk container (for use with save(pnginfo=)) @@ -293,8 +300,7 @@ def add_text(self, key, value, zip=False): class PngStream(ChunkStream): def __init__(self, fp): - - ChunkStream.__init__(self, fp) + super().__init__(fp) # local copies of Image attributes self.im_info = {} @@ -304,6 +310,9 @@ def __init__(self, fp): 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 @@ -315,6 +324,18 @@ def check_text_memory(self, chunklen): % self.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 @@ -362,7 +383,13 @@ def chunk_IHDR(self, pos, length): 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 @@ -450,9 +477,8 @@ def chunk_tEXt(self, pos, length): k = s v = b"" if k: - if py3: - k = k.decode("latin-1", "strict") - v = v.decode("latin-1", "replace") + k = k.decode("latin-1", "strict") + v = v.decode("latin-1", "replace") self.im_info[k] = self.im_text[k] = v self.check_text_memory(len(v)) @@ -487,9 +513,8 @@ def chunk_zTXt(self, pos, length): v = b"" if k: - if py3: - k = k.decode("latin-1", "strict") - v = v.decode("latin-1", "replace") + k = k.decode("latin-1", "strict") + v = v.decode("latin-1", "replace") self.im_info[k] = self.im_text[k] = v self.check_text_memory(len(v)) @@ -524,14 +549,13 @@ def chunk_iTXt(self, pos, length): return s else: return s - if py3: - try: - k = k.decode("latin-1", "strict") - lang = lang.decode("utf-8", "strict") - tk = tk.decode("utf-8", "strict") - v = v.decode("utf-8", "strict") - except UnicodeError: - return s + try: + k = k.decode("latin-1", "strict") + lang = lang.decode("utf-8", "strict") + tk = tk.decode("utf-8", "strict") + v = v.decode("utf-8", "strict") + except UnicodeError: + return s self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk) self.check_text_memory(len(v)) @@ -546,9 +570,49 @@ def chunk_eXIf(self, pos, length): # 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 @@ -571,9 +635,10 @@ def _open(self): if self.fp.read(8) != _MAGIC: raise SyntaxError("not a PNG file") + self.__fp = self.fp # - # Parse headers up to the first IDAT chunk + # Parse headers up to the first IDAT or fDAT chunk self.png = PngStream(self.fp) @@ -607,12 +672,27 @@ def _open(self): 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 + 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._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) @property def text(self): @@ -620,9 +700,25 @@ def text(self): 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 + @property + def n_frames(self): + if self._n_frames is None: + return 1 + return self._n_frames + + @property + def is_animated(self): + return self._n_frames is not None and self._n_frames > 1 + def verify(self): """Verify PNG file""" @@ -639,6 +735,97 @@ def verify(self): 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: + self.seek(last_frame) + raise EOFError("no more images in APNG file") + + 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("cannot seek to frame %d" % 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""" @@ -658,11 +845,18 @@ def load_read(self, read_bytes): 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: @@ -686,31 +880,84 @@ def load_end(self): 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) 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("RGBA", self.size, (0, 0, 0, 0)) + 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) + + if self._exif is None: + self._exif = Image.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) + return self._exif + + def _close__fp(self): + try: + if self.__fp != self.fp: + self.__fp.close() + except AttributeError: + pass + finally: + self.__fp = None # -------------------------------------------------------------------- @@ -746,7 +993,7 @@ def putchunk(fp, cid, *data): fp.write(o32(crc)) -class _idat(object): +class _idat: # wrap output from the encoder in IDAT chunks def __init__(self, fp, chunk): @@ -757,7 +1004,147 @@ def write(self, data): 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)), o32(loop), # 0: num_frames # 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 @@ -799,7 +1186,7 @@ def _save(im, fp, filename, chunk=putchunk): try: rawmode, mode = _OUTMODES[mode] except KeyError: - raise IOError("cannot write mode %s as PNG" % mode) + raise OSError("cannot write mode %s as PNG" % mode) # # write minimal PNG file @@ -874,7 +1261,7 @@ def _save(im, fp, filename, chunk=putchunk): if "transparency" in im.encoderinfo: # don't bother with transparency if it's an RGBA # and it's in the info dict. It's probably just stale. - raise IOError("cannot use transparency for this mode") + raise OSError("cannot use transparency for this mode") else: if im.mode == "P" and im.im.getpalettemode() == "RGBA": alpha = im.im.getpalette("RGBA", "A") @@ -891,7 +1278,6 @@ 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: @@ -907,7 +1293,10 @@ 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)]) chunk(fp, b"IEND", b"") @@ -922,7 +1311,7 @@ def _save(im, fp, filename, chunk=putchunk): def getchunks(im, **params): """Return a list of PNG chunks representing this image.""" - class collector(object): + class collector: data = [] def write(self, data): @@ -952,6 +1341,7 @@ def append(fp, cid, *data): 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 c3e9eed6dac..35a77bafb37 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -17,10 +17,6 @@ from . import Image, ImageFile -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.2" - # # -------------------------------------------------------------------- @@ -139,7 +135,7 @@ def _save(im, fp, filename): elif im.mode == "RGBA": rawmode, head = "RGB", b"P6" else: - raise IOError("cannot write mode %s as PPM" % im.mode) + raise OSError("cannot write mode %s as PPM" % im.mode) 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 f72ad5f4433..cceb85c5b35 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -16,10 +16,6 @@ # See the README file for information on usage and redistribution. # -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.4" - import io from . import Image, ImageFile, ImagePalette @@ -75,7 +71,7 @@ def _open(self): mode, channels = MODES[(psd_mode, psd_bits)] if channels > psd_channels: - raise IOError("not enough channels") + raise OSError("not enough channels") self.mode = mode self._size = i32(s[18:]), i32(s[14:]) diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 2ab06f93ffe..359a9491975 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -40,7 +40,7 @@ ffi.cdef(defs) -class PyAccess(object): +class PyAccess: def __init__(self, img, readonly=False): vals = dict(img.im.unsafe_ptrs) self.readonly = readonly diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 99408fdc344..ddd3de379aa 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -27,11 +27,6 @@ from . import Image, ImageFile from ._binary import i8, i16be as i16, o8 -from ._util import py3 - -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.3" def _accept(prefix): @@ -164,7 +159,9 @@ 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: %s vs %s" % (z, len(im.getbands())) + "incorrect number of bands in SGI write: {} vs {}".format( + z, len(im.getbands()) + ) ) # Minimum Byte value @@ -173,8 +170,7 @@ def _save(im, fp, filename): pinmax = 255 # Image name (79 characters max, truncated below in write) imgName = os.path.splitext(os.path.basename(filename))[0] - if py3: - imgName = imgName.encode("ascii", "ignore") + imgName = imgName.encode("ascii", "ignore") # Standard representation of pixel in the file colormap = 0 fp.write(struct.pack(">h", magicNumber)) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index f1cae4d9f02..cbd31cf82ed 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -32,9 +32,6 @@ # Details about the Spider image format: # https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html # - -from __future__ import print_function - import os import struct import sys @@ -219,7 +216,8 @@ def loadImageSeries(filelist=None): print("unable to find %s" % img) continue try: - im = Image.open(img).convert2byte() + with Image.open(img) as im: + im = im.convert2byte() except Exception: if not isSpiderImage(img): print(img + " is not a Spider image file") @@ -273,7 +271,7 @@ def _save(im, fp, filename): hdr = makeSpiderHeader(im) if len(hdr) < 256: - raise IOError("Error creating Spider header") + raise OSError("Error creating Spider header") # write the SPIDER header fp.writelines(hdr) @@ -306,21 +304,21 @@ def _save_spider(im, fp, filename): 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()) - - 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) + 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] + + # 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) diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index 74fa5f7bdca..fd7ca8a403e 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -20,10 +20,6 @@ from . import Image, ImageFile, ImagePalette from ._binary import i32be as i32 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.3" - def _accept(prefix): return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index e180b802ccb..ede64645358 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -15,7 +15,6 @@ # import io -import sys from . import ContainerIO @@ -38,12 +37,12 @@ def __init__(self, tarfile, file): s = self.fh.read(512) if len(s) != 512: - raise IOError("unexpected end of tar file") + raise OSError("unexpected end of tar file") name = s[:100].decode("utf-8") i = name.find("\0") if i == 0: - raise IOError("cannot find subfile") + raise OSError("cannot find subfile") if i > 0: name = name[:i] @@ -55,7 +54,7 @@ def __init__(self, tarfile, file): self.fh.seek((size + 511) & (~511), io.SEEK_CUR) # Open region - ContainerIO.ContainerIO.__init__(self, self.fh, self.fh.tell(), size) + super().__init__(self.fh, self.fh.tell(), size) # Context manager support def __enter__(self): @@ -64,10 +63,5 @@ def __enter__(self): def __exit__(self, *args): self.close() - if sys.version_info.major >= 3: - - def __del__(self): - self.close() - def close(self): self.fh.close() diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index b1b35139602..fd71e545d62 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -22,11 +22,6 @@ from . import Image, ImageFile, ImagePalette from ._binary import i8, i16le as i16, o8, o16le as o16 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.3" - - # # -------------------------------------------------------------------- # Read RGA file @@ -173,7 +168,7 @@ def _save(im, fp, filename): try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError: - raise IOError("cannot write mode %s as TGA" % im.mode) + raise OSError("cannot write mode %s as TGA" % im.mode) if "rle" in im.encoderinfo: rle = im.encoderinfo["rle"] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index a927cd3ed90..74fb695162f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -38,35 +38,19 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import division, print_function - -import distutils.version import io import itertools import os import struct -import sys import warnings +from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational from . import Image, ImageFile, ImagePalette, TiffTags from ._binary import i8, o8 -from ._util import py3 from .TiffTags import TYPES -try: - # Python 3 - from collections.abc import MutableMapping -except ImportError: - # Python 2.7 - from collections import MutableMapping - - -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "1.3.5" DEBUG = False # Needs to be merged with the new logging approach. # Set these to true to force use of libtiff for reading or writing. @@ -279,8 +263,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 ## @@ -310,25 +304,21 @@ def __init__(self, value, denominator=1): float/rational/other number, or an IFDRational :param denominator: Optional integer denominator """ - self._denominator = denominator - self._numerator = value - self._val = float(1) - - if isinstance(value, Fraction): - self._numerator = value.numerator - self._denominator = value.denominator - self._val = value - if isinstance(value, IFDRational): - self._denominator = value.denominator self._numerator = value.numerator + self._denominator = value.denominator self._val = value._val return + if isinstance(value, Fraction): + self._numerator = value.numerator + self._denominator = value.denominator + else: + self._numerator = value + self._denominator = denominator + if denominator == 0: self._val = float("nan") - return - elif denominator == 1: self._val = Fraction(value) else: @@ -370,10 +360,10 @@ def delegate(self, *args): return delegate - """ a = ['add','radd', 'sub', 'rsub','div', 'rdiv', 'mul', 'rmul', - 'truediv', 'rtruediv', 'floordiv', - 'rfloordiv','mod','rmod', 'pow','rpow', 'pos', 'neg', - 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'nonzero', + """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', + 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', + 'mod','rmod', 'pow','rpow', 'pos', 'neg', + 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool', 'ceil', 'floor', 'round'] print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) """ @@ -382,8 +372,6 @@ def delegate(self, *args): __radd__ = _delegate("__radd__") __sub__ = _delegate("__sub__") __rsub__ = _delegate("__rsub__") - __div__ = _delegate("__div__") - __rdiv__ = _delegate("__rdiv__") __mul__ = _delegate("__mul__") __rmul__ = _delegate("__rmul__") __truediv__ = _delegate("__truediv__") @@ -402,7 +390,7 @@ def delegate(self, *args): __gt__ = _delegate("__gt__") __le__ = _delegate("__le__") __ge__ = _delegate("__ge__") - __nonzero__ = _delegate("__nonzero__") + __bool__ = _delegate("__bool__") __ceil__ = _delegate("__ceil__") __floor__ = _delegate("__floor__") __round__ = _delegate("__round__") @@ -484,7 +472,7 @@ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): else: raise SyntaxError("not a TIFF IFD") self.reset() - self.next, = self._unpack("L", ifh[4:]) + (self.next,) = self._unpack("L", ifh[4:]) self._legacy_api = False prefix = property(lambda self: self._prefix) @@ -531,18 +519,11 @@ def __getitem__(self, tag): def __contains__(self, tag): return tag in self._tags_v2 or tag in self._tagdata - if not py3: - - def has_key(self, tag): - return tag in self - def __setitem__(self, tag, value): self._setitem(tag, value, self.legacy_api) def _setitem(self, tag, value, legacy_api): basetypes = (Number, bytes, str) - if not py3: - basetypes += (unicode,) # noqa: F821 info = TiffTags.lookup(tag) values = [value] if isinstance(value, basetypes) else value @@ -553,23 +534,29 @@ def _setitem(self, tag, value, legacy_api): 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 py3: - if all(isinstance(v, str) for v in values): - self.tagtype[tag] = TiffTags.ASCII - else: - # Never treat data as binary by default on Python 2. + if all(isinstance(v, str) for v in values): self.tagtype[tag] = TiffTags.ASCII - if self.tagtype[tag] == TiffTags.UNDEFINED and py3: + if self.tagtype[tag] == TiffTags.UNDEFINED: values = [ value.encode("ascii", "replace") if isinstance(value, str) else value ] @@ -595,7 +582,7 @@ def _setitem(self, tag, value, legacy_api): ]: # rationals values = (values,) try: - dest[tag], = values + (dest[tag],) = values except ValueError: # We've got a builtin tag with 1 expected entry warnings.warn( @@ -689,8 +676,6 @@ def load_string(self, data, legacy_api=True): @_register_writer(2) def write_string(self, value): # remerge of https://github.com/python-pillow/Pillow/pull/1416 - if sys.version_info.major == 2: - value = value.decode("ascii", "replace") return b"" + value.encode("ascii", "replace") + b"\0" @_register_loader(5, 8) @@ -705,7 +690,7 @@ def combine(a, b): @_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) @@ -728,13 +713,14 @@ def combine(a, b): @_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): ret = fp.read(size) if len(ret) != size: - raise IOError( + raise OSError( "Corrupt EXIF data. " + "Expecting to read %d bytes but only got %d. " % (size, len(ret)) ) @@ -765,10 +751,10 @@ def load(self, fp): size = count * unit_size if size > 4: here = fp.tell() - offset, = self._unpack("L", data) + (offset,) = self._unpack("L", data) if DEBUG: print( - "Tag Location: %s - Data Location: %s" % (here, offset), + "Tag Location: {} - Data Location: {}".format(here, offset), end=" ", ) fp.seek(offset) @@ -797,8 +783,8 @@ def load(self, fp): else: print("- value:", self[tag]) - self.next, = self._unpack("L", self._ensure_read(fp, 4)) - except IOError as msg: + (self.next,) = self._unpack("L", self._ensure_read(fp, 4)) + except OSError as msg: warnings.warn(str(msg)) return @@ -817,7 +803,7 @@ def tobytes(self, offset=0): stripoffsets = len(entries) typ = self.tagtype.get(tag) if DEBUG: - print("Tag %s, Type: %s, Value: %s" % (tag, typ, value)) + 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: @@ -854,7 +840,7 @@ def tobytes(self, offset=0): # pass 2: write entries to file for tag, typ, count, value, data in entries: - if DEBUG > 1: + if DEBUG: print(tag, typ, count, repr(value), repr(data)) result += self._pack("HHL4s", tag, typ, count, value) @@ -911,7 +897,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): """ def __init__(self, *args, **kwargs): - ImageFileDirectory_v2.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) self._legacy_api = True tags = property(lambda self: self._tags_v1) @@ -1054,7 +1040,7 @@ def _seek(self, frame): "Seeking to frame %s, on frame %s, __next %s, location: %s" % (frame, self.__frame, self.__next, self.fp.tell()) ) - # reset python3 buffered io handle in case fp + # reset buffered io handle in case fp # was passed to libtiff, invalidating the buffer self.fp.tell() self.fp.seek(self.__next) @@ -1079,23 +1065,10 @@ def tell(self): """Return the current frame number""" return self.__frame - @property - def size(self): - return self._size - - @size.setter - def size(self, value): - warnings.warn( - "Setting the size of a TIFF image directly is deprecated, and will" - " be removed in a future version. Use the resize method instead.", - DeprecationWarning, - ) - self._size = value - def load(self): if self.use_load_libtiff: return self._load_libtiff() - return super(TiffImageFile, self).load() + return super().load() def load_end(self): if self._tile_orientation: @@ -1124,14 +1097,14 @@ def _load_libtiff(self): pixel = Image.Image.load(self) if self.tile is None: - raise IOError("cannot load this image") + raise OSError("cannot load this image") if not self.tile: return pixel self.load_prepare() if not len(self.tile) == 1: - raise IOError("Not exactly one tile") + raise OSError("Not exactly one tile") # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) @@ -1145,11 +1118,11 @@ def _load_libtiff(self): try: fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno()) # flush the file descriptor, prevents error on pypy 2.4+ - # should also eliminate the need for fp.tell for py3 + # should also eliminate the need for fp.tell # in _seek if hasattr(self.fp, "flush"): self.fp.flush() - except IOError: + except OSError: # io.BytesIO have a fileno, but returns an IOError if # it doesn't use a file descriptor. fp = False @@ -1163,7 +1136,7 @@ def _load_libtiff(self): try: decoder.setimage(self.im, extents) except ValueError: - raise IOError("Couldn't set the image") + raise OSError("Couldn't set the image") close_self_fp = self._exclusive_fp and not self._is_animated if hasattr(self.fp, "getvalue"): @@ -1206,7 +1179,7 @@ def _load_libtiff(self): self.fp = None # might be shared if err < 0: - raise IOError(err) + raise OSError(err) return Image.Image.load(self) @@ -1214,7 +1187,7 @@ def _setup(self): """Setup this image object based on current tags""" if 0xBC01 in self.tag_v2: - raise IOError("Windows Media Photo files not yet supported") + raise OSError("Windows Media Photo files not yet supported") # extract relevant tags self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] @@ -1456,7 +1429,7 @@ def _save(im, fp, filename): try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError: - raise IOError("cannot write mode %s as TIFF" % im.mode) + raise OSError("cannot write mode %s as TIFF" % im.mode) ifd = ImageFileDirectory_v2(prefix=prefix) @@ -1606,22 +1579,18 @@ 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 ( + TiffTags.lookup(tag).type == TiffTags.UNDEFINED + or 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)) - or (not py3 and isinstance(value, unicode)) # noqa: F821 - ): + elif not (isinstance(value, (int, float, str, bytes))): continue if tag not in atts and tag not in blocklist: - if isinstance(value, str if py3 else unicode): # noqa: F821 + if isinstance(value, str): atts[tag] = value.encode("ascii", "replace") + b"\0" elif isinstance(value, IFDRational): atts[tag] = float(value) @@ -1654,7 +1623,7 @@ def _save(im, fp, filename): if s: break if s < 0: - raise IOError("encoder error %d when writing image file" % s) + raise OSError("encoder error %d when writing image file" % s) else: offset = ifd.save(fp) @@ -1702,9 +1671,9 @@ def __init__(self, fn, new=False): self.name = fn self.close_fp = True try: - self.f = io.open(fn, "w+b" if new else "r+b") - except IOError: - self.f = io.open(fn, "w+b") + self.f = open(fn, "w+b" if new else "r+b") + except OSError: + self.f = open(fn, "w+b") self.beginning = self.f.tell() self.setup() @@ -1785,7 +1754,7 @@ def goToEnd(self): # pad to 16 byte boundary padBytes = 16 - pos % 16 if 0 < padBytes < 16: - self.f.write(bytes(bytearray(padBytes))) + self.f.write(bytes(padBytes)) self.offsetOfNewPage = self.f.tell() def setEndian(self, endian): @@ -1809,11 +1778,11 @@ def write(self, data): return self.f.write(data) def readShort(self): - value, = struct.unpack(self.shortFmt, self.f.read(2)) + (value,) = struct.unpack(self.shortFmt, self.f.read(2)) return value def readLong(self): - value, = struct.unpack(self.longFmt, self.f.read(4)) + (value,) = struct.unpack(self.longFmt, self.f.read(4)) return value def rewriteLastShortToLong(self, value): diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 82719db0ef2..6cc9ff7f349 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -24,7 +24,7 @@ class TagInfo(namedtuple("_TagInfo", "value name type length enum")): __slots__ = [] def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): - return super(TagInfo, cls).__new__(cls, value, name, type, length, enum or {}) + return super().__new__(cls, value, name, type, length, enum or {}) def cvt_enum(self, value): # Using get will call hash(value), which can be expensive @@ -120,7 +120,7 @@ def lookup(tag): 277: ("SamplesPerPixel", SHORT, 1), 278: ("RowsPerStrip", LONG, 1), 279: ("StripByteCounts", LONG, 0), - 280: ("MinSampleValue", LONG, 0), + 280: ("MinSampleValue", SHORT, 0), 281: ("MaxSampleValue", SHORT, 0), 282: ("XResolution", RATIONAL, 1), 283: ("YResolution", RATIONAL, 1), @@ -182,7 +182,7 @@ def lookup(tag): # FIXME add more tags here 34665: ("ExifIFD", LONG, 1), 34675: ("ICCProfile", UNDEFINED, 1), - 34853: ("GPSInfoIFD", BYTE, 1), + 34853: ("GPSInfoIFD", LONG, 1), # MPInfo 45056: ("MPFVersion", UNDEFINED, 1), 45057: ("NumberOfImages", LONG, 1), diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index e2e1cd4f57a..d5a5c8e67bb 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -1,4 +1,3 @@ -# encoding: utf-8 # # The Python Imaging Library. # $Id$ @@ -21,16 +20,11 @@ # https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml # and has been tested with a few sample files found using google. +import builtins + from . import Image from ._binary import i32le as i32 -try: - import builtins -except ImportError: - import __builtin__ - - builtins = __builtin__ - def open(filename): """ diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 18eda6d1848..eda6855087d 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -105,7 +105,7 @@ def is_animated(self): def seek(self, frame): if not _webp.HAVE_WEBPANIM: - return super(WebPImageFile, self).seek(frame) + return super().seek(frame) # Perform some simple checks first if frame >= self._n_frames: @@ -168,11 +168,11 @@ def load(self): self.fp = BytesIO(data) self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] - return super(WebPImageFile, self).load() + return super().load() def tell(self): if not _webp.HAVE_WEBPANIM: - return super(WebPImageFile, self).tell() + return super().tell() return self.__logical_frame @@ -233,7 +233,7 @@ def _save_all(im, fp, filename): or len(background) != 4 or not all(v >= 0 and v < 256 for v in background) ): - raise IOError( + raise OSError( "Background color is not an RGBA tuple clamped to (0-255): %s" % str(background) ) @@ -312,7 +312,7 @@ def _save_all(im, fp, filename): # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) if data is None: - raise IOError("cannot write file as WebP (encoder returned None)") + raise OSError("cannot write file as WebP (encoder returned None)") fp.write(data) @@ -346,7 +346,7 @@ def _save(im, fp, filename): xmp, ) if data is None: - raise IOError("cannot write file as WebP (encoder returned None)") + raise OSError("cannot write file as WebP (encoder returned None)") fp.write(data) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 416af6fd745..024222c9b79 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -19,21 +19,11 @@ # http://wvware.sourceforge.net/caolan/index.html # http://wvware.sourceforge.net/caolan/ora-wmf.html -from __future__ import print_function - from . import Image, ImageFile from ._binary import i16le as word, i32le as dword, si16le as short, si32le as _long -from ._util import py3 - -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.2" _handler = None -if py3: - long = int - def register_handler(handler): """ @@ -48,7 +38,7 @@ def register_handler(handler): if hasattr(Image.core, "drawwmf"): # install default handler (windows only) - class WmfHandler(object): + class WmfHandler: def open(self, im): im.mode = "RGB" self.bbox = im.info["wmf_bbox"] @@ -88,6 +78,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): format_description = "Windows Metafile" def _open(self): + self._inch = None # check placable header s = self.fp.read(80) @@ -97,7 +88,7 @@ def _open(self): # placeable windows metafile # get units per inch - inch = word(s, 14) + self._inch = word(s, 14) # get bounding box x0 = short(s, 6) @@ -106,12 +97,14 @@ def _open(self): 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") @@ -128,7 +121,6 @@ def _open(self): # 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 @@ -155,10 +147,20 @@ def _open(self): 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"): - raise IOError("WMF save handler not installed") + raise OSError("WMF save handler not installed") _handler.save(im, fp, filename) diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index aa3536d85fb..c0d8db09afc 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -20,10 +20,6 @@ from . import Image, ImageFile, ImagePalette from ._binary import i8, o8 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - _MAGIC = b"P7 332" # standard color palette for thumbnails (RGB332) diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index bc825c3f312..ead9722c88e 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -23,10 +23,6 @@ from . import Image, ImageFile -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.6" - # XBM header xbm_head = re.compile( br"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" @@ -73,7 +69,7 @@ def _open(self): def _save(im, fp, filename): if im.mode != "1": - raise IOError("cannot write mode %s as XBM" % im.mode) + raise OSError("cannot write mode %s as XBM" % im.mode) fp.write(("#define im_width %d\n" % im.size[0]).encode("ascii")) fp.write(("#define im_height %d\n" % im.size[1]).encode("ascii")) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 27514882726..d8bd00a1b59 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -20,10 +20,6 @@ from . import Image, ImageFile, ImagePalette from ._binary import i8, o8 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.2" - # XPM header xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 59eccc9b5aa..f9cb15772dd 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -9,17 +9,75 @@ Copyright (c) 1999 by Secret Labs AB. Use PIL.__version__ for this Pillow version. -PIL.VERSION is the old PIL version and will be removed in the future. ;-) """ +import sys +import warnings + from . import _version # VERSION was removed in Pillow 6.0.0. -# PILLOW_VERSION is deprecated and will be removed in Pillow 7.0.0. +__version__ = _version.__version__ + + +# PILLOW_VERSION is deprecated and will be removed in a future release. # Use __version__ instead. -PILLOW_VERSION = __version__ = _version.__version__ +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("module '{}' has no attribute '{}'".format(__name__, 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 @@ -71,3 +129,7 @@ "XpmImagePlugin", "XVThumbImagePlugin", ] + + +class UnidentifiedImageError(IOError): + pass diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 53b1ca95629..529b8c94b78 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -13,24 +13,13 @@ from struct import pack, unpack_from -from ._util import py3 -if py3: +def i8(c): + return c if c.__class__ is int else c[0] - def i8(c): - return c if c.__class__ is int else c[0] - def o8(i): - return bytes((i & 255,)) - - -else: - - def i8(c): - return ord(c) - - def o8(i): - return chr(i & 255) +def o8(i): + return bytes((i & 255,)) # Input, le = little endian, be = big endian diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index d4f34196ebb..30493066af5 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -1,11 +1,7 @@ """ Find compiled module linking to Tcl / Tk libraries """ import sys - -if sys.version_info.major > 2: - from tkinter import _tkinter as tk -else: - from Tkinter import tkinter as tk +from tkinter import _tkinter as tk if hasattr(sys, "pypy_find_executable"): # Tested with packages at https://bitbucket.org/pypy/pypy/downloads. diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 59964c7efae..755b4b27233 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,33 +1,20 @@ import os import sys -py3 = sys.version_info.major >= 3 py36 = sys.version_info[0:2] >= (3, 6) -if py3: - def isStringType(t): - return isinstance(t, str) +if py36: + 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)) else: - def isStringType(t): - return isinstance(t, basestring) # noqa: F821 - def isPath(f): - return isinstance(f, basestring) # noqa: F821 + return isinstance(f, (bytes, str)) # Checks if an object is a string, and that it points to a directory. @@ -35,7 +22,7 @@ def isDirectory(f): return isPath(f) and os.path.isdir(f) -class deferred_error(object): +class deferred_error: def __init__(self, ex): self.ex = ex diff --git a/src/PIL/_version.py b/src/PIL/_version.py index c927be6ec32..1e1f1af9343 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "6.2.0" +__version__ = "7.1.0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 9fd52236807..ac06c0f7142 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -1,8 +1,7 @@ -from __future__ import print_function, unicode_literals - import collections import os import sys +import warnings import PIL @@ -56,6 +55,8 @@ def get_supported_codecs(): "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), "raqm": ("PIL._imagingft", "HAVE_RAQM"), "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO"), + "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT"), + "xcb": ("PIL._imaging", "HAVE_XCB"), } @@ -77,14 +78,14 @@ def get_supported_features(): 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) - ) + 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("Unknown feature '%s'." % feature, stacklevel=2) + return False def get_supported(): @@ -94,7 +95,7 @@ def get_supported(): return ret -def pilinfo(out=None): +def pilinfo(out=None, supported_formats=True): if out is None: out = sys.stdout @@ -102,6 +103,10 @@ def pilinfo(out=None): print("-" * 68, file=out) print("Pillow {}".format(PIL.__version__), file=out) + py_version = sys.version.splitlines() + print("Python {}".format(py_version[0].strip()), file=out) + for py_version in py_version[1:]: + print(" {}".format(py_version.strip()), file=out) print("-" * 68, file=out) print( "Python modules loaded from {}".format(os.path.dirname(Image.__file__)), @@ -113,12 +118,6 @@ def pilinfo(out=None): ) print("-" * 68, file=out) - v = sys.version.splitlines() - print("Python {}".format(v[0].strip()), file=out) - for v in v[1:]: - print(" {}".format(v.strip()), file=out) - print("-" * 68, file=out) - for name, feature in [ ("pil", "PIL CORE"), ("tkinter", "TKINTER"), @@ -133,6 +132,8 @@ def pilinfo(out=None): ("zlib", "ZLIB (PNG/ZIP)"), ("libtiff", "LIBTIFF"), ("raqm", "RAQM (Bidirectional Text)"), + ("libimagequant", "LIBIMAGEQUANT (Quantization method)"), + ("xcb", "XCB (X protocol)"), ]: if check(name): print("---", feature, "support ok", file=out) @@ -140,30 +141,33 @@ def pilinfo(out=None): print("***", feature, "support not installed", file=out) print("-" * 68, file=out) - extensions = collections.defaultdict(list) - for ext, i in Image.EXTENSION.items(): - extensions[i].append(ext) - - for i in sorted(Image.ID): - line = "{}".format(i) - if i in Image.MIME: - line = "{} {}".format(line, Image.MIME[i]) - print(line, file=out) - - if i in extensions: - print("Extensions: {}".format(", ".join(sorted(extensions[i]))), file=out) - - features = [] - if i in Image.OPEN: - features.append("open") - if i in Image.SAVE: - features.append("save") - if i in Image.SAVE_ALL: - features.append("save_all") - if i in Image.DECODERS: - features.append("decode") - if i in Image.ENCODERS: - features.append("encode") - - print("Features: {}".format(", ".join(features)), file=out) - print("-" * 68, file=out) + if supported_formats: + extensions = collections.defaultdict(list) + for ext, i in Image.EXTENSION.items(): + extensions[i].append(ext) + + for i in sorted(Image.ID): + line = "{}".format(i) + if i in Image.MIME: + line = "{} {}".format(line, Image.MIME[i]) + print(line, file=out) + + if i in extensions: + print( + "Extensions: {}".format(", ".join(sorted(extensions[i]))), file=out + ) + + features = [] + if i in Image.OPEN: + features.append("open") + if i in Image.SAVE: + features.append("save") + if i in Image.SAVE_ALL: + features.append("save_all") + if i in Image.DECODERS: + features.append("decode") + if i in Image.ENCODERS: + features.append("encode") + + print("Features: {}".format(", ".join(features)), file=out) + print("-" * 68, file=out) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index bb0fd33a378..59801f58eb9 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -225,11 +225,7 @@ TkImaging_Init(Tcl_Interp* interp) #include /* Must be linked with 'psapi' library */ -#if PY_VERSION_HEX >= 0x03000000 #define TKINTER_PKG "tkinter" -#else -#define TKINTER_PKG "Tkinter" -#endif FARPROC _dfunc(HMODULE lib_handle, const char *func_name) { @@ -354,7 +350,6 @@ int load_tkinter_funcs(void) */ /* From module __file__ attribute to char *string for dlopen. */ -#if PY_VERSION_HEX >= 0x03000000 char *fname2char(PyObject *fname) { PyObject* bytes; @@ -364,9 +359,6 @@ char *fname2char(PyObject *fname) } return PyBytes_AsString(bytes); } -#else -#define fname2char(s) (PyString_AsString(s)) -#endif #include diff --git a/src/_imaging.c b/src/_imaging.c index 04520b1a1fd..0c3d766f34e 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -82,9 +82,13 @@ #include "zlib.h" #endif -#include "Imaging.h" +#ifdef HAVE_LIBTIFF +#ifndef _TIFFIO_ +#include +#endif +#endif -#include "py3.h" +#include "Imaging.h" #define _USE_MATH_DEFINES #include @@ -237,45 +241,13 @@ void ImagingSectionLeave(ImagingSectionCookie* cookie) int PyImaging_CheckBuffer(PyObject* buffer) { -#if PY_VERSION_HEX >= 0x03000000 return PyObject_CheckBuffer(buffer); -#else - return PyObject_CheckBuffer(buffer) || PyObject_CheckReadBuffer(buffer); -#endif } int PyImaging_GetBuffer(PyObject* buffer, Py_buffer *view) { /* must call check_buffer first! */ -#if PY_VERSION_HEX >= 0x03000000 return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); -#else - /* Use new buffer protocol if available - (mmap doesn't support this in 2.7, go figure) */ - if (PyObject_CheckBuffer(buffer)) { - int success = PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); - if (!success) { return success; } - PyErr_Clear(); - } - - /* Pretend we support the new protocol; PyBuffer_Release happily ignores - calling bf_releasebuffer on objects that don't support it */ - view->buf = NULL; - view->len = 0; - view->readonly = 1; - view->format = NULL; - view->ndim = 0; - view->shape = NULL; - view->strides = NULL; - view->suboffsets = NULL; - view->itemsize = 0; - view->internal = NULL; - - Py_INCREF(buffer); - view->obj = buffer; - - return PyObject_AsReadBuffer(buffer, (void *) &view->buf, &view->len); -#endif } /* -------------------------------------------------------------------- */ @@ -416,11 +388,11 @@ getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type) // on this switch. And 3 fewer loops to copy/paste. switch (type) { case TYPE_UINT8: - itemp = PyInt_AsLong(op); + itemp = PyLong_AsLong(op); list[i] = CLIP8(itemp); break; case TYPE_INT32: - itemp = PyInt_AsLong(op); + itemp = PyLong_AsLong(op); memcpy(list + i * sizeof(INT32), &itemp, sizeof(itemp)); break; case TYPE_FLOAT32: @@ -499,7 +471,7 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) case IMAGING_TYPE_UINT8: switch (im->bands) { case 1: - return PyInt_FromLong(pixel.b[0]); + return PyLong_FromLong(pixel.b[0]); case 2: return Py_BuildValue("BB", pixel.b[0], pixel.b[1]); case 3: @@ -509,12 +481,12 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) } break; case IMAGING_TYPE_INT32: - return PyInt_FromLong(pixel.i); + return PyLong_FromLong(pixel.i); case IMAGING_TYPE_FLOAT32: return PyFloat_FromDouble(pixel.f); case IMAGING_TYPE_SPECIAL: if (strncmp(im->mode, "I;16", 4) == 0) - return PyInt_FromLong(pixel.h); + return PyLong_FromLong(pixel.h); break; } @@ -543,16 +515,8 @@ getink(PyObject* color, Imaging im, char* ink) if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || im->type == IMAGING_TYPE_SPECIAL) { -#if PY_VERSION_HEX >= 0x03000000 if (PyLong_Check(color)) { r = PyLong_AsLongLong(color); -#else - if (PyInt_Check(color) || PyLong_Check(color)) { - if (PyInt_Check(color)) - r = PyInt_AS_LONG(color); - else - r = PyLong_AsLongLong(color); -#endif rIsInt = 1; } if (r == -1 && PyErr_Occurred()) { @@ -1129,16 +1093,16 @@ _getxy(PyObject* xy, int* x, int *y) goto badarg; value = PyTuple_GET_ITEM(xy, 0); - if (PyInt_Check(value)) - *x = PyInt_AS_LONG(value); + if (PyLong_Check(value)) + *x = PyLong_AS_LONG(value); else if (PyFloat_Check(value)) *x = (int) PyFloat_AS_DOUBLE(value); else goto badval; value = PyTuple_GET_ITEM(xy, 1); - if (PyInt_Check(value)) - *y = PyInt_AS_LONG(value); + if (PyLong_Check(value)) + *y = PyLong_AS_LONG(value); else if (PyFloat_Check(value)) *y = (int) PyFloat_AS_DOUBLE(value); else @@ -1255,7 +1219,7 @@ _histogram(ImagingObject* self, PyObject* args) list = PyList_New(h->bands * 256); for (i = 0; i < h->bands * 256; i++) { PyObject* item; - item = PyInt_FromLong(h->histogram[i]); + item = PyLong_FromLong(h->histogram[i]); if (item == NULL) { Py_DECREF(list); list = NULL; @@ -1524,7 +1488,7 @@ _putdata(ImagingObject* self, PyObject* args) /* Clipped data */ for (i = x = y = 0; i < n; i++) { op = PySequence_Fast_GET_ITEM(seq, i); - image->image8[y][x] = (UINT8) CLIP8(PyInt_AsLong(op)); + image->image8[y][x] = (UINT8) CLIP8(PyLong_AsLong(op)); if (++x >= (int) image->xsize){ x = 0, y++; } @@ -1635,7 +1599,7 @@ _putpalette(ImagingObject* self, PyObject* args) char* rawmode; UINT8* palette; Py_ssize_t palettesize; - if (!PyArg_ParseTuple(args, "s"PY_ARG_BYTES_LENGTH, &rawmode, &palette, &palettesize)) + if (!PyArg_ParseTuple(args, "sy#", &rawmode, &palette, &palettesize)) return NULL; if (strcmp(self->image->mode, "L") && strcmp(self->image->mode, "LA") && @@ -1698,7 +1662,7 @@ _putpalettealphas(ImagingObject* self, PyObject* args) int i; UINT8 *values; Py_ssize_t length; - if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH, &values, &length)) + if (!PyArg_ParseTuple(args, "y#", &values, &length)) return NULL; if (!self->image->palette) { @@ -1829,6 +1793,51 @@ _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")) @@ -1843,7 +1852,7 @@ im_setmode(ImagingObject* self, PyObject* args) char* mode; Py_ssize_t modelen; if (!PyArg_ParseTuple(args, "s#:setmode", &mode, &modelen)) - return NULL; + return NULL; im = self->image; @@ -2136,7 +2145,7 @@ _getprojection(ImagingObject* self, PyObject* args) ImagingGetProjection(self->image, (unsigned char *)xprofile, (unsigned char *)yprofile); - result = Py_BuildValue(PY_ARG_BYTES_LENGTH PY_ARG_BYTES_LENGTH, + result = Py_BuildValue("y#y#", xprofile, (Py_ssize_t)self->image->xsize, yprofile, (Py_ssize_t)self->image->ysize); @@ -2397,6 +2406,38 @@ _chop_subtract_modulo(ImagingObject* self, PyObject* args) 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 @@ -2414,7 +2455,7 @@ _font_new(PyObject* self_, PyObject* args) ImagingObject* imagep; unsigned char* glyphdata; Py_ssize_t glyphdata_length; - if (!PyArg_ParseTuple(args, "O!"PY_ARG_BYTES_LENGTH, + if (!PyArg_ParseTuple(args, "O!y#", &Imaging_Type, &imagep, &glyphdata, &glyphdata_length)) return NULL; @@ -2648,7 +2689,7 @@ _draw_ink(ImagingDrawObject* self, PyObject* args) if (!getink(color, self->image->image, (char*) &ink)) return NULL; - return PyInt_FromLong((int) ink); + return PyLong_FromLong((int) ink); } static PyObject* @@ -3277,6 +3318,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}, @@ -3315,6 +3357,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 @@ -3356,13 +3402,13 @@ _getattr_size(ImagingObject* self, void* closure) static PyObject* _getattr_bands(ImagingObject* self, void* closure) { - return PyInt_FromLong(self->image->bands); + return PyLong_FromLong(self->image->bands); } static PyObject* _getattr_id(ImagingObject* self, void* closure) { - return PyInt_FromSsize_t((Py_ssize_t) self->image); + return PyLong_FromSsize_t((Py_ssize_t) self->image); } static PyObject* @@ -3575,17 +3621,17 @@ _get_stats(PyObject* self, PyObject* args) if ( ! d) return NULL; PyDict_SetItemString(d, "new_count", - PyInt_FromLong(arena->stats_new_count)); + PyLong_FromLong(arena->stats_new_count)); PyDict_SetItemString(d, "allocated_blocks", - PyInt_FromLong(arena->stats_allocated_blocks)); + PyLong_FromLong(arena->stats_allocated_blocks)); PyDict_SetItemString(d, "reused_blocks", - PyInt_FromLong(arena->stats_reused_blocks)); + PyLong_FromLong(arena->stats_reused_blocks)); PyDict_SetItemString(d, "reallocated_blocks", - PyInt_FromLong(arena->stats_reallocated_blocks)); + PyLong_FromLong(arena->stats_reallocated_blocks)); PyDict_SetItemString(d, "freed_blocks", - PyInt_FromLong(arena->stats_freed_blocks)); + PyLong_FromLong(arena->stats_freed_blocks)); PyDict_SetItemString(d, "blocks_cached", - PyInt_FromLong(arena->blocks_cached)); + PyLong_FromLong(arena->blocks_cached)); return d; } @@ -3613,7 +3659,7 @@ _get_alignment(PyObject* self, PyObject* args) if (!PyArg_ParseTuple(args, ":get_alignment")) return NULL; - return PyInt_FromLong(ImagingDefaultArena.alignment); + return PyLong_FromLong(ImagingDefaultArena.alignment); } static PyObject* @@ -3622,7 +3668,7 @@ _get_block_size(PyObject* self, PyObject* args) if (!PyArg_ParseTuple(args, ":get_block_size")) return NULL; - return PyInt_FromLong(ImagingDefaultArena.block_size); + return PyLong_FromLong(ImagingDefaultArena.block_size); } static PyObject* @@ -3631,7 +3677,7 @@ _get_blocks_max(PyObject* self, PyObject* args) if (!PyArg_ParseTuple(args, ":get_blocks_max")) return NULL; - return PyInt_FromLong(ImagingDefaultArena.blocks_max); + return PyLong_FromLong(ImagingDefaultArena.blocks_max); } static PyObject* @@ -3771,6 +3817,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); @@ -3843,13 +3892,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}, @@ -3934,6 +3986,12 @@ setup_module(PyObject* m) { PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", Py_False); #endif +#ifdef HAVE_LIBIMAGEQUANT + PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", Py_True); +#else + PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", Py_False); +#endif + #ifdef HAVE_LIBZ /* zip encoding strategies */ PyModule_AddIntConstant(m, "DEFAULT_STRATEGY", Z_DEFAULT_STRATEGY); @@ -3951,15 +4009,29 @@ 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; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imaging(void) { PyObject* m; @@ -3979,11 +4051,3 @@ PyInit__imaging(void) { return m; } -#else -PyMODINIT_FUNC -init_imaging(void) -{ - PyObject* m = Py_InitModule("_imaging", functions); - setup_module(m); -} -#endif diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 2c9f3aa68f4..0b22ab69547 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -32,7 +32,6 @@ kevin@cazabon.com\n\ #include "lcms2.h" #include "Imaging.h" -#include "py3.h" #define PYCMSVERSION "1.0.0 pil" @@ -122,13 +121,8 @@ cms_profile_fromstring(PyObject* self, PyObject* args) char* pProfile; Py_ssize_t nProfile; -#if PY_VERSION_HEX >= 0x03000000 if (!PyArg_ParseTuple(args, "y#:profile_frombytes", &pProfile, &nProfile)) return NULL; -#else - if (!PyArg_ParseTuple(args, "s#:profile_fromstring", &pProfile, &nProfile)) - return NULL; -#endif hProfile = cmsOpenProfileFromMem(pProfile, nProfile); if (!hProfile) { @@ -172,11 +166,7 @@ cms_profile_tobytes(PyObject* self, PyObject* args) return NULL; } -#if PY_VERSION_HEX >= 0x03000000 ret = PyBytes_FromStringAndSize(pProfile, (Py_ssize_t)nProfile); -#else - ret = PyString_FromStringAndSize(pProfile, (Py_ssize_t)nProfile); -#endif free(pProfile); return ret; @@ -592,7 +582,7 @@ cms_profile_is_intent_supported(CmsProfileObject *self, PyObject *args) /* printf("cmsIsIntentSupported(%p, %d, %d) => %d\n", self->profile, intent, direction, result); */ - return PyInt_FromLong(result != 0); + return PyLong_FromLong(result != 0); } #ifdef _WIN32 @@ -691,11 +681,7 @@ _profile_read_int_as_string(cmsUInt32Number nr) buf[3] = (char) (nr & 0xff); buf[4] = 0; -#if PY_VERSION_HEX >= 0x03000000 ret = PyUnicode_DecodeASCII(buf, 4, NULL); -#else - ret = PyString_FromStringAndSize(buf, 4); -#endif return ret; } @@ -898,7 +884,7 @@ _is_intent_supported(CmsProfileObject* self, int clut) || intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) continue; - id = PyInt_FromLong((long) intent); + id = PyLong_FromLong((long) intent); entry = Py_BuildValue("(OOO)", _check_intent(clut, self->profile, intent, LCMS_USED_AS_INPUT) ? Py_True : Py_False, _check_intent(clut, self->profile, intent, LCMS_USED_AS_OUTPUT) ? Py_True : Py_False, @@ -1010,7 +996,7 @@ cms_profile_getattr_product_copyright(CmsProfileObject* self, void* closure) static PyObject* cms_profile_getattr_rendering_intent(CmsProfileObject* self, void* closure) { - return PyInt_FromLong(cmsGetHeaderRenderingIntent(self->profile)); + return PyLong_FromLong(cmsGetHeaderRenderingIntent(self->profile)); } static PyObject* @@ -1098,7 +1084,7 @@ cms_profile_getattr_version(CmsProfileObject* self, void* closure) static PyObject* cms_profile_getattr_icc_version(CmsProfileObject* self, void* closure) { - return PyInt_FromLong((long) cmsGetEncodedICCversion(self->profile)); + return PyLong_FromLong((long) cmsGetEncodedICCversion(self->profile)); } static PyObject* @@ -1115,7 +1101,7 @@ static PyObject* cms_profile_getattr_header_flags(CmsProfileObject* self, void* closure) { cmsUInt32Number flags = cmsGetHeaderFlags(self->profile); - return PyInt_FromLong(flags); + return PyLong_FromLong(flags); } static PyObject* @@ -1611,7 +1597,6 @@ setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingcms(void) { PyObject* m; @@ -1633,12 +1618,3 @@ PyInit__imagingcms(void) { return m; } -#else -PyMODINIT_FUNC -init_imagingcms(void) -{ - PyObject *m = Py_InitModule("_imagingcms", pyCMSdll_methods); - setup_module(m); - PyDateTime_IMPORT; -} -#endif diff --git a/src/_imagingft.c b/src/_imagingft.c index 7776e43f1b7..f6a5b7d59b3 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -30,7 +30,6 @@ #include FT_SFNT_NAMES_H #define KEEP_PY_UNICODE -#include "py3.h" #if !defined(_MSC_VER) #include @@ -266,8 +265,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) return NULL; } - if (!PyArg_ParseTupleAndKeywords(args, kw, "etn|ns"PY_ARG_BYTES_LENGTH"n", - kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kw, "etn|nsy#n", kwlist, Py_FileSystemDefaultEncoding, &filename, &size, &index, &encoding, &font_bytes, &font_bytes_size, &layout_engine)) { @@ -328,34 +326,12 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) static int font_getchar(PyObject* string, int index, FT_ULong* char_out) { -#if (PY_VERSION_HEX < 0x03030000) || (defined(PYPY_VERSION_NUM)) - if (PyUnicode_Check(string)) { - Py_UNICODE* p = PyUnicode_AS_UNICODE(string); - int size = PyUnicode_GET_SIZE(string); - if (index >= size) - return 0; - *char_out = p[index]; - return 1; - } -#if PY_VERSION_HEX < 0x03000000 - if (PyString_Check(string)) { - unsigned char* p = (unsigned char*) PyString_AS_STRING(string); - int size = PyString_GET_SIZE(string); - if (index >= size) - return 0; - *char_out = (unsigned char) p[index]; - return 1; - } -#endif -#else if (PyUnicode_Check(string)) { if (index >= PyUnicode_GET_LENGTH(string)) return 0; *char_out = PyUnicode_READ_CHAR(string, index); return 1; } -#endif - return 0; } @@ -375,7 +351,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * goto failed; } -#if (PY_VERSION_HEX < 0x03030000) || (defined(PYPY_VERSION_NUM)) +#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); @@ -395,25 +371,6 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * } } } -#if PY_VERSION_HEX < 0x03000000 - else if (PyString_Check(string)) { - char *text = PyString_AS_STRING(string); - int size = PyString_GET_SIZE(string); - if (! size) { - goto failed; - } - if (!(*p_raqm.set_text_utf8)(rq, text, size)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_text_utf8() failed"); - goto failed; - } - if (lang) { - if (!(*p_raqm.set_language)(rq, lang, start, size)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); - goto failed; - } - } - } -#endif #else if (PyUnicode_Check(string)) { Py_UCS4 *text = PyUnicode_AsUCS4Copy(string); @@ -423,7 +380,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * and raqm fails with empty strings */ goto failed; } - int set_text = (*p_raqm.set_text)(rq, (const uint32_t *)(text), size); + int set_text = (*p_raqm.set_text)(rq, text, size); PyMem_Free(text); if (!set_text) { PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); @@ -479,11 +436,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * Py_ssize_t size = 0; PyObject *bytes; -#if PY_VERSION_HEX >= 0x03000000 if (!PyUnicode_Check(item)) { -#else - if (!PyUnicode_Check(item) && !PyString_Check(item)) { -#endif PyErr_SetString(PyExc_TypeError, "expected a string"); goto failed; } @@ -495,12 +448,6 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * feature = PyBytes_AS_STRING(bytes); size = PyBytes_GET_SIZE(bytes); } -#if PY_VERSION_HEX < 0x03000000 - else { - feature = PyString_AsString(item); - size = PyString_GET_SIZE(item); - } -#endif if (!(*p_raqm.add_font_feature)(rq, feature, size)) { PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); goto failed; @@ -581,11 +528,7 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje if (features != Py_None || dir != NULL || lang != NULL) { PyErr_SetString(PyExc_KeyError, "setting text direction, language or font features is not supported without libraqm"); } -#if PY_VERSION_HEX >= 0x03000000 if (!PyUnicode_Check(string)) { -#else - if (!PyUnicode_Check(string) && !PyString_Check(string)) { -#endif PyErr_SetString(PyExc_TypeError, "expected string"); return 0; } @@ -621,9 +564,10 @@ 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; @@ -839,9 +783,6 @@ 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; - } if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } @@ -849,7 +790,7 @@ font_render(FontObject* self, PyObject* args) ascender = 0; for (i = 0; i < count; i++) { index = glyph_info[i].index; - error = FT_Load_Glyph(self->face, index, load_flags); + error = FT_Load_Glyph(self->face, index, load_flags | FT_LOAD_RENDER); if (error) { return geterror(error); } @@ -863,6 +804,10 @@ font_render(FontObject* self, PyObject* args) ascender = temp; } + 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++) { @@ -890,8 +835,6 @@ font_render(FontObject* self, PyObject* args) bitmap = bitmap_glyph->bitmap; left = bitmap_glyph->left; - - FT_Done_Glyph(glyph); } else { bitmap = glyph_slot->bitmap; left = glyph_slot->bitmap_left; @@ -953,6 +896,9 @@ font_render(FontObject* self, PyObject* args) } x += glyph_info[i].x_advance; y -= glyph_info[i].y_advance; + if (stroker != NULL) { + FT_Done_Glyph(glyph); + } } FT_Stroker_Done(stroker); @@ -964,7 +910,7 @@ font_render(FontObject* self, PyObject* args) (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; @@ -990,8 +936,7 @@ font_render(FontObject* self, PyObject* args) continue; if (master->namedstyle[j].strid == name.name_id) { - list_name = Py_BuildValue(PY_ARG_BYTES_LENGTH, - name.string, name.string_len); + list_name = Py_BuildValue("y#", name.string, name.string_len); PyList_SetItem(list_names, j, list_name); break; } @@ -1004,7 +949,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; @@ -1025,11 +970,11 @@ font_render(FontObject* self, PyObject* args) list_axis = PyDict_New(); PyDict_SetItemString(list_axis, "minimum", - PyInt_FromLong(axis.minimum / 65536)); + PyLong_FromLong(axis.minimum / 65536)); PyDict_SetItemString(list_axis, "default", - PyInt_FromLong(axis.def / 65536)); + PyLong_FromLong(axis.def / 65536)); PyDict_SetItemString(list_axis, "maximum", - PyInt_FromLong(axis.maximum / 65536)); + PyLong_FromLong(axis.maximum / 65536)); for (j = 0; j < name_count; j++) { error = FT_Get_Sfnt_Name(self->face, j, &name); @@ -1037,8 +982,7 @@ font_render(FontObject* self, PyObject* args) return geterror(error); if (name.name_id == axis.strid) { - axis_name = Py_BuildValue(PY_ARG_BYTES_LENGTH, - name.string, name.string_len); + axis_name = Py_BuildValue("y#", name.string, name.string_len); PyDict_SetItemString(list_axis, "name", axis_name); break; } @@ -1095,8 +1039,8 @@ font_render(FontObject* self, PyObject* args) item = PyList_GET_ITEM(axes, i); if (PyFloat_Check(item)) coord = PyFloat_AS_DOUBLE(item); - else if (PyInt_Check(item)) - coord = (float) PyInt_AS_LONG(item); + else if (PyLong_Check(item)) + coord = (float) PyLong_AS_LONG(item); else if (PyNumber_Check(item)) coord = PyFloat_AsDouble(item); else { @@ -1135,8 +1079,8 @@ static PyMethodDef font_methods[] = { #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 @@ -1146,64 +1090,54 @@ static PyMethodDef font_methods[] = { static PyObject* font_getattr_family(FontObject* self, void* closure) { -#if PY_VERSION_HEX >= 0x03000000 if (self->face->family_name) return PyUnicode_FromString(self->face->family_name); -#else - if (self->face->family_name) - return PyString_FromString(self->face->family_name); -#endif Py_RETURN_NONE; } static PyObject* font_getattr_style(FontObject* self, void* closure) { -#if PY_VERSION_HEX >= 0x03000000 if (self->face->style_name) return PyUnicode_FromString(self->face->style_name); -#else - if (self->face->style_name) - return PyString_FromString(self->face->style_name); -#endif Py_RETURN_NONE; } static PyObject* font_getattr_ascent(FontObject* self, void* closure) { - return PyInt_FromLong(PIXEL(self->face->size->metrics.ascender)); + return PyLong_FromLong(PIXEL(self->face->size->metrics.ascender)); } static PyObject* font_getattr_descent(FontObject* self, void* closure) { - return PyInt_FromLong(-PIXEL(self->face->size->metrics.descender)); + return PyLong_FromLong(-PIXEL(self->face->size->metrics.descender)); } static PyObject* font_getattr_height(FontObject* self, void* closure) { - return PyInt_FromLong(PIXEL(self->face->size->metrics.height)); + return PyLong_FromLong(PIXEL(self->face->size->metrics.height)); } static PyObject* font_getattr_x_ppem(FontObject* self, void* closure) { - return PyInt_FromLong(self->face->size->metrics.x_ppem); + return PyLong_FromLong(self->face->size->metrics.x_ppem); } static PyObject* font_getattr_y_ppem(FontObject* self, void* closure) { - return PyInt_FromLong(self->face->size->metrics.y_ppem); + return PyLong_FromLong(self->face->size->metrics.y_ppem); } static PyObject* font_getattr_glyphs(FontObject* self, void* closure) { - return PyInt_FromLong(self->face->num_glyphs); + return PyLong_FromLong(self->face->num_glyphs); } static struct PyGetSetDef font_getsetters[] = { @@ -1271,11 +1205,7 @@ setup_module(PyObject* m) { FT_Library_Version(library, &major, &minor, &patch); -#if PY_VERSION_HEX >= 0x03000000 v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); -#else - v = PyString_FromFormat("%d.%d.%d", major, minor, patch); -#endif PyDict_SetItemString(d, "freetype2_version", v); @@ -1286,7 +1216,6 @@ setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingft(void) { PyObject* m; @@ -1306,12 +1235,3 @@ PyInit__imagingft(void) { return m; } -#else -PyMODINIT_FUNC -init_imagingft(void) -{ - PyObject* m = Py_InitModule("_imagingft", _functions); - setup_module(m); -} -#endif - diff --git a/src/_imagingmath.c b/src/_imagingmath.c index ea9f103c683..bc66a581a22 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -16,7 +16,6 @@ #include "Python.h" #include "Imaging.h" -#include "py3.h" #include "math.h" #include "float.h" @@ -215,7 +214,7 @@ static PyMethodDef _functions[] = { static void install(PyObject *d, char* name, void* value) { - PyObject *v = PyInt_FromSsize_t((Py_ssize_t) value); + PyObject *v = PyLong_FromSsize_t((Py_ssize_t) value); if (!v || PyDict_SetItemString(d, name, v)) PyErr_Clear(); Py_XDECREF(v); @@ -273,7 +272,6 @@ setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingmath(void) { PyObject* m; @@ -293,12 +291,3 @@ PyInit__imagingmath(void) { return m; } -#else -PyMODINIT_FUNC -init_imagingmath(void) -{ - PyObject* m = Py_InitModule("_imagingmath", _functions); - setup_module(m); -} -#endif - diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index fc8f246cc86..050ae9f0287 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -13,7 +13,6 @@ #include "Python.h" #include "Imaging.h" -#include "py3.h" #define LUT_SIZE (1<<9) @@ -273,7 +272,6 @@ static PyMethodDef functions[] = { {NULL, NULL, 0, NULL} }; -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingmorph(void) { PyObject* m; @@ -293,12 +291,3 @@ PyInit__imagingmorph(void) { return m; } -#else -PyMODINIT_FUNC -init_imagingmorph(void) -{ - PyObject* m = Py_InitModule("_imagingmorph", functions); - setup_module(m); -} -#endif - diff --git a/src/_imagingtk.c b/src/_imagingtk.c index d0295f317f9..bdf5e68d192 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -64,7 +64,6 @@ static PyMethodDef functions[] = { {NULL, NULL} /* sentinel */ }; -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingtk(void) { static PyModuleDef module_def = { @@ -78,12 +77,3 @@ PyInit__imagingtk(void) { m = PyModule_Create(&module_def); return (load_tkinter_funcs() == 0) ? m : NULL; } -#else -PyMODINIT_FUNC -init_imagingtk(void) -{ - Py_InitModule("_imagingtk", functions); - load_tkinter_funcs(); -} -#endif - diff --git a/src/_webp.c b/src/_webp.c index 66b6d3268ac..93cf7ae85f8 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -1,7 +1,6 @@ #define PY_SSIZE_T_CLEAN #include #include "Imaging.h" -#include "py3.h" #include #include #include @@ -22,6 +21,14 @@ #endif +void ImagingSectionEnter(ImagingSectionCookie* cookie) { + *cookie = (PyThreadState *) PyEval_SaveThread(); +} + +void ImagingSectionLeave(ImagingSectionCookie* cookie) { + PyEval_RestoreThread((PyThreadState*) *cookie); +} + /* -------------------------------------------------------------------- */ /* WebP Muxer Error Handling */ /* -------------------------------------------------------------------- */ @@ -370,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); @@ -407,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; @@ -429,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); @@ -490,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 */ }; @@ -556,8 +556,9 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) Py_ssize_t exif_size; Py_ssize_t xmp_size; size_t ret_size; + ImagingSectionCookie cookie; - if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH"iiifss#s#s#", + if (!PyArg_ParseTuple(args, "y#iiifss#s#s#", (char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode, &icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) { return NULL; @@ -568,11 +569,15 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) } #if WEBP_ENCODER_ABI_VERSION >= 0x0100 if (lossless) { + ImagingSectionEnter(&cookie); ret_size = WebPEncodeLosslessRGBA(rgb, width, height, 4 * width, &output); + ImagingSectionLeave(&cookie); } else #endif { + ImagingSectionEnter(&cookie); ret_size = WebPEncodeRGBA(rgb, width, height, 4 * width, quality_factor, &output); + ImagingSectionLeave(&cookie); } } else if (strcmp(mode, "RGB")==0){ if (size < width * height * 3){ @@ -580,11 +585,15 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) } #if WEBP_ENCODER_ABI_VERSION >= 0x0100 if (lossless) { + ImagingSectionEnter(&cookie); ret_size = WebPEncodeLosslessRGB(rgb, width, height, 3 * width, &output); + ImagingSectionLeave(&cookie); } else #endif { + ImagingSectionEnter(&cookie); ret_size = WebPEncodeRGB(rgb, width, height, 3 * width, quality_factor, &output); + ImagingSectionLeave(&cookie); } } else { Py_RETURN_NONE; @@ -754,11 +763,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) config.output.u.YUVA.y_size); } -#if PY_VERSION_HEX >= 0x03000000 pymode = PyUnicode_FromString(mode); -#else - pymode = PyString_FromString(mode); -#endif ret = Py_BuildValue("SiiSSS", bytes, config.output.width, config.output.height, pymode, NULL == icc_profile ? Py_None : icc_profile, @@ -780,7 +785,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) // 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()); } @@ -792,7 +797,7 @@ int WebPDecoderBuggyAlpha(void) { return WebPGetDecoderVersion()==0x0103; } -PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){ +PyObject* WebPDecoderBuggyAlpha_wrapper() { return Py_BuildValue("i", WebPDecoderBuggyAlpha()); } @@ -808,8 +813,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} }; @@ -848,7 +853,6 @@ static int setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__webp(void) { PyObject* m; @@ -867,11 +871,3 @@ PyInit__webp(void) { return m; } -#else -PyMODINIT_FUNC -init_webp(void) -{ - PyObject* m = Py_InitModule("_webp", webpMethods); - setup_module(m); -} -#endif diff --git a/src/decode.c b/src/decode.c index 79133f48fcf..5ab6ca9d156 100644 --- a/src/decode.c +++ b/src/decode.c @@ -33,7 +33,6 @@ #include "Python.h" #include "Imaging.h" -#include "py3.h" #include "Gif.h" #include "Raw.h" @@ -122,7 +121,7 @@ _decode(ImagingDecoderObject* decoder, PyObject* args) int status; ImagingSectionCookie cookie; - if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH, &buffer, &bufsize)) + if (!PyArg_ParseTuple(args, "y#", &buffer, &bufsize)) return NULL; if (!decoder->pulls_fd) { diff --git a/src/display.c b/src/display.c index efabf1c86af..21869b26ebe 100644 --- a/src/display.c +++ b/src/display.c @@ -26,7 +26,6 @@ #include "Python.h" #include "Imaging.h" -#include "py3.h" /* -------------------------------------------------------------------- */ /* Windows DIB support */ @@ -187,13 +186,8 @@ _frombytes(ImagingDisplayObject* display, PyObject* args) char* ptr; int bytes; -#if PY_VERSION_HEX >= 0x03000000 if (!PyArg_ParseTuple(args, "y#:frombytes", &ptr, &bytes)) return NULL; -#else - if (!PyArg_ParseTuple(args, "s#:fromstring", &ptr, &bytes)) - return NULL; -#endif if (display->dib->ysize * display->dib->linesize != bytes) { PyErr_SetString(PyExc_ValueError, "wrong size"); @@ -209,13 +203,8 @@ _frombytes(ImagingDisplayObject* display, PyObject* args) static PyObject* _tobytes(ImagingDisplayObject* display, PyObject* args) { -#if PY_VERSION_HEX >= 0x03000000 if (!PyArg_ParseTuple(args, ":tobytes")) return NULL; -#else - if (!PyArg_ParseTuple(args, ":tostring")) - return NULL; -#endif return PyBytes_FromStringAndSize( display->dib->bits, display->dib->ysize * display->dib->linesize @@ -741,7 +730,7 @@ PyImaging_DrawWmf(PyObject* self, PyObject* args) int datasize; int width, height; int x0, y0, x1, y1; - if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH"(ii)(iiii):_load", &data, &datasize, + if (!PyArg_ParseTuple(args, "y#(ii)(iiii):_load", &data, &datasize, &width, &height, &x0, &x1, &y0, &y1)) return NULL; @@ -826,3 +815,88 @@ PyImaging_DrawWmf(PyObject* self, PyObject* args) } #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_IOError, "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_IOError, "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_IOError, "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_IOError, "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 7dc1035c499..41ba124c4ee 100644 --- a/src/encode.c +++ b/src/encode.c @@ -26,7 +26,6 @@ #include "Python.h" #include "Imaging.h" -#include "py3.h" #include "Gif.h" #ifdef HAVE_UNISTD_H @@ -567,7 +566,7 @@ PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) Py_ssize_t compress_type = -1; char* dictionary = NULL; Py_ssize_t dictionary_size = 0; - if (!PyArg_ParseTuple(args, "ss|nnn"PY_ARG_BYTES_LENGTH, &mode, &rawmode, + if (!PyArg_ParseTuple(args, "ss|nnny#", &mode, &rawmode, &optimize, &compress_level, &compress_type, &dictionary, &dictionary_size)) @@ -693,7 +692,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) item = PyList_GetItem(tags, pos); // We already checked that tags is a 2-tuple list. key = PyTuple_GetItem(item, 0); - key_int = (int)PyInt_AsLong(key); + key_int = (int)PyLong_AsLong(key); value = PyTuple_GetItem(item, 1); status = 0; is_core_tag = 0; @@ -710,7 +709,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) if (!is_core_tag) { PyObject *tag_type = PyDict_GetItem(types, key); if (tag_type) { - int type_int = PyInt_AsLong(tag_type); + int type_int = PyLong_AsLong(tag_type); if (type_int >= TIFF_BYTE && type_int <= TIFF_DOUBLE) { type = (TIFFDataType)type_int; } @@ -721,7 +720,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) if (type == TIFF_NOTYPE) { // Autodetect type. Types should not be changed for backwards // compatibility. - if (PyInt_Check(value)) { + if (PyLong_Check(value)) { type = TIFF_LONG; } else if (PyFloat_Check(value)) { type = TIFF_DOUBLE; @@ -749,7 +748,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) if (type == TIFF_NOTYPE) { // Autodetect type based on first item. Types should not be // changed for backwards compatibility. - if (PyInt_Check(PyTuple_GetItem(value,0))) { + if (PyLong_Check(PyTuple_GetItem(value,0))) { type = TIFF_LONG; } else if (PyFloat_Check(PyTuple_GetItem(value,0))) { type = TIFF_FLOAT; @@ -775,7 +774,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) av = calloc(len, sizeof(UINT8)); if (av) { for (i=0;istate, (ttag_t) key_int, len, av); free(av); @@ -786,7 +785,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) av = calloc(len, sizeof(UINT16)); if (av) { for (i=0;istate, (ttag_t) key_int, len, av); free(av); @@ -797,7 +796,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) av = calloc(len, sizeof(UINT32)); if (av) { for (i=0;istate, (ttag_t) key_int, len, av); free(av); @@ -808,7 +807,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) av = calloc(len, sizeof(INT8)); if (av) { for (i=0;istate, (ttag_t) key_int, len, av); free(av); @@ -819,7 +818,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) av = calloc(len, sizeof(INT16)); if (av) { for (i=0;istate, (ttag_t) key_int, len, av); free(av); @@ -830,7 +829,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) av = calloc(len, sizeof(INT32)); if (av) { for (i=0;istate, (ttag_t) key_int, len, av); free(av); @@ -862,19 +861,19 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) if (type == TIFF_SHORT) { status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, - (UINT16)PyInt_AsLong(value)); + (UINT16)PyLong_AsLong(value)); } else if (type == TIFF_LONG) { status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, - (UINT32)PyInt_AsLong(value)); + (UINT32)PyLong_AsLong(value)); } else if (type == TIFF_SSHORT) { status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, - (INT16)PyInt_AsLong(value)); + (INT16)PyLong_AsLong(value)); } else if (type == TIFF_SLONG) { status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, - (INT32)PyInt_AsLong(value)); + (INT32)PyLong_AsLong(value)); } else if (type == TIFF_FLOAT) { status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, @@ -886,11 +885,11 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) } else if (type == TIFF_BYTE) { status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, - (UINT8)PyInt_AsLong(value)); + (UINT8)PyLong_AsLong(value)); } else if (type == TIFF_SBYTE) { status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, - (INT8)PyInt_AsLong(value)); + (INT8)PyLong_AsLong(value)); } else if (type == TIFF_ASCII) { status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, @@ -984,7 +983,7 @@ static unsigned int* get_qtables_arrays(PyObject* qtables, int* qtablesLen) { } table_data = PySequence_Fast(table, "expected a sequence"); for (j = 0; j < DCTSIZE2; j++) { - qarrays[i * DCTSIZE2 + j] = PyInt_AS_LONG(PySequence_Fast_GET_ITEM(table_data, j)); + qarrays[i * DCTSIZE2 + j] = PyLong_AS_LONG(PySequence_Fast_GET_ITEM(table_data, j)); } Py_DECREF(table_data); } @@ -1024,7 +1023,7 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) char* rawExif = NULL; Py_ssize_t rawExifLen = 0; - if (!PyArg_ParseTuple(args, "ss|nnnnnnnnO"PY_ARG_BYTES_LENGTH""PY_ARG_BYTES_LENGTH, + if (!PyArg_ParseTuple(args, "ss|nnnnnnnnOy#y#", &mode, &rawmode, &quality, &progressive, &smooth, &optimize, &streamtype, &xdpi, &ydpi, &subsampling, &qtables, &extra, &extra_size, @@ -1109,8 +1108,8 @@ j2k_decode_coord_tuple(PyObject *tuple, int *x, int *y) *x = *y = 0; if (tuple && PyTuple_Check(tuple) && PyTuple_GET_SIZE(tuple) == 2) { - *x = (int)PyInt_AsLong(PyTuple_GET_ITEM(tuple, 0)); - *y = (int)PyInt_AsLong(PyTuple_GET_ITEM(tuple, 1)); + *x = (int)PyLong_AsLong(PyTuple_GET_ITEM(tuple, 0)); + *y = (int)PyLong_AsLong(PyTuple_GET_ITEM(tuple, 1)); if (*x < 0) *x = 0; diff --git a/src/libImaging/Chops.c b/src/libImaging/Chops.c index 8059b6ffbd6..a1673dff6c5 100644 --- a/src/libImaging/Chops.c +++ b/src/libImaging/Chops.c @@ -146,3 +146,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 60513c66d5e..9f57225431f 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) { diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index dee7c524dba..58adc1b6355 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -42,7 +42,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))) @@ -415,6 +415,35 @@ x_cmp(const void *x0, const void *x1) } +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,10 +471,7 @@ 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) { @@ -472,6 +498,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 +512,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); diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index 5f4485f890c..108e1edf93a 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -24,7 +24,12 @@ #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) { @@ -40,8 +45,7 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt 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; @@ -52,6 +56,10 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt /* 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; @@ -75,10 +83,12 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt 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* buf = (UINT8*) im->image[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 */ @@ -88,29 +98,33 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt state->errcode = IMAGING_CODEC_OVERRUN; return -1; } - buf = (UINT8*) im->image[y]; + local_buf = (UINT8*) im->image[y]; } else { /* store last byte (used if line width is odd) */ - buf[state->xsize-1] = (UINT8) packets; + 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++) { - buf[x++] = data[2]; - buf[x++] = data[3]; + 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; - memcpy(buf + x, data + 2, i); + ERR_IF_DATA_OOB(2+i) + memcpy(local_buf + x, data + 2, i); data += 2 + i; x += i; } @@ -126,22 +140,27 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt 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; } @@ -162,14 +181,18 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt 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) + if (x + i > state->xsize) { break; /* safety first */ + } + ERR_IF_DATA_OOB(i+1) memcpy(out + x, data + 1, i); data += i + 1; } else { @@ -189,9 +212,13 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt 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* buf = (UINT8*) im->image[y]; - memcpy(buf, data, state->xsize); + UINT8* local_buf = (UINT8*) im->image[y]; + memcpy(local_buf, data, state->xsize); data += state->xsize; } break; @@ -205,6 +232,10 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt return -1; } advance = I32(ptr); + if (advance < 0 || advance > bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } ptr += advance; bytes -= advance; } diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index b63888f8746..ea7a35e4870 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -55,8 +55,19 @@ ImagingGetBBox(Imaging im, int bbox[4]) GETBBOX(image8, 0xff); } else { INT32 mask = 0xffffffff; - if (im->bands == 3) + 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); } @@ -166,11 +177,21 @@ 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)); + 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) diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 25c15e758cf..9032fcf0783 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -313,6 +313,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,6 +339,9 @@ 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); diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index d25da80ae2b..21c2688d840 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 82e1b449fec..7ff658c3231 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -67,7 +67,7 @@ typedef struct { /* CONFIGURATION */ - /* Quality (1-100, 0 means default) */ + /* Quality (0-100, -1 means default) */ int quality; /* Progressive mode */ diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index f2e437dda28..d304511d1a9 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -110,6 +110,7 @@ j2ku_gray_l(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, if (shift < 0) offset += 1 << (-shift - 1); + /* csiz*h*w + offset = tileinfo.datasize */ switch (csiz) { case 1: for (y = 0; y < h; ++y) { @@ -557,8 +558,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); @@ -703,8 +706,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 < 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; + } + + /* 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 +754,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 +765,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); } diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 10ad886e042..9d838279162 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -153,7 +153,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) int i; int quality = 100; int last_q = 0; - if (context->quality > 0) { + if (context->quality != -1) { quality = context->quality; } for (i = 0; i < context->qtablesLen; i++) { @@ -171,7 +171,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) 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) { + } else if (context->quality != -1) { jpeg_set_quality(&context->cinfo, context->quality, 1); } diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index eaa276af44a..a239464d4a0 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -555,6 +555,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}, diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index 67dcc1e0858..e5a38f4bec1 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -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; } diff --git a/src/libImaging/QuantHeap.c b/src/libImaging/QuantHeap.c index 121b8727560..498d44b1dfb 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,7 +44,7 @@ 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; @@ -64,7 +64,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 +80,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) { diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index 6c0f605c968..83d9875446b 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -44,7 +44,7 @@ typedef struct _ColorCube{ unsigned int rWidth, gWidth, bWidth, aWidth; unsigned int rOffset, gOffset, bOffset, aOffset; - long size; + unsigned long size; ColorBucket buckets; } *ColorCube; @@ -134,10 +134,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; @@ -194,7 +194,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; @@ -302,7 +302,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)) { @@ -345,8 +345,8 @@ map_image_pixels(const Pixel *pixelData, } } -const int CUBE_LEVELS[8] = {4, 4, 4, 0, 2, 2, 2, 0}; -const int CUBE_LEVELS_ALPHA[8] = {3, 4, 3, 3, 2, 2, 2, 2}; +const unsigned int CUBE_LEVELS[8] = {4, 4, 4, 0, 2, 2, 2, 0}; +const unsigned int CUBE_LEVELS_ALPHA[8] = {3, 4, 3, 3, 2, 2, 2, 2}; int quantize_octree(Pixel *pixelData, uint32_t nPixels, @@ -365,8 +365,8 @@ int quantize_octree(Pixel *pixelData, ColorBucket paletteBuckets = NULL; uint32_t *qp = NULL; long i; - long nCoarseColors, nFineColors, nAlreadySubtracted; - const int *cubeBits; + unsigned long nCoarseColors, nFineColors, nAlreadySubtracted; + const unsigned int *cubeBits; if (withAlpha) { cubeBits = CUBE_LEVELS_ALPHA; diff --git a/src/libImaging/Reduce.c b/src/libImaging/Reduce.c new file mode 100644 index 00000000000..d6ef92f5b41 --- /dev/null +++ b/src/libImaging/Reduce.c @@ -0,0 +1,1438 @@ +#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 4a98e847701..0dc08611da0 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -13,7 +13,7 @@ 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; } @@ -627,8 +627,6 @@ ImagingResampleInner(Imaging imIn, int xsize, int ysize, if ( ! ksize_vert) { free(bounds_horiz); free(kk_horiz); - free(bounds_vert); - free(kk_vert); return NULL; } diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index 8a81ba8e6c0..3f9400a5bf9 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -25,9 +25,10 @@ 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--) { @@ -37,6 +38,10 @@ static int expandrow(UINT8* dest, UINT8* src, int n, int z) count = pixel & RLE_MAX_RUN; if (!count) return count; + if (x + count > xsize) { + return -1; + } + x += count; if (pixel & RLE_COPY_FLAG) { while(count--) { *dest = *src++; @@ -56,10 +61,11 @@ 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--) { @@ -70,6 +76,10 @@ static int expandrow2(UINT8* dest, const UINT8* src, int n, int z) count = pixel & RLE_MAX_RUN; if (!count) return count; + if (x + count > xsize) { + return -1; + } + x += count; if (pixel & RLE_COPY_FLAG) { while(count--) { memcpy(dest, src, 2); @@ -96,6 +106,7 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *ptr; SGISTATE *c; int err = 0; + int status; /* Get all data from File descriptor */ c = (SGISTATE*)state->context; @@ -164,12 +175,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/TiffDecode.c b/src/libImaging/TiffDecode.c index 7592f7f39d1..532db1f685f 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -171,7 +171,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 +228,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 @@ -353,16 +353,25 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ // 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; @@ -415,11 +424,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; diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index ab0c8dc6082..adf2dd27700 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1306,6 +1306,9 @@ static struct { /* greyscale w. alpha */ {"LA", "LA", 16, unpackLA}, {"LA", "LA;L", 16, unpackLAL}, + + /* greyscale w. alpha premultiplied */ + {"La", "La", 16, unpackLA}, /* palette */ {"P", "P;1", 1, unpackP1}, @@ -1384,7 +1387,6 @@ static struct { {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, #endif - /* true colour w. alpha premultiplied */ {"RGBa", "RGBa", 32, copy4}, {"RGBa", "BGRa", 32, unpackBGRA}, diff --git a/src/libImaging/codec_fd.c b/src/libImaging/codec_fd.c index 7bd4dadf868..5cde31cdc5d 100644 --- a/src/libImaging/codec_fd.c +++ b/src/libImaging/codec_fd.c @@ -1,6 +1,5 @@ #include "Python.h" #include "Imaging.h" -#include "../py3.h" Py_ssize_t @@ -72,7 +71,7 @@ _imaging_tell_pyFd(PyObject *fd) Py_ssize_t location; result = PyObject_CallMethod(fd, "tell", NULL); - location = PyInt_AsSsize_t(result); + location = PyLong_AsSsize_t(result); Py_DECREF(result); return location; diff --git a/src/map.c b/src/map.c index 099bb4b3ef3..54e0fdb2296 100644 --- a/src/map.c +++ b/src/map.c @@ -22,8 +22,6 @@ #include "Imaging.h" -#include "py3.h" - /* compatibility wrappers (defined in _imaging.c) */ extern int PyImaging_CheckBuffer(PyObject* buffer); extern int PyImaging_GetBuffer(PyObject* buffer, Py_buffer *view); @@ -315,14 +313,13 @@ 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)) { @@ -357,17 +354,21 @@ PyImaging_MapBuffer(PyObject* self, PyObject* args) 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) diff --git a/src/path.c b/src/path.c index 5f0541b0beb..f69755d1685 100644 --- a/src/path.c +++ b/src/path.c @@ -31,8 +31,6 @@ #include -#include "py3.h" - /* compatibility wrappers (defined in _imaging.c) */ extern int PyImaging_CheckBuffer(PyObject* buffer); extern int PyImaging_GetBuffer(PyObject* buffer, Py_buffer *view); @@ -170,8 +168,8 @@ PyPath_Flatten(PyObject* data, double **pxy) PyObject *op = PyList_GET_ITEM(data, i); if (PyFloat_Check(op)) xy[j++] = PyFloat_AS_DOUBLE(op); - else if (PyInt_Check(op)) - xy[j++] = (float) PyInt_AS_LONG(op); + else if (PyLong_Check(op)) + xy[j++] = (float) PyLong_AS_LONG(op); else if (PyNumber_Check(op)) xy[j++] = PyFloat_AsDouble(op); else if (PyArg_ParseTuple(op, "dd", &x, &y)) { @@ -188,8 +186,8 @@ PyPath_Flatten(PyObject* data, double **pxy) PyObject *op = PyTuple_GET_ITEM(data, i); if (PyFloat_Check(op)) xy[j++] = PyFloat_AS_DOUBLE(op); - else if (PyInt_Check(op)) - xy[j++] = (float) PyInt_AS_LONG(op); + else if (PyLong_Check(op)) + xy[j++] = (float) PyLong_AS_LONG(op); else if (PyNumber_Check(op)) xy[j++] = PyFloat_AsDouble(op); else if (PyArg_ParseTuple(op, "dd", &x, &y)) { @@ -217,8 +215,8 @@ PyPath_Flatten(PyObject* data, double **pxy) } if (PyFloat_Check(op)) xy[j++] = PyFloat_AS_DOUBLE(op); - else if (PyInt_Check(op)) - xy[j++] = (float) PyInt_AS_LONG(op); + else if (PyLong_Check(op)) + xy[j++] = (float) PyLong_AS_LONG(op); else if (PyNumber_Check(op)) xy[j++] = PyFloat_AsDouble(op); else if (PyArg_ParseTuple(op, "dd", &x, &y)) { @@ -552,13 +550,8 @@ path_subscript(PyPathObject* self, PyObject* item) { int len = 4; Py_ssize_t start, stop, step, slicelength; -#if PY_VERSION_HEX >= 0x03020000 if (PySlice_GetIndicesEx(item, len, &start, &stop, &step, &slicelength) < 0) return NULL; -#else - if (PySlice_GetIndicesEx((PySliceObject*)item, len, &start, &stop, &step, &slicelength) < 0) - return NULL; -#endif if (slicelength <= 0) { double *xy = alloc_array(0); diff --git a/src/py3.h b/src/py3.h deleted file mode 100644 index 310583845f7..00000000000 --- a/src/py3.h +++ /dev/null @@ -1,56 +0,0 @@ -/* - Python3 definition file to consistently map the code to Python 2 or - Python 3. - - PyInt and PyLong were merged into PyLong in Python 3, so all PyInt functions - are mapped to PyLong. - - PyString, on the other hand, was split into PyBytes and PyUnicode. We map - both back onto PyString, so use PyBytes or PyUnicode where appropriate. The - only exception to this is _imagingft.c, where PyUnicode is left alone. -*/ - -#if PY_VERSION_HEX >= 0x03000000 -#define PY_ARG_BYTES_LENGTH "y#" - -/* Map PyInt -> PyLong */ -#define PyInt_AsLong PyLong_AsLong -#define PyInt_Check PyLong_Check -#define PyInt_FromLong PyLong_FromLong -#define PyInt_AS_LONG PyLong_AS_LONG -#define PyInt_FromSsize_t PyLong_FromSsize_t -#define PyInt_AsSsize_t PyLong_AsSsize_t - -#else /* PY_VERSION_HEX < 0x03000000 */ -#define PY_ARG_BYTES_LENGTH "s#" - -#if !defined(KEEP_PY_UNICODE) -/* Map PyUnicode -> PyString */ -#undef PyUnicode_AsString -#undef PyUnicode_AS_STRING -#undef PyUnicode_Check -#undef PyUnicode_FromStringAndSize -#undef PyUnicode_FromString -#undef PyUnicode_FromFormat -#undef PyUnicode_DecodeFSDefault - -#define PyUnicode_AsString PyString_AsString -#define PyUnicode_AS_STRING PyString_AS_STRING -#define PyUnicode_Check PyString_Check -#define PyUnicode_FromStringAndSize PyString_FromStringAndSize -#define PyUnicode_FromString PyString_FromString -#define PyUnicode_FromFormat PyString_FromFormat -#define PyUnicode_DecodeFSDefault PyString_FromString -#endif - -/* Map PyBytes -> PyString */ -#define PyBytesObject PyStringObject -#define PyBytes_AsString PyString_AsString -#define PyBytes_AS_STRING PyString_AS_STRING -#define PyBytes_Check PyString_Check -#define PyBytes_AsStringAndSize PyString_AsStringAndSize -#define PyBytes_FromStringAndSize PyString_FromStringAndSize -#define PyBytes_FromString PyString_FromString -#define _PyBytes_Resize _PyString_Resize - -#endif /* PY_VERSION_HEX < 0x03000000 */ diff --git a/tox.ini b/tox.ini index 2dc92037148..9f310ca3a05 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ [tox] envlist = lint - py{27,35,36,37} + py{35,36,37,38,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,9 @@ deps = [testenv:lint] commands = - black --check --diff . - flake8 --statistics --count - isort --check-only --diff + pre-commit run --all-files check-manifest deps = - black + pre-commit check-manifest - flake8 - isort skip_install = true diff --git a/winbuild/appveyor_install_msys2_deps.sh b/winbuild/appveyor_install_msys2_deps.sh index 02b75e210e2..4cc01082d98 100644 --- a/winbuild/appveyor_install_msys2_deps.sh +++ b/winbuild/appveyor_install_msys2_deps.sh @@ -2,15 +2,11 @@ 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 \ - mingw32/mingw-w64-i686-python2-pip \ - mingw32/mingw-w64-i686-python2-setuptools \ - mingw32/mingw-w64-i686-python2-pytest \ - mingw32/mingw-w64-i686-python2-pytest-cov \ - mingw-w64-i686-libjpeg-turbo \ - mingw-w64-i686-libimagequant + 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 diff --git a/winbuild/appveyor_install_pypy2.cmd b/winbuild/appveyor_install_pypy2.cmd deleted file mode 100644 index fc56d0e56ab..00000000000 --- a/winbuild/appveyor_install_pypy2.cmd +++ /dev/null @@ -1,3 +0,0 @@ -curl -fsSL -o pypy2.zip https://bitbucket.org/pypy/pypy/downloads/pypy2.7-v7.1.1-win32.zip -7z x pypy2.zip -oc:\ -c:\Python37\Scripts\virtualenv.exe -p c:\pypy2.7-v7.1.1-win32\pypy.exe c:\vp\pypy2 diff --git a/winbuild/appveyor_install_pypy3.cmd b/winbuild/appveyor_install_pypy3.cmd index 63659b165d4..75a22ca59e4 100644 --- a/winbuild/appveyor_install_pypy3.cmd +++ b/winbuild/appveyor_install_pypy3.cmd @@ -1,3 +1,3 @@ -curl -fsSL -o pypy3.zip http://buildbot.pypy.org/nightly/py3.6/pypy-c-jit-97588-7392d01b93d0-win32.zip +curl -fsSL -o pypy3.zip https://bitbucket.org/pypy/pypy/downloads/pypy3.6-v7.3.0-win32.zip 7z x pypy3.zip -oc:\ -c:\Python37\Scripts\virtualenv.exe -p c:\pypy-c-jit-97588-7392d01b93d0-win32\pypy3.exe c:\vp\pypy3 +c:\Python37\Scripts\virtualenv.exe -p c:\pypy3.6-v7.3.0-win32\pypy3.exe c:\vp\pypy3 diff --git a/winbuild/build.py b/winbuild/build.py index 0617022dcf9..e565226bd92 100755 --- a/winbuild/build.py +++ b/winbuild/build.py @@ -53,10 +53,10 @@ def run_script(params): print(stderr.decode()) print("-- stdout --") print(trace.decode()) - print("Done with %s: %s" % (version, status)) + print("Done with {}: {}".format(version, status)) return (version, status, trace, stderr) except Exception as msg: - print("Error with %s: %s" % (version, str(msg))) + print("Error with {}: {}".format(version, str(msg))) return (version, -1, "", str(msg)) @@ -98,17 +98,14 @@ def build_one(py_ver, compiler, bit): if "PYTHON" in os.environ: args["python_path"] = "%PYTHON%" else: - args["python_path"] = "%s%s\\Scripts" % (VIRT_BASE, py_ver) + 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 - if "27" in py_ver: - args["tcl_ver"] = "85" - else: - args["tcl_ver"] = "86" + args["tcl_ver"] = "86" if compiler["vc_version"] == "2015": args["imaging_libs"] = " build_ext --add-imaging-libs=msvcrt" @@ -127,7 +124,7 @@ def build_one(py_ver, compiler, bit): 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('%%INCLIB%%\\freetype.dll', os.path.dirname(_webp.__file__));" +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 @@ -160,7 +157,7 @@ def main(op): scripts.append( ( - "%s%s" % (py_version, X64_EXT), + "{}{}".format(py_version, X64_EXT), "\n".join( [ header(op), @@ -174,7 +171,7 @@ def main(op): results = map(run_script, scripts) for (version, status, trace, err) in results: - print("Compiled %s: %s" % (version, status and "ERR" or "OK")) + print("Compiled {}: {}".format(version, status and "ERR" or "OK")) def run_one(op): diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index 487329db8b5..77857013938 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -45,9 +45,7 @@ def extract(src, dest): def extract_libs(): for name, lib in libs.items(): - filename = lib["filename"] - if not os.path.exists(filename): - filename = fetch(lib["url"]) + filename = fetch(lib["url"]) if name == "openjpeg": for compiler in all_compilers(): if not os.path.exists( @@ -103,7 +101,7 @@ def header(): set INCLIB=%~dp0\depends set BUILD=%~dp0\build """ + "\n".join( - r"set %s=%%BUILD%%\%s" % (k.upper(), v["dir"]) + r"set {}=%BUILD%\{}".format(k.upper(), v["dir"]) for (k, v) in libs.items() if v["dir"] ) @@ -202,7 +200,7 @@ def nmake_libs(compiler, bit): + vc_setup(compiler, bit) + r""" rem do after building jpeg and zlib -copy %%~dp0\nmake.opt %%TIFF%% +copy %%~dp0\tiff.opt %%TIFF%%\nmake.opt cd /D %%TIFF%% nmake -nologo -f makefile.vc clean @@ -227,8 +225,8 @@ def msbuild_freetype(compiler, bit): 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" + r'"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Lib\x64\*.Lib" ' + r"%%FREETYPE%%\builds\windows\vc2010" ) properties += r" /p:_IsNativeEnvironment=false" script += ( @@ -270,6 +268,7 @@ def build_lcms_70(compiler): 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 @@ -287,8 +286,10 @@ def build_lcms_71(compiler): 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%% @@ -299,33 +300,6 @@ def build_lcms_71(compiler): ) -def build_ghostscript(compiler, bit): - script = ( - r""" -rem Build gs -setlocal -""" - + vc_setup(compiler, bit) - + r""" -set MSVC_VERSION=""" - + {"2010": "90", "2015": "14"}[compiler["vc_version"]] - + r""" -set RCOMP="C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\RC.Exe" -cd /D %%GHOSTSCRIPT%% -""" - ) - if bit == 64: - script += r""" -set WIN64="" -""" - script += r""" -nmake -nologo -f psi/msvc.mak -copy /Y /B bin\ C:\Python27\ -endlocal -""" - return script % compiler - - def add_compiler(compiler, bit): script.append(setup_compiler(compiler)) script.append(nmake_libs(compiler, bit)) @@ -335,7 +309,6 @@ def add_compiler(compiler, bit): script.append(msbuild_freetype(compiler, bit)) script.append(build_lcms2(compiler)) script.append(nmake_openjpeg(compiler, bit)) - script.append(build_ghostscript(compiler, bit)) script.append(end_compiler()) diff --git a/winbuild/config.py b/winbuild/config.py index 16a1d9cadf0..93413d1e573 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -1,15 +1,19 @@ import os -SF_MIRROR = "http://iweb.dl.sourceforge.net" -PILLOW_DEPENDS_DIR = "C:\\pillow-depends\\" +SF_MIRROR = "https://iweb.dl.sourceforge.net" pythons = { - "27": {"compiler": 7, "vc": 2010}, - "pypy2": {"compiler": 7, "vc": 2010}, + "pypy3": {"compiler": 7.1, "vc": 2015}, + # for AppVeyor "35": {"compiler": 7.1, "vc": 2015}, "36": {"compiler": 7.1, "vc": 2015}, - "pypy3": {"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/" @@ -22,66 +26,92 @@ # }, "zlib": { "url": "http://zlib.net/zlib1211.zip", - "filename": PILLOW_DEPENDS_DIR + "zlib1211.zip", + "filename": "zlib1211.zip", "dir": "zlib-1.2.11", }, "jpeg": { - "url": "http://www.ijg.org/files/jpegsr9c.zip", - "filename": PILLOW_DEPENDS_DIR + "jpegsr9c.zip", - "dir": "jpeg-9c", + "url": "http://www.ijg.org/files/jpegsr9d.zip", + "filename": "jpegsr9d.zip", + "dir": "jpeg-9d", }, "tiff": { - "url": "ftp://download.osgeo.org/libtiff/tiff-4.0.10.tar.gz", - "filename": PILLOW_DEPENDS_DIR + "tiff-4.0.10.tar.gz", - "dir": "tiff-4.0.10", + "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": PILLOW_DEPENDS_DIR + "freetype-2.10.1.tar.gz", + "filename": "freetype-2.10.1.tar.gz", "dir": "freetype-2.10.1", }, - "lcms": { + "lcms-2.7": { "url": SF_MIRROR + "/project/lcms/lcms/2.7/lcms2-2.7.zip", - "filename": PILLOW_DEPENDS_DIR + "lcms2-2.7.zip", + "filename": "lcms2-2.7.zip", "dir": "lcms2-2.7", }, - "ghostscript": { - "url": "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs927/ghostscript-9.27.tar.gz", # noqa: E501 - "filename": PILLOW_DEPENDS_DIR + "ghostscript-9.27.tar.gz", - "dir": "ghostscript-9.27", + "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": PILLOW_DEPENDS_DIR + "tcl8519-src.zip", + "filename": "tcl8519-src.zip", "dir": "", }, "tk-8.5": { "url": SF_MIRROR + "/project/tcl/Tcl/8.5.19/tk8519-src.zip", - "filename": PILLOW_DEPENDS_DIR + "tk8519-src.zip", + "filename": "tk8519-src.zip", "dir": "", "version": "8.5.19", }, "tcl-8.6": { - "url": SF_MIRROR + "/project/tcl/Tcl/8.6.9/tcl869-src.zip", - "filename": PILLOW_DEPENDS_DIR + "tcl869-src.zip", + "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.9/tk869-src.zip", - "filename": PILLOW_DEPENDS_DIR + "tk869-src.zip", + "url": SF_MIRROR + "/project/tcl/Tcl/8.6.10/tk8610-src.zip", + "filename": "tk8610-src.zip", "dir": "", - "version": "8.6.9", + "version": "8.6.10", }, "webp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.0.3.tar.gz", - "filename": PILLOW_DEPENDS_DIR + "libwebp-1.0.3.tar.gz", - "dir": "libwebp-1.0.3", + "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", }, "openjpeg": { "url": "https://github.com/uclouvain/openjpeg/archive/v2.3.1.tar.gz", - "filename": PILLOW_DEPENDS_DIR + "openjpeg-2.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", + }, + # e5d454b: Merge tag '2.12.6' into msvc + "imagequant": { + "url": "https://github.com/ImageOptim/libimagequant/archive/e5d454bc7f5eb63ee50c84a83a7fa5ac94f68ec4.zip", # noqa: E501 + "filename": "libimagequant-e5d454bc7f5eb63ee50c84a83a7fa5ac94f68ec4.zip", + "dir": "libimagequant-e5d454bc7f5eb63ee50c84a83a7fa5ac94f68ec4", + }, + "harfbuzz": { + "url": "https://github.com/harfbuzz/harfbuzz/archive/2.6.4.zip", + "filename": "harfbuzz-2.6.4.zip", + "dir": "harfbuzz-2.6.4", + }, + "fribidi": { + "url": "https://github.com/fribidi/fribidi/archive/v1.0.9.zip", + "filename": "fribidi-1.0.9.zip", + "dir": "fribidi-1.0.9", + }, + "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 = { @@ -131,14 +161,14 @@ def pyversion_from_env(): py = os.environ["PYTHON"] - py_version = "27" + py_version = "35" for k in pythons: if k in py: py_version = k break if "64" in py: - py_version = "%s%s" % (py_version, X64_EXT) + py_version = "{}{}".format(py_version, X64_EXT) return py_version diff --git a/winbuild/fetch.py b/winbuild/fetch.py index 804e4ef0c12..adc45429a44 100644 --- a/winbuild/fetch.py +++ b/winbuild/fetch.py @@ -3,16 +3,37 @@ 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): - print("Fetching", url) + + 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 = urllib.request.urlopen(url) - except urllib.error.URLError: - r = urllib.request.urlopen(url) + 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) diff --git a/winbuild/fribidi.cmake b/winbuild/fribidi.cmake new file mode 100644 index 00000000000..247e79e4cdd --- /dev/null +++ b/winbuild/fribidi.cmake @@ -0,0 +1,102 @@ +cmake_minimum_required(VERSION 3.13) + +project(fribidi) + +add_definitions(-D_CRT_SECURE_NO_WARNINGS) + +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +include_directories(lib) + +function(extract_regex_1 var text regex) + string(REGEX MATCH ${regex} _ ${text}) + set(${var} "${CMAKE_MATCH_1}" PARENT_SCOPE) +endfunction() + + +function(fribidi_conf) + file(READ configure.ac FRIBIDI_CONF) + extract_regex_1(FRIBIDI_MAJOR_VERSION "${FRIBIDI_CONF}" "\\(fribidi_major_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_MINOR_VERSION "${FRIBIDI_CONF}" "\\(fribidi_minor_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_MICRO_VERSION "${FRIBIDI_CONF}" "\\(fribidi_micro_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_INTERFACE_VERSION "${FRIBIDI_CONF}" "\\(fribidi_interface_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_INTERFACE_AGE "${FRIBIDI_CONF}" "\\(fribidi_interface_age, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_BINARY_AGE "${FRIBIDI_CONF}" "\\(fribidi_binary_age, ([0-9]+)\\)") + set(FRIBIDI_VERSION "${FRIBIDI_MAJOR_VERSION}.${FRIBIDI_MINOR_VERSION}.${FRIBIDI_MICRO_VERSION}") + set(PACKAGE "fribidi") + set(PACKAGE_NAME "GNU FriBidi") + set(PACKAGE_BUGREPORT "https://github.com/fribidi/fribidi/issues/new") + set(SIZEOF_INT 4) + set(FRIBIDI_MSVC_BUILD_PLACEHOLDER "#define FRIBIDI_BUILT_WITH_MSVC") + message("detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}") + configure_file(lib/fribidi-config.h.in lib/fribidi-config.h @ONLY) +endfunction() +fribidi_conf() + + +function(prepend var prefix) + set(out "") + foreach(f ${ARGN}) + list(APPEND out "${prefix}${f}") + endforeach() + set(${var} "${out}" PARENT_SCOPE) +endfunction() + +macro(fribidi_definitions _TGT) + target_compile_definitions(${_TGT} PUBLIC + HAVE_MEMSET + HAVE_MEMMOVE + HAVE_STRDUP + HAVE_STDLIB_H=1 + HAVE_STRING_H=1 + HAVE_MEMORY_H=1 + #HAVE_STRINGS_H + #HAVE_SYS_TIMES_H + STDC_HEADERS=1 + HAVE_STRINGIZE=1) +endmacro() + +function(fribidi_gen _NAME _OUTNAME _PARAM) + set(_OUT lib/${_OUTNAME}) + prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN}) + add_executable(gen-${_NAME} + gen.tab/gen-${_NAME}.c + gen.tab/packtab.c) + fribidi_definitions(gen-${_NAME}) + target_compile_definitions(gen-${_NAME} + PUBLIC DONT_HAVE_FRIBIDI_CONFIG_H) + add_custom_command( + COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT} + DEPENDS ${_DEP} + OUTPUT ${_OUT}) + list(APPEND FRIBIDI_SOURCES_GENERATED "${_OUT}") + set(FRIBIDI_SOURCES_GENERATED ${FRIBIDI_SOURCES_GENERATED} PARENT_SCOPE) +endfunction() + +fribidi_gen(unicode-version fribidi-unicode-version.h "" + unidata/ReadMe.txt unidata/BidiMirroring.txt) + + +macro(fribidi_tab _NAME) + fribidi_gen(${_NAME}-tab ${_NAME}.tab.i 2 ${ARGN}) + target_sources(gen-${_NAME}-tab + PRIVATE lib/fribidi-unicode-version.h) +endmacro() + +fribidi_tab(bidi-type unidata/UnicodeData.txt) +fribidi_tab(joining-type unidata/UnicodeData.txt unidata/ArabicShaping.txt) +fribidi_tab(arabic-shaping unidata/UnicodeData.txt) +fribidi_tab(mirroring unidata/BidiMirroring.txt) +fribidi_tab(brackets unidata/BidiBrackets.txt unidata/UnicodeData.txt) +fribidi_tab(brackets-type unidata/BidiBrackets.txt) + + +file(GLOB FRIBIDI_SOURCES lib/*.c) +file(GLOB FRIBIDI_HEADERS lib/*.h) + +add_library(fribidi STATIC + ${FRIBIDI_SOURCES} + ${FRIBIDI_HEADERS} + ${FRIBIDI_SOURCES_GENERATED}) +fribidi_definitions(fribidi) +target_compile_definitions(fribidi + PUBLIC -DFRIBIDI_ENTRY=extern) diff --git a/winbuild/get_pythons.py b/winbuild/get_pythons.py index e24bb65f78e..a853fc6f7ea 100644 --- a/winbuild/get_pythons.py +++ b/winbuild/get_pythons.py @@ -3,7 +3,7 @@ from fetch import fetch if __name__ == "__main__": - for version in ["2.7.15", "3.4.4"]: + for version in ["3.4.4"]: for platform in ["", ".amd64"]: for extension in ["", ".asc"]: fetch( diff --git a/winbuild/lcms2_patch.ps1 b/winbuild/lcms2_patch.ps1 new file mode 100644 index 00000000000..7fc48c034e5 --- /dev/null +++ b/winbuild/lcms2_patch.ps1 @@ -0,0 +1,9 @@ + +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 new file mode 100644 index 00000000000..88eb7f28479 --- /dev/null +++ b/winbuild/raqm.cmake @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.13) + +project(libraqm) + + +find_library(fribidi NAMES fribidi) +find_library(harfbuzz NAMES harfbuzz) +find_library(freetype NAMES freetype) + +add_definitions(-DFRIBIDI_ENTRY=extern) + + +function(raqm_conf) + file(READ configure.ac RAQM_CONF) + string(REGEX MATCH "\\[([0-9]+)\\.([0-9]+)\\.([0-9]+)\\]," _ "${RAQM_CONF}") + set(RAQM_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(RAQM_VERSION_MINOR "${CMAKE_MATCH_2}") + set(RAQM_VERSION_MICRO "${CMAKE_MATCH_3}") + set(RAQM_VERSION "${RAQM_VERSION_MAJOR}.${RAQM_VERSION_MINOR}.${RAQM_VERSION_MICRO}") + message("detected libraqm version ${RAQM_VERSION}") + configure_file(src/raqm-version.h.in src/raqm-version.h @ONLY) +endfunction() +raqm_conf() + + +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +set(RAQM_SOURCES + src/raqm.c) +set(RAQM_HEADERS + src/raqm.h + src/raqm-version.h) + +add_library(libraqm SHARED + ${RAQM_SOURCES} + ${RAQM_HEADERS}) +target_link_libraries(libraqm + ${fribidi} + ${harfbuzz} + ${freetype}) diff --git a/winbuild/test.py b/winbuild/test.py index 559ecdec10d..a05a20b1892 100755 --- a/winbuild/test.py +++ b/winbuild/test.py @@ -13,7 +13,7 @@ def test_one(params): try: print("Running: %s, %s" % params) command = [ - r"%s\%s%s\Scripts\python.exe" % (VIRT_BASE, python, architecture), + r"{}\{}{}\Scripts\python.exe".format(VIRT_BASE, python, architecture), "test-installed.py", "--processes=-0", "--process-timeout=30", @@ -22,10 +22,10 @@ def test_one(params): proc = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE) (trace, stderr) = proc.communicate() status = proc.returncode - print("Done with %s, %s -- %s" % (python, architecture, status)) + print("Done with {}, {} -- {}".format(python, architecture, status)) return (python, architecture, status, trace) except Exception as msg: - print("Error with %s, %s: %s" % (python, architecture, msg)) + print("Error with {}, {}: {}".format(python, architecture, msg)) return (python, architecture, -1, str(msg)) @@ -39,7 +39,7 @@ def test_one(params): results = map(test_one, matrix) for (python, architecture, status, trace) in results: - print("%s%s: %s" % (python, architecture, status and "ERR" or "PASS")) + 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/nmake.opt b/winbuild/tiff.opt similarity index 100% rename from winbuild/nmake.opt rename to winbuild/tiff.opt