diff --git a/.appveyor.yml b/.appveyor.yml index 2a4e67dc9..2e3fd0a58 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 @@ -30,6 +22,10 @@ environment: PIP_DIR: bin TEST_OPTIONS: --processes=0 DEPLOY: NO + - PYTHON: C:/vp/pypy3 + EXECUTABLE: bin/pypy.exe + PIP_DIR: bin + VENV: YES install: @@ -41,9 +37,9 @@ install: - xcopy /s c:\pillow-depends\test_images\* c:\pillow\tests\images - cd c:\pillow\winbuild\ - ps: | - if ($env:PYTHON -eq "c:/vp/pypy2") + if ($env:PYTHON -eq "c:/vp/pypy3") { - c:\pillow\winbuild\appveyor_install_pypy.cmd + c:\pillow\winbuild\appveyor_install_pypy3.cmd } - ps: | if ($env:PYTHON -eq "c:/msys64/mingw32") @@ -56,6 +52,9 @@ install: c:\pillow\winbuild\build_deps.cmd $host.SetShouldExit(0) } +- curl -fsSL -o gs950.exe https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs950/gs950w32.exe +- gs950.exe /S +- path %path%;C:\Program Files (x86)\gs\gs9.50\bin build_script: - ps: | @@ -76,7 +75,8 @@ 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: diff --git a/.codecov.yml b/.codecov.yml index 3e147d151..a9ab1c2d7 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -3,7 +3,14 @@ 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 + # https://docs.codecov.io/docs/comparing-commits allow_coverage_offsets: true comment: off + +# Matches 'omit:' in .coveragerc +ignore: + - "Tests/32bit_segfault_check.py" + - "Tests/bench_cffi_access.py" + - "Tests/check_*.py" + - "Tests/createfontdatachunk.py" diff --git a/.coveragerc b/.coveragerc index ea79190ae..f1f095806 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,3 +12,10 @@ exclude_lines = # 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 b3d456659..3d27b5d88 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 ca04afe02..e0e6804bf 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -tidelift: pypi/pillow +tidelift: "pypi/Pillow" diff --git a/.github/codecov-upstream.yml b/.github/codecov-upstream.yml new file mode 100644 index 000000000..060b2685e --- /dev/null +++ b/.github/codecov-upstream.yml @@ -0,0 +1,18 @@ +# 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 + + token: 6dafc396-e7f5-4221-a38a-8b07a49fbdae + +comment: off + +# Matches 'omit:' in .coveragerc +ignore: + - "Tests/32bit_segfault_check.py" + - "Tests/bench_cffi_access.py" + - "Tests/check_*.py" + - "Tests/createfontdatachunk.py" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..576b88414 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,32 @@ +name: Lint + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8"] + + name: Python ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v1 + + - 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 dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade tox + + - 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 000000000..6cd9dadf3 --- /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 000000000..8e840319a --- /dev/null +++ b/.github/workflows/system-info.py @@ -0,0 +1,25 @@ +""" +Print out some handy system info like Travis CI does. + +This sort of info is missing from GitHub Actions. + +Requested here: +https://github.com/actions/virtual-environments/issues/79 +""" +import os +import platform +import sys + +print("Build system information") +print() + +print("sys.version\t\t", sys.version.split("\n")) +print("os.name\t\t\t", os.name) +print("sys.platform\t\t", sys.platform) +print("platform.system()\t", platform.system()) +print("platform.machine()\t", platform.machine()) +print("platform.platform()\t", platform.platform()) +print("platform.version()\t", platform.version()) +print("platform.uname()\t", platform.uname()) +if sys.platform == "darwin": + print("platform.mac_ver()\t", platform.mac_ver()) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml new file mode 100644 index 000000000..1a6eaac69 --- /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: + 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 -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + sudo chown -R runner $GITHUB_WORKSPACE + + - name: After success + if: success() + run: | + pip install wheel + sudo apt-get install -qq ruby-dev + PATH="$PATH:~/.local/bin" + .travis/after_success.sh + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + + - name: Prepare coverage token + if: success() && github.repository == 'python-pillow/Pillow' + run: cp .github/codecov-upstream.yml .codecov.yml + + - name: Upload coverage + if: success() + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: ${{ matrix.docker }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml new file mode 100644 index 000000000..e257a9502 --- /dev/null +++ b/.github/workflows/test-windows.yml @@ -0,0 +1,399 @@ +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.6"] + architecture: ["x86", "x64"] + include: + - architecture: "x86" + platform-vcvars: "x86" + platform-msbuild: "Win32" + - architecture: "x64" + platform-vcvars: "x86_amd64" + platform-msbuild: "x64" + - python-version: "pypy3.6" + pypy-version: "pypy3.6-v7.3.0-win32" + pypy-url: "https://bitbucket.org/pypy/pypy/downloads/pypy3.6-v7.3.0-win32.zip" + exclude: + - python-version: "pypy3.6" + 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 }}- + + - name: Install PyPy + if: "contains(matrix.python-version, 'pypy')" + run: | + curl -fsSL -o pypy3.zip "${{ matrix.pypy-url }}" + 7z x pypy3.zip "-o$env:RUNNER_WORKSPACE\" + mv "$env:RUNNER_WORKSPACE\${{ matrix.pypy-version }}" "$env:RUNNER_WORKSPACE\${{ matrix.python-version }}" + $env:PYTHON="$env:RUNNER_WORKSPACE\${{ matrix.python-version }}" + # set env: pythonLocation + Write-Host "`#`#[set-env name=pythonLocation;]$env:PYTHON" # old syntax https://github.com/actions/toolkit/issues/61 + Write-Host "::set-env name=pythonLocation::$env:PYTHON" # new syntax https://github.com/actions/toolkit/blob/5bb77ec03fea98332e41f9347c8fbb1ce1e48f4a/docs/commands.md + New-Item -ItemType SymbolicLink -Path "$env:PYTHON\python.exe" -Target "$env:PYTHON\pypy3.exe" + curl -fsSL -o get-pip.py https://bootstrap.pypa.io/get-pip.py + $env:PATH = "$env:PYTHON\bin;$env:PATH" + & $env:PYTHON\python.exe get-pip.py + shell: pwsh + + # sets env: pythonLocation + - name: Set up Python + if: "!contains(matrix.python-version, 'pypy')" + 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-9c + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + nmake -nologo -f makefile.vc setup-vc6 + nmake -nologo -f makefile.vc clean + nmake -nologo -f makefile.vc nodebug=1 libjpeg.lib cjpeg.exe djpeg.exe + copy /Y /B j*.h %INCLIB% + copy /Y /B *.lib %INCLIB% + copy /Y /B *.exe %INCLIB% + shell: cmd + + - name: 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.0.3 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + rmdir /S /Q output\release-static + nmake -nologo -f Makefile.vc CFG=release-static OBJDIR=output ARCH=${{ matrix.architecture }} all + mkdir %INCLIB%\webp + copy /Y /B src\webp\*.h %INCLIB%\webp + copy /Y /B output\release-static\${{ matrix.architecture }}\lib\* %INCLIB% + shell: cmd + + - 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.1 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF + set CMAKE=%CMAKE% -DHB_HAVE_FREETYPE:BOOL=ON -DCMAKE_BUILD_TYPE=Release + %CMAKE% -G "NMake Makefiles" . + nmake -nologo -f Makefile clean + nmake -nologo -f Makefile harfbuzz + copy /Y /B src\*.h %INCLIB% + copy /Y /B *.lib %INCLIB% + shell: cmd + + # for Raqm + - name: Build dependencies / FriBidi + run: | + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\fribidi-1.0.7 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + copy /Y /B %GITHUB_WORKSPACE%\winbuild\fribidi.cmake CMakeLists.txt + set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF + set CMAKE=%CMAKE% -DCMAKE_BUILD_TYPE=Release + %CMAKE% -G "NMake Makefiles" . + nmake -nologo -f Makefile clean + nmake -nologo -f Makefile fribidi + copy /Y /B lib\*.h %INCLIB% + copy /Y /B *.lib %INCLIB% + shell: cmd + + - 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 --cov PIL --cov Tests --cov-report term --cov-report xml Tests + shell: cmd + + - name: Upload errors + uses: actions/upload-artifact@v1 + if: failure() + with: + name: errors + path: Tests/errors + + - name: Prepare coverage token + if: success() && github.repository == 'python-pillow/Pillow' + run: cp .github/codecov-upstream.yml .codecov.yml + + - name: Upload coverage + if: success() + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + 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 000000000..8c80bcd71 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,103 @@ +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 + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v1 + + - name: Ubuntu cache + uses: actions/cache@v1 + if: startsWith(matrix.os, 'ubuntu') + with: + path: ~/.cache/pip + key: + ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/.travis/*.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('**/.travis/*.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: | + .travis/install.sh + + - name: Install macOS dependencies + if: startsWith(matrix.os, 'macOS') + run: | + .github/workflows/macos-install.sh + + - name: Build + run: | + .travis/build.sh + + - name: Test + run: | + .travis/test.sh + + - name: Upload errors + uses: actions/upload-artifact@v1 + if: failure() + with: + name: errors + path: Tests/errors + + - name: After success + if: success() + run: | + .travis/after_success.sh + env: + MATRIX_OS: ${{ matrix.os }} + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + + - name: Prepare coverage token + if: success() && github.repository == 'python-pillow/Pillow' + run: cp .github/codecov-upstream.yml .codecov.yml + + - name: Upload coverage + if: success() + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/.travis.yml b/.travis.yml index 545cb0b50..936e64520 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,8 @@ 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. +# Run slow PyPy next, to give it a headstart and reduce waiting time. +# Run latest 3.x next, to get quick compatibility results. # Then run the remainder, with fastest Docker jobs last. matrix: @@ -16,25 +16,22 @@ matrix: - python: "3.6" name: "Lint" env: LINT="true" - - python: "pypy" - name: "PyPy2 Xenial" - 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" + services: xvfb - env: DOCKER="alpine" DOCKER_TAG="master" - env: DOCKER="arch" DOCKER_TAG="master" # contains PyQt5 - env: DOCKER="ubuntu-16.04-xenial-amd64" DOCKER_TAG="master" @@ -43,10 +40,11 @@ matrix: - 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="centos-8-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" + - env: DOCKER="fedora-31-amd64" DOCKER_TAG="master" services: - docker @@ -67,7 +65,8 @@ script: if [ "$LINT" == "true" ]; then tox -e lint elif [ "$DOCKER" == "" ]; then - .travis/script.sh + .travis/build.sh + .travis/test.sh elif [ "$DOCKER" ]; then # the Pillow user in the docker container is UID 1000 sudo chown -R 1000 $TRAVIS_BUILD_DIR diff --git a/.travis/after_success.sh b/.travis/after_success.sh index 1dca2ccb9..2b1bc9f84 100755 --- a/.travis/after_success.sh +++ b/.travis/after_success.sh @@ -1,26 +1,30 @@ #!/bin/bash # gather the coverage data -sudo apt-get -qq install lcov +if [[ "$MATRIX_OS" == "macOS-latest" ]]; then + brew install lcov +else + sudo apt-get -qq install lcov +fi + lcov --capture --directory . -b . --output-file coverage.info # filter to remove system headers lcov --remove coverage.info '/usr/*' -o coverage.filtered.info # convert to json -gem install coveralls-lcov +sudo 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 +coverage report -if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ] && [ "$DOCKER" == "" ]; then +pip install coveralls-merge +coveralls-merge coverage.c.json +if [[ $TRAVIS ]]; then + codecov +fi + +if [ "$TRAVIS_PYTHON_VERSION" == "3.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/build.sh b/.travis/build.sh new file mode 100755 index 000000000..a2e3041bd --- /dev/null +++ b/.travis/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/.travis/install.sh index 725880934..6fba26ccb 100755 --- a/.travis/install.sh +++ b/.travis/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,13 @@ pip install -U pytest-cov pip install pyroma pip install test-image-results pip install numpy +if [[ $TRAVIS_PYTHON_VERSION == 3.* ]]; then + sudo apt-get -qq install pyqt5-dev-tools + pip install pyqt5 +fi -# docs only on Python 2.7 -if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then pip install -r requirements.txt ; 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/.travis/script.sh b/.travis/script.sh deleted file mode 100755 index af56cc6ab..000000000 --- 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/.travis/test.sh b/.travis/test.sh new file mode 100755 index 000000000..832d90433 --- /dev/null +++ b/.travis/test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +python -m pytest -v -x --cov PIL --cov Tests --cov-report term Tests + +# Docs +if [ "$TRAVIS_PYTHON_VERSION" == "3.8" ]; then make doccheck; fi diff --git a/CHANGES.rst b/CHANGES.rst index 064497dcf..e4c9ce09a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,11 +2,152 @@ Changelog (Pillow) ================== -6.2.0 (unreleased) +7.0.0 (unreleased) +------------------ + +- 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] + +- Drop support for EOL Python 2.7 #4109 + [hugovk, radarhere, jdufresne] + +- 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.1 (2019-10-21) ------------------ - This is the last Pillow release to support Python 2.7 #3642 +- Add support for Python 3.8 #4141 + [hugovk] + +6.2.0 (2019-10-01) +------------------ + +- Catch buffer overruns #4104 + [radarhere] + +- Initialize rows_per_strip when RowsPerStrip tag is missing #4034 + [cgohlke, radarhere] + +- Raise error if TIFF dimension is a string #4103 + [radarhere] + +- Added decompression bomb checks #4102 + [radarhere] + +- Fix ImageGrab.grab DPI scaling on Windows 10 version 1607+ #4000 + [nulano, radarhere] + +- Corrected negative seeks #4101 + [radarhere] + +- Added argument to capture all screens on Windows #3950 + [nulano, radarhere] + +- Updated warning to specify when Image.frombuffer defaults will change #4086 + [radarhere] + +- Changed WindowsViewer format to PNG #4080 + [radarhere] + +- Use TIFF orientation #4063 + [radarhere] + +- Raise the same error if a truncated image is loaded a second time #3965 + [radarhere] + +- Lazily use ImageFileDirectory_v1 values from Exif #4031 + [radarhere] + +- Improved HSV conversion #4004 + [radarhere] + +- Added text stroking #3978 + [radarhere, hugovk] + +- No more deprecated bdist_wininst .exe installers #4029 + [hugovk] + +- Do not allow floodfill to extend into negative coordinates #4017 + [radarhere] + +- Fixed arc drawing bug for a non-whole number of degrees #4014 + [radarhere] + - Fix bug when merging identical images to GIF with a list of durations #4003 [djy0, radarhere] @@ -70,7 +211,7 @@ Changelog (Pillow) - Updated TIFF tile descriptors to match current decoding functionality #3795 [dmnisson] -- Added an `image.entropy()` method (second revision) #3608 +- Added an ``image.entropy()`` method (second revision) #3608 [fish2000] - Pass the correct types to PyArg_ParseTuple #3880 @@ -706,7 +847,7 @@ Changelog (Pillow) - Enable background colour parameter on rotate #3057 [storesource] -- Remove unnecessary `#if 1` directive #3072 +- Remove unnecessary ``#if 1`` directive #3072 [jdufresne] - Remove unused Python class, Path #3070 @@ -1243,7 +1384,7 @@ Changelog (Pillow) - Add decompression bomb check to Image.crop #2410 [wiredfool] -- ImageFile: Ensure that the `err_code` variable is initialized in case of exception. #2363 +- ImageFile: Ensure that the ``err_code`` variable is initialized in case of exception. #2363 [alexkiro] - Tiff: Support append_images for saving multipage TIFFs #2406 @@ -1480,7 +1621,7 @@ Changelog (Pillow) - Removed PIL 1.0 era TK readme that concerns Windows 95/NT #2360 [wiredfool] -- Prevent `nose -v` printing docstrings #2369 +- Prevent ``nose -v`` printing docstrings #2369 [hugovk] - Replaced absolute PIL imports with relative imports #2349 @@ -1925,7 +2066,7 @@ Changelog (Pillow) - Changed depends/install_*.sh urls to point to github pillow-depends repo #1983 [wiredfool] -- Allow ICC profile from `encoderinfo` while saving PNGs #1909 +- Allow ICC profile from ``encoderinfo`` while saving PNGs #1909 [homm] - Fix integer overflow on ILP32 systems (32-bit Linux). #1975 @@ -2368,7 +2509,7 @@ Changelog (Pillow) - Added PDF multipage saving #1445 [radarhere] -- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype `file` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343 +- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype ``file`` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343 [radarhere] - Load more broken images #1428 @@ -2860,7 +3001,7 @@ Changelog (Pillow) - Doc cleanup [wiredfool] -- Fix `ImageStat` docs #796 +- Fix ``ImageStat`` docs #796 [akx] - Added docs for ExifTags #794 @@ -3297,7 +3438,7 @@ Changelog (Pillow) - Add RGBA support to ImageColor #309 [yoavweiss] -- Test for `str`, not `"utf-8"` #306 (fixes #304) +- Test for ``str``, not ``"utf-8"`` #306 (fixes #304) [mjpieters] - Fix missing import os in _util.py #303 @@ -3403,7 +3544,7 @@ Changelog (Pillow) - Partial work to add a wrapper for WebPGetFeatures to correctly support #220 (fixes #204) -- Significant performance improvement of `alpha_composite` function #156 +- Significant performance improvement of ``alpha_composite`` function #156 [homm] - Support explicitly disabling features via --disable-* options #240 diff --git a/Makefile b/Makefile index 1803e617d..fdde3416b 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,8 @@ co: done coverage: - python selftest.py - python setup.py test + python3 selftest.py + python3 setup.py test rm -r htmlcov || true coverage report @@ -30,7 +30,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 +50,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 +76,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 +97,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 ddbd12f16..b8cad5e9d 100644 --- a/README.rst +++ b/README.rst @@ -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 da3297bce..9c959a4bf 100644 --- a/Tests/README.rst +++ b/Tests/README.rst @@ -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 99c7006fa..1797d34fc 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -1,8 +1,9 @@ import time +import unittest from PIL import PyAccess -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper # Not running this test by default. No DOS against Travis CI. diff --git a/Tests/bench_get.py b/Tests/bench_get.py deleted file mode 100644 index 8a54ff921..000000000 --- a/Tests/bench_get.py +++ /dev/null @@ -1,23 +0,0 @@ -import sys -import timeit - -from . import helper - -sys.path.insert(0, ".") - - -def bench(mode): - im = helper.hopper(mode) - get = im.im.getpixel - xy = 50, 50 # position shouldn't really matter - t0 = timeit.default_timer() - for _ in range(1000000): - get(xy) - print(mode, timeit.default_timer() - t0, "us") - - -bench("L") -bench("I") -bench("I;16") -bench("F") -bench("RGB") diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index db6559f1e..206a86007 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase TEST_FILE = "Tests/images/fli_overflow.fli" @@ -9,8 +11,8 @@ class TestFliOverflow(PillowTestCase): def test_fli_overflow(self): # this should not crash with a malloc error or access violation - im = Image.open(TEST_FILE) - im.load() + with Image.open(TEST_FILE) as im: + im.load() if __name__ == "__main__": diff --git a/Tests/check_icns_dos.py b/Tests/check_icns_dos.py index 03eda2e3f..3f4fb6518 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 2b9a9605b..2c1793a4f 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,18 +1,15 @@ #!/usr/bin/env python - -from __future__ import division - -import sys +import unittest from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase, is_win32 min_iterations = 100 max_iterations = 10000 -@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") +@unittest.skipIf(is_win32(), "requires Unix or macOS") class TestImagingLeaks(PillowTestCase): def _get_mem_usage(self): from resource import getpagesize, getrusage, RUSAGE_SELF diff --git a/Tests/check_j2k_dos.py b/Tests/check_j2k_dos.py index 7d0e95a60..273c18585 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 4614529ed..1635f1001 100755 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,9 +1,9 @@ -import sys +import unittest from io import BytesIO from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase, is_win32 # Limits for testing the leak mem_limit = 1024 * 1048576 @@ -13,7 +13,7 @@ codecs = dir(Image.core) test_file = "Tests/images/rgb_trns_ycbc.jp2" -@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") +@unittest.skipIf(is_win32(), "requires Unix or macOS") class TestJpegLeaks(PillowTestCase): def setUp(self): if "jpeg2k_encoder" not in codecs or "jpeg2k_decoder" not in codecs: diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index 3e6cf8d34..d5b6e455f 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase class TestJ2kEncodeOverflow(PillowTestCase): diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 2f758ba10..6b2801a21 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,7 +1,7 @@ -import sys +import unittest from io import BytesIO -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper, is_win32 iterations = 5000 @@ -15,7 +15,7 @@ valgrind --tool=massif python test-installed.py -s -v Tests/check_jpeg_leaks.py """ -@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") +@unittest.skipIf(is_win32(), "requires Unix or macOS") class TestJpegLeaks(PillowTestCase): """ diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 5df476e0a..7fcaa4cf9 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,8 +1,9 @@ import sys +import unittest from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase # This test is not run automatically. # diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 4653a6013..8e65dc1cb 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,8 +1,9 @@ import sys +import unittest from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase # This test is not run automatically. # diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index ae9a46d1b..b272c601c 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase TEST_FILE = "Tests/images/libtiff_segfault.tif" @@ -12,8 +14,8 @@ class TestLibtiffSegfault(PillowTestCase): """ with self.assertRaises(IOError): - im = Image.open(TEST_FILE) - im.load() + with Image.open(TEST_FILE) as im: + im.load() if __name__ == "__main__": diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 5c78ce122..f133b2695 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -1,9 +1,10 @@ +import unittest import zlib from io import BytesIO from PIL import Image, ImageFile, PngImagePlugin -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase TEST_FILE = "Tests/images/png_decompression_dos.png" diff --git a/Tests/conftest.py b/Tests/conftest.py new file mode 100644 index 000000000..2b796148d --- /dev/null +++ b/Tests/conftest.py @@ -0,0 +1,11 @@ +def pytest_report_header(config): + import io + + 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" % str(e) diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index 4d189dbad..c7055995e 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/helper.py b/Tests/helper.py index 9a41ce7df..0f8e05c19 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 subprocess import sys import tempfile import unittest +from io import BytesIO from PIL import Image, ImageMath -from PIL._util import py3 logger = logging.getLogger(__name__) @@ -22,12 +22,26 @@ if os.environ.get("SHOW_ERRORS", None): 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 @@ -51,37 +65,22 @@ def convert_to_comparable(a, 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 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) + os.remove(path) + except OSError: + pass # report? 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)) + len(a), + len(b), + msg or "got length {}, expected {}".format(len(a), len(b)), ) self.assertTrue( - all(x == y for x, y in zip(a, b)), msg or "got %s, expected %s" % (a, b) + all(x == y for x, y in zip(a, b)), + msg or "got {}, expected {}".format(a, b), ) except Exception: self.assertEqual(a, b, msg) @@ -89,20 +88,24 @@ class PillowTestCase(unittest.TestCase): 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) + im.mode, + mode, + msg or "got mode {!r}, expected {!r}".format(im.mode, mode), ) if size is not None: self.assertEqual( - im.size, size, msg or "got size %r, expected %r" % (im.size, size) + im.size, + size, + msg or "got size {!r}, expected {!r}".format(im.size, size), ) def assert_image_equal(self, a, b, msg=None): self.assertEqual( - a.mode, b.mode, msg or "got mode %r, expected %r" % (a.mode, b.mode) + a.mode, b.mode, msg or "got mode {!r}, expected {!r}".format(a.mode, b.mode) ) self.assertEqual( - a.size, b.size, msg or "got size %r, expected %r" % (a.size, b.size) + a.size, b.size, msg or "got size {!r}, expected {!r}".format(a.size, b.size) ) if a.tobytes() != b.tobytes(): if HAS_UPLOADER: @@ -123,10 +126,10 @@ class PillowTestCase(unittest.TestCase): 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) + a.mode, b.mode, msg or "got mode {!r}, expected {!r}".format(a.mode, b.mode) ) self.assertEqual( - a.size, b.size, msg or "got size %r, expected %r" % (a.size, b.size) + a.size, b.size, msg or "got size {!r}, expected {!r}".format(a.size, b.size) ) a, b = convert_to_comparable(a, b) @@ -200,24 +203,13 @@ class PillowTestCase(unittest.TestCase): 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. + def skipKnownBadTest(self, msg=None): + # Skip if PILLOW_RUN_KNOWN_BAD is not true in the environment. if os.environ.get("PILLOW_RUN_KNOWN_BAD", False): print(os.environ.get("PILLOW_RUN_KNOWN_BAD", False)) return - 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") + self.skipTest(msg or "Known Bad Test") def tempfile(self, template): assert template[:5] in ("temp.", "temp_") @@ -229,12 +221,12 @@ class PillowTestCase(unittest.TestCase): def open_withImagemagick(self, f): if not imagemagick_available(): - raise IOError() + raise OSError() outfile = self.tempfile("temp.png") if command_succeeds([IMCONVERT, f, outfile]): return Image.open(outfile) - raise IOError() + raise OSError() @unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") @@ -277,21 +269,12 @@ class PillowLeakTestCase(PillowTestCase): # 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() @@ -323,13 +306,10 @@ 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 + try: + subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + except OSError: + return False return True @@ -355,6 +335,27 @@ 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" + # GitHub Actions has "GITHUB_ACTIONS" + return ( + "CI" in os.environ or "TF_BUILD" in os.environ or "GITHUB_ACTIONS" in os.environ + ) + + +def is_win32(): + return sys.platform.startswith("win32") + + +def is_pypy(): + return hasattr(sys, "pypy_translation_info") + + if sys.platform == "win32": IMCONVERT = os.environ.get("MAGICK_HOME", "") if IMCONVERT: @@ -371,7 +372,7 @@ def distro(): return line.strip().split("=")[1] -class cached_property(object): +class cached_property: def __init__(self, func): self.func = func 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 000000000..9b4d8e21f 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 000000000..57177fe2b Binary files /dev/null and b/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.png differ diff --git a/Tests/images/combined_larger_than_size.psd b/Tests/images/combined_larger_than_size.psd new file mode 100644 index 000000000..2e6caef39 Binary files /dev/null and b/Tests/images/combined_larger_than_size.psd differ diff --git a/Tests/images/decompression_bomb.gif b/Tests/images/decompression_bomb.gif new file mode 100644 index 000000000..3ca21b60a Binary files /dev/null and b/Tests/images/decompression_bomb.gif differ diff --git a/Tests/images/decompression_bomb.ico b/Tests/images/decompression_bomb.ico new file mode 100644 index 000000000..0efc9eaf7 Binary files /dev/null and b/Tests/images/decompression_bomb.ico differ diff --git a/Tests/images/drawing_wmf_ref_144.png b/Tests/images/drawing_wmf_ref_144.png new file mode 100644 index 000000000..20ed9ce59 Binary files /dev/null and b/Tests/images/drawing_wmf_ref_144.png differ diff --git a/Tests/images/fli_overrun.bin b/Tests/images/fli_overrun.bin new file mode 100644 index 000000000..e1e8c5901 Binary files /dev/null and b/Tests/images/fli_overrun.bin differ diff --git a/Tests/images/g4_orientation_1.tif b/Tests/images/g4_orientation_1.tif new file mode 100755 index 000000000..8ab0f1d0d Binary files /dev/null and b/Tests/images/g4_orientation_1.tif differ diff --git a/Tests/images/g4_orientation_2.tif b/Tests/images/g4_orientation_2.tif new file mode 100755 index 000000000..4ab085641 Binary files /dev/null and b/Tests/images/g4_orientation_2.tif differ diff --git a/Tests/images/g4_orientation_3.tif b/Tests/images/g4_orientation_3.tif new file mode 100755 index 000000000..ca0d0fe29 Binary files /dev/null and b/Tests/images/g4_orientation_3.tif differ diff --git a/Tests/images/g4_orientation_4.tif b/Tests/images/g4_orientation_4.tif new file mode 100755 index 000000000..166381fb7 Binary files /dev/null and b/Tests/images/g4_orientation_4.tif differ diff --git a/Tests/images/g4_orientation_5.tif b/Tests/images/g4_orientation_5.tif new file mode 100755 index 000000000..9fecaad65 Binary files /dev/null and b/Tests/images/g4_orientation_5.tif differ diff --git a/Tests/images/g4_orientation_6.tif b/Tests/images/g4_orientation_6.tif new file mode 100755 index 000000000..6abc001eb Binary files /dev/null and b/Tests/images/g4_orientation_6.tif differ diff --git a/Tests/images/g4_orientation_7.tif b/Tests/images/g4_orientation_7.tif new file mode 100755 index 000000000..0babc9108 Binary files /dev/null and b/Tests/images/g4_orientation_7.tif differ diff --git a/Tests/images/g4_orientation_8.tif b/Tests/images/g4_orientation_8.tif new file mode 100755 index 000000000..3216a3725 Binary files /dev/null and b/Tests/images/g4_orientation_8.tif differ diff --git a/Tests/images/imagedraw_arc_width_non_whole_angle.png b/Tests/images/imagedraw_arc_width_non_whole_angle.png new file mode 100644 index 000000000..1fb9a3c86 Binary files /dev/null and b/Tests/images/imagedraw_arc_width_non_whole_angle.png differ diff --git a/Tests/images/imagedraw_floodfill_not_negative.png b/Tests/images/imagedraw_floodfill_not_negative.png new file mode 100644 index 000000000..c3f34a174 Binary files /dev/null and b/Tests/images/imagedraw_floodfill_not_negative.png differ diff --git a/Tests/images/imagedraw_stroke_different.png b/Tests/images/imagedraw_stroke_different.png new file mode 100644 index 000000000..e58cbdc4e Binary files /dev/null and b/Tests/images/imagedraw_stroke_different.png differ diff --git a/Tests/images/imagedraw_stroke_multiline.png b/Tests/images/imagedraw_stroke_multiline.png new file mode 100644 index 000000000..fc5e07c86 Binary files /dev/null and b/Tests/images/imagedraw_stroke_multiline.png differ diff --git a/Tests/images/imagedraw_stroke_same.png b/Tests/images/imagedraw_stroke_same.png new file mode 100644 index 000000000..8f2f3abe1 Binary files /dev/null and b/Tests/images/imagedraw_stroke_same.png differ diff --git a/Tests/images/imageops_pad_h_0.jpg b/Tests/images/imageops_pad_h_0.jpg index f9fcb1cdb..7afbbb96a 100644 Binary files a/Tests/images/imageops_pad_h_0.jpg and b/Tests/images/imageops_pad_h_0.jpg differ diff --git a/Tests/images/imageops_pad_h_1.jpg b/Tests/images/imageops_pad_h_1.jpg index 4b9b9ebc4..b9bf8a49a 100644 Binary files a/Tests/images/imageops_pad_h_1.jpg and b/Tests/images/imageops_pad_h_1.jpg differ diff --git a/Tests/images/imageops_pad_h_2.jpg b/Tests/images/imageops_pad_h_2.jpg index 2c8224892..7e0eb9599 100644 Binary files a/Tests/images/imageops_pad_h_2.jpg and b/Tests/images/imageops_pad_h_2.jpg differ diff --git a/Tests/images/imageops_pad_v_0.jpg b/Tests/images/imageops_pad_v_0.jpg index caf435796..73a96c86c 100644 Binary files a/Tests/images/imageops_pad_v_0.jpg and b/Tests/images/imageops_pad_v_0.jpg differ diff --git a/Tests/images/imageops_pad_v_1.jpg b/Tests/images/imageops_pad_v_1.jpg index 4a6698e91..04545f817 100644 Binary files a/Tests/images/imageops_pad_v_1.jpg and b/Tests/images/imageops_pad_v_1.jpg differ diff --git a/Tests/images/imageops_pad_v_2.jpg b/Tests/images/imageops_pad_v_2.jpg index 792952bcd..f3e399d7b 100644 Binary files a/Tests/images/imageops_pad_v_2.jpg and b/Tests/images/imageops_pad_v_2.jpg differ diff --git a/Tests/images/invalid-exif-without-x-resolution.jpg b/Tests/images/invalid-exif-without-x-resolution.jpg new file mode 100644 index 000000000..00f6bd2f3 Binary files /dev/null and b/Tests/images/invalid-exif-without-x-resolution.jpg differ diff --git a/Tests/images/no_rows_per_strip.tif b/Tests/images/no_rows_per_strip.tif new file mode 100644 index 000000000..67942aec4 Binary files /dev/null and b/Tests/images/no_rows_per_strip.tif differ diff --git a/Tests/images/pcx_overrun.bin b/Tests/images/pcx_overrun.bin new file mode 100644 index 000000000..ea46d2c11 Binary files /dev/null and b/Tests/images/pcx_overrun.bin differ diff --git a/Tests/images/photoshop-200dpi-broken.jpg b/Tests/images/photoshop-200dpi-broken.jpg new file mode 100644 index 000000000..a574872f2 Binary files /dev/null and b/Tests/images/photoshop-200dpi-broken.jpg differ diff --git a/Tests/images/radial_gradients.png b/Tests/images/radial_gradients.png new file mode 100644 index 000000000..39a02fbbf Binary files /dev/null and b/Tests/images/radial_gradients.png differ diff --git a/Tests/images/raw_negative_stride.bin b/Tests/images/raw_negative_stride.bin new file mode 100644 index 000000000..312e82a5f Binary files /dev/null and b/Tests/images/raw_negative_stride.bin differ diff --git a/Tests/images/sgi_overrun.bin b/Tests/images/sgi_overrun.bin new file mode 100644 index 000000000..9a45d065a Binary files /dev/null and b/Tests/images/sgi_overrun.bin differ diff --git a/Tests/images/string_dimension.tiff b/Tests/images/string_dimension.tiff new file mode 100644 index 000000000..d0b558301 Binary files /dev/null and b/Tests/images/string_dimension.tiff differ diff --git a/Tests/images/sugarshack_no_data.mpo b/Tests/images/sugarshack_no_data.mpo new file mode 100644 index 000000000..d94bad53b Binary files /dev/null and b/Tests/images/sugarshack_no_data.mpo differ diff --git a/Tests/images/test_direction_ttb_stroke.png b/Tests/images/test_direction_ttb_stroke.png new file mode 100644 index 000000000..3fa844e9a Binary files /dev/null and b/Tests/images/test_direction_ttb_stroke.png differ diff --git a/Tests/import_all.py b/Tests/import_all.py deleted file mode 100644 index 4dfacb291..000000000 --- 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 bacb391fa..000000000 --- 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_bmp_reference.py b/Tests/test_bmp_reference.py index e6a75e2c3..3a33d2c8a 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import os from PIL import Image @@ -24,8 +22,8 @@ class TestBmpReference(PillowTestCase): def open(f): try: - im = Image.open(f) - im.load() + with Image.open(f) as im: + im.load() except Exception: # as msg: pass @@ -48,8 +46,8 @@ class TestBmpReference(PillowTestCase): ] for f in self.get_files("q"): try: - im = Image.open(f) - im.load() + 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: @@ -89,17 +87,17 @@ class TestBmpReference(PillowTestCase): for f in self.get_files("g"): try: - im = Image.open(f) - 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) + with Image.open(f) as im: + im.load() + with Image.open(get_compare(f)) as compare: + compare.load() + if im.mode == "P": + # assert image similar doesn't really work + # with paletized image, since the palette might + # be differently ordered for an equivalent image. + im = im.convert("RGBA") + compare = im.convert("RGBA") + self.assert_image_similar(im, compare, 5) except Exception as msg: # there are three here that are unsupported: @@ -109,4 +107,4 @@ class TestBmpReference(PillowTestCase): os.path.join(base, "g", "pal4rle.bmp"), ) if f not in unsupported: - self.fail("Unsupported Image %s: %s" % (f, msg)) + self.fail("Unsupported Image {}: {}".format(f, msg)) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index ca82209c2..96461ca3a 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,10 +1,9 @@ -from __future__ import division - +import unittest from array import array from PIL import Image, ImageFilter -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase try: import numpy diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index eefb1a0ef..ac2970de2 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,12 +1,9 @@ -from __future__ import division, print_function - import sys +import unittest from PIL import Image -from .helper import PillowTestCase, unittest - -is_pypy = hasattr(sys, "pypy_version_info") +from .helper import PillowTestCase, is_pypy class TestCoreStats(PillowTestCase): @@ -87,7 +84,7 @@ class TestCoreMemory(PillowTestCase): stats = Image.core.get_stats() self.assertGreaterEqual(stats["new_count"], 1) self.assertGreaterEqual(stats["allocated_blocks"], 64) - if not is_pypy: + if not is_pypy(): self.assertGreaterEqual(stats["freed_blocks"], 64) def test_get_blocks_max(self): @@ -108,7 +105,7 @@ class TestCoreMemory(PillowTestCase): if sys.maxsize < 2 ** 32: self.assertRaises(ValueError, Image.core.set_blocks_max, 2 ** 29) - @unittest.skipIf(is_pypy, "images are not collected") + @unittest.skipIf(is_pypy(), "images are not collected") def test_set_blocks_max_stats(self): Image.core.reset_stats() Image.core.set_blocks_max(128) @@ -123,7 +120,7 @@ class TestCoreMemory(PillowTestCase): self.assertEqual(stats["freed_blocks"], 0) self.assertEqual(stats["blocks_cached"], 64) - @unittest.skipIf(is_pypy, "images are not collected") + @unittest.skipIf(is_pypy(), "images are not collected") def test_clear_cache_stats(self): Image.core.reset_stats() Image.core.clear_cache() @@ -153,7 +150,7 @@ class TestCoreMemory(PillowTestCase): self.assertGreaterEqual(stats["allocated_blocks"], 16) self.assertGreaterEqual(stats["reused_blocks"], 0) self.assertEqual(stats["blocks_cached"], 0) - if not is_pypy: + if not is_pypy(): self.assertGreaterEqual(stats["freed_blocks"], 16) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index b3a36fe78..5d0d37099 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -14,7 +14,8 @@ class TestDecompressionBomb(PillowTestCase): def test_no_warning_small_file(self): # Implicit assert: no warning. # A warning would cause a failure. - Image.open(TEST_FILE) + with Image.open(TEST_FILE): + pass def test_no_warning_no_limit(self): # Arrange @@ -25,26 +26,42 @@ class TestDecompressionBomb(PillowTestCase): # 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) - self.assert_warning(Image.DecompressionBombWarning, Image.open, TEST_FILE) + def open(): + with Image.open(TEST_FILE): + pass + + self.assert_warning(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) - self.assertRaises(Image.DecompressionBombError, lambda: Image.open(TEST_FILE)) + with self.assertRaises(Image.DecompressionBombError): + with Image.open(TEST_FILE): + pass + + def test_exception_ico(self): + with self.assertRaises(Image.DecompressionBombError): + Image.open("Tests/images/decompression_bomb.ico") + + def test_exception_gif(self): + with self.assertRaises(Image.DecompressionBombError): + Image.open("Tests/images/decompression_bomb.gif") class TestDecompressionCrop(PillowTestCase): def setUp(self): self.src = hopper() + self.addCleanup(self.src.close) Image.MAX_IMAGE_PIXELS = self.src.height * self.src.width * 4 - 1 def tearDown(self): diff --git a/Tests/test_features.py b/Tests/test_features.py index 64b0302ca..eb51407a1 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,10 +1,9 @@ -from __future__ import unicode_literals - import io +import unittest from PIL import features -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase try: from PIL import _webp @@ -70,11 +69,14 @@ class TestFeatures(PillowTestCase): 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 ")) + self.assertTrue(lines[2].startswith("Python ")) + lines = lines[3:] + while lines[0].startswith(" "): + lines = lines[1:] + self.assertEqual(lines[0], "-" * 68) + self.assertTrue(lines[1].startswith("Python modules loaded from ")) + self.assertTrue(lines[2].startswith("Binary modules loaded from ")) + self.assertEqual(lines[3], "-" * 68) jpeg = ( "\n" + "-" * 68 diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 59951a890..1e8dff184 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -5,16 +5,16 @@ from .helper import PillowTestCase 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) + with Image.open("Tests/images/blp/blp2_raw.blp") as im: + with Image.open("Tests/images/blp/blp2_raw.png") as target: + self.assert_image_equal(im, target) def test_load_blp2_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) + with Image.open("Tests/images/blp/blp2_dxt1.blp") as im: + with Image.open("Tests/images/blp/blp2_dxt1.png") as target: + self.assert_image_equal(im, target) def test_load_blp2_dxt1a(self): - im = Image.open("Tests/images/blp/blp2_dxt1a.blp") - target = Image.open("Tests/images/blp/blp2_dxt1a.png") - self.assert_image_equal(im, target) + with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: + with Image.open("Tests/images/blp/blp2_dxt1a.png") as target: + self.assert_image_equal(im, target) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 2180835ba..338f52cd7 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -11,12 +11,12 @@ class TestFileBmp(PillowTestCase): 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() + 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") def test_sanity(self): self.roundtrip(hopper()) @@ -36,89 +36,93 @@ class TestFileBmp(PillowTestCase): im.save(output, "BMP") output.seek(0) - reloaded = Image.open(output) + with Image.open(output) as reloaded: + self.assertEqual(im.mode, reloaded.mode) + self.assertEqual(im.size, reloaded.size) + self.assertEqual(reloaded.format, "BMP") - self.assertEqual(im.mode, reloaded.mode) - self.assertEqual(im.size, reloaded.size) - self.assertEqual(reloaded.format, "BMP") + def test_save_too_large(self): + outfile = self.tempfile("temp.bmp") + with Image.new("RGB", (1, 1)) as im: + im._size = (37838, 37838) + with self.assertRaises(ValueError): + im.save(outfile) def test_dpi(self): dpi = (72, 72) output = io.BytesIO() - im = hopper() - im.save(output, "BMP", dpi=dpi) + with hopper() as im: + im.save(output, "BMP", dpi=dpi) output.seek(0) - reloaded = Image.open(output) - - self.assertEqual(reloaded.info["dpi"], dpi) + with Image.open(output) as reloaded: + 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") + with Image.open("Tests/images/hopper.bmp") 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"]) - self.assertEqual(im.size, reloaded.size) - self.assertEqual(reloaded.format, "JPEG") + # Assert + with Image.open(outfile) as reloaded: + reloaded.load() + self.assertEqual(im.info["dpi"], reloaded.info["dpi"]) + self.assertEqual(im.size, reloaded.size) + self.assertEqual(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)) + with Image.open("Tests/images/hopper.bmp") as im: + self.assertEqual(im.info["dpi"], (96, 96)) # Round down - im = Image.open("Tests/images/hopper_roundDown.bmp") - self.assertEqual(im.info["dpi"], (72, 72)) + with Image.open("Tests/images/hopper_roundDown.bmp") as im: + self.assertEqual(im.info["dpi"], (72, 72)) def test_save_dpi_rounding(self): outfile = self.tempfile("temp.bmp") - im = Image.open("Tests/images/hopper.bmp") + with Image.open("Tests/images/hopper.bmp") as im: + im.save(outfile, dpi=(72.2, 72.2)) + with Image.open(outfile) as reloaded: + self.assertEqual(reloaded.info["dpi"], (72, 72)) - 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)) - reloaded = Image.open(outfile) - self.assertEqual(reloaded.info["dpi"], (73, 73)) + im.save(outfile, dpi=(72.8, 72.8)) + with Image.open(outfile) as reloaded: + self.assertEqual(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") + with Image.open("Tests/images/clipboard.dib") as im: + self.assertEqual(im.format, "DIB") + self.assertEqual(im.get_format_mimetype(), "image/bmp") - target = Image.open("Tests/images/clipboard_target.png") - self.assert_image_equal(im, target) + with Image.open("Tests/images/clipboard_target.png") as target: + self.assert_image_equal(im, target) def test_save_dib(self): outfile = self.tempfile("temp.dib") - im = Image.open("Tests/images/clipboard.dib") - im.save(outfile) + 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: + self.assertEqual(reloaded.format, "DIB") + self.assertEqual(reloaded.get_format_mimetype(), "image/bmp") + self.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") + 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)) + # 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: + self.assert_image_equal(im, target) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 37573e340..540d89719 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -8,14 +8,14 @@ TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" class TestFileBufrStub(PillowTestCase): def test_open(self): # Act - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Assert - self.assertEqual(im.format, "BUFR") + # Assert + self.assertEqual(im.format, "BUFR") - # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + # Dummy data from the stub + self.assertEqual(im.mode, "F") + self.assertEqual(im.size, (1, 1)) def test_invalid_file(self): # Arrange @@ -28,10 +28,10 @@ class TestFileBufrStub(PillowTestCase): def test_load(self): # Arrange - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) + # Act / Assert: stub cannot load without an implemented handler + self.assertRaises(IOError, im.load) def test_save(self): # Arrange diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 5f14001d9..2f931fb68 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -11,8 +11,8 @@ class TestFileContainer(PillowTestCase): dir(ContainerIO) def test_isatty(self): - im = hopper() - container = ContainerIO.ContainerIO(im, 0, 0) + with hopper() as im: + container = ContainerIO.ContainerIO(im, 0, 0) self.assertFalse(container.isatty()) diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 0b2f7a98c..246404bab 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -7,14 +7,13 @@ 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) - # 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)) + with Image.open(TEST_FILE) as im: + self.assertEqual(im.size, (32, 32)) + self.assertIsInstance(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)) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 4d3690d30..e9411dbf8 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,6 +1,8 @@ +import unittest + from PIL import DcxImagePlugin, Image -from .helper import PillowTestCase, hopper +from .helper import PillowTestCase, hopper, is_pypy # Created with ImageMagick: convert hopper.ppm hopper.dcx TEST_FILE = "Tests/images/hopper.dcx" @@ -11,19 +13,35 @@ class TestFileDcx(PillowTestCase): # Arrange # Act - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Assert - self.assertEqual(im.size, (128, 128)) - self.assertIsInstance(im, DcxImagePlugin.DcxImageFile) - orig = hopper() - self.assert_image_equal(im, orig) + # Assert + self.assertEqual(im.size, (128, 128)) + self.assertIsInstance(im, DcxImagePlugin.DcxImageFile) + orig = hopper() + self.assert_image_equal(im, orig) + @unittest.skipIf(is_pypy(), "Requires CPython") def test_unclosed_file(self): def open(): im = Image.open(TEST_FILE) im.load() + self.assert_warning(ResourceWarning, open) + + def test_closed_file(self): + def open(): + im = Image.open(TEST_FILE) + im.load() + im.close() + + self.assert_warning(None, open) + + def test_context_manager(self): + def open(): + with Image.open(TEST_FILE) as im: + im.load() + self.assert_warning(None, open) def test_invalid_file(self): @@ -32,34 +50,34 @@ class TestFileDcx(PillowTestCase): def test_tell(self): # Arrange - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Act - frame = im.tell() + # Act + frame = im.tell() - # Assert - self.assertEqual(frame, 0) + # Assert + self.assertEqual(frame, 0) def test_n_frames(self): - im = Image.open(TEST_FILE) - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) + with Image.open(TEST_FILE) as im: + self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) def test_eoferror(self): - im = Image.open(TEST_FILE) - n_frames = im.n_frames + with Image.open(TEST_FILE) as im: + n_frames = im.n_frames - # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + # Test seeking past the last frame + self.assertRaises(EOFError, im.seek, n_frames) + self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) + # 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) - frame = 999 # too big on purpose + with Image.open(TEST_FILE) as im: + frame = 999 # too big on purpose # Act / Assert self.assertRaises(EOFError, im.seek, frame) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 498c64f21..053d72568 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -8,6 +8,7 @@ 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" @@ -16,58 +17,71 @@ class TestFileDds(PillowTestCase): def test_sanity_dxt1(self): """Check DXT1 images can be opened""" - target = Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) + 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() - im = Image.open(TEST_FILE_DXT1) - im.load() + self.assertEqual(im.format, "DDS") + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (256, 256)) - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (256, 256)) - - self.assert_image_equal(target.convert("RGBA"), im) + self.assert_image_equal(im, target) def test_sanity_dxt5(self): """Check DXT5 images can be opened""" - target = Image.open(TEST_FILE_DXT5.replace(".dds", ".png")) - - im = Image.open(TEST_FILE_DXT5) - im.load() + 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)) - self.assert_image_equal(target, im) + with Image.open(TEST_FILE_DXT5.replace(".dds", ".png")) as target: + self.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")) + with Image.open(TEST_FILE_DXT3.replace(".dds", ".png")) as target: + with Image.open(TEST_FILE_DXT3) as im: + im.load() - im = Image.open(TEST_FILE_DXT3) - im.load() + self.assertEqual(im.format, "DDS") + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (256, 256)) - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (256, 256)) - - self.assert_image_equal(target, im) + self.assert_image_equal(target, im) def test_dx10_bc7(self): """Check DX10 images can be opened""" - target = Image.open(TEST_FILE_DX10_BC7.replace(".dds", ".png")) + with Image.open(TEST_FILE_DX10_BC7) as im: + im.load() - im = Image.open(TEST_FILE_DX10_BC7) - im.load() + self.assertEqual(im.format, "DDS") + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (256, 256)) - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (256, 256)) + with Image.open(TEST_FILE_DX10_BC7.replace(".dds", ".png")) as target: + self.assert_image_equal(target, im) - self.assert_image_equal(target, im) + def test_dx10_bc7_unorm_srgb(self): + """Check DX10 unsigned normalized integer images can be opened""" + + 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, (16, 16)) + self.assertEqual(im.info["gamma"], 1 / 2.2) + + with Image.open( + TEST_FILE_DX10_BC7_UNORM_SRGB.replace(".dds", ".png") + ) as target: + self.assert_image_equal(target, im) def test_unimplemented_dxgi_format(self): self.assertRaises( @@ -79,16 +93,17 @@ class TestFileDds(PillowTestCase): def test_uncompressed_rgb(self): """Check uncompressed RGB images can be opened""" - target = Image.open(TEST_FILE_UNCOMPRESSED_RGB.replace(".dds", ".png")) + with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: + im.load() - im = Image.open(TEST_FILE_UNCOMPRESSED_RGB) - im.load() + self.assertEqual(im.format, "DDS") + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (800, 600)) - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (800, 600)) - - self.assert_image_equal(target, im) + with Image.open( + TEST_FILE_UNCOMPRESSED_RGB.replace(".dds", ".png") + ) as target: + self.assert_image_equal(target, im) def test__validate_true(self): """Check valid prefix""" @@ -129,8 +144,8 @@ class TestFileDds(PillowTestCase): img_file = f.read() def short_file(): - im = Image.open(BytesIO(img_file[:-100])) - im.load() + with Image.open(BytesIO(img_file[:-100])) as im: + im.load() self.assertRaises(IOError, short_file) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 3459310df..6fd201d48 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,8 +1,9 @@ import io +import unittest from PIL import EpsImagePlugin, Image -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() @@ -25,30 +26,30 @@ class TestFileEps(PillowTestCase): @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") def test_sanity(self): # Regular scale - image1 = Image.open(file1) - image1.load() - self.assertEqual(image1.mode, "RGB") - self.assertEqual(image1.size, (460, 352)) - self.assertEqual(image1.format, "EPS") + with Image.open(file1) as image1: + image1.load() + self.assertEqual(image1.mode, "RGB") + self.assertEqual(image1.size, (460, 352)) + self.assertEqual(image1.format, "EPS") - image2 = Image.open(file2) - image2.load() - self.assertEqual(image2.mode, "RGB") - self.assertEqual(image2.size, (360, 252)) - self.assertEqual(image2.format, "EPS") + with Image.open(file2) as image2: + image2.load() + self.assertEqual(image2.mode, "RGB") + self.assertEqual(image2.size, (360, 252)) + self.assertEqual(image2.format, "EPS") # Double scale - image1_scale2 = Image.open(file1) - image1_scale2.load(scale=2) - self.assertEqual(image1_scale2.mode, "RGB") - self.assertEqual(image1_scale2.size, (920, 704)) - self.assertEqual(image1_scale2.format, "EPS") + 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") - image2_scale2 = Image.open(file2) - image2_scale2.load(scale=2) - self.assertEqual(image2_scale2.mode, "RGB") - self.assertEqual(image2_scale2.size, (720, 504)) - self.assertEqual(image2_scale2.format, "EPS") + with Image.open(file2) as image2_scale2: + image2_scale2.load(scale=2) + self.assertEqual(image2_scale2.mode, "RGB") + self.assertEqual(image2_scale2.size, (720, 504)) + self.assertEqual(image2_scale2.format, "EPS") def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -57,55 +58,55 @@ class TestFileEps(PillowTestCase): @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") def test_cmyk(self): - cmyk_image = Image.open("Tests/images/pil_sample_cmyk.eps") + with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: - self.assertEqual(cmyk_image.mode, "CMYK") - self.assertEqual(cmyk_image.size, (100, 100)) - self.assertEqual(cmyk_image.format, "EPS") + self.assertEqual(cmyk_image.mode, "CMYK") + self.assertEqual(cmyk_image.size, (100, 100)) + self.assertEqual(cmyk_image.format, "EPS") - cmyk_image.load() - self.assertEqual(cmyk_image.mode, "RGB") + 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) + if "jpeg_decoder" in dir(Image.core): + with Image.open("Tests/images/pil_sample_rgb.jpg") as target: + 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) + with Image.open("Tests/images/reqd_showpage.eps") as plot_image: + with Image.open("Tests/images/reqd_showpage.png") as target: + # should not crash/hang + plot_image.load() + # fonts could be slightly different + self.assert_image_similar(plot_image, target, 6) @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: - image1.save(fh, "EPS") + with Image.open(file1) as image1: + with open(self.tempfile("temp_file.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: - image1.save(fh, "EPS") + with Image.open(file1) as image1: + with open(self.tempfile("temp_iobase.eps"), "wb") as fh: + image1.save(fh, "EPS") @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") def test_bytesio_object(self): with open(file1, "rb") as f: img_bytes = io.BytesIO(f.read()) - img = Image.open(img_bytes) - img.load() + with Image.open(img_bytes) as img: + img.load() - image1_scale1_compare = Image.open(file1_compare).convert("RGB") - image1_scale1_compare.load() - self.assert_image_similar(img, image1_scale1_compare, 5) + 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") @@ -120,18 +121,20 @@ class TestFileEps(PillowTestCase): self.skipTest("zip/deflate support not available") # Zero bounding box - image1_scale1 = Image.open(file1) - image1_scale1.load() - image1_scale1_compare = Image.open(file1_compare).convert("RGB") - image1_scale1_compare.load() - self.assert_image_similar(image1_scale1, image1_scale1_compare, 5) + with Image.open(file1) as image1_scale1: + image1_scale1.load() + with Image.open(file1_compare) as image1_scale1_compare: + image1_scale1_compare = image1_scale1_compare.convert("RGB") + image1_scale1_compare.load() + self.assert_image_similar(image1_scale1, image1_scale1_compare, 5) # Non-Zero bounding box - image2_scale1 = Image.open(file2) - image2_scale1.load() - image2_scale1_compare = Image.open(file2_compare).convert("RGB") - image2_scale1_compare.load() - self.assert_image_similar(image2_scale1, image2_scale1_compare, 10) + with Image.open(file2) as image2_scale1: + image2_scale1.load() + with Image.open(file2_compare) as image2_scale1_compare: + image2_scale1_compare = image2_scale1_compare.convert("RGB") + image2_scale1_compare.load() + self.assert_image_similar(image2_scale1, image2_scale1_compare, 10) @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") def test_render_scale2(self): @@ -141,57 +144,46 @@ class TestFileEps(PillowTestCase): self.skipTest("zip/deflate support not available") # Zero bounding box - image1_scale2 = Image.open(file1) - image1_scale2.load(scale=2) - image1_scale2_compare = Image.open(file1_compare_scale2).convert("RGB") - image1_scale2_compare.load() - self.assert_image_similar(image1_scale2, image1_scale2_compare, 5) + with Image.open(file1) as image1_scale2: + image1_scale2.load(scale=2) + with Image.open(file1_compare_scale2) as image1_scale2_compare: + image1_scale2_compare = image1_scale2_compare.convert("RGB") + image1_scale2_compare.load() + self.assert_image_similar(image1_scale2, image1_scale2_compare, 5) # Non-Zero bounding box - image2_scale2 = Image.open(file2) - image2_scale2.load(scale=2) - image2_scale2_compare = Image.open(file2_compare_scale2).convert("RGB") - image2_scale2_compare.load() - self.assert_image_similar(image2_scale2, image2_scale2_compare, 10) + with Image.open(file2) as image2_scale2: + image2_scale2.load(scale=2) + with Image.open(file2_compare_scale2) as image2_scale2_compare: + image2_scale2_compare = image2_scale2_compare.convert("RGB") + image2_scale2_compare.load() + 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) + files = [file1, file2, "Tests/images/illu10_preview.eps"] + for fn in files: + with Image.open(fn) as im: + new_size = (100, 100) + im = im.resize(new_size) + self.assertEqual(im.size, new_size) @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)) + files = [file1, file2] + for fn in files: + with Image.open(file1) as im: + new_size = (100, 100) + im.thumbnail(new_size) + self.assertEqual(max(im.size), max(new_size)) def test_read_binary_preview(self): # Issue 302 # open image with binary preview - Image.open(file3) + with Image.open(file3): + pass def _test_readline(self, t, ending): ending = "Failure with line ending: %s" % ( @@ -239,16 +231,16 @@ class TestFileEps(PillowTestCase): # Act / Assert for filename in FILES: - img = Image.open(filename) - self.assertEqual(img.mode, "RGB") + with Image.open(filename) as img: + 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) - image.load() + 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") diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py index 0221bab99..933e0fd12 100644 --- a/Tests/test_file_fitsstub.py +++ b/Tests/test_file_fitsstub.py @@ -8,14 +8,14 @@ TEST_FILE = "Tests/images/hopper.fits" class TestFileFitsStub(PillowTestCase): def test_open(self): # Act - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Assert - self.assertEqual(im.format, "FITS") + # Assert + self.assertEqual(im.format, "FITS") - # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + # Dummy data from the stub + self.assertEqual(im.mode, "F") + self.assertEqual(im.size, (1, 1)) def test_invalid_file(self): # Arrange @@ -28,19 +28,19 @@ class TestFileFitsStub(PillowTestCase): def test_load(self): # Arrange - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) + # Act / Assert: stub cannot load without an implemented handler + self.assertRaises(IOError, im.load) def test_save(self): # Arrange - im = Image.open(TEST_FILE) - dummy_fp = None - dummy_filename = "dummy.filename" + 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 - ) + # 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 + ) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index ad3e84a5b..895942d70 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,6 +1,8 @@ +import unittest + from PIL import FliImagePlugin, Image -from .helper import PillowTestCase +from .helper import PillowTestCase, is_pypy # created as an export of a palette image from Gimp2.6 # save as...-> hopper.fli, default options. @@ -12,36 +14,52 @@ animated_test_file = "Tests/images/a.fli" class TestFileFli(PillowTestCase): def test_sanity(self): - 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) + with Image.open(static_test_file) as im: + im.load() + self.assertEqual(im.mode, "P") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "FLI") + self.assertFalse(im.is_animated) - 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) + with Image.open(animated_test_file) as im: + self.assertEqual(im.mode, "P") + self.assertEqual(im.size, (320, 200)) + self.assertEqual(im.format, "FLI") + self.assertEqual(im.info["duration"], 71) + self.assertTrue(im.is_animated) + @unittest.skipIf(is_pypy(), "Requires CPython") def test_unclosed_file(self): def open(): im = Image.open(static_test_file) im.load() + self.assert_warning(ResourceWarning, open) + + def test_closed_file(self): + def open(): + im = Image.open(static_test_file) + im.load() + im.close() + + self.assert_warning(None, open) + + def test_context_manager(self): + def open(): + with Image.open(static_test_file) as im: + im.load() + self.assert_warning(None, open) def test_tell(self): # Arrange - im = Image.open(static_test_file) + with Image.open(static_test_file) as im: - # Act - frame = im.tell() + # Act + frame = im.tell() - # Assert - self.assertEqual(frame, 0) + # Assert + self.assertEqual(frame, 0) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -49,50 +67,50 @@ class TestFileFli(PillowTestCase): self.assertRaises(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) + with Image.open(static_test_file) as im: + 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) + with Image.open(animated_test_file) as im: + self.assertEqual(im.n_frames, 384) + self.assertTrue(im.is_animated) def test_eoferror(self): - im = Image.open(animated_test_file) - n_frames = im.n_frames + 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) + # Test seeking past the last frame + self.assertRaises(EOFError, im.seek, n_frames) + self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) + # 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) + with Image.open(animated_test_file) as im: - layer_number = im.tell() - self.assertEqual(layer_number, 0) + layer_number = im.tell() + self.assertEqual(layer_number, 0) - im.seek(0) - layer_number = im.tell() - self.assertEqual(layer_number, 0) + im.seek(0) + layer_number = im.tell() + self.assertEqual(layer_number, 0) - im.seek(1) - layer_number = im.tell() - self.assertEqual(layer_number, 1) + im.seek(1) + layer_number = im.tell() + self.assertEqual(layer_number, 1) - im.seek(2) - layer_number = im.tell() - self.assertEqual(layer_number, 2) + im.seek(2) + layer_number = im.tell() + self.assertEqual(layer_number, 2) - im.seek(1) - layer_number = im.tell() - self.assertEqual(layer_number, 1) + im.seek(1) + layer_number = im.tell() + self.assertEqual(layer_number, 1) def test_seek(self): - im = Image.open(animated_test_file) - im.seek(50) + 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: + self.assert_image_equal(im, expected) diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index 68412c8ca..7c985be30 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -1,4 +1,6 @@ -from .helper import PillowTestCase, unittest +import unittest + +from .helper import PillowTestCase try: from PIL import FpxImagePlugin diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index 7d30042ca..335a96e83 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -5,12 +5,11 @@ from .helper import PillowTestCase class TestFileFtex(PillowTestCase): def test_load_raw(self): - im = Image.open("Tests/images/ftex_uncompressed.ftu") - target = Image.open("Tests/images/ftex_uncompressed.png") - - self.assert_image_equal(im, target) + with Image.open("Tests/images/ftex_uncompressed.ftu") as im: + with Image.open("Tests/images/ftex_uncompressed.png") as 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) + with Image.open("Tests/images/ftex_dxt1.ftc") as im: + with Image.open("Tests/images/ftex_dxt1.png") as target: + self.assert_image_similar(im, target.convert("RGBA"), 15) diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index 659179a4e..4c26579a8 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -10,8 +10,6 @@ class TestFileGbr(PillowTestCase): self.assertRaises(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) + with Image.open("Tests/images/gbr.gbr") as im: + with Image.open("Tests/images/gbr.png") as target: + self.assert_image_equal(target, im) diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index 6467d1e92..0b7543a31 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -1,4 +1,4 @@ -from PIL import GdImageFile +from PIL import GdImageFile, UnidentifiedImageError from .helper import PillowTestCase @@ -7,9 +7,9 @@ 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") + with GdImageFile.open(TEST_GD_FILE) as im: + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "GD") def test_bad_mode(self): self.assertRaises(ValueError, GdImageFile.open, TEST_GD_FILE, "bad mode") @@ -17,4 +17,4 @@ class TestFileGd(PillowTestCase): def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(IOError, GdImageFile.open, invalid_file) + self.assertRaises(UnidentifiedImageError, GdImageFile.open, invalid_file) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 4ff9727e1..bbd589ada 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,8 +1,9 @@ +import unittest from io import BytesIO from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette -from .helper import PillowTestCase, hopper, netpbm_available, unittest +from .helper import PillowTestCase, hopper, is_pypy, netpbm_available try: from PIL import _webp @@ -26,18 +27,34 @@ class TestFileGif(PillowTestCase): self.skipTest("gif support not available") # can this happen? def test_sanity(self): - 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") + with Image.open(TEST_GIF) as im: + im.load() + self.assertEqual(im.mode, "P") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "GIF") + self.assertEqual(im.info["version"], b"GIF89a") + @unittest.skipIf(is_pypy(), "Requires CPython") def test_unclosed_file(self): def open(): im = Image.open(TEST_GIF) im.load() + self.assert_warning(ResourceWarning, open) + + def test_closed_file(self): + def open(): + im = Image.open(TEST_GIF) + im.load() + im.close() + + self.assert_warning(None, open) + + def test_context_manager(self): + def open(): + with Image.open(TEST_GIF) as im: + im.load() + self.assert_warning(None, open) def test_invalid_file(self): @@ -71,21 +88,20 @@ class TestFileGif(PillowTestCase): 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), + "P", (colors, colors), bytes(range(256 - colors, 256)) * colors ) im = im.resize((size, size)) outfile = BytesIO() im.save(outfile, "GIF") outfile.seek(0) - reloaded = Image.open(outfile) + 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) - # check palette length - palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v) - self.assertEqual(expected_palette_length, palette_length) - - self.assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) + self.assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) # These do optimize the palette check(128, 511, 128) @@ -103,7 +119,7 @@ class TestFileGif(PillowTestCase): check(256, 511, 256) def test_optimize_full_l(self): - im = Image.frombytes("L", (16, 16), bytes(bytearray(range(256)))) + im = Image.frombytes("L", (16, 16), bytes(range(256))) test_file = BytesIO() im.save(test_file, "GIF", optimize=True) self.assertEqual(im.mode, "L") @@ -112,67 +128,67 @@ class TestFileGif(PillowTestCase): out = self.tempfile("temp.gif") im = hopper() im.save(out) - reread = Image.open(out) + with Image.open(out) as reread: - self.assert_image_similar(reread.convert("RGB"), im, 50) + 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) - im2 = im.copy() - im2.save(out) - reread = Image.open(out) + with Image.open(TEST_GIF) as im: + im2 = im.copy() + im2.save(out) + with Image.open(out) as reread: - self.assert_image_similar(reread.convert("RGB"), hopper(), 50) + self.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) + with Image.open(out) as reread: - self.assert_image_similar(reread.convert("RGB"), im, 50) + self.assert_image_similar(reread.convert("RGB"), im, 50) # Multiframe image - im = Image.open("Tests/images/dispose_bgnd.gif") + with Image.open("Tests/images/dispose_bgnd.gif") as im: - out = self.tempfile("temp.gif") - im.save(out, save_all=True) - reread = Image.open(out) + out = self.tempfile("temp.gif") + im.save(out, save_all=True) + with Image.open(out) as reread: - self.assertEqual(reread.n_frames, 5) + self.assertEqual(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") + with Image.open("Tests/images/dispose_bgnd.gif") as im: - info = im.info.copy() + info = im.info.copy() - out = self.tempfile("temp.gif") - im.save(out, save_all=True) - reread = Image.open(out) + out = self.tempfile("temp.gif") + im.save(out, save_all=True) + with Image.open(out) as reread: - for header in important_headers: - self.assertEqual(info[header], reread.info[header]) + for header in important_headers: + self.assertEqual(info[header], reread.info[header]) def test_palette_handling(self): # see https://github.com/python-pillow/Pillow/issues/513 - im = Image.open(TEST_GIF) - im = im.convert("RGB") + with Image.open(TEST_GIF) as im: + im = im.convert("RGB") - im = im.resize((100, 100), Image.LANCZOS) - im2 = im.convert("P", palette=Image.ADAPTIVE, colors=256) + im = im.resize((100, 100), Image.LANCZOS) + im2 = im.convert("P", palette=Image.ADAPTIVE, colors=256) - f = self.tempfile("temp.gif") - im2.save(f, optimize=True) + f = self.tempfile("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) + self.assert_image_similar(im, reloaded.convert("RGB"), 10) def test_palette_434(self): # see https://github.com/python-pillow/Pillow/issues/434 @@ -185,108 +201,115 @@ class TestFileGif(PillowTestCase): return reloaded orig = "Tests/images/test.colors.gif" - im = Image.open(orig) + 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: + self.assert_image_similar(im, reloaded, 1) + with roundtrip(im, optimize=True) as reloaded: + self.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) + im = im.convert("RGB") + # check automatic P conversion + with roundtrip(im) as reloaded: + reloaded = reloaded.convert("RGB") + self.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") + with Image.open(TEST_GIF) as img: + img = img.convert("RGB") - tempfile = self.tempfile("temp.gif") - GifImagePlugin._save_netpbm(img, 0, tempfile) - self.assert_image_similar(img, Image.open(tempfile).convert("RGB"), 0) + tempfile = self.tempfile("temp.gif") + GifImagePlugin._save_netpbm(img, 0, tempfile) + with Image.open(tempfile) as reloaded: + self.assert_image_similar(img, reloaded.convert("RGB"), 0) @unittest.skipUnless(netpbm_available(), "netpbm not available") def test_save_netpbm_l_mode(self): - img = Image.open(TEST_GIF).convert("L") + with Image.open(TEST_GIF) as img: + img = img.convert("L") - tempfile = self.tempfile("temp.gif") - GifImagePlugin._save_netpbm(img, 0, tempfile) - self.assert_image_similar(img, Image.open(tempfile).convert("L"), 0) + tempfile = self.tempfile("temp.gif") + GifImagePlugin._save_netpbm(img, 0, tempfile) + with Image.open(tempfile) as reloaded: + self.assert_image_similar(img, reloaded.convert("L"), 0) def test_seek(self): - img = Image.open("Tests/images/dispose_none.gif") - framecount = 0 - try: - while True: - framecount += 1 - img.seek(img.tell() + 1) - except EOFError: - self.assertEqual(framecount, 5) + with Image.open("Tests/images/dispose_none.gif") as img: + framecount = 0 + try: + while True: + framecount += 1 + img.seek(img.tell() + 1) + except EOFError: + self.assertEqual(framecount, 5) def test_seek_info(self): - im = Image.open("Tests/images/iss634.gif") - info = im.info.copy() + with Image.open("Tests/images/iss634.gif") as im: + info = im.info.copy() - im.seek(1) - im.seek(0) + im.seek(1) + im.seek(0) - self.assertEqual(im.info, info) + self.assertEqual(im.info, info) def test_seek_rewind(self): - im = Image.open("Tests/images/iss634.gif") - im.seek(2) - im.seek(1) + 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) + self.assert_image_equal(im, expected) def test_n_frames(self): for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]: # Test is_animated before n_frames - im = Image.open(path) - self.assertEqual(im.is_animated, n_frames != 1) + with Image.open(path) as im: + 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) + with Image.open(path) as im: + self.assertEqual(im.n_frames, n_frames) + self.assertEqual(im.is_animated, n_frames != 1) def test_eoferror(self): - im = Image.open(TEST_GIF) - n_frames = im.n_frames + with Image.open(TEST_GIF) as im: + n_frames = im.n_frames - # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + # Test seeking past the last frame + self.assertRaises(EOFError, im.seek, n_frames) + self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) + # 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") - try: - while True: - img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, 1) - except EOFError: - pass + with Image.open("Tests/images/dispose_none.gif") as img: + try: + while True: + img.seek(img.tell() + 1) + self.assertEqual(img.disposal_method, 1) + except EOFError: + pass def test_dispose_background(self): - img = Image.open("Tests/images/dispose_bgnd.gif") - try: - while True: - img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, 2) - except EOFError: - pass + with Image.open("Tests/images/dispose_bgnd.gif") as img: + try: + while True: + img.seek(img.tell() + 1) + self.assertEqual(img.disposal_method, 2) + except EOFError: + pass def test_dispose_previous(self): - img = Image.open("Tests/images/dispose_prev.gif") - try: - while True: - img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, 3) - except EOFError: - pass + with Image.open("Tests/images/dispose_prev.gif") as img: + try: + while True: + img.seek(img.tell() + 1) + self.assertEqual(img.disposal_method, 3) + except EOFError: + pass def test_save_dispose(self): out = self.tempfile("temp.gif") @@ -299,10 +322,10 @@ class TestFileGif(PillowTestCase): im_list[0].save( out, save_all=True, append_images=im_list[1:], disposal=method ) - img = Image.open(out) - for _ in range(2): - img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, method) + with Image.open(out) as img: + for _ in range(2): + img.seek(img.tell() + 1) + self.assertEqual(img.disposal_method, method) # check per frame disposal im_list[0].save( @@ -312,11 +335,11 @@ class TestFileGif(PillowTestCase): 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) + for i in range(2): + img.seek(img.tell() + 1) + self.assertEqual(img.disposal_method, i + 1) def test_dispose2_palette(self): out = self.tempfile("temp.gif") @@ -336,17 +359,16 @@ class TestFileGif(PillowTestCase): im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) - img = Image.open(out) + with Image.open(out) as img: + for i, circle in enumerate(circles): + img.seek(i) + rgb_img = img.convert("RGB") - 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)) - # Check top left pixel matches background - self.assertEqual(rgb_img.getpixel((0, 0)), (255, 0, 0)) - - # Center remains red every frame - self.assertEqual(rgb_img.getpixel((50, 50)), circle) + # Center remains red every frame + self.assertEqual(rgb_img.getpixel((50, 50)), circle) def test_dispose2_diff(self): out = self.tempfile("temp.gif") @@ -375,20 +397,19 @@ class TestFileGif(PillowTestCase): out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0 ) - img = Image.open(out) + with Image.open(out) as img: + for i, colours in enumerate(circles): + img.seek(i) + rgb_img = img.convert("RGBA") - for i, colours in enumerate(circles): - img.seek(i) - rgb_img = img.convert("RGBA") + # Check left circle is correct colour + self.assertEqual(rgb_img.getpixel((20, 50)), colours[0]) - # Check left circle is correct colour - self.assertEqual(rgb_img.getpixel((20, 50)), colours[0]) + # Check right circle is correct colour + self.assertEqual(rgb_img.getpixel((80, 50)), colours[1]) - # Check right circle is correct colour - self.assertEqual(rgb_img.getpixel((80, 50)), colours[1]) - - # Check BG is correct colour - self.assertEqual(rgb_img.getpixel((1, 1)), (255, 255, 255, 0)) + # Check BG is correct colour + self.assertEqual(rgb_img.getpixel((1, 1)), (255, 255, 255, 0)) def test_dispose2_background(self): out = self.tempfile("temp.gif") @@ -411,17 +432,17 @@ class TestFileGif(PillowTestCase): out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1 ) - im = Image.open(out) - im.seek(1) - self.assertEqual(im.getpixel((0, 0)), 0) + with Image.open(out) as im: + im.seek(1) + self.assertEqual(im.getpixel((0, 0)), 0) def test_iss634(self): - img = Image.open("Tests/images/iss634.gif") - # 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) + 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) def test_duration(self): duration = 1000 @@ -433,8 +454,8 @@ class TestFileGif(PillowTestCase): im.info["duration"] = 100 im.save(out, duration=duration) - reread = Image.open(out) - self.assertEqual(reread.info["duration"], duration) + with Image.open(out) as reread: + self.assertEqual(reread.info["duration"], duration) def test_multiple_duration(self): duration_list = [1000, 2000, 3000] @@ -450,27 +471,27 @@ class TestFileGif(PillowTestCase): im_list[0].save( out, save_all=True, append_images=im_list[1:], duration=duration_list ) - reread = Image.open(out) + with Image.open(out) as reread: - for duration in duration_list: - self.assertEqual(reread.info["duration"], duration) - try: - reread.seek(reread.tell() + 1) - except EOFError: - pass + for duration in duration_list: + self.assertEqual(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) + with Image.open(out) as reread: - for duration in duration_list: - self.assertEqual(reread.info["duration"], duration) - try: - reread.seek(reread.tell() + 1) - except EOFError: - pass + for duration in duration_list: + self.assertEqual(reread.info["duration"], duration) + try: + reread.seek(reread.tell() + 1) + except EOFError: + pass def test_identical_frames(self): duration_list = [1000, 1500, 2000, 4000] @@ -487,13 +508,13 @@ class TestFileGif(PillowTestCase): im_list[0].save( out, save_all=True, append_images=im_list[1:], duration=duration_list ) - reread = Image.open(out) + with Image.open(out) as reread: - # Assert that the first three frames were combined - self.assertEqual(reread.n_frames, 2) + # 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) + # 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): @@ -507,13 +528,12 @@ class TestFileGif(PillowTestCase): 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 that all frames were combined - self.assertEqual(reread.n_frames, 1) - - # Assert that the new duration is the total of the identical frames - self.assertEqual(reread.info["duration"], 8500) + # Assert that the new duration is the total of the identical frames + self.assertEqual(reread.info["duration"], 8500) def test_number_of_loops(self): number_of_loops = 2 @@ -521,35 +541,37 @@ class TestFileGif(PillowTestCase): out = self.tempfile("temp.gif") im = Image.new("L", (100, 100), "#000") im.save(out, loop=number_of_loops) - reread = Image.open(out) + with Image.open(out) as reread: - self.assertEqual(reread.info["loop"], number_of_loops) + self.assertEqual(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) + with Image.open(out) as reread: - self.assertEqual(reread.info["background"], im.info["background"]) + self.assertEqual(reread.info["background"], im.info["background"]) if HAVE_WEBP and _webp.HAVE_WEBPANIM: - im = Image.open("Tests/images/hopper.webp") - self.assertIsInstance(im.info["background"], tuple) - im.save(out) + with Image.open("Tests/images/hopper.webp") as im: + self.assertIsInstance(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") + with Image.open(TEST_GIF) as im: + 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) + out = self.tempfile("temp.gif") + im = Image.new("L", (100, 100), "#000") + im.info["comment"] = b"Test comment text" + im.save(out) + with Image.open(out) as reread: - self.assertEqual(reread.info["comment"], im.info["comment"]) + self.assertEqual(reread.info["comment"], im.info["comment"]) def test_comment_over_255(self): out = self.tempfile("temp.gif") @@ -559,22 +581,22 @@ class TestFileGif(PillowTestCase): comment += comment im.info["comment"] = comment im.save(out) - reread = Image.open(out) + with Image.open(out) as reread: - self.assertEqual(reread.info["comment"], comment) + 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) + with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: + with Image.open(TEST_GIF) as expected: + self.assert_image_equal(im, expected) def test_version(self): out = self.tempfile("temp.gif") def assertVersionAfterSave(im, version): im.save(out) - reread = Image.open(out) - self.assertEqual(reread.info["version"], version) + with Image.open(out) as reread: + self.assertEqual(reread.info["version"], version) # Test that GIF87a is used by default im = Image.new("L", (100, 100), "#000") @@ -590,12 +612,12 @@ class TestFileGif(PillowTestCase): assertVersionAfterSave(im, b"GIF89a") # Test that a GIF87a image is also saved in that format - im = Image.open("Tests/images/test.colors.gif") - assertVersionAfterSave(im, b"GIF87a") + 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") + # 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") @@ -605,26 +627,25 @@ class TestFileGif(PillowTestCase): ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] im.copy().save(out, save_all=True, append_images=ims) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 3) + with Image.open(out) as reread: + self.assertEqual(reread.n_frames, 3) # Tests appending using a generator def imGenerator(ims): - for im in ims: - yield im + yield from ims im.save(out, save_all=True, append_images=imGenerator(ims)) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 3) + with Image.open(out) as reread: + self.assertEqual(reread.n_frames, 3) # 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("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]) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 10) + with Image.open(out) as reread: + self.assertEqual(reread.n_frames, 10) def test_transparent_optimize(self): # from issue #2195, if the transparent color is incorrectly @@ -633,7 +654,7 @@ class TestFileGif(PillowTestCase): # that's > 128 items where the transparent color is actually # the top palette entry to trigger the bug. - data = bytes(bytearray(range(1, 254))) + data = bytes(range(1, 254)) palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) im = Image.new("L", (253, 1)) @@ -642,9 +663,9 @@ class TestFileGif(PillowTestCase): out = self.tempfile("temp.gif") im.save(out, transparency=253) - reloaded = Image.open(out) + with Image.open(out) as reloaded: - self.assertEqual(reloaded.info["transparency"], 253) + self.assertEqual(reloaded.info["transparency"], 253) def test_rgb_transparency(self): out = self.tempfile("temp.gif") @@ -654,8 +675,8 @@ class TestFileGif(PillowTestCase): im.info["transparency"] = (255, 0, 0) self.assert_warning(UserWarning, im.save, out) - reloaded = Image.open(out) - self.assertNotIn("transparency", reloaded.info) + with Image.open(out) as reloaded: + self.assertNotIn("transparency", reloaded.info) # Multiple frames im = Image.new("RGB", (1, 1)) @@ -663,8 +684,8 @@ class TestFileGif(PillowTestCase): ims = [Image.new("RGB", (1, 1))] self.assert_warning(UserWarning, im.save, out, save_all=True, append_images=ims) - reloaded = Image.open(out) - self.assertNotIn("transparency", reloaded.info) + with Image.open(out) as reloaded: + self.assertNotIn("transparency", reloaded.info) def test_bbox(self): out = self.tempfile("temp.gif") @@ -673,22 +694,22 @@ class TestFileGif(PillowTestCase): ims = [Image.new("RGB", (100, 100), "#000")] im.save(out, save_all=True, append_images=ims) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 2) + with Image.open(out) as reread: + self.assertEqual(reread.n_frames, 2) 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())) + palette = bytes(im.getpalette()) out = self.tempfile("temp.gif") im_l.save(out, palette=palette) - reloaded = Image.open(out) + with Image.open(out) as reloaded: - self.assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) + 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 @@ -696,14 +717,14 @@ class TestFileGif(PillowTestCase): # Forcing a non-straight grayscale palette. im = hopper("P") - palette = bytes(bytearray([255 - i // 3 for i in range(768)])) + palette = bytes([255 - i // 3 for i in range(768)]) out = self.tempfile("temp.gif") im.save(out, palette=palette) - reloaded = Image.open(out) - im.putpalette(palette) - self.assert_image_equal(reloaded, im) + with Image.open(out) as reloaded: + im.putpalette(palette) + self.assert_image_equal(reloaded, im) def test_palette_save_ImagePalette(self): # pass in a different palette, as an ImagePalette.ImagePalette @@ -715,9 +736,9 @@ class TestFileGif(PillowTestCase): out = self.tempfile("temp.gif") im.save(out, palette=palette) - reloaded = Image.open(out) - im.putpalette(palette) - self.assert_image_equal(reloaded, im) + with Image.open(out) as reloaded: + im.putpalette(palette) + self.assert_image_equal(reloaded, im) def test_save_I(self): # Test saving something that would trigger the auto-convert to 'L' @@ -727,17 +748,17 @@ class TestFileGif(PillowTestCase): out = self.tempfile("temp.gif") im.save(out) - reloaded = Image.open(out) - self.assert_image_equal(reloaded.convert("L"), im.convert("L")) + with Image.open(out) as reloaded: + self.assert_image_equal(reloaded.convert("L"), im.convert("L")) def test_getdata(self): # test getheader/getdata against legacy values # Create a 'P' image with holes in the palette - im = Image._wedge().resize((16, 16)) + im = Image._wedge().resize((16, 16), Image.NEAREST) im.putpalette(ImagePalette.ImagePalette("RGB")) im.info = {"background": 0} - passed_palette = bytes(bytearray([255 - i // 3 for i in range(768)])) + passed_palette = bytes([255 - i // 3 for i in range(768)]) GifImagePlugin._FORCE_OPTIMIZE = True try: @@ -759,14 +780,13 @@ class TestFileGif(PillowTestCase): def test_lzw_bits(self): # see https://github.com/python-pillow/Pillow/issues/2811 - im = Image.open("Tests/images/issue_2811.gif") - - self.assertEqual(im.tile[0][3][0], 11) # LZW bits - # codec error prepatch - im.load() + with Image.open("Tests/images/issue_2811.gif") as im: + self.assertEqual(im.tile[0][3][0], 11) # LZW bits + # codec error prepatch + im.load() def test_extents(self): - im = Image.open("Tests/images/test_extents.gif") - self.assertEqual(im.size, (100, 100)) - im.seek(1) - self.assertEqual(im.size, (150, 150)) + with Image.open("Tests/images/test_extents.gif") as im: + self.assertEqual(im.size, (100, 100)) + im.seek(1) + self.assertEqual(im.size, (150, 150)) diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index d322e1c70..92d112ced 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -8,14 +8,14 @@ TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" class TestFileGribStub(PillowTestCase): def test_open(self): # Act - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Assert - self.assertEqual(im.format, "GRIB") + # Assert + self.assertEqual(im.format, "GRIB") - # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + # Dummy data from the stub + self.assertEqual(im.mode, "F") + self.assertEqual(im.size, (1, 1)) def test_invalid_file(self): # Arrange @@ -28,10 +28,10 @@ class TestFileGribStub(PillowTestCase): def test_load(self): # Arrange - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) + # Act / Assert: stub cannot load without an implemented handler + self.assertRaises(IOError, im.load) def test_save(self): # Arrange diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index c300bae20..cdaad0cf7 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -8,14 +8,14 @@ TEST_FILE = "Tests/images/hdf5.h5" class TestFileHdf5Stub(PillowTestCase): def test_open(self): # Act - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Assert - self.assertEqual(im.format, "HDF5") + # Assert + self.assertEqual(im.format, "HDF5") - # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + # Dummy data from the stub + self.assertEqual(im.mode, "F") + self.assertEqual(im.size, (1, 1)) def test_invalid_file(self): # Arrange @@ -28,19 +28,19 @@ class TestFileHdf5Stub(PillowTestCase): def test_load(self): # Arrange - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) + # Act / Assert: stub cannot load without an implemented handler + self.assertRaises(IOError, im.load) def test_save(self): # Arrange - im = Image.open(TEST_FILE) - dummy_fp = None - dummy_filename = "dummy.filename" + 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 - ) + # 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 + ) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 2e33e0ae5..6b6543d8d 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,9 +1,10 @@ import io import sys +import unittest from PIL import IcnsImagePlugin, Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase # sample icon file TEST_FILE = "Tests/images/pillow.icns" @@ -15,72 +16,71 @@ 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) + with Image.open(TEST_FILE) as im: - # Assert that there is no unclosed file warning - self.assert_warning(None, im.load) + # Assert that there is no unclosed file warning + self.assert_warning(None, im.load) - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (1024, 1024)) - self.assertEqual(im.format, "ICNS") + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (1024, 1024)) + self.assertEqual(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") - im.save(temp_file) - reread = Image.open(temp_file) + with Image.open(TEST_FILE) as im: + im.save(temp_file) - self.assertEqual(reread.mode, "RGBA") - self.assertEqual(reread.size, (1024, 1024)) - self.assertEqual(reread.format, "ICNS") + with Image.open(temp_file) as reread: + 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) - temp_file = self.tempfile("temp.icns") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) - im.save(temp_file, append_images=[provided_im]) - reread = Image.open(temp_file) - self.assert_image_similar(reread, im, 1) + with Image.open(TEST_FILE) as im: + im.save(temp_file, append_images=[provided_im]) - reread = Image.open(temp_file) - reread.size = (16, 16, 2) - reread.load() - self.assert_image_equal(reread, provided_im) + with Image.open(temp_file) as reread: + self.assert_image_similar(reread, im, 1) + + with Image.open(temp_file) as reread: + 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) - 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)) + with Image.open(TEST_FILE) as im: + for w, h, r in im.info["sizes"]: + wr = w * r + hr = h * r + im.size = (w, h, r) + im.load() + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (wr, hr)) - # Check that we cannot load an incorrect size - with self.assertRaises(ValueError): - im.size = (1, 1) + # Check that we cannot load an incorrect size + with self.assertRaises(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") - 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 im: + for w, h, r in im.info["sizes"]: + wr = w * r + hr = h * r + with Image.open("Tests/images/pillow2.icns") as im2: + im2.size = (w, h, r) + im2.load() + self.assertEqual(im2.mode, "RGBA") + self.assertEqual(im2.size, (wr, hr)) def test_jp2_icon(self): # This icon was made by using Uli Kusterer's oldiconutil to replace @@ -93,15 +93,15 @@ class TestFileIcns(PillowTestCase): if not enable_jpeg2k: return - im = Image.open("Tests/images/pillow3.icns") - 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)) + with Image.open("Tests/images/pillow3.icns") as im: + for w, h, r in im.info["sizes"]: + wr = w * r + hr = h * r + with Image.open("Tests/images/pillow3.icns") as im2: + im2.size = (w, h, r) + im2.load() + self.assertEqual(im2.mode, "RGBA") + self.assertEqual(im2.size, (wr, hr)) def test_getimage(self): with open(TEST_FILE, "rb") as fp: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 8a01e417f..d8bb9630f 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -9,8 +9,8 @@ TEST_ICO_FILE = "Tests/images/hopper.ico" class TestFileIco(PillowTestCase): def test_sanity(self): - im = Image.open(TEST_ICO_FILE) - im.load() + 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") @@ -27,41 +27,41 @@ class TestFileIco(PillowTestCase): # the default image output.seek(0) - reloaded = Image.open(output) - self.assertEqual(reloaded.info["sizes"], {(32, 32), (64, 64)}) + with Image.open(output) as reloaded: + 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)) + 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) - reloaded.size = (32, 32) + 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)) + 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)) def test_incorrect_size(self): - im = Image.open(TEST_ICO_FILE) - with self.assertRaises(ValueError): - im.size = (1, 1) + with Image.open(TEST_ICO_FILE) as im: + with self.assertRaises(ValueError): + im.size = (1, 1) def test_save_256x256(self): """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" # Arrange - im = Image.open("Tests/images/hopper_256x256.ico") - outfile = self.tempfile("temp_saved_hopper_256x256.ico") + with Image.open("Tests/images/hopper_256x256.ico") as im: + outfile = self.tempfile("temp_saved_hopper_256x256.ico") - # Act - im.save(outfile) - im_saved = Image.open(outfile) + # Act + im.save(outfile) + with Image.open(outfile) as im_saved: - # Assert - self.assertEqual(im_saved.size, (256, 256)) + # Assert + self.assertEqual(im_saved.size, (256, 256)) def test_only_save_relevant_sizes(self): """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266 @@ -69,35 +69,35 @@ class TestFileIco(PillowTestCase): 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") + with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 + outfile = self.tempfile("temp_saved_python.ico") + # Act + im.save(outfile) - # Act - im.save(outfile) - im_saved = Image.open(outfile) - - # Assert - self.assertEqual( - im_saved.info["sizes"], {(16, 16), (24, 24), (32, 32), (48, 48)} - ) + 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 open(): + with Image.open("Tests/images/hopper_unexpected.ico") as im: + self.assertEqual(im.size, (16, 16)) + + self.assert_warning(UserWarning, open) def test_draw_reloaded(self): - im = Image.open(TEST_ICO_FILE) - outfile = self.tempfile("temp_saved_hopper_draw.ico") + with Image.open(TEST_ICO_FILE) as im: + outfile = self.tempfile("temp_saved_hopper_draw.ico") - draw = ImageDraw.Draw(im) - draw.line((0, 0) + im.size, "#f00") - im.save(outfile) + draw = ImageDraw.Draw(im) + draw.line((0, 0) + im.size, "#f00") + im.save(outfile) - im = Image.open(outfile) - im.save("Tests/images/hopper_draw.ico") - reloaded = Image.open("Tests/images/hopper_draw.ico") - self.assert_image_equal(im, reloaded) + with Image.open(outfile) as im: + im.save("Tests/images/hopper_draw.ico") + with Image.open("Tests/images/hopper_draw.ico") as reloaded: + self.assert_image_equal(im, reloaded) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 90e26efd5..1a5638523 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image, ImImagePlugin -from .helper import PillowTestCase, hopper +from .helper import PillowTestCase, hopper, is_pypy # sample im TEST_IM = "Tests/images/hopper.im" @@ -8,53 +10,69 @@ TEST_IM = "Tests/images/hopper.im" class TestFileIm(PillowTestCase): def test_sanity(self): - im = Image.open(TEST_IM) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "IM") + with Image.open(TEST_IM) as im: + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "IM") + @unittest.skipIf(is_pypy(), "Requires CPython") def test_unclosed_file(self): def open(): im = Image.open(TEST_IM) im.load() + self.assert_warning(ResourceWarning, open) + + def test_closed_file(self): + def open(): + im = Image.open(TEST_IM) + im.load() + im.close() + + self.assert_warning(None, open) + + def test_context_manager(self): + def open(): + with Image.open(TEST_IM) as im: + im.load() + self.assert_warning(None, open) def test_tell(self): # Arrange - im = Image.open(TEST_IM) + with Image.open(TEST_IM) as im: - # Act - frame = im.tell() + # Act + frame = im.tell() # Assert self.assertEqual(frame, 0) def test_n_frames(self): - im = Image.open(TEST_IM) - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) + with Image.open(TEST_IM) as im: + self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) def test_eoferror(self): - im = Image.open(TEST_IM) - n_frames = im.n_frames + 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) + # Test seeking past the last frame + self.assertRaises(EOFError, im.seek, n_frames) + self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) + # 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) + with Image.open(out) as reread: - self.assert_image_equal(reread, im) + self.assert_image_equal(reread, im) def test_save_unsupported_mode(self): out = self.tempfile("temp.im") diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 800563af1..b6cc4c225 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,3 +1,6 @@ +import sys +from io import StringIO + from PIL import Image, IptcImagePlugin from .helper import PillowTestCase, hopper @@ -8,20 +11,20 @@ TEST_FILE = "Tests/images/iptc.jpg" class TestFileIptc(PillowTestCase): def test_getiptcinfo_jpg_none(self): # Arrange - im = hopper() + with hopper() as im: - # Act - iptc = IptcImagePlugin.getiptcinfo(im) + # Act + iptc = IptcImagePlugin.getiptcinfo(im) # Assert self.assertIsNone(iptc) def test_getiptcinfo_jpg_found(self): # Arrange - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Act - iptc = IptcImagePlugin.getiptcinfo(im) + # Act + iptc = IptcImagePlugin.getiptcinfo(im) # Assert self.assertIsInstance(iptc, dict) @@ -30,10 +33,10 @@ class TestFileIptc(PillowTestCase): def test_getiptcinfo_tiff_none(self): # Arrange - im = Image.open("Tests/images/hopper.tif") + with Image.open("Tests/images/hopper.tif") as im: - # Act - iptc = IptcImagePlugin.getiptcinfo(im) + # Act + iptc = IptcImagePlugin.getiptcinfo(im) # Assert self.assertIsNone(iptc) @@ -52,12 +55,6 @@ class TestFileIptc(PillowTestCase): # Arrange c = b"abc" # Temporarily redirect stdout - try: - from cStringIO import StringIO - except ImportError: - from io import StringIO - import sys - old_stdout = sys.stdout sys.stdout = mystdout = StringIO() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index e0cb7528e..af95139ac 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,10 +1,16 @@ import os -import sys from io import BytesIO from PIL import Image, ImageFile, JpegImagePlugin -from .helper import PillowTestCase, cjpeg_available, djpeg_available, hopper, unittest +from .helper import ( + PillowTestCase, + cjpeg_available, + djpeg_available, + hopper, + is_win32, + unittest, +) codecs = dir(Image.core) @@ -38,52 +44,56 @@ class TestFileJpeg(PillowTestCase): # internal version number self.assertRegex(Image.core.jpeglib_version, r"\d+\.\d+$") - 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() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "JPEG") + self.assertEqual(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: + 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) 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))] + 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) 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)) @@ -93,20 +103,20 @@ class TestFileJpeg(PillowTestCase): def test_icc(self): # 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"] + self.assertEqual(len(icc_profile), 3144) + # Roundtrip via physical file. + f = self.tempfile("temp.jpg") + im1.save(f, icc_profile=icc_profile) + with Image.open(f) as im2: + 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")) def test_icc_big(self): # Make sure that the "extra" support handles large blocks @@ -134,18 +144,18 @@ class TestFileJpeg(PillowTestCase): # 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 = 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, + ) def test_optimize(self): im1 = self.roundtrip(hopper()) @@ -199,24 +209,24 @@ class TestFileJpeg(PillowTestCase): 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) @@ -250,17 +260,17 @@ class TestFileJpeg(PillowTestCase): 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]) 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()) @@ -323,168 +333,172 @@ class TestFileJpeg(PillowTestCase): self.assertRaises(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() + self.assertEqual(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: + self.assertIsNone(im._getmp()) def test_quality_keep(self): # 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 = self.tempfile("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 = self.tempfile("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 = self.tempfile("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 + self.assertIsNotNone(im.getbbox()) def test_truncated_jpeg_throws_IOError(self): filename = "Tests/images/truncated_jpeg.jpg" - im = Image.open(filename) + with Image.open(filename) as im: + with self.assertRaises(IOError): + im.load() - 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) + with Image.open(test_file) as im: + f = self.tempfile("temp.jpg") + im.save(f, qtables=[[n] * 64] * n) + with Image.open(f) as im: + 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) + with Image.open("Tests/images/hopper.jpg") as im: + 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]) + # valid bounds for baseline qtable + bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)] + self.roundtrip(im, qtables=[bounds_qtable]) - # values from wizard.txt in jpeg9-a src package. - standard_l_qtable = [ - int(s) - for s in """ - 16 11 10 16 24 40 51 61 - 12 12 14 19 26 58 60 55 - 14 13 16 24 40 57 69 56 - 14 17 22 29 51 87 80 62 - 18 22 37 56 68 109 103 77 - 24 35 55 64 81 104 113 92 - 49 64 78 87 103 121 120 101 - 72 92 95 98 112 100 103 99 - """.split( - None + # values from wizard.txt in jpeg9-a src package. + standard_l_qtable = [ + int(s) + for s in """ + 16 11 10 16 24 40 51 61 + 12 12 14 19 26 58 60 55 + 14 13 16 24 40 57 69 56 + 14 17 22 29 51 87 80 62 + 18 22 37 56 68 109 103 77 + 24 35 55 64 81 104 113 92 + 49 64 78 87 103 121 120 101 + 72 92 95 98 112 100 103 99 + """.split( + None + ) + ] + + standard_chrominance_qtable = [ + int(s) + for s in """ + 17 18 24 47 99 99 99 99 + 18 21 26 66 99 99 99 99 + 24 26 56 99 99 99 99 99 + 47 66 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + """.split( + None + ) + ] + # list of qtable lists + self.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 + self.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, + ) - # 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") - 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]) - # 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]]) + # 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) + with Image.open(TEST_FILE) as img: + 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) - - 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) + with Image.open(TEST_FILE) as img: + 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) def test_no_duplicate_0x1001_tag(self): # Arrange @@ -501,12 +515,11 @@ class TestFileJpeg(PillowTestCase): f = self.tempfile("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 """ @@ -515,10 +528,10 @@ class TestFileJpeg(PillowTestCase): # Act # Shouldn't raise error fn = "Tests/images/sugarshack_bad_mpo_header.jpg" - im = self.assert_warning(UserWarning, Image.open, fn) + with self.assert_warning(UserWarning, Image.open, fn) as im: - # Assert - self.assertEqual(im.format, "JPEG") + # Assert + self.assertEqual(im.format, "JPEG") def test_save_correct_modes(self): out = BytesIO() @@ -536,121 +549,135 @@ class TestFileJpeg(PillowTestCase): def test_save_tiff_with_dpi(self): # Arrange outfile = self.tempfile("temp.tif") - im = Image.open("Tests/images/hopper.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() + self.assertEqual(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: + self.assertEqual(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: + self.assertEqual(im.info["dpi"], (2, 2)) def test_save_dpi_rounding(self): outfile = self.tempfile("temp.jpg") - im = Image.open("Tests/images/hopper.jpg") + with Image.open("Tests/images/hopper.jpg") as im: + im.save(outfile, dpi=(72.2, 72.2)) - im.save(outfile, dpi=(72.2, 72.2)) - reloaded = Image.open(outfile) - self.assertEqual(reloaded.info["dpi"], (72, 72)) + with Image.open(outfile) as reloaded: + self.assertEqual(reloaded.info["dpi"], (72, 72)) - im.save(outfile, dpi=(72.8, 72.8)) - reloaded = Image.open(outfile) - self.assertEqual(reloaded.info["dpi"], (73, 73)) + im.save(outfile, dpi=(72.8, 72.8)) + + with Image.open(outfile) as reloaded: + self.assertEqual(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 + self.assertEqual(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 + self.assertEqual(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 + self.assertEqual(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 + self.assertEqual(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 + self.assertEqual(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. + # This should return the default, and not a SyntaxError or + # OSError for unidentified image. + self.assertEqual(im.info.get("dpi"), (72, 72)) + + def test_invalid_exif_x_resolution(self): + # When no x or y resolution is defined in EXIF + im = Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") + + # This should return the default, and not a ValueError or + # OSError for an unidentified image. self.assertEqual(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 + self.assertEqual(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], - { - "XResolution": 200.0, - "DisplayedUnitsX": 1, - "YResolution": 200.0, - "DisplayedUnitsY": 1, - }, - ) + with Image.open("Tests/images/photoshop-200dpi.jpg") as im: + self.assertEqual( + im.info["photoshop"][0x03ED], + { + "XResolution": 200.0, + "DisplayedUnitsX": 1, + "YResolution": 200.0, + "DisplayedUnitsY": 1, + }, + ) + + # 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: + self.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: + self.assertNotIn("photoshop", im.info) -@unittest.skipUnless(sys.platform.startswith("win32"), "Windows only") +@unittest.skipUnless(is_win32(), "Windows only") class TestFileCloseW32(PillowTestCase): def setUp(self): if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 7643cfebf..37ce726db 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -33,18 +33,18 @@ class TestFileJpeg2k(PillowTestCase): # Internal version number self.assertRegex(Image.core.jp2klib_version, r"\d+\.\d+\.\d+$") - im = Image.open("Tests/images/test-card-lossless.jp2") - 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") + 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") + with Image.open("Tests/images/balloon.jpf") as im: + self.assertEqual(im.format, "JPEG2000") + self.assertEqual(im.get_format_mimetype(), "image/jpx") def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -54,24 +54,24 @@ class TestFileJpeg2k(PillowTestCase): def test_bytesio(self): with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) - im = Image.open(data) - im.load() - self.assert_image_similar(im, test_card, 1.0e-3) + with Image.open(data) as im: + im.load() + self.assert_image_similar(im, test_card, 1.0e-3) # These two test pre-written JPEG 2000 files that were not written with # PIL (they were made using Adobe Photoshop) def test_lossless(self): - im = Image.open("Tests/images/test-card-lossless.jp2") - im.load() - outfile = self.tempfile("temp_test-card.png") - im.save(outfile) + with Image.open("Tests/images/test-card-lossless.jp2") as im: + im.load() + outfile = self.tempfile("temp_test-card.png") + im.save(outfile) self.assert_image_similar(im, test_card, 1.0e-3) def test_lossy_tiled(self): - im = Image.open("Tests/images/test-card-lossy-tiled.jp2") - im.load() - self.assert_image_similar(im, test_card, 2.0) + 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) @@ -91,6 +91,12 @@ class TestFileJpeg2k(PillowTestCase): ) 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) @@ -104,10 +110,10 @@ class TestFileJpeg2k(PillowTestCase): self.assert_image_equal(im, test_card) def test_reduce(self): - im = Image.open("Tests/images/test-card-lossless.jp2") - im.reduce = 2 - im.load() - self.assertEqual(im.size, (160, 120)) + with Image.open("Tests/images/test-card-lossless.jp2") as im: + im.reduce = 2 + im.load() + self.assertEqual(im.size, (160, 120)) def test_layers_type(self): outfile = self.tempfile("temp_layers.jp2") @@ -126,64 +132,58 @@ class TestFileJpeg2k(PillowTestCase): ) out.seek(0) - im = Image.open(out) - im.layers = 1 - im.load() - self.assert_image_similar(im, test_card, 13) + with Image.open(out) as im: + im.layers = 1 + im.load() + self.assert_image_similar(im, test_card, 13) out.seek(0) - im = Image.open(out) - im.layers = 3 - im.load() - self.assert_image_similar(im, test_card, 0.4) + with Image.open(out) as im: + im.layers = 3 + im.load() + self.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") + with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: + with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: - # Act - j2k.load() - jp2.load() + # Act + j2k.load() + jp2.load() - # Assert - self.assertEqual(j2k.mode, "RGBA") - self.assertEqual(jp2.mode, "RGBA") + # Assert + self.assertEqual(j2k.mode, "RGBA") + self.assertEqual(jp2.mode, "RGBA") def test_16bit_monochrome_has_correct_mode(self): + with Image.open("Tests/images/16bit.cropped.j2k") as j2k: + j2k.load() + self.assertEqual(j2k.mode, "I;16") - j2k = Image.open("Tests/images/16bit.cropped.j2k") - jp2 = Image.open("Tests/images/16bit.cropped.jp2") - - j2k.load() - jp2.load() - - self.assertEqual(j2k.mode, "I;16") - self.assertEqual(jp2.mode, "I;16") + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + jp2.load() + self.assertEqual(jp2.mode, "I;16") def test_16bit_monochrome_jp2_like_tiff(self): - - 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) + with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + self.assert_image_similar(jp2, tiff_16bit, 1e-3) def test_16bit_monochrome_j2k_like_tiff(self): - - 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) + with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: + with Image.open("Tests/images/16bit.cropped.j2k") as j2k: + self.assert_image_similar(j2k, tiff_16bit, 1e-3) def test_16bit_j2k_roundtrips(self): - - j2k = Image.open("Tests/images/16bit.cropped.j2k") - im = self.roundtrip(j2k) - self.assert_image_equal(im, j2k) + with Image.open("Tests/images/16bit.cropped.j2k") as j2k: + im = self.roundtrip(j2k) + self.assert_image_equal(im, j2k) def test_16bit_jp2_roundtrips(self): - - jp2 = Image.open("Tests/images/16bit.cropped.jp2") - im = self.roundtrip(jp2) - self.assert_image_equal(im, jp2) + with Image.open("Tests/images/16bit.cropped.jp2") as 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 diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 6339d878a..4414dbe5e 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 @@ -9,7 +7,6 @@ from collections import namedtuple from ctypes import c_float from PIL import Image, TiffImagePlugin, TiffTags, features -from PIL._util import py3 from .helper import PillowTestCase, hopper @@ -49,25 +46,23 @@ class TestFileLibTiff(LibTiffTestCase): """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: + self.assertEqual(im.size, (500, 500)) + self._assert_noerr(im) def test_g4_large(self): 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(im) def test_g4_tiff_file(self): """Testing the string load path""" test_file = "Tests/images/hopper_g4_500.tif" with open(test_file, "rb") as f: - im = Image.open(f) - - self.assertEqual(im.size, (500, 500)) - self._assert_noerr(im) + with Image.open(f) as im: + self.assertEqual(im.size, (500, 500)) + self._assert_noerr(im) def test_g4_tiff_bytesio(self): """Testing the stringio loading code path""" @@ -76,10 +71,9 @@ class TestFileLibTiff(LibTiffTestCase): 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: + self.assertEqual(im.size, (500, 500)) + self._assert_noerr(im) def test_g4_non_disk_file_object(self): """Testing loading from non-disk non-BytesIO file object""" @@ -89,69 +83,63 @@ class TestFileLibTiff(LibTiffTestCase): s.write(f.read()) s.seek(0) r = io.BufferedReader(s) - im = Image.open(r) - - self.assertEqual(im.size, (500, 500)) - self._assert_noerr(im) + with Image.open(r) as im: + self.assertEqual(im.size, (500, 500)) + self._assert_noerr(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: + self.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: + self.assert_image_equal(g4, png) def test_g4_write(self): """Checking to see that the saved image is the same as what we wrote""" test_file = "Tests/images/hopper_g4_500.tif" - orig = Image.open(test_file) + with Image.open(test_file) as orig: + out = self.tempfile("temp.tif") + rot = orig.transpose(Image.ROTATE_90) + self.assertEqual(rot.size, (500, 500)) + rot.save(out) - out = self.tempfile("temp.tif") - rot = orig.transpose(Image.ROTATE_90) - self.assertEqual(rot.size, (500, 500)) - rot.save(out) + with Image.open(out) as reread: + self.assertEqual(reread.size, (500, 500)) + self._assert_noerr(reread) + self.assert_image_equal(reread, rot) + self.assertEqual(reread.info["compression"], "group4") - reread = Image.open(out) - self.assertEqual(reread.size, (500, 500)) - self._assert_noerr(reread) - self.assert_image_equal(reread, rot) - self.assertEqual(reread.info["compression"], "group4") + self.assertEqual(reread.info["compression"], orig.info["compression"]) - self.assertEqual(reread.info["compression"], orig.info["compression"]) - - self.assertNotEqual(orig.tobytes(), reread.tobytes()) + self.assertNotEqual(orig.tobytes(), reread.tobytes()) def test_adobe_deflate_tiff(self): test_file = "Tests/images/tiff_adobe_deflate.tif" - im = Image.open(test_file) + with Image.open(test_file) as im: + 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() - 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() - - self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") def test_write_metadata(self): """ Test metadata writing through libtiff """ for legacy_api in [False, True]: - img = Image.open("Tests/images/hopper_g4.tif") f = self.tempfile("temp.tiff") + with Image.open("Tests/images/hopper_g4.tif") as img: + img.save(f, tiffinfo=img.tag) - 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,11 +150,11 @@ class TestFileLibTiff(LibTiffTestCase): "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: @@ -207,44 +195,44 @@ class TestFileLibTiff(LibTiffTestCase): # Exclude ones that have special meaning # that we're already testing them - im = Image.open("Tests/images/hopper_g4.tif") - for tag in im.tag_v2: - try: - del core_items[tag] - except KeyError: - pass + with Image.open("Tests/images/hopper_g4.tif") as im: + for tag in im.tag_v2: + try: + del core_items[tag] + except KeyError: + pass - # Type codes: - # 2: "ascii", - # 3: "short", - # 4: "long", - # 5: "rational", - # 12: "double", - # Type: dummy value - values = { - 2: "test", - 3: 1, - 4: 2 ** 20, - 5: TiffImagePlugin.IFDRational(100, 1), - 12: 1.05, - } + # Type codes: + # 2: "ascii", + # 3: "short", + # 4: "long", + # 5: "rational", + # 12: "double", + # Type: dummy value + values = { + 2: "test", + 3: 1, + 4: 2 ** 20, + 5: TiffImagePlugin.IFDRational(100, 1), + 12: 1.05, + } - new_ifd = TiffImagePlugin.ImageFileDirectory_v2() - for tag, info in core_items.items(): - if info.length == 1: - new_ifd[tag] = values[info.type] - if info.length == 0: - new_ifd[tag] = tuple(values[info.type] for _ in range(3)) - else: - new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) + new_ifd = TiffImagePlugin.ImageFileDirectory_v2() + for tag, info in core_items.items(): + if info.length == 1: + new_ifd[tag] = values[info.type] + if info.length == 0: + new_ifd[tag] = tuple(values[info.type] for _ in range(3)) + else: + new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) - # Extra samples really doesn't make sense in this application. - del new_ifd[338] + # Extra samples really doesn't make sense in this application. + del new_ifd[338] - out = self.tempfile("temp.tif") - TiffImagePlugin.WRITE_LIBTIFF = True + out = self.tempfile("temp.tif") + TiffImagePlugin.WRITE_LIBTIFF = True - im.save(out, tiffinfo=new_ifd) + im.save(out, tiffinfo=new_ifd) TiffImagePlugin.WRITE_LIBTIFF = False @@ -263,7 +251,6 @@ class TestFileLibTiff(LibTiffTestCase): 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 +271,8 @@ class TestFileLibTiff(LibTiffTestCase): ) } - libtiff_version = TiffImagePlugin._libtiff_version() - libtiffs = [False] - if distutils.version.StrictVersion( - libtiff_version - ) >= distutils.version.StrictVersion("4.0"): + if Image.core.libtiff_support_custom_tags: libtiffs.append(True) for libtiff in libtiffs: @@ -301,21 +284,21 @@ class TestFileLibTiff(LibTiffTestCase): out = self.tempfile("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 + self.assertAlmostEqual(float(reloaded_value), float(value)) + continue - if libtiff and isinstance(value, bytes): - value = value.decode() + if libtiff and isinstance(value, bytes): + value = value.decode() - self.assertEqual(reloaded_value, value) + self.assertEqual(reloaded_value, value) # Test with types ifd = TiffImagePlugin.ImageFileDirectory_v2() @@ -342,93 +325,81 @@ class TestFileLibTiff(LibTiffTestCase): 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)) + with Image.open(out) as reloaded: + 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") + with Image.open("Tests/images/hopper_g4_500.tif") as i: + 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) + with Image.open(out) as reread: + 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") + with Image.open("Tests/images/16bit.deflate.tif") as im: + 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: + b = im.tobytes() + # Bytes are in image native order (little endian) self.assertEqual(b[0], ord(b"\xe0")) self.assertEqual(b[1], ord(b"\x01")) - 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) + out = self.tempfile("temp.tif") + # out = "temp.le.tif" + im.save(out) + with Image.open(out) as reread: + self.assertEqual(reread.info["compression"], im.info["compression"]) + self.assertEqual(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") + with Image.open("Tests/images/16bit.MM.deflate.tif") as im: + self.assertEqual(im.getpixel((0, 0)), 480) + self.assertEqual(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: + # Bytes are in image native order (big endian) 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) - - self.assertEqual(reread.info["compression"], im.info["compression"]) - self.assertEqual(reread.getpixel((0, 0)), 480) + out = self.tempfile("temp.tif") + im.save(out) + with Image.open(out) as reread: + self.assertEqual(reread.info["compression"], im.info["compression"]) + self.assertEqual(reread.getpixel((0, 0)), 480) def test_g4_string_info(self): """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 = self.tempfile("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: + self.assertEqual("temp.tif", reread.tag_v2[269]) + self.assertEqual("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") + self.assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") def test_blur(self): # test case from irc, how to do blur on b/w image @@ -436,16 +407,16 @@ class TestFileLibTiff(LibTiffTestCase): from PIL import ImageFilter out = self.tempfile("temp.tif") - im = Image.open("Tests/images/pport_g4.tif") - im = im.convert("L") + 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) + self.assert_image_equal(im, im2) def test_compressions(self): # Test various tiff compressions and assert similar image content but reduced @@ -458,18 +429,18 @@ class TestFileLibTiff(LibTiffTestCase): 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: + self.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: + self.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: + self.assert_image_similar(im2, im3, 30) self.assertGreater(size_raw, size_compressed) self.assertGreater(size_compressed, size_jpeg) @@ -491,8 +462,8 @@ class TestFileLibTiff(LibTiffTestCase): out = self.tempfile("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: + self.assert_image_equal(im, im2) def xtest_bw_compression_w_rgb(self): """ This test passes, but when running all tests causes a failure due @@ -520,45 +491,45 @@ class TestFileLibTiff(LibTiffTestCase): 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) + self.assertEqual(im.size, (10, 10)) + self.assertEqual(im.convert("RGB").getpixel((0, 0)), (0, 128, 0)) + self.assertTrue(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) + self.assertEqual(im.size, (10, 10)) + self.assertEqual(im.convert("RGB").getpixel((0, 0)), (255, 0, 0)) + self.assertTrue(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) + self.assertFalse(im.tag.next) + self.assertEqual(im.size, (20, 20)) + self.assertEqual(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 + self.assertEqual(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: + self.assertFalse(im.tag.next) + im.load() + self.assertFalse(im.tag.next) def test_4bit(self): # Arrange @@ -567,13 +538,13 @@ class TestFileLibTiff(LibTiffTestCase): # Act TiffImagePlugin.READ_LIBTIFF = True - im = Image.open(test_file) - TiffImagePlugin.READ_LIBTIFF = False + with Image.open(test_file) as im: + TiffImagePlugin.READ_LIBTIFF = False - # Assert - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.mode, "L") - self.assert_image_similar(im, original, 7.3) + # Assert + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.mode, "L") + self.assert_image_similar(im, original, 7.3) def test_gray_semibyte_per_pixel(self): test_files = ( @@ -598,15 +569,15 @@ class TestFileLibTiff(LibTiffTestCase): ) original = hopper("L") for epsilon, group in test_files: - im = Image.open(group[0]) - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.mode, "L") - self.assert_image_similar(im, original, epsilon) + with Image.open(group[0]) as im: + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.mode, "L") + self.assert_image_similar(im, original, epsilon) for file in group[1:]: - im2 = Image.open(file) - self.assertEqual(im2.size, (128, 128)) - self.assertEqual(im2.mode, "L") - self.assert_image_equal(im, im2) + with Image.open(file) as im2: + self.assertEqual(im2.size, (128, 128)) + self.assertEqual(im2.mode, "L") + self.assert_image_equal(im, im2) def test_save_bytesio(self): # PR 1011 @@ -624,8 +595,8 @@ class TestFileLibTiff(LibTiffTestCase): pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) - pilim_load = Image.open(buffer_io) - self.assert_image_similar(pilim, pilim_load, 0) + with Image.open(buffer_io) as pilim_load: + self.assert_image_similar(pilim, pilim_load, 0) save_bytesio() save_bytesio("raw") @@ -637,12 +608,12 @@ class TestFileLibTiff(LibTiffTestCase): def test_crashing_metadata(self): # issue 1597 - im = Image.open("Tests/images/rdf.tif") - out = self.tempfile("temp.tif") + with Image.open("Tests/images/rdf.tif") as im: + out = self.tempfile("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): @@ -655,9 +626,9 @@ class TestFileLibTiff(LibTiffTestCase): # /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): # https://github.com/python-pillow/Pillow/issues/1651 @@ -685,21 +656,21 @@ class TestFileLibTiff(LibTiffTestCase): self.assertEqual(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) + self.assertEqual(im._compression, "tiff_ccitt") + self.assertEqual(im.size, (10, 10)) - im.seek(1) - self.assertEqual(im._compression, "packbits") - self.assertEqual(im.size, (10, 10)) - im.load() + im.seek(1) + self.assertEqual(im._compression, "packbits") + self.assertEqual(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) + self.assertEqual(im._compression, "tiff_ccitt") + self.assertEqual(im.size, (10, 10)) + im.load() def test_save_tiff_with_jpegtables(self): # Arrange @@ -708,44 +679,50 @@ class TestFileLibTiff(LibTiffTestCase): # 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") + with Image.open("Tests/images/tiff_16bit_RGB.tiff") as im: + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (100, 40)) + self.assertEqual( + im.tile, + [ + ( + "libtiff", + (0, 0, 100, 40), + 0, + ("RGB;16N", "tiff_adobe_deflate", False, 8), + ) + ], + ) + im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (100, 40)) - self.assertEqual( - im.tile, - [ - ( - "libtiff", - (0, 0, 100, 40), - 0, - ("RGB;16N", "tiff_adobe_deflate", False, 8), - ) - ], - ) - im.load() - - self.assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") + self.assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") def test_16bit_RGBa_tiff(self): - im = Image.open("Tests/images/tiff_16bit_RGBa.tiff") + with Image.open("Tests/images/tiff_16bit_RGBa.tiff") as im: + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (100, 40)) + self.assertEqual( + im.tile, + [ + ( + "libtiff", + (0, 0, 100, 40), + 0, + ("RGBa;16N", "tiff_lzw", False, 38236), + ) + ], + ) + im.load() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (100, 40)) - self.assertEqual( - im.tile, - [("libtiff", (0, 0, 100, 40), 0, ("RGBa;16N", "tiff_lzw", False, 38236))], - ) - im.load() - - self.assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + self.assert_image_equal_tofile( + im, "Tests/images/tiff_16bit_RGBa_target.png" + ) def test_gimp_tiff(self): # Read TIFF JPEG images from GIMP [@PIL168] @@ -755,79 +732,112 @@ class TestFileLibTiff(LibTiffTestCase): self.skipTest("jpeg support not available") filename = "Tests/images/pil168.tif" - im = Image.open(filename) + with Image.open(filename) as im: + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (256, 256)) + self.assertEqual( + im.tile, + [("libtiff", (0, 0, 256, 256), 0, ("RGB", "jpeg", False, 5122))], + ) + im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (256, 256)) - self.assertEqual( - im.tile, [("libtiff", (0, 0, 256, 256), 0, ("RGB", "jpeg", False, 5122))] - ) - im.load() - - self.assert_image_equal_tofile(im, "Tests/images/pil168.png") + self.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: + self.assertEqual(im.mode, "RGB") - self.assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") + self.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: + 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) 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: + self.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: + self.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: + self.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: + self.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: + self.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: + self.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: + self.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) + with Image.open(infile) as im: + self.assert_image_equal_tofile( + im, "Tests/images/old-style-jpeg-compression.png" + ) - self.assert_image_equal_tofile( - im, "Tests/images/old-style-jpeg-compression.png" + def test_no_rows_per_strip(self): + # This image does not have a RowsPerStrip TIFF tag + infile = "Tests/images/no_rows_per_strip.tif" + with Image.open(infile) as im: + im.load() + self.assertEqual(im.size, (950, 975)) + + def test_orientation(self): + 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) + + 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". + tiff = io.BytesIO( + base64.b64decode( + b"SUkqAAgAAAAPAP4ABAABAAAAAAAAAAABBAABAAAAAQAAAAEBBAABAAAAAQAA" + b"AAIBAwADAAAAwgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAA" + b"4AAAABUBAwABAAAAAwAAABYBBAABAAAAAQAAABcBBAABAAAACwAAABoBBQAB" + b"AAAAyAAAABsBBQABAAAA0AAAABwBAwABAAAAAQAAACgBAwABAAAAAQAAAFMB" + b"AwADAAAA2AAAAAAAAAAIAAgACAABAAAAAQAAAAEAAAABAAAAAQABAAEAAAB4" + b"nGNgYAAAAAMAAQ==" + ) ) + out = io.BytesIO() + with Image.open(tiff) as im: + im.save(out, format="tiff") + out.seek(0) + with Image.open(out) as im: + im.load() diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 0db37c7ea..a4e74896f 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 @@ -18,30 +20,25 @@ class TestFileLibTiffSmall(LibTiffTestCase): 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: + self.assertEqual(im.size, (128, 128)) + self._assert_noerr(im) def test_g4_hopper_bytesio(self): """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) - - self.assertEqual(im.size, (128, 128)) - self._assert_noerr(im) + with Image.open(s) as im: + self.assertEqual(im.size, (128, 128)) + self._assert_noerr(im) def test_g4_hopper(self): """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: + self.assertEqual(im.size, (128, 128)) + self._assert_noerr(im) diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index acc4ddb91..ed2653cd3 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -17,12 +17,12 @@ class TestFileMcIdas(PillowTestCase): saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png" # Act - im = Image.open(test_file) - im.load() + 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 + self.assertEqual(im.format, "MCIDAS") + self.assertEqual(im.mode, "I") + self.assertEqual(im.size, (1800, 400)) + with Image.open(saved_file) as im2: + self.assert_image_equal(im, im2) diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 5ec110c80..00f42fa4a 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image, ImagePalette, features -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper try: from PIL import MicImagePlugin @@ -16,42 +18,42 @@ TEST_FILE = "Tests/images/hopper.mic" @unittest.skipUnless(features.check("libtiff"), "libtiff not installed") class TestFileMic(PillowTestCase): def test_sanity(self): - im = Image.open(TEST_FILE) - im.load() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "MIC") + 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") - # Adjust for the gamma of 2.2 encoded into the file - lut = ImagePalette.make_gamma_lut(1 / 2.2) - im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) + # Adjust for the gamma of 2.2 encoded into the file + lut = ImagePalette.make_gamma_lut(1 / 2.2) + im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) - im2 = hopper("RGBA") - self.assert_image_similar(im, im2, 10) + im2 = hopper("RGBA") + self.assert_image_similar(im, im2, 10) def test_n_frames(self): - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - self.assertEqual(im.n_frames, 1) + self.assertEqual(im.n_frames, 1) def test_is_animated(self): - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - self.assertFalse(im.is_animated) + self.assertFalse(im.is_animated) def test_tell(self): - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - self.assertEqual(im.tell(), 0) + self.assertEqual(im.tell(), 0) def test_seek(self): - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - im.seek(0) - self.assertEqual(im.tell(), 0) + im.seek(0) + self.assertEqual(im.tell(), 0) - self.assertRaises(EOFError, im.seek, 99) - self.assertEqual(im.tell(), 0) + self.assertRaises(EOFError, im.seek, 99) + self.assertEqual(im.tell(), 0) def test_invalid_file(self): # Test an invalid OLE file diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 82ecf6457..cdcf2b041 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,8 +1,9 @@ +import unittest from io import BytesIO from PIL import Image -from .helper import PillowTestCase +from .helper import PillowTestCase, is_pypy test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] @@ -25,78 +26,104 @@ class TestFileMpo(PillowTestCase): def test_sanity(self): for test_file in test_files: - im = Image.open(test_file) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (640, 480)) - self.assertEqual(im.format, "MPO") + with Image.open(test_file) as im: + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (640, 480)) + self.assertEqual(im.format, "MPO") + @unittest.skipIf(is_pypy(), "Requires CPython") def test_unclosed_file(self): def open(): im = Image.open(test_files[0]) im.load() + self.assert_warning(ResourceWarning, open) + + def test_closed_file(self): + def open(): + im = Image.open(test_files[0]) + im.load() + im.close() + + self.assert_warning(None, open) + + def test_context_manager(self): + def open(): + with Image.open(test_files[0]) as im: + im.load() + self.assert_warning(None, open) def test_app(self): for test_file in test_files: # Test APP/COM reader (@PIL135) - 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" - ) - self.assertEqual(len(im.applist), 2) + with Image.open(test_file) as im: + self.assertEqual(im.applist[0][0], "APP1") + self.assertEqual(im.applist[1][0], "APP2") + self.assertEqual( + im.applist[1][1][:16], + b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00", + ) + self.assertEqual(len(im.applist), 2) def test_exif(self): for test_file in test_files: - im = Image.open(test_file) - info = im._getexif() - self.assertEqual(info[272], "Nintendo 3DS") - self.assertEqual(info[296], 2) - self.assertEqual(info[34665], 188) + with Image.open(test_file) as im: + info = im._getexif() + self.assertEqual(info[272], "Nintendo 3DS") + self.assertEqual(info[296], 2) + self.assertEqual(info[34665], 188) def test_frame_size(self): # This image has been hexedited to contain a different size # in the EXIF data of the second frame - im = Image.open("Tests/images/sugarshack_frame_size.mpo") - self.assertEqual(im.size, (640, 480)) + with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: + self.assertEqual(im.size, (640, 480)) - im.seek(1) - self.assertEqual(im.size, (680, 480)) + im.seek(1) + self.assertEqual(im.size, (680, 480)) def test_parallax(self): # Nintendo - im = Image.open("Tests/images/sugarshack.mpo") - exif = im.getexif() - self.assertEqual(exif.get_ifd(0x927C)[0x1101]["Parallax"], -44.798187255859375) + with Image.open("Tests/images/sugarshack.mpo") as im: + exif = im.getexif() + self.assertEqual( + exif.get_ifd(0x927C)[0x1101]["Parallax"], -44.798187255859375 + ) # Fujifilm - im = Image.open("Tests/images/fujifilm.mpo") - im.seek(1) - exif = im.getexif() - self.assertEqual(exif.get_ifd(0x927C)[0xB211], -3.125) + with Image.open("Tests/images/fujifilm.mpo") as im: + im.seek(1) + exif = im.getexif() + self.assertEqual(exif.get_ifd(0x927C)[0xB211], -3.125) def test_mp(self): for test_file in test_files: - im = Image.open(test_file) - mpinfo = im._getmp() - self.assertEqual(mpinfo[45056], b"0100") - self.assertEqual(mpinfo[45057], 2) + with Image.open(test_file) as im: + mpinfo = im._getmp() + self.assertEqual(mpinfo[45056], b"0100") + self.assertEqual(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") - mpinfo = im._getmp() - self.assertEqual(mpinfo[45056], b"0100") - self.assertEqual(mpinfo[45057], 2) + 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) + + def test_mp_no_data(self): + # 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 self.assertRaises(ValueError): + im.seek(1) def test_mp_attribute(self): for test_file in test_files: - im = Image.open(test_file) - mpinfo = im._getmp() + with Image.open(test_file) as im: + mpinfo = im._getmp() frameNumber = 0 for mpentry in mpinfo[45058]: mpattr = mpentry["Attribute"] @@ -113,62 +140,62 @@ class TestFileMpo(PillowTestCase): def test_seek(self): for test_file in test_files: - im = Image.open(test_file) - self.assertEqual(im.tell(), 0) - # prior to first image raises an error, both blatant and borderline - self.assertRaises(EOFError, im.seek, -1) - self.assertRaises(EOFError, im.seek, -523) - # after the final image raises an error, - # both blatant and borderline - self.assertRaises(EOFError, im.seek, 2) - self.assertRaises(EOFError, im.seek, 523) - # bad calls shouldn't change the frame - self.assertEqual(im.tell(), 0) - # this one will work - im.seek(1) - self.assertEqual(im.tell(), 1) - # and this one, too - im.seek(0) - self.assertEqual(im.tell(), 0) + with Image.open(test_file) as im: + self.assertEqual(im.tell(), 0) + # prior to first image raises an error, both blatant and borderline + self.assertRaises(EOFError, im.seek, -1) + self.assertRaises(EOFError, im.seek, -523) + # after the final image raises an error, + # both blatant and borderline + self.assertRaises(EOFError, im.seek, 2) + self.assertRaises(EOFError, im.seek, 523) + # bad calls shouldn't change the frame + self.assertEqual(im.tell(), 0) + # this one will work + im.seek(1) + self.assertEqual(im.tell(), 1) + # and this one, too + im.seek(0) + self.assertEqual(im.tell(), 0) def test_n_frames(self): - im = Image.open("Tests/images/sugarshack.mpo") - self.assertEqual(im.n_frames, 2) - self.assertTrue(im.is_animated) + with Image.open("Tests/images/sugarshack.mpo") as im: + self.assertEqual(im.n_frames, 2) + self.assertTrue(im.is_animated) def test_eoferror(self): - im = Image.open("Tests/images/sugarshack.mpo") - n_frames = im.n_frames + 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) + # Test seeking past the last frame + self.assertRaises(EOFError, im.seek, n_frames) + self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) + # 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) - im0 = im.tobytes() - im.seek(1) - self.assertEqual(im.tell(), 1) - im1 = im.tobytes() - im.seek(0) - self.assertEqual(im.tell(), 0) - im02 = im.tobytes() - self.assertEqual(im0, im02) - self.assertNotEqual(im0, im1) + with Image.open(test_file) as im: + self.assertEqual(im.tell(), 0) + im0 = im.tobytes() + im.seek(1) + self.assertEqual(im.tell(), 1) + im1 = im.tobytes() + im.seek(0) + self.assertEqual(im.tell(), 0) + im02 = im.tobytes() + self.assertEqual(im0, im02) + self.assertNotEqual(im0, im1) def test_save(self): # Note that only individual frames can be saved at present for test_file in test_files: - im = Image.open(test_file) - self.assertEqual(im.tell(), 0) - jpg0 = self.frame_roundtrip(im) - self.assert_image_similar(im, jpg0, 30) - im.seek(1) - self.assertEqual(im.tell(), 1) - jpg1 = self.frame_roundtrip(im) - self.assert_image_similar(im, jpg1, 30) + with Image.open(test_file) as im: + self.assertEqual(im.tell(), 0) + jpg0 = self.frame_roundtrip(im) + self.assert_image_similar(im, jpg0, 30) + im.seek(1) + self.assertEqual(im.tell(), 1) + jpg1 = self.frame_roundtrip(im) + self.assert_image_similar(im, jpg1, 30) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 5d512047b..18c312046 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,8 +1,9 @@ import os +import unittest from PIL import Image, MspImagePlugin -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper TEST_FILE = "Tests/images/hopper.msp" EXTRA_DIR = "Tests/images/picins" @@ -15,11 +16,11 @@ class TestFileMsp(PillowTestCase): hopper("1").save(test_file) - im = Image.open(test_file) - im.load() - self.assertEqual(im.mode, "1") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "MSP") + 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") def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -37,18 +38,18 @@ class TestFileMsp(PillowTestCase): def test_open_windows_v1(self): # Arrange # Act - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Assert - self.assert_image_equal(im, hopper("1")) - self.assertIsInstance(im, MspImagePlugin.MspImageFile) + # 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) + with Image.open(target_path) as target: + self.assert_image_equal(im, target) - @unittest.skipIf(not os.path.exists(EXTRA_DIR), "Extra image files not installed") + @unittest.skipUnless(os.path.exists(EXTRA_DIR), "Extra image files not installed") def test_open_windows_v2(self): files = ( diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index b23328ba5..55d753fc3 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -5,8 +5,8 @@ from .helper import PillowTestCase class TestFilePcd(PillowTestCase): def test_load_raw(self): - im = Image.open("Tests/images/hopper.pcd") - im.load() # should not segfault. + 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 diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index eb2c7d611..780739422 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -7,13 +7,12 @@ class TestFilePcx(PillowTestCase): def _roundtrip(self, im): f = self.tempfile("temp.pcx") im.save(f) - im2 = Image.open(f) - - self.assertEqual(im2.mode, im.mode) - self.assertEqual(im2.size, im.size) - self.assertEqual(im2.format, "PCX") - self.assertEqual(im2.get_format_mimetype(), "image/x-pcx") - self.assert_image_equal(im2, im) + with Image.open(f) as im2: + self.assertEqual(im2.mode, im.mode) + self.assertEqual(im2.size, im.size) + self.assertEqual(im2.format, "PCX") + self.assertEqual(im2.get_format_mimetype(), "image/x-pcx") + self.assert_image_equal(im2, im) def test_sanity(self): for mode in ("1", "L", "P", "RGB"): @@ -42,13 +41,12 @@ class TestFilePcx(PillowTestCase): # Check reading of files where xmin/xmax is not zero. test_file = "Tests/images/pil184.pcx" - im = Image.open(test_file) + with Image.open(test_file) as im: + self.assertEqual(im.size, (447, 144)) + self.assertEqual(im.tile[0][1], (0, 0, 447, 144)) - self.assertEqual(im.size, (447, 144)) - self.assertEqual(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) + # 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)) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 25c2f6bf6..9b8b789b9 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -82,44 +82,43 @@ class TestFilePdf(PillowTestCase): self.helper_save_as_pdf("RGB", save_all=True) # Multiframe image - im = Image.open("Tests/images/dispose_bgnd.gif") + with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = self.tempfile("temp.pdf") - im.save(outfile, save_all=True) + outfile = self.tempfile("temp.pdf") + im.save(outfile, save_all=True) - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + self.assertTrue(os.path.isfile(outfile)) + self.assertGreater(os.path.getsize(outfile), 0) - # Append images - ims = [hopper()] - im.copy().save(outfile, save_all=True, append_images=ims) + # 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) + self.assertTrue(os.path.isfile(outfile)) + self.assertGreater(os.path.getsize(outfile), 0) - # Test appending using a generator - def imGenerator(ims): - for im in ims: - yield im + # Test appending using a generator + def imGenerator(ims): + yield from ims - im.save(outfile, save_all=True, append_images=imGenerator(ims)) + im.save(outfile, save_all=True, append_images=imGenerator(ims)) self.assertTrue(os.path.isfile(outfile)) self.assertGreater(os.path.getsize(outfile), 0) # Append JPEG images - jpeg = Image.open("Tests/images/flower.jpg") - jpeg.save(outfile, save_all=True, append_images=[jpeg.copy()]) + 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) def test_multiframe_normal_save(self): # Test saving a multiframe image without save_all - im = Image.open("Tests/images/dispose_bgnd.gif") + with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = self.tempfile("temp.pdf") - im.save(outfile) + outfile = self.tempfile("temp.pdf") + im.save(outfile) self.assertTrue(os.path.isfile(outfile)) self.assertGreater(os.path.getsize(outfile), 0) @@ -163,13 +162,10 @@ class TestFilePdf(PillowTestCase): def test_pdf_append_fails_on_nonexistent_file(self): im = hopper("RGB") - temp_dir = tempfile.mkdtemp() - try: + with tempfile.TemporaryDirectory() as temp_dir: 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) @@ -207,7 +203,7 @@ class TestFilePdf(PillowTestCase): # append some info pdf.info.Title = "abc" pdf.info.Author = "def" - pdf.info.Subject = u"ghi\uABCD" + pdf.info.Subject = "ghi\uABCD" pdf.info.Keywords = "qw)e\\r(ty" pdf.info.Creator = "hopper()" pdf.start_writing() @@ -235,7 +231,7 @@ class TestFilePdf(PillowTestCase): 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.assertEqual(pdf.info.Subject, "ghi\uABCD") self.assertIn(b"CreationDate", pdf.info) self.assertIn(b"ModDate", pdf.info) self.check_pdf_pages_consistency(pdf) diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index c744932d4..eae2fabba 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -7,15 +7,15 @@ TEST_FILE = "Tests/images/hopper.pxr" class TestFilePixar(PillowTestCase): def test_sanity(self): - im = Image.open(TEST_FILE) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PIXAR") - self.assertIsNone(im.get_format_mimetype()) + 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()) - im2 = hopper() - self.assert_image_similar(im, im2, 4.8) + im2 = hopper() + self.assert_image_similar(im, im2, 4.8) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 6d76a6caa..4cd613785 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,11 +1,10 @@ -import sys +import unittest import zlib from io import BytesIO from PIL import Image, ImageFile, PngImagePlugin -from PIL._util import py3 -from .helper import PillowLeakTestCase, PillowTestCase, hopper, unittest +from .helper import PillowLeakTestCase, PillowTestCase, hopper, is_win32 try: from PIL import _webp @@ -82,20 +81,20 @@ class TestFilePng(PillowTestCase): 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() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "PNG") + self.assertEqual(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) + self.assert_image_equal(reloaded, im) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -196,27 +195,24 @@ class TestFilePng(PillowTestCase): def test_interlace(self): test_file = "Tests/images/pil123p.png" - im = Image.open(test_file) + with Image.open(test_file) as im: + self.assert_image(im, "P", (162, 150)) + self.assertTrue(im.info.get("interlace")) - self.assert_image(im, "P", (162, 150)) - self.assertTrue(im.info.get("interlace")) - - im.load() + im.load() test_file = "Tests/images/pil123rgba.png" - im = Image.open(test_file) + with Image.open(test_file) as im: + self.assert_image(im, "RGBA", (162, 150)) + self.assertTrue(im.info.get("interlace")) - self.assert_image(im, "RGBA", (162, 150)) - self.assertTrue(im.info.get("interlace")) - - im.load() + im.load() def test_load_transparent_p(self): test_file = "Tests/images/pil123p.png" - im = Image.open(test_file) - - self.assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") + with Image.open(test_file) as im: + self.assert_image(im, "P", (162, 150)) + im = im.convert("RGBA") self.assert_image(im, "RGBA", (162, 150)) # image has 124 unique alpha values @@ -224,11 +220,11 @@ class TestFilePng(PillowTestCase): 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: + self.assertEqual(im.info["transparency"], (0, 255, 52)) - self.assert_image(im, "RGB", (64, 64)) - im = im.convert("RGBA") + self.assert_image(im, "RGB", (64, 64)) + im = im.convert("RGBA") self.assert_image(im, "RGBA", (64, 64)) # image has 876 transparent pixels @@ -236,21 +232,20 @@ class TestFilePng(PillowTestCase): def test_save_p_transparent_palette(self): in_file = "Tests/images/pil123p.png" - im = Image.open(in_file) + with Image.open(in_file) as im: + # 'transparency' contains a byte string with the opacity for + # each palette entry + self.assertEqual(len(im.info["transparency"]), 256) - # 'transparency' contains a byte string with the opacity for - # each palette entry - self.assertEqual(len(im.info["transparency"]), 256) - - test_file = self.tempfile("temp.png") - im.save(test_file) + test_file = self.tempfile("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: + self.assertEqual(len(im.info["transparency"]), 256) - self.assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") + self.assert_image(im, "P", (162, 150)) + im = im.convert("RGBA") self.assert_image(im, "RGBA", (162, 150)) # image has 124 unique alpha values @@ -258,21 +253,20 @@ class TestFilePng(PillowTestCase): def test_save_p_single_transparency(self): in_file = "Tests/images/p_trns_single.png" - im = Image.open(in_file) + with Image.open(in_file) as im: + # pixel value 164 is full transparent + self.assertEqual(im.info["transparency"], 164) + self.assertEqual(im.getpixel((31, 31)), 164) - # pixel value 164 is full transparent - self.assertEqual(im.info["transparency"], 164) - self.assertEqual(im.getpixel((31, 31)), 164) - - test_file = self.tempfile("temp.png") - im.save(test_file) + test_file = self.tempfile("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") + with Image.open(test_file) as im: + 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)) self.assertEqual(im.getpixel((31, 31)), (0, 255, 52, 0)) @@ -291,30 +285,30 @@ class TestFilePng(PillowTestCase): 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") + with Image.open(test_file) as im: + 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): 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: + self.assertEqual(im.mode, mode) + self.assertEqual(im.info["transparency"], 255) - im_rgba = im.convert("RGBA") + im_rgba = im.convert("RGBA") self.assertEqual(im_rgba.getchannel("A").getcolors()[0][0], num_transparent) test_file = self.tempfile("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: + self.assertEqual(test_im.mode, mode) + self.assertEqual(test_im.info["transparency"], 255) + self.assert_image_equal(im, test_im) test_im_rgba = test_im.convert("RGBA") self.assertEqual( @@ -323,22 +317,20 @@ class TestFilePng(PillowTestCase): def test_save_rgb_single_transparency(self): 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 = self.tempfile("temp.png") + im.save(test_file) def test_load_verify(self): # Check open/load/verify exception (@PIL150) - im = Image.open(TEST_PNG_FILE) + with Image.open(TEST_PNG_FILE) as im: + # Assert that there is no unclosed file warning + self.assert_warning(None, im.verify) - # Assert that there is no unclosed file warning - self.assert_warning(None, im.verify) - - im = Image.open(TEST_PNG_FILE) - im.load() - self.assertRaises(RuntimeError, im.verify) + with Image.open(TEST_PNG_FILE) as im: + im.load() + self.assertRaises(RuntimeError, im.verify) def test_verify_struct_error(self): # Check open/load/verify exception (#1755) @@ -351,9 +343,9 @@ class TestFilePng(PillowTestCase): with open(TEST_PNG_FILE, "rb") as f: test_file = f.read()[:offset] - im = Image.open(BytesIO(test_file)) - self.assertIsNotNone(im.fp) - self.assertRaises((IOError, SyntaxError), im.verify) + with Image.open(BytesIO(test_file)) as im: + self.assertIsNotNone(im.fp) + self.assertRaises((IOError, SyntaxError), im.verify) def test_verify_ignores_crc_error(self): # check ignores crc errors in ancillary chunks @@ -387,24 +379,22 @@ class TestFilePng(PillowTestCase): def test_roundtrip_dpi(self): # Check dpi roundtripping - im = Image.open(TEST_PNG_FILE) - - im = roundtrip(im, dpi=(100, 100)) + with Image.open(TEST_PNG_FILE) as im: + im = roundtrip(im, dpi=(100, 100)) self.assertEqual(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: + self.assertEqual(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: + self.assertEqual(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)) + with Image.open(TEST_PNG_FILE) as im: + im = roundtrip(im, dpi=(72.2, 72.2)) self.assertEqual(im.info["dpi"], (72, 72)) im = roundtrip(im, dpi=(72.8, 72.8)) @@ -413,13 +403,12 @@ class TestFilePng(PillowTestCase): def test_roundtrip_text(self): # Check text roundtripping - im = Image.open(TEST_PNG_FILE) + with Image.open(TEST_PNG_FILE) as im: + info = PngImagePlugin.PngInfo() + info.add_text("TXT", "VALUE") + info.add_text("ZIP", "VALUE", zip=True) - info = PngImagePlugin.PngInfo() - info.add_text("TXT", "VALUE") - info.add_text("ZIP", "VALUE", zip=True) - - im = roundtrip(im, pnginfo=info) + im = roundtrip(im, pnginfo=info) self.assertEqual(im.info, {"TXT": "VALUE", "ZIP": "VALUE"}) self.assertEqual(im.text, {"TXT": "VALUE", "ZIP": "VALUE"}) @@ -449,9 +438,7 @@ class TestFilePng(PillowTestCase): self.assertIsInstance(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)) @@ -460,12 +447,11 @@ class TestFilePng(PillowTestCase): im = roundtrip(im, pnginfo=info) self.assertEqual(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: @@ -484,11 +470,11 @@ class TestFilePng(PillowTestCase): # 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: + self.assertEqual(im.info["transparency"], (248, 248, 248)) - # check saving transparency by default - im = roundtrip(im) + # check saving transparency by default + im = roundtrip(im) self.assertEqual(im.info["transparency"], (248, 248, 248)) im = roundtrip(im, transparency=(0, 1, 2)) @@ -502,59 +488,58 @@ class TestFilePng(PillowTestCase): f = self.tempfile("temp.png") im.save(f) - im2 = Image.open(f) - self.assertIn("transparency", im2.info) + with Image.open(f) as im2: + self.assertIn("transparency", im2.info) - self.assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) + self.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) + self.assertEqual(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: + self.assertIsNone(im.info["icc_profile"]) - 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) + self.assertEqual(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) + with Image.open("Tests/images/icc_profile.png") as im: + im = roundtrip(im, icc_profile=None) self.assertNotIn("icc_profile", 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) + im = roundtrip(im) self.assertEqual(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: + self.assertIsNone(im.info["icc_profile"]) - im = roundtrip(im) + im = roundtrip(im) self.assertNotIn("icc_profile", 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: + self.assertEqual(repr_png.format, "PNG") + self.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)) + with Image.open("Tests/images/icc_profile.png") as im: + test_file = self.tempfile("temp.png") + im.convert("P").save(test_file, dpi=(100, 100)) chunks = self.get_chunks(test_file) @@ -579,78 +564,75 @@ class TestFilePng(PillowTestCase): self.assertEqual(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: + 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) # 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 self.assertRaises(IOError): + self.assertIsInstance(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 + self.assertRaises(IOError, lambda: im.text) + ImageFile.LOAD_TRUNCATED_IMAGES = True + self.assertIsInstance(im.text, dict) + ImageFile.LOAD_TRUNCATED_IMAGES = False # Raises an EOFError in load_end - im = Image.open("Tests/images/hopper_idat_after_image_end.png") - self.assertEqual(im.text, {"TXT": "VALUE", "ZIP": "VALUE"}) + with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: + self.assertEqual(im.text, {"TXT": "VALUE", "ZIP": "VALUE"}) def test_exif(self): - im = Image.open("Tests/images/exif.png") - exif = im._getexif() + with Image.open("Tests/images/exif.png") as im: + exif = im._getexif() self.assertEqual(exif[274], 1) def test_exif_save(self): - im = Image.open("Tests/images/exif.png") + with Image.open("Tests/images/exif.png") as im: + test_file = self.tempfile("temp.png") + im.save(test_file) - test_file = self.tempfile("temp.png") - im.save(test_file) - - reloaded = Image.open(test_file) - exif = reloaded._getexif() + with Image.open(test_file) as reloaded: + exif = reloaded._getexif() self.assertEqual(exif[274], 1) def test_exif_from_jpg(self): - im = Image.open("Tests/images/pil_sample_rgb.jpg") + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + test_file = self.tempfile("temp.png") + im.save(test_file) - test_file = self.tempfile("temp.png") - im.save(test_file) - - reloaded = Image.open(test_file) - exif = reloaded._getexif() + with Image.open(test_file) as reloaded: + exif = reloaded._getexif() self.assertEqual(exif[305], "Adobe Photoshop CS Macintosh") def test_exif_argument(self): - im = Image.open(TEST_PNG_FILE) + with Image.open(TEST_PNG_FILE) as im: + test_file = self.tempfile("temp.png") + im.save(test_file, exif=b"exifstring") - test_file = self.tempfile("temp.png") - im.save(test_file, exif=b"exifstring") - - reloaded = Image.open(test_file) - self.assertEqual(reloaded.info["exif"], b"Exif\x00\x00exifstring") + with Image.open(test_file) as reloaded: + self.assertEqual(reloaded.info["exif"], b"Exif\x00\x00exifstring") @unittest.skipUnless( HAVE_WEBP and _webp.HAVE_WEBPANIM, "WebP support not installed with animation" ) def test_apng(self): - im = Image.open("Tests/images/iss634.apng") - self.assertEqual(im.get_format_mimetype(), "image/apng") + with Image.open("Tests/images/iss634.apng") as im: + self.assertEqual(im.get_format_mimetype(), "image/apng") - # 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) + # This also tests reading unknown PNG chunks (fcTL and fdAT) in load_end + with Image.open("Tests/images/iss634.webp") as expected: + self.assert_image_similar(im, expected, 0.23) -@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") +@unittest.skipIf(is_win32(), "requires Unix or macOS") class TestTruncatedPngPLeaks(PillowLeakTestCase): mem_limit = 2 * 1024 # max increase in K iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 5d2a0bc69..cd3719c18 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -8,42 +8,42 @@ test_file = "Tests/images/hopper.ppm" class TestFilePpm(PillowTestCase): def test_sanity(self): - im = Image.open(test_file) - 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") + 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") def test_16bit_pgm(self): - im = Image.open("Tests/images/16_bit_binary.pgm") - im.load() - self.assertEqual(im.mode, "I") - self.assertEqual(im.size, (20, 100)) - self.assertEqual(im.get_format_mimetype(), "image/x-portable-graymap") + 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") - tgt = Image.open("Tests/images/16_bit_binary_pgm.png") - self.assert_image_equal(im, tgt) + with Image.open("Tests/images/16_bit_binary_pgm.png") as tgt: + self.assert_image_equal(im, tgt) def test_16bit_pgm_write(self): - im = Image.open("Tests/images/16_bit_binary.pgm") - im.load() + with Image.open("Tests/images/16_bit_binary.pgm") as im: + im.load() - f = self.tempfile("temp.pgm") - im.save(f, "PPM") + f = self.tempfile("temp.pgm") + im.save(f, "PPM") - reloaded = Image.open(f) - self.assert_image_equal(im, reloaded) + with Image.open(f) as reloaded: + self.assert_image_equal(im, reloaded) def test_pnm(self): - im = Image.open("Tests/images/hopper.pnm") - self.assert_image_similar(im, hopper(), 0.0001) + with Image.open("Tests/images/hopper.pnm") as im: + self.assert_image_similar(im, hopper(), 0.0001) - f = self.tempfile("temp.pnm") - im.save(f) + f = self.tempfile("temp.pnm") + im.save(f) - reloaded = Image.open(f) - self.assert_image_equal(im, reloaded) + with Image.open(f) as reloaded: + self.assert_image_equal(im, reloaded) def test_truncated_file(self): path = self.tempfile("temp.pgm") @@ -66,10 +66,10 @@ class TestFilePpm(PillowTestCase): 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 Image.open(path) as im: + self.assertEqual(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 Image.open(path) as im: + self.assertEqual(im.get_format_mimetype(), "image/x-portable-anymap") diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index f55ee1dcb..939485f67 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,26 +1,44 @@ +import unittest + from PIL import Image, PsdImagePlugin -from .helper import PillowTestCase, hopper +from .helper import PillowTestCase, hopper, is_pypy test_file = "Tests/images/hopper.psd" class TestImagePsd(PillowTestCase): def test_sanity(self): - im = Image.open(test_file) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PSD") + with Image.open(test_file) as im: + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "PSD") - im2 = hopper() - self.assert_image_similar(im, im2, 4.8) + im2 = hopper() + self.assert_image_similar(im, im2, 4.8) + @unittest.skipIf(is_pypy(), "Requires CPython") def test_unclosed_file(self): def open(): im = Image.open(test_file) im.load() + self.assert_warning(ResourceWarning, open) + + def test_closed_file(self): + def open(): + im = Image.open(test_file) + im.load() + im.close() + + self.assert_warning(None, open) + + def test_context_manager(self): + def open(): + with Image.open(test_file) as im: + im.load() + self.assert_warning(None, open) def test_invalid_file(self): @@ -29,61 +47,69 @@ class TestImagePsd(PillowTestCase): self.assertRaises(SyntaxError, PsdImagePlugin.PsdImageFile, invalid_file) 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 Image.open("Tests/images/hopper_merged.psd") as im: + self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) - im = Image.open(test_file) - self.assertEqual(im.n_frames, 2) - self.assertTrue(im.is_animated) + with Image.open(test_file) as im: + self.assertEqual(im.n_frames, 2) + self.assertTrue(im.is_animated) def test_eoferror(self): - im = Image.open(test_file) - # PSD seek index starts at 1 rather than 0 - n_frames = im.n_frames + 1 + 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) + # Test seeking past the last frame + self.assertRaises(EOFError, im.seek, n_frames) + self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) + # 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) + with Image.open(test_file) as im: - layer_number = im.tell() - self.assertEqual(layer_number, 1) + layer_number = im.tell() + self.assertEqual(layer_number, 1) - self.assertRaises(EOFError, im.seek, 0) + self.assertRaises(EOFError, im.seek, 0) - im.seek(1) - layer_number = im.tell() - self.assertEqual(layer_number, 1) + im.seek(1) + layer_number = im.tell() + self.assertEqual(layer_number, 1) - im.seek(2) - layer_number = im.tell() - self.assertEqual(layer_number, 2) + im.seek(2) + layer_number = im.tell() + self.assertEqual(layer_number, 2) def test_seek_eoferror(self): - im = Image.open(test_file) + with Image.open(test_file) as im: - self.assertRaises(EOFError, im.seek, -1) + self.assertRaises(EOFError, im.seek, -1) def test_open_after_exclusive_load(self): - im = Image.open(test_file) - im.load() - im.seek(im.tell() + 1) - im.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) + with Image.open(test_file) as im: + self.assertIn("icc_profile", im.info) - icc_profile = im.info["icc_profile"] - self.assertEqual(len(icc_profile), 3144) + icc_profile = im.info["icc_profile"] + self.assertEqual(len(icc_profile), 3144) def test_no_icc_profile(self): - im = Image.open("Tests/images/hopper_merged.psd") + with Image.open("Tests/images/hopper_merged.psd") as im: + self.assertNotIn("icc_profile", 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. + + # 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") diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index ff3aea1d5..2cb510c6f 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -9,50 +9,50 @@ class TestFileSgi(PillowTestCase): # 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: + self.assert_image_equal(im, hopper()) + self.assertEqual(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()) + with Image.open(test_file) as im: + self.assert_image_equal(im, hopper()) def test_l(self): # Created with ImageMagick # convert hopper.ppm -monochrome -compress None sgi:hopper.bw test_file = "Tests/images/hopper.bw" - im = Image.open(test_file) - self.assert_image_similar(im, hopper("L"), 2) - self.assertEqual(im.get_format_mimetype(), "image/sgi") + with Image.open(test_file) as im: + 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" - 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: + with Image.open("Tests/images/transparent.png") as target: + self.assert_image_equal(im, target) + self.assertEqual(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) + with Image.open(test_file) as im: + with Image.open("Tests/images/hopper.rgb") as target: + self.assert_image_equal(im, target) def test_rle16(self): test_file = "Tests/images/tv16.sgi" - im = Image.open(test_file) - target = Image.open("Tests/images/tv.rgb") - self.assert_image_equal(im, target) + with Image.open(test_file) as im: + with Image.open("Tests/images/tv.rgb") as target: + self.assert_image_equal(im, target) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -63,8 +63,8 @@ class TestFileSgi(PillowTestCase): def roundtrip(img): out = self.tempfile("temp.sgi") img.save(out, format="sgi") - reloaded = Image.open(out) - self.assert_image_equal(img, reloaded) + with Image.open(out) as reloaded: + self.assert_image_equal(img, reloaded) for mode in ("L", "RGB", "RGBA"): roundtrip(hopper(mode)) @@ -75,12 +75,12 @@ class TestFileSgi(PillowTestCase): def test_write16(self): test_file = "Tests/images/hopper16.rgb" - im = Image.open(test_file) - out = self.tempfile("temp.sgi") - im.save(out, format="sgi", bpc=2) + with Image.open(test_file) as im: + out = self.tempfile("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: + self.assert_image_equal(im, reloaded) def test_unsupported_mode(self): im = hopper("LA") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 8a1eb6637..4f617ce51 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,25 +1,43 @@ import tempfile +import unittest +from io import BytesIO from PIL import Image, ImageSequence, SpiderImagePlugin -from .helper import PillowTestCase, hopper +from .helper import PillowTestCase, hopper, is_pypy TEST_FILE = "Tests/images/hopper.spider" class TestImageSpider(PillowTestCase): def test_sanity(self): - im = Image.open(TEST_FILE) - im.load() - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "SPIDER") + with Image.open(TEST_FILE) as im: + im.load() + self.assertEqual(im.mode, "F") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "SPIDER") + @unittest.skipIf(is_pypy(), "Requires CPython") def test_unclosed_file(self): def open(): im = Image.open(TEST_FILE) im.load() + self.assert_warning(ResourceWarning, open) + + def test_closed_file(self): + def open(): + im = Image.open(TEST_FILE) + im.load() + im.close() + + self.assert_warning(None, open) + + def test_context_manager(self): + def open(): + with Image.open(TEST_FILE) as im: + im.load() + self.assert_warning(None, open) def test_save(self): @@ -31,10 +49,10 @@ class TestImageSpider(PillowTestCase): im.save(temp, "SPIDER") # Assert - im2 = Image.open(temp) - self.assertEqual(im2.mode, "F") - self.assertEqual(im2.size, (128, 128)) - self.assertEqual(im2.format, "SPIDER") + with Image.open(temp) as im2: + self.assertEqual(im2.mode, "F") + self.assertEqual(im2.size, (128, 128)) + self.assertEqual(im2.format, "SPIDER") def test_tempfile(self): # Arrange @@ -46,28 +64,28 @@ class TestImageSpider(PillowTestCase): # Assert fp.seek(0) - reloaded = Image.open(fp) - self.assertEqual(reloaded.mode, "F") - self.assertEqual(reloaded.size, (128, 128)) - self.assertEqual(reloaded.format, "SPIDER") + with Image.open(fp) as reloaded: + self.assertEqual(reloaded.mode, "F") + self.assertEqual(reloaded.size, (128, 128)) + self.assertEqual(reloaded.format, "SPIDER") def test_isSpiderImage(self): self.assertTrue(SpiderImagePlugin.isSpiderImage(TEST_FILE)) def test_tell(self): # Arrange - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Act - index = im.tell() + # Act + index = im.tell() - # Assert - self.assertEqual(index, 0) + # Assert + self.assertEqual(index, 0) def test_n_frames(self): - im = Image.open(TEST_FILE) - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) + with Image.open(TEST_FILE) as im: + self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) def test_loadImageSeries(self): # Arrange @@ -108,12 +126,22 @@ class TestImageSpider(PillowTestCase): self.assertRaises(IOError, Image.open, invalid_file) def test_nonstack_file(self): - im = Image.open(TEST_FILE) - - self.assertRaises(EOFError, im.seek, 0) + with Image.open(TEST_FILE) as im: + self.assertRaises(EOFError, im.seek, 0) def test_nonstack_dos(self): - im = Image.open(TEST_FILE) - for i, frame in enumerate(ImageSequence.Iterator(im)): - if i > 1: - self.fail("Non-stack DOS file test failed") + 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) + with Image.open(data) as im2: + self.assert_image_equal(im, im2) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 84d59e0c7..6fe9ff29c 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,8 +1,9 @@ import os +import unittest from PIL import Image, SunImagePlugin -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper EXTRA_DIR = "Tests/images/sunraster" @@ -14,22 +15,22 @@ class TestFileSun(PillowTestCase): test_file = "Tests/images/hopper.ras" # Act - im = Image.open(test_file) + with Image.open(test_file) as im: - # Assert - self.assertEqual(im.size, (128, 128)) + # Assert + self.assertEqual(im.size, (128, 128)) - self.assert_image_similar(im, hopper(), 5) # visually verified + 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) + with Image.open("Tests/images/sunraster.im1") as im: + with Image.open("Tests/images/sunraster.im1.png") as target: + self.assert_image_equal(im, target) - @unittest.skipIf(not os.path.exists(EXTRA_DIR), "Extra image files not installed") + @unittest.skipUnless(os.path.exists(EXTRA_DIR), "Extra image files not installed") def test_others(self): files = ( os.path.join(EXTRA_DIR, f) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index c4666a65a..a77e4e84c 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image, TarIO -from .helper import PillowTestCase +from .helper import PillowTestCase, is_pypy codecs = dir(Image.core) @@ -19,17 +21,30 @@ class TestFileTar(PillowTestCase): ["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) + with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: + with Image.open(tar) as im: + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, format) + + @unittest.skipIf(is_pypy(), "Requires CPython") + def test_unclosed_file(self): + def open(): + TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") + + self.assert_warning(ResourceWarning, open) def test_close(self): - tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") - tar.close() + def open(): + tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") + tar.close() + + self.assert_warning(None, open) def test_contextmanager(self): - with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): - pass + def open(): + with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): + pass + + self.assert_warning(None, open) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index abbebe0eb..01664cd6e 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -24,177 +24,179 @@ class TestFileTga(PillowTestCase): ) for png_path in png_paths: - reference_im = Image.open(png_path) - self.assertEqual(reference_im.mode, mode) + with Image.open(png_path) as reference_im: + self.assertEqual(reference_im.mode, mode) - path_no_ext = os.path.splitext(png_path)[0] - for origin, rle in product(self._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() + path_no_ext = os.path.splitext(png_path)[0] + for origin, rle in product(self._ORIGINS, (True, False)): + tga_path = "{}_{}_{}.tga".format( + path_no_ext, origin, "rle" if rle else "raw" ) - self.assert_image_equal(original_im, reference_im) + with Image.open(tga_path) as original_im: + 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() + ) - # Generate a new test name every time so the - # test will not fail with permission error - # on Windows. - out = self.tempfile("temp.tga") + self.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() - ) + # Generate a new test name every time so the + # test will not fail with permission error + # on Windows. + out = self.tempfile("temp.tga") - self.assert_image_equal(saved_im, original_im) + original_im.save(out, rle=rle) + with Image.open(out) as saved_im: + if rle: + self.assertEqual( + saved_im.info["compression"], + original_im.info["compression"], + ) + self.assertEqual( + saved_im.info["orientation"], + original_im.info["orientation"], + ) + if mode == "P": + self.assertEqual( + saved_im.getpalette(), original_im.getpalette() + ) + + self.assert_image_equal(saved_im, original_im) def test_id_field(self): # tga file with id field test_file = "Tests/images/tga_id_field.tga" # Act - im = Image.open(test_file) + with Image.open(test_file) as im: - # Assert - self.assertEqual(im.size, (100, 100)) + # Assert + self.assertEqual(im.size, (100, 100)) def test_id_field_rle(self): # tga file with id field test_file = "Tests/images/rgb32rle.tga" # Act - im = Image.open(test_file) + with Image.open(test_file) as im: - # Assert - self.assertEqual(im.size, (199, 199)) + # Assert + self.assertEqual(im.size, (199, 199)) def test_save(self): test_file = "Tests/images/tga_id_field.tga" - im = Image.open(test_file) + with Image.open(test_file) as im: + out = self.tempfile("temp.tga") - out = self.tempfile("temp.tga") + # Save + im.save(out) + with Image.open(out) as test_im: + self.assertEqual(test_im.size, (100, 100)) + self.assertEqual(test_im.info["id_section"], im.info["id_section"]) - # 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"]) - - # RGBA save - im.convert("RGBA").save(out) - test_im = Image.open(out) - self.assertEqual(test_im.size, (100, 100)) + # RGBA save + im.convert("RGBA").save(out) + with Image.open(out) as test_im: + self.assertEqual(test_im.size, (100, 100)) def test_save_id_section(self): test_file = "Tests/images/rgb32rle.tga" - im = Image.open(test_file) + with Image.open(test_file) as im: + out = self.tempfile("temp.tga") - out = self.tempfile("temp.tga") - - # Check there is no id section - im.save(out) - test_im = Image.open(out) - self.assertNotIn("id_section", test_im.info) + # Check there is no id section + im.save(out) + with Image.open(out) as test_im: + self.assertNotIn("id_section", 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") + with Image.open(out) as test_im: + self.assertEqual(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]) + with Image.open(out) as test_im: + self.assertEqual(test_im.info["id_section"], id_section[:255]) test_file = "Tests/images/tga_id_field.tga" - im = Image.open(test_file) + 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) + # Save with no id section + im.save(out, id_section="") + with Image.open(out) as test_im: + self.assertNotIn("id_section", 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") + with Image.open(test_file) as im: + self.assertEqual(im.info["orientation"], -1) - im.save(out, orientation=1) - test_im = Image.open(out) - self.assertEqual(test_im.info["orientation"], 1) + im.save(out, orientation=1) + with Image.open(out) as test_im: + self.assertEqual(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") + with Image.open(test_file) as im: + self.assertEqual(im.info["compression"], "tga_rle") - out = self.tempfile("temp.tga") + out = self.tempfile("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") + # Save + im.save(out) + with Image.open(out) as test_im: + self.assertEqual(test_im.size, (199, 199)) + self.assertEqual(test_im.info["compression"], "tga_rle") # Save without compression im.save(out, compression=None) - test_im = Image.open(out) - self.assertNotIn("compression", test_im.info) + with Image.open(out) as test_im: + self.assertNotIn("compression", test_im.info) # RGBA save im.convert("RGBA").save(out) - test_im = Image.open(out) - self.assertEqual(test_im.size, (199, 199)) + with Image.open(out) as test_im: + self.assertEqual(test_im.size, (199, 199)) test_file = "Tests/images/tga_id_field.tga" - im = Image.open(test_file) - self.assertNotIn("compression", im.info) + with Image.open(test_file) as im: + self.assertNotIn("compression", im.info) - # Save with compression - im.save(out, compression="tga_rle") - test_im = Image.open(out) - self.assertEqual(test_im.info["compression"], "tga_rle") + # Save with compression + im.save(out, compression="tga_rle") + with Image.open(out) as test_im: + self.assertEqual(test_im.info["compression"], "tga_rle") def test_save_l_transparency(self): # There are 559 transparent pixels in la.tga. num_transparent = 559 in_file = "Tests/images/la.tga" - im = Image.open(in_file) - self.assertEqual(im.mode, "LA") - self.assertEqual(im.getchannel("A").getcolors()[0][0], num_transparent) + with Image.open(in_file) as im: + self.assertEqual(im.mode, "LA") + self.assertEqual(im.getchannel("A").getcolors()[0][0], num_transparent) - out = self.tempfile("temp.tga") - im.save(out) + out = self.tempfile("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: + self.assertEqual(test_im.mode, "LA") + self.assertEqual(test_im.getchannel("A").getcolors()[0][0], num_transparent) - self.assert_image_equal(im, test_im) + self.assert_image_equal(im, test_im) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index c3f55150c..2353fb34e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,12 +1,13 @@ import logging -import sys +import os +import unittest from io import BytesIO -from PIL import Image, TiffImagePlugin, features -from PIL._util import py3 +import pytest +from PIL import Image, TiffImagePlugin from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper, is_pypy, is_win32 logger = logging.getLogger(__name__) @@ -18,54 +19,73 @@ class TestFileTiff(PillowTestCase): hopper("RGB").save(filename) - im = Image.open(filename) - im.load() + with Image.open(filename) as im: + im.load() self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (128, 128)) self.assertEqual(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 + @unittest.skipIf(is_pypy(), "Requires CPython") def test_unclosed_file(self): def open(): im = Image.open("Tests/images/multipage.tiff") im.load() + self.assert_warning(ResourceWarning, open) + + def test_closed_file(self): + def open(): + im = Image.open("Tests/images/multipage.tiff") + im.load() + im.close() + + self.assert_warning(None, open) + + def test_context_manager(self): + def open(): + with Image.open("Tests/images/multipage.tiff") as im: + im.load() + self.assert_warning(None, open) def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] filename = "Tests/images/pil136.tiff" - im = Image.open(filename) + with Image.open(filename) as im: + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (55, 43)) + self.assertEqual(im.tile, [("raw", (0, 0, 55, 43), 8, ("RGBa", 0, 1))]) + im.load() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (55, 43)) - self.assertEqual(im.tile, [("raw", (0, 0, 55, 43), 8, ("RGBa", 0, 1))]) - im.load() - - self.assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) + self.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: + 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() def test_set_legacy_api(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() @@ -73,86 +93,76 @@ class TestFileTiff(PillowTestCase): 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) - 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 + self.assertIsInstance(im.tag[X_RESOLUTION][0], tuple) + self.assertIsInstance(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 + self.assertIsInstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) + self.assertIsInstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) - self.assertEqual(im.info["dpi"], (72.0, 72.0)) + self.assertEqual(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 + 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]) - # Legacy. - self.assertEqual(im.info["resolution"], (100.0, 100.0)) - # Fallback "inch". - self.assertEqual(im.info["dpi"], (100.0, 100.0)) + # Legacy. + self.assertEqual(im.info["resolution"], (100.0, 100.0)) + # Fallback "inch". + self.assertEqual(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() + self.assertEqual(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: + self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit) + self.assertEqual(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])) + ) as im: + 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") + with Image.open("Tests/images/hopper.tif") as im: + for dpi in (72.2, 72.8): + im.save(outfile, dpi=(dpi, dpi)) - for dpi in (72.2, 72.8): - im.save(outfile, dpi=(dpi, dpi)) - - reloaded = Image.open(outfile) - reloaded.load() - self.assertEqual((round(dpi), round(dpi)), reloaded.info["dpi"]) + with Image.open(outfile) as reloaded: + reloaded.load() + self.assertEqual((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: + self.assertEqual(float(im.tag_v2[X_RESOLUTION]), 123.45) + self.assertEqual(float(im.tag_v2[Y_RESOLUTION]), 123.45) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -164,9 +174,9 @@ class TestFileTiff(PillowTestCase): 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. + self.assert_warning(UserWarning, i._getexif) def test_save_rgba(self): im = hopper("RGBA") @@ -179,63 +189,53 @@ class TestFileTiff(PillowTestCase): self.assertRaises(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: + self.assertEqual(im.getpixel((0, 0)), 480) + self.assertEqual(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") + self.assertEqual(b[0], ord(b"\xe0")) + self.assertEqual(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: + self.assertEqual(im.getpixel((0, 0)), 480) + self.assertEqual(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") + self.assertEqual(b[0], ord(b"\x01")) + self.assertEqual(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() + self.assertEqual(im.mode, "I") + self.assertEqual(im.getpixel((0, 0)), 32767) + self.assertEqual(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") + self.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)) + self.assertEqual(im.getpixel((0, 0)), -0.4526388943195343) + self.assertEqual(im.getextrema(), (-3.140936851501465, 3.140684127807617)) def test_unknown_pixel_mode(self): self.assertRaises( @@ -247,109 +247,109 @@ class TestFileTiff(PillowTestCase): ["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: + self.assertEqual(im.n_frames, n_frames) + self.assertEqual(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 + self.assertRaises(EOFError, im.seek, n_frames) + self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) + # 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) + self.assertEqual(im.size, (10, 10)) + self.assertEqual(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() + self.assertEqual(im.size, (10, 10)) + self.assertEqual(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() + self.assertEqual(im.size, (10, 10)) + self.assertEqual(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() + self.assertEqual(im.size, (20, 20)) + self.assertEqual(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() + self.assertEqual(im.size, (20, 20)) + self.assertEqual(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 + self.assertIsInstance(ret, str) def test_dict(self): # Arrange filename = "Tests/images/pil136.tiff" - im = Image.open(filename) + 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, - } - self.assertEqual(dict(im.tag_v2), v2_tags) + # 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) + # 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) 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)) + self.assertEqual(len_before, len_after + 1) def test_load_byte(self): for legacy_api in [False, True]: @@ -378,16 +378,16 @@ class TestFileTiff(PillowTestCase): 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) + self.assertEqual(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: + self.assertEqual(im.tell(), 0) + self.assertRaises(EOFError, im.seek, -1) + self.assertRaises(EOFError, im.seek, 1) def test__limit_rational_int(self): from PIL.TiffImagePlugin import _limit_rational @@ -406,10 +406,10 @@ class TestFileTiff(PillowTestCase): 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: + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.mode, "L") + self.assert_image_similar(im, original, 7.3) def test_gray_semibyte_per_pixel(self): test_files = ( @@ -434,73 +434,68 @@ class TestFileTiff(PillowTestCase): ) original = hopper("L") for epsilon, group in test_files: - im = Image.open(group[0]) - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.mode, "L") - self.assert_image_similar(im, original, epsilon) - for file in group[1:]: - im2 = Image.open(file) - self.assertEqual(im2.size, (128, 128)) - self.assertEqual(im2.mode, "L") - self.assert_image_equal(im, im2) + with Image.open(group[0]) as im: + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.mode, "L") + self.assert_image_similar(im, original, epsilon) + for file in group[1:]: + with Image.open(file) as im2: + self.assertEqual(im2.size, (128, 128)) + self.assertEqual(im2.mode, "L") + self.assert_image_equal(im, im2) def test_with_underscores(self): kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} filename = self.tempfile("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 + self.assertEqual(im.tag[X_RESOLUTION][0][0], 72) + self.assertEqual(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 + self.assertEqual(im.tag_v2[X_RESOLUTION], 72) + self.assertEqual(im.tag_v2[Y_RESOLUTION], 36) def test_roundtrip_tiff_uint16(self): # Test an image of all '0' values pixel_value = 0x1234 infile = "Tests/images/uint16_1_4660.tif" - im = Image.open(infile) - self.assertEqual(im.getpixel((0, 0)), pixel_value) + with Image.open(infile) as im: + self.assertEqual(im.getpixel((0, 0)), pixel_value) - tmpfile = self.tempfile("temp.tif") - im.save(tmpfile) + tmpfile = self.tempfile("temp.tif") + im.save(tmpfile) - reloaded = Image.open(tmpfile) - - self.assert_image_equal(im, reloaded) + with Image.open(tmpfile) as reloaded: + self.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: + self.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: + self.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: + self.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) - - self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + with Image.open(infile) as im: + self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") def test_palette(self): for mode in ["P", "PA"]: @@ -509,14 +504,11 @@ class TestFileTiff(PillowTestCase): 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: + self.assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) def test_tiff_save_all(self): - import io - import os - - mp = io.BytesIO() + mp = BytesIO() with Image.open("Tests/images/multipage.tiff") as im: im.save(mp, format="tiff", save_all=True) @@ -525,26 +517,25 @@ class TestFileTiff(PillowTestCase): self.assertEqual(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: + self.assertEqual(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: + self.assertEqual(reread.n_frames, 3) def test_saving_icc_profile(self): # Tests saving TIFF with icc_profile set. @@ -557,9 +548,8 @@ class TestFileTiff(PillowTestCase): # Try save-load round trip to make sure both handle icc_profile. tmpfile = self.tempfile("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: + self.assertEqual(b"Dummy value", reloaded.info["icc_profile"]) def test_close_on_load_exclusive(self): # similar to test_fd_leak, but runs on unixlike os @@ -587,36 +577,19 @@ class TestFileTiff(PillowTestCase): im.load() self.assertFalse(fp.closed) - @unittest.skipUnless(features.check("libtiff"), "libtiff not installed") - 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 = BytesIO( - base64.b64decode( - b"SUkqAAgAAAAPAP4ABAABAAAAAAAAAAABBAABAAAAAQAAAAEBBAABAAAAAQAA" - b"AAIBAwADAAAAwgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAA" - b"4AAAABUBAwABAAAAAwAAABYBBAABAAAAAQAAABcBBAABAAAACwAAABoBBQAB" - b"AAAAyAAAABsBBQABAAAA0AAAABwBAwABAAAAAQAAACgBAwABAAAAAQAAAFMB" - b"AwADAAAA2AAAAAAAAAAIAAgACAABAAAAAQAAAAEAAAABAAAAAQABAAEAAAB4" - b"nGNgYAAAAAMAAQ==" - ) - ) - out = BytesIO() - with Image.open(tiff) as im: - im.save(out, format="tiff") - out.seek(0) - with Image.open(out) as im: - im.load() + # 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): + Image.open("Tests/images/string_dimension.tiff") -@unittest.skipUnless(sys.platform.startswith("win32"), "Windows only") +@unittest.skipUnless(is_win32(), "Windows only") class TestFileTiffW32(PillowTestCase): def test_fd_leak(self): tmpfile = self.tempfile("temp.tif") - import os # this is an mmaped file. with Image.open("Tests/images/uint16_1_4660.tif") as im: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index fcff8f056..035a4fcc1 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -52,89 +52,92 @@ class TestFileTiffMetadata(PillowTestCase): img.save(f, tiffinfo=info) - loaded = Image.open(f) + with Image.open(f) as loaded: - self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (len(bindata),)) + 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[ImageJMetaData], bindata) + self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata) - self.assertEqual(loaded.tag[ImageDescription], (reloaded_textdata,)) - self.assertEqual(loaded.tag_v2[ImageDescription], reloaded_textdata) + 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) + 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) + with Image.open(f) as loaded: - self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (8, len(bindata) - 8)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (8, len(bindata) - 8)) + 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") + with Image.open("Tests/images/hopper_g4.tif") as img: - self.assertEqual( - { - "YResolution": IFDRational(4294967295, 113653537), - "PlanarConfiguration": 1, - "BitsPerSample": (1,), - "ImageLength": 128, - "Compression": 4, - "FillOrder": 1, - "RowsPerStrip": 128, - "ResolutionUnit": 3, - "PhotometricInterpretation": 0, - "PageNumber": (0, 1), - "XResolution": IFDRational(4294967295, 113653537), - "ImageWidth": 128, - "Orientation": 1, - "StripByteCounts": (1968,), - "SamplesPerPixel": 1, - "StripOffsets": (8,), - }, - img.tag_v2.named(), - ) + 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(), - ) + 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") + with Image.open("Tests/images/hopper.tif") as img: + f = self.tempfile("temp.tiff") + img.save(f, tiffinfo=img.tag) - f = self.tempfile("temp.tiff") - img.save(f, tiffinfo=img.tag) + original = img.tag_v2.named() - loaded = Image.open(f) - - original = img.tag_v2.named() - reloaded = loaded.tag_v2.named() + with Image.open(f) as loaded: + reloaded = loaded.tag_v2.named() ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] @@ -149,13 +152,13 @@ class TestFileTiffMetadata(PillowTestCase): self.assert_deep_equal( original[tag], value, - "%s didn't roundtrip, %s, %s" % (tag, original[tag], value), + "{} didn't roundtrip, {}, {}".format(tag, original[tag], value), ) else: self.assertEqual( original[tag], value, - "%s didn't roundtrip, %s, %s" % (tag, original[tag], value), + "{} didn't roundtrip, {}, {}".format(tag, original[tag], value), ) for tag, value in original.items(): @@ -175,32 +178,32 @@ class TestFileTiffMetadata(PillowTestCase): 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("Tests/images/hopper.iccprofile.tif") as im: + im.save(out) - im.save(out) - reloaded = Image.open(out) - self.assertNotIsInstance(im.info["icc_profile"], tuple) - self.assertEqual(im.info["icc_profile"], reloaded.info["icc_profile"]) + with Image.open(out) as reloaded: + self.assertNotIsInstance(im.info["icc_profile"], tuple) + self.assertEqual(im.info["icc_profile"], reloaded.info["icc_profile"]) def test_iccprofile_binary(self): # https://github.com/python-pillow/Pillow/issues/1526 # We should be able to load this, # but probably won't be able to save it. - im = Image.open("Tests/images/hopper.iccprofile_binary.tif") - self.assertEqual(im.tag_v2.tagtype[34675], 1) - self.assertTrue(im.info["icc_profile"]) + with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + self.assertEqual(im.tag_v2.tagtype[34675], 1) + self.assertTrue(im.info["icc_profile"]) def test_iccprofile_save_png(self): - im = Image.open("Tests/images/hopper.iccprofile.tif") - outfile = self.tempfile("temp.png") - im.save(outfile) + with Image.open("Tests/images/hopper.iccprofile.tif") as im: + outfile = self.tempfile("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") - im.save(outfile) + with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + outfile = self.tempfile("temp.png") + im.save(outfile) def test_exif_div_zero(self): im = hopper() @@ -210,9 +213,9 @@ class TestFileTiffMetadata(PillowTestCase): 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) + with Image.open(out) as reloaded: + self.assertEqual(0, reloaded.tag_v2[41988].numerator) + self.assertEqual(0, reloaded.tag_v2[41988].denominator) def test_ifd_unsigned_rational(self): im = hopper() @@ -301,13 +304,14 @@ class TestFileTiffMetadata(PillowTestCase): self.assertIn(33432, info) def test_PhotoshopInfo(self): - im = Image.open("Tests/images/issue_2278.tif") - - self.assertIsInstance(im.tag_v2[34377], bytes) - out = self.tempfile("temp.tiff") - im.save(out) - reloaded = Image.open(out) - self.assertIsInstance(reloaded.tag_v2[34377], bytes) + with Image.open("Tests/images/issue_2278.tif") as im: + self.assertEqual(len(im.tag_v2[34377]), 1) + self.assertIsInstance(im.tag_v2[34377][0], bytes) + out = self.tempfile("temp.tiff") + im.save(out) + with Image.open(out) as reloaded: + self.assertEqual(len(reloaded.tag_v2[34377]), 1) + self.assertIsInstance(reloaded.tag_v2[34377][0], bytes) def test_too_many_entries(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 4d44f47b6..00028edb4 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image, WebPImagePlugin -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper try: from PIL import _webp @@ -24,7 +26,7 @@ class TestUnsupportedWebp(PillowTestCase): WebPImagePlugin.SUPPORTED = True -@unittest.skipIf(not HAVE_WEBP, "WebP support not installed") +@unittest.skipUnless(HAVE_WEBP, "WebP support not installed") class TestFileWebp(PillowTestCase): def setUp(self): self.rgb_mode = "RGB" @@ -39,19 +41,18 @@ class TestFileWebp(PillowTestCase): Does it have the bits we expect? """ - image = Image.open("Tests/images/hopper.webp") + with Image.open("Tests/images/hopper.webp") as image: + self.assertEqual(image.mode, self.rgb_mode) + self.assertEqual(image.size, (128, 128)) + self.assertEqual(image.format, "WEBP") + image.load() + image.getdata() - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") - image.load() - image.getdata() - - # generated with: - # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm - 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 + self.assert_image_similar_tofile( + image, "Tests/images/hopper_webp_bits.ppm", 1.0 + ) def test_write_rgb(self): """ @@ -62,26 +63,25 @@ class TestFileWebp(PillowTestCase): temp_file = self.tempfile("temp.webp") hopper(self.rgb_mode).save(temp_file) - image = Image.open(temp_file) + with Image.open(temp_file) as image: + self.assertEqual(image.mode, self.rgb_mode) + self.assertEqual(image.size, (128, 128)) + self.assertEqual(image.format, "WEBP") + image.load() + image.getdata() - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") - image.load() - image.getdata() + # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm + self.assert_image_similar_tofile( + image, "Tests/images/hopper_webp_write.ppm", 12.0 + ) - # 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) + # 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): """ @@ -91,17 +91,16 @@ class TestFileWebp(PillowTestCase): temp_file = self.tempfile("temp.webp") hopper("L").save(temp_file) - image = Image.open(temp_file) + with Image.open(temp_file) as image: + self.assertEqual(image.mode, self.rgb_mode) + self.assertEqual(image.size, (128, 128)) + self.assertEqual(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("L").convert(self.rgb_mode) - image.load() - image.getdata() - target = hopper("L").convert(self.rgb_mode) - - self.assert_image_similar(image, target, 10.0) + self.assert_image_similar(image, target, 10.0) def test_write_unsupported_mode_P(self): """ @@ -111,17 +110,16 @@ class TestFileWebp(PillowTestCase): temp_file = self.tempfile("temp.webp") hopper("P").save(temp_file) - image = Image.open(temp_file) + with Image.open(temp_file) as image: + self.assertEqual(image.mode, self.rgb_mode) + self.assertEqual(image.size, (128, 128)) + self.assertEqual(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) + self.assert_image_similar(image, target, 50.0) def test_WebPEncode_with_invalid_args(self): """ @@ -143,10 +141,9 @@ class TestFileWebp(PillowTestCase): def test_no_resource_warning(self): 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 = self.tempfile("temp.webp") + self.assert_warning(None, image.save, temp_file) def test_file_pointer_could_be_reused(self): file_path = "Tests/images/hopper.webp" @@ -158,19 +155,19 @@ class TestFileWebp(PillowTestCase): 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)) + 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 = self.tempfile("temp.webp") + im.save(out_webp, save_all=True) # Save as GIF out_gif = self.tempfile("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)] ) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index f2f10d7b7..85a1e3d2f 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper try: from PIL import _webp @@ -24,18 +26,17 @@ class TestFileWebpAlpha(PillowTestCase): # Generated with `cwebp transparent.png -o transparent.webp` file_path = "Tests/images/transparent.webp" - image = Image.open(file_path) + with Image.open(file_path) as image: + self.assertEqual(image.mode, "RGBA") + self.assertEqual(image.size, (200, 150)) + self.assertEqual(image.format, "WEBP") + image.load() + image.getdata() - self.assertEqual(image.mode, "RGBA") - self.assertEqual(image.size, (200, 150)) - self.assertEqual(image.format, "WEBP") - image.load() - image.getdata() + image.tobytes() - 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: + self.assert_image_similar(image, target, 20.0) def test_write_lossless_rgb(self): """ @@ -54,16 +55,16 @@ class TestFileWebpAlpha(PillowTestCase): pil_image.save(temp_file, lossless=True) - image = Image.open(temp_file) - image.load() + 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") - image.load() - image.getdata() + self.assertEqual(image.mode, "RGBA") + self.assertEqual(image.size, pil_image.size) + self.assertEqual(image.format, "WEBP") + image.load() + image.getdata() - self.assert_image_equal(image, pil_image) + self.assert_image_equal(image, pil_image) def test_write_rgba(self): """ @@ -79,21 +80,21 @@ class TestFileWebpAlpha(PillowTestCase): if _webp.WebPDecoderBuggyAlpha(self): return - image = Image.open(temp_file) - image.load() + 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") - image.load() - image.getdata() + self.assertEqual(image.mode, "RGBA") + self.assertEqual(image.size, (10, 10)) + self.assertEqual(image.format, "WEBP") + image.load() + image.getdata() - # 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) - else: - self.assert_image_similar(image, pil_image, 1.0) + # 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) + else: + self.assert_image_similar(image, pil_image, 1.0) def test_write_unsupported_mode_PA(self): """ @@ -103,15 +104,16 @@ class TestFileWebpAlpha(PillowTestCase): temp_file = self.tempfile("temp.webp") file_path = "Tests/images/transparent.gif" - Image.open(file_path).save(temp_file) - image = Image.open(temp_file) + with Image.open(file_path) as im: + im.save(temp_file) + with Image.open(temp_file) as image: + self.assertEqual(image.mode, "RGBA") + self.assertEqual(image.size, (200, 150)) + self.assertEqual(image.format, "WEBP") - self.assertEqual(image.mode, "RGBA") - self.assertEqual(image.size, (200, 150)) - self.assertEqual(image.format, "WEBP") + image.load() + image.getdata() + with Image.open(file_path) as im: + target = im.convert("RGBA") - image.load() - image.getdata() - target = Image.open(file_path).convert("RGBA") - - self.assert_image_similar(image, target, 25.0) + self.assert_image_similar(image, target, 25.0) diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index dec74d0d0..bf425d079 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -28,13 +28,13 @@ class TestFileWebpAnimation(PillowTestCase): attributes correctly. """ - im = Image.open("Tests/images/hopper.webp") - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) + with Image.open("Tests/images/hopper.webp") as im: + self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) - im = Image.open("Tests/images/iss634.webp") - self.assertEqual(im.n_frames, 42) - self.assertTrue(im.is_animated) + with Image.open("Tests/images/iss634.webp") as im: + self.assertEqual(im.n_frames, 42) + self.assertTrue(im.is_animated) def test_write_animation_L(self): """ @@ -43,23 +43,23 @@ class TestFileWebpAnimation(PillowTestCase): visually similar. """ - orig = Image.open("Tests/images/iss634.gif") - self.assertGreater(orig.n_frames, 1) + with Image.open("Tests/images/iss634.gif") as orig: + self.assertGreater(orig.n_frames, 1) - temp_file = self.tempfile("temp.webp") - orig.save(temp_file, save_all=True) - im = Image.open(temp_file) - self.assertEqual(im.n_frames, orig.n_frames) + temp_file = self.tempfile("temp.webp") + orig.save(temp_file, save_all=True) + with Image.open(temp_file) as im: + 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) + # 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): """ @@ -68,40 +68,38 @@ class TestFileWebpAnimation(PillowTestCase): """ def check(temp_file): - im = Image.open(temp_file) - self.assertEqual(im.n_frames, 2) + with Image.open(temp_file) as im: + self.assertEqual(im.n_frames, 2) - # Compare first frame to original - im.load() - self.assert_image_equal(im, frame1.convert("RGBA")) + # Compare first frame to original + im.load() + self.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")) + # 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") + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + temp_file1 = self.tempfile("temp.webp") + frame1.copy().save( + temp_file1, save_all=True, append_images=[frame2], lossless=True + ) + check(temp_file1) - temp_file1 = self.tempfile("temp.webp") - frame1.copy().save( - temp_file1, save_all=True, append_images=[frame2], lossless=True - ) - check(temp_file1) + # Tests appending using a generator + def imGenerator(ims): + yield from ims - # Tests appending using a generator - def imGenerator(ims): - 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) + 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): """ @@ -111,27 +109,27 @@ class TestFileWebpAnimation(PillowTestCase): 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, - ) + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2, frame1], + duration=durations, + ) - im = Image.open(temp_file) - self.assertEqual(im.n_frames, 5) - self.assertTrue(im.is_animated) + with Image.open(temp_file) as im: + self.assertEqual(im.n_frames, 5) + self.assertTrue(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) - ts += durations[frame] + # 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) + ts += durations[frame] def test_seeking(self): """ @@ -142,24 +140,24 @@ class TestFileWebpAnimation(PillowTestCase): 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, - ) + 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, + ) - im = Image.open(temp_file) - self.assertEqual(im.n_frames, 5) - self.assertTrue(im.is_animated) + with Image.open(temp_file) as im: + self.assertEqual(im.n_frames, 5) + self.assertTrue(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) - ts -= dur + # 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) + ts -= dur diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 2eff41529..5d184a766 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -26,13 +26,13 @@ class TestFileWebpLossless(PillowTestCase): hopper(self.rgb_mode).save(temp_file, lossless=True) - image = Image.open(temp_file) - image.load() + 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") - image.load() - image.getdata() + self.assertEqual(image.mode, self.rgb_mode) + self.assertEqual(image.size, (128, 128)) + self.assertEqual(image.format, "WEBP") + image.load() + image.getdata() - self.assert_image_equal(image, hopper(self.rgb_mode)) + self.assert_image_equal(image, hopper(self.rgb_mode)) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index ae528e3bf..06c780299 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,3 +1,5 @@ +from io import BytesIO + from PIL import Image from .helper import PillowTestCase @@ -22,37 +24,33 @@ class TestFileWebpMetadata(PillowTestCase): def test_read_exif_metadata(self): file_path = "Tests/images/flower.webp" - image = Image.open(file_path) + with Image.open(file_path) as image: - self.assertEqual(image.format, "WEBP") - exif_data = image.info.get("exif", None) - self.assertTrue(exif_data) + self.assertEqual(image.format, "WEBP") + exif_data = image.info.get("exif", None) + self.assertTrue(exif_data) - exif = image._getexif() + exif = image._getexif() - # camera make - self.assertEqual(exif[271], "Canon") + # camera make + self.assertEqual(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) + self.assertEqual(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) - expected_exif = image.info["exif"] - test_buffer = BytesIO() + with Image.open(file_path) as image: + expected_exif = image.info["exif"] - image.save(test_buffer, "webp", exif=expected_exif) + image.save(test_buffer, "webp", exif=expected_exif) test_buffer.seek(0) - webp_image = Image.open(test_buffer) - - webp_exif = webp_image.info.get("exif", None) + 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") @@ -60,33 +58,29 @@ class TestFileWebpMetadata(PillowTestCase): def test_read_icc_profile(self): file_path = "Tests/images/flower2.webp" - image = Image.open(file_path) + with Image.open(file_path) as image: - self.assertEqual(image.format, "WEBP") - self.assertTrue(image.info.get("icc_profile", None)) + self.assertEqual(image.format, "WEBP") + self.assertTrue(image.info.get("icc_profile", None)) - icc = image.info["icc_profile"] + 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) + self.assertEqual(icc, expected_icc) def test_write_icc_metadata(self): - from io import BytesIO - file_path = "Tests/images/flower2.jpg" - image = Image.open(file_path) - expected_icc_profile = image.info["icc_profile"] - test_buffer = BytesIO() + with Image.open(file_path) as image: + expected_icc_profile = image.info["icc_profile"] - image.save(test_buffer, "webp", icc_profile=expected_icc_profile) + image.save(test_buffer, "webp", icc_profile=expected_icc_profile) test_buffer.seek(0) - webp_image = Image.open(test_buffer) - - webp_icc_profile = webp_image.info.get("icc_profile", None) + 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: @@ -95,45 +89,41 @@ class TestFileWebpMetadata(PillowTestCase): ) def test_read_no_exif(self): - from io import BytesIO - file_path = "Tests/images/flower.jpg" - image = Image.open(file_path) - self.assertIn("exif", image.info) - test_buffer = BytesIO() + with Image.open(file_path) as image: + self.assertIn("exif", image.info) - image.save(test_buffer, "webp") + image.save(test_buffer, "webp") test_buffer.seek(0) - webp_image = Image.open(test_buffer) - - self.assertFalse(webp_image._getexif()) + with Image.open(test_buffer) as webp_image: + 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") + iccp_data = b"" + exif_data = b"" + xmp_data = b"" temp_file = self.tempfile("temp.webp") - frame1 = Image.open("Tests/images/anim_frame1.webp") - frame2 = Image.open("Tests/images/anim_frame2.webp") - frame1.save( - temp_file, - save_all=True, - append_images=[frame2, frame1, frame2], - icc_profile=iccp_data, - exif=exif_data, - xmp=xmp_data, - ) + with Image.open("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, + ) - 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)) + with Image.open(temp_file) as image: + self.assertIn("icc_profile", image.info) + self.assertIn("exif", image.info) + self.assertIn("xmp", image.info) + self.assertEqual(iccp_data, image.info.get("icc_profile", None)) + self.assertEqual(exif_data, image.info.get("exif", None)) + self.assertEqual(xmp_data, image.info.get("xmp", None)) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index cea0cec5b..0aa36c33c 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -7,24 +7,24 @@ class TestFileWmf(PillowTestCase): def test_load_raw(self): # Test basic EMF open and rendering - im = Image.open("Tests/images/drawing.emf") - 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") as im: + if hasattr(Image.core, "drawwmf"): + # Currently, support for WMF/EMF is Windows-only + im.load() + # Compare to reference rendering + with Image.open("Tests/images/drawing_emf_ref.png") as imref: + imref.load() + self.assert_image_similar(im, imref, 0) # Test basic WMF open and rendering - im = Image.open("Tests/images/drawing.wmf") - 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") as im: + if hasattr(Image.core, "drawwmf"): + # Currently, support for WMF/EMF is Windows-only + im.load() + # Compare to reference rendering + with Image.open("Tests/images/drawing_wmf_ref.png") as imref: + imref.load() + self.assert_image_similar(im, imref, 2.0) def test_register_handler(self): class TestHandler: @@ -46,12 +46,23 @@ class TestFileWmf(PillowTestCase): def test_load_dpi_rounding(self): # Round up - im = Image.open("Tests/images/drawing.emf") - self.assertEqual(im.info["dpi"], 1424) + with Image.open("Tests/images/drawing.emf") as im: + self.assertEqual(im.info["dpi"], 1424) # Round down - im = Image.open("Tests/images/drawing_roundDown.emf") - self.assertEqual(im.info["dpi"], 1426) + with Image.open("Tests/images/drawing_roundDown.emf") as im: + self.assertEqual(im.info["dpi"], 1426) + + def test_load_set_dpi(self): + with Image.open("Tests/images/drawing.wmf") as im: + self.assertEquals(im.size, (82, 82)) + + if hasattr(Image.core, "drawwmf"): + im.load(144) + self.assertEquals(im.size, (164, 164)) + + with Image.open("Tests/images/drawing_wmf_ref_144.png") as expected: + self.assert_image_similar(im, expected, 2.0) def test_save(self): im = hopper() diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 9693ba05a..972c5abe0 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,3 +1,5 @@ +from io import BytesIO + from PIL import Image from .helper import PillowTestCase @@ -28,13 +30,10 @@ static char basic_bits[] = { class TestFileXbm(PillowTestCase): def test_pil151(self): - from io import BytesIO - - im = Image.open(BytesIO(PIL151)) - - im.load() - self.assertEqual(im.mode, "1") - self.assertEqual(im.size, (32, 32)) + with Image.open(BytesIO(PIL151)) as im: + im.load() + self.assertEqual(im.mode, "1") + self.assertEqual(im.size, (32, 32)) def test_open(self): # Arrange @@ -42,11 +41,11 @@ class TestFileXbm(PillowTestCase): filename = "Tests/images/hopper.xbm" # Act - im = Image.open(filename) + with Image.open(filename) as im: - # Assert - self.assertEqual(im.mode, "1") - self.assertEqual(im.size, (128, 128)) + # Assert + self.assertEqual(im.mode, "1") + self.assertEqual(im.size, (128, 128)) def test_open_filename_with_underscore(self): # Arrange @@ -54,8 +53,8 @@ class TestFileXbm(PillowTestCase): filename = "Tests/images/hopper_underscore.xbm" # Act - im = Image.open(filename) + with Image.open(filename) as im: - # Assert - self.assertEqual(im.mode, "1") - self.assertEqual(im.size, (128, 128)) + # Assert + self.assertEqual(im.mode, "1") + self.assertEqual(im.size, (128, 128)) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index a49b7c8dd..38ecbc3de 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -7,14 +7,14 @@ TEST_FILE = "Tests/images/hopper.xpm" class TestFileXpm(PillowTestCase): def test_sanity(self): - im = Image.open(TEST_FILE) - im.load() - self.assertEqual(im.mode, "P") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "XPM") + 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") - # large error due to quantization->44 colors. - self.assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) + # large error due to quantization->44 colors. + self.assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -23,11 +23,11 @@ class TestFileXpm(PillowTestCase): def test_load_read(self): # Arrange - im = Image.open(TEST_FILE) - dummy_bytes = 1 + with Image.open(TEST_FILE) as im: + dummy_bytes = 1 - # Act - data = im.load_read(dummy_bytes) + # Act + data = im.load_read(dummy_bytes) # Assert self.assertEqual(len(data), 16384) diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index f8b6d3531..3b63ab36f 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -8,14 +8,14 @@ TEST_FILE = "Tests/images/hopper.p7" class TestFileXVThumb(PillowTestCase): def test_open(self): # Act - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Assert - self.assertEqual(im.format, "XVThumb") + # Assert + self.assertEqual(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) + # Create a Hopper image with a similar XV palette + im_hopper = hopper().quantize(palette=im) + self.assert_image_similar(im, im_hopper, 9) def test_unexpected_eof(self): # Test unexpected EOF reading XV thumbnail file diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 14b368585..be0612fa2 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,13 +1,10 @@ -from __future__ import division - -import sys +import unittest from PIL import Image, ImageDraw, ImageFont, features -from .helper import PillowLeakTestCase, unittest +from .helper import PillowLeakTestCase -@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") class TestTTypeFontLeak(PillowLeakTestCase): # fails at iteration 3 in master iterations = 10 @@ -22,7 +19,7 @@ class TestTTypeFontLeak(PillowLeakTestCase): ) ) - @unittest.skipIf(not features.check("freetype2"), "Test requires freetype2") + @unittest.skipUnless(features.check("freetype2"), "Test requires 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 a2b4ef27e..e37f43207 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,5 +1,4 @@ from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile -from PIL._util import py3 from .helper import PillowTestCase @@ -74,6 +73,5 @@ class TestFontPcf(PillowTestCase): 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")) + # accept bytes instances. + self._test_high_characters(message.encode("latin1")) diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index ce0524e1e..e80e16ffe 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -2,7 +2,6 @@ import colorsys import itertools from PIL import Image -from PIL._util import py3 from .helper import PillowTestCase, hopper @@ -49,27 +48,18 @@ class TestFormatHSV(PillowTestCase): (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 + conv_func = self.int_to_float 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()) + for (_r, _g, _b) in itertools.zip_longest( + 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) + 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) diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index a98c20579..08e9ad068 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -5,21 +5,21 @@ from .helper import PillowTestCase class TestFormatLab(PillowTestCase): def test_white(self): - i = Image.open("Tests/images/lab.tif") + with Image.open("Tests/images/lab.tif") as i: + i.load() - i.load() + self.assertEqual(i.mode, "LAB") - self.assertEqual(i.mode, "LAB") + self.assertEqual(i.getbands(), ("L", "A", "B")) - self.assertEqual(i.getbands(), ("L", "A", "B")) + k = i.getpixel((0, 0)) + + L = i.getdata(0) + a = i.getdata(1) + b = i.getdata(2) - 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) @@ -27,15 +27,13 @@ class TestFormatLab(PillowTestCase): 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") - - k = i.getpixel((0, 0)) + with Image.open("Tests/images/lab-green.tif") as i: + k = i.getpixel((0, 0)) self.assertEqual(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") - - k = i.getpixel((0, 0)) + with Image.open("Tests/images/lab-red.tif") as i: + k = i.getpixel((0, 0)) self.assertEqual(k, (128, 228, 128)) diff --git a/Tests/test_image.py b/Tests/test_image.py index 493b4735a..47e7420ef 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,11 +1,12 @@ +import io import os import shutil -import sys +import tempfile +import unittest -from PIL import Image -from PIL._util import py3 +from PIL import Image, UnidentifiedImageError -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper, is_win32 class TestImage(PillowTestCase): @@ -48,6 +49,9 @@ class TestImage(PillowTestCase): Image.new(mode, (1, 1)) self.assertEqual(str(e.exception), "unrecognized image mode") + def test_exception_inheritance(self): + self.assertTrue(issubclass(UnidentifiedImageError, IOError)) + def test_sanity(self): im = Image.new("L", (100, 100)) @@ -80,40 +84,37 @@ class TestImage(PillowTestCase): im.size = (3, 4) def test_invalid_image(self): - if py3: - import io + import io - im = io.BytesIO(b"") - else: - import StringIO - - im = StringIO.StringIO("") - self.assertRaises(IOError, Image.open, im) + im = io.BytesIO(b"") + self.assertRaises(UnidentifiedImageError, Image.open, im) def test_bad_mode(self): self.assertRaises(ValueError, Image.open, "filename", "bad mode") - @unittest.skipUnless(Image.HAS_PATHLIB, "requires pathlib/pathlib2") + def test_stringio(self): + self.assertRaises(ValueError, Image.open, io.StringIO()) + def test_pathlib(self): from PIL.Image import Path - im = Image.open(Path("Tests/images/multipage-mmap.tiff")) - self.assertEqual(im.mode, "P") - self.assertEqual(im.size, (10, 10)) + with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: + self.assertEqual(im.mode, "P") + self.assertEqual(im.size, (10, 10)) - im = Image.open(Path("Tests/images/hopper.jpg")) - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) + with Image.open(Path("Tests/images/hopper.jpg")) as im: + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) - temp_file = self.tempfile("temp.jpg") - if os.path.exists(temp_file): - os.remove(temp_file) - im.save(Path(temp_file)) + temp_file = self.tempfile("temp.jpg") + if os.path.exists(temp_file): + os.remove(temp_file) + im.save(Path(temp_file)) def test_fp_name(self): temp_file = self.tempfile("temp.jpg") - class FP(object): + class FP: def write(a, b): pass @@ -126,14 +127,12 @@ class TestImage(PillowTestCase): def test_tempfile(self): # see #1460, pathlib support breaks tempfile.TemporaryFile on py27 # Will error out on save on 3.0.0 - import tempfile - im = hopper() with tempfile.TemporaryFile() as fp: im.save(fp, "JPEG") fp.seek(0) - reloaded = Image.open(fp) - self.assert_image_similar(im, reloaded, 20) + with Image.open(fp) as reloaded: + self.assert_image_similar(im, reloaded, 20) def test_unknown_extension(self): im = hopper() @@ -150,16 +149,14 @@ class TestImage(PillowTestCase): im.paste(0, (0, 0, 100, 100)) self.assertFalse(im.readonly) - @unittest.skipIf( - sys.platform.startswith("win32"), "Test requires opening tempfile twice" - ) + @unittest.skipIf(is_win32(), "Test requires opening tempfile twice") def test_readonly_save(self): temp_file = self.tempfile("temp.bmp") shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) - im = Image.open(temp_file) - self.assertTrue(im.readonly) - im.save(temp_file) + with Image.open(temp_file) as im: + self.assertTrue(im.readonly) + im.save(temp_file) def test_dump(self): im = Image.new("L", (10, 10)) @@ -350,7 +347,8 @@ class TestImage(PillowTestCase): def test_registered_extensions(self): # Arrange # Open an image to trigger plugin registration - Image.open("Tests/images/rgb.jpg") + with Image.open("Tests/images/rgb.jpg"): + pass # Act extensions = Image.registered_extensions() @@ -371,8 +369,8 @@ class TestImage(PillowTestCase): # Assert self.assertEqual(im.size, (512, 512)) - im2 = Image.open("Tests/images/effect_mandelbrot.png") - self.assert_image_equal(im, im2) + with Image.open("Tests/images/effect_mandelbrot.png") as im2: + self.assert_image_equal(im, im2) def test_effect_mandelbrot_bad_arguments(self): # Arrange @@ -413,8 +411,8 @@ class TestImage(PillowTestCase): # Assert self.assertEqual(im.size, (128, 128)) - im3 = Image.open("Tests/images/effect_spread.png") - self.assert_image_similar(im2, im3, 110) + with Image.open("Tests/images/effect_spread.png") as im3: + self.assert_image_similar(im2, im3, 110) def test_check_size(self): # Checking that the _check_size function throws value errors @@ -452,10 +450,10 @@ class TestImage(PillowTestCase): def test_offset_not_implemented(self): # Arrange - im = hopper() + with hopper() as im: - # Act / Assert - self.assertRaises(NotImplementedError, im.offset, None) + # Act / Assert + self.assertRaises(NotImplementedError, im.offset, None) def test_fromstring(self): self.assertRaises(NotImplementedError, Image.fromstring) @@ -481,7 +479,8 @@ class TestImage(PillowTestCase): self.assertEqual(im.mode, mode) self.assertEqual(im.getpixel((0, 0)), 0) self.assertEqual(im.getpixel((255, 255)), 255) - target = Image.open(target_file).convert(mode) + with Image.open(target_file) as target: + target = target.convert(mode) self.assert_image_equal(im, target) def test_radial_gradient_wrong_mode(self): @@ -505,7 +504,8 @@ class TestImage(PillowTestCase): self.assertEqual(im.mode, mode) self.assertEqual(im.getpixel((0, 0)), 255) self.assertEqual(im.getpixel((128, 128)), 0) - target = Image.open(target_file).convert(mode) + with Image.open(target_file) as target: + target = target.convert(mode) self.assert_image_equal(im, target) def test_register_extensions(self): @@ -526,8 +526,8 @@ class TestImage(PillowTestCase): def test_remap_palette(self): # Test illegal image mode - im = hopper() - self.assertRaises(ValueError, im.remap_palette, None) + with hopper() as im: + self.assertRaises(ValueError, im.remap_palette, None) def test__new(self): from PIL import ImagePalette @@ -589,8 +589,17 @@ class TestImage(PillowTestCase): self.assertFalse(fp.closed) + def test_overrun(self): + for file in ["fli_overrun.bin", "sgi_overrun.bin", "pcx_overrun.bin"]: + with Image.open(os.path.join("Tests/images", file)) as im: + try: + im.load() + self.assertFail() + except OSError as e: + self.assertEqual(str(e), "buffer overrun when reading image file") -class MockEncoder(object): + +class MockEncoder: pass diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index b06814cb9..57bac753d 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 +import unittest +from distutils import ccompiler, sysconfig from PIL import Image -from .helper import PillowTestCase, hopper, on_appveyor, unittest +from .helper import PillowTestCase, hopper, is_win32, on_ci # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -122,7 +126,7 @@ class TestImageGetPixel(AccessTest): self.assertEqual( im.getpixel((0, 0)), c, - "put/getpixel roundtrip failed for mode %s, color %s" % (mode, c), + "put/getpixel roundtrip failed for mode {}, color {}".format(mode, c), ) # check putpixel negative index @@ -151,7 +155,7 @@ class TestImageGetPixel(AccessTest): self.assertEqual( im.getpixel((0, 0)), c, - "initial color failed for mode %s, color %s " % (mode, c), + "initial color failed for mode {}, color {} ".format(mode, c), ) # check initial color negative index self.assertEqual( @@ -333,14 +337,10 @@ class TestCffi(AccessTest): class TestEmbeddable(unittest.TestCase): @unittest.skipIf( - not sys.platform.startswith("win32") or on_appveyor(), - "Failing on AppVeyor when run from subprocess, not from shell", + not is_win32() or on_ci(), + "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 +349,8 @@ class TestEmbeddable(unittest.TestCase): 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 +360,7 @@ int main(int argc, char* argv[]) Py_DECREF(PyImport_ImportModule("PIL.Image")); Py_Finalize(); -#if PY_MAJOR_VERSION >= 3 PyMem_RawFree(whome); -#endif return 0; } diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 02e5c80f2..12c3b34cb 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -25,7 +25,7 @@ class TestImageArray(PillowTestCase): self.assertEqual(test("RGBX"), (3, (100, 128, 4), "|u1", 51200)) def test_fromarray(self): - class Wrapper(object): + class Wrapper: """ Class with API matching Image.fromarray """ def __init__(self, img, arr_params): @@ -52,3 +52,8 @@ class TestImageArray(PillowTestCase): self.assertEqual(test("RGB"), ("RGB", (128, 100), True)) self.assertEqual(test("RGBA"), ("RGBA", (128, 100), True)) self.assertEqual(test("RGBX"), ("RGBA", (128, 100), True)) + + # Test mode is None with no "typestr" in the array interface + with self.assertRaises(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 96ecf8996..5bbe3abfa 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -23,6 +23,7 @@ class TestImageConvert(PillowTestCase): "RGBX", "CMYK", "YCbCr", + "HSV", ) for mode in modes: @@ -52,16 +53,16 @@ class TestImageConvert(PillowTestCase): self.assertEqual(orig, converted) def test_8bit(self): - im = Image.open("Tests/images/hopper.jpg") - self._test_float_conversion(im.convert("L")) + with Image.open("Tests/images/hopper.jpg") as im: + self._test_float_conversion(im.convert("L")) def test_16bit(self): - im = Image.open("Tests/images/16bit.cropped.tif") - self._test_float_conversion(im) + with Image.open("Tests/images/16bit.cropped.tif") as im: + self._test_float_conversion(im) def test_16bit_workaround(self): - im = Image.open("Tests/images/16bit.cropped.tif") - self._test_float_conversion(im.convert("I")) + with Image.open("Tests/images/16bit.cropped.tif") as im: + self._test_float_conversion(im.convert("I")) def test_rgba_p(self): im = hopper("RGBA") @@ -143,11 +144,11 @@ class TestImageConvert(PillowTestCase): 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") - im.info["transparency"] = 255 - im.load() - self.assertEqual(im.palette.mode, "RGBA") - im_p = im.convert("P") + with Image.open("Tests/images/hopper.gif") as im: + im.info["transparency"] = 255 + im.load() + self.assertEqual(im.palette.mode, "RGBA") + im_p = im.convert("P") # Should not raise ValueError: unrecognized raw mode im_p.load() @@ -209,13 +210,13 @@ class TestImageConvert(PillowTestCase): # 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) + with Image.open("Tests/images/hopper-XYZ.png") as target: + if converted_im.mode == "RGB": + self.assert_image_similar(converted_im, target, 3) + self.assertEqual(converted_im.info["transparency"], (105, 54, 4)) + else: + self.assert_image_similar(converted_im, target.getchannel(0), 1) + self.assertEqual(converted_im.info["transparency"], 105) matrix_convert("RGB") matrix_convert("L") diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 6a604f494..480ab2b8e 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -76,13 +76,13 @@ class TestImageCrop(PillowTestCase): test_img = "Tests/images/bmp/g/pal8-0.bmp" extents = (1, 1, 10, 10) # works prepatch - img = Image.open(test_img) - img2 = img.crop(extents) + with Image.open(test_img) as img: + img2 = img.crop(extents) img2.load() # fail prepatch - img = Image.open(test_img) - img = img.crop(extents) + with Image.open(test_img) as img: + img = img.crop(extents) img.load() def test_crop_zero(self): diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 5d92ee797..a185b3a63 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -13,7 +13,11 @@ class TestImageDraft(PillowTestCase): im = Image.new(in_mode, in_size) data = tostring(im, "JPEG") im = fromstring(data) - im.draft(req_mode, req_size) + mode, box = im.draft(req_mode, req_size) + scale, _ = im.decoderconfig + self.assertEqual(box[:2], (0, 0)) + self.assertTrue((im.width - scale) < box[2] <= im.width) + self.assertTrue((im.height - scale) < box[3] <= im.height) return im def test_size(self): diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index bbd10e6e5..50d2ebbe8 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -99,45 +99,45 @@ class TestImageFilter(PillowTestCase): 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 + 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, + ) + 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)]), - ) + for mode in ["L", "LA", "RGB", "CMYK"]: + self.assert_image_equal( + Image.merge(mode, source[: len(mode)]).filter(kernel), + Image.merge(mode, reference[: len(mode)]), + ) def test_consistency_5x5(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 + 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"]: - self.assert_image_equal( - Image.merge(mode, source[: len(mode)]).filter(kernel), - Image.merge(mode, reference[: len(mode)]), - ) + for mode in ["L", "LA", "RGB", "CMYK"]: + self.assert_image_equal( + Image.merge(mode, source[: len(mode)]).filter(kernel), + Image.merge(mode, reference[: len(mode)]), + ) diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index d7556a680..0d961572c 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -5,12 +5,15 @@ from .test_imageqt import PillowQtTestCase class TestFromQImage(PillowQtTestCase, PillowTestCase): - - files_to_test = [ - hopper(), - Image.open("Tests/images/transparent.png"), - Image.open("Tests/images/7x13.png"), - ] + def setUp(self): + super().setUp() + self.files_to_test = [ + hopper(), + Image.open("Tests/images/transparent.png"), + Image.open("Tests/images/7x13.png"), + ] + for im in self.files_to_test: + self.addCleanup(im.close) def roundtrip(self, expected): # PIL -> Qt diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index d9bfcc7dd..18d381390 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,3 +1,5 @@ +from PIL import Image + from .helper import PillowTestCase, hopper @@ -13,7 +15,7 @@ class TestImageGetData(PillowTestCase): def test_roundtrip(self): def getdata(mode): - im = hopper(mode).resize((32, 30)) + im = hopper(mode).resize((32, 30), Image.NEAREST) data = im.getdata() return data[0], len(data), len(list(data)) diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 1944b041c..228eb82c8 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -19,7 +19,7 @@ class TestImageGetExtrema(PillowTestCase): self.assertEqual(extrema("I;16"), (0, 255)) def test_true_16(self): - im = Image.open("Tests/images/16_bit_noise.tif") - self.assertEqual(im.mode, "I;16") - extrema = im.getextrema() + with Image.open("Tests/images/16_bit_noise.tif") as im: + self.assertEqual(im.mode, "I;16") + extrema = im.getextrema() self.assertEqual(extrema, (106, 285)) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 3f0c46c46..f6908af4b 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,5 +1,3 @@ -from PIL._util import py3 - from .helper import PillowTestCase, hopper @@ -8,7 +6,5 @@ class TestImageGetIm(PillowTestCase): im = hopper() type_repr = repr(type(im.getim())) - if py3: - self.assertIn("PyCapsule", type_repr) - + self.assertIn("PyCapsule", type_repr) self.assertIsInstance(im.im.id, int) diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index e23957916..1d41d8609 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -6,8 +6,8 @@ from .helper import PillowTestCase, hopper class TestImageMode(PillowTestCase): def test_sanity(self): - im = hopper() - im.mode + with hopper() as im: + im.mode from PIL import ImageMode diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 2be5b74fc..ee5a09899 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -42,21 +42,24 @@ class TestImageQuantize(PillowTestCase): self.assertEqual(image.quantize().convert().mode, "RGBA") def test_quantize(self): - image = Image.open("Tests/images/caption_6_33_22.png").convert("RGB") + with Image.open("Tests/images/caption_6_33_22.png") as image: + image = image.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") + with Image.open("Tests/images/caption_6_33_22.png") as palette: + palette = palette.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") + 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) diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py new file mode 100644 index 000000000..d8f2ce1ec --- /dev/null +++ b/Tests/test_image_reduce.py @@ -0,0 +1,244 @@ +from PIL import Image, ImageMath, ImageMode + +from .helper import PillowTestCase, convert_to_comparable + + +class TestImageReduce(PillowTestCase): + # 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), + ] + + @classmethod + def setUpClass(cls): + cls.gradients_image = Image.open("Tests/images/radial_gradients.png") + cls.gradients_image.load() + + def test_args_factor(self): + im = Image.new("L", (10, 10)) + + self.assertEqual((4, 4), im.reduce(3).size) + self.assertEqual((4, 10), im.reduce((3, 1)).size) + self.assertEqual((10, 4), im.reduce((1, 3)).size) + + with self.assertRaises(ValueError): + im.reduce(0) + with self.assertRaises(TypeError): + im.reduce(2.0) + with self.assertRaises(ValueError): + im.reduce((0, 10)) + + def test_args_box(self): + im = Image.new("L", (10, 10)) + + self.assertEqual((5, 5), im.reduce(2, (0, 0, 10, 10)).size) + self.assertEqual((1, 1), im.reduce(2, (5, 5, 6, 6)).size) + + with self.assertRaises(TypeError): + im.reduce(2, "stri") + with self.assertRaises(TypeError): + im.reduce(2, 2) + with self.assertRaises(ValueError): + im.reduce(2, (0, 0, 11, 10)) + with self.assertRaises(ValueError): + im.reduce(2, (0, 0, 10, 11)) + with self.assertRaises(ValueError): + im.reduce(2, (-1, 0, 10, 10)) + with self.assertRaises(ValueError): + im.reduce(2, (0, -1, 10, 10)) + with self.assertRaises(ValueError): + im.reduce(2, (0, 5, 10, 5)) + with self.assertRaises(ValueError): + im.reduce(2, (5, 0, 5, 10)) + + def test_unsupported_modes(self): + im = Image.new("P", (10, 10)) + with self.assertRaises(ValueError): + im.reduce(3) + + im = Image.new("1", (10, 10)) + with self.assertRaises(ValueError): + im.reduce(3) + + im = Image.new("I;16", (10, 10)) + with self.assertRaises(ValueError): + im.reduce(3) + + def get_image(self, mode): + mode_info = ImageMode.getmode(mode) + if mode_info.basetype == "L": + bands = [self.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 = self.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(self, im, factor): + box = (11, 13, 146, 164) + reduced = im.reduce(factor, box=box) + reference = im.crop(box).reduce(factor) + self.assertEqual(reduced, reference) + + def compare_reduce_with_reference(self, 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]: + self.assertEqual(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]: + self.assertEqual(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) + + self.assert_compare_images(reduced, reference, average_diff, max_diff) + + def assert_compare_images(self, a, b, max_average_diff, max_diff=255): + self.assertEqual(a.mode, b.mode, "got mode %r, expected %r" % (a.mode, b.mode)) + self.assertEqual(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)) / float( + a.size[0] * a.size[1] + ) + self.assertGreaterEqual( + max_average_diff, + average_diff, + ( + "average pixel value difference {:.4f} > expected {:.4f} " + "for '{}' band" + ).format(average_diff, max_average_diff, band), + ) + + last_diff = [i for i, num in enumerate(ch_hist) if num > 0][-1] + self.assertGreaterEqual( + max_diff, + last_diff, + "max pixel value difference {} > expected {} for '{}' band".format( + last_diff, max_diff, band + ), + ) + + def test_mode_L(self): + im = self.get_image("L") + for factor in self.remarkable_factors: + self.compare_reduce_with_reference(im, factor) + self.compare_reduce_with_box(im, factor) + + def test_mode_LA(self): + im = self.get_image("LA") + for factor in self.remarkable_factors: + self.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 self.remarkable_factors: + self.compare_reduce_with_reference(im, factor) + self.compare_reduce_with_box(im, factor) + + def test_mode_La(self): + im = self.get_image("La") + for factor in self.remarkable_factors: + self.compare_reduce_with_reference(im, factor) + self.compare_reduce_with_box(im, factor) + + def test_mode_RGB(self): + im = self.get_image("RGB") + for factor in self.remarkable_factors: + self.compare_reduce_with_reference(im, factor) + self.compare_reduce_with_box(im, factor) + + def test_mode_RGBA(self): + im = self.get_image("RGBA") + for factor in self.remarkable_factors: + self.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 self.remarkable_factors: + self.compare_reduce_with_reference(im, factor) + self.compare_reduce_with_box(im, factor) + + def test_mode_RGBa(self): + im = self.get_image("RGBa") + for factor in self.remarkable_factors: + self.compare_reduce_with_reference(im, factor) + self.compare_reduce_with_box(im, factor) + + def test_mode_I(self): + im = self.get_image("I") + for factor in self.remarkable_factors: + self.compare_reduce_with_reference(im, factor) + self.compare_reduce_with_box(im, factor) + + def test_mode_F(self): + im = self.get_image("F") + for factor in self.remarkable_factors: + self.compare_reduce_with_reference(im, factor, 0, 0) + self.compare_reduce_with_box(im, factor) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 7d1dc009d..e94fbfe94 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,21 +1,24 @@ -from __future__ import division, print_function - +import unittest from contextlib import contextmanager from PIL import Image, ImageDraw -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper class TestImagingResampleVulnerability(PillowTestCase): # 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 self.assertRaises(MemoryError): + # any resampling filter will do here + im.im.resize((xsize, ysize), Image.BILINEAR) def test_invalid_size(self): im = hopper() @@ -209,6 +212,11 @@ class TestImagingCoreResampleAccuracy(PillowTestCase): for channel in case.split(): self.check_case(channel, self.make_sample(data, (12, 12))) + def test_box_filter_correct_range(self): + im = Image.new("RGB", (8, 8), "#1688ff").resize((100, 100), Image.BOX) + ref = Image.new("RGB", (100, 100), "#1688ff") + self.assert_image_equal(im, ref) + class CoreResampleConsistencyTest(PillowTestCase): def make_case(self, mode, fill): @@ -447,25 +455,25 @@ class CoreResampleBoxTest(PillowTestCase): return tiled def test_tiles(self): - im = Image.open("Tests/images/flower.jpg") - self.assertEqual(im.size, (480, 360)) - dst_size = (251, 188) - reference = im.resize(dst_size, Image.BICUBIC) + with Image.open("Tests/images/flower.jpg") as im: + self.assertEqual(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) + self.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: + 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_box = supersampled.resize(dst_size, Image.BICUBIC, (0, 0, 59.125, 44.125)) without_box = supersampled.resize(dst_size, Image.BICUBIC) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 7c35be570..fb1067e52 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -136,6 +136,93 @@ class TestImagingCoreResize(PillowTestCase): self.assertRaises(ValueError, self.resize, hopper(), (10, 10), 9) +class TestReducingGapResize(PillowTestCase): + @classmethod + def setUpClass(cls): + cls.gradients_image = Image.open("Tests/images/radial_gradients.png") + cls.gradients_image.load() + + def test_reducing_gap_values(self): + ref = self.gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=None) + im = self.gradients_image.resize((52, 34), Image.BICUBIC) + self.assert_image_equal(ref, im) + + with self.assertRaises(ValueError): + self.gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0) + + with self.assertRaises(ValueError): + self.gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0.99) + + def test_reducing_gap_1(self): + for box, epsilon in [ + (None, 4), + ((1.1, 2.2, 510.8, 510.9), 4), + ((3, 10, 410, 256), 10), + ]: + ref = self.gradients_image.resize((52, 34), Image.BICUBIC, box=box) + im = self.gradients_image.resize( + (52, 34), Image.BICUBIC, box=box, reducing_gap=1.0 + ) + + with self.assertRaises(AssertionError): + self.assert_image_equal(ref, im) + + self.assert_image_similar(ref, im, epsilon) + + def test_reducing_gap_2(self): + for box, epsilon in [ + (None, 1.5), + ((1.1, 2.2, 510.8, 510.9), 1.5), + ((3, 10, 410, 256), 1), + ]: + ref = self.gradients_image.resize((52, 34), Image.BICUBIC, box=box) + im = self.gradients_image.resize( + (52, 34), Image.BICUBIC, box=box, reducing_gap=2.0 + ) + + with self.assertRaises(AssertionError): + self.assert_image_equal(ref, im) + + self.assert_image_similar(ref, im, epsilon) + + def test_reducing_gap_3(self): + for box, epsilon in [ + (None, 1), + ((1.1, 2.2, 510.8, 510.9), 1), + ((3, 10, 410, 256), 0.5), + ]: + ref = self.gradients_image.resize((52, 34), Image.BICUBIC, box=box) + im = self.gradients_image.resize( + (52, 34), Image.BICUBIC, box=box, reducing_gap=3.0 + ) + + with self.assertRaises(AssertionError): + self.assert_image_equal(ref, im) + + self.assert_image_similar(ref, im, epsilon) + + def test_reducing_gap_8(self): + for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]: + ref = self.gradients_image.resize((52, 34), Image.BICUBIC, box=box) + im = self.gradients_image.resize( + (52, 34), Image.BICUBIC, box=box, reducing_gap=8.0 + ) + + self.assert_image_equal(ref, im) + + def test_box_filter(self): + for box, epsilon in [ + ((0, 0, 512, 512), 5.5), + ((0.9, 1.7, 128, 128), 9.5), + ]: + ref = self.gradients_image.resize((52, 34), Image.BOX, box=box) + im = self.gradients_image.resize( + (52, 34), Image.BOX, box=box, reducing_gap=1.0 + ) + + self.assert_image_similar(ref, im, epsilon) + + class TestImageResize(PillowTestCase): def test_resize(self): def resize(mode, size): @@ -148,5 +235,14 @@ class TestImageResize(PillowTestCase): resize(mode, (188, 214)) # Test unknown resampling filter - im = hopper() - self.assertRaises(ValueError, im.resize, (10, 10), "unknown") + with hopper() as im: + self.assertRaises(ValueError, im.resize, (10, 10), "unknown") + + def test_default_filter(self): + for mode in "L", "RGB", "I", "F": + im = hopper(mode) + self.assertEqual(im.resize((20, 20), Image.BICUBIC), im.resize((20, 20))) + + for mode in "1", "P": + im = hopper(mode) + self.assertEqual(im.resize((20, 20), Image.NEAREST), im.resize((20, 20))) diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 9c62e7362..b758531d4 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -24,8 +24,8 @@ class TestImageRotate(PillowTestCase): 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) + with Image.open("Tests/images/test-card.png") as im: + self.rotate(im, im.mode, angle) def test_zero(self): for angle in (0, 45, 90, 180, 270): @@ -38,43 +38,43 @@ class TestImageRotate(PillowTestCase): # >>> im = im.rotate(45, resample=Image.BICUBIC, expand=True) # >>> im.save('Tests/images/hopper_45.png') - target = Image.open("Tests/images/hopper_45.png") - for (resample, epsilon) in ( - (Image.NEAREST, 10), - (Image.BILINEAR, 5), - (Image.BICUBIC, 0), - ): - im = hopper() - im = im.rotate(45, resample=resample, expand=True) - self.assert_image_similar(im, target, epsilon) + with Image.open("Tests/images/hopper_45.png") as target: + for (resample, epsilon) in ( + (Image.NEAREST, 10), + (Image.BILINEAR, 5), + (Image.BICUBIC, 0), + ): + im = hopper() + im = im.rotate(45, resample=resample, expand=True) + self.assert_image_similar(im, target, epsilon) def test_center_0(self): im = hopper() - target = Image.open("Tests/images/hopper_45.png") - 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) + 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)) + self.assert_image_similar(im, target, 15) def test_center_14(self): im = hopper() - target = Image.open("Tests/images/hopper_45.png") - 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) - self.assert_image_similar(im, target, 10) + 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)) + + self.assert_image_similar(im, target, 10) def test_translate(self): im = hopper() - target = Image.open("Tests/images/hopper_45.png") - target_origin = (target.size[1] / 2 - 64) - 5 - target = target.crop( - (target_origin, target_origin, target_origin + 128, target_origin + 128) - ) + 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) @@ -102,15 +102,15 @@ class TestImageRotate(PillowTestCase): 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) + with Image.open("Tests/images/rotate_45_no_fill.png") as target: + 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) + with Image.open("Tests/images/rotate_45_with_fill.png") as target: + self.assert_image_equal(im, target) def test_alpha_rotate_no_fill(self): # Alpha images are handled differently internally diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index a19878aae..63918d073 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -53,8 +53,8 @@ class TestImageSplit(PillowTestCase): def split_open(mode): hopper(mode).save(test_file) - im = Image.open(test_file) - return len(im.split()) + with Image.open(test_file) as im: + return len(im.split()) self.assertEqual(split_open("1"), 1) self.assertEqual(split_open("L"), 1) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index bd7c98c28..58cebe1fb 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,49 +1,100 @@ from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import PillowTestCase, fromstring, hopper, tostring class TestImageThumbnail(PillowTestCase): def test_sanity(self): - im = hopper() - im.thumbnail((100, 100)) + self.assertIsNone(im.thumbnail((100, 100))) - self.assert_image(im, im.mode, (100, 100)) + self.assertEqual(im.size, (100, 100)) def test_aspect(self): - - im = hopper() + im = Image.new("L", (128, 128)) im.thumbnail((100, 100)) - self.assert_image(im, im.mode, (100, 100)) + self.assertEqual(im.size, (100, 100)) - im = hopper().resize((128, 256)) + im = Image.new("L", (128, 256)) im.thumbnail((100, 100)) - self.assert_image(im, im.mode, (50, 100)) + self.assertEqual(im.size, (50, 100)) - im = hopper().resize((128, 256)) + im = Image.new("L", (128, 256)) im.thumbnail((50, 100)) - self.assert_image(im, im.mode, (50, 100)) + self.assertEqual(im.size, (50, 100)) - im = hopper().resize((256, 128)) + im = Image.new("L", (256, 128)) im.thumbnail((100, 100)) - self.assert_image(im, im.mode, (100, 50)) + self.assertEqual(im.size, (100, 50)) - im = hopper().resize((256, 128)) + im = Image.new("L", (256, 128)) im.thumbnail((100, 50)) - self.assert_image(im, im.mode, (100, 50)) + self.assertEqual(im.size, (100, 50)) - im = hopper().resize((128, 128)) + im = Image.new("L", (128, 128)) im.thumbnail((100, 100)) - self.assert_image(im, im.mode, (100, 100)) + self.assertEqual(im.size, (100, 100)) + + im = Image.new("L", (256, 162)) # ratio is 1.5802469136 + im.thumbnail((33, 33)) + self.assertEqual(im.size, (33, 21)) # ratio is 1.5714285714 + + im = Image.new("L", (162, 256)) # ratio is 0.6328125 + im.thumbnail((33, 33)) + self.assertEqual(im.size, (21, 33)) # ratio is 0.6363636364 + + def test_float(self): + im = Image.new("L", (128, 128)) + im.thumbnail((99.9, 99.9)) + self.assertEqual(im.size, (100, 100)) def test_no_resize(self): # Check that draft() can resize the image to the destination size - im = Image.open("Tests/images/hopper.jpg") - im.draft(None, (64, 64)) - self.assertEqual(im.size, (64, 64)) + with Image.open("Tests/images/hopper.jpg") as im: + im.draft(None, (64, 64)) + self.assertEqual(im.size, (64, 64)) # Test thumbnail(), where only draft() is necessary to resize the image - im = Image.open("Tests/images/hopper.jpg") - im.thumbnail((64, 64)) - self.assert_image(im, im.mode, (64, 64)) + with Image.open("Tests/images/hopper.jpg") as im: + im.thumbnail((64, 64)) + self.assertEqual(im.size, (64, 64)) + + def test_DCT_scaling_edges(self): + # 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 + self.assert_image_similar(thumb, ref, 1.5) + + def test_reducing_gap_values(self): + 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 + self.assert_image_equal(ref, im) + + ref = hopper() + ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=None) + with self.assertRaises(AssertionError): + self.assert_image_equal(ref, im) + + self.assert_image_similar(ref, im, 3.5) + + def test_reducing_gap_for_DCT_scaling(self): + 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) + + self.assert_image_equal(ref, im) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index a0e54176a..781ba7923 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,14 +1,12 @@ import math -from PIL import Image +from PIL import Image, ImageTransform from .helper import PillowTestCase, hopper class TestImageTransform(PillowTestCase): def test_sanity(self): - from PIL import ImageTransform - im = Image.new("L", (100, 100)) seq = tuple(range(10)) @@ -22,6 +20,16 @@ class TestImageTransform(PillowTestCase): 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: + self.assertEqual(im.info["comment"], comment) + + transform = ImageTransform.ExtentTransform((0, 0, 0, 0)) + new_im = im.transform((100, 100), transform) + self.assertEqual(new_im.info["comment"], comment) + def test_extent(self): im = hopper("RGB") (w, h) = im.size @@ -156,21 +164,21 @@ class TestImageTransform(PillowTestCase): self.test_mesh() def test_missing_method_data(self): - im = hopper() - self.assertRaises(ValueError, im.transform, (100, 100), None) + with hopper() as im: + self.assertRaises(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"): + self.assertRaises( + ValueError, + im.transform, + (100, 100), + Image.EXTENT, + (0, 0, w, h), + resample, + ) class TestImageTransformAffine(PillowTestCase): diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 6f42a28df..ad1fee3e5 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -45,11 +45,11 @@ class TestImageChops(PillowTestCase): def test_add(self): # Arrange - im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") - im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + 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.add(im1, im2) + # Act + new = ImageChops.add(im1, im2) # Assert self.assertEqual(new.getbbox(), (25, 25, 76, 76)) @@ -57,11 +57,11 @@ class TestImageChops(PillowTestCase): 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") + 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.add(im1, im2, scale=2.5, offset=100) + # Act + new = ImageChops.add(im1, im2, scale=2.5, offset=100) # Assert self.assertEqual(new.getbbox(), (0, 0, 100, 100)) @@ -79,11 +79,11 @@ class TestImageChops(PillowTestCase): def test_add_modulo(self): # Arrange - im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") - im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + 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.add_modulo(im1, im2) + # Act + new = ImageChops.add_modulo(im1, im2) # Assert self.assertEqual(new.getbbox(), (25, 25, 76, 76)) @@ -101,11 +101,11 @@ class TestImageChops(PillowTestCase): def test_blend(self): # Arrange - im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") - im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act - new = ImageChops.blend(im1, im2, 0.5) + # Act + new = ImageChops.blend(im1, im2, 0.5) # Assert self.assertEqual(new.getbbox(), (25, 25, 76, 76)) @@ -125,33 +125,33 @@ class TestImageChops(PillowTestCase): 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") + 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) + # Act + new = ImageChops.darker(im1, im2) - # Assert - self.assert_image_equal(new, im2) + # Assert + self.assert_image_equal(new, im2) def test_darker_pixel(self): # Arrange im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act - new = ImageChops.darker(im1, im2) + # Act + new = ImageChops.darker(im1, im2) # Assert self.assertEqual(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") + 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: - # Act - new = ImageChops.difference(im1, im2) + # Act + new = ImageChops.difference(im1, im2) # Assert self.assertEqual(new.getbbox(), (25, 25, 76, 76)) @@ -159,10 +159,10 @@ class TestImageChops(PillowTestCase): def test_difference_pixel(self): # Arrange im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") + with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: - # Act - new = ImageChops.difference(im1, im2) + # Act + new = ImageChops.difference(im1, im2) # Assert self.assertEqual(new.getpixel((50, 50)), (240, 166, 128)) @@ -179,10 +179,10 @@ class TestImageChops(PillowTestCase): def test_invert(self): # Arrange - im = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: - # Act - new = ImageChops.invert(im) + # Act + new = ImageChops.invert(im) # Assert self.assertEqual(new.getbbox(), (0, 0, 100, 100)) @@ -191,11 +191,11 @@ class TestImageChops(PillowTestCase): 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") + 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) + # Act + new = ImageChops.lighter(im1, im2) # Assert self.assert_image_equal(new, im1) @@ -203,10 +203,10 @@ class TestImageChops(PillowTestCase): def test_lighter_pixel(self): # Arrange im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act - new = ImageChops.lighter(im1, im2) + # Act + new = ImageChops.lighter(im1, im2) # Assert self.assertEqual(new.getpixel((50, 50)), (255, 255, 127)) @@ -226,11 +226,11 @@ class TestImageChops(PillowTestCase): def test_multiply_green(self): # Arrange - im = Image.open("Tests/images/imagedraw_floodfill_RGB.png") - green = Image.new("RGB", im.size, "green") + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: + green = Image.new("RGB", im.size, "green") - # Act - new = ImageChops.multiply(im, green) + # Act + new = ImageChops.multiply(im, green) # Assert self.assertEqual(new.getbbox(), (25, 25, 76, 76)) @@ -252,12 +252,12 @@ class TestImageChops(PillowTestCase): def test_offset(self): # Arrange - im = Image.open("Tests/images/imagedraw_ellipse_RGB.png") xoffset = 45 yoffset = 20 + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im: - # Act - new = ImageChops.offset(im, xoffset, yoffset) + # Act + new = ImageChops.offset(im, xoffset, yoffset) # Assert self.assertEqual(new.getbbox(), (0, 45, 100, 96)) @@ -271,11 +271,11 @@ class TestImageChops(PillowTestCase): def test_screen(self): # Arrange - im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") - im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + 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.screen(im1, im2) + # Act + new = ImageChops.screen(im1, im2) # Assert self.assertEqual(new.getbbox(), (25, 25, 76, 76)) @@ -283,11 +283,11 @@ class TestImageChops(PillowTestCase): def test_subtract(self): # Arrange - im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") - im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + 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) + # Act + new = ImageChops.subtract(im1, im2) # Assert self.assertEqual(new.getbbox(), (25, 50, 76, 76)) @@ -296,11 +296,11 @@ class TestImageChops(PillowTestCase): 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") + 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) + # Act + new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) # Assert self.assertEqual(new.getbbox(), (0, 0, 100, 100)) @@ -309,21 +309,21 @@ class TestImageChops(PillowTestCase): def test_subtract_clip(self): # Arrange im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act - new = ImageChops.subtract(im1, im2) + # Act + new = ImageChops.subtract(im1, im2) # Assert self.assertEqual(new.getpixel((50, 50)), (0, 0, 127)) def test_subtract_modulo(self): # Arrange - im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") - im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + 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) + # Act + new = ImageChops.subtract_modulo(im1, im2) # Assert self.assertEqual(new.getbbox(), (25, 50, 76, 76)) @@ -333,10 +333,10 @@ class TestImageChops(PillowTestCase): def test_subtract_modulo_no_clip(self): # Arrange im1 = hopper() - im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act - new = ImageChops.subtract_modulo(im1, im2) + # Act + new = ImageChops.subtract_modulo(im1, im2) # Assert self.assertEqual(new.getpixel((50, 50)), (241, 167, 127)) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 10465e739..98b1a80ab 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -58,10 +58,10 @@ class TestImageCms(PillowTestCase): i = ImageCms.applyTransform(hopper(), t) self.assert_image(i, "RGB", (128, 128)) - i = hopper() - t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") - ImageCms.applyTransform(hopper(), t, inPlace=True) - self.assert_image(i, "RGB", (128, 128)) + with hopper() as i: + t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") + ImageCms.applyTransform(hopper(), t, inPlace=True) + self.assert_image(i, "RGB", (128, 128)) p = ImageCms.createProfile("sRGB") o = ImageCms.getOpenProfile(SRGB) @@ -151,8 +151,8 @@ class TestImageCms(PillowTestCase): def test_extensions(self): # extensions - i = Image.open("Tests/images/rgb.jpg") - p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) + 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", @@ -166,9 +166,10 @@ class TestImageCms(PillowTestCase): 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" - ) + with hopper() as im: + self.assertRaises( + ImageCms.PyCMSError, ImageCms.profileToProfile, im, "foo", "bar" + ) self.assertRaises( ImageCms.PyCMSError, ImageCms.buildTransform, "foo", "bar", "RGB", "RGB" ) @@ -228,18 +229,16 @@ class TestImageCms(PillowTestCase): # i.save('temp.lab.tif') # visually verified vs PS. - target = Image.open("Tests/images/hopper.Lab.tif") - - self.assert_image_similar(i, target, 3.5) + with Image.open("Tests/images/hopper.Lab.tif") as target: + self.assert_image_similar(i, target, 3.5) def test_lab_srgb(self): psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - img = Image.open("Tests/images/hopper.Lab.tif") - - img_srgb = ImageCms.applyTransform(img, t) + 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. @@ -266,8 +265,8 @@ class TestImageCms(PillowTestCase): self.assert_image_similar(hopper(), out, 2) def test_profile_tobytes(self): - i = Image.open("Tests/images/rgb.jpg") - p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) + with Image.open("Tests/images/rgb.jpg") as i: + p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) p2 = ImageCms.getOpenProfile(BytesIO(p.tobytes())) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index ffe35a4fa..4535a4838 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,6 +1,7 @@ import os.path +import unittest -from PIL import Image, ImageColor, ImageDraw +from PIL import Image, ImageColor, ImageDraw, ImageFont, features from .helper import PillowTestCase, hopper @@ -29,6 +30,8 @@ POINTS2 = [10, 10, 20, 40, 30, 30] KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] +HAS_FREETYPE = features.check("freetype2") + class TestImageDraw(PillowTestCase): def test_sanity(self): @@ -43,10 +46,10 @@ class TestImageDraw(PillowTestCase): draw.rectangle(list(range(4))) def test_valueerror(self): - im = Image.open("Tests/images/chi.gif") + with Image.open("Tests/images/chi.gif") as im: - draw = ImageDraw.Draw(im) - draw.line((0, 0), fill=(0, 0, 0)) + draw = ImageDraw.Draw(im) + draw.line((0, 0), fill=(0, 0, 0)) def test_mode_mismatch(self): im = hopper("RGB").copy() @@ -140,14 +143,27 @@ class TestImageDraw(PillowTestCase): # Assert self.assert_image_similar(im, Image.open(expected), 1) - def test_bitmap(self): + def test_arc_width_non_whole_angle(self): # Arrange - small = Image.open("Tests/images/pil123rgba.png").resize((50, 50)) im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png" # Act - draw.bitmap((10, 10), small) + draw.arc(BBOX1, 10, 259.5, width=5) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + + def test_bitmap(self): + # 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")) @@ -508,8 +524,8 @@ class TestImageDraw(PillowTestCase): # Assert expected = "Tests/images/imagedraw_floodfill_" + mode + ".png" - im_floodfill = Image.open(expected) - self.assert_image_equal(im, im_floodfill) + with Image.open(expected) as im_floodfill: + self.assert_image_equal(im, im_floodfill) # Test that using the same colour does not change the image ImageDraw.floodfill(im, centre_point, red) @@ -559,6 +575,24 @@ class TestImageDraw(PillowTestCase): # 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 + + # 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 + 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 ): @@ -570,152 +604,166 @@ class TestImageDraw(PillowTestCase): return img, ImageDraw.Draw(img) def test_square(self): - expected = Image.open(os.path.join(IMAGES_PATH, "square.png")) - expected.load() - img, draw = self.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)) - 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)) - 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)) - draw.rectangle((7, 7, 2, 2), BLACK) - self.assert_image_equal(img, expected, "square as inverted rectangle failed") + with Image.open(os.path.join(IMAGES_PATH, "square.png")) as expected: + expected.load() + img, draw = self.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)) + 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)) + 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)) + draw.rectangle((7, 7, 2, 2), BLACK) + self.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")) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK) - self.assert_image_equal(img, expected, "triangle right failed") + with Image.open(os.path.join(IMAGES_PATH, "triangle_right.png")) as expected: + expected.load() + img, draw = self.create_base_image_draw((20, 20)) + draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK) + self.assert_image_equal(img, expected, "triangle right failed") def test_line_horizontal(self): - expected = Image.open( + with Image.open( os.path.join(IMAGES_PATH, "line_horizontal_w2px_normal.png") - ) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 5, 14, 5), BLACK, 2) - self.assert_image_equal( - img, expected, "line straight horizontal normal 2px wide failed" - ) - expected = Image.open( + ) as expected: + expected.load() + img, draw = self.create_base_image_draw((20, 20)) + draw.line((5, 5, 14, 5), BLACK, 2) + self.assert_image_equal( + img, expected, "line straight horizontal normal 2px wide failed" + ) + with Image.open( os.path.join(IMAGES_PATH, "line_horizontal_w2px_inverted.png") - ) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((14, 5, 5, 5), BLACK, 2) - self.assert_image_equal( - img, expected, "line straight horizontal inverted 2px wide failed" - ) - expected = Image.open(os.path.join(IMAGES_PATH, "line_horizontal_w3px.png")) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 5, 14, 5), BLACK, 3) - self.assert_image_equal( - img, expected, "line straight horizontal normal 3px wide failed" - ) - img, draw = self.create_base_image_draw((20, 20)) - draw.line((14, 5, 5, 5), BLACK, 3) - self.assert_image_equal( - img, expected, "line straight horizontal inverted 3px wide failed" - ) - expected = Image.open(os.path.join(IMAGES_PATH, "line_horizontal_w101px.png")) - expected.load() - img, draw = self.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" - ) + ) as expected: + expected.load() + img, draw = self.create_base_image_draw((20, 20)) + draw.line((14, 5, 5, 5), BLACK, 2) + self.assert_image_equal( + img, expected, "line straight horizontal inverted 2px wide failed" + ) + 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)) + draw.line((5, 5, 14, 5), BLACK, 3) + self.assert_image_equal( + img, expected, "line straight horizontal normal 3px wide failed" + ) + img, draw = self.create_base_image_draw((20, 20)) + draw.line((14, 5, 5, 5), BLACK, 3) + self.assert_image_equal( + img, expected, "line straight horizontal inverted 3px wide failed" + ) + 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)) + draw.line((5, 55, 195, 55), BLACK, 101) + self.assert_image_equal( + img, expected, "line straight horizontal 101px wide failed" + ) def test_line_h_s1_w2(self): self.skipTest("failing") - expected = Image.open( + with Image.open( os.path.join(IMAGES_PATH, "line_horizontal_slope1px_w2px.png") - ) - expected.load() - img, draw = self.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" - ) + ) as expected: + expected.load() + img, draw = self.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" + ) def test_line_vertical(self): - expected = Image.open( + with Image.open( os.path.join(IMAGES_PATH, "line_vertical_w2px_normal.png") - ) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 5, 5, 14), BLACK, 2) - self.assert_image_equal( - img, expected, "line straight vertical normal 2px wide failed" - ) - expected = Image.open( + ) as expected: + expected.load() + img, draw = self.create_base_image_draw((20, 20)) + draw.line((5, 5, 5, 14), BLACK, 2) + self.assert_image_equal( + img, expected, "line straight vertical normal 2px wide failed" + ) + with Image.open( os.path.join(IMAGES_PATH, "line_vertical_w2px_inverted.png") - ) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 14, 5, 5), BLACK, 2) - self.assert_image_equal( - img, expected, "line straight vertical inverted 2px wide failed" - ) - expected = Image.open(os.path.join(IMAGES_PATH, "line_vertical_w3px.png")) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 5, 5, 14), BLACK, 3) - self.assert_image_equal( - img, expected, "line straight vertical normal 3px wide failed" - ) - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 14, 5, 5), BLACK, 3) - self.assert_image_equal( - img, expected, "line straight vertical inverted 3px wide failed" - ) - expected = Image.open(os.path.join(IMAGES_PATH, "line_vertical_w101px.png")) - expected.load() - img, draw = self.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( + ) as expected: + expected.load() + img, draw = self.create_base_image_draw((20, 20)) + draw.line((5, 14, 5, 5), BLACK, 2) + self.assert_image_equal( + img, expected, "line straight vertical inverted 2px wide failed" + ) + 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)) + draw.line((5, 5, 5, 14), BLACK, 3) + self.assert_image_equal( + img, expected, "line straight vertical normal 3px wide failed" + ) + img, draw = self.create_base_image_draw((20, 20)) + draw.line((5, 14, 5, 5), BLACK, 3) + self.assert_image_equal( + img, expected, "line straight vertical inverted 3px wide failed" + ) + 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)) + draw.line((55, 5, 55, 195), BLACK, 101) + self.assert_image_equal( + img, expected, "line straight vertical 101px wide failed" + ) + with Image.open( os.path.join(IMAGES_PATH, "line_vertical_slope1px_w2px.png") - ) - expected.load() - img, draw = self.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" - ) + ) as expected: + expected.load() + img, draw = self.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" + ) def test_line_oblique_45(self): - expected = Image.open(os.path.join(IMAGES_PATH, "line_oblique_45_w3px_a.png")) - expected.load() - img, draw = self.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)) - 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")) - expected.load() - img, draw = self.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)) - draw.line((5, 14, 14, 5), BLACK, 3) - self.assert_image_equal( - img, expected, "line oblique 45 inverted 3px wide B failed" - ) + 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)) + 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)) + draw.line((14, 14, 5, 5), BLACK, 3) + self.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)) + 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)) + 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 @@ -771,6 +819,54 @@ class TestImageDraw(PillowTestCase): 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)) + 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 diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 9ce472dd0..577727664 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -1,8 +1,9 @@ import os.path +import unittest from PIL import Image, ImageDraw2, features -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper BLACK = (0, 0, 0) WHITE = (255, 255, 255) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index b2235853a..d0d994eee 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -35,7 +35,7 @@ class TestImageEnhance(PillowTestCase): self.assert_image_equal( im.getchannel("A"), original.getchannel("A"), - "Diff on %s: %s" % (op, amount), + "Diff on {}: {}".format(op, amount), ) def test_alpha(self): diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 2ca5abe4c..f575c8c1a 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,8 +1,9 @@ +import unittest from io import BytesIO from PIL import EpsImagePlugin, Image, ImageFile -from .helper import PillowTestCase, fromstring, hopper, tostring, unittest +from .helper import PillowTestCase, fromstring, hopper, tostring try: from PIL import _webp @@ -23,7 +24,7 @@ class TestImageFile(PillowTestCase): 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") @@ -103,45 +104,55 @@ class TestImageFile(PillowTestCase): parser = ImageFile.Parser() parser.feed(1) + def test_negative_stride(self): + with open("Tests/images/raw_negative_stride.bin", "rb") as f: + input = f.read() + p = ImageFile.Parser() + p.feed(input) + with self.assertRaises(IOError): + p.close() + 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 self.assertRaises(IOError): + im.load() + + # Test that the error is raised if loaded a second time + with self.assertRaises(IOError): + im.load() 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 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 self.assertRaises(IOError): + im.load() 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): @@ -233,91 +244,91 @@ class TestPyDecoder(PillowTestCase): 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") + with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # 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") + out = self.tempfile("temp.jpg") + exif[258] = 8 + del exif[40960] + exif[40963] = 455 + exif[11] = "Pillow test" + im.save(out, exif=exif) + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + self.assertEqual(reloaded_exif[258], 8) + self.assertNotIn(40960, exif) + self.assertEqual(reloaded_exif[40963], 455) + self.assertEqual(exif[11], "Pillow test") - im = Image.open("Tests/images/no-dpi-in-exif.jpg") # Big endian - exif = im.getexif() - self.assertNotIn(258, exif) - self.assertIn(40962, exif) - self.assertEqual(exif[40963], 200) - self.assertEqual(exif[305], "Adobe Photoshop CC 2017 (Macintosh)") + with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: # 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") + out = self.tempfile("temp.jpg") + exif[258] = 8 + del exif[34665] + exif[40963] = 455 + exif[305] = "Pillow test" + im.save(out, exif=exif) + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + self.assertEqual(reloaded_exif[258], 8) + self.assertNotIn(40960, exif) + self.assertEqual(reloaded_exif[40963], 455) + self.assertEqual(exif[305], "Pillow test") @unittest.skipIf( not HAVE_WEBP or not _webp.HAVE_WEBPANIM, "WebP support not installed with animation", ) def test_exif_webp(self): - im = Image.open("Tests/images/hopper.webp") - exif = im.getexif() - self.assertEqual(exif, {}) + with Image.open("Tests/images/hopper.webp") as im: + exif = im.getexif() + self.assertEqual(exif, {}) - out = self.tempfile("temp.webp") - exif[258] = 8 - exif[40963] = 455 - exif[305] = "Pillow test" + out = self.tempfile("temp.webp") + exif[258] = 8 + exif[40963] = 455 + exif[305] = "Pillow test" - def check_exif(): - reloaded = Image.open(out) - reloaded_exif = reloaded.getexif() - self.assertEqual(reloaded_exif[258], 8) - self.assertEqual(reloaded_exif[40963], 455) - self.assertEqual(exif[305], "Pillow test") + def check_exif(): + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + self.assertEqual(reloaded_exif[258], 8) + self.assertEqual(reloaded_exif[40963], 455) + self.assertEqual(exif[305], "Pillow test") - im.save(out, exif=exif) - check_exif() - im.save(out, exif=exif, save_all=True) - check_exif() + 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}) + with Image.open("Tests/images/exif.png") as im: + 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) + 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"}) + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + self.assertEqual(reloaded_exif, {258: 8, 40963: 455, 305: "Pillow test"}) def test_exif_interop(self): - 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} - ) + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + self.assertEqual( + exif.get_ifd(0xA005), {1: "R98", 2: b"0100", 4097: 2272, 4098: 1704} + ) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 8a23e6339..1777e2978 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,15 +1,15 @@ -# -*- coding: utf-8 -*- import copy import distutils.version import os import re import shutil import sys +import unittest from io import BytesIO from PIL import Image, ImageDraw, ImageFont, features -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase, is_pypy, is_win32 FONT_PATH = "Tests/fonts/FreeMono.ttf" FONT_SIZE = 20 @@ -20,7 +20,7 @@ HAS_FREETYPE = features.check("freetype2") HAS_RAQM = features.check("raqm") -class SimplePatcher(object): +class SimplePatcher: def __init__(self, parent_obj, attr_name, value): self._parent_obj = parent_obj self._attr_name = attr_name @@ -179,10 +179,10 @@ class TestImageFont(PillowTestCase): draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) target = "Tests/images/rectangle_surrounding_text.png" - target_img = Image.open(target) + with Image.open(target) as target_img: - # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics["textsize"]) + # Epsilon ~.5 fails with FreeType 2.7 + self.assert_image_similar(im, target_img, self.metrics["textsize"]) def test_render_multiline(self): im = Image.new(mode="RGB", size=(300, 100)) @@ -196,12 +196,12 @@ class TestImageFont(PillowTestCase): y += line_spacing target = "Tests/images/multiline_text.png" - target_img = Image.open(target) + with Image.open(target) as target_img: - # some versions of freetype have different horizontal spacing. - # setting a tight epsilon, I'm showing the original test failure - # at epsilon = ~38. - self.assert_image_similar(im, target_img, self.metrics["multiline"]) + # some versions of freetype have different horizontal spacing. + # setting a tight epsilon, I'm showing the original test failure + # at epsilon = ~38. + self.assert_image_similar(im, target_img, self.metrics["multiline"]) def test_render_multiline_text(self): ttf = self.get_font() @@ -213,10 +213,10 @@ class TestImageFont(PillowTestCase): draw.text((0, 0), TEST_TEXT, font=ttf) target = "Tests/images/multiline_text.png" - target_img = Image.open(target) + with Image.open(target) as target_img: - # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics["multiline"]) + # Epsilon ~.5 fails with FreeType 2.7 + self.assert_image_similar(im, target_img, self.metrics["multiline"]) # Test that text() can pass on additional arguments # to multiline_text() @@ -232,10 +232,10 @@ class TestImageFont(PillowTestCase): draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align) target = "Tests/images/multiline_text" + ext + ".png" - target_img = Image.open(target) + with Image.open(target) as target_img: - # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics["multiline"]) + # Epsilon ~.5 fails with FreeType 2.7 + self.assert_image_similar(im, target_img, self.metrics["multiline"]) def test_unknown_align(self): im = Image.new(mode="RGB", size=(300, 100)) @@ -297,10 +297,10 @@ class TestImageFont(PillowTestCase): draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) target = "Tests/images/multiline_text_spacing.png" - target_img = Image.open(target) + with Image.open(target) as target_img: - # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics["multiline"]) + # Epsilon ~.5 fails with FreeType 2.7 + self.assert_image_similar(im, target_img, self.metrics["multiline"]) def test_rotated_transposed_font(self): img_grey = Image.new("L", (100, 100)) @@ -432,14 +432,14 @@ class TestImageFont(PillowTestCase): draw = ImageDraw.Draw(im) target = "Tests/images/default_font.png" - target_img = Image.open(target) + with Image.open(target) as target_img: - # Act - default_font = ImageFont.load_default() - draw.text((10, 10), txt, font=default_font) + # Act + default_font = ImageFont.load_default() + draw.text((10, 10), txt, font=default_font) - # Assert - self.assert_image_equal(im, target_img) + # Assert + self.assert_image_equal(im, target_img) def test_getsize_empty(self): # issue #2614 @@ -462,15 +462,12 @@ class TestImageFont(PillowTestCase): # issue #2826 font = ImageFont.load_default() with self.assertRaises(UnicodeEncodeError): - font.getsize(u"’") + font.getsize("’") - @unittest.skipIf( - sys.version.startswith("2") or hasattr(sys, "pypy_translation_info"), - "requires CPython 3.3+", - ) + @unittest.skipIf(is_pypy(), "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( @@ -504,7 +501,7 @@ class TestImageFont(PillowTestCase): name = font.getname() self.assertEqual(("FreeMono", "Regular"), name) - @unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") + @unittest.skipIf(is_win32(), "requires Unix or macOS") def test_find_linux_font(self): # A lot of mocking here - this is more for hitting code and # catching syntax like errors @@ -550,7 +547,7 @@ class TestImageFont(PillowTestCase): font_directory + "/Duplicate.ttf", "Duplicate" ) - @unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") + @unittest.skipIf(is_win32(), "requires Unix or macOS") def test_find_macos_font(self): # Like the linux test, more cover hitting code rather than testing # correctness. @@ -605,6 +602,21 @@ class TestImageFont(PillowTestCase): self.assertEqual(t.getsize_multiline("ABC\nA"), (36, 36)) self.assertEqual(t.getsize_multiline("ABC\nAaaa"), (48, 36)) + def test_getsize_stroke(self): + # Arrange + t = self.get_font() + + # 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), + ) + self.assertEqual( + t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width), + (48 + stroke_width * 2, 36 + stroke_width * 4), + ) + def test_complex_font_settings(self): # Arrange t = self.get_font() @@ -691,8 +703,8 @@ class TestImageFont(PillowTestCase): 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: + self.assert_image_similar(im, expected, epsilon) font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) _check_text(font, "Tests/images/variation_adobe.png", 11) @@ -721,8 +733,8 @@ class TestImageFont(PillowTestCase): 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: + self.assert_image_similar(im, expected, epsilon) font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) font.set_variation_by_axes([500, 50]) diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py index b7be8f723..6853504f6 100644 --- a/Tests/test_imagefont_bitmap.py +++ b/Tests/test_imagefont_bitmap.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image, ImageDraw, ImageFont -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase image_font_installed = True try: @@ -9,7 +11,7 @@ except ImportError: image_font_installed = False -@unittest.skipIf(not image_font_installed, "image font not installed") +@unittest.skipUnless(image_font_installed, "image font not installed") class TestImageFontBitmap(PillowTestCase): def test_similar(self): text = "EmbeddedBitmap" diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index afd45ce19..c1f976dfa 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,7 +1,8 @@ -# -*- coding: utf-8 -*- +import unittest + from PIL import Image, ImageDraw, ImageFont, features -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase FONT_SIZE = 20 FONT_PATH = "Tests/fonts/DejaVuSans.ttf" @@ -24,9 +25,8 @@ class TestImagecomplextext(PillowTestCase): draw.text((0, 0), "اهلا عمان", font=ttf, fill=500) target = "Tests/images/test_text.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 0.5) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 0.5) def test_y_offset(self): ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) @@ -36,9 +36,8 @@ class TestImagecomplextext(PillowTestCase): draw.text((0, 0), "العالم العربي", font=ttf, fill=500) target = "Tests/images/test_y_offset.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 1.7) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 1.7) def test_complex_unicode_text(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -48,9 +47,8 @@ class TestImagecomplextext(PillowTestCase): draw.text((0, 0), "السلام عليكم", font=ttf, fill=500) target = "Tests/images/test_complex_unicode_text.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 0.5) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 0.5) ttf = ImageFont.truetype("Tests/fonts/KhmerOSBattambang-Regular.ttf", FONT_SIZE) @@ -59,9 +57,8 @@ class TestImagecomplextext(PillowTestCase): draw.text((0, 0), "លោកុប្បត្តិ", font=ttf, fill=500) target = "Tests/images/test_complex_unicode_text2.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 2.3) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 2.3) def test_text_direction_rtl(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -71,9 +68,8 @@ class TestImagecomplextext(PillowTestCase): draw.text((0, 0), "English عربي", font=ttf, fill=500, direction="rtl") target = "Tests/images/test_direction_rtl.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 0.5) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 0.5) def test_text_direction_ltr(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -83,9 +79,8 @@ class TestImagecomplextext(PillowTestCase): draw.text((0, 0), "سلطنة عمان Oman", font=ttf, fill=500, direction="ltr") target = "Tests/images/test_direction_ltr.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 0.5) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 0.5) def test_text_direction_rtl2(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -95,9 +90,8 @@ class TestImagecomplextext(PillowTestCase): draw.text((0, 0), "Oman سلطنة عمان", font=ttf, fill=500, direction="rtl") target = "Tests/images/test_direction_ltr.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 0.5) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 0.5) def test_text_direction_ttb(self): ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE) @@ -111,9 +105,31 @@ class TestImagecomplextext(PillowTestCase): self.skipTest("libraqm 0.7 or greater not available") target = "Tests/images/test_direction_ttb.png" - target_img = Image.open(target) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 1.15) - self.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") + + target = "Tests/images/test_direction_ttb_stroke.png" + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 12.4) def test_ligature_features(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -122,9 +138,8 @@ class TestImagecomplextext(PillowTestCase): draw = ImageDraw.Draw(im) draw.text((0, 0), "filling", font=ttf, fill=500, features=["-liga"]) target = "Tests/images/test_ligature_features.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 0.5) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 0.5) liga_size = ttf.getsize("fi", features=["-liga"]) self.assertEqual(liga_size, (13, 19)) @@ -137,9 +152,8 @@ class TestImagecomplextext(PillowTestCase): draw.text((0, 0), "TeToAV", font=ttf, fill=500, features=["-kern"]) target = "Tests/images/test_kerning_features.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 0.5) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 0.5) def test_arabictext_features(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -155,9 +169,8 @@ class TestImagecomplextext(PillowTestCase): ) target = "Tests/images/test_arabictext_features.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 0.5) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 0.5) def test_x_max_and_y_offset(self): ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) @@ -167,9 +180,8 @@ class TestImagecomplextext(PillowTestCase): draw.text((0, 0), "لح", font=ttf, fill=500) target = "Tests/images/test_x_max_and_y_offset.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 0.5) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 0.5) def test_language(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -179,6 +191,5 @@ class TestImagecomplextext(PillowTestCase): draw.text((0, 0), "абвг", font=ttf, fill=500, language="sr") target = "Tests/images/test_language.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 0.5) + with Image.open(target) as target_img: + self.assert_image_similar(im, target_img, 0.5) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index a29bc019e..bea7f68b3 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -8,7 +8,11 @@ try: class TestImageGrab(PillowTestCase): def test_grab(self): - for im in [ImageGrab.grab(), ImageGrab.grab(include_layered_windows=True)]: + 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): diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index da41b3a12..8d2b94226 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from PIL import Image, ImageMath from .helper import PillowTestCase @@ -7,7 +5,7 @@ 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 diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 1492872b6..c9fd37c54 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -51,8 +51,8 @@ class MorphTests(PillowTestCase): 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) + with Image.open("Tests/images/morph_a.png") as im: + self.assert_image_equal(self.A, im) def create_lut(self): for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 85529a708..ea50a4c76 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -11,7 +11,7 @@ except ImportError: class TestImageOps(PillowTestCase): - class Deformer(object): + class Deformer: def getmesh(self, im): x, y = im.size return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] @@ -81,6 +81,16 @@ class TestImageOps(PillowTestCase): newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) self.assertEqual(newimg.size, (35, 35)) + def test_fit_same_ratio(self): + # The ratio for this image is 1000.0 / 755 = 1.3245033112582782 + # If the ratios are not acknowledged to be the same, + # and Pillow attempts to adjust the width to + # 1.3245033112582782 * 755 = 1000.0000000000001 + # then centering this greater width causes a negative x offset when cropping + with Image.new("RGB", (1000, 755)) as im: + new_im = ImageOps.fit(im, (1000, 755)) + self.assertEqual(new_im.size, (1000, 755)) + def test_pad(self): # Same ratio im = hopper() @@ -96,10 +106,10 @@ class TestImageOps(PillowTestCase): new_im = ImageOps.pad(im, new_size, color=color, centering=centering) self.assertEqual(new_im.size, new_size) - target = Image.open( + with Image.open( "Tests/images/imageops_pad_" + label + "_" + str(i) + ".jpg" - ) - self.assert_image_similar(new_im, target, 6) + ) as target: + self.assert_image_similar(new_im, target, 6) def test_pil163(self): # Division by zero in equalize if < 255 pixels in image (@PIL163) @@ -130,8 +140,8 @@ class TestImageOps(PillowTestCase): # Test the colorizing function with 2-color functionality # Open test image (256px by 10px, black to white) - im = Image.open("Tests/images/bw_gradient.png") - im = im.convert("L") + 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") @@ -163,8 +173,8 @@ class TestImageOps(PillowTestCase): # 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") - im = im.convert("L") + 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( @@ -198,8 +208,8 @@ class TestImageOps(PillowTestCase): # 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") - im = im.convert("L") + 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( @@ -251,27 +261,36 @@ class TestImageOps(PillowTestCase): if HAVE_WEBP and _webp.HAVE_WEBPANIM: exts.append(".webp") for ext in exts: - base_im = Image.open("Tests/images/hopper" + ext) + with Image.open("Tests/images/hopper" + ext) as base_im: - 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) - 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) - else: - self.assertNotEqual(transposed_im.info["exif"], original_exif) + def check(orientation_im): + for im in [ + orientation_im, + orientation_im.copy(), + ]: # ImageFile # Image + if orientation_im is base_im: + self.assertNotIn("exif", im.info) + else: + original_exif = im.info["exif"] + transposed_im = ImageOps.exif_transpose(im) + self.assert_image_similar(base_im, transposed_im, 17) + if orientation_im is base_im: + self.assertNotIn("exif", im.info) + else: + self.assertNotEqual( + transposed_im.info["exif"], original_exif + ) - self.assertNotIn(0x0112, transposed_im.getexif()) + self.assertNotIn(0x0112, transposed_im.getexif()) - # 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) + # 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) + + 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 8340c5f0d..71e858681 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -2,57 +2,61 @@ 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 setUp(self): + super().setUp() + self.im = Image.open("Tests/images/hopper.ppm") + self.addCleanup(self.im.close) + self.snakes = Image.open("Tests/images/color_snakes.png") + self.addCleanup(self.snakes.close) + def test_filter_api(self): test_filter = ImageFilter.GaussianBlur(2.0) - i = im.filter(test_filter) + i = self.im.filter(test_filter) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) test_filter = ImageFilter.UnsharpMask(2.0, 125, 8) - i = im.filter(test_filter) + i = self.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) + self.assertRaises(ValueError, self.im.convert("1").filter, usm) + self.im.convert("L").filter(usm) + self.assertRaises(ValueError, self.im.convert("I").filter, usm) + self.assertRaises(ValueError, self.im.convert("F").filter, usm) + self.im.convert("RGB").filter(usm) + self.im.convert("RGBA").filter(usm) + self.im.convert("CMYK").filter(usm) + self.assertRaises(ValueError, self.im.convert("YCbCr").filter, usm) 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) + self.assertRaises(ValueError, self.im.convert("1").filter, blur) + blur(self.im.convert("L")) + self.assertRaises(ValueError, self.im.convert("I").filter, blur) + self.assertRaises(ValueError, self.im.convert("F").filter, blur) + self.im.convert("RGB").filter(blur) + self.im.convert("RGBA").filter(blur) + self.im.convert("CMYK").filter(blur) + self.assertRaises(ValueError, self.im.convert("YCbCr").filter, blur) def test_usm_accuracy(self): - src = snakes.convert("RGB") + src = self.snakes.convert("RGB") i = src.filter(ImageFilter.UnsharpMask(5, 1024, 0)) # Image should not be changed because it have only 0 and 255 levels. self.assertEqual(i.tobytes(), src.tobytes()) def test_blur_accuracy(self): - i = snakes.filter(ImageFilter.GaussianBlur(0.4)) + i = self.snakes.filter(ImageFilter.GaussianBlur(0.4)) # These pixels surrounded with pixels with 255 intensity. # They must be very close to 255. for x, y, c in [ diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 1297712ef..933fc9923 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -128,9 +128,8 @@ class TestImagePalette(PillowTestCase): 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) + with Image.open(outfile) as reloaded: + self.assert_image_equal(img, reloaded) def test_invalid_palette(self): self.assertRaises(IOError, ImagePalette.load, "Tests/images/hopper.jpg") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index ed65d47c1..35c78cd3b 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -2,7 +2,6 @@ import array import struct from PIL import Image, ImagePath -from PIL._util import py3 from .helper import PillowTestCase @@ -75,10 +74,7 @@ class TestImagePath(PillowTestCase): # 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 dc43e6bc7..f69a21d2a 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,13 +1,7 @@ -import sys -import warnings - from PIL import ImageQt from .helper import PillowTestCase, hopper -if sys.version_info.major >= 3: - from importlib import reload - if ImageQt.qt_is_installed: from PIL.ImageQt import qRgba @@ -21,7 +15,7 @@ else: test_case.skipTest("Qt bindings are not installed") -class PillowQtTestCase(object): +class PillowQtTestCase: def setUp(self): skip_if_qt_is_not_installed(self) @@ -31,14 +25,10 @@ class PillowQtTestCase(object): class PillowQPixmapTestCase(PillowQtTestCase): def setUp(self): - PillowQtTestCase.setUp(self) + super().setUp() 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: @@ -47,7 +37,7 @@ class PillowQPixmapTestCase(PillowQtTestCase): self.app = QGuiApplication([]) def tearDown(self): - PillowQtTestCase.tearDown(self) + super().tearDown() self.app.quit() @@ -59,10 +49,6 @@ class TestImageQt(PillowQtTestCase, PillowTestCase): # 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 @@ -83,13 +69,3 @@ class TestImageQt(PillowQtTestCase, PillowTestCase): 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, []) diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 1eea839da..17f8100b7 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -24,25 +24,25 @@ class TestImageSequence(PillowTestCase): self.assertRaises(AttributeError, ImageSequence.Iterator, 0) def test_iterator(self): - im = Image.open("Tests/images/multipage.tiff") - 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) + with Image.open("Tests/images/multipage.tiff") as im: + i = ImageSequence.Iterator(im) + for index in range(0, im.n_frames): + self.assertEqual(i[index], next(i)) + self.assertRaises(IndexError, lambda: i[index + 1]) + self.assertRaises(StopIteration, next, i) def test_iterator_min_frame(self): - im = Image.open("Tests/images/hopper.psd") - i = ImageSequence.Iterator(im) - for index in range(1, im.n_frames): - self.assertEqual(i[index], next(i)) + with Image.open("Tests/images/hopper.psd") as im: + i = ImageSequence.Iterator(im) + for index in range(1, im.n_frames): + self.assertEqual(i[index], next(i)) def _test_multipage_tiff(self): - im = Image.open("Tests/images/multipage.tiff") - for index, frame in enumerate(ImageSequence.Iterator(im)): - frame.load() - self.assertEqual(index, im.tell()) - frame.convert("RGB") + with Image.open("Tests/images/multipage.tiff") as im: + for index, frame in enumerate(ImageSequence.Iterator(im)): + frame.load() + self.assertEqual(index, im.tell()) + frame.convert("RGB") def test_tiff(self): self._test_multipage_tiff() @@ -58,41 +58,41 @@ class TestImageSequence(PillowTestCase): TiffImagePlugin.READ_LIBTIFF = False def test_consecutive(self): - im = Image.open("Tests/images/multipage.tiff") - firstFrame = None - for frame in ImageSequence.Iterator(im): - if firstFrame is None: - firstFrame = frame.copy() - for frame in ImageSequence.Iterator(im): - self.assert_image_equal(frame, firstFrame) - break + with Image.open("Tests/images/multipage.tiff") as im: + firstFrame = None + for frame in ImageSequence.Iterator(im): + if firstFrame is None: + firstFrame = frame.copy() + for frame in ImageSequence.Iterator(im): + self.assert_image_equal(frame, firstFrame) + break def test_palette_mmap(self): # Using mmap in ImageFile can require to reload the palette. - im = Image.open("Tests/images/multipage-mmap.tiff") - color1 = im.getpalette()[0:3] - im.seek(0) - color2 = im.getpalette()[0:3] - self.assertEqual(color1, color2) + with Image.open("Tests/images/multipage-mmap.tiff") as im: + color1 = im.getpalette()[0:3] + im.seek(0) + color2 = im.getpalette()[0:3] + self.assertEqual(color1, color2) def test_all_frames(self): # Test a single image - im = Image.open("Tests/images/iss634.gif") - ims = ImageSequence.all_frames(im) + with Image.open("Tests/images/iss634.gif") as im: + ims = ImageSequence.all_frames(im) - self.assertEqual(len(ims), 42) - for i, im_frame in enumerate(ims): - self.assertFalse(im_frame is im) + self.assertEqual(len(ims), 42) + for i, im_frame in enumerate(ims): + self.assertFalse(im_frame is im) - im.seek(i) - self.assert_image_equal(im, im_frame) + im.seek(i) + self.assert_image_equal(im, im_frame) - # Test a series of images - ims = ImageSequence.all_frames([im, hopper(), im]) - self.assertEqual(len(ims), 85) + # Test a series of images + ims = ImageSequence.all_frames([im, hopper(), im]) + self.assertEqual(len(ims), 85) - # Test an operation - ims = ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90)) - for i, im_frame in enumerate(ims): - im.seek(i) - self.assert_image_equal(im.rotate(90), im_frame) + # 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) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 378afe2db..858714e57 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image, ImageShow -from .helper import PillowTestCase, hopper +from .helper import PillowTestCase, hopper, is_win32, on_ci, on_github_actions class TestImageShow(PillowTestCase): @@ -15,7 +17,7 @@ class TestImageShow(PillowTestCase): # Restore original state ImageShow._viewers.pop() - def test_show(self): + def test_viewer_show(self): class TestViewer(ImageShow.Viewer): methodCalled = False @@ -27,13 +29,22 @@ class TestImageShow(PillowTestCase): ImageShow.register(viewer, -1) for mode in ("1", "I;16", "LA", "RGB", "RGBA"): - im = hopper(mode) - self.assertTrue(ImageShow.show(im)) + with hopper() as im: + self.assertTrue(ImageShow.show(im)) self.assertTrue(viewer.methodCalled) # Restore original state ImageShow._viewers.pop(0) + @unittest.skipUnless( + on_ci() and not (is_win32() and on_github_actions()), + "Only run on CIs; hangs on Windows on GitHub Actions", + ) + def test_show(self): + for mode in ("1", "I;16", "LA", "RGB", "RGBA"): + im = hopper(mode) + self.assertTrue(ImageShow.show(im)) + def test_viewer(self): viewer = ImageShow.Viewer() diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index c397c84be..e6032cc2e 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,15 +1,14 @@ -from PIL import Image -from PIL._util import py3 +import unittest -from .helper import PillowTestCase, hopper, unittest +from PIL import Image + +from .helper import PillowTestCase, 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): @@ -19,7 +18,7 @@ except (OSError, ImportError): TK_MODES = ("1", "L", "P", "RGB", "RGBA") -@unittest.skipIf(not HAS_TK, "Tk not installed") +@unittest.skipUnless(HAS_TK, "Tk not installed") class TestImageTk(PillowTestCase): def setUp(self): try: @@ -32,19 +31,19 @@ class TestImageTk(PillowTestCase): 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} + 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} - # Test "file" - im = ImageTk._get_image_from_kw(kw) - self.assert_image_equal(im, im1) + # Test "file" + im = ImageTk._get_image_from_kw(kw) + self.assert_image_equal(im, im1) - # Test "data" - im = ImageTk._get_image_from_kw(kw) - self.assert_image_equal(im, im2) + # 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) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 92fcdc28d..1cd8c674e 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,8 +1,8 @@ -import sys +import unittest from PIL import ImageWin -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper, is_win32 class TestImageWin(PillowTestCase): @@ -32,7 +32,7 @@ class TestImageWin(PillowTestCase): self.assertEqual(wnd2, 50) -@unittest.skipUnless(sys.platform.startswith("win32"), "Windows only") +@unittest.skipUnless(is_win32(), "Windows only") class TestImageWinDib(PillowTestCase): def test_dib_image(self): # Arrange diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index efa3753b9..d7c22a209 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 PillowTestCase, 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): diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 68e72bc4e..240783960 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase class TestLibImage(PillowTestCase): diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 6178184bc..f9e6e7c4c 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -18,7 +18,7 @@ class TestLibPack(PillowTestCase): 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)) @@ -44,6 +44,9 @@ class TestLibPack(PillowTestCase): self.assert_pack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) self.assert_pack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) + def test_La(self): + self.assert_pack("La", "La", 2, (1, 2), (3, 4), (5, 6)) + def test_P(self): self.assert_pack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 255, 0, 0) self.assert_pack("P", "P;2", b"\xe4", 3, 2, 1, 0) @@ -226,7 +229,7 @@ class TestLibUnpack(PillowTestCase): """ 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) @@ -269,6 +272,9 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) self.assert_unpack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) + def test_La(self): + self.assert_unpack("La", "La", 2, (1, 2), (3, 4), (5, 6)) + def test_P(self): self.assert_unpack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 1, 0, 0) self.assert_unpack("P", "P;2", b"\xe4", 3, 2, 1, 0) @@ -371,7 +377,7 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack( "RGBA", "RGBa;16L", - b"\x88\x01\x88\x02\x88\x03\x88\x00" b"\x88\x10\x88\x20\x88\x30\x88\xff", + b"\x88\x01\x88\x02\x88\x03\x88\x00\x88\x10\x88\x20\x88\x30\x88\xff", (0, 0, 0, 0), (16, 32, 48, 255), ) @@ -386,7 +392,7 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack( "RGBA", "RGBa;16B", - b"\x01\x88\x02\x88\x03\x88\x00\x88" b"\x10\x88\x20\x88\x30\x88\xff\x88", + b"\x01\x88\x02\x88\x03\x88\x00\x88\x10\x88\x20\x88\x30\x88\xff\x88", (0, 0, 0, 0), (16, 32, 48, 255), ) diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 86229dbcb..0b1f330ac 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,10 +1,9 @@ -from __future__ import print_function - import locale +import unittest from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase # ref https://github.com/python-pillow/Pillow/issues/272 # on windows, in polish locale: @@ -26,9 +25,15 @@ path = "Tests/images/hopper.jpg" class TestLocale(PillowTestCase): def test_sanity(self): - Image.open(path) + with Image.open(path): + pass try: locale.setlocale(locale.LC_ALL, "polish") except locale.Error: unittest.skip("Polish locale not available") - Image.open(path) + + 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 847def834..d0f1410e7 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os import subprocess import sys @@ -12,11 +10,14 @@ class TestMain(TestCase): 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 ")) + self.assertTrue(lines[2].startswith("Python ")) + lines = lines[3:] + while lines[0].startswith(" "): + lines = lines[1:] + self.assertEqual(lines[0], "-" * 68) + self.assertTrue(lines[1].startswith("Python modules loaded from ")) + self.assertTrue(lines[2].startswith("Binary modules loaded from ")) + self.assertEqual(lines[3], "-" * 68) jpeg = ( os.linesep + "-" * 68 diff --git a/Tests/test_map.py b/Tests/test_map.py index 3fc42651b..bdc3a7e2c 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,8 +1,9 @@ import sys +import unittest from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase, is_win32 try: import numpy @@ -10,7 +11,7 @@ except ImportError: numpy = None -@unittest.skipIf(sys.platform.startswith("win32"), "Win32 does not call map_buffer") +@unittest.skipIf(is_win32(), "Win32 does not call map_buffer") class TestMap(PillowTestCase): def test_overflow(self): # There is the potential to overflow comparisons in map.c @@ -23,9 +24,9 @@ class TestMap(PillowTestCase): 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)): - im.load() + with Image.open("Tests/images/l2rgb_read.bmp") as im: + with self.assertRaises((ValueError, MemoryError, IOError)): + im.load() Image.MAX_IMAGE_PIXELS = max_pixels diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index b1cf2a233..fcd45b3c1 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -20,7 +20,11 @@ class TestModeI16(PillowTestCase): self.assertEqual( p1, p2, - ("got %r from mode %s at %s, expected %r" % (p1, im1.mode, xy, p2)), + ( + "got {!r} from mode {} at {}, expected {!r}".format( + p1, im1.mode, xy, p2 + ) + ), ) def test_basic(self): @@ -43,10 +47,10 @@ class TestModeI16(PillowTestCase): filename = self.tempfile("temp.im") imIn.save(filename) - imOut = Image.open(filename) + with Image.open(filename) as imOut: - self.verify(imIn) - self.verify(imOut) + self.verify(imIn) + self.verify(imOut) imOut = imIn.crop((0, 0, w, h)) self.verify(imOut) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 872ecdbb6..d3c4ce11d 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,8 +1,8 @@ -from __future__ import print_function +import unittest from PIL import Image -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper try: import numpy @@ -101,9 +101,9 @@ class TestNumpy(PillowTestCase): self.assert_deep_equal(px[x, y], np[y, x]) def test_16bit(self): - img = Image.open("Tests/images/16bit.cropped.tif") - np_img = numpy.array(img) - self._test_img_equals_nparray(img, np_img) + with Image.open("Tests/images/16bit.cropped.tif") as img: + np_img = numpy.array(img) + self._test_img_equals_nparray(img, np_img) self.assertEqual(np_img.dtype, numpy.dtype(" 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 1ac226c9d..000000000 --- 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 index f26f2c037..6ddbb08ff 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,7 +2,7 @@ # 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 +# https://docs.microsoft.com/azure/devops/pipelines/ecosystems/python jobs: @@ -51,6 +51,11 @@ jobs: docker: 'centos-7-amd64' name: 'centos_7_amd64' +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'centos-8-amd64' + name: 'centos_8_amd64' + - template: .azure-pipelines/jobs/test-docker.yml parameters: docker: 'amazon-1-amd64' @@ -63,10 +68,10 @@ jobs: - template: .azure-pipelines/jobs/test-docker.yml parameters: - docker: 'fedora-29-amd64' - name: 'fedora_29_amd64' + docker: 'fedora-30-amd64' + name: 'fedora_30_amd64' - template: .azure-pipelines/jobs/test-docker.yml parameters: - docker: 'fedora-30-amd64' - name: 'fedora_30_amd64' + docker: 'fedora-31-amd64' + name: 'fedora_31_amd64' diff --git a/depends/README.rst b/depends/README.rst index 069d2b81f..ce88fa47b 100644 --- a/depends/README.rst +++ b/depends/README.rst @@ -7,24 +7,3 @@ build & install non-packaged dependencies; useful for testing with Travis CI. ``install_extra_test_images.sh`` can be used to install additional test images that are used for Travis CI and AppVeyor. - -The other scripts can be used to install all of the dependencies for -the listed operating systems/distros. The ``ubuntu_14.04.sh`` and -``debian_8.2.sh`` scripts have been tested on bare AWS images and will -install all required dependencies for the system Python 2.7 and 3.4 -for all of the optional dependencies. Git may also be required prior -to running the script to actually download Pillow. - -e.g.:: - - $ sudo apt-get install git - $ git clone https://github.com/python-pillow/Pillow.git - $ cd Pillow/depends - $ ./debian_8.2.sh - $ cd .. - $ git checkout [branch or tag] - $ virtualenv -p /usr/bin/python2.7 ~/vpy27 - $ source ~/vpy27/bin/activate - $ make install - $ make test - diff --git a/depends/alpine_Dockerfile b/depends/alpine_Dockerfile deleted file mode 100644 index 69bdf84f6..000000000 --- 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 c4f72bf8e..000000000 --- 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 5bdcf7f17..000000000 --- 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 36d9c1069..000000000 --- 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 0a98fc9d9..36af34b54 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 0120dbc0b..1f2b677fd 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/termux.sh b/depends/termux.sh index f117790c5..1acc09c44 100755 --- a/depends/termux.sh +++ b/depends/termux.sh @@ -1,5 +1,5 @@ #!/bin/sh -pkg -y install python python-dev ndk-sysroot clang make \ - libjpeg-turbo-dev +pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo diff --git a/depends/ubuntu_12.04.sh b/depends/ubuntu_12.04.sh deleted file mode 100755 index 9bfae43b0..000000000 --- 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 f7d28fba7..000000000 --- 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/Makefile b/docs/Makefile index 1a912039e..ba79d9070 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." diff --git a/docs/about.rst b/docs/about.rst index 323593a36..ce6537e14 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 a9ca91de7..66effdfb2 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 @@ source_suffix = ".rst" 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-2019 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 @@ -220,8 +219,8 @@ latex_documents = [ ( master_doc, "PillowPILFork.tex", - u"Pillow (PIL Fork) Documentation", - u"Alex Clark", + "Pillow (PIL Fork) Documentation", + "Alex Clark", "manual", ) ] @@ -252,7 +251,7 @@ latex_documents = [ # 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 @@ texinfo_documents = [ ( 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 f00f3e31f..a9b534dca 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,29 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -Image.__del__ -~~~~~~~~~~~~~ - -.. deprecated:: 6.1.0 - -Implicitly closing the image's underlying file in ``Image.__del__`` has been deprecated. -Use a context manager or call ``Image.close()`` instead to close the file in a -deterministic way. - -Deprecated: - -.. 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") - Python 2.7 ~~~~~~~~~~ @@ -45,63 +22,6 @@ 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 - -The version constants of individual plugins have been deprecated and will be removed in -a future version. Use ``PIL.__version__`` instead. - -=============================== ================================= ================================== -Deprecated Deprecated Deprecated -=============================== ================================= ================================== -``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__`` -=============================== ================================= ================================== - -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. - ImageCms.CmsProfile attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -128,6 +48,80 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +Image.__del__ +~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. +Use a context manager or call ``Image.close()`` instead to close the file in a +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") + +PILLOW_VERSION constant +~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +PIL.*ImagePlugin.__version__ attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +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 +~~~~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since +2018-08-31 and PySide since 2015-10-14. + +Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 +or PySide2. + +Setting the size of TIFF images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws +an error. Use ``Image.resize`` instead. + VERSION constant ~~~~~~~~~~~~~~~~ diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index be493a316..45f63f164 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(">> 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 @@ -89,7 +87,6 @@ Create JPEG thumbnails :: - from __future__ import print_function import os, sys from PIL import Image @@ -99,9 +96,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 +117,6 @@ Identify Image Files :: - from __future__ import print_function import sys from PIL import Image @@ -247,7 +243,7 @@ Transposing an image out = im.transpose(Image.ROTATE_270) ``transpose(ROTATE)`` operations can also be performed identically with -:py:meth:`~PIL.Image.Image.rotate` operations, provided the `expand` flag is +:py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is true, to provide for the same changes to the image's size. A more general form of image transformations can be carried out via the @@ -267,7 +263,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 +380,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 +419,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 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. @@ -513,12 +512,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/index.rst b/docs/index.rst index 424ccd521..034da6eed 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,6 +3,8 @@ 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 diff --git a/docs/installation.rst b/docs/installation.rst index 35547cb55..dba4d5ec0 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -15,23 +15,26 @@ Notes .. note:: Pillow is supported on the following Python versions -+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|**Python** |**2.4**|**2.5**|**2.6**|**2.7**|**3.2**|**3.3**|**3.4**|**3.5**|**3.6**|**3.7**| -+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow < 2.0.0 | Yes | Yes | Yes | Yes | | | | | | | -+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 2.x - 3.x | | | Yes | Yes | Yes | Yes | Yes | Yes | | | -+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 4.x | | | | Yes | | Yes | Yes | Yes | Yes | | -+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 5.0.x - 5.1.x | | | | Yes | | | Yes | Yes | Yes | | -+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 5.2.x - 5.4.x | | | | Yes | | | Yes | Yes | Yes | Yes | -+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 6.x | | | | Yes | | | | Yes | Yes | Yes | -+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow >= 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 | 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,7 +47,8 @@ Basic Installation Install Pillow with :command:`pip`:: - $ pip install Pillow + python -m pip install pip + python -m pip install Pillow Windows Installation @@ -55,7 +59,8 @@ 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:: - > pip install Pillow + python -m pip install pip + python -m pip install Pillow macOS Installation @@ -66,7 +71,8 @@ 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:: - $ pip install Pillow + python -m pip install pip + python -m pip install 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 + python -m pip install pip + python -m pip install 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,10 +129,9 @@ 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: @@ -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,12 @@ 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 Once you have installed the prerequisites, run:: - $ pip install Pillow + python -m pip install pip + python -m pip install 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 +206,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" python -m pip install pillow If Pillow has been previously built without the required prerequisites, it may be necessary to manually clear the pip cache or @@ -246,11 +250,11 @@ Build Options Sample usage:: - $ MAX_CONCURRENCY=1 python setup.py build_ext --enable-[feature] install + MAX_CONCURRENCY=1 python setup.py build_ext --enable-[feature] install or using pip:: - $ pip install pillow --global-option="build_ext" --global-option="--enable-[feature]" + python -m pip install pillow --global-option="build_ext" --global-option="--enable-[feature]" Building on macOS @@ -266,21 +270,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 + python -m pip install pip + python -m pip install Pillow or from within the uncompressed source directory:: - $ python setup.py install + python setup.py install Building on Windows ^^^^^^^^^^^^^^^^^^^ @@ -294,17 +299,17 @@ 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 + 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 Then see ``depends/install_raqm_cmake.sh`` to install libraqm. @@ -317,33 +322,33 @@ development libraries installed. In Debian or Ubuntu:: - $ sudo apt-get install python-dev python-setuptools + 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 + 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 \ + 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 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 @@ -360,8 +365,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 +385,46 @@ Continuous Integration Targets These platforms are built and tested for every change. -+----------------------------------+-------------------------------+-----------------------+ -|**Operating system** |**Tested Python versions** |**Tested Architecture**| -+----------------------------------+-------------------------------+-----------------------+ -| Alpine | 2.7, 3.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Arch | 2.7, 3.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Amazon Linux 1 | 2.7, 3.6 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Amazon Linux 2 | 2.7, 3.6 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| CentOS 6 | 2.7, 3.6 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| CentOS 7 | 2.7, 3.6 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Debian 9 Stretch | 2.7, 3.5 |x86 | -+----------------------------------+-------------------------------+-----------------------+ -| Debian 10 Buster | 2.7, 3.7 |x86 | -+----------------------------------+-------------------------------+-----------------------+ -| Fedora 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 | -+----------------------------------+-------------------------------+-----------------------+ ++----------------------------------+--------------------------+-----------------------+ +|**Operating system** |**Tested Python versions**|**Tested architecture**| ++----------------------------------+--------------------------+-----------------------+ +| Alpine | 3.7 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Arch | 3.7 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Amazon Linux 1 | 3.6 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| Amazon Linux 2 | 3.6 |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.13 High Sierra | 3.5, 3.6, 3.7, 3.8 |x86-64 | ++----------------------------------+--------------------------+-----------------------+ +| macOS 10.15 Catalina | 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 | ++----------------------------------+--------------------------+-----------------------+ -\* macOS CI is not run for every commit, but is run for every release. Other Platforms ^^^^^^^^^^^^^^^ diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index cfbcb8b6b..8af56f6c1 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -52,7 +52,7 @@ Functions .. warning:: To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files which decompress into a huge amount of data and are designed to crash or cause disruption by using up - a lot of memory), Pillow will issue a `DecompressionBombWarning` if the image is over a certain + a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the image is over a certain limit. If desired, the warning can be turned into an error with ``warnings.simplefilter('error', Image.DecompressionBombWarning)`` or suppressed entirely with ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also `the logging @@ -262,10 +262,10 @@ Instances of the :py:class:`Image` class have the following attributes: .. py:attribute:: filename The filename or path of the source file. Only images created with the - factory function `open` have a filename attribute. If the input is a + factory function ``open`` have a filename attribute. If the input is a file like object, the filename attribute is set to an empty string. - :type: :py:class: `string` + :type: :py:class:`string` .. py:attribute:: format diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index ea6334708..922e1685a 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -33,13 +33,13 @@ can be easily displayed in a chromaticity diagram, for example). .. py:attribute:: version The version number of the ICC standard that this profile follows - (e.g. `2.0`). + (e.g. ``2.0``). :type: :py:class:`float` .. py:attribute:: icc_version - Same as `version`, but in encoded format (see 7.2.4 of ICC.1:2010). + Same as ``version``, but in encoded format (see 7.2.4 of ICC.1:2010). .. py:attribute:: device_class diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 5fac7914b..51eaf925e 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -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) +.. 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) Draws the string at the given position. @@ -297,6 +297,15 @@ Methods .. versionadded:: 6.0.0 + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param stroke_fill: Color to use for the text stroke. If not given, will default to + the ``fill`` parameter. + + .. versionadded:: 6.2.0 + .. 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) Draws the string at the given position. @@ -336,7 +345,7 @@ Methods .. versionadded:: 6.0.0 -.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None) +.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) Return the size of the given string, in pixels. @@ -372,7 +381,11 @@ Methods .. versionadded:: 6.0.0 -.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None) + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) Return the size of the given string, in pixels. @@ -408,6 +421,10 @@ Methods .. versionadded:: 6.0.0 + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + .. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None) .. warning:: This method is experimental. diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index ed7482e99..e94e21cb9 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -11,7 +11,7 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 -.. py:function:: PIL.ImageGrab.grab(bbox=None, include_layered_windows=False) +.. py:function:: PIL.ImageGrab.grab(bbox=None, include_layered_windows=False, all_screens=False) Take a snapshot of the screen. The pixels inside the bounding box are returned as an "RGB" image on Windows or "RGBA" on macOS. @@ -20,7 +20,13 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS) :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. :param include_layered_windows: Includes layered windows. Windows OS only. + + .. versionadded:: 6.1.0 + :param all_screens: Capture all monitors. Windows OS only. + + .. versionadded:: 6.2.0 :return: An image .. py:function:: PIL.ImageGrab.grabclipboard() diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index 50cea90ca..1c86d168f 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/ImagePath.rst b/docs/reference/ImagePath.rst index 978db4caf..5ab350ef3 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -53,7 +53,7 @@ vector data. Path objects can be passed to the methods on the Converts the path to a Python list [(x, y), …]. :param flat: By default, this function returns a list of 2-tuples - [(x, y), ...]. If this argument is `True`, it + [(x, y), ...]. If this argument is ``True``, it returns a flat list [x, y, ...] instead. :return: A list of coordinates. See **flat**. diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 5128f28fb..7dd7084db 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/open_files.rst b/docs/reference/open_files.rst index e26d9e639..ed0ab1a0c 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')) + with Image.open(pathlib.Path('test.jpg')) as im2: + ... - f = open('test.jpg', 'rb') - im3 = Image.open(f) + with open('test.jpg', 'rb') as f: + im3 = Image.open(f) + ... with open('test.jpg', 'rb') as f: im4 = Image.open(io.BytesIO(f.read())) - - # Dangerous FAIL: - with open('test.jpg', 'rb') as f: - im5 = Image.open(f) - im5.load() # FAILS, closed file + ... 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/2.7.0.rst b/docs/releasenotes/2.7.0.rst index 4bb25e371..931f9fd1e 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -27,7 +27,7 @@ Image resizing filters ---------------------- Image resizing methods :py:meth:`~PIL.Image.Image.resize` and -:py:meth:`~PIL.Image.Image.thumbnail` take a `resample` argument, which tells +:py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells which filter should be used for resampling. Possible values are: :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`, :py:attr:`PIL.Image.BICUBIC` and :py:attr:`PIL.Image.ANTIALIAS`. diff --git a/docs/releasenotes/2.8.0.rst b/docs/releasenotes/2.8.0.rst index 85235d72a..c522fe8b0 100644 --- a/docs/releasenotes/2.8.0.rst +++ b/docs/releasenotes/2.8.0.rst @@ -4,18 +4,28 @@ Open HTTP response objects with Image.open ------------------------------------------ -HTTP response objects returned from `urllib2.urlopen(url)` or `requests.get(url, stream=True).raw` are 'file-like' but do not support `.seek()` operations. As a result PIL was unable to open them as images, requiring a wrap in `cStringIO` or `BytesIO`. +HTTP response objects returned from ``urllib2.urlopen(url)`` or +``requests.get(url, stream=True).raw`` are 'file-like' but do not support ``.seek()`` +operations. As a result PIL was unable to open them as images, requiring a wrap in +``cStringIO`` or ``BytesIO``. -Now new functionality has been added to `Image.open()` by way of an `.seek(0)` check and catch on exception `AttributeError` or `io.UnsupportedOperation`. If this is caught we attempt to wrap the object using `io.BytesIO` (which will only work on buffer-file-like objects). +Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and +catch on exception ``AttributeError`` or ``io.UnsupportedOperation``. If this is caught we +attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like +objects). -This allows opening of files using both `urllib2` and `requests`, e.g.:: +This allows opening of files using both ``urllib2`` and ``requests``, e.g.:: Image.open(urllib2.urlopen(url)) Image.open(requests.get(url, stream=True).raw) -If the response uses content-encoding (compression, either gzip or deflate) then this will fail as both the urllib2 and requests raw file object will produce compressed data in that case. Using Content-Encoding on images is rather non-sensical as most images are already compressed, but it can still happen. +If the response uses content-encoding (compression, either gzip or deflate) then this +will fail as both the urllib2 and requests raw file object will produce compressed data +in that case. Using Content-Encoding on images is rather non-sensical as most images are +already compressed, but it can still happen. -For requests the work-around is to set the decode_content attribute on the raw object to True:: +For requests the work-around is to set the decode_content attribute on the raw object to +True:: response = requests.get(url, stream=True) response.raw.decode_content = True diff --git a/docs/releasenotes/3.0.0.rst b/docs/releasenotes/3.0.0.rst index 9cc1de98c..67569d337 100644 --- a/docs/releasenotes/3.0.0.rst +++ b/docs/releasenotes/3.0.0.rst @@ -5,8 +5,8 @@ Saving Multipage Images ----------------------- -There is now support for saving multipage images in the `GIF` and -`PDF` formats. To enable this functionality, pass in `save_all=True` +There is now support for saving multipage images in the ``GIF`` and +``PDF`` formats. To enable this functionality, pass in ``save_all=True`` as a keyword argument to the save:: im.save('test.pdf', save_all=True) @@ -37,7 +37,7 @@ have been removed in this release:: ImageDraw.setink() ImageDraw.setfill() The ImageFileIO module - The ImageFont.FreeTypeFont and ImageFont.truetype `file` keyword arg + The ImageFont.FreeTypeFont and ImageFont.truetype ``file`` keyword arg The ImagePalette private _make functions ImageWin.fromstring() ImageWin.tostring() diff --git a/docs/releasenotes/3.1.0.rst b/docs/releasenotes/3.1.0.rst index 388af03ac..3cdb6939d 100644 --- a/docs/releasenotes/3.1.0.rst +++ b/docs/releasenotes/3.1.0.rst @@ -5,8 +5,8 @@ ImageDraw arc, chord and pieslice can now use floats ---------------------------------------------------- -There is no longer a need to ensure that the start and end arguments for `arc`, -`chord` and `pieslice` are integers. +There is no longer a need to ensure that the start and end arguments for ``arc``, +``chord`` and ``pieslice`` are integers. Note that these numbers are not simply rounded internally, but are actually utilised in the drawing process. diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst new file mode 100644 index 000000000..c31ade197 --- /dev/null +++ b/docs/releasenotes/6.2.0.rst @@ -0,0 +1,88 @@ +6.2.0 +----- + +API Additions +============= + +Text stroking +^^^^^^^^^^^^^ + +``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing +operations. They allow text to be outlined, setting the width of the stroke and +and the color respectively. If not provided, ``stroke_fill`` will default to +the ``fill`` parameter. + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 40) + font.getsize_multiline("A", stroke_width=2) + font.getsize("ABC\nAaaa", stroke_width=2) + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.textsize("A", font, stroke_width=2) + draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0") + draw.multiline_text((10, 10), "A\nB", "#f00", font, + stroke_width=2, stroke_fill="#0f0") + +For example, + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0") + + +creates the following image: + +.. image:: ../../Tests/images/imagedraw_stroke_different.png + +ImageGrab on multi-monitor Windows +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An ``all_screens`` argument has been added to ``ImageGrab.grab``. If ``True``, +all monitors will be included in the created image. + +API Changes +=========== + +Image.getexif +^^^^^^^^^^^^^ + +To allow for lazy loading of Exif data, ``Image.getexif()`` now returns a +shared instance of ``Image.Exif``. + +Deprecations +^^^^^^^^^^^^ + +Image.frombuffer +~~~~~~~~~~~~~~~~ + +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. + +Other Changes +============= + +Removed bdist_wininst .exe installers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.exe installers fell out of favour with PEP 527, and will be deprecated in +Python 3.8. Pillow will no longer be distributing them. Wheels should be used +instead. + +Flags for libwebp in wheels +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When building libwebp for inclusion in wheels, Pillow now adds the -O3 and +-DNDEBUG CFLAGS. These flags would be used by default if building libwebp +without debugging, and using them fixes a significant decrease in speed when +a wheel-installed copy of Pillow performs libwebp operations. diff --git a/docs/releasenotes/6.2.1.rst b/docs/releasenotes/6.2.1.rst new file mode 100644 index 000000000..ca298fa70 --- /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/7.0.0.rst b/docs/releasenotes/7.0.0.rst new file mode 100644 index 000000000..e0e764342 --- /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/index.rst b/docs/releasenotes/index.rst index 400288883..6f8ecb95c 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,6 +6,9 @@ Release Notes .. toctree:: :maxdepth: 2 + 7.0.0 + 6.2.1 + 6.2.0 6.1.0 6.0.0 5.4.1 diff --git a/mp_compile.py b/mp_compile.py deleted file mode 100644 index ec73e927e..000000000 --- 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/selftest.py b/selftest.py index dcac54a5a..ea52256f7 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 @@ if __name__ == "__main__": 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.py b/setup.py index 76bdfb159..fe024ceae 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 distutils.command.build_ext import build_ext 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 @@ _LIB_IMAGING = ( "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") @@ -303,8 +301,7 @@ class pil_build_ext(build_ext): return getattr(self, feat) is None def __iter__(self): - for x in self.features: - yield x + yield from self.features feature = feature() @@ -332,12 +329,15 @@ class pil_build_ext(build_ext): 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 +345,7 @@ class pil_build_ext(build_ext): _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) @@ -717,7 +717,7 @@ class pil_build_ext(build_ext): defs.append(("HAVE_LIBTIFF", 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 +798,7 @@ class pil_build_ext(build_ext): 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) @@ -821,7 +821,7 @@ class pil_build_ext(build_ext): 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 @@ -857,16 +857,21 @@ try: 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", + }, 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,7 +880,7 @@ try: "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, diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index fdf2c097e..7a485cf80 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 7b97964a8..5ccba37db 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 @@ class BmpImageFile(ImageFile.ImageFile): # ------------------------------- 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 @@ class BmpImageFile(ImageFile.ImageFile): ): 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 56cac3bb1..48f21e1b3 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -60,7 +60,7 @@ class BufrStubImageFile(ImageFile.StubImageFile): 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 3cf9d82d2..9727601ab 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. diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index 9e2d8c96f..3a1b6d2e5 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 57c321417..7d2aff325 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -25,10 +25,6 @@ from . import Image 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 b2d508942..9ba6e0ff8 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/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index 7e6d35ee5..c2ce8651c 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -63,7 +63,7 @@ class FITSStubImageFile(ImageFile.StubImageFile): 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 82015e2fc..9bf7d74d6 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 e57c2f3fd..979a1e33c 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 15ebe0e3b..3938f0f09 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 @@ class FpxImageFile(ImageFile.ImageFile): 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": @@ -145,7 +138,7 @@ class FpxImageFile(ImageFile.ImageFile): 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 +211,7 @@ class FpxImageFile(ImageFile.ImageFile): 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 06f4a72d0..096ccacac 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -79,7 +79,7 @@ class FtexImageFile(ImageFile.ImageFile): 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 07f5ab683..5f9ba59c1 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -25,15 +25,12 @@ # import itertools +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 @@ -265,6 +262,7 @@ class GifImageFile(ImageFile.ImageFile): self.dispose = None elif self.disposal_method == 2: # replace with background colour + Image._decompression_bomb_check(self.size) self.dispose = Image.core.fill("P", self.size, self.info["background"]) else: # replace with previous contents @@ -616,42 +614,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() + # Allow ppmquant to receive SIGPIPE if ppmtogif exits + quant_proc.stdout.close() - retcode = quant_proc.wait() - if retcode: - raise CalledProcessError(retcode, quant_cmd) + retcode = quant_proc.wait() + if retcode: + raise subprocess.CalledProcessError(retcode, quant_cmd) - retcode = togif_proc.wait() - if retcode: - raise CalledProcessError(retcode, togif_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 @@ -852,7 +852,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 f48e7f76e..851e24d62 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 @@ -132,7 +132,7 @@ class GimpGradientFile(GradientFile): 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 2994bbeab..e3060ab8a 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -22,7 +22,7 @@ from ._binary import o8 # 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 8a24a9829..515c272f7 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -61,7 +61,7 @@ class GribStubImageFile(ImageFile.StubImageFile): 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 a3ea12f99..362f2d399 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -60,7 +60,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile): 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 75ea18b6b..18e8403d1 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -19,6 +19,7 @@ import io 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) + 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) + 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 + 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 + # iconutil -c icns -o {} {} - 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_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] + convert_proc = subprocess.Popen( + convert_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) - convert_proc.stdout.close() + convert_proc.stdout.close() - retcode = convert_proc.wait() + retcode = convert_proc.wait() - # remove the temporary files - shutil.rmtree(iconset) - - 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") diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index fc728d6fb..e4a74321b 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -30,10 +30,6 @@ from math import ceil, log 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: - - def __del__(self): - self.__exit__() - def _copy(self): self.load() self.im = self.im.copy() @@ -641,8 +591,6 @@ class Image(object): self.load() def _dump(self, file=None, format=None, **options): - import tempfile - suffix = "" if format: suffix = "." + format @@ -676,10 +624,6 @@ class Image(object): 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__, @@ -1185,16 +1129,18 @@ class Image(object): """ 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. @@ -1324,10 +1270,10 @@ class Image(object): return self.im.getextrema() def getexif(self): - exif = Exif() - if "exif" in self.info: - exif.load(self.info["exif"]) - return exif + if self._exif is None: + self._exif = Exif() + self._exif.load(self.info.get("exif")) + return self._exif def getim(self): """ @@ -1349,10 +1295,7 @@ class Image(object): 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 @@ -1503,7 +1446,7 @@ class Image(object): 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) @@ -1703,10 +1646,7 @@ class Image(object): 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 @@ -1826,7 +1766,24 @@ class Image(object): 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. @@ -1836,13 +1793,26 @@ class Image(object): 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. """ @@ -1864,6 +1834,9 @@ class Image(object): 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: @@ -1884,8 +1857,54 @@ class Image(object): 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) + self = self.reduce((factor_x, factor_y), 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, @@ -1907,7 +1926,7 @@ class Image(object): 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 @@ -2038,7 +2057,7 @@ class Image(object): 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): @@ -2073,10 +2092,10 @@ class Image(object): if open_fp: if params.get("append", False): - fp = builtins.open(filename, "r+b") - else: # Open also for reading ("+"), because TIFF save_all # writer needs to go back and edit the written data. + fp = builtins.open(filename, "r+b") + else: fp = builtins.open(filename, "w+b") try: @@ -2109,15 +2128,15 @@ class Image(object): Displays this image. This method is mainly intended for debugging purposes. - On Unix platforms, this method saves the image to a temporary - PNG file, and calls the **display**, **eog** or **xv** - utility, depending on which one can be found. + The image is first saved to a temporary file. By default, it will be in + PNG format. - On macOS, this method saves the image to a temporary PNG file, and - opens it with the native Preview application. + On Unix, the image is then opened using the **display**, **eog** or + **xv** utility, depending on which one can be found. - On Windows, it saves the image to a temporary BMP file, and uses - the standard BMP display utility to show it (usually Paint). + On macOS, the image is opened with the native Preview application. + + On Windows, the image is opened with the standard PNG display utility. :param title: Optional title to use for the image window, where possible. @@ -2160,7 +2179,7 @@ class Image(object): """ self.load() - if isStringType(channel): + if isinstance(channel, str): try: channel = self.getbands().index(channel) except ValueError: @@ -2176,7 +2195,7 @@ class Image(object): """ 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 @@ -2195,27 +2214,45 @@ class Image(object): 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]) + y = max(round(y * size[0] / x), 1) + x = round(size[0]) if y > size[1]: - x = int(max(x * size[1] / y, 1)) - y = int(size[1]) + x = max(round(x * size[1] / y), 1) + y = round(size[1]) size = x, y + box = None if size == self.size: return - self.draft(None, size) + 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 @@ -2245,12 +2282,14 @@ class Image(object): 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 @@ -2296,6 +2335,7 @@ class Image(object): 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: @@ -2424,12 +2464,12 @@ class Image(object): # 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 @@ -2487,7 +2527,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 @@ -2591,14 +2631,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): if decoder_name == "raw": if args == (): - warnings.warn( - "the frombuffer defaults may change in a future release; " - "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)) @@ -2641,9 +2674,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"]: @@ -2747,16 +2783,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 @@ -2814,7 +2858,9 @@ def open(fp, mode="r"): 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) + ) # @@ -3137,25 +3183,27 @@ class Exif(MutableMapping): def __init__(self): self._data = {} self._ifds = {} + self._info = None + self._loaded_exif = None + + def _fixup(self, value): + try: + if len(value) == 1 and not isinstance(value, dict): + return value[0] + except Exception: + pass + return value def _fixup_dict(self, src_dict): # Helper function for _getexif() # returns a dict with any single item tuples/lists as individual values - def _fixup(value): - try: - if len(value) == 1 and not isinstance(value, dict): - return value[0] - except Exception: - pass - return value - - return {k: _fixup(v) for k, v in src_dict.items()} + return {k: self._fixup(v) for k, v in src_dict.items()} def _get_ifd_dict(self, tag): try: # an offset pointer to the location of the nested embedded IFD. # It should be a long, but may be corrupted. - self.fp.seek(self._data[tag]) + self.fp.seek(self[tag]) except (KeyError, TypeError): pass else: @@ -3172,16 +3220,24 @@ class Exif(MutableMapping): # The EXIF record consists of a TIFF file embedded in a JPEG # application marker (!). + if data == self._loaded_exif: + return + self._loaded_exif = data + self._data.clear() + self._ifds.clear() + self._info = None + if not data: + return + self.fp = io.BytesIO(data[6:]) self.head = self.fp.read(8) # process dictionary from . import TiffImagePlugin - info = TiffImagePlugin.ImageFileDirectory_v1(self.head) - self.endian = info._endian - self.fp.seek(info.next) - info.load(self.fp) - self._data = dict(self._fixup_dict(info)) + self._info = TiffImagePlugin.ImageFileDirectory_v1(self.head) + self.endian = self._info._endian + self.fp.seek(self._info.next) + self._info.load(self.fp) # get EXIF extension ifd = self._get_ifd_dict(0x8769) @@ -3189,12 +3245,6 @@ class Exif(MutableMapping): self._data.update(ifd) self._ifds[0x8769] = ifd - # get gpsinfo extension - ifd = self._get_ifd_dict(0x8825) - if ifd: - self._data[0x8825] = ifd - self._ifds[0x8825] = ifd - def tobytes(self, offset=0): from . import TiffImagePlugin @@ -3203,19 +3253,20 @@ class Exif(MutableMapping): else: head = b"MM\x00\x2A\x00\x00\x00\x08" ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) - for tag, value in self._data.items(): + for tag, value in self.items(): ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) def get_ifd(self, tag): - if tag not in self._ifds and tag in self._data: - if tag == 0xA005: # interop + if tag not in self._ifds and tag in self: + if tag in [0x8825, 0xA005]: + # gpsinfo, interop self._ifds[tag] = self._get_ifd_dict(tag) elif tag == 0x927C: # makernote from .TiffImagePlugin import ImageFileDirectory_v2 - if self._data[0x927C][:8] == b"FUJIFILM": - exif_data = self._data[0x927C] + if self[0x927C][:8] == b"FUJIFILM": + exif_data = self[0x927C] ifd_offset = i32le(exif_data[8:12]) ifd_data = exif_data[ifd_offset:] @@ -3232,7 +3283,7 @@ class Exif(MutableMapping): continue size = count * unit_size if size > 4: - offset, = struct.unpack("H", ifd_data[:2])[0]): @@ -3262,7 +3313,7 @@ class Exif(MutableMapping): ) if ifd_tag == 0x1101: # CameraInfo - offset, = struct.unpack(">L", data) + (offset,) = struct.unpack(">L", data) self.fp.seek(offset) camerainfo = {"ModelID": self.fp.read(4)} @@ -3291,27 +3342,42 @@ class Exif(MutableMapping): return self._ifds.get(tag, {}) def __str__(self): + if self._info is not None: + # Load all keys into self._data + for tag in self._info.keys(): + self[tag] + return str(self._data) def __len__(self): - return len(self._data) + keys = set(self._data) + if self._info is not None: + keys.update(self._info) + return len(keys) def __getitem__(self, tag): + if self._info is not None and tag not in self._data and tag in self._info: + self._data[tag] = self._fixup(self._info[tag]) + if tag == 0x8825: + self._data[tag] = self.get_ifd(tag) + del self._info[tag] return self._data[tag] def __contains__(self, tag): - return tag in self._data - - if not py3: - - def has_key(self, tag): - return tag in self + return tag in self._data or (self._info is not None and tag in self._info) def __setitem__(self, tag, value): + if self._info is not None and tag in self._info: + del self._info[tag] self._data[tag] = value def __delitem__(self, tag): + if self._info is not None and tag in self._info: + del self._info[tag] del self._data[tag] def __iter__(self): - return iter(set(self._data)) + keys = set(self._data) + if self._info is not None: + keys.update(self._info) + return iter(keys) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index ed4eefc0d..d05b3102c 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 @@ for flag in FLAGS.values(): # Profile. -class ImageCmsProfile(object): +class ImageCmsProfile: def __init__(self, profile): """ :param profile: Either a string representing a filename, @@ -161,7 +158,7 @@ class ImageCmsProfile(object): """ - 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())) @@ -374,7 +371,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 +395,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 +476,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 +587,7 @@ def buildProofTransform( proofRenderingIntent, flags, ) - except (IOError, TypeError, ValueError) as v: + except (OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -733,9 +730,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 +772,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 +800,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 +828,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 +857,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 +886,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 +925,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 +976,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/ImageDraw.py b/src/PIL/ImageDraw.py index c9b277388..c6e12150e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,7 +34,6 @@ import math import numbers from . import Image, ImageColor -from ._util import isStringType """ @@ -45,7 +44,7 @@ directly. """ -class ImageDraw(object): +class ImageDraw: def __init__(self, im, mode=None): """ Create a drawing instance. @@ -107,13 +106,13 @@ class ImageDraw(object): 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) @@ -261,24 +260,95 @@ class ImageDraw(object): return text.split(split_character) - def text(self, xy, text, fill=None, font=None, anchor=None, *args, **kwargs): + def text( + self, + xy, + text, + fill=None, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + stroke_fill=None, + *args, + **kwargs + ): if self._multiline_check(text): - return self.multiline_text(xy, text, fill, font, anchor, *args, **kwargs) - ink, fill = self._getink(fill) + return self.multiline_text( + xy, + text, + fill, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + stroke_fill, + ) + if font is None: font = self.getfont() - if ink is None: - ink = fill - if ink is not None: + + def getink(fill): + ink, fill = self._getink(fill) + if ink is None: + return fill + return ink + + def draw_text(ink, stroke_width=0, stroke_offset=None): + coord = xy try: - mask, offset = font.getmask2(text, self.fontmode, *args, **kwargs) - xy = xy[0] + offset[0], xy[1] + offset[1] + mask, offset = font.getmask2( + text, + self.fontmode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + *args, + **kwargs, + ) + coord = coord[0] + offset[0], coord[1] + offset[1] except AttributeError: try: - mask = font.getmask(text, self.fontmode, *args, **kwargs) + mask = font.getmask( + text, + self.fontmode, + direction, + features, + language, + stroke_width, + *args, + **kwargs, + ) except TypeError: mask = font.getmask(text) - self.draw.draw_bitmap(xy, mask, ink) + if stroke_offset: + coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] + self.draw.draw_bitmap(coord, mask, ink) + + ink = getink(fill) + if ink is not None: + stroke_ink = None + if stroke_width: + stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink + + if stroke_ink is not None: + # Draw stroked text + draw_text(stroke_ink, stroke_width) + + # Draw normal text + draw_text(ink, 0, (stroke_width, stroke_width)) + else: + # Only draw normal text + draw_text(ink) def multiline_text( self, @@ -292,14 +362,23 @@ class ImageDraw(object): direction=None, features=None, language=None, + stroke_width=0, + stroke_fill=None, ): widths = [] max_width = 0 lines = self._multiline_split(text) - line_spacing = self.textsize("A", font=font)[1] + spacing + line_spacing = ( + self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing + ) for line in lines: line_width, line_height = self.textsize( - line, font, direction=direction, features=features, language=language + line, + font, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, ) widths.append(line_width) max_width = max(max_width, line_width) @@ -322,32 +401,50 @@ class ImageDraw(object): direction=direction, features=features, language=language, + stroke_width=stroke_width, + stroke_fill=stroke_fill, ) top += line_spacing left = xy[0] def textsize( - self, text, font=None, spacing=4, direction=None, features=None, language=None + self, + text, + font=None, + spacing=4, + direction=None, + features=None, + language=None, + stroke_width=0, ): """Get the size of a given string, in pixels.""" if self._multiline_check(text): return self.multiline_textsize( - text, font, spacing, direction, features, language + text, font, spacing, direction, features, language, stroke_width ) if font is None: font = self.getfont() - return font.getsize(text, direction, features, language) + return font.getsize(text, direction, features, language, stroke_width) def multiline_textsize( - self, text, font=None, spacing=4, direction=None, features=None, language=None + self, + text, + font=None, + spacing=4, + direction=None, + features=None, + language=None, + stroke_width=0, ): max_width = 0 lines = self._multiline_split(text) - line_spacing = self.textsize("A", font=font)[1] + spacing + line_spacing = ( + self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing + ) for line in lines: line_width, line_height = self.textsize( - line, font, spacing, direction, features, language + line, font, spacing, direction, features, language, stroke_width ) max_width = max(max_width, line_width) return max_width, len(lines) * line_spacing - spacing @@ -437,8 +534,9 @@ def floodfill(image, xy, value, border=None, thresh=0): new_edge = set() for (x, y) in edge: # 4 adjacent method for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): - if (s, t) in full_edge: - continue # if already processed, skip + # If already processed, or if a coordinate is negative, skip + if (s, t) in full_edge or s < 0 or t < 0: + continue try: p = pixel[s, t] except (ValueError, IndexError): diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 324d869f0..20b5fe4c4 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 534eb4f16..3b79d5c46 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 2b3d8e40a..c8d6b6ba3 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 @@ class ImageFile(Image.Image): 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 @@ class ImageFile(Image.Image): 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 @@ -203,7 +201,7 @@ class ImageFile(Image.Image): # 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,14 +236,13 @@ class ImageFile(Image.Image): 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: - self.tile = [] - raise IOError( + raise OSError( "image file is truncated " "(%d bytes not processed)" % len(b) ) @@ -323,7 +320,7 @@ class StubImageFile(ImageFile): 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 (!) @@ -335,7 +332,7 @@ class StubImageFile(ImageFile): raise NotImplementedError("StubImageFile subclass must implement _load") -class Parser(object): +class Parser: """ Incremental image parser. This class implements the standard feed/close consumer interface. @@ -412,7 +409,7 @@ class Parser(object): try: with io.BytesIO(self.data) as fp: im = Image.open(fp) - except IOError: + except OSError: # traceback.print_exc() pass # not enough data else: @@ -457,9 +454,9 @@ class Parser(object): 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 @@ -515,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 @@ -530,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() @@ -560,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 @@ -571,7 +568,7 @@ class PyCodecState(object): 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 fa4162b61..6b0f5eb37 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 @@ except ImportError: # pragma: no cover numpy = None -class Filter(object): +class Filter: pass @@ -498,7 +495,7 @@ class Color3DLUT(MultibandFilter): 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 16c1052f4..619800829 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,7 +65,7 @@ except ImportError: # -------------------------------------------------------------------- -class ImageFont(object): +class ImageFont: "PIL font wrapper" def _load_pilfont(self, filename): @@ -79,7 +81,7 @@ class ImageFont(object): if image and image.mode in ("1", "L"): break else: - raise IOError("cannot find glyph data file") + raise OSError("cannot find glyph data file") self.file = fullname @@ -145,7 +147,7 @@ class ImageFont(object): # 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): @@ -207,7 +209,9 @@ class FreeTypeFont(object): """ return self.font.ascent, self.font.descent - def getsize(self, text, direction=None, features=None, language=None): + def getsize( + self, text, direction=None, features=None, language=None, stroke_width=0 + ): """ Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language. @@ -243,13 +247,26 @@ class FreeTypeFont(object): .. versionadded:: 6.0.0 + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + :return: (width, height) """ size, offset = self.font.getsize(text, direction, features, language) - return (size[0] + offset[0], size[1] + offset[1]) + return ( + size[0] + stroke_width * 2 + offset[0], + size[1] + stroke_width * 2 + offset[1], + ) def getsize_multiline( - self, text, direction=None, spacing=4, features=None, language=None + self, + text, + direction=None, + spacing=4, + features=None, + language=None, + stroke_width=0, ): """ Returns width and height (in pixels) of given text if rendered in font @@ -285,13 +302,19 @@ class FreeTypeFont(object): .. versionadded:: 6.0.0 + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + :return: (width, height) """ max_width = 0 lines = self._multiline_split(text) - line_spacing = self.getsize("A")[1] + spacing + line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing for line in lines: - line_width, line_height = self.getsize(line, direction, features, language) + line_width, line_height = self.getsize( + line, direction, features, language, stroke_width + ) max_width = max(max_width, line_width) return max_width, len(lines) * line_spacing - spacing @@ -308,7 +331,15 @@ class FreeTypeFont(object): """ return self.font.getsize(text)[1] - def getmask(self, text, mode="", direction=None, features=None, language=None): + def getmask( + self, + text, + mode="", + direction=None, + features=None, + language=None, + stroke_width=0, + ): """ Create a bitmap for the text. @@ -352,11 +383,20 @@ class FreeTypeFont(object): .. versionadded:: 6.0.0 + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + :return: An internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module. """ return self.getmask2( - text, mode, direction=direction, features=features, language=language + text, + mode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, )[0] def getmask2( @@ -367,6 +407,7 @@ class FreeTypeFont(object): direction=None, features=None, language=None, + stroke_width=0, *args, **kwargs ): @@ -413,13 +454,20 @@ class FreeTypeFont(object): .. versionadded:: 6.0.0 + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + :return: A tuple of an internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ size, offset = self.font.getsize(text, direction, features, language) + size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 im = fill("L", size, 0) - self.font.render(text, im.id, mode == "1", direction, features, language) + self.font.render( + text, im.id, mode == "1", direction, features, language, stroke_width + ) return im, offset def font_variant( @@ -496,7 +544,7 @@ class FreeTypeFont(object): 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): @@ -562,11 +610,25 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): :param size: The requested size, in points. :param index: Which font face to load (default is first available face). - :param encoding: Which font encoding to use (default is Unicode). Common - encodings are "unic" (Unicode), "symb" (Microsoft - Symbol), "ADOB" (Adobe Standard), "ADBE" (Adobe Expert), - and "armn" (Apple Roman). See the FreeType documentation - for more information. + :param encoding: Which font encoding to use (default is Unicode). Possible + encodings include (see the FreeType documentation for more + information): + + * "unic" (Unicode) + * "symb" (Microsoft Symbol) + * "ADOB" (Adobe Standard) + * "ADBE" (Adobe Expert) + * "ADBC" (Adobe Custom) + * "armn" (Apple Roman) + * "sjis" (Shift JIS) + * "gb " (PRC) + * "big5" + * "wans" (Extended Wansung) + * "joha" (Johab) + * "lat1" (Latin-1) + + This specifies the character set to use. It does not alter the + encoding of any text provided in subsequent operations. :param layout_engine: Which layout engine to use, if available: `ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`. :return: A font object. @@ -578,7 +640,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): try: return freetype(font) - except IOError: + except OSError: if not isPath(font): raise ttf_filename = os.path.basename(font) @@ -635,15 +697,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(): @@ -653,9 +712,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 8f359e229..e587d942d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -15,21 +15,18 @@ # See the README file for information on usage and redistribution. # +import os +import subprocess import sys +import tempfile from . import Image -if sys.platform == "win32": - grabber = Image.core.grabscreen -elif sys.platform == "darwin": - import os - import tempfile - import subprocess -else: +if sys.platform not in ["win32", "darwin"]: raise ImportError("ImageGrab is macOS and Windows only") -def grab(bbox=None, include_layered_windows=False): +def grab(bbox=None, include_layered_windows=False, all_screens=False): if sys.platform == "darwin": fh, filepath = tempfile.mkstemp(".png") os.close(fh) @@ -37,8 +34,10 @@ def grab(bbox=None, include_layered_windows=False): im = Image.open(filepath) im.load() os.unlink(filepath) + if bbox: + im = im.crop(bbox) else: - size, data = grabber(include_layered_windows) + offset, size, data = Image.core.grabscreen(include_layered_windows, all_screens) im = Image.frombytes( "RGB", size, @@ -49,8 +48,10 @@ def grab(bbox=None, include_layered_windows=False): (size[0] * 3 + 3) & -4, -1, ) - if bbox: - im = im.crop(bbox) + if bbox: + x0, y0 = offset + left, top, right, bottom = bbox + im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) return im diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 392151c10..adbb94000 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. # +import builtins + from . import Image, _imagingmath -from ._util import py3 - -try: - import builtins -except ImportError: - import __builtin__ - - builtins = __builtin__ 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 @@ class _Operand(object): # 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 @@ class _Operand(object): 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 596be7b9d..988288329 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 61199234b..d1ec09eac 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 @@ MIRROR_MATRIX = [ # 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 @@ class LutBuilder(object): 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 cfdd5c02a..4391af569 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,7 +21,6 @@ import functools 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 @@ -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: @@ -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). @@ -426,7 +426,11 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): output_ratio = float(size[0]) / size[1] # figure out if the sides or top/bottom will be cropped off - if live_size_ratio >= output_ratio: + if live_size_ratio == output_ratio: + # live_size is already the needed ratio + crop_width = live_size[0] + crop_height = live_size[1] + elif live_size_ratio >= output_ratio: # live_size is wider than what's needed, crop the sides crop_width = output_ratio * live_size[1] crop_height = live_size[1] diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 2d4f5cb6b..e0d439c98 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -21,7 +21,7 @@ import array 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 2edb0a12b..dfe2f80bd 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 @@ for qt_version, qt_module in qt_versions: 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 @@ if qt_is_installed: # 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 f9be92d48..4e9f5c210 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 @@ class Iterator(object): 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 13df7dfbe..f7e809279 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 @@ -105,7 +99,8 @@ class Viewer(object): if sys.platform == "win32": class WindowsViewer(Viewer): - format = "BMP" + format = "PNG" + options = {"compress_level": 1} def get_command(self, file, **options): return ( @@ -126,10 +121,8 @@ elif sys.platform == "darwin": # 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 @@ -153,23 +146,13 @@ else: # 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""" @@ -191,7 +174,7 @@ else: command = executable = "display" return command, executable - if which("display"): + if shutil.which("display"): register(DisplayViewer) class EogViewer(UnixViewer): @@ -199,7 +182,7 @@ else: command = executable = "eog" return command, executable - if which("eog"): + if shutil.which("eog"): register(EogViewer) class XVViewer(UnixViewer): @@ -211,7 +194,7 @@ else: command += " -name %s" % quote(title) return command, executable - if which("xv"): + if shutil.which("xv"): register(XVViewer) if __name__ == "__main__": diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 9ba16fd85..50bafc972 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -26,7 +26,7 @@ import math 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 fd480007a..ee707cffb 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 @@ class PhotoImage(object): # 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 _show(image, title): 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 ed2c18ec4..927b1694b 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 @@ class HDC(object): 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 @@ class HWND(object): 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 @@ class Dib(object): 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 @@ class ImageWindow(Window): 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 a9e991fbe..21ffd7475 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -19,11 +19,6 @@ import re 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 aedf2e48c..86a7ee8cb 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 @@ class IptcImageFile(ImageFile.ImageFile): # 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 @@ class IptcImageFile(ImageFile.ImageFile): 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): @@ -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 37f111778..2c51d3678 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -18,10 +18,6 @@ import struct 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 diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index f1a2f7813..82fc12e10 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 @@ -115,7 +109,10 @@ def APP(self, marker): while blocks[offset : offset + 4] == b"8BIM": offset += 4 # resource code - code = i16(blocks, offset) + try: + code = i16(blocks, offset) + except struct.error: + break offset += 2 # resource name (usually empty) name_len = i8(blocks[offset]) @@ -158,7 +155,7 @@ def APP(self, marker): # If DPI isn't in JPEG header, fetch from EXIF if "dpi" not in self.info and "exif" in self.info: try: - exif = self._getexif() + exif = self.getexif() resolution_unit = exif[0x0128] x_resolution = exif[0x011A] try: @@ -169,10 +166,11 @@ def APP(self, marker): # 1 dpcm = 2.54 dpi dpi *= 2.54 self.info["dpi"] = int(dpi + 0.5), int(dpi + 0.5) - except (KeyError, SyntaxError, ZeroDivisionError): + except (KeyError, SyntaxError, ValueError, ZeroDivisionError): # SyntaxError for invalid/unreadable EXIF # KeyError for dpi not included # ZeroDivisionError for invalid dpi rational value + # ValueError for x_resolution[0] being an invalid float self.info["dpi"] = 72, 72 @@ -415,7 +413,8 @@ class JpegImageFile(ImageFile.ImageFile): return d, e, o, a = self.tile[0] - scale = 0 + scale = 1 + original_size = self.size if a[0] == "RGB" and mode in ["L", "YCbCr"]: self.mode = mode @@ -438,16 +437,13 @@ class JpegImageFile(ImageFile.ImageFile): self.tile = [(d, e, o, a)] self.decoderconfig = (scale, 0) - return self + box = (0, 0, original_size[0] / float(scale), original_size[1] / float(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): @@ -485,19 +481,9 @@ def _fixup_dict(src_dict): def _getexif(self): - # Use the cached version if possible - try: - return self.info["parsed_exif"] - except KeyError: - pass - if "exif" not in self.info: return None - exif = dict(self.getexif()) - - # Cache the result for future use - self.info["parsed_exif"] = exif - return exif + return dict(self.getexif()) def _getmp(self): @@ -628,7 +614,7 @@ 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 @@ -652,7 +638,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": @@ -673,7 +659,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) @@ -782,9 +768,6 @@ def _save(im, fp, filename): 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 387844f8e..012bf81b0 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -33,7 +33,10 @@ Possible subsampling values are 0, 1 and 2 that correspond to 4:4:4, 4:2:2 and 4:2:0. You can get the subsampling of a JPEG with the -`JpegImagePlugin.get_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 bddd33abb..cd047fe9d 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -20,10 +20,6 @@ import struct 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 b48905bda..8610988fc 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -21,11 +21,6 @@ import olefile 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 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): 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 9c662fcc2..a358dfdce 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 81b37172a..e97176d57 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,13 +82,14 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): self.offset = self.__mpoffsets[frame] self.fp.seek(self.offset + 2) # skip SOI marker - if "parsed_exif" in self.info: - del self.info["parsed_exif"] - 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) - exif = self._getexif() + exif = self.getexif() if 40962 in exif and 40963 in exif: self._size = (exif[40962], exif[40963]) elif "exif" in self.info: diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 7315ab66e..2b2937ecf 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -29,11 +29,6 @@ import struct 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 @@ class MspDecoder(ImageFile.PyDecoder): "<%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 @@ class MspDecoder(ImageFile.PyDecoder): 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 @@ class MspDecoder(ImageFile.PyDecoder): 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 @@ Image.register_decoder("MSP", MspDecoder) 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 f37701ce9..90bcad036 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 @@ class PSDraw(object): 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 @@ class PSDraw(object): """ 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)) @@ -133,12 +132,12 @@ class PSDraw(object): 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 ab22d5f0c..73f1b4b27 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -19,7 +19,7 @@ from ._binary import o8 # 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 dd068d794..804ece34a 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 6f01845ec..625f55646 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 074124612..605cbbdf3 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -62,7 +62,7 @@ class PcfFontFile(FontFile.FontFile): 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 +184,7 @@ class PcfFontFile(FontFile.FontFile): 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): diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 397af8c10..6cf10deb3 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -33,10 +33,6 @@ from ._binary import i8, i16le as i16, o8, o16le as o16 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 @@ class PcxImageFile(ImageFile.ImageFile): 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 1fd40f5ba..d9bbf6fab 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -24,12 +24,7 @@ import io 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 diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 0ec6bba14..3267ee491 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -7,24 +7,9 @@ import re 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 +def make_bytes(s): + return s.encode("us-ascii") # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -34,57 +19,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): @@ -247,21 +230,15 @@ class PdfName: 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(make_bytes("#%02X" % b)) return bytes(result) __str__ = __bytes__ @@ -274,13 +251,10 @@ class PdfArray(list): __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 +298,13 @@ class PdfDict(UserDict): 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 make_bytes("<%s>" % "".join("%02X" % b for b in self.data)) class PdfStream: @@ -382,9 +346,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 +433,7 @@ class PdfParser: 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() diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index dc71ca17a..5ea32ba89 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 658f5c04b..ee1400d67 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -38,11 +38,6 @@ 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" logger = logging.getLogger(__name__) @@ -102,7 +97,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 +175,7 @@ class ChunkStream(object): 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 +207,7 @@ class iTXt(str): return self -class PngInfo(object): +class PngInfo: """ PNG chunk container (for use with save(pnginfo=)) @@ -293,8 +288,7 @@ class PngInfo(object): class PngStream(ChunkStream): def __init__(self, fp): - - ChunkStream.__init__(self, fp) + super().__init__(fp) # local copies of Image attributes self.im_info = {} @@ -450,9 +444,8 @@ class PngStream(ChunkStream): 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 +480,8 @@ class PngStream(ChunkStream): 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 +516,13 @@ class PngStream(ChunkStream): 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)) @@ -612,7 +603,7 @@ class PngImageFile(ImageFile.ImageFile): rawmode, data = self.png.im_palette self.palette = ImagePalette.raw(rawmode, data) - self.__idat = length # used by load_read() + self.__prepare_idat = length # used by load_prepare() @property def text(self): @@ -645,6 +636,7 @@ class PngImageFile(ImageFile.ImageFile): if self.info.get("interlace"): self.decoderconfig = self.decoderconfig + (1,) + self.__idat = self.__prepare_idat # used by load_read() ImageFile.ImageFile.load_prepare(self) def load_read(self, read_bytes): @@ -745,7 +737,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): @@ -798,7 +790,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 @@ -873,7 +865,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") @@ -890,7 +882,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: @@ -921,7 +912,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): diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index c3e9eed6d..35a77bafb 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 9eb0c0966..cceb85c5b 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 @@ class PsdImageFile(ImageFile.ImageFile): 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:]) @@ -224,9 +220,11 @@ def _layerinfo(file): # skip over blend flags and extra information read(12) # filler name = "" - size = i32(read(4)) + size = i32(read(4)) # length of the extra data field combined = 0 if size: + data_end = file.tell() + size + length = i32(read(4)) if length: file.seek(length - 16, io.SEEK_CUR) @@ -244,7 +242,7 @@ def _layerinfo(file): name = read(length).decode("latin-1", "replace") combined += length + 1 - file.seek(size - combined, io.SEEK_CUR) + file.seek(data_end) layers.append((name, mode, (x0, y0, x1, y1))) # get tiles diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 2ab06f93f..359a94919 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -40,7 +40,7 @@ ffi = FFI() 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 99408fdc3..ddd3de379 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -27,11 +27,6 @@ import struct 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 5663a5b53..dd0620c14 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") @@ -236,7 +234,7 @@ def loadImageSeries(filelist=None): def makeSpiderHeader(im): nsam, nrow = im.size lenbyt = nsam * 4 # There are labrec records in the header - labrec = 1024 / lenbyt + labrec = int(1024 / lenbyt) if 1024 % lenbyt != 0: labrec += 1 labbyt = labrec * lenbyt @@ -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) diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index 74fa5f7bd..fd7ca8a40 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 e180b802c..ede646453 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 @@ class TarIO(ContainerIO.ContainerIO): 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 @@ class TarIO(ContainerIO.ContainerIO): 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 @@ class TarIO(ContainerIO.ContainerIO): 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 b1b351396..fd71e545d 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -22,11 +22,6 @@ import warnings 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 9680a5db6..f8993310e 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. @@ -295,10 +279,6 @@ def _limit_signed_rational(frac, max_val, min_val): return num, denom -def _libtiff_version(): - return Image.core.libtiff_version.split("\n")[0].split("Version ")[1] - - ## # Wrapper for TIFF IFDs. @@ -326,25 +306,21 @@ class IFDRational(Rational): 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, IFDRational): + 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 - self._val = value - - if isinstance(value, IFDRational): - self._denominator = value.denominator - self._numerator = value.numerator - self._val = value._val - return + else: + self._numerator = value + self._denominator = denominator if denominator == 0: self._val = float("nan") - return - elif denominator == 1: self._val = Fraction(value) else: @@ -386,10 +362,10 @@ class IFDRational(Rational): 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)) """ @@ -398,8 +374,6 @@ class IFDRational(Rational): __radd__ = _delegate("__radd__") __sub__ = _delegate("__sub__") __rsub__ = _delegate("__rsub__") - __div__ = _delegate("__div__") - __rdiv__ = _delegate("__rdiv__") __mul__ = _delegate("__mul__") __rmul__ = _delegate("__rmul__") __truediv__ = _delegate("__truediv__") @@ -418,7 +392,7 @@ class IFDRational(Rational): __gt__ = _delegate("__gt__") __le__ = _delegate("__le__") __ge__ = _delegate("__ge__") - __nonzero__ = _delegate("__nonzero__") + __bool__ = _delegate("__bool__") __ceil__ = _delegate("__ceil__") __floor__ = _delegate("__floor__") __round__ = _delegate("__round__") @@ -500,7 +474,7 @@ class ImageFileDirectory_v2(MutableMapping): 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) @@ -547,18 +521,11 @@ class ImageFileDirectory_v2(MutableMapping): 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 @@ -588,14 +555,10 @@ class ImageFileDirectory_v2(MutableMapping): 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 ] @@ -621,7 +584,7 @@ class ImageFileDirectory_v2(MutableMapping): ]: # rationals values = (values,) try: - dest[tag], = values + (dest[tag],) = values except ValueError: # We've got a builtin tag with 1 expected entry warnings.warn( @@ -715,8 +678,6 @@ class ImageFileDirectory_v2(MutableMapping): @_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) @@ -761,7 +722,7 @@ class ImageFileDirectory_v2(MutableMapping): 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)) ) @@ -792,10 +753,10 @@ class ImageFileDirectory_v2(MutableMapping): 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) @@ -824,8 +785,8 @@ class ImageFileDirectory_v2(MutableMapping): 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 @@ -844,7 +805,7 @@ class ImageFileDirectory_v2(MutableMapping): 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: @@ -881,7 +842,7 @@ class ImageFileDirectory_v2(MutableMapping): # 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) @@ -938,7 +899,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) @@ -1081,7 +1042,7 @@ class TiffImageFile(ImageFile.ImageFile): "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) @@ -1106,25 +1067,26 @@ class TiffImageFile(ImageFile.ImageFile): """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: + method = { + 2: Image.FLIP_LEFT_RIGHT, + 3: Image.ROTATE_180, + 4: Image.FLIP_TOP_BOTTOM, + 5: Image.TRANSPOSE, + 6: Image.ROTATE_270, + 7: Image.TRANSVERSE, + 8: Image.ROTATE_90, + }.get(self._tile_orientation) + if method is not None: + self.im = self.im.transpose(method) + self._size = self.im.size + # allow closing if we're on the first frame, there's no next # This is the ImageFile.load path only, libtiff specific below. if not self._is_animated: @@ -1137,14 +1099,14 @@ class TiffImageFile(ImageFile.ImageFile): 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)) @@ -1158,11 +1120,11 @@ class TiffImageFile(ImageFile.ImageFile): 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 @@ -1176,8 +1138,9 @@ class TiffImageFile(ImageFile.ImageFile): 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"): # We've got a stringio like thing passed in. Yay for all in memory. # The decoder needs the entire file in one shot, so there's not @@ -1195,7 +1158,8 @@ class TiffImageFile(ImageFile.ImageFile): # we've got a actual file on disk, pass in the fp. if DEBUG: print("have fileno, calling fileno version of the decoder.") - self.fp.seek(0) + if not close_self_fp: + self.fp.seek(0) # 4 bytes, otherwise the trace might error out n, err = decoder.decode(b"fpfp") else: @@ -1208,13 +1172,16 @@ class TiffImageFile(ImageFile.ImageFile): self.tile = [] self.readonly = 0 + + self.load_end() + # libtiff closed the fp in a, we need to close self.fp, if possible - if self._exclusive_fp and not self._is_animated: + if close_self_fp: self.fp.close() self.fp = None # might be shared if err < 0: - raise IOError(err) + raise OSError(err) return Image.Image.load(self) @@ -1222,7 +1189,7 @@ class TiffImageFile(ImageFile.ImageFile): """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)] @@ -1247,8 +1214,8 @@ class TiffImageFile(ImageFile.ImageFile): print("- YCbCr subsampling:", self.tag.get(530)) # size - xsize = self.tag_v2.get(IMAGEWIDTH) - ysize = self.tag_v2.get(IMAGELENGTH) + xsize = int(self.tag_v2.get(IMAGEWIDTH)) + ysize = int(self.tag_v2.get(IMAGELENGTH)) self._size = xsize, ysize if DEBUG: @@ -1415,6 +1382,8 @@ class TiffImageFile(ImageFile.ImageFile): palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) + self._tile_orientation = self.tag_v2.get(0x0112) + def _close__fp(self): try: if self.__fp != self.fp: @@ -1462,7 +1431,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) @@ -1612,22 +1581,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) @@ -1660,7 +1625,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) @@ -1708,9 +1673,9 @@ class AppendingTiffWriter: 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() @@ -1791,7 +1756,7 @@ class AppendingTiffWriter: # 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): @@ -1815,11 +1780,11 @@ class AppendingTiffWriter: 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 f5a27be42..6cc9ff7f3 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 @@ TAGS_V2 = { 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), @@ -175,13 +175,14 @@ TAGS_V2 = { 530: ("YCbCrSubSampling", SHORT, 2), 531: ("YCbCrPositioning", SHORT, 1), 532: ("ReferenceBlackWhite", RATIONAL, 6), - 700: ("XMP", BYTE, 1), + 700: ("XMP", BYTE, 0), 33432: ("Copyright", ASCII, 1), - 34377: ("PhotoshopInfo", BYTE, 1), + 33723: ("IptcNaaInfo", UNDEFINED, 0), + 34377: ("PhotoshopInfo", BYTE, 0), # 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 e2e1cd4f5..d5a5c8e67 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 18eda6d18..eda685508 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -105,7 +105,7 @@ class WebPImageFile(ImageFile.ImageFile): 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 @@ class WebPImageFile(ImageFile.ImageFile): 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 416af6fd7..024222c9b 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 @@ class WmfStubImageFile(ImageFile.StubImageFile): # placeable windows metafile # get units per inch - inch = word(s, 14) + self._inch = word(s, 14) # get bounding box x0 = short(s, 6) @@ -106,12 +97,14 @@ class WmfStubImageFile(ImageFile.StubImageFile): y1 = short(s, 12) # normalize size to 72 dots per inch - size = (x1 - x0) * 72 // inch, (y1 - y0) * 72 // inch + self.info["dpi"] = 72 + size = ( + (x1 - x0) * self.info["dpi"] // self._inch, + (y1 - y0) * self.info["dpi"] // self._inch, + ) self.info["wmf_bbox"] = x0, y0, x1, y1 - self.info["dpi"] = 72 - # sanity check (standard metafile header) if s[22:26] != b"\x01\x00\t\x00": raise SyntaxError("Unsupported WMF file format") @@ -128,7 +121,6 @@ class WmfStubImageFile(ImageFile.StubImageFile): # get frame (in 0.01 millimeter units) frame = _long(s, 24), _long(s, 28), _long(s, 32), _long(s, 36) - # normalize size to 72 dots per inch size = x1 - x0, y1 - y0 # calculate dots per inch from bbox and frame @@ -155,10 +147,20 @@ class WmfStubImageFile(ImageFile.StubImageFile): def _load(self): return _handler + def load(self, dpi=None): + if dpi is not None and self._inch is not None: + self.info["dpi"] = int(dpi + 0.5) + x0, y0, x1, y1 = self.info["wmf_bbox"] + self._size = ( + (x1 - x0) * self.info["dpi"] // self._inch, + (y1 - y0) * self.info["dpi"] // self._inch, + ) + super().load() + def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - 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 aa3536d85..c0d8db09a 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 bc825c3f3..ead9722c8 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -23,10 +23,6 @@ import re 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 @@ class XbmImageFile(ImageFile.ImageFile): 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 275148827..d8bd00a1b 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -20,10 +20,6 @@ import re 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 59eccc9b5..e7f26488d 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -9,7 +9,6 @@ PIL is the Python Imaging Library by Fredrik Lundh and Contributors. 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. ;-) """ @@ -17,9 +16,9 @@ PIL.VERSION is the old PIL version and will be removed in the future. from . import _version # VERSION was removed in Pillow 6.0.0. -# PILLOW_VERSION is deprecated and will be removed in Pillow 7.0.0. +# PILLOW_VERSION was removed in Pillow 7.0.0. # Use __version__ instead. -PILLOW_VERSION = __version__ = _version.__version__ +__version__ = _version.__version__ del _version @@ -71,3 +70,7 @@ _plugins = [ "XpmImagePlugin", "XVThumbImagePlugin", ] + + +class UnidentifiedImageError(IOError): + pass diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 53b1ca956..529b8c94b 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 o8(i): - return bytes((i & 255,)) +def i8(c): + return c if c.__class__ is int else c[0] -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 d4f34196e..30493066a 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 59964c7ef..755b4b272 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 d9eaea530..eddf15683 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "6.2.0.dev0" +__version__ = "7.0.0.dev0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 9fd522368..5822febab 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -1,5 +1,3 @@ -from __future__ import print_function, unicode_literals - import collections import os import sys @@ -56,6 +54,7 @@ features = { "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), "raqm": ("PIL._imagingft", "HAVE_RAQM"), "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO"), + "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT"), } @@ -94,7 +93,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 +101,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 +116,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 +130,7 @@ def pilinfo(out=None): ("zlib", "ZLIB (PNG/ZIP)"), ("libtiff", "LIBTIFF"), ("raqm", "RAQM (Bidirectional Text)"), + ("libimagequant", "LIBIMAGEQUANT (Quantization method)"), ]: if check(name): print("---", feature, "support ok", file=out) @@ -140,30 +138,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) + 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) + 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) + 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") + 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) + print("Features: {}".format(", ".join(features)), file=out) + print("-" * 68, file=out) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index bb0fd33a3..59801f58e 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 04520b1a1..190b312bc 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); @@ -2414,7 +2423,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 +2657,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 +3286,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}, @@ -3356,13 +3366,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 +3585,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 +3623,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 +3632,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 +3641,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* @@ -3934,6 +3944,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,6 +3967,15 @@ 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 @@ -3959,7 +3984,6 @@ setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imaging(void) { PyObject* m; @@ -3979,11 +4003,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 2c9f3aa68..0b22ab695 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -32,7 +32,6 @@ http://www.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 87376383e..62a4c283e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -25,11 +25,11 @@ #include #include FT_FREETYPE_H #include FT_GLYPH_H +#include FT_STROKER_H #include FT_MULTIPLE_MASTERS_H #include FT_SFNT_NAMES_H #define KEEP_PY_UNICODE -#include "py3.h" #if !defined(_MSC_VER) #include @@ -265,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)) { @@ -327,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; } @@ -374,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); @@ -394,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); @@ -422,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"); @@ -478,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; } @@ -494,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; @@ -580,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; } @@ -790,7 +734,13 @@ font_render(FontObject* self, PyObject* args) int index, error, ascender, horizontal_dir; int load_flags; unsigned char *source; - FT_GlyphSlot glyph; + FT_Glyph glyph; + FT_GlyphSlot glyph_slot; + FT_Bitmap bitmap; + FT_BitmapGlyph bitmap_glyph; + int stroke_width = 0; + FT_Stroker stroker = NULL; + FT_Int left; /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ PyObject* string; @@ -806,7 +756,8 @@ font_render(FontObject* self, PyObject* args) GlyphInfo *glyph_info; PyObject *features = NULL; - if (!PyArg_ParseTuple(args, "On|izOz:render", &string, &id, &mask, &dir, &features, &lang)) { + if (!PyArg_ParseTuple(args, "On|izOzi:render", &string, &id, &mask, &dir, &features, &lang, + &stroke_width)) { return NULL; } @@ -819,21 +770,37 @@ font_render(FontObject* self, PyObject* args) Py_RETURN_NONE; } + if (stroke_width) { + error = FT_Stroker_New(library, &stroker); + if (error) { + return geterror(error); + } + + FT_Stroker_Set(stroker, (FT_Fixed)stroke_width*64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); + } + im = (Imaging) id; /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ - load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP; - if (mask) + load_flags = FT_LOAD_NO_BITMAP; + if (stroker == NULL) { + load_flags |= FT_LOAD_RENDER; + } + if (mask) { load_flags |= FT_LOAD_TARGET_MONO; + } ascender = 0; for (i = 0; i < count; i++) { index = glyph_info[i].index; error = FT_Load_Glyph(self->face, index, load_flags); - if (error) + if (error) { return geterror(error); + } - glyph = self->face->glyph; - temp = glyph->bitmap.rows - glyph->bitmap_top; + glyph_slot = self->face->glyph; + bitmap = glyph_slot->bitmap; + + temp = bitmap.rows - glyph_slot->bitmap_top; temp -= PIXEL(glyph_info[i].y_offset); if (temp > ascender) ascender = temp; @@ -844,37 +811,60 @@ font_render(FontObject* self, PyObject* args) for (i = 0; i < count; i++) { index = glyph_info[i].index; error = FT_Load_Glyph(self->face, index, load_flags); - if (error) + if (error) { return geterror(error); + } - glyph = self->face->glyph; - if (horizontal_dir) { - if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) { - x = -self->face->glyph->metrics.horiBearingX; + glyph_slot = self->face->glyph; + if (stroker != NULL) { + error = FT_Get_Glyph(glyph_slot, &glyph); + if (!error) { + error = FT_Glyph_Stroke(&glyph, stroker, 1); } - xx = PIXEL(x) + glyph->bitmap_left; - xx += PIXEL(glyph_info[i].x_offset); + if (!error) { + FT_Vector origin = {0, 0}; + error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1); + } + if (error) { + return geterror(error); + } + + bitmap_glyph = (FT_BitmapGlyph)glyph; + + bitmap = bitmap_glyph->bitmap; + left = bitmap_glyph->left; } else { - if (self->face->glyph->metrics.vertBearingX < 0) { - x = -self->face->glyph->metrics.vertBearingX; + bitmap = glyph_slot->bitmap; + left = glyph_slot->bitmap_left; + } + + if (horizontal_dir) { + if (i == 0 && glyph_slot->metrics.horiBearingX < 0) { + x = -glyph_slot->metrics.horiBearingX; } - xx = im->xsize / 2 - glyph->bitmap.width / 2; + xx = PIXEL(x) + left; + xx += PIXEL(glyph_info[i].x_offset) + stroke_width; + } else { + if (glyph_slot->metrics.vertBearingX < 0) { + x = -glyph_slot->metrics.vertBearingX; + } + xx = im->xsize / 2 - bitmap.width / 2; } x0 = 0; - x1 = glyph->bitmap.width; + x1 = bitmap.width; if (xx < 0) x0 = -xx; if (xx + x1 > im->xsize) x1 = im->xsize - xx; - source = (unsigned char*) glyph->bitmap.buffer; - for (bitmap_y = 0; bitmap_y < glyph->bitmap.rows; bitmap_y++) { + source = (unsigned char*) bitmap.buffer; + for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++) { if (horizontal_dir) { - yy = bitmap_y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); - yy -= PIXEL(glyph_info[i].y_offset); + yy = bitmap_y + im->ysize - (PIXEL(glyph_slot->metrics.horiBearingY) + ascender); + yy -= PIXEL(glyph_info[i].y_offset) + stroke_width * 2; } else { - yy = bitmap_y + PIXEL(y + glyph->metrics.vertBearingY) + ascender; + yy = bitmap_y + PIXEL(y + glyph_slot->metrics.vertBearingY) + ascender; yy += PIXEL(glyph_info[i].y_offset); } if (yy >= 0 && yy < im->ysize) { @@ -900,12 +890,16 @@ font_render(FontObject* self, PyObject* args) } } } - source += glyph->bitmap.pitch; + source += bitmap.pitch; } x += glyph_info[i].x_advance; y -= glyph_info[i].y_advance; + if (stroker != NULL) { + FT_Done_Glyph(glyph); + } } + FT_Stroker_Done(stroker); PyMem_Del(glyph_info); Py_RETURN_NONE; } @@ -940,8 +934,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; } @@ -975,11 +968,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); @@ -987,8 +980,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; } @@ -1045,8 +1037,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 { @@ -1096,64 +1088,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[] = { @@ -1221,11 +1203,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); @@ -1236,7 +1214,6 @@ setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingft(void) { PyObject* m; @@ -1256,12 +1233,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 ea9f103c6..bc66a581a 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 fc8f246cc..050ae9f02 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 d0295f317..bdf5e68d1 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 66b6d3268..4581ef89d 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 @@ -557,7 +556,7 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) Py_ssize_t xmp_size; size_t ret_size; - 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; @@ -754,11 +753,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, @@ -848,7 +843,6 @@ static int setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__webp(void) { PyObject* m; @@ -867,11 +861,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 79133f48f..5ab6ca9d1 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 ab005d4b4..4c2faf9e0 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 @@ -319,18 +308,23 @@ PyImaging_DisplayModeWin32(PyObject* self, PyObject* args) /* -------------------------------------------------------------------- */ /* Windows screen grabber */ +typedef HANDLE(__stdcall* Func_SetThreadDpiAwarenessContext)(HANDLE); + PyObject* PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) { - int width, height; - int includeLayeredWindows = 0; + int x = 0, y = 0, width, height; + int includeLayeredWindows = 0, all_screens = 0; HBITMAP bitmap; BITMAPCOREHEADER core; HDC screen, screen_copy; DWORD rop; PyObject* buffer; + HANDLE dpiAwareness; + HMODULE user32; + Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; - if (!PyArg_ParseTuple(args, "|i", &includeLayeredWindows)) + if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) return NULL; /* step 1: create a memory DC large enough to hold the @@ -339,8 +333,32 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) screen = CreateDC("DISPLAY", NULL, NULL, NULL); screen_copy = CreateCompatibleDC(screen); - width = GetDeviceCaps(screen, HORZRES); - height = GetDeviceCaps(screen, VERTRES); + // added in Windows 10 (1607) + // loaded dynamically to avoid link errors + user32 = LoadLibraryA("User32.dll"); + SetThreadDpiAwarenessContext_function = + (Func_SetThreadDpiAwarenessContext) + GetProcAddress(user32, "SetThreadDpiAwarenessContext"); + if (SetThreadDpiAwarenessContext_function != NULL) { + // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) + dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE) -3); + } + + if (all_screens) { + x = GetSystemMetrics(SM_XVIRTUALSCREEN); + y = GetSystemMetrics(SM_YVIRTUALSCREEN); + width = GetSystemMetrics(SM_CXVIRTUALSCREEN); + height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + } else { + width = GetDeviceCaps(screen, HORZRES); + height = GetDeviceCaps(screen, VERTRES); + } + + if (SetThreadDpiAwarenessContext_function != NULL) { + SetThreadDpiAwarenessContext_function(dpiAwareness); + } + + FreeLibrary(user32); bitmap = CreateCompatibleBitmap(screen, width, height); if (!bitmap) @@ -354,7 +372,7 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) rop = SRCCOPY; if (includeLayeredWindows) rop |= CAPTUREBLT; - if (!BitBlt(screen_copy, 0, 0, width, height, screen, 0, 0, rop)) + if (!BitBlt(screen_copy, 0, 0, width, height, screen, x, y, rop)) goto error; /* step 3: extract bits from bitmap */ @@ -376,7 +394,7 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) DeleteDC(screen_copy); DeleteDC(screen); - return Py_BuildValue("(ii)N", width, height, buffer); + return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer); error: PyErr_SetString(PyExc_IOError, "screen grab failed"); @@ -712,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; diff --git a/src/encode.c b/src/encode.c index ac729f455..41ba124c4 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; @@ -1211,6 +1210,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) PyErr_SetString(PyExc_ValueError, "JPEG 2000 tile offset too small; top left tile must " "intersect image area"); + Py_DECREF(encoder); + return NULL; } if (context->tile_offset_x > context->offset_x diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 5df48fb23..60513c66d 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -101,6 +101,19 @@ bit2ycbcr(UINT8* out, const UINT8* in, int xsize) } } +static void +bit2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, out += 4) { + UINT8 v = (*in++ != 0) ? 255 : 0; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = 255; + } +} + /* ----------------- */ /* RGB/L conversions */ /* ----------------- */ @@ -175,6 +188,19 @@ l2rgb(UINT8* out, const UINT8* in, int xsize) } } +static void +l2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, out += 4) { + UINT8 v = *in++; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = 255; + } +} + static void la2l(UINT8* out, const UINT8* in, int xsize) { @@ -196,6 +222,19 @@ la2rgb(UINT8* out, const UINT8* in, int xsize) } } +static void +la2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + UINT8 v = in[0]; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = in[3]; + } +} + static void rgb2bit(UINT8* out, const UINT8* in, int xsize) { @@ -283,54 +322,58 @@ rgb2bgr24(UINT8* out, const UINT8* in, int xsize) } static void -rgb2hsv(UINT8* out, const UINT8* in, int xsize) +rgb2hsv_row(UINT8* out, const UINT8* in) { // following colorsys.py float h,s,rc,gc,bc,cr; UINT8 maxc,minc; UINT8 r, g, b; UINT8 uh,us,uv; - int x; - for (x = 0; x < xsize; x++, in += 4) { - r = in[0]; - g = in[1]; - b = in[2]; - - maxc = MAX(r,MAX(g,b)); - minc = MIN(r,MIN(g,b)); - uv = maxc; - if (minc == maxc){ - *out++ = 0; - *out++ = 0; - *out++ = uv; + r = in[0]; + g = in[1]; + b = in[2]; + maxc = MAX(r,MAX(g,b)); + minc = MIN(r,MIN(g,b)); + uv = maxc; + if (minc == maxc){ + uh = 0; + us = 0; + } else { + cr = (float)(maxc-minc); + s = cr/(float)maxc; + rc = ((float)(maxc-r))/cr; + gc = ((float)(maxc-g))/cr; + bc = ((float)(maxc-b))/cr; + if (r == maxc) { + h = bc-gc; + } else if (g == maxc) { + h = 2.0 + rc-bc; } else { - cr = (float)(maxc-minc); - s = cr/(float)maxc; - rc = ((float)(maxc-r))/cr; - gc = ((float)(maxc-g))/cr; - bc = ((float)(maxc-b))/cr; - if (r == maxc) { - h = bc-gc; - } else if (g == maxc) { - h = 2.0 + rc-bc; - } else { - h = 4.0 + gc-rc; - } - // incorrect hue happens if h/6 is negative. - h = fmod((h/6.0 + 1.0), 1.0); - - uh = (UINT8)CLIP8((int)(h*255.0)); - us = (UINT8)CLIP8((int)(s*255.0)); - - *out++ = uh; - *out++ = us; - *out++ = uv; - + h = 4.0 + gc-rc; } - *out++ = in[3]; + // incorrect hue happens if h/6 is negative. + h = fmod((h/6.0 + 1.0), 1.0); + + uh = (UINT8)CLIP8((int)(h*255.0)); + us = (UINT8)CLIP8((int)(s*255.0)); + } + out[0] = uh; + out[1] = us; + out[2] = uv; +} + +static void +rgb2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + rgb2hsv_row(out, in); + out[3] = in[3]; } } + + static void hsv2rgb(UINT8* out, const UINT8* in, int xsize) { // following colorsys.py @@ -562,6 +605,22 @@ cmyk2rgb(UINT8* out, const UINT8* in, int xsize) } } +static void +cmyk2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x, nk, tmp; + for (x = 0; x < xsize; x++) { + nk = 255 - in[3]; + out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp)); + out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp)); + out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp)); + rgb2hsv_row(out, out); + out[3] = 255; + out += 4; + in += 4; + } +} + /* ------------- */ /* I conversions */ /* ------------- */ @@ -631,6 +690,25 @@ i2rgb(UINT8* out, const UINT8* in_, int xsize) } } +static void +i2hsv(UINT8* out, const UINT8* in_, int xsize) +{ + int x; + INT32* in = (INT32*) in_; + for (x = 0; x < xsize; x++, in++, out+=4) { + out[0] = 0; + out[1] = 0; + if (*in <= 0) { + out[2] = 0; + } else if (*in >= 255) { + out[2] = 255; + } else { + out[2] = (UINT8) *in; + } + out[3] = 255; + } +} + /* ------------- */ /* F conversions */ /* ------------- */ @@ -861,6 +939,7 @@ static struct { { "1", "RGBX", bit2rgb }, { "1", "CMYK", bit2cmyk }, { "1", "YCbCr", bit2ycbcr }, + { "1", "HSV", bit2hsv }, { "L", "1", l2bit }, { "L", "LA", l2la }, @@ -871,6 +950,7 @@ static struct { { "L", "RGBX", l2rgb }, { "L", "CMYK", l2cmyk }, { "L", "YCbCr", l2ycbcr }, + { "L", "HSV", l2hsv }, { "LA", "L", la2l }, { "LA", "La", lA2la }, @@ -879,6 +959,7 @@ static struct { { "LA", "RGBX", la2rgb }, { "LA", "CMYK", la2cmyk }, { "LA", "YCbCr", la2ycbcr }, + { "LA", "HSV", la2hsv }, { "La", "LA", la2lA }, @@ -887,6 +968,7 @@ static struct { { "I", "RGB", i2rgb }, { "I", "RGBA", i2rgb }, { "I", "RGBX", i2rgb }, + { "I", "HSV", i2hsv }, { "F", "L", f2l }, { "F", "I", f2i }, @@ -915,6 +997,7 @@ static struct { { "RGBA", "RGBX", rgb2rgba }, { "RGBA", "CMYK", rgb2cmyk }, { "RGBA", "YCbCr", ImagingConvertRGB2YCbCr }, + { "RGBA", "HSV", rgb2hsv }, { "RGBa", "RGBA", rgba2rgbA }, @@ -926,10 +1009,12 @@ static struct { { "RGBX", "RGB", rgba2rgb }, { "RGBX", "CMYK", rgb2cmyk }, { "RGBX", "YCbCr", ImagingConvertRGB2YCbCr }, + { "RGBX", "HSV", rgb2hsv }, { "CMYK", "RGB", cmyk2rgb }, { "CMYK", "RGBA", cmyk2rgb }, { "CMYK", "RGBX", cmyk2rgb }, + { "CMYK", "HSV", cmyk2hsv }, { "YCbCr", "L", ycbcr2l }, { "YCbCr", "LA", ycbcr2la }, @@ -1101,6 +1186,28 @@ pa2rgb(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) } } +static void +p2hsv(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + for (x = 0; x < xsize; x++, out += 4) { + const UINT8* rgb = &palette[*in++ * 4]; + rgb2hsv_row(out, rgb); + out[3] = 255; + } +} + +static void +pa2hsv(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + const UINT8* rgb = &palette[in[0] * 4]; + rgb2hsv_row(out, rgb); + out[3] = 255; + } +} + static void p2rgba(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { @@ -1192,6 +1299,8 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) convert = alpha ? pa2cmyk : p2cmyk; else if (strcmp(mode, "YCbCr") == 0) convert = alpha ? pa2ycbcr : p2ycbcr; + else if (strcmp(mode, "HSV") == 0) + convert = alpha ? pa2hsv : p2hsv; else return (Imaging) ImagingError_ValueError("conversion not supported"); diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 559be1b00..dee7c524d 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -834,7 +834,7 @@ ellipse(Imaging im, int x0, int y0, int x1, int y1, // Build edge list // malloc check UNDONE, FLOAT? - maxEdgeCount = end - start; + maxEdgeCount = ceil(end - start); if (inner) { maxEdgeCount *= 2; } diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index e21259900..5f4485f89 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -30,7 +30,7 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt { UINT8* ptr; int framesize; - int c, chunks; + int c, chunks, advance; int l, lines; int i, j, x = 0, y, ymax; @@ -59,10 +59,16 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt chunks = I16(ptr+6); ptr += 16; + bytes -= 16; /* Process subchunks */ for (c = 0; c < chunks; c++) { - UINT8 *data = ptr + 6; + UINT8* data; + if (bytes < 10) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + data = ptr + 6; switch (I16(ptr+4)) { case 4: case 11: /* FLI COLOR chunk */ @@ -198,7 +204,9 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt state->errcode = IMAGING_CODEC_UNKNOWN; return -1; } - ptr += I32(ptr); + advance = I32(ptr); + ptr += advance; + bytes -= advance; } return -1; /* end of frame */ diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 25c15e758..71dc9c003 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); diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index eaa276af4..a239464d4 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 4a5931bee..67dcc1e08 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -22,6 +22,11 @@ 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) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + ptr = buf; for (;;) { diff --git a/src/libImaging/RawDecode.c b/src/libImaging/RawDecode.c index 774d4245b..c069bdb88 100644 --- a/src/libImaging/RawDecode.c +++ b/src/libImaging/RawDecode.c @@ -33,8 +33,15 @@ ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt /* get size of image data and padding */ state->bytes = (state->xsize * state->bits + 7) / 8; - rawstate->skip = (rawstate->stride) ? - rawstate->stride - state->bytes : 0; + if (rawstate->stride) { + rawstate->skip = rawstate->stride - state->bytes; + if (rawstate->skip < 0) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + } else { + rawstate->skip = 0; + } /* check image orientation */ if (state->ystep < 0) { diff --git a/src/libImaging/Reduce.c b/src/libImaging/Reduce.c new file mode 100644 index 000000000..d6ef92f5b --- /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 4a98e8477..0dc08611d 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 d8341f3e5..8a81ba8e6 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -157,6 +157,11 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, c->rlelength = c->lengthtab[c->rowno + c->channo * im->ysize]; c->rleoffset -= SGI_HEADER_SIZE; + if (c->rleoffset + c->rlelength > c->bufsize) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + /* row decompression */ if (c->bpc ==1) { if(expandrow(&state->buffer[c->channo], &ptr[c->rleoffset], c->rlelength, im->bands)) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 389089e11..ab476939a 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -132,7 +132,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) } else if (strcmp(mode, "BGR;15") == 0) { /* EXPERIMENTAL */ - /* 15-bit true colour */ + /* 15-bit reversed true colour */ im->bands = 1; im->pixelsize = 2; im->linesize = (xsize*2 + 3) & -4; diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index e72dae0c8..7592f7f39 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -405,8 +405,12 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ UINT32 strip_row, row_byte_size; UINT8 *new_data; UINT32 rows_per_strip; + int ret; - TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); + ret = TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); + if (ret != 1) { + rows_per_strip = state->ysize; + } TRACE(("RowsPerStrip: %u \n", rows_per_strip)); // We could use TIFFStripSize, but for YCbCr data it returns subsampled data size diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index ab0c8dc60..adf2dd277 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 7bd4dadf8..5cde31cdc 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 099bb4b3e..a8fb69c6e 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); diff --git a/src/path.c b/src/path.c index 5f0541b0b..f69755d16 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 310583845..000000000 --- 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 2dc920371..07d75be64 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] @@ -24,7 +24,7 @@ deps = [testenv:lint] commands = - black --check --diff . + black --target-version py35 --check --diff . flake8 --statistics --count isort --check-only --diff check-manifest diff --git a/winbuild/appveyor_install_msys2_deps.sh b/winbuild/appveyor_install_msys2_deps.sh index 02b75e210..4cc01082d 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_pypy.cmd b/winbuild/appveyor_install_pypy.cmd deleted file mode 100644 index fc56d0e56..000000000 --- a/winbuild/appveyor_install_pypy.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 new file mode 100644 index 000000000..75a22ca59 --- /dev/null +++ b/winbuild/appveyor_install_pypy3.cmd @@ -0,0 +1,3 @@ +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:\pypy3.6-v7.3.0-win32\pypy3.exe c:\vp\pypy3 diff --git a/winbuild/build.py b/winbuild/build.py index f3121283a..e565226bd 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)) @@ -84,7 +84,8 @@ def vc_setup(compiler, bit): arch = "x86" if bit == 32 else "x86_amd64" script = ( r""" -call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %s""" +call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %s +echo on""" % arch ) return script @@ -97,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" @@ -126,7 +124,7 @@ set INCLUDE=%%INCLUDE%%;%%INCLIB%%\%(inc_dir)s;%%INCLIB%%\tcl%(tcl_ver)s\include setlocal set LIB=%%LIB%%;C:\Python%(py_ver)s\tcl%(vc_setup)s call %(python_path)s\%(executable)s setup.py %(imaging_libs)s %%BLDOPT%% -call %(python_path)s\%(executable)s -c "from PIL import _webp;import os, shutil;shutil.copy('%%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 @@ -159,7 +157,7 @@ def main(op): scripts.append( ( - "%s%s" % (py_version, X64_EXT), + "{}{}".format(py_version, X64_EXT), "\n".join( [ header(op), @@ -173,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): @@ -191,16 +189,14 @@ def run_one(op): if __name__ == "__main__": - opts, args = getopt.getopt(sys.argv[1:], "", ["clean", "dist", "wheel"]) + opts, args = getopt.getopt(sys.argv[1:], "", ["clean", "wheel"]) opts = dict(opts) if "--clean" in opts: clean() op = "install" - if "--dist" in opts: - op = "bdist_wininst --user-access-control=auto" - elif "--wheel" in opts: + if "--wheel" in opts: op = "bdist_wheel" if "PYTHON" in os.environ: diff --git a/winbuild/build.rst b/winbuild/build.rst index a56f43d1a..1d2084044 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -37,8 +37,8 @@ virtualenv as well, reducing the number of packages that we need to install.) Download the rest of the Pythons by opening a command window, changing -to the `winbuild` directory, and running `python -get_pythons.py`. +to the ``winbuild`` directory, and running ``python +get_pythons.py``. UNDONE -- gpg verify the signatures (note that we can download from https) @@ -65,8 +65,8 @@ Dependencies ------------ The script 'build_dep.py' downloads and builds the dependencies. Open -a command window, change directory into `winbuild` and run `python -build_dep.py`. +a command window, change directory into ``winbuild`` and run ``python +build_dep.py``. This will download libjpeg, libtiff, libz, and freetype. It will then compile 32 and 64-bit versions of the libraries, with both versions of @@ -78,9 +78,9 @@ UNDONE -- webp, jpeg2k not recognized Building Pillow --------------- -Once the dependencies are built, run `python build.py --clean` to -build and install Pillow in virtualenvs for each python -build. `build.py --dist` will build Windows installers instead of +Once the dependencies are built, run ``python build.py --clean`` to +build and install Pillow in virtualenvs for each Python +build. ``build.py --wheel`` will build wheels instead of installing into virtualenvs. UNDONE -- suppressed output, what about failures. @@ -88,6 +88,6 @@ UNDONE -- suppressed output, what about failures. Testing Pillow -------------- -Build and install Pillow, then run `python test.py` from the -`winbuild` directory. +Build and install Pillow, then run ``python test.py`` from the +``winbuild`` directory. diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index a640d4279..778570139 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( @@ -67,7 +65,6 @@ def extract_openjpeg(compiler): r""" rem build openjpeg setlocal -@echo on cd %%BUILD%% mkdir %%INCLIB%%\openjpeg-2.0 copy /Y /B openjpeg-2.0.0-win32-x86\include\openjpeg-2.0 %%INCLIB%%\openjpeg-2.0 @@ -104,7 +101,7 @@ set CMAKE="cmake.exe" 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"] ) @@ -114,6 +111,7 @@ def setup_compiler(compiler): return ( r"""setlocal EnableDelayedExpansion call "%%ProgramFiles%%\Microsoft SDKs\Windows\%(env_version)s\Bin\SetEnv.Cmd" /Release %(env_flags)s +echo on set INCLIB=%%INCLIB%%\%(inc_dir)s """ # noqa: E501 % compiler @@ -139,12 +137,11 @@ setlocal """ + vc_setup(compiler, bit) + r""" -@echo on cd /D %%OPENJPEG%%%(inc_dir)s -%%CMAKE%% -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF -G "NMake Makefiles" . -nmake -f Makefile clean -nmake -f Makefile +%%CMAKE%% -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF -DCMAKE_BUILD_TYPE=Release -G "NMake Makefiles" . +nmake -nologo -f Makefile clean +nmake -nologo -f Makefile copy /Y /B bin\* %%INCLIB%% mkdir %%INCLIB%%\openjpeg-%(op_ver)s copy /Y /B src\lib\openjp2\*.h %%INCLIB%%\openjpeg-%(op_ver)s @@ -164,9 +161,9 @@ setlocal + vc_setup(compiler, bit) + r""" cd /D %%JPEG%% -nmake -f makefile.vc setup-vc6 -nmake -f makefile.vc clean -nmake -f makefile.vc libjpeg.lib +nmake -nologo -f makefile.vc setup-vc6 +nmake -nologo -f makefile.vc clean +nmake -nologo -f makefile.vc nodebug=1 libjpeg.lib copy /Y /B *.dll %%INCLIB%% copy /Y /B *.lib %%INCLIB%% copy /Y /B j*.h %%INCLIB%% @@ -175,8 +172,8 @@ endlocal rem Build zlib setlocal cd /D %%ZLIB%% -nmake -f win32\Makefile.msc clean -nmake -f win32\Makefile.msc zlib.lib +nmake -nologo -f win32\Makefile.msc clean +nmake -nologo -f win32\Makefile.msc zlib.lib copy /Y /B *.dll %%INCLIB%% copy /Y /B *.lib %%INCLIB%% copy /Y /B zlib.lib %%INCLIB%%\z.lib @@ -191,7 +188,7 @@ setlocal + r""" cd /D %%WEBP%% rd /S /Q %%WEBP%%\output\release-static -nmake -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output all +nmake -nologo -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output all copy /Y /B output\release-static\%(webp_platform)s\lib\* %%INCLIB%% mkdir %%INCLIB%%\webp copy /Y /B src\webp\*.h %%INCLIB%%\\webp @@ -203,11 +200,11 @@ setlocal + 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 -f makefile.vc clean -nmake -f makefile.vc lib +nmake -nologo -f makefile.vc clean +nmake -nologo -f makefile.vc lib copy /Y /B libtiff\*.dll %%INCLIB%% copy /Y /B libtiff\*.lib %%INCLIB%% copy /Y /B libtiff\tiff*.h %%INCLIB%% @@ -228,8 +225,8 @@ set DefaultPlatformToolset=v100 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 += ( @@ -271,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 @@ -288,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%% @@ -300,33 +300,6 @@ endlocal ) -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 -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)) @@ -336,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 debfe9527..1bbc07e3a 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -1,14 +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}, "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/" @@ -21,66 +26,92 @@ libs = { # }, "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", + "filename": "jpegsr9c.zip", "dir": "jpeg-9c", }, "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", + "filename": "libwebp-1.0.3.tar.gz", "dir": "libwebp-1.0.3", }, "openjpeg": { "url": "https://github.com/uclouvain/openjpeg/archive/v2.3.1.tar.gz", - "filename": 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.1.zip", + "filename": "harfbuzz-2.6.1.zip", + "dir": "harfbuzz-2.6.1", + }, + "fribidi": { + "url": "https://github.com/fribidi/fribidi/archive/v1.0.7.zip", + "filename": "fribidi-1.0.7.zip", + "dir": "fribidi-1.0.7", + }, + "libraqm": { + "url": "https://github.com/HOST-Oman/libraqm/archive/v0.7.0.zip", + "filename": "libraqm-0.7.0.zip", + "dir": "libraqm-0.7.0", + }, } compilers = { @@ -130,14 +161,14 @@ compilers = { 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 804e4ef0c..adc45429a 100644 --- a/winbuild/fetch.py +++ b/winbuild/fetch.py @@ -3,16 +3,37 @@ import sys import urllib.parse import urllib.request +from config import libs + def fetch(url): + depends_filename = None + for lib in libs.values(): + if lib["url"] == url: + depends_filename = lib["filename"] + break + if depends_filename and os.path.exists(depends_filename): + return depends_filename name = urllib.parse.urlsplit(url)[2].split("/")[-1] if not os.path.exists(name): - 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 000000000..247e79e4c --- /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 e24bb65f7..a853fc6f7 100644 --- a/winbuild/get_pythons.py +++ b/winbuild/get_pythons.py @@ -3,7 +3,7 @@ import os 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 000000000..7fc48c034 --- /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 000000000..88eb7f284 --- /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 559ecdec1..a05a20b18 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 @@ if __name__ == "__main__": 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