From 58d2814ab5ab7ba6296f8025b1748d18efa6aa3c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Aug 2019 19:31:24 +1000 Subject: [PATCH 001/186] Corrected tag types --- src/PIL/TiffTags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 82719db0e..c047f42b6 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -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), @@ -182,7 +182,7 @@ TAGS_V2 = { # 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), From 8aa5ec661fff4e1ed77c2ab294271f69e468e21b Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 23 Sep 2019 13:12:01 +0200 Subject: [PATCH 002/186] Create test-windows.yml --- .github/workflows/test-windows.yml | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/test-windows.yml diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml new file mode 100644 index 000000000..7c8ca949c --- /dev/null +++ b/.github/workflows/test-windows.yml @@ -0,0 +1,32 @@ +name: Test Windows + +on: [push, pull_request] + +jobs: + build: + + runs-on: windows-2016 + strategy: + matrix: + python-version: ["3.7"] + + name: Python ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v1 + + - name: Prepare dependencies + run: python.exe winbuild\build_dep.py + + - name: Build dependencies + run: winbuild\build_deps.cmd + + - name: Build + run: python.exe winbuild\build.py + + - name: Test + run: python.exe selftest.py --installed + + - name: After success + run: echo TODO + From a0f62fc3064a4aee14b1544bbbe639ecd14811e1 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 23 Sep 2019 13:47:45 +0200 Subject: [PATCH 003/186] test-windows.yml bulid_dep nmake --- .github/workflows/test-windows.yml | 93 ++++++++++++++++++++++++++---- winbuild/config.py | 3 + 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 7c8ca949c..223758b67 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -8,25 +8,98 @@ jobs: runs-on: windows-2016 strategy: matrix: - python-version: ["3.7"] + python-version: ["3.6.8"] - name: Python ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} x86 steps: - uses: actions/checkout@v1 - - name: Prepare dependencies - run: python.exe winbuild\build_dep.py - - - name: Build dependencies - run: winbuild\build_deps.cmd + - name: Fetch dependencies + run: | + curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip + 7z x pillow-depends.zip -oc:\ + mv c:\pillow-depends-master c:\pillow-depends + xcopy c:\pillow-depends\*.zip $env:GITHUB_WORKSPACE\winbuild\ + xcopy c:\pillow-depends\*.tar.gz $env:GITHUB_WORKSPACE\winbuild\ + xcopy /s c:\pillow-depends\test_images\* $env:GITHUB_WORKSPACE\tests\images\ + cd $env:GITHUB_WORKSPACE/winbuild/ + python.exe $env:GITHUB_WORKSPACE\winbuild\build_dep.py + # .\build_deps.cmd + env: + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ + EXECUTABLE: bin\python.exe + shell: pwsh + + - name: Build dependencies / libjpeg + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 + echo on + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include;%INCLUDE% + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\jpeg-9c + 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 j*.h %INCLIB% + copy /Y /B *.lib %INCLIB% + + - name: Build dependencies / zlib + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 + echo on + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include;%INCLUDE% + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\zlib-1.2.11 + 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 + + - name: Build dependencies / libtiff + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 + echo on + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include;%INCLUDE% + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\tiff-4.0.10 + copy %GITHUB_WORKSPACE%\winbuild\nmake.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% + + - name: Build dependencies / webp + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 + echo on + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include;%INCLUDE% + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\libwebp-1.0.3 + rmdir /S /Q output\release-static + nmake -nologo -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output ARCH=x86 all + mkdir %INCLIB%\webp + copy /Y /B src\webp\*.h %INCLIB%\webp + copy /Y /B output\release-static\x86\lib\* %INCLIB% - name: Build - run: python.exe winbuild\build.py + run: '%PYTHON%\%EXECUTABLE% %GITHUB_WORKSPACE%\winbuild\build.py' + env: + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ + EXECUTABLE: python.exe - name: Test - run: python.exe selftest.py --installed + run: '%PYTHON%\%EXECUTABLE% %GITHUB_WORKSPACE%\selftest.py --installed' + env: + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ + EXECUTABLE: python.exe - name: After success - run: echo TODO + run: echo TODO 1>&2 diff --git a/winbuild/config.py b/winbuild/config.py index debfe9527..ff775a921 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -4,11 +4,14 @@ SF_MIRROR = "http://iweb.dl.sourceforge.net" PILLOW_DEPENDS_DIR = "C:\\pillow-depends\\" pythons = { + # for AppVeyor "27": {"compiler": 7, "vc": 2010}, "pypy2": {"compiler": 7, "vc": 2010}, "35": {"compiler": 7.1, "vc": 2015}, "36": {"compiler": 7.1, "vc": 2015}, "37": {"compiler": 7.1, "vc": 2015}, + # for GitHub Actions + "3.6": {"compiler": 7.1, "vc": 2015}, } VIRT_BASE = "c:/vp/" From 09715955c8b0cdbae840886ab11d37791e3b7f2d Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 23 Sep 2019 22:01:19 +0200 Subject: [PATCH 004/186] test-windows.yml bulid --- .github/workflows/test-windows.yml | 40 +++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 223758b67..3ee9fb440 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -33,12 +33,12 @@ jobs: - name: Build dependencies / libjpeg run: | - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 - echo on - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include;%INCLUDE% + 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 14.0\VC\vcvarsall.bat" x86 8.1 + echo on nmake -nologo -f makefile.vc setup-vc6 nmake -nologo -f makefile.vc clean nmake -nologo -f makefile.vc nodebug=1 libjpeg.lib @@ -47,12 +47,12 @@ jobs: - name: Build dependencies / zlib run: | - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 - echo on - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include;%INCLUDE% + 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 14.0\VC\vcvarsall.bat" x86 8.1 + echo on nmake -nologo -f win32\Makefile.msc clean nmake -nologo -f win32\Makefile.msc zlib.lib copy /Y /B z*.h %INCLIB% @@ -61,12 +61,12 @@ jobs: - name: Build dependencies / libtiff run: | - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 - echo on - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include;%INCLUDE% + 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.0.10 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 + echo on copy %GITHUB_WORKSPACE%\winbuild\nmake.opt nmake.opt nmake -nologo -f makefile.vc clean nmake -nologo -f makefile.vc lib @@ -76,25 +76,35 @@ jobs: - name: Build dependencies / webp run: | - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 - echo on - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include;%INCLUDE% + 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 14.0\VC\vcvarsall.bat" x86 8.1 + echo on rmdir /S /Q output\release-static nmake -nologo -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output ARCH=x86 all mkdir %INCLIB%\webp copy /Y /B src\webp\*.h %INCLIB%\webp copy /Y /B output\release-static\x86\lib\* %INCLIB% - - name: Build - run: '%PYTHON%\%EXECUTABLE% %GITHUB_WORKSPACE%\winbuild\build.py' + - name: Build Pillow + run: | + 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 DISTUTILS_USE_SDK=1 + set LIB=%INCLIB%;%PYTHON%\tcl + set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 + set BLDOPT=install + %PYTHON%\%EXECUTABLE% setup.py build_ext --add-imaging-libs=msvcrt install env: PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ EXECUTABLE: python.exe - - name: Test + - name: Test Pillow run: '%PYTHON%\%EXECUTABLE% %GITHUB_WORKSPACE%\selftest.py --installed' env: PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ From 339e3838a18dbf8b48897777b79f74174ac4df9c Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 23 Sep 2019 22:45:21 +0200 Subject: [PATCH 005/186] test-windows.yml test --- .github/workflows/test-windows.yml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 3ee9fb440..ad807961c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -100,16 +100,38 @@ jobs: call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 set BLDOPT=install %PYTHON%\%EXECUTABLE% setup.py build_ext --add-imaging-libs=msvcrt install + %PYTHON%\%EXECUTABLE% selftest.py --installed env: PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ EXECUTABLE: python.exe + + - name: Install PyTest + run: '%PYTHON%\%PIP% install pytest pytest-cov' + env: + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ + PIP: Scripts\pip.exe - name: Test Pillow - run: '%PYTHON%\%EXECUTABLE% %GITHUB_WORKSPACE%\selftest.py --installed' + run: | + cd /D %GITHUB_WORKSPACE% + %PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests + env: + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ + EXECUTABLE: python.exe + + - name: Install Codecov + run: '%PYTHON%\%PIP% install codecov' + env: + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ + PIP: Scripts\pip.exe + + - name: Upload coverage + run: | + cd /D %GITHUB_WORKSPACE% + codecov --file coverage.xml --name %PYTHON% + echo TODO upload coverage + exit /B 1 env: PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ EXECUTABLE: python.exe - - name: After success - run: echo TODO 1>&2 - From a26a6e502840aa89c45d839bc1d06496321c4e23 Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 24 Sep 2019 08:41:30 +0200 Subject: [PATCH 006/186] test-windows.yml build_dep others, codecov --- .github/workflows/test-windows.yml | 100 +++++++++++++++++++++++------ winbuild/build.py | 2 +- 2 files changed, 83 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index ad807961c..321940bd2 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -27,7 +27,7 @@ jobs: python.exe $env:GITHUB_WORKSPACE\winbuild\build_dep.py # .\build_deps.cmd env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 EXECUTABLE: bin\python.exe shell: pwsh @@ -87,7 +87,78 @@ jobs: mkdir %INCLIB%\webp copy /Y /B src\webp\*.h %INCLIB%\webp copy /Y /B output\release-static\x86\lib\* %INCLIB% - + + - 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 14.0\VC\vcvarsall.bat" x86 8.1 + echo on + rmdir /S /Q objs + set DefaultPlatformToolset=v140 + set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\VC\VCTargets + set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" + %MSBUILD% builds\windows\vc2010\freetype.sln /t:Build /p:Configuration="Release" /p:Platform=Win32 /m + xcopy /Y /E /Q include %INCLIB% + copy /Y /B objs\Win32\Release\freetype.dll %INCLIB% + copy /Y /B objs\Win32\Release\freetype.lib %INCLIB% + + - name: Build dependencies / lcms2 + if: false + run: | + REM Projects\VC2015\lcms2.sln is not available in lcms2-2.7 + + 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.7 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 + echo on + rmdir /S /Q Lib + rmdir /S /Q Projects\VC2015\Release + set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\VC\VCTargets + set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" + %MSBUILD% Projects\VC2015\lcms2.sln /t:Clean;lcms2_static /p:Configuration="Release" /p:Platform=Win32 /m + xcopy /Y /E /Q include %INCLIB% + copy /Y /B Lib\MS\*.lib %INCLIB% + + - 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 14.0\VC\vcvarsall.bat" x86 8.1 + echo on + cmake.exe -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 + mkdir %INCLIB%\openjpeg-2.3.1 + copy /Y /B src\lib\openjp2\*.h %INCLIB%\openjpeg-2.3.1 + copy /Y /B bin\*.lib %INCLIB% + + - name: Build dependencies / ghostscript + if: false + run: | + REM only used for Python 2.7 in AppVeyor? + + 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%\ghostscript-9.27 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 + echo on + set MSVC_VERSION=14 + set RCOMP="C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\RC.Exe" + nmake -nologo -f psi\msvc.mak + copy /Y /B bin\ %PYTHON% + env: + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 + - name: Build Pillow run: | set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include @@ -100,15 +171,16 @@ jobs: call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 set BLDOPT=install %PYTHON%\%EXECUTABLE% setup.py build_ext --add-imaging-libs=msvcrt install + %PYTHON%\%EXECUTABLE% -c "from PIL import _webp;import os, shutil;shutil.copy(r'%INCLIB%\freetype.dll', os.path.dirname(_webp.__file__));" %PYTHON%\%EXECUTABLE% selftest.py --installed env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 EXECUTABLE: python.exe - - name: Install PyTest - run: '%PYTHON%\%PIP% install pytest pytest-cov' + - name: pip install pytest pytest-cov codecov + run: '%PYTHON%\%PIP% install pytest pytest-cov codecov' env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 PIP: Scripts\pip.exe - name: Test Pillow @@ -116,22 +188,14 @@ jobs: cd /D %GITHUB_WORKSPACE% %PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 EXECUTABLE: python.exe - - - name: Install Codecov - run: '%PYTHON%\%PIP% install codecov' - env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ - PIP: Scripts\pip.exe - name: Upload coverage run: | cd /D %GITHUB_WORKSPACE% - codecov --file coverage.xml --name %PYTHON% - echo TODO upload coverage - exit /B 1 + %PYTHON%\%CODECOV% --file coverage.xml --name %PYTHON% env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86\ - EXECUTABLE: python.exe + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 + CODECOV: Scripts\codecov.exe diff --git a/winbuild/build.py b/winbuild/build.py index 0617022dc..e66df2f03 100755 --- a/winbuild/build.py +++ b/winbuild/build.py @@ -127,7 +127,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 From 1b59dd4f7980cb0abf4d012f449fd5c6ba4228f3 Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 24 Sep 2019 11:48:14 +0200 Subject: [PATCH 007/186] test-windows.yml build_dep ghostscript --- .github/workflows/test-windows.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 321940bd2..6ed500c83 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -142,10 +142,7 @@ jobs: copy /Y /B bin\*.lib %INCLIB% - name: Build dependencies / ghostscript - if: false run: | - REM only used for Python 2.7 in AppVeyor? - 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 @@ -155,7 +152,8 @@ jobs: set MSVC_VERSION=14 set RCOMP="C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\RC.Exe" nmake -nologo -f psi\msvc.mak - copy /Y /B bin\ %PYTHON% + rem Add bin to PATH variable: Copy to INCLIB, then add INCLIB to PATH in Test step. + copy /Y /B bin\* %INCLIB% env: PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 @@ -185,6 +183,9 @@ jobs: - name: Test Pillow run: | + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + rem Add GhostScript executables (copied to INCLIB) to PATH. + path %INCLIB%;%PATH% cd /D %GITHUB_WORKSPACE% %PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests env: From df1e290d88751857c264bb440cab3f14785b5c6d Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 24 Sep 2019 12:59:44 +0200 Subject: [PATCH 008/186] test-windows.yml matrix python-version --- .github/workflows/test-windows.yml | 2 +- winbuild/config.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6ed500c83..2669d42fd 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -8,7 +8,7 @@ jobs: runs-on: windows-2016 strategy: matrix: - python-version: ["3.6.8"] + python-version: ["3.5.4", "3.6.8", "3.7.4"] name: Python ${{ matrix.python-version }} x86 diff --git a/winbuild/config.py b/winbuild/config.py index ff775a921..77be97769 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -11,7 +11,9 @@ pythons = { "36": {"compiler": 7.1, "vc": 2015}, "37": {"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}, } VIRT_BASE = "c:/vp/" From 74a7fd598549ced7e54ff11f66e5cd328970c3c6 Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 24 Sep 2019 13:33:52 +0200 Subject: [PATCH 009/186] test-windows.yml matrix platform --- .github/workflows/test-windows.yml | 57 +++++++++++++++++------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 2669d42fd..4a2b79cfd 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -8,9 +8,19 @@ jobs: runs-on: windows-2016 strategy: matrix: - python-version: ["3.5.4", "3.6.8", "3.7.4"] + python-version: + - "3.5.4" + - "3.6.8" + - "3.7.4" + platform: + - name: "x86" + vars: "x86" + msbuild: "Win32" + - name: "x64" + vars: "x86_amd64" + msbuild: "x64" - name: Python ${{ matrix.python-version }} x86 + name: Python ${{ matrix.python-version }} ${{ matrix.platform.name }} steps: - uses: actions/checkout@v1 @@ -27,7 +37,7 @@ jobs: python.exe $env:GITHUB_WORKSPACE\winbuild\build_dep.py # .\build_deps.cmd env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\${{ matrix.platform.name }} EXECUTABLE: bin\python.exe shell: pwsh @@ -37,7 +47,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" x86 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 echo on nmake -nologo -f makefile.vc setup-vc6 nmake -nologo -f makefile.vc clean @@ -51,7 +61,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" x86 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 echo on nmake -nologo -f win32\Makefile.msc clean nmake -nologo -f win32\Makefile.msc zlib.lib @@ -65,7 +75,7 @@ jobs: set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 set BUILD=%GITHUB_WORKSPACE%\winbuild\build cd /D %BUILD%\tiff-4.0.10 - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 echo on copy %GITHUB_WORKSPACE%\winbuild\nmake.opt nmake.opt nmake -nologo -f makefile.vc clean @@ -80,13 +90,13 @@ jobs: 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 14.0\VC\vcvarsall.bat" x86 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 echo on rmdir /S /Q output\release-static - nmake -nologo -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output ARCH=x86 all + nmake -nologo -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output ARCH=${{ matrix.platform.name }} all mkdir %INCLIB%\webp copy /Y /B src\webp\*.h %INCLIB%\webp - copy /Y /B output\release-static\x86\lib\* %INCLIB% + copy /Y /B output\release-static\${{ matrix.platform.name }}\lib\* %INCLIB% - name: Build dependencies / freetype run: | @@ -96,16 +106,16 @@ jobs: 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 14.0\VC\vcvarsall.bat" x86 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 echo on rmdir /S /Q objs set DefaultPlatformToolset=v140 set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\VC\VCTargets set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" - %MSBUILD% builds\windows\vc2010\freetype.sln /t:Build /p:Configuration="Release" /p:Platform=Win32 /m + %MSBUILD% builds\windows\vc2010\freetype.sln /t:Build /p:Configuration="Release" /p:Platform=${{ matrix.platform.msbuild }} /m xcopy /Y /E /Q include %INCLIB% - copy /Y /B objs\Win32\Release\freetype.dll %INCLIB% - copy /Y /B objs\Win32\Release\freetype.lib %INCLIB% + copy /Y /B objs\${{ matrix.platform.msbuild }}\Release\freetype.dll %INCLIB% + copy /Y /B objs\${{ matrix.platform.msbuild }}\Release\freetype.lib %INCLIB% - name: Build dependencies / lcms2 if: false @@ -116,13 +126,13 @@ jobs: set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 set BUILD=%GITHUB_WORKSPACE%\winbuild\build cd /D %BUILD%\lcms2-2.7 - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 echo on rmdir /S /Q Lib rmdir /S /Q Projects\VC2015\Release set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\VC\VCTargets set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" - %MSBUILD% Projects\VC2015\lcms2.sln /t:Clean;lcms2_static /p:Configuration="Release" /p:Platform=Win32 /m + %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% @@ -132,7 +142,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" x86 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 echo on cmake.exe -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF -DCMAKE_BUILD_TYPE=Release -G "NMake Makefiles" . nmake -nologo -f Makefile clean @@ -147,15 +157,14 @@ jobs: set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 set BUILD=%GITHUB_WORKSPACE%\winbuild\build cd /D %BUILD%\ghostscript-9.27 - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 echo on set MSVC_VERSION=14 set RCOMP="C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\RC.Exe" + if "${{ matrix.platform.name }}"=="x64" set WIN64="" nmake -nologo -f psi\msvc.mak rem Add bin to PATH variable: Copy to INCLIB, then add INCLIB to PATH in Test step. copy /Y /B bin\* %INCLIB% - env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 - name: Build Pillow run: | @@ -166,19 +175,19 @@ jobs: set DISTUTILS_USE_SDK=1 set LIB=%INCLIB%;%PYTHON%\tcl set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 set BLDOPT=install %PYTHON%\%EXECUTABLE% setup.py build_ext --add-imaging-libs=msvcrt install %PYTHON%\%EXECUTABLE% -c "from PIL import _webp;import os, shutil;shutil.copy(r'%INCLIB%\freetype.dll', os.path.dirname(_webp.__file__));" %PYTHON%\%EXECUTABLE% selftest.py --installed env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\${{ matrix.platform.name }} EXECUTABLE: python.exe - name: pip install pytest pytest-cov codecov run: '%PYTHON%\%PIP% install pytest pytest-cov codecov' env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\${{ matrix.platform.name }} PIP: Scripts\pip.exe - name: Test Pillow @@ -189,7 +198,7 @@ jobs: cd /D %GITHUB_WORKSPACE% %PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\${{ matrix.platform.name }} EXECUTABLE: python.exe - name: Upload coverage @@ -197,6 +206,6 @@ jobs: cd /D %GITHUB_WORKSPACE% %PYTHON%\%CODECOV% --file coverage.xml --name %PYTHON% env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\x86 + PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\${{ matrix.platform.name }} CODECOV: Scripts\codecov.exe From 4d35cb9d0a4514ff62464dc0abcee5effa3d240d Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 24 Sep 2019 18:15:31 +0200 Subject: [PATCH 010/186] test-windows.yml pypy3, use actions/setup-python --- .github/workflows/test-windows.yml | 108 ++++++++++++++++++----------- Tests/helper.py | 9 ++- Tests/test_image_access.py | 6 +- Tests/test_imageshow.py | 9 ++- Tests/test_main.py | 8 +++ 5 files changed, 92 insertions(+), 48 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 4a2b79cfd..d7b303702 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -7,26 +7,57 @@ jobs: runs-on: windows-2016 strategy: + fail-fast: false matrix: - python-version: - - "3.5.4" - - "3.6.8" - - "3.7.4" - platform: - - name: "x86" - vars: "x86" - msbuild: "Win32" - - name: "x64" - vars: "x86_amd64" - msbuild: "x64" + python-version: ["3.5", "3.6", "3.7", "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: "pypy-c-jit-97588-7392d01b93d0-win32" + pypy-url: "http://buildbot.pypy.org/nightly/py3.6/pypy-c-jit-97588-7392d01b93d0-win32.zip" + # pypy-url: "https://bitbucket.org/pypy/pypy/downloads/${{ matrix.pypy-version }}.zip" + exclude: + - python-version: "pypy3.6" + architecture: "x64" + timeout-minutes: 30 - name: Python ${{ matrix.python-version }} ${{ matrix.platform.name }} + name: Python ${{ matrix.python-version }} ${{ matrix.architecture }} steps: - uses: actions/checkout@v1 + + - 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: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: Fetch dependencies run: | + $env:PYTHON=$env:pythonLocation curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip 7z x pillow-depends.zip -oc:\ mv c:\pillow-depends-master c:\pillow-depends @@ -37,7 +68,6 @@ jobs: python.exe $env:GITHUB_WORKSPACE\winbuild\build_dep.py # .\build_deps.cmd env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\${{ matrix.platform.name }} EXECUTABLE: bin\python.exe shell: pwsh @@ -47,7 +77,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on nmake -nologo -f makefile.vc setup-vc6 nmake -nologo -f makefile.vc clean @@ -61,7 +91,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on nmake -nologo -f win32\Makefile.msc clean nmake -nologo -f win32\Makefile.msc zlib.lib @@ -75,7 +105,7 @@ jobs: set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 set BUILD=%GITHUB_WORKSPACE%\winbuild\build cd /D %BUILD%\tiff-4.0.10 - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on copy %GITHUB_WORKSPACE%\winbuild\nmake.opt nmake.opt nmake -nologo -f makefile.vc clean @@ -90,13 +120,13 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on rmdir /S /Q output\release-static - nmake -nologo -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output ARCH=${{ matrix.platform.name }} all + nmake -nologo -f Makefile.vc CFG=release-static RTLIBCFG=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.platform.name }}\lib\* %INCLIB% + copy /Y /B output\release-static\${{ matrix.architecture }}\lib\* %INCLIB% - name: Build dependencies / freetype run: | @@ -106,16 +136,16 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on rmdir /S /Q objs set DefaultPlatformToolset=v140 set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\VC\VCTargets set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" - %MSBUILD% builds\windows\vc2010\freetype.sln /t:Build /p:Configuration="Release" /p:Platform=${{ matrix.platform.msbuild }} /m + %MSBUILD% builds\windows\vc2010\freetype.sln /t:Build /p:Configuration="Release" /p:Platform=${{ matrix.platform-msbuild }} /m xcopy /Y /E /Q include %INCLIB% - copy /Y /B objs\${{ matrix.platform.msbuild }}\Release\freetype.dll %INCLIB% - copy /Y /B objs\${{ matrix.platform.msbuild }}\Release\freetype.lib %INCLIB% + copy /Y /B objs\${{ matrix.platform-msbuild }}\Release\freetype.dll %INCLIB% + copy /Y /B objs\${{ matrix.platform-msbuild }}\Release\freetype.lib %INCLIB% - name: Build dependencies / lcms2 if: false @@ -126,13 +156,13 @@ jobs: set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 set BUILD=%GITHUB_WORKSPACE%\winbuild\build cd /D %BUILD%\lcms2-2.7 - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on rmdir /S /Q Lib rmdir /S /Q Projects\VC2015\Release set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\VC\VCTargets set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" - %MSBUILD% Projects\VC2015\lcms2.sln /t:Clean;lcms2_static /p:Configuration="Release" /p:Platform=${{ matrix.platform.msbuild }} /m + %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% @@ -142,7 +172,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on cmake.exe -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF -DCMAKE_BUILD_TYPE=Release -G "NMake Makefiles" . nmake -nologo -f Makefile clean @@ -157,17 +187,18 @@ jobs: set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 set BUILD=%GITHUB_WORKSPACE%\winbuild\build cd /D %BUILD%\ghostscript-9.27 - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on set MSVC_VERSION=14 set RCOMP="C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\RC.Exe" - if "${{ matrix.platform.name }}"=="x64" set WIN64="" + if "${{ matrix.architecture }}"=="x64" set WIN64="" nmake -nologo -f psi\msvc.mak rem Add bin to PATH variable: Copy to INCLIB, then add INCLIB to PATH in Test step. copy /Y /B bin\* %INCLIB% - 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 @@ -175,37 +206,30 @@ jobs: set DISTUTILS_USE_SDK=1 set LIB=%INCLIB%;%PYTHON%\tcl set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform.vars }} 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 set BLDOPT=install %PYTHON%\%EXECUTABLE% setup.py build_ext --add-imaging-libs=msvcrt install %PYTHON%\%EXECUTABLE% -c "from PIL import _webp;import os, shutil;shutil.copy(r'%INCLIB%\freetype.dll', os.path.dirname(_webp.__file__));" %PYTHON%\%EXECUTABLE% selftest.py --installed env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\${{ matrix.platform.name }} EXECUTABLE: python.exe - name: pip install pytest pytest-cov codecov - run: '%PYTHON%\%PIP% install pytest pytest-cov codecov' - env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\${{ matrix.platform.name }} - PIP: Scripts\pip.exe + run: | + "%pythonLocation%\python.exe" -m pip install pytest pytest-cov + pip install codecov - name: Test Pillow run: | + set PYTHON=%pythonLocation% set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 rem Add GhostScript executables (copied to INCLIB) to PATH. path %INCLIB%;%PATH% cd /D %GITHUB_WORKSPACE% - %PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests - env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\${{ matrix.platform.name }} - EXECUTABLE: python.exe + %PYTHON%\python.exe -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests - name: Upload coverage - run: | - cd /D %GITHUB_WORKSPACE% - %PYTHON%\%CODECOV% --file coverage.xml --name %PYTHON% + run: 'codecov --file "%GITHUB_WORKSPACE%\coverage.xml" --name "%pythonLocation%"' env: - PYTHON: C:\hostedtoolcache\windows\Python\${{ matrix.python-version }}\${{ matrix.platform.name }} CODECOV: Scripts\codecov.exe diff --git a/Tests/helper.py b/Tests/helper.py index 78a2f520f..c352f54b9 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -355,10 +355,17 @@ def on_appveyor(): return "APPVEYOR" in os.environ +def on_github_actions(): + return "GITHUB_ACTIONS" in os.environ + + def on_ci(): # Travis and AppVeyor have "CI" # Azure Pipelines has "TF_BUILD" - return "CI" in os.environ or "TF_BUILD" in os.environ + # GitHub Actions has "GITHUB_ACTIONS" + return ( + "CI" in os.environ or "TF_BUILD" in os.environ or "GITHUB_ACTIONS" in os.environ + ) if sys.platform == "win32": diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index b06814cb9..b69c2040f 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -3,7 +3,7 @@ import sys from PIL import Image -from .helper import PillowTestCase, hopper, on_appveyor, unittest +from .helper import PillowTestCase, hopper, on_ci, unittest # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -333,8 +333,8 @@ 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 sys.platform.startswith("win32") or on_ci(), + "Failing on AppVeyor / GitHub Actions when run from subprocess, not from shell", ) def test_embeddable(self): import subprocess diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 2f2620b74..bad0329af 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,6 +1,8 @@ +import sys + from PIL import Image, ImageShow -from .helper import PillowTestCase, hopper, on_ci, unittest +from .helper import PillowTestCase, hopper, on_ci, on_github_actions, unittest class TestImageShow(PillowTestCase): @@ -34,7 +36,10 @@ class TestImageShow(PillowTestCase): # Restore original state ImageShow._viewers.pop(0) - @unittest.skipUnless(on_ci(), "Only run on CIs") + @unittest.skipUnless( + on_ci() and not (sys.platform == "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) diff --git a/Tests/test_main.py b/Tests/test_main.py index 847def834..936a938f9 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -5,8 +5,16 @@ import subprocess import sys from unittest import TestCase +from .helper import on_github_actions, unittest + class TestMain(TestCase): + @unittest.skipIf( + sys.platform == "win32" + and hasattr(sys, "pypy_translation_info") + and on_github_actions(), + "Failing on Windows on GitHub Actions running PyPy", + ) def test_main(self): out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8") lines = out.splitlines() From 8f91eff078534a738d3a8f6b863585ee36d98895 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Feb 2016 20:02:15 +1100 Subject: [PATCH 011/186] Changed default frombuffer raw decoder args --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0cdfcc9a9..1f571fd3b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2599,7 +2599,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): 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)) From 2f5e24da48ee0ef0c1c1add6703f18f24a2f928f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Aug 2018 09:27:29 +1000 Subject: [PATCH 012/186] Removed warning --- src/PIL/Image.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1f571fd3b..f04fb73c1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2592,13 +2592,6 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): if decoder_name == "raw": if args == (): - warnings.warn( - "the frombuffer defaults will change in Pillow 7.0.0; " - "for portability, change the call to read:\n" - " frombuffer(mode, size, data, 'raw', mode, 0, 1)", - RuntimeWarning, - stacklevel=2, - ) args = mode, 0, 1 if args[0] in _MAPMODES: im = new(mode, (1, 1)) From e1d3437644eebcdb0389510880642b3646967e5b Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 25 Sep 2019 09:58:29 +0200 Subject: [PATCH 013/186] test-windows.yml upload-artifact --- .github/workflows/test-windows.yml | 43 +++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index d7b303702..7d0f0656a 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -54,6 +54,11 @@ jobs: with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.architecture }} + + - name: pip install wheel pytest pytest-cov codecov + run: | + "%pythonLocation%\python.exe" -m pip install wheel pytest pytest-cov + pip install codecov - name: Fetch dependencies run: | @@ -207,17 +212,9 @@ jobs: set LIB=%INCLIB%;%PYTHON%\tcl set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 - set BLDOPT=install - %PYTHON%\%EXECUTABLE% setup.py build_ext --add-imaging-libs=msvcrt install - %PYTHON%\%EXECUTABLE% -c "from PIL import _webp;import os, shutil;shutil.copy(r'%INCLIB%\freetype.dll', os.path.dirname(_webp.__file__));" - %PYTHON%\%EXECUTABLE% selftest.py --installed - env: - EXECUTABLE: python.exe - - - name: pip install pytest pytest-cov codecov - run: | - "%pythonLocation%\python.exe" -m pip install pytest pytest-cov - pip install codecov + %PYTHON%\python.exe setup.py build_ext --add-imaging-libs=msvcrt install + %PYTHON%\python.exe -c "from PIL import _webp;import os, shutil;shutil.copy(r'%INCLIB%\freetype.dll', os.path.dirname(_webp.__file__));" + %PYTHON%\python.exe selftest.py --installed - name: Test Pillow run: | @@ -232,4 +229,26 @@ jobs: run: 'codecov --file "%GITHUB_WORKSPACE%\coverage.xml" --name "%pythonLocation%"' env: CODECOV: Scripts\codecov.exe - + + - 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 DISTUTILS_USE_SDK=1 + set LIB=%INCLIB%;%PYTHON%\tcl + set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + %PYTHON%\python.exe setup.py build_ext --add-imaging-libs=msvcrt bdist_wheel + + - uses: actions/upload-artifact@v1 + if: "github.event_name == 'push' && !contains(matrix.python-version, 'pypy')" + with: + name: ${{ steps.wheel.outputs.dist }} + path: dist From 113a72633aa6a7bec710a215313c6796424ef891 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 25 Sep 2019 11:28:52 +0200 Subject: [PATCH 014/186] test-windows.yml clean names --- .github/workflows/test-windows.yml | 10 +++++----- Tests/test_imageshow.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 7d0f0656a..33123b178 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -104,7 +104,7 @@ jobs: copy /Y /B *.lib %INCLIB% copy /Y /B zlib.lib %INCLIB%\z.lib - - name: Build dependencies / libtiff + - 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 @@ -119,7 +119,7 @@ jobs: copy /Y /B libtiff\*.dll %INCLIB% copy /Y /B libtiff\*.lib %INCLIB% - - name: Build dependencies / webp + - 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 @@ -133,7 +133,7 @@ jobs: copy /Y /B src\webp\*.h %INCLIB%\webp copy /Y /B output\release-static\${{ matrix.architecture }}\lib\* %INCLIB% - - name: Build dependencies / freetype + - name: Build dependencies / FreeType run: | REM Toolkit v100 not available; missing VCTargetsPath; Clean fails @@ -152,7 +152,7 @@ jobs: copy /Y /B objs\${{ matrix.platform-msbuild }}\Release\freetype.dll %INCLIB% copy /Y /B objs\${{ matrix.platform-msbuild }}\Release\freetype.lib %INCLIB% - - name: Build dependencies / lcms2 + - name: Build dependencies / LCMS2 if: false run: | REM Projects\VC2015\lcms2.sln is not available in lcms2-2.7 @@ -171,7 +171,7 @@ jobs: xcopy /Y /E /Q include %INCLIB% copy /Y /B Lib\MS\*.lib %INCLIB% - - name: Build dependencies / openjpeg + - 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 diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index bad0329af..1c756da9f 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -38,7 +38,7 @@ class TestImageShow(PillowTestCase): @unittest.skipUnless( on_ci() and not (sys.platform == "win32" and on_github_actions()), - "Only run on CIs; hangs on Windows 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"): From cf1f8b04985233816a6971dcdd3831bb5afa46c1 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 25 Sep 2019 11:46:54 +0200 Subject: [PATCH 015/186] Tests.helper cleanup --- Tests/check_imaging_leaks.py | 6 ++---- Tests/check_j2k_leaks.py | 5 ++--- Tests/check_jpeg_leaks.py | 5 ++--- Tests/helper.py | 8 ++++++++ Tests/test_core_resources.py | 12 +++++------- Tests/test_file_jpeg.py | 12 +++++++++--- Tests/test_file_png.py | 5 ++--- Tests/test_file_tiff.py | 5 ++--- Tests/test_font_leaks.py | 6 ++---- Tests/test_image.py | 7 ++----- Tests/test_image_access.py | 4 ++-- Tests/test_imagefont.py | 11 ++++------- Tests/test_imageshow.py | 6 ++---- Tests/test_imagewin.py | 6 ++---- Tests/test_imagewin_pointers.py | 5 ++--- Tests/test_main.py | 6 ++---- Tests/test_map.py | 4 ++-- Tests/test_shell_injection.py | 4 ++-- 18 files changed, 54 insertions(+), 63 deletions(-) diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 2b9a9605b..a109b5cd5 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -2,17 +2,15 @@ from __future__ import division -import sys - from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase, is_win32, unittest 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_leaks.py b/Tests/check_j2k_leaks.py index 4614529ed..d9c6c68b7 100755 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,9 +1,8 @@ -import sys from io import BytesIO from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase, is_win32, unittest # Limits for testing the leak mem_limit = 1024 * 1048576 @@ -13,7 +12,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_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 2f758ba10..8cb93f2dd 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,7 +1,6 @@ -import sys from io import BytesIO -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper, is_win32, unittest iterations = 5000 @@ -15,7 +14,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/helper.py b/Tests/helper.py index c352f54b9..312c6aa4e 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -368,6 +368,14 @@ def on_ci(): ) +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: diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index eefb1a0ef..0aedb91c9 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -4,9 +4,7 @@ import sys from PIL import Image -from .helper import PillowTestCase, unittest - -is_pypy = hasattr(sys, "pypy_version_info") +from .helper import PillowTestCase, is_pypy, unittest class TestCoreStats(PillowTestCase): @@ -87,7 +85,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 +106,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 +121,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 +151,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_file_jpeg.py b/Tests/test_file_jpeg.py index 7f9bf7c1d..5a34a3faa 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) @@ -654,7 +660,7 @@ class TestFileJpeg(PillowTestCase): 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_png.py b/Tests/test_file_png.py index 6d76a6caa..07e84ef72 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,11 +1,10 @@ -import sys 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, unittest try: from PIL import _webp @@ -650,7 +649,7 @@ class TestFilePng(PillowTestCase): 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_tiff.py b/Tests/test_file_tiff.py index c3f55150c..c8a4c2df3 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,12 +1,11 @@ import logging -import sys from io import BytesIO from PIL import Image, TiffImagePlugin, features from PIL._util import py3 from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper, is_win32, unittest logger = logging.getLogger(__name__) @@ -612,7 +611,7 @@ class TestFileTiff(PillowTestCase): im.load() -@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") diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 14b368585..9b5fe2055 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,13 +1,11 @@ from __future__ import division -import sys - from PIL import Image, ImageDraw, ImageFont, features -from .helper import PillowLeakTestCase, unittest +from .helper import PillowLeakTestCase, is_win32, unittest -@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") +@unittest.skipIf(is_win32(), "requires Unix or macOS") class TestTTypeFontLeak(PillowLeakTestCase): # fails at iteration 3 in master iterations = 10 diff --git a/Tests/test_image.py b/Tests/test_image.py index 493b4735a..cbf52b3b4 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,11 +1,10 @@ import os import shutil -import sys from PIL import Image from PIL._util import py3 -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper, is_win32, unittest class TestImage(PillowTestCase): @@ -150,9 +149,7 @@ 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) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index b69c2040f..577df3dff 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -3,7 +3,7 @@ import sys from PIL import Image -from .helper import PillowTestCase, hopper, on_ci, unittest +from .helper import PillowTestCase, hopper, is_win32, on_ci, unittest # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -333,7 +333,7 @@ class TestCffi(AccessTest): class TestEmbeddable(unittest.TestCase): @unittest.skipIf( - not sys.platform.startswith("win32") or on_ci(), + not is_win32() or on_ci(), "Failing on AppVeyor / GitHub Actions when run from subprocess, not from shell", ) def test_embeddable(self): diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 6a2d572a9..6944404c1 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -9,7 +9,7 @@ from io import BytesIO from PIL import Image, ImageDraw, ImageFont, features -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase, is_pypy, is_win32, unittest FONT_PATH = "Tests/fonts/FreeMono.ttf" FONT_SIZE = 20 @@ -464,10 +464,7 @@ class TestImageFont(PillowTestCase): with self.assertRaises(UnicodeEncodeError): font.getsize(u"’") - @unittest.skipIf( - sys.version.startswith("2") or hasattr(sys, "pypy_translation_info"), - "requires CPython 3.3+", - ) + @unittest.skipIf(sys.version.startswith("2") or is_pypy(), "requires CPython 3.3+") def test_unicode_extended(self): # issue #3777 text = u"A\u278A\U0001F12B" @@ -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. diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 1c756da9f..aac48d6c0 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,8 +1,6 @@ -import sys - from PIL import Image, ImageShow -from .helper import PillowTestCase, hopper, on_ci, on_github_actions, unittest +from .helper import PillowTestCase, hopper, is_win32, on_ci, on_github_actions, unittest class TestImageShow(PillowTestCase): @@ -37,7 +35,7 @@ class TestImageShow(PillowTestCase): ImageShow._viewers.pop(0) @unittest.skipUnless( - on_ci() and not (sys.platform == "win32" and on_github_actions()), + on_ci() and not (is_win32() and on_github_actions()), "Only run on CIs; hangs on Windows on GitHub Actions", ) def test_show(self): diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 92fcdc28d..2b2034187 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,8 +1,6 @@ -import sys - from PIL import ImageWin -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper, is_win32, unittest class TestImageWin(PillowTestCase): @@ -32,7 +30,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_main.py b/Tests/test_main.py index 936a938f9..c6f9a694e 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -5,14 +5,12 @@ import subprocess import sys from unittest import TestCase -from .helper import on_github_actions, unittest +from .helper import is_pypy, is_win32, on_github_actions, unittest class TestMain(TestCase): @unittest.skipIf( - sys.platform == "win32" - and hasattr(sys, "pypy_translation_info") - and on_github_actions(), + is_win32() and is_pypy() and on_github_actions(), "Failing on Windows on GitHub Actions running PyPy", ) def test_main(self): diff --git a/Tests/test_map.py b/Tests/test_map.py index 3fc42651b..8d4e32219 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -2,7 +2,7 @@ import sys from PIL import Image -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase, is_win32, unittest try: import numpy @@ -10,7 +10,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 diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 35a3dcfcd..97774a0a4 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,5 +1,4 @@ import shutil -import sys from PIL import GifImagePlugin, Image, JpegImagePlugin @@ -7,6 +6,7 @@ from .helper import ( PillowTestCase, cjpeg_available, djpeg_available, + is_win32, netpbm_available, unittest, ) @@ -17,7 +17,7 @@ TEST_GIF = "Tests/images/hopper.gif" test_filenames = ("temp_';", 'temp_";', "temp_'\"|", "temp_'\"||", "temp_'\"&&") -@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") +@unittest.skipIf(is_win32(), "requires Unix or macOS") class TestShellInjection(PillowTestCase): def assert_save_filename_check(self, src_img, save_func): for filename in test_filenames: From 04868c9e51e781601e0c4fc5f5d26f684c69a909 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 26 Sep 2019 10:10:37 +0200 Subject: [PATCH 016/186] winbuild use lcms2-2.8 for python3.x; use static freetype on GHA --- .github/workflows/test-windows.yml | 11 ++++------- winbuild/build_dep.py | 3 +++ winbuild/config.py | 7 ++++++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 33123b178..9faec2c56 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -147,26 +147,23 @@ jobs: set DefaultPlatformToolset=v140 set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\VC\VCTargets set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" - %MSBUILD% builds\windows\vc2010\freetype.sln /t:Build /p:Configuration="Release" /p:Platform=${{ matrix.platform-msbuild }} /m + %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\freetype.dll %INCLIB% - copy /Y /B objs\${{ matrix.platform-msbuild }}\Release\freetype.lib %INCLIB% + copy /Y /B "objs\${{ matrix.platform-msbuild }}\Release Static\freetype.lib" %INCLIB% - name: Build dependencies / LCMS2 - if: false run: | - REM Projects\VC2015\lcms2.sln is not available in lcms2-2.7 - 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.7 + cd /D %BUILD%\lcms2-2.8 call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on rmdir /S /Q Lib rmdir /S /Q Projects\VC2015\Release set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\VC\VCTargets set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" + 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% 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% diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index 487329db8..3ed727ef8 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -270,6 +270,7 @@ def build_lcms_70(compiler): r""" rem Build lcms2 setlocal +set LCMS=%%LCMS-2.7%% rd /S /Q %%LCMS%%\Lib rd /S /Q %%LCMS%%\Projects\VC%(vc_version)s\Release %%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:Clean /p:Configuration="Release" /p:Platform=Win32 /m @@ -287,8 +288,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%% diff --git a/winbuild/config.py b/winbuild/config.py index 77be97769..a8fce3005 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -44,11 +44,16 @@ libs = { "filename": PILLOW_DEPENDS_DIR + "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", "dir": "lcms2-2.7", }, + "lcms-2.8": { + "url": SF_MIRROR + "/project/lcms/lcms/2.8/lcms2-2.8.zip", + "filename": PILLOW_DEPENDS_DIR + "lcms2-2.8.zip", + "dir": "lcms2-2.8", + }, "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", From b81f5481b8f24bede9958b688926e056a894205b Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 26 Sep 2019 20:30:03 +0200 Subject: [PATCH 017/186] test-windows.yml fix LNK4098 for CPython --- .github/workflows/test-windows.yml | 14 +- winbuild/build_dep.py | 2 +- winbuild/{nmake.opt => tiff-md.opt} | 0 winbuild/tiff-mt.opt | 216 ++++++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 7 deletions(-) rename winbuild/{nmake.opt => tiff-md.opt} (100%) create mode 100644 winbuild/tiff-mt.opt diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 9faec2c56..3f22b458a 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -99,7 +99,7 @@ jobs: call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on nmake -nologo -f win32\Makefile.msc clean - nmake -nologo -f win32\Makefile.msc zlib.lib + nmake -nologo -f win32\Makefile.msc LOC=-MT zlib.lib copy /Y /B z*.h %INCLIB% copy /Y /B *.lib %INCLIB% copy /Y /B zlib.lib %INCLIB%\z.lib @@ -112,7 +112,7 @@ jobs: cd /D %BUILD%\tiff-4.0.10 call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on - copy %GITHUB_WORKSPACE%\winbuild\nmake.opt nmake.opt + copy %GITHUB_WORKSPACE%\winbuild\tiff-mt.opt nmake.opt nmake -nologo -f makefile.vc clean nmake -nologo -f makefile.vc lib copy /Y /B libtiff\tiff*.h %INCLIB% @@ -176,7 +176,10 @@ jobs: cd /D %BUILD%\openjpeg-2.3.1msvcr10-x32 call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on - cmake.exe -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF -DCMAKE_BUILD_TYPE=Release -G "NMake Makefiles" . + set CMAKE=cmake.exe -DOPENJP2_COMPILE_OPTIONS=-MT + set CMAKE=%CMAKE% -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF + set CMAKE=%CMAKE% -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF + %CMAKE% -DCMAKE_BUILD_TYPE=Release -G "NMake Makefiles" . nmake -nologo -f Makefile clean nmake -nologo -f Makefile mkdir %INCLIB%\openjpeg-2.3.1 @@ -209,8 +212,7 @@ jobs: set LIB=%INCLIB%;%PYTHON%\tcl set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 - %PYTHON%\python.exe setup.py build_ext --add-imaging-libs=msvcrt install - %PYTHON%\python.exe -c "from PIL import _webp;import os, shutil;shutil.copy(r'%INCLIB%\freetype.dll', os.path.dirname(_webp.__file__));" + %PYTHON%\python.exe setup.py build_ext install %PYTHON%\python.exe selftest.py --installed - name: Test Pillow @@ -242,7 +244,7 @@ jobs: set LIB=%INCLIB%;%PYTHON%\tcl set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 - %PYTHON%\python.exe setup.py build_ext --add-imaging-libs=msvcrt bdist_wheel + %PYTHON%\python.exe setup.py bdist_wheel - uses: actions/upload-artifact@v1 if: "github.event_name == 'push' && !contains(matrix.python-version, 'pypy')" diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index 3ed727ef8..4e4b70dfe 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -202,7 +202,7 @@ setlocal + vc_setup(compiler, bit) + r""" rem do after building jpeg and zlib -copy %%~dp0\nmake.opt %%TIFF%% +copy %%~dp0\tiff-md.opt %%TIFF%%\nmake.opt cd /D %%TIFF%% nmake -nologo -f makefile.vc clean diff --git a/winbuild/nmake.opt b/winbuild/tiff-md.opt similarity index 100% rename from winbuild/nmake.opt rename to winbuild/tiff-md.opt diff --git a/winbuild/tiff-mt.opt b/winbuild/tiff-mt.opt new file mode 100644 index 000000000..9298f4ca7 --- /dev/null +++ b/winbuild/tiff-mt.opt @@ -0,0 +1,216 @@ +# $Id: nmake.opt,v 1.18 2006/06/07 16:33:45 dron Exp $ +# +# Copyright (C) 2004, Andrey Kiselev +# +# Permission to use, copy, modify, distribute, and sell this software and +# its documentation for any purpose is hereby granted without fee, provided +# that (i) the above copyright notices and this permission notice appear in +# all copies of the software and related documentation, and (ii) the names of +# Sam Leffler and Silicon Graphics may not be used in any advertising or +# publicity relating to the software without the specific, prior written +# permission of Sam Leffler and Silicon Graphics. +# +# THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, +# EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY +# WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. +# +# IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR +# ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, +# OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF +# LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. + +# Compile time parameters for MS Visual C++ compiler. +# You may edit this file to specify building options. + +# +###### Edit the following lines to choose a feature set you need. ####### +# + +# +# Select WINMODE_CONSOLE to build a library which reports errors to stderr, or +# WINMODE_WINDOWED to build such that errors are reported via MessageBox(). +# +WINMODE_CONSOLE = 1 +#WINMODE_WINDOWED = 1 + +# +# Comment out the following lines to disable internal codecs. +# +# Support for CCITT Group 3 & 4 algorithms +CCITT_SUPPORT = 1 +# Support for Macintosh PackBits algorithm +PACKBITS_SUPPORT = 1 +# Support for LZW algorithm +LZW_SUPPORT = 1 +# Support for ThunderScan 4-bit RLE algorithm +THUNDER_SUPPORT = 1 +# Support for NeXT 2-bit RLE algorithm +NEXT_SUPPORT = 1 +# Support for LogLuv high dynamic range encoding +LOGLUV_SUPPORT = 1 + +# +# Uncomment and edit following lines to enable JPEG support. +# +JPEG_SUPPORT = 1 +JPEG_INCLUDE = -I$(INCLIB) +JPEG_LIB = $(INCLIB)/libjpeg.lib + +# +# Uncomment and edit following lines to enable ZIP support +# (required for Deflate compression and Pixar log-format) +# +ZIP_SUPPORT = 1 +ZLIB_INCLUDE = -I$(INCLIB) +ZLIB_LIB = $(INCLIB)/zlib.lib + +# +# Uncomment and edit following lines to enable ISO JBIG support +# +#JBIG_SUPPORT = 1 +#JBIGDIR = d:/projects/jbigkit +#JBIG_INCLUDE = -I$(JBIGDIR)/libjbig +#JBIG_LIB = $(JBIGDIR)/libjbig/jbig.lib + +# +# Uncomment following line to enable Pixar log-format algorithm +# (Zlib required). +# +#PIXARLOG_SUPPORT = 1 + +# +# Comment out the following lines to disable strip chopping +# (whether or not to convert single-strip uncompressed images to multiple +# strips of specified size to reduce memory usage). Default strip size +# is 8192 bytes, it can be configured via the STRIP_SIZE_DEFAULT parameter +# +STRIPCHOP_SUPPORT = 1 +STRIP_SIZE_DEFAULT = 8192 + +# +# Comment out the following lines to disable treating the fourth sample with +# no EXTRASAMPLE_ value as being ASSOCALPHA. Many packages produce RGBA +# files but don't mark the alpha properly. +# +EXTRASAMPLE_AS_ALPHA_SUPPORT = 1 + +# +# Comment out the following lines to disable picking up YCbCr subsampling +# info from the JPEG data stream to support files lacking the tag. +# See Bug 168 in Bugzilla, and JPEGFixupTestSubsampling() for details. +# +CHECK_JPEG_YCBCR_SUBSAMPLING = 1 + +# +####################### Compiler related options. ####################### +# + +# +# Pick debug or optimized build flags. We default to an optimized build +# with no debugging information. +# NOTE: /EHsc option required if you want to build the C++ stream API +# +OPTFLAGS = /Ox /MT /EHsc /W3 /D_CRT_SECURE_NO_DEPRECATE +#OPTFLAGS = /Zi + +# +# Uncomment following line to enable using Windows Common RunTime Library +# instead of Windows specific system calls. See notes on top of tif_unix.c +# module for details. +# +USE_WIN_CRT_LIB = 1 + +# Compiler specific options. You may probably want to adjust compilation +# parameters in CFLAGS variable. Refer to your compiler documentation +# for the option reference. +# +MAKE = nmake /nologo +CC = cl /nologo +CXX = cl /nologo +AR = lib /nologo +LD = link /nologo + +CFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS) +CXXFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS) +EXTRAFLAGS = +LIBS = + +# Name of the output shared library +DLLNAME = libtiff.dll + +# +########### There is nothing to edit below this line normally. ########### +# + +# Set the native cpu bit order +EXTRAFLAGS = -DFILLODER_LSB2MSB $(EXTRAFLAGS) + +!IFDEF WINMODE_WINDOWED +EXTRAFLAGS = -DTIF_PLATFORM_WINDOWED $(EXTRAFLAGS) +LIBS = user32.lib $(LIBS) +!ELSE +EXTRAFLAGS = -DTIF_PLATFORM_CONSOLE $(EXTRAFLAGS) +!ENDIF + +# Codec stuff +!IFDEF CCITT_SUPPORT +EXTRAFLAGS = -DCCITT_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF PACKBITS_SUPPORT +EXTRAFLAGS = -DPACKBITS_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF LZW_SUPPORT +EXTRAFLAGS = -DLZW_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF THUNDER_SUPPORT +EXTRAFLAGS = -DTHUNDER_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF NEXT_SUPPORT +EXTRAFLAGS = -DNEXT_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF LOGLUV_SUPPORT +EXTRAFLAGS = -DLOGLUV_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF JPEG_SUPPORT +LIBS = $(LIBS) $(JPEG_LIB) +EXTRAFLAGS = -DJPEG_SUPPORT -DOJPEG_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF ZIP_SUPPORT +LIBS = $(LIBS) $(ZLIB_LIB) +EXTRAFLAGS = -DZIP_SUPPORT $(EXTRAFLAGS) +!IFDEF PIXARLOG_SUPPORT +EXTRAFLAGS = -DPIXARLOG_SUPPORT $(EXTRAFLAGS) +!ENDIF +!ENDIF + +!IFDEF JBIG_SUPPORT +LIBS = $(LIBS) $(JBIG_LIB) +EXTRAFLAGS = -DJBIG_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF STRIPCHOP_SUPPORT +EXTRAFLAGS = -DSTRIPCHOP_DEFAULT=TIFF_STRIPCHOP -DSTRIP_SIZE_DEFAULT=$(STRIP_SIZE_DEFAULT) $(EXTRAFLAGS) +!ENDIF + +!IFDEF EXTRASAMPLE_AS_ALPHA_SUPPORT +EXTRAFLAGS = -DDEFAULT_EXTRASAMPLE_AS_ALPHA $(EXTRAFLAGS) +!ENDIF + +!IFDEF CHECK_JPEG_YCBCR_SUBSAMPLING +EXTRAFLAGS = -DCHECK_JPEG_YCBCR_SUBSAMPLING $(EXTRAFLAGS) +!ENDIF + +!IFDEF USE_WIN_CRT_LIB +EXTRAFLAGS = -DAVOID_WIN32_FILEIO $(EXTRAFLAGS) +!ELSE +EXTRAFLAGS = -DUSE_WIN32_FILEIO $(EXTRAFLAGS) +!ENDIF From 5f4c1e113c91246e546c7f021cb2da6328606666 Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 27 Sep 2019 00:09:04 +0200 Subject: [PATCH 018/186] add libimagequant to features.py --- selftest.py | 28 +------------------- src/PIL/features.py | 64 +++++++++++++++++++++++++-------------------- src/_imaging.c | 6 +++++ 3 files changed, 42 insertions(+), 56 deletions(-) diff --git a/selftest.py b/selftest.py index dcac54a5a..817b2872a 100755 --- a/selftest.py +++ b/selftest.py @@ -2,7 +2,6 @@ # minimal sanity check from __future__ import print_function -import os import sys from PIL import Image, features @@ -162,32 +161,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, True) # use doctest to make sure the test program behaves as documented! import doctest diff --git a/src/PIL/features.py b/src/PIL/features.py index 9fd522368..e8ce1e4ff 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -56,6 +56,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 +95,7 @@ def get_supported(): return ret -def pilinfo(out=None): +def pilinfo(out=None, brief=False): if out is None: out = sys.stdout @@ -113,11 +114,12 @@ 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) + if not brief: + 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"), @@ -133,6 +135,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 +143,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 not brief: + 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/_imaging.c b/src/_imaging.c index 04520b1a1..81de38e2f 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3934,6 +3934,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); From d05655095b25a223938bf2dd3266068d5d4f2350 Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 27 Sep 2019 10:18:32 +0200 Subject: [PATCH 019/186] test-windows.yml libjpeg-turbo --- .github/workflows/test-windows.yml | 30 +++++++++++++++++++++++++++--- winbuild/config.py | 7 ++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 3f22b458a..b42dabf39 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -62,6 +62,11 @@ jobs: - name: Fetch dependencies run: | + curl -fsSL -o nasm.zip https://www.nasm.us/pub/nasm/releasebuilds/2.14.02/win64/nasm-2.14.02-win64.zip + 7z x nasm.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" + $env:PYTHON=$env:pythonLocation curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip 7z x pillow-depends.zip -oc:\ @@ -76,7 +81,9 @@ jobs: EXECUTABLE: bin\python.exe shell: pwsh + # libjpeg-turbo build is slow, pypy test is slow -> don't do both at once - name: Build dependencies / libjpeg + if: "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 @@ -90,6 +97,23 @@ jobs: copy /Y /B j*.h %INCLIB% copy /Y /B *.lib %INCLIB% + - name: Build dependencies / libjpeg-turbo + if: "!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 + cd /D %BUILD%\libjpeg-turbo-2.0.3 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + 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 -DCMAKE_BUILD_TYPE=Release + %CMAKE% -G "NMake Makefiles" . + nmake -nologo -f Makefile clean + nmake -nologo -f Makefile + copy /Y /B j*.h %INCLIB% + copy /Y /B jpeg-static.lib %INCLIB%\libjpeg.lib + - name: Build dependencies / zlib run: | set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include @@ -176,10 +200,10 @@ jobs: cd /D %BUILD%\openjpeg-2.3.1msvcr10-x32 call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on - set CMAKE=cmake.exe -DOPENJP2_COMPILE_OPTIONS=-MT - set CMAKE=%CMAKE% -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF + 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 - %CMAKE% -DCMAKE_BUILD_TYPE=Release -G "NMake Makefiles" . + set CMAKE=%CMAKE% -DOPENJP2_COMPILE_OPTIONS=-MT -DCMAKE_BUILD_TYPE=Release + %CMAKE% -G "NMake Makefiles" . nmake -nologo -f Makefile clean nmake -nologo -f Makefile mkdir %INCLIB%\openjpeg-2.3.1 diff --git a/winbuild/config.py b/winbuild/config.py index a8fce3005..728fd5ced 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -1,6 +1,6 @@ import os -SF_MIRROR = "http://iweb.dl.sourceforge.net" +SF_MIRROR = "https://iweb.dl.sourceforge.net" PILLOW_DEPENDS_DIR = "C:\\pillow-depends\\" pythons = { @@ -91,6 +91,11 @@ libs = { "filename": PILLOW_DEPENDS_DIR + "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": PILLOW_DEPENDS_DIR + "libjpeg-turbo-2.0.3.tar.gz", + "dir": "libjpeg-turbo-2.0.3", + }, } compilers = { From 58f1a143a21c861f006287490f0698e973cb277f Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 27 Sep 2019 12:37:54 +0200 Subject: [PATCH 020/186] test-windows.yml imagequant --- .github/workflows/test-windows.yml | 22 ++++++++++++++++++++++ winbuild/config.py | 7 +++++++ 2 files changed, 29 insertions(+) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index b42dabf39..26f7ef6a3 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -210,6 +210,28 @@ jobs: copy /Y /B src\lib\openjp2\*.h %INCLIB%\openjpeg-2.3.1 copy /Y /B bin\*.lib %INCLIB% + # 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 ba653c8: Merge tag '2.12.5' into msvc + cd /D %BUILD%\libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + echo on + echo (gc CMakeLists.txt) -replace 'add_library', "add_compile_options(-MT -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% + - name: Build dependencies / ghostscript run: | set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include diff --git a/winbuild/config.py b/winbuild/config.py index 728fd5ced..962f0112b 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -96,6 +96,13 @@ libs = { "filename": PILLOW_DEPENDS_DIR + "libjpeg-turbo-2.0.3.tar.gz", "dir": "libjpeg-turbo-2.0.3", }, + # ba653c8: Merge tag '2.12.5' into msvc + "imagequant": { + "url": "https://github.com/ImageOptim/libimagequant/archive/ba653c8ccb34dde4e21c6076d85a72d21ed9d971.zip", # noqa: E501 + "filename": PILLOW_DEPENDS_DIR + + "libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971.zip", + "dir": "libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971", + }, } compilers = { From 820d088115f228a2519398ac6cf1e7df3beb356f Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 29 Sep 2019 22:39:18 +0200 Subject: [PATCH 021/186] test-windows.yml harfbuzz --- .github/workflows/test-windows.yml | 21 +++++++++++++++++++++ winbuild/config.py | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 26f7ef6a3..693a5f565 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -232,7 +232,28 @@ jobs: copy /Y /B *.h %INCLIB% copy /Y /B *.lib %INCLIB% + - 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 + cd /D %BUILD%\harfbuzz-2.6.1 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + echo on + echo (gc CMakeLists.txt) -replace 'enable_testing', "add_compile_options(-MT)`r`nenable_testing" ^| Out-File -encoding ASCII CMakeLists.txt > patch.ps1 + powershell .\patch.ps1 + type 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 harfbuzz + copy /Y /B src\*.h %INCLIB% + copy /Y /B *.lib %INCLIB% + + - name: Build dependencies / ghostscript + if: false run: | set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 diff --git a/winbuild/config.py b/winbuild/config.py index 962f0112b..8eabae2e9 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -103,6 +103,11 @@ libs = { + "libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971.zip", "dir": "libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971", }, + "harfbuzz": { + "url": "https://github.com/harfbuzz/harfbuzz/archive/2.6.1.zip", + "filename": PILLOW_DEPENDS_DIR + "harfbuzz-2.6.1.zip", + "dir": "harfbuzz-2.6.1", + }, } compilers = { From a2b40f6caba9d511c947b43b5d29402b5db52afb Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 30 Sep 2019 01:04:26 +0200 Subject: [PATCH 022/186] fribidi --- .github/workflows/test-windows.yml | 19 +++++- winbuild/config.py | 5 ++ winbuild/fribidi.cmake | 104 +++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 winbuild/fribidi.cmake diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 693a5f565..4d117cc35 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -232,7 +232,7 @@ jobs: copy /Y /B *.h %INCLIB% copy /Y /B *.lib %INCLIB% - - name: Build dependencies / harfbuzz + - 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 @@ -242,7 +242,6 @@ jobs: echo on echo (gc CMakeLists.txt) -replace 'enable_testing', "add_compile_options(-MT)`r`nenable_testing" ^| Out-File -encoding ASCII CMakeLists.txt > patch.ps1 powershell .\patch.ps1 - type 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" . @@ -251,6 +250,22 @@ jobs: copy /Y /B src\*.h %INCLIB% copy /Y /B *.lib %INCLIB% + - 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + 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% - name: Build dependencies / ghostscript if: false diff --git a/winbuild/config.py b/winbuild/config.py index 8eabae2e9..93ccf3806 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -108,6 +108,11 @@ libs = { "filename": PILLOW_DEPENDS_DIR + "harfbuzz-2.6.1.zip", "dir": "harfbuzz-2.6.1", }, + "fribidi": { + "url": "https://github.com/fribidi/fribidi/archive/v1.0.7.zip", + "filename": PILLOW_DEPENDS_DIR + "fribidi-1.0.7.zip", + "dir": "fribidi-1.0.7", + }, } compilers = { diff --git a/winbuild/fribidi.cmake b/winbuild/fribidi.cmake new file mode 100644 index 000000000..82d1d9ccb --- /dev/null +++ b/winbuild/fribidi.cmake @@ -0,0 +1,104 @@ +cmake_minimum_required(VERSION 3.13) + +project(fribidi) + +add_definitions(-D_CRT_SECURE_NO_WARNINGS) +set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -MT) + +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 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 ${CMAKE_CURRENT_BINARY_DIR}/${_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 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-config.h + ${FRIBIDI_SOURCES_GENERATED}) +fribidi_definitions(fribidi) +target_compile_definitions(fribidi + PUBLIC -DFRIBIDI_ENTRY=extern) From f0a87e25a4a83969ada40dda60b39e718e43b234 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 26 Sep 2019 21:38:19 +0300 Subject: [PATCH 023/186] Drop support for EOL PyQt4 and PySide --- Tests/test_imageqt.py | 8 -------- Tests/test_qt_image_toqimage.py | 24 ++---------------------- docs/deprecations.rst | 22 +++++++++++----------- docs/reference/ImageQt.rst | 12 +++--------- src/PIL/ImageQt.py | 18 +----------------- 5 files changed, 17 insertions(+), 67 deletions(-) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index dc43e6bc7..a680b916f 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -35,10 +35,6 @@ class PillowQPixmapTestCase(PillowQtTestCase): 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: @@ -59,10 +55,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 diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index d0c223b1a..5115c1862 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -9,25 +9,9 @@ if ImageQt.qt_is_installed: try: from PyQt5 import QtGui from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication - - QT_VERSION = 5 except (ImportError, RuntimeError): - try: - from PySide2 import QtGui - from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication - - QT_VERSION = 5 - except (ImportError, RuntimeError): - try: - from PyQt4 import QtGui - from PyQt4.QtGui import QWidget, QHBoxLayout, QLabel, QApplication - - QT_VERSION = 4 - except (ImportError, RuntimeError): - from PySide import QtGui - from PySide.QtGui import QWidget, QHBoxLayout, QLabel, QApplication - - QT_VERSION = 4 + from PySide2 import QtGui + from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication class TestToQImage(PillowQtTestCase, PillowTestCase): @@ -60,10 +44,6 @@ class TestToQImage(PillowQtTestCase, PillowTestCase): # Check that it actually worked. reloaded = Image.open(tempfile) - # Gray images appear to come back in palette mode. - # They're roughly equivalent - if QT_VERSION == 4 and mode == "L": - src = src.convert("P") self.assert_image_equal(reloaded, src) def test_segfault(self): diff --git a/docs/deprecations.rst b/docs/deprecations.rst index f00f3e31f..4dd267460 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -45,17 +45,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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -128,6 +117,17 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +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. + VERSION constant ~~~~~~~~~~~~~~~~ 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/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 2edb0a12b..da60cacd0 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 -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 From 485ea4ee8fb73f3134e76412338ae45ca06135ec Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 26 Sep 2019 21:39:27 +0300 Subject: [PATCH 024/186] Drop support for EOL PyQt4 --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 545cb0b50..921fffd44 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,9 +24,6 @@ matrix: 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 From 490852915e6bfeaed4140d95446c9e3548c05f53 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Oct 2019 20:27:20 +1000 Subject: [PATCH 025/186] Do not download dev wheels [ci skip] --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index e1f57883c..feeb7c0d7 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -92,7 +92,7 @@ Released as needed privately to individual vendors for critical security-related ``` * [ ] Download distributions from the [Pillow Wheel Builder container](http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com/). ```bash - wget -m -A 'Pillow-*' \ + wget -m -A 'Pillow--*' \ http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com ``` From f898ccbaf8da9e238b62a1b6ac2f608b6ee464d7 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 1 Oct 2019 14:43:15 +0300 Subject: [PATCH 026/186] Remove deprecated PILLOW_VERSION --- docs/deprecations.rst | 15 +++++++-------- src/PIL/Image.py | 7 ++----- src/PIL/__init__.py | 4 ++-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index f00f3e31f..2ce2b5fa5 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -94,14 +94,6 @@ a ``DeprecationWarning``: 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 +120,13 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +PILLOW_VERSION constant +~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + VERSION constant ~~~~~~~~~~~~~~~~ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0cdfcc9a9..8bbc805c5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -35,9 +35,9 @@ import sys import warnings # 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. -from . import PILLOW_VERSION, ImageMode, TiffTags, __version__, _plugins +from . import ImageMode, TiffTags, __version__, _plugins from ._binary import i8, i32le from ._util import deferred_error, isPath, isStringType, py3 @@ -57,9 +57,6 @@ except ImportError: from collections import Callable, MutableMapping -# Silence warning -assert PILLOW_VERSION - logger = logging.getLogger(__name__) diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 59eccc9b5..2acf4f496 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -17,9 +17,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 From ae8fd7446f9520aed3cec75d04caa9ff94f822ce Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 30 Sep 2019 10:37:46 +0200 Subject: [PATCH 027/186] test-windows.yml raqm --- .github/workflows/test-windows.yml | 35 +++++++++++++++++++++----- winbuild/config.py | 5 ++++ winbuild/fribidi.cmake | 9 +++---- winbuild/raqm.cmake | 40 ++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 winbuild/raqm.cmake diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 4d117cc35..c3bb75e3c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -76,7 +76,6 @@ jobs: xcopy /s c:\pillow-depends\test_images\* $env:GITHUB_WORKSPACE\tests\images\ cd $env:GITHUB_WORKSPACE/winbuild/ python.exe $env:GITHUB_WORKSPACE\winbuild\build_dep.py - # .\build_deps.cmd env: EXECUTABLE: bin\python.exe shell: pwsh @@ -233,17 +232,20 @@ jobs: copy /Y /B *.lib %INCLIB% - name: Build dependencies / HarfBuzz + if: "!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 + set INCLUDE=%INCLUDE%;%INCLIB% + set LIB=%LIB%;%INCLIB% cd /D %BUILD%\harfbuzz-2.6.1 call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on echo (gc CMakeLists.txt) -replace 'enable_testing', "add_compile_options(-MT)`r`nenable_testing" ^| 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 + 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 @@ -251,6 +253,7 @@ jobs: copy /Y /B *.lib %INCLIB% - name: Build dependencies / FriBidi + if: "!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 @@ -267,8 +270,28 @@ jobs: copy /Y /B lib\*.h %INCLIB% copy /Y /B *.lib %INCLIB% + # failing with PyPy3 + - name: Build dependencies / Raqm + if: "!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 + set INCLUDE=%INCLUDE%;%INCLIB% + set LIB=%LIB%;%INCLIB% + cd /D %BUILD%\libraqm-0.7.0 + call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + 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% + - name: Build dependencies / ghostscript - if: false run: | set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 @@ -295,21 +318,21 @@ jobs: set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 %PYTHON%\python.exe setup.py build_ext install + rem Add GhostScript and Raqm binaries (copied to INCLIB) to PATH. + path %INCLIB%;%PATH% %PYTHON%\python.exe selftest.py --installed - name: Test Pillow run: | set PYTHON=%pythonLocation% set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - rem Add GhostScript executables (copied to INCLIB) to PATH. + rem Add GhostScript and Raqm binaries (copied to INCLIB) to PATH. path %INCLIB%;%PATH% cd /D %GITHUB_WORKSPACE% %PYTHON%\python.exe -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests - name: Upload coverage run: 'codecov --file "%GITHUB_WORKSPACE%\coverage.xml" --name "%pythonLocation%"' - env: - CODECOV: Scripts\codecov.exe - name: Build wheel id: wheel diff --git a/winbuild/config.py b/winbuild/config.py index 93ccf3806..0eccdc1f0 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -113,6 +113,11 @@ libs = { "filename": PILLOW_DEPENDS_DIR + "fribidi-1.0.7.zip", "dir": "fribidi-1.0.7", }, + "libraqm": { + "url": "https://github.com/HOST-Oman/libraqm/archive/v0.7.0.zip", + "filename": PILLOW_DEPENDS_DIR + "libraqm-0.7.0.zip", + "dir": "libraqm-0.7.0", + }, } compilers = { diff --git a/winbuild/fribidi.cmake b/winbuild/fribidi.cmake index 82d1d9ccb..962badd46 100644 --- a/winbuild/fribidi.cmake +++ b/winbuild/fribidi.cmake @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.13) project(fribidi) add_definitions(-D_CRT_SECURE_NO_WARNINGS) -set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -MT) +add_compile_options(-MT) include_directories(${CMAKE_CURRENT_BINARY_DIR}) include_directories(lib) @@ -29,7 +29,7 @@ function(fribidi_conf) 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 fribidi-config.h @ONLY) + configure_file(lib/fribidi-config.h.in lib/fribidi-config.h @ONLY) endfunction() fribidi_conf() @@ -57,7 +57,7 @@ macro(fribidi_definitions _TGT) endmacro() function(fribidi_gen _NAME _OUTNAME _PARAM) - set(_OUT ${CMAKE_CURRENT_BINARY_DIR}/${_OUTNAME}) + set(_OUT lib/${_OUTNAME}) prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN}) add_executable(gen-${_NAME} gen.tab/gen-${_NAME}.c @@ -80,7 +80,7 @@ fribidi_gen(unicode-version fribidi-unicode-version.h "" macro(fribidi_tab _NAME) fribidi_gen(${_NAME}-tab ${_NAME}.tab.i 2 ${ARGN}) target_sources(gen-${_NAME}-tab - PRIVATE fribidi-unicode-version.h) + PRIVATE lib/fribidi-unicode-version.h) endmacro() fribidi_tab(bidi-type unidata/UnicodeData.txt) @@ -97,7 +97,6 @@ file(GLOB FRIBIDI_HEADERS lib/*.h) add_library(fribidi STATIC ${FRIBIDI_SOURCES} ${FRIBIDI_HEADERS} - fribidi-config.h ${FRIBIDI_SOURCES_GENERATED}) fribidi_definitions(fribidi) target_compile_definitions(fribidi diff --git a/winbuild/raqm.cmake b/winbuild/raqm.cmake new file mode 100644 index 000000000..39ae915d7 --- /dev/null +++ b/winbuild/raqm.cmake @@ -0,0 +1,40 @@ +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) +add_compile_options(-MT) + + +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}) From 7fb70695bde2f1283bbce6db7eb903629926890a Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 1 Oct 2019 20:32:38 +0200 Subject: [PATCH 028/186] fix distutils not finding vcredist, use /MD --- .github/workflows/test-windows.yml | 34 ++--- winbuild/build_dep.py | 2 +- winbuild/fribidi.cmake | 1 - winbuild/raqm.cmake | 1 - winbuild/tiff-mt.opt | 216 ----------------------------- winbuild/{tiff-md.opt => tiff.opt} | 0 6 files changed, 19 insertions(+), 235 deletions(-) delete mode 100644 winbuild/tiff-mt.opt rename winbuild/{tiff-md.opt => tiff.opt} (100%) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c3bb75e3c..541fe021c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -80,10 +80,11 @@ jobs: EXECUTABLE: bin\python.exe shell: pwsh - # libjpeg-turbo build is slow, pypy test is slow -> don't do both at once - name: Build dependencies / libjpeg - if: "contains(matrix.python-version, 'pypy')" + 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 @@ -92,12 +93,12 @@ jobs: echo on nmake -nologo -f makefile.vc setup-vc6 nmake -nologo -f makefile.vc clean - nmake -nologo -f makefile.vc nodebug=1 libjpeg.lib + 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% - name: Build dependencies / libjpeg-turbo - if: "!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 @@ -106,12 +107,14 @@ jobs: call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 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 -DCMAKE_BUILD_TYPE=Release + 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 + 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 - name: Build dependencies / zlib run: | @@ -122,7 +125,7 @@ jobs: call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on nmake -nologo -f win32\Makefile.msc clean - nmake -nologo -f win32\Makefile.msc LOC=-MT zlib.lib + 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 @@ -135,7 +138,7 @@ jobs: cd /D %BUILD%\tiff-4.0.10 call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on - copy %GITHUB_WORKSPACE%\winbuild\tiff-mt.opt nmake.opt + 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% @@ -151,7 +154,7 @@ jobs: call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on rmdir /S /Q output\release-static - nmake -nologo -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output ARCH=${{ matrix.architecture }} all + 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% @@ -170,6 +173,7 @@ jobs: set DefaultPlatformToolset=v140 set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\VC\VCTargets set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\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% @@ -186,7 +190,7 @@ jobs: rmdir /S /Q Projects\VC2015\Release set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\VC\VCTargets set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" - powershell -Command "(gc Projects\VC2015\lcms2_static\lcms2_static.vcxproj) -replace 'MultiThreadedDLL', 'MultiThreaded' | Out-File -encoding ASCII Projects\VC2015\lcms2_static\lcms2_static.vcxproj" + powershell -Command "(gc Projects\VC2015\lcms2_static\lcms2_static.vcxproj) -replace 'MultiThreaded<', 'MultiThreadedDLL<' | Out-File -encoding ASCII Projects\VC2015\lcms2_static\lcms2_static.vcxproj" %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% @@ -201,7 +205,7 @@ jobs: 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% -DOPENJP2_COMPILE_OPTIONS=-MT -DCMAKE_BUILD_TYPE=Release + set CMAKE=%CMAKE% -DCMAKE_BUILD_TYPE=Release %CMAKE% -G "NMake Makefiles" . nmake -nologo -f Makefile clean nmake -nologo -f Makefile @@ -220,7 +224,7 @@ jobs: cd /D %BUILD%\libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971 call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on - echo (gc CMakeLists.txt) -replace 'add_library', "add_compile_options(-MT -openmp-)`r`nadd_library" ^| Out-File -encoding ASCII CMakeLists.txt > patch.ps1 + 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 @@ -231,6 +235,7 @@ jobs: copy /Y /B *.h %INCLIB% copy /Y /B *.lib %INCLIB% + # for Raqm - name: Build dependencies / HarfBuzz if: "!contains(matrix.python-version, 'pypy')" run: | @@ -242,8 +247,6 @@ jobs: cd /D %BUILD%\harfbuzz-2.6.1 call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on - echo (gc CMakeLists.txt) -replace 'enable_testing', "add_compile_options(-MT)`r`nenable_testing" ^| 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% -DHB_HAVE_FREETYPE:BOOL=ON -DCMAKE_BUILD_TYPE=Release %CMAKE% -G "NMake Makefiles" . @@ -252,6 +255,7 @@ jobs: copy /Y /B src\*.h %INCLIB% copy /Y /B *.lib %INCLIB% + # for Raqm - name: Build dependencies / FriBidi if: "!contains(matrix.python-version, 'pypy')" run: | @@ -313,7 +317,6 @@ jobs: set MPLSRC=%GITHUB_WORKSPACE% set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 cd /D %GITHUB_WORKSPACE% - set DISTUTILS_USE_SDK=1 set LIB=%INCLIB%;%PYTHON%\tcl set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 @@ -345,7 +348,6 @@ jobs: set MPLSRC=%GITHUB_WORKSPACE% set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 cd /D %GITHUB_WORKSPACE% - set DISTUTILS_USE_SDK=1 set LIB=%INCLIB%;%PYTHON%\tcl set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index 4e4b70dfe..d7df22584 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -202,7 +202,7 @@ setlocal + vc_setup(compiler, bit) + r""" rem do after building jpeg and zlib -copy %%~dp0\tiff-md.opt %%TIFF%%\nmake.opt +copy %%~dp0\tiff.opt %%TIFF%%\nmake.opt cd /D %%TIFF%% nmake -nologo -f makefile.vc clean diff --git a/winbuild/fribidi.cmake b/winbuild/fribidi.cmake index 962badd46..247e79e4c 100644 --- a/winbuild/fribidi.cmake +++ b/winbuild/fribidi.cmake @@ -3,7 +3,6 @@ cmake_minimum_required(VERSION 3.13) project(fribidi) add_definitions(-D_CRT_SECURE_NO_WARNINGS) -add_compile_options(-MT) include_directories(${CMAKE_CURRENT_BINARY_DIR}) include_directories(lib) diff --git a/winbuild/raqm.cmake b/winbuild/raqm.cmake index 39ae915d7..88eb7f284 100644 --- a/winbuild/raqm.cmake +++ b/winbuild/raqm.cmake @@ -8,7 +8,6 @@ find_library(harfbuzz NAMES harfbuzz) find_library(freetype NAMES freetype) add_definitions(-DFRIBIDI_ENTRY=extern) -add_compile_options(-MT) function(raqm_conf) diff --git a/winbuild/tiff-mt.opt b/winbuild/tiff-mt.opt deleted file mode 100644 index 9298f4ca7..000000000 --- a/winbuild/tiff-mt.opt +++ /dev/null @@ -1,216 +0,0 @@ -# $Id: nmake.opt,v 1.18 2006/06/07 16:33:45 dron Exp $ -# -# Copyright (C) 2004, Andrey Kiselev -# -# Permission to use, copy, modify, distribute, and sell this software and -# its documentation for any purpose is hereby granted without fee, provided -# that (i) the above copyright notices and this permission notice appear in -# all copies of the software and related documentation, and (ii) the names of -# Sam Leffler and Silicon Graphics may not be used in any advertising or -# publicity relating to the software without the specific, prior written -# permission of Sam Leffler and Silicon Graphics. -# -# THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, -# EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY -# WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. -# -# IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR -# ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, -# OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -# WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF -# LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE -# OF THIS SOFTWARE. - -# Compile time parameters for MS Visual C++ compiler. -# You may edit this file to specify building options. - -# -###### Edit the following lines to choose a feature set you need. ####### -# - -# -# Select WINMODE_CONSOLE to build a library which reports errors to stderr, or -# WINMODE_WINDOWED to build such that errors are reported via MessageBox(). -# -WINMODE_CONSOLE = 1 -#WINMODE_WINDOWED = 1 - -# -# Comment out the following lines to disable internal codecs. -# -# Support for CCITT Group 3 & 4 algorithms -CCITT_SUPPORT = 1 -# Support for Macintosh PackBits algorithm -PACKBITS_SUPPORT = 1 -# Support for LZW algorithm -LZW_SUPPORT = 1 -# Support for ThunderScan 4-bit RLE algorithm -THUNDER_SUPPORT = 1 -# Support for NeXT 2-bit RLE algorithm -NEXT_SUPPORT = 1 -# Support for LogLuv high dynamic range encoding -LOGLUV_SUPPORT = 1 - -# -# Uncomment and edit following lines to enable JPEG support. -# -JPEG_SUPPORT = 1 -JPEG_INCLUDE = -I$(INCLIB) -JPEG_LIB = $(INCLIB)/libjpeg.lib - -# -# Uncomment and edit following lines to enable ZIP support -# (required for Deflate compression and Pixar log-format) -# -ZIP_SUPPORT = 1 -ZLIB_INCLUDE = -I$(INCLIB) -ZLIB_LIB = $(INCLIB)/zlib.lib - -# -# Uncomment and edit following lines to enable ISO JBIG support -# -#JBIG_SUPPORT = 1 -#JBIGDIR = d:/projects/jbigkit -#JBIG_INCLUDE = -I$(JBIGDIR)/libjbig -#JBIG_LIB = $(JBIGDIR)/libjbig/jbig.lib - -# -# Uncomment following line to enable Pixar log-format algorithm -# (Zlib required). -# -#PIXARLOG_SUPPORT = 1 - -# -# Comment out the following lines to disable strip chopping -# (whether or not to convert single-strip uncompressed images to multiple -# strips of specified size to reduce memory usage). Default strip size -# is 8192 bytes, it can be configured via the STRIP_SIZE_DEFAULT parameter -# -STRIPCHOP_SUPPORT = 1 -STRIP_SIZE_DEFAULT = 8192 - -# -# Comment out the following lines to disable treating the fourth sample with -# no EXTRASAMPLE_ value as being ASSOCALPHA. Many packages produce RGBA -# files but don't mark the alpha properly. -# -EXTRASAMPLE_AS_ALPHA_SUPPORT = 1 - -# -# Comment out the following lines to disable picking up YCbCr subsampling -# info from the JPEG data stream to support files lacking the tag. -# See Bug 168 in Bugzilla, and JPEGFixupTestSubsampling() for details. -# -CHECK_JPEG_YCBCR_SUBSAMPLING = 1 - -# -####################### Compiler related options. ####################### -# - -# -# Pick debug or optimized build flags. We default to an optimized build -# with no debugging information. -# NOTE: /EHsc option required if you want to build the C++ stream API -# -OPTFLAGS = /Ox /MT /EHsc /W3 /D_CRT_SECURE_NO_DEPRECATE -#OPTFLAGS = /Zi - -# -# Uncomment following line to enable using Windows Common RunTime Library -# instead of Windows specific system calls. See notes on top of tif_unix.c -# module for details. -# -USE_WIN_CRT_LIB = 1 - -# Compiler specific options. You may probably want to adjust compilation -# parameters in CFLAGS variable. Refer to your compiler documentation -# for the option reference. -# -MAKE = nmake /nologo -CC = cl /nologo -CXX = cl /nologo -AR = lib /nologo -LD = link /nologo - -CFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS) -CXXFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS) -EXTRAFLAGS = -LIBS = - -# Name of the output shared library -DLLNAME = libtiff.dll - -# -########### There is nothing to edit below this line normally. ########### -# - -# Set the native cpu bit order -EXTRAFLAGS = -DFILLODER_LSB2MSB $(EXTRAFLAGS) - -!IFDEF WINMODE_WINDOWED -EXTRAFLAGS = -DTIF_PLATFORM_WINDOWED $(EXTRAFLAGS) -LIBS = user32.lib $(LIBS) -!ELSE -EXTRAFLAGS = -DTIF_PLATFORM_CONSOLE $(EXTRAFLAGS) -!ENDIF - -# Codec stuff -!IFDEF CCITT_SUPPORT -EXTRAFLAGS = -DCCITT_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF PACKBITS_SUPPORT -EXTRAFLAGS = -DPACKBITS_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF LZW_SUPPORT -EXTRAFLAGS = -DLZW_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF THUNDER_SUPPORT -EXTRAFLAGS = -DTHUNDER_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF NEXT_SUPPORT -EXTRAFLAGS = -DNEXT_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF LOGLUV_SUPPORT -EXTRAFLAGS = -DLOGLUV_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF JPEG_SUPPORT -LIBS = $(LIBS) $(JPEG_LIB) -EXTRAFLAGS = -DJPEG_SUPPORT -DOJPEG_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF ZIP_SUPPORT -LIBS = $(LIBS) $(ZLIB_LIB) -EXTRAFLAGS = -DZIP_SUPPORT $(EXTRAFLAGS) -!IFDEF PIXARLOG_SUPPORT -EXTRAFLAGS = -DPIXARLOG_SUPPORT $(EXTRAFLAGS) -!ENDIF -!ENDIF - -!IFDEF JBIG_SUPPORT -LIBS = $(LIBS) $(JBIG_LIB) -EXTRAFLAGS = -DJBIG_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF STRIPCHOP_SUPPORT -EXTRAFLAGS = -DSTRIPCHOP_DEFAULT=TIFF_STRIPCHOP -DSTRIP_SIZE_DEFAULT=$(STRIP_SIZE_DEFAULT) $(EXTRAFLAGS) -!ENDIF - -!IFDEF EXTRASAMPLE_AS_ALPHA_SUPPORT -EXTRAFLAGS = -DDEFAULT_EXTRASAMPLE_AS_ALPHA $(EXTRAFLAGS) -!ENDIF - -!IFDEF CHECK_JPEG_YCBCR_SUBSAMPLING -EXTRAFLAGS = -DCHECK_JPEG_YCBCR_SUBSAMPLING $(EXTRAFLAGS) -!ENDIF - -!IFDEF USE_WIN_CRT_LIB -EXTRAFLAGS = -DAVOID_WIN32_FILEIO $(EXTRAFLAGS) -!ELSE -EXTRAFLAGS = -DUSE_WIN32_FILEIO $(EXTRAFLAGS) -!ENDIF diff --git a/winbuild/tiff-md.opt b/winbuild/tiff.opt similarity index 100% rename from winbuild/tiff-md.opt rename to winbuild/tiff.opt From 94a64ea09c8e21f7f2e19daac0eab00a0c5bf6e8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Oct 2019 06:42:14 +1000 Subject: [PATCH 029/186] 7.0.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index c927be6ec..eddf15683 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "6.2.0" +__version__ = "7.0.0.dev0" From e487ed7fef099c8338b4d169efa0c7f8ba309925 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Oct 2019 22:01:31 +1000 Subject: [PATCH 030/186] Removed deprecation test --- Tests/test_imageqt.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index a680b916f..9248291c3 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 @@ -75,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, []) From 59153f5e125bf677b257b0310e710583a62506cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Oct 2019 19:37:00 +1000 Subject: [PATCH 031/186] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d9e3bac80..1af7173e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +7.0.0 (unreleased) +------------------ + +- Changed default frombuffer raw decoder args #1730 + [radarhere] + 6.2.0 (2019-10-01) ------------------ From 1e6eac40fc8dc680f2e3eb45f289aacea2f68f12 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Oct 2019 20:23:49 +1000 Subject: [PATCH 032/186] Changed condition to use DEBUG as a boolean --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index a927cd3ed..e03d5b105 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -854,7 +854,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) From 6238f07d8c7256a20b234a3e82c2ee7e9f9bae5f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Oct 2019 20:53:54 +1000 Subject: [PATCH 033/186] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1af7173e9..fbbd6ba96 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 7.0.0 (unreleased) ------------------ +- Removed deprecated PILLOW_VERSION #4107 + [hugovk] + - Changed default frombuffer raw decoder args #1730 [radarhere] From 3cd7d9e4a2d474c4b8bd8c2e9b4bd2418298e002 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Oct 2019 20:54:55 +1000 Subject: [PATCH 034/186] Removed outdated VERSION comment --- src/PIL/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 2acf4f496..fd02b40d9 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. ;-) """ From d6ae0a99a73c8d12a45e9f96023c7d57f3bc6607 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Oct 2019 22:12:28 +1000 Subject: [PATCH 035/186] Removed deprecated setting of TIFF image sizes --- Tests/test_file_tiff.py | 9 --------- docs/deprecations.rst | 21 ++++++++------------- src/PIL/TiffImagePlugin.py | 13 ------------- 3 files changed, 8 insertions(+), 35 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 2d15de2bd..048539f86 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -73,15 +73,6 @@ 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) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 2ce2b5fa5..95c5cd3b1 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -81,19 +81,6 @@ Deprecated Deprecated Deprecated ``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` =============================== ================================= ================================== -Setting the size of TIFF images -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.3.0 - -Setting the image size of a TIFF image (eg. ``im.size = (256, 256)``) issues -a ``DeprecationWarning``: - -.. code-block:: none - - Setting the size of a TIFF image directly is deprecated, and will - be removed in a future version. Use the resize method instead. - ImageCms.CmsProfile attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -127,6 +114,14 @@ PILLOW_VERSION constant ``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. +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/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index a927cd3ed..b901bd06d 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1079,19 +1079,6 @@ 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() From ecb3a30487c41c3ad4d4e9f8ed7c30c38207ff78 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 6 Oct 2019 13:52:58 +1100 Subject: [PATCH 036/186] Clarified documentation [ci skip] --- docs/handbook/image-file-formats.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 3ce4ccb2b..088fd5a03 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -113,8 +113,8 @@ to seek to the next frame (``im.seek(im.tell() + 1)``). Saving ~~~~~~ -When calling :py:meth:`~PIL.Image.Image.save`, the following options -are available:: +When calling :py:meth:`~PIL.Image.Image.save` to write a GIF file, the +following options are available:: im.save(out, save_all=True, append_images=[im1, im2, ...]) @@ -813,8 +813,9 @@ Saving sequences library is v0.5.0 or later. You can check webp animation support at runtime by calling ``features.check("webp_anim")``. -When calling :py:meth:`~PIL.Image.Image.save`, the following options -are available when the ``save_all`` argument is present and true. +When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, the +following options are available when the ``save_all`` argument is present and +true. **append_images** A list of images to append as additional frames. Each of the From e0d67a1f9f34b113b5d9858d274f06a5e928df2d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 6 Oct 2019 14:39:10 +1100 Subject: [PATCH 037/186] Use double backticks [ci skip] --- docs/releasenotes/6.2.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 0c3ce77e2..9576e6be8 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -47,7 +47,7 @@ creates the following image: ImageGrab on multi-monitor Windows ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -An `all_screens` argument has been added to ``ImageGrab.grab``. If ``True``, +An ``all_screens`` argument has been added to ``ImageGrab.grab``. If ``True``, all monitors will be included in the created image. API Changes From 6fe2df01cd00c293c05a388e72d6f49df7da0ddf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 6 Oct 2019 15:38:56 +1100 Subject: [PATCH 038/186] Do not install qt4 --- .travis/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/install.sh b/.travis/install.sh index 725880934..e37ec8a0f 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 python-tk\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ cmake imagemagick libharfbuzz-dev libfribidi-dev From c50a309a104325ce397808f3ea761c43cfb8c7f2 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 6 Oct 2019 10:24:48 -0700 Subject: [PATCH 039/186] Remove duplicate cleanup in test_decompression_bomb.py The same cleanup is done in the teardDown() method. There is no need to do it a 2nd time. --- Tests/test_decompression_bomb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 7c18f85d2..3afad674f 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -14,7 +14,6 @@ class TestDecompressionBomb(PillowTestCase): def test_no_warning_small_file(self): # Implicit assert: no warning. # A warning would cause a failure. - Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT Image.open(TEST_FILE) def test_no_warning_no_limit(self): From c3822de70db1a92c0ffab63cfe42e21b0b79bd50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Oct 2019 12:41:33 +1100 Subject: [PATCH 040/186] Test Qt5 --- .travis.yml | 4 ++++ .travis/install.sh | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 921fffd44..286f828f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,16 +22,20 @@ matrix: name: "PyPy3 Xenial" - python: '3.7' name: "3.7 Xenial" + services: xvfb - python: '2.7' name: "2.7 Xenial" - python: '3.6' name: "3.6 Xenial PYTHONOPTIMIZE=1" env: PYTHONOPTIMIZE=1 + services: xvfb - python: '3.5' name: "3.5 Xenial PYTHONOPTIMIZE=2" env: PYTHONOPTIMIZE=2 + services: xvfb - 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" diff --git a/.travis/install.sh b/.travis/install.sh index e37ec8a0f..6ad89402e 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -15,6 +15,10 @@ 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 From bbfa8bc1ca3ab76b1bfdbada0defee9a5ee61c41 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Oct 2019 13:07:36 +1100 Subject: [PATCH 041/186] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fbbd6ba96..3d532f5e8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 7.0.0 (unreleased) ------------------ +- 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] From 922f55c265773e631399233ff32aa13fa9d330a6 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 6 Oct 2019 19:26:55 -0700 Subject: [PATCH 042/186] Use bytes literals instead of bytes(str) Bytes literals are available on all supported Python versions. Rather than convert strings literals to bytes at runtime, simply use a bytes literal. --- Tests/check_icns_dos.py | 6 +----- Tests/check_j2k_dos.py | 16 +--------------- 2 files changed, 2 insertions(+), 20 deletions(-) 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_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")) From af770a6c555013c87d7c208fb610886fef59d5b8 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 26 Sep 2019 15:12:28 +0300 Subject: [PATCH 043/186] Drop support for EOL Python 2.7 --- .appveyor.yml | 11 --- .github/CONTRIBUTING.md | 4 +- .travis.yml | 8 +- .travis/after_success.sh | 10 +-- .travis/install.sh | 4 +- .travis/script.sh | 2 +- Makefile | 36 ++++---- Tests/helper.py | 6 -- Tests/test_file_iptc.py | 9 +- Tests/test_file_libtiff.py | 17 +--- Tests/test_file_png.py | 12 ++- Tests/test_file_tiff.py | 17 +--- Tests/test_font_pcf.py | 6 +- Tests/test_format_hsv.py | 24 ++---- Tests/test_image.py | 11 +-- Tests/test_image_access.py | 6 -- Tests/test_image_getim.py | 6 +- Tests/test_imagepath.py | 6 +- Tests/test_imagetk.py | 7 +- Tests/test_psdraw.py | 5 +- mp_compile.py | 93 --------------------- setup.py | 15 +--- src/PIL/EpsImagePlugin.py | 14 +--- src/PIL/Image.py | 58 +++---------- src/PIL/ImageFont.py | 7 +- src/PIL/ImageMath.py | 22 +---- src/PIL/ImageQt.py | 7 +- src/PIL/ImageShow.py | 6 +- src/PIL/ImageTk.py | 8 +- src/PIL/PSDraw.py | 3 +- src/PIL/PdfParser.py | 50 +++-------- src/PIL/PngImagePlugin.py | 26 +++--- src/PIL/SgiImagePlugin.py | 4 +- src/PIL/TarIO.py | 7 +- src/PIL/TiffImagePlugin.py | 37 ++------- src/PIL/WalImageFile.py | 9 +- src/PIL/WmfImagePlugin.py | 4 - src/PIL/_binary.py | 19 +---- src/PIL/_tkinter_finder.py | 6 +- src/PIL/_util.py | 23 ++---- src/Tk/tkImaging.c | 8 -- src/_imaging.c | 105 ++++++------------------ src/_imagingcms.c | 34 ++------ src/_imagingft.c | 103 ++++------------------- src/_imagingmath.c | 13 +-- src/_imagingmorph.c | 11 --- src/_imagingtk.c | 10 --- src/_webp.c | 16 +--- src/decode.c | 3 +- src/display.c | 13 +-- src/encode.c | 43 +++++----- src/libImaging/codec_fd.c | 3 +- src/map.c | 2 - src/path.c | 19 ++--- src/py3.h | 56 ------------- tox.ini | 2 +- winbuild/appveyor_install_msys2_deps.sh | 14 ++-- winbuild/appveyor_install_pypy2.cmd | 3 - winbuild/build.py | 5 +- winbuild/config.py | 4 +- winbuild/get_pythons.py | 2 +- 61 files changed, 217 insertions(+), 877 deletions(-) delete mode 100644 mp_compile.py delete mode 100644 src/py3.h delete mode 100644 winbuild/appveyor_install_pypy2.cmd diff --git a/.appveyor.yml b/.appveyor.yml index f299794b6..27c7d7597 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,13 +13,7 @@ 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 @@ -44,11 +38,6 @@ install: - xcopy c:\pillow-depends\*.tar.gz c:\pillow\winbuild\ - xcopy /s c:\pillow-depends\test_images\* c:\pillow\tests\images - cd c:\pillow\winbuild\ -- ps: | - if ($env:PYTHON -eq "c:/vp/pypy2") - { - c:\pillow\winbuild\appveyor_install_pypy2.cmd - } - ps: | if ($env:PYTHON -eq "c:/vp/pypy3") { 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/.travis.yml b/.travis.yml index 286f828f2..232688971 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,15 +16,11 @@ matrix: - python: "3.6" name: "Lint" env: LINT="true" - - python: "pypy" - name: "PyPy2 Xenial" - python: "pypy3" name: "PyPy3 Xenial" - python: '3.7' name: "3.7 Xenial" services: xvfb - - python: '2.7' - name: "2.7 Xenial" - python: '3.6' name: "3.6 Xenial PYTHONOPTIMIZE=1" env: PYTHONOPTIMIZE=1 diff --git a/.travis/after_success.sh b/.travis/after_success.sh index 1dca2ccb9..721a469b6 100755 --- a/.travis/after_success.sh +++ b/.travis/after_success.sh @@ -11,16 +11,12 @@ 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 +pip install coveralls-merge +coveralls-merge coverage.c.json codecov -if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ] && [ "$DOCKER" == "" ]; then +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/install.sh b/.travis/install.sh index 6ad89402e..747acb448 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -20,8 +20,8 @@ if [[ $TRAVIS_PYTHON_VERSION == 3.* ]]; then 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.7 +if [ "$TRAVIS_PYTHON_VERSION" == "3.7" ]; then pip install -r requirements.txt ; fi # webp pushd depends && ./install_webp.sh && popd diff --git a/.travis/script.sh b/.travis/script.sh index af56cc6ab..c2b903601 100755 --- a/.travis/script.sh +++ b/.travis/script.sh @@ -9,4 +9,4 @@ 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 +if [ "$TRAVIS_PYTHON_VERSION" == "3.7" ]; then make doccheck; fi 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/Tests/helper.py b/Tests/helper.py index 78a2f520f..87f034449 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -10,7 +10,6 @@ import tempfile import unittest from PIL import Image, ImageMath -from PIL._util import py3 logger = logging.getLogger(__name__) @@ -277,11 +276,6 @@ 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 diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 800563af1..c1c6ecbad 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 @@ -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_libtiff.py b/Tests/test_file_libtiff.py index ea73a7ad5..c1cce1936 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -9,7 +9,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 @@ -361,12 +360,8 @@ class TestFileLibTiff(LibTiffTestCase): 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")) out = self.tempfile("temp.tif") # out = "temp.le.tif" @@ -387,12 +382,8 @@ class TestFileLibTiff(LibTiffTestCase): 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")) out = self.tempfile("temp.tif") im.save(out) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 6d76a6caa..3f5f003ac 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -3,7 +3,6 @@ import zlib from io import BytesIO from PIL import Image, ImageFile, PngImagePlugin -from PIL._util import py3 from .helper import PillowLeakTestCase, PillowTestCase, hopper, unittest @@ -460,12 +459,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: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 048539f86..9919bacf8 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -3,7 +3,6 @@ import sys from io import BytesIO from PIL import Image, TiffImagePlugin -from PIL._util import py3 from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from .helper import PillowTestCase, hopper, unittest @@ -176,12 +175,8 @@ class TestFileTiff(PillowTestCase): 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") @@ -191,12 +186,8 @@ class TestFileTiff(PillowTestCase): 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") 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_image.py b/Tests/test_image.py index 47196a139..53f0199de 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -3,7 +3,6 @@ import shutil import sys from PIL import Image -from PIL._util import py3 from .helper import PillowTestCase, hopper, unittest @@ -80,20 +79,14 @@ 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("") + im = io.BytesIO(b"") self.assertRaises(IOError, 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_pathlib(self): from PIL.Image import Path diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index b06814cb9..4004ca5df 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -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_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_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_imagetk.py b/Tests/test_imagetk.py index c397c84be..808ebd392 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,15 +1,12 @@ from PIL import Image -from PIL._util import py3 from .helper import PillowTestCase, hopper, unittest 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): diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 561df4ee6..0ef9acb06 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -1,5 +1,6 @@ import os import sys +from io import StringIO from PIL import Image, PSDraw @@ -47,10 +48,6 @@ class TestPsDraw(PillowTestCase): def test_stdout(self): # Temporarily redirect stdout - try: - from cStringIO import StringIO - except ImportError: - from io import StringIO old_stdout = sys.stdout sys.stdout = mystdout = StringIO() 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/setup.py b/setup.py index 76bdfb159..547034f50 100755 --- a/setup.py +++ b/setup.py @@ -20,10 +20,6 @@ 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): warnings.warn( "Pillow does not yet support Python {}.{} and does not yet provide " @@ -332,12 +328,6 @@ 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 for x in self.feature: if getattr(self, "disable_%s" % x): setattr(self.feature, x, False) @@ -861,12 +851,11 @@ try: 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 :: Only", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Multimedia :: Graphics", @@ -875,7 +864,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/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 219f2dbb8..6d8828951 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -42,15 +42,8 @@ gs_windows_binary = None if sys.platform.startswith("win"): import shutil - if hasattr(shutil, "which"): - which = shutil.which - else: - # Python 2 - import distutils.spawn - - which = distutils.spawn.find_executable for binary in ("gswin32c", "gswin64c", "gs"): - if which(binary) is not None: + if shutil.which(binary) is not None: gs_windows_binary = binary break else: @@ -378,9 +371,8 @@ def _save(im, fp, filename, eps=1): base_fp = fp wrapped_fp = False if fp != sys.stdout: - if sys.version_info.major > 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/Image.py b/src/PIL/Image.py index cca9c8a91..c73cb5eb8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -25,6 +25,7 @@ # import atexit +import builtins import io import logging import math @@ -33,29 +34,15 @@ import os import struct import sys import warnings +from collections.abc import Callable, MutableMapping +from pathlib import Path # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 7.0.0. # Use __version__ instead. from . import ImageMode, TiffTags, __version__, _plugins from ._binary import i8, i32le -from ._util import deferred_error, isPath, isStringType, py3 - -try: - import builtins -except ImportError: - import __builtin__ - - builtins = __builtin__ - - -try: - # Python 3 - from collections.abc import Callable, MutableMapping -except ImportError: - # Python 2.7 - from collections import Callable, MutableMapping - +from ._util import deferred_error, isPath, isStringType logger = logging.getLogger(__name__) @@ -134,18 +121,6 @@ try: except ImportError: cffi = None -try: - from pathlib import Path - - HAS_PATHLIB = True -except ImportError: - try: - from pathlib2 import Path - - HAS_PATHLIB = True - except ImportError: - HAS_PATHLIB = False - def isImageType(t): """ @@ -621,10 +596,8 @@ class Image(object): # object is gone. self.im = deferred_error(ValueError("Operation on closed image")) - if sys.version_info.major >= 3: - - def __del__(self): - self.__exit__() + def __del__(self): + self.__exit__() def _copy(self): self.load() @@ -1347,10 +1320,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 @@ -1701,10 +1671,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 @@ -2036,7 +2003,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): @@ -2747,7 +2714,7 @@ def open(fp, mode="r"): exclusive_fp = False filename = "" - if HAS_PATHLIB and isinstance(fp, Path): + if isinstance(fp, Path): filename = str(fp.resolve()) elif isPath(fp): filename = fp @@ -3311,11 +3278,6 @@ class Exif(MutableMapping): def __contains__(self, tag): return tag in self._data or (self._info is not None and tag in self._info) - if not py3: - - def has_key(self, tag): - return tag in self - def __setitem__(self, tag, value): if self._info is not None and tag in self._info: del self._info[tag] diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 5cce9af8e..1453a290c 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -29,7 +29,7 @@ import os import sys from . import Image -from ._util import isDirectory, isPath, py3 +from ._util import isDirectory, isPath LAYOUT_BASIC = 0 LAYOUT_RAQM = 1 @@ -695,10 +695,7 @@ 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: diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 392151c10..d7598b932 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 @@ -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/ImageQt.py b/src/PIL/ImageQt.py index da60cacd0..ce34b0a04 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -20,7 +20,7 @@ import sys from io import BytesIO from . import Image -from ._util import isPath, py3 +from ._util import isPath qt_versions = [["5", "PyQt5"], ["side2", "PySide2"]] @@ -125,10 +125,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) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index ca622c525..6d3ca52d8 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -18,14 +18,10 @@ import os 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 = [] diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index fd480007a..769fae662 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 diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index f37701ce9..baad510e1 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -18,7 +18,6 @@ import sys from . import EpsImagePlugin -from ._util import py3 ## # Simple Postscript graphics interface. @@ -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")) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 0ec6bba14..14079f6b1 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -7,24 +7,14 @@ 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 @@ -81,10 +71,8 @@ PDFDocEncoding = { 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): @@ -252,16 +240,10 @@ class PdfName: 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__ @@ -324,23 +306,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 +354,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 diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index be237b3ee..c3a5dd627 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -38,7 +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. @@ -450,9 +449,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 +485,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 +521,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)) diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 99408fdc3..7de62c117 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -27,7 +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. @@ -173,8 +172,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/TarIO.py b/src/PIL/TarIO.py index e180b802c..457d75d03 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -15,7 +15,6 @@ # import io -import sys from . import ContainerIO @@ -64,10 +63,8 @@ class TarIO(ContainerIO.ContainerIO): def __exit__(self, *args): self.close() - if sys.version_info.major >= 3: - - def __del__(self): - self.close() + def __del__(self): + self.close() def close(self): self.fh.close() diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index b0d465fe2..6d56df217 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -46,24 +46,15 @@ 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" @@ -531,18 +522,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 @@ -562,14 +546,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 ] @@ -689,8 +669,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) @@ -1132,7 +1110,7 @@ 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() @@ -1602,13 +1580,10 @@ def _save(im, fp, filename): 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) diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index e2e1cd4f5..daa806a8b 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -21,16 +21,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/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 416af6fd7..be07a747b 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -23,7 +23,6 @@ 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. @@ -31,9 +30,6 @@ __version__ = "0.2" _handler = None -if py3: - long = int - def register_handler(handler): """ 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..127e94d82 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,33 +1,24 @@ 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) +def isStringType(t): + return isinstance(t, str) - if py36: - from pathlib import Path - def isPath(f): - return isinstance(f, (bytes, str, Path)) +if py36: + from pathlib import 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. 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..a12fdfb15 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -84,8 +84,6 @@ #include "Imaging.h" -#include "py3.h" - #define _USE_MATH_DEFINES #include @@ -237,45 +235,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 +382,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 +465,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 +475,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 +509,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 +1087,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 +1213,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 +1482,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 +1593,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 +1656,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) { @@ -2136,7 +2094,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 +2372,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 +2606,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* @@ -3356,13 +3314,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 +3533,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 +3571,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 +3580,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 +3589,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* @@ -3959,7 +3917,6 @@ setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imaging(void) { PyObject* m; @@ -3979,11 +3936,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 7776e43f1..fb6a46d17 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -30,7 +30,6 @@ #include FT_SFNT_NAMES_H #define KEEP_PY_UNICODE -#include "py3.h" #if !defined(_MSC_VER) #include @@ -266,8 +265,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) return NULL; } - if (!PyArg_ParseTupleAndKeywords(args, kw, "etn|ns"PY_ARG_BYTES_LENGTH"n", - kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kw, "etn|nsy#n", kwlist, Py_FileSystemDefaultEncoding, &filename, &size, &index, &encoding, &font_bytes, &font_bytes_size, &layout_engine)) { @@ -328,7 +326,7 @@ 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 (defined(PYPY_VERSION_NUM)) if (PyUnicode_Check(string)) { Py_UNICODE* p = PyUnicode_AS_UNICODE(string); int size = PyUnicode_GET_SIZE(string); @@ -337,16 +335,6 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out) *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)) @@ -375,7 +363,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)) if (PyUnicode_Check(string)) { Py_UNICODE *text = PyUnicode_AS_UNICODE(string); Py_ssize_t size = PyUnicode_GET_SIZE(string); @@ -395,25 +383,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); @@ -479,11 +448,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * Py_ssize_t size = 0; PyObject *bytes; -#if PY_VERSION_HEX >= 0x03000000 if (!PyUnicode_Check(item)) { -#else - if (!PyUnicode_Check(item) && !PyString_Check(item)) { -#endif PyErr_SetString(PyExc_TypeError, "expected a string"); goto failed; } @@ -495,12 +460,6 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * feature = PyBytes_AS_STRING(bytes); size = PyBytes_GET_SIZE(bytes); } -#if PY_VERSION_HEX < 0x03000000 - else { - feature = PyString_AsString(item); - size = PyString_GET_SIZE(item); - } -#endif if (!(*p_raqm.add_font_feature)(rq, feature, size)) { PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); goto failed; @@ -581,11 +540,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; } @@ -990,8 +945,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; } @@ -1025,11 +979,11 @@ font_render(FontObject* self, PyObject* args) list_axis = PyDict_New(); PyDict_SetItemString(list_axis, "minimum", - PyInt_FromLong(axis.minimum / 65536)); + PyLong_FromLong(axis.minimum / 65536)); PyDict_SetItemString(list_axis, "default", - PyInt_FromLong(axis.def / 65536)); + PyLong_FromLong(axis.def / 65536)); PyDict_SetItemString(list_axis, "maximum", - PyInt_FromLong(axis.maximum / 65536)); + PyLong_FromLong(axis.maximum / 65536)); for (j = 0; j < name_count; j++) { error = FT_Get_Sfnt_Name(self->face, j, &name); @@ -1037,8 +991,7 @@ font_render(FontObject* self, PyObject* args) return geterror(error); if (name.name_id == axis.strid) { - axis_name = Py_BuildValue(PY_ARG_BYTES_LENGTH, - name.string, name.string_len); + axis_name = Py_BuildValue("y#", name.string, name.string_len); PyDict_SetItemString(list_axis, "name", axis_name); break; } @@ -1095,8 +1048,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 { @@ -1146,64 +1099,54 @@ static PyMethodDef font_methods[] = { static PyObject* font_getattr_family(FontObject* self, void* closure) { -#if PY_VERSION_HEX >= 0x03000000 if (self->face->family_name) return PyUnicode_FromString(self->face->family_name); -#else - if (self->face->family_name) - return PyString_FromString(self->face->family_name); -#endif Py_RETURN_NONE; } static PyObject* font_getattr_style(FontObject* self, void* closure) { -#if PY_VERSION_HEX >= 0x03000000 if (self->face->style_name) return PyUnicode_FromString(self->face->style_name); -#else - if (self->face->style_name) - return PyString_FromString(self->face->style_name); -#endif Py_RETURN_NONE; } static PyObject* font_getattr_ascent(FontObject* self, void* closure) { - return PyInt_FromLong(PIXEL(self->face->size->metrics.ascender)); + return PyLong_FromLong(PIXEL(self->face->size->metrics.ascender)); } static PyObject* font_getattr_descent(FontObject* self, void* closure) { - return PyInt_FromLong(-PIXEL(self->face->size->metrics.descender)); + return PyLong_FromLong(-PIXEL(self->face->size->metrics.descender)); } static PyObject* font_getattr_height(FontObject* self, void* closure) { - return PyInt_FromLong(PIXEL(self->face->size->metrics.height)); + return PyLong_FromLong(PIXEL(self->face->size->metrics.height)); } static PyObject* font_getattr_x_ppem(FontObject* self, void* closure) { - return PyInt_FromLong(self->face->size->metrics.x_ppem); + return PyLong_FromLong(self->face->size->metrics.x_ppem); } static PyObject* font_getattr_y_ppem(FontObject* self, void* closure) { - return PyInt_FromLong(self->face->size->metrics.y_ppem); + return PyLong_FromLong(self->face->size->metrics.y_ppem); } static PyObject* font_getattr_glyphs(FontObject* self, void* closure) { - return PyInt_FromLong(self->face->num_glyphs); + return PyLong_FromLong(self->face->num_glyphs); } static struct PyGetSetDef font_getsetters[] = { @@ -1271,11 +1214,7 @@ setup_module(PyObject* m) { FT_Library_Version(library, &major, &minor, &patch); -#if PY_VERSION_HEX >= 0x03000000 v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); -#else - v = PyString_FromFormat("%d.%d.%d", major, minor, patch); -#endif PyDict_SetItemString(d, "freetype2_version", v); @@ -1286,7 +1225,6 @@ setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingft(void) { PyObject* m; @@ -1306,12 +1244,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 efabf1c86..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 @@ -741,7 +730,7 @@ PyImaging_DrawWmf(PyObject* self, PyObject* args) int datasize; int width, height; int x0, y0, x1, y1; - if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH"(ii)(iiii):_load", &data, &datasize, + if (!PyArg_ParseTuple(args, "y#(ii)(iiii):_load", &data, &datasize, &width, &height, &x0, &x1, &y0, &y1)) return NULL; diff --git a/src/encode.c b/src/encode.c index 7dc1035c4..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; 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..11bcec500 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ [tox] envlist = lint - py{27,35,36,37} + py{35,36,37} minversion = 1.9 [testenv] 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_pypy2.cmd b/winbuild/appveyor_install_pypy2.cmd deleted file mode 100644 index fc56d0e56..000000000 --- a/winbuild/appveyor_install_pypy2.cmd +++ /dev/null @@ -1,3 +0,0 @@ -curl -fsSL -o pypy2.zip https://bitbucket.org/pypy/pypy/downloads/pypy2.7-v7.1.1-win32.zip -7z x pypy2.zip -oc:\ -c:\Python37\Scripts\virtualenv.exe -p c:\pypy2.7-v7.1.1-win32\pypy.exe c:\vp\pypy2 diff --git a/winbuild/build.py b/winbuild/build.py index 0617022dc..f4561d933 100755 --- a/winbuild/build.py +++ b/winbuild/build.py @@ -105,10 +105,7 @@ def build_one(py_ver, compiler, bit): 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" diff --git a/winbuild/config.py b/winbuild/config.py index 16a1d9cad..46c9f9b2b 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -4,8 +4,6 @@ SF_MIRROR = "http://iweb.dl.sourceforge.net" PILLOW_DEPENDS_DIR = "C:\\pillow-depends\\" pythons = { - "27": {"compiler": 7, "vc": 2010}, - "pypy2": {"compiler": 7, "vc": 2010}, "35": {"compiler": 7.1, "vc": 2015}, "36": {"compiler": 7.1, "vc": 2015}, "pypy3": {"compiler": 7.1, "vc": 2015}, @@ -131,7 +129,7 @@ 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 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( From 538d9e2e5d252bea23e12da08495945d776aed78 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 30 Sep 2019 17:56:31 +0300 Subject: [PATCH 044/186] Upgrade Python syntax with pyupgrade --py3-plus --- Tests/check_imaging_leaks.py | 3 -- Tests/createfontdatachunk.py | 2 - Tests/helper.py | 30 ++++++----- Tests/import_all.py | 2 - Tests/make_hash.py | 2 - Tests/test_bmp_reference.py | 4 +- Tests/test_color_lut.py | 2 - Tests/test_core_resources.py | 2 - Tests/test_features.py | 2 - Tests/test_file_eps.py | 2 +- Tests/test_file_gif.py | 3 +- Tests/test_file_libtiff.py | 4 +- Tests/test_file_pdf.py | 7 ++- Tests/test_file_tiff.py | 3 +- Tests/test_file_tiff_metadata.py | 4 +- Tests/test_file_webp_animated.py | 3 +- Tests/test_file_webp_metadata.py | 6 +-- Tests/test_font_leaks.py | 2 - Tests/test_image.py | 6 +-- Tests/test_image_access.py | 4 +- Tests/test_image_array.py | 2 +- Tests/test_image_resample.py | 2 - Tests/test_imageenhance.py | 2 +- Tests/test_imagefont.py | 7 ++- Tests/test_imagefontctl.py | 1 - Tests/test_imagemath.py | 4 +- Tests/test_imageops.py | 2 +- Tests/test_imageqt.py | 2 +- Tests/test_locale.py | 2 - Tests/test_main.py | 2 - Tests/test_mode_i16.py | 6 ++- Tests/test_numpy.py | 2 - Tests/test_pdfparser.py | 2 +- Tests/test_qt_image_toqimage.py | 2 +- Tests/threaded_save.py | 2 - Tests/versions.py | 2 - docs/conf.py | 15 +++--- docs/example/DdsImagePlugin.py | 8 +-- selftest.py | 1 - setup.py | 12 ++--- src/PIL/BdfFontFile.py | 1 - src/PIL/BlpImagePlugin.py | 2 +- src/PIL/BmpImagePlugin.py | 16 +++--- src/PIL/BufrStubImagePlugin.py | 2 +- src/PIL/ContainerIO.py | 2 +- src/PIL/CurImagePlugin.py | 3 -- src/PIL/DdsImagePlugin.py | 4 +- src/PIL/EpsImagePlugin.py | 8 +-- src/PIL/FitsStubImagePlugin.py | 2 +- src/PIL/FontFile.py | 3 +- src/PIL/FpxImagePlugin.py | 9 ++-- src/PIL/GdImageFile.py | 2 +- src/PIL/GifImagePlugin.py | 2 +- src/PIL/GimpGradientFile.py | 4 +- src/PIL/GimpPaletteFile.py | 2 +- src/PIL/GribStubImagePlugin.py | 2 +- src/PIL/Hdf5StubImagePlugin.py | 2 +- src/PIL/IcnsImagePlugin.py | 2 +- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/Image.py | 16 +++--- src/PIL/ImageCms.py | 30 ++++++----- src/PIL/ImageDraw.py | 2 +- src/PIL/ImageDraw2.py | 8 +-- src/PIL/ImageEnhance.py | 2 +- src/PIL/ImageFile.py | 28 +++++------ src/PIL/ImageFilter.py | 5 +- src/PIL/ImageFont.py | 16 +++--- src/PIL/ImageMath.py | 2 +- src/PIL/ImageMode.py | 2 +- src/PIL/ImageMorph.py | 6 +-- src/PIL/ImageOps.py | 2 +- src/PIL/ImagePalette.py | 4 +- src/PIL/ImageSequence.py | 2 +- src/PIL/ImageShow.py | 13 ++--- src/PIL/ImageStat.py | 2 +- src/PIL/ImageTk.py | 6 +-- src/PIL/ImageWin.py | 8 +-- src/PIL/IptcImagePlugin.py | 9 ++-- src/PIL/JpegImagePlugin.py | 5 +- src/PIL/MicImagePlugin.py | 2 +- src/PIL/MpegImagePlugin.py | 2 +- src/PIL/MspImagePlugin.py | 8 +-- src/PIL/PSDraw.py | 8 +-- src/PIL/PaletteFile.py | 2 +- src/PIL/PalmImagePlugin.py | 4 +- src/PIL/PcfFontFile.py | 2 +- src/PIL/PcxImagePlugin.py | 2 +- src/PIL/PdfParser.py | 86 ++++++++++++++++---------------- src/PIL/PngImagePlugin.py | 14 +++--- src/PIL/PpmImagePlugin.py | 2 +- src/PIL/PsdImagePlugin.py | 2 +- src/PIL/PyAccess.py | 2 +- src/PIL/SgiImagePlugin.py | 4 +- src/PIL/SpiderImagePlugin.py | 5 +- src/PIL/TarIO.py | 4 +- src/PIL/TgaImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 35 ++++++------- src/PIL/TiffTags.py | 2 +- src/PIL/WalImageFile.py | 1 - src/PIL/WebPImagePlugin.py | 12 ++--- src/PIL/WmfImagePlugin.py | 6 +-- src/PIL/XbmImagePlugin.py | 2 +- src/PIL/_util.py | 2 +- src/PIL/features.py | 2 - winbuild/build.py | 10 ++-- winbuild/build_dep.py | 2 +- winbuild/config.py | 2 +- winbuild/test.py | 8 +-- 108 files changed, 287 insertions(+), 357 deletions(-) diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 2b9a9605b..35157d1c7 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,7 +1,4 @@ #!/usr/bin/env python - -from __future__ import division - import sys from PIL import Image 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 87f034449..282482940 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -1,7 +1,6 @@ """ Helper functions. """ -from __future__ import print_function import logging import os @@ -77,10 +76,13 @@ class PillowTestCase(unittest.TestCase): 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) @@ -88,20 +90,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: @@ -122,10 +128,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) @@ -228,12 +234,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") @@ -371,7 +377,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/import_all.py b/Tests/import_all.py index 4dfacb291..33b07f9a2 100644 --- a/Tests/import_all.py +++ b/Tests/import_all.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import glob import os import sys diff --git a/Tests/make_hash.py b/Tests/make_hash.py index bacb391fa..7199f8c7f 100644 --- a/Tests/make_hash.py +++ b/Tests/make_hash.py @@ -1,7 +1,5 @@ # brute-force search for access descriptor hash table -from __future__ import print_function - modes = [ "1", "L", diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index e6a75e2c3..2038ac540 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 @@ -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..301a99a3f 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,5 +1,3 @@ -from __future__ import division - from array import array from PIL import Image, ImageFilter diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index eefb1a0ef..8f3cf7105 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,5 +1,3 @@ -from __future__ import division, print_function - import sys from PIL import Image diff --git a/Tests/test_features.py b/Tests/test_features.py index 64b0302ca..b8e02edea 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import io from PIL import features diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 3459310df..08d58b3ba 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -92,7 +92,7 @@ class TestFileEps(PillowTestCase): def test_iobase_object(self): # issue 479 image1 = Image.open(file1) - with io.open(self.tempfile("temp_iobase.eps"), "wb") as fh: + with open(self.tempfile("temp_iobase.eps"), "wb") as fh: image1.save(fh, "EPS") @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 4ff9727e1..8be614412 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -610,8 +610,7 @@ class TestFileGif(PillowTestCase): # 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)) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index c1cce1936..881d9096c 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import distutils.version import io import itertools @@ -262,7 +260,7 @@ 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("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), diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 25c2f6bf6..0158807f7 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -99,8 +99,7 @@ class TestFilePdf(PillowTestCase): # Test appending using a generator def imGenerator(ims): - for im in ims: - yield im + yield from ims im.save(outfile, save_all=True, append_images=imGenerator(ims)) @@ -207,7 +206,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 +234,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_tiff.py b/Tests/test_file_tiff.py index 9919bacf8..d3e39e8c6 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -518,8 +518,7 @@ class TestFileTiff(PillowTestCase): # Test appending using a generator def imGenerator(ims): - for im in ims: - yield im + yield from ims mp = io.BytesIO() im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims)) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 170cac71e..c30d055ae 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -157,13 +157,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(): diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index dec74d0d0..11ccbc329 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -91,8 +91,7 @@ class TestFileWebpAnimation(PillowTestCase): # Tests appending using a generator def imGenerator(ims): - for im in ims: - yield im + yield from ims temp_file2 = self.tempfile("temp_generator.webp") frame1.copy().save( diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index ae528e3bf..39d6f30c4 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -114,9 +114,9 @@ class TestFileWebpMetadata(PillowTestCase): 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") diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 14b368585..9f172c212 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,5 +1,3 @@ -from __future__ import division - import sys from PIL import Image, ImageDraw, ImageFont, features diff --git a/Tests/test_image.py b/Tests/test_image.py index 53f0199de..75b5e778a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -106,7 +106,7 @@ class TestImage(PillowTestCase): def test_fp_name(self): temp_file = self.tempfile("temp.jpg") - class FP(object): + class FP: def write(a, b): pass @@ -588,11 +588,11 @@ class TestImage(PillowTestCase): try: im.load() self.assertFail() - except IOError as e: + 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 4004ca5df..a15a8b75e 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -122,7 +122,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 +151,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( diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 02e5c80f2..8277e42af 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): diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 7d1dc009d..b831b3e4a 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,5 +1,3 @@ -from __future__ import division, print_function - from contextlib import contextmanager from PIL import Image, ImageDraw 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_imagefont.py b/Tests/test_imagefont.py index 6a2d572a9..6daac701b 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import copy import distutils.version import os @@ -20,7 +19,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 @@ -462,7 +461,7 @@ 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"), @@ -470,7 +469,7 @@ class TestImageFont(PillowTestCase): ) 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( diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 5b88f94cc..84bf1c58f 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from PIL import Image, ImageDraw, ImageFont, features from .helper import PillowTestCase, unittest 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_imageops.py b/Tests/test_imageops.py index 2cdbbe02f..d0fd73689 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))] diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 9248291c3..8134dcbc4 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -15,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) diff --git a/Tests/test_locale.py b/Tests/test_locale.py index cbec8b965..14b2317ab 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import locale from PIL import Image diff --git a/Tests/test_main.py b/Tests/test_main.py index 847def834..9f3758256 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 diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index b1cf2a233..5b2ace010 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): diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 872ecdbb6..358180f3f 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from PIL import Image from .helper import PillowTestCase, hopper, unittest diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index 0d4923480..d599c8264 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -22,7 +22,7 @@ class TestPdfParser(PillowTestCase): self.assertEqual(encode_text("abc"), b"\xFE\xFF\x00a\x00b\x00c") self.assertEqual(decode_text(b"\xFE\xFF\x00a\x00b\x00c"), "abc") self.assertEqual(decode_text(b"abc"), "abc") - self.assertEqual(decode_text(b"\x1B a \x1C"), u"\u02D9 a \u02DD") + self.assertEqual(decode_text(b"\x1B a \x1C"), "\u02D9 a \u02DD") def test_indirect_refs(self): self.assertEqual(IndirectReference(1, 2), IndirectReference(1, 2)) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 5115c1862..39d60d197 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -57,7 +57,7 @@ if ImageQt.qt_is_installed: class Example(QWidget): def __init__(self): - super(Example, self).__init__() + super().__init__() img = hopper().resize((1000, 1000)) diff --git a/Tests/threaded_save.py b/Tests/threaded_save.py index 11eb86779..d47c2cf99 100644 --- a/Tests/threaded_save.py +++ b/Tests/threaded_save.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import io import queue import sys diff --git a/Tests/versions.py b/Tests/versions.py index 1ac226c9d..d6433b063 100644 --- a/Tests/versions.py +++ b/Tests/versions.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from PIL import Image 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/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("= 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 diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index fdf2c097e..13c79b460 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 diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 7b97964a8..8a9d656de 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -283,7 +283,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): self._read_blp_header() self._load() except struct.error: - raise IOError("Truncated Blp file") + raise OSError("Truncated Blp file") return 0, 0 def _read_palette(self): diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 8426e2497..2b46c97a8 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -148,7 +148,7 @@ class BmpImageFile(ImageFile.ImageFile): file_info["a_mask"], ) else: - raise IOError("Unsupported BMP header type (%d)" % file_info["header_size"]) + raise OSError("Unsupported BMP header type (%d)" % file_info["header_size"]) # ------------------ Special case : header is reported 40, which # ---------------------- is shorter than real size for bpp >= 16 @@ -163,12 +163,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 +206,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 +309,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 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..054b00d27 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -15,9 +15,6 @@ # # 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 diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index b2d508942..57769af78 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("= xsize: diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 2d492358c..d6113f8d7 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -87,4 +87,4 @@ def open(fp, mode="r"): try: return GdImageFile(fp) except SyntaxError: - raise IOError("cannot identify this image file") + raise OSError("cannot identify this image file") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 9d8e96fee..69ff2f66a 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -853,7 +853,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..03484cf0e 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -128,7 +128,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)], diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 148e604f8..ff1d37d7d 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -86,7 +86,7 @@ def _accept(prefix): return prefix[:4] == _MAGIC -class IcoFile(object): +class IcoFile: def __init__(self, buf): """ Parse image from file-like object containing ico file data diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c73cb5eb8..026b04491 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -55,7 +55,7 @@ class DecompressionBombError(Exception): pass -class _imaging_not_installed(object): +class _imaging_not_installed: # module placeholder def __getattr__(self, id): raise ImportError("The _imaging C module is not installed") @@ -427,7 +427,7 @@ def _getdecoder(mode, decoder_name, args, extra=()): decoder = getattr(core, decoder_name + "_decoder") return decoder(mode, *args + extra) except AttributeError: - raise IOError("decoder %s not available" % decoder_name) + raise OSError("decoder %s not available" % decoder_name) def _getencoder(mode, encoder_name, args, extra=()): @@ -448,7 +448,7 @@ def _getencoder(mode, encoder_name, args, extra=()): encoder = getattr(core, encoder_name + "_encoder") return encoder(mode, *args + extra) except AttributeError: - raise IOError("encoder %s not available" % encoder_name) + raise OSError("encoder %s not available" % encoder_name) # -------------------------------------------------------------------- @@ -459,7 +459,7 @@ def coerce_e(value): return value if isinstance(value, _E) else _E(value) -class _E(object): +class _E: def __init__(self, data): self.data = data @@ -500,7 +500,7 @@ def _getscaleoffset(expr): # Implementation wrapper -class Image(object): +class Image: """ This class represents an image object. To create :py:class:`~PIL.Image.Image` objects, use the appropriate factory @@ -2389,12 +2389,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 @@ -2772,7 +2772,7 @@ 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 OSError("cannot identify image file %r" % (filename if filename else fp)) # diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index ed4eefc0d..89a4426fa 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -15,8 +15,6 @@ # 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 @@ -152,7 +150,7 @@ for flag in FLAGS.values(): # Profile. -class ImageCmsProfile(object): +class ImageCmsProfile: def __init__(self, profile): """ :param profile: Either a string representing a filename, @@ -374,7 +372,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 +396,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 +477,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 +588,7 @@ def buildProofTransform( proofRenderingIntent, flags, ) - except (IOError, TypeError, ValueError) as v: + except (OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -733,9 +731,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 +773,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 +801,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 +829,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 +858,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 +887,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 +926,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 +977,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 ed3383f05..65e4d7ac9 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -45,7 +45,7 @@ directly. """ -class ImageDraw(object): +class ImageDraw: def __init__(self, im, mode=None): """ Create a drawing instance. 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 836e6318c..354159703 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") # @@ -145,7 +145,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 +203,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,13 +238,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: - raise IOError( + raise OSError( "image file is truncated " "(%d bytes not processed)" % len(b) ) @@ -322,7 +322,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 (!) @@ -334,7 +334,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. @@ -411,7 +411,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: @@ -456,9 +456,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 @@ -514,7 +514,7 @@ def _save(im, fp, tile, bufsize=0): if s: break if s < 0: - raise IOError("encoder error %d when writing image file" % s) + raise OSError("encoder error %d when writing image file" % s) e.cleanup() else: # slight speedup: compress to real file object @@ -529,7 +529,7 @@ def _save(im, fp, tile, bufsize=0): else: s = e.encode_to_file(fh, bufsize) if s < 0: - raise IOError("encoder error %d when writing image file" % s) + raise OSError("encoder error %d when writing image file" % s) e.cleanup() if hasattr(fp, "flush"): fp.flush() @@ -559,7 +559,7 @@ def _safe_read(fp, size): return b"".join(data) -class PyCodecState(object): +class PyCodecState: def __init__(self): self.xsize = 0 self.ysize = 0 @@ -570,7 +570,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..9cb62ad27 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 diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 1453a290c..f1bd6242a 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -35,7 +35,7 @@ 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 +63,7 @@ except ImportError: # -------------------------------------------------------------------- -class ImageFont(object): +class ImageFont: "PIL font wrapper" def _load_pilfont(self, filename): @@ -79,7 +79,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 +145,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): @@ -542,7 +542,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): @@ -638,7 +638,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) @@ -698,9 +698,9 @@ def load_path(filename): 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(): diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index d7598b932..adbb94000 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -26,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): 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 5052cb74d..5a06adaf3 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -55,7 +55,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") # 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/ImageSequence.py b/src/PIL/ImageSequence.py index f9be92d48..28a54c7b0 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. diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 6d3ca52d8..2999d2087 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -11,9 +11,6 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - import os import subprocess import sys @@ -52,7 +49,7 @@ def show(image, title=None, **options): return 0 -class Viewer(object): +class Viewer: """Base class for viewers.""" # main api @@ -123,10 +120,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 @@ -166,7 +161,7 @@ else: 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""" 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 769fae662..31615b838 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -62,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 @@ -203,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. @@ -293,7 +293,7 @@ def _show(image, title): tkinter.Label.__init__(self, 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..c04e70eb5 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): diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index aedf2e48c..042e96740 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -14,9 +14,6 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - import os import tempfile @@ -75,7 +72,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 +123,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 +212,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/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 020b95219..8c88a94c4 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -31,9 +31,6 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - import array import io import struct @@ -618,7 +615,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 diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index b48905bda..add344f24 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -51,7 +51,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..34d0922ab 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -26,7 +26,7 @@ __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/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 7315ab66e..5aa5f7974 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -122,7 +122,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 +131,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 +148,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 +165,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 baad510e1..90bcad036 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -23,7 +23,7 @@ from . import EpsImagePlugin # 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. @@ -71,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)) @@ -132,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..0f42d72d2 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -141,7 +141,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 +157,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/PcfFontFile.py b/src/PIL/PcfFontFile.py index 074124612..63735cecb 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -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..f7995d6b2 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -107,7 +107,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/PdfParser.py b/src/PIL/PdfParser.py index 14079f6b1..6e054af08 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -24,47 +24,47 @@ 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", } @@ -235,7 +235,7 @@ 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"/") @@ -441,7 +441,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/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index c3a5dd627..49e0c358f 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -101,7 +101,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 @@ -179,7 +179,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 @@ -211,7 +211,7 @@ class iTXt(str): return self -class PngInfo(object): +class PngInfo: """ PNG chunk container (for use with save(pnginfo=)) @@ -742,7 +742,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): @@ -795,7 +795,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 @@ -870,7 +870,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") @@ -918,7 +918,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..1ba46255b 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -139,7 +139,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 f72ad5f44..f2171bf6e 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -75,7 +75,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:]) 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 7de62c117..aa5e54386 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -163,7 +163,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 diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index f1cae4d9f..68559729c 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 @@ -273,7 +270,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/TarIO.py b/src/PIL/TarIO.py index 457d75d03..06769846e 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -37,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] diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index b1b351396..72cd2d61e 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -173,7 +173,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 6d56df217..0fffbfb14 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -38,9 +38,6 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import division, print_function - import distutils.version import io import itertools @@ -712,7 +709,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)) ) @@ -746,7 +743,7 @@ class ImageFileDirectory_v2(MutableMapping): 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) @@ -776,7 +773,7 @@ class ImageFileDirectory_v2(MutableMapping): print("- value:", self[tag]) self.next, = self._unpack("L", self._ensure_read(fp, 4)) - except IOError as msg: + except OSError as msg: warnings.warn(str(msg)) return @@ -795,7 +792,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: @@ -1060,7 +1057,7 @@ class TiffImageFile(ImageFile.ImageFile): 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: @@ -1089,14 +1086,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)) @@ -1114,7 +1111,7 @@ class TiffImageFile(ImageFile.ImageFile): # 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 @@ -1128,7 +1125,7 @@ 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"): @@ -1171,7 +1168,7 @@ class TiffImageFile(ImageFile.ImageFile): self.fp = None # might be shared if err < 0: - raise IOError(err) + raise OSError(err) return Image.Image.load(self) @@ -1179,7 +1176,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)] @@ -1421,7 +1418,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) @@ -1616,7 +1613,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) @@ -1664,9 +1661,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() diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index c047f42b6..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 diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index daa806a8b..d5a5c8e67 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -1,4 +1,3 @@ -# encoding: utf-8 # # The Python Imaging Library. # $Id$ 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 be07a747b..4818c00b0 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -19,8 +19,6 @@ # 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 @@ -44,7 +42,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"] @@ -154,7 +152,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): 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/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index bc825c3f3..22b8fe34e 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -73,7 +73,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/_util.py b/src/PIL/_util.py index 127e94d82..95bb457af 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -26,7 +26,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/features.py b/src/PIL/features.py index 9fd522368..a5ab108de 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 diff --git a/winbuild/build.py b/winbuild/build.py index f4561d933..caa63c437 100755 --- a/winbuild/build.py +++ b/winbuild/build.py @@ -53,10 +53,10 @@ def run_script(params): print(stderr.decode()) print("-- stdout --") print(trace.decode()) - print("Done with %s: %s" % (version, status)) + print("Done with {}: {}".format(version, status)) return (version, status, trace, stderr) except Exception as msg: - print("Error with %s: %s" % (version, str(msg))) + print("Error with {}: {}".format(version, str(msg))) return (version, -1, "", str(msg)) @@ -98,7 +98,7 @@ 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: @@ -157,7 +157,7 @@ def main(op): scripts.append( ( - "%s%s" % (py_version, X64_EXT), + "{}{}".format(py_version, X64_EXT), "\n".join( [ header(op), @@ -171,7 +171,7 @@ def main(op): results = map(run_script, scripts) for (version, status, trace, err) in results: - print("Compiled %s: %s" % (version, status and "ERR" or "OK")) + print("Compiled {}: {}".format(version, status and "ERR" or "OK")) def run_one(op): diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index 487329db8..b5ece5f06 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -103,7 +103,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"] ) diff --git a/winbuild/config.py b/winbuild/config.py index 46c9f9b2b..e1f181210 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -136,7 +136,7 @@ def pyversion_from_env(): 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/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) From 74d2767c575779cfd4c6bb9685d7adde953c9a5a Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 2 Oct 2019 14:16:04 +0300 Subject: [PATCH 045/186] Remove duplicate line --- Tests/test_file_libtiff.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 881d9096c..f92fc3b9b 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -260,7 +260,6 @@ class TestFileLibTiff(LibTiffTestCase): tc(4.25, TiffTags.FLOAT, True), tc(4.25, TiffTags.DOUBLE, True), tc("custom tag value", TiffTags.ASCII, True), - tc("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), From 810b61e78f873cc973407c4010fc0579e712b8a2 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 2 Oct 2019 18:11:35 +0300 Subject: [PATCH 046/186] Reinstate and simplify parallel auto-detection --- setup.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/setup.py b/setup.py index c3e0dfc9f..c72e02966 100755 --- a/setup.py +++ b/setup.py @@ -326,6 +326,15 @@ class pil_build_ext(build_ext): if self.debug: global DEBUG DEBUG = True + 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) From 7e3156eb173c00d8cac0961d44e0d9c1f1dbcd8f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Oct 2019 20:09:16 +1100 Subject: [PATCH 047/186] Updated IFDRational operators --- src/PIL/TiffImagePlugin.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0fffbfb14..59d323fc2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -358,10 +358,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)) """ @@ -370,8 +370,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__") @@ -390,7 +388,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__") From 865b17d5cf8682cc77889ba26db36f486a6cf6f3 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 7 Oct 2019 15:34:12 +0300 Subject: [PATCH 048/186] Remove Python 2-compatibility code --- Tests/test_file_png.py | 4 +--- Tests/test_imagefont.py | 5 +---- src/PIL/ImageQt.py | 6 +----- src/PIL/PdfParser.py | 12 ++---------- src/PIL/TiffImagePlugin.py | 2 +- 5 files changed, 6 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 3f5f003ac..26c40ab44 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -448,9 +448,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)) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 6daac701b..d2e24836e 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -463,10 +463,7 @@ class TestImageFont(PillowTestCase): with self.assertRaises(UnicodeEncodeError): font.getsize("’") - @unittest.skipIf( - sys.version.startswith("2") or hasattr(sys, "pypy_translation_info"), - "requires CPython 3.3+", - ) + @unittest.skipIf(hasattr(sys, "pypy_translation_info"), "requires CPython") def test_unicode_extended(self): # issue #3777 text = "A\u278A\U0001F12B" diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index ce34b0a04..f19ad3df9 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -65,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) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 6e054af08..3267ee491 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -7,11 +7,6 @@ import re import time import zlib -try: - from UserDict import UserDict # Python 2.x -except ImportError: - UserDict = collections.UserDict # Python 3.x - def make_bytes(s): return s.encode("us-ascii") @@ -256,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 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 59d323fc2..1094a6072 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1027,7 +1027,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) From 4382413bb4bd7986e24cc838ff41181416a2d28f Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 7 Oct 2019 15:40:00 +0300 Subject: [PATCH 049/186] Remove redundant bytearray --- Tests/test_file_gif.py | 14 ++++++-------- Tests/test_lib_pack.py | 4 ++-- src/PIL/TiffImagePlugin.py | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 8be614412..68301a84f 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -71,9 +71,7 @@ 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() @@ -103,7 +101,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") @@ -632,7 +630,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)) @@ -680,7 +678,7 @@ class TestFileGif(PillowTestCase): 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) @@ -695,7 +693,7 @@ 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) @@ -736,7 +734,7 @@ class TestFileGif(PillowTestCase): 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: diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 6178184bc..45958d2eb 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)) @@ -226,7 +226,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) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 1094a6072..a6fa412ca 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1742,7 +1742,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): From 64032061c0d358c0750b5973c059c8ea703ec6c3 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 7 Oct 2019 06:28:36 -0700 Subject: [PATCH 050/186] Move several imports to the top-level of the file This better follows PEP 8 style guide: https://www.python.org/dev/peps/pep-0008/#imports > Imports are always put at the top of the file, just after any module > comments and docstrings, and before module globals and constants. This also avoids duplicate import code within the same file. --- Tests/helper.py | 8 ++------ Tests/test_file_libtiff.py | 3 +-- Tests/test_file_libtiff_small.py | 4 ++-- Tests/test_file_tiff.py | 11 ++++------- Tests/test_file_webp_metadata.py | 8 ++------ Tests/test_file_xbm.py | 4 ++-- Tests/test_image.py | 3 +-- Tests/test_image_access.py | 7 +++---- src/PIL/EpsImagePlugin.py | 7 ++----- src/PIL/GifImagePlugin.py | 18 +++++++++--------- src/PIL/IcnsImagePlugin.py | 8 +++++--- src/PIL/Image.py | 3 +-- src/PIL/ImageFont.py | 5 ++--- src/PIL/ImageGrab.py | 13 +++++-------- src/PIL/JpegImagePlugin.py | 10 +++------- 15 files changed, 44 insertions(+), 68 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 78a2f520f..c39b11c83 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -5,9 +5,11 @@ 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 @@ -284,14 +286,10 @@ if not py3: 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,8 +321,6 @@ 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) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index ea73a7ad5..3372a68f0 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,5 +1,6 @@ from __future__ import print_function +import base64 import distutils.version import io import itertools @@ -852,8 +853,6 @@ class TestFileLibTiff(LibTiffTestCase): # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted # when saving to a new file. # Pillow 6.0 fails with "OSError: cannot identify image file". - import base64 - tiff = io.BytesIO( base64.b64decode( b"SUkqAAgAAAAPAP4ABAABAAAAAAAAAAABBAABAAAAAQAAAAEBBAABAAAAAQAA" diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 0db37c7ea..2eabc60fd 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 @@ -25,8 +27,6 @@ class TestFileLibTiffSmall(LibTiffTestCase): 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: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 048539f86..076f5b8cd 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,4 +1,5 @@ import logging +import os import sys from io import BytesIO @@ -504,10 +505,7 @@ class TestFileTiff(PillowTestCase): 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) @@ -516,7 +514,7 @@ 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) @@ -530,7 +528,7 @@ class TestFileTiff(PillowTestCase): for im in ims: yield im - mp = io.BytesIO() + mp = BytesIO() im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims)) mp.seek(0, os.SEEK_SET) @@ -588,7 +586,6 @@ class TestFileTiff(PillowTestCase): 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_webp_metadata.py b/Tests/test_file_webp_metadata.py index ae528e3bf..6351dc1e1 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 @@ -39,8 +41,6 @@ class TestFileWebpMetadata(PillowTestCase): 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"] @@ -73,8 +73,6 @@ class TestFileWebpMetadata(PillowTestCase): 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"] @@ -95,8 +93,6 @@ 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) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 9693ba05a..3d2259a21 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,8 +30,6 @@ static char basic_bits[] = { class TestFileXbm(PillowTestCase): def test_pil151(self): - from io import BytesIO - im = Image.open(BytesIO(PIL151)) im.load() diff --git a/Tests/test_image.py b/Tests/test_image.py index 47196a139..d48b70ffb 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,6 +1,7 @@ import os import shutil import sys +import tempfile from PIL import Image from PIL._util import py3 @@ -126,8 +127,6 @@ 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") diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index b06814cb9..4b28a2eda 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,5 +1,8 @@ +import ctypes import os +import subprocess import sys +from distutils import ccompiler, sysconfig from PIL import Image @@ -337,10 +340,6 @@ class TestEmbeddable(unittest.TestCase): "Failing on AppVeyor 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( """ diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 219f2dbb8..721ad4c21 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -23,7 +23,9 @@ import io import os import re +import subprocess import sys +import tempfile from . import Image, ImageFile from ._binary import i32le as i32 @@ -61,8 +63,6 @@ def has_ghostscript(): if gs_windows_binary: return True if not sys.platform.startswith("win"): - import subprocess - try: with open(os.devnull, "wb") as devnull: subprocess.check_call(["gs", "--version"], stdout=devnull) @@ -91,9 +91,6 @@ def Ghostscript(tile, size, fp, scale=1): float((72.0 * size[1]) / (bbox[3] - bbox[1])), ) - import subprocess - import tempfile - out_fd, outfile = tempfile.mkstemp() os.close(out_fd) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 9d8e96fee..a92c142f0 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -25,6 +25,8 @@ # import itertools +import os +import subprocess from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i8, i16le as i16, o8, o16le as o16 @@ -617,24 +619,22 @@ 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) + subprocess.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( + quant_proc = subprocess.Popen( + quant_cmd, stdout=subprocess.PIPE, stderr=devnull + ) + togif_proc = subprocess.Popen( togif_cmd, stdin=quant_proc.stdout, stdout=f, stderr=devnull ) @@ -643,11 +643,11 @@ def _save_netpbm(im, fp, filename): retcode = quant_proc.wait() if retcode: - raise CalledProcessError(retcode, quant_cmd) + raise subprocess.CalledProcessError(retcode, quant_cmd) retcode = togif_proc.wait() if retcode: - raise CalledProcessError(retcode, togif_cmd) + raise subprocess.CalledProcessError(retcode, togif_cmd) try: os.unlink(tempfile) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 75ea18b6b..9cf7a90cd 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 @@ -333,11 +334,12 @@ def _save(im, fp, filename): last_w = w * 2 # iconutil -c icns -o {} {} - from subprocess import Popen, PIPE, CalledProcessError convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] with open(os.devnull, "wb") as devnull: - convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=devnull) + convert_proc = subprocess.Popen( + convert_cmd, stdout=subprocess.PIPE, stderr=devnull + ) convert_proc.stdout.close() @@ -347,7 +349,7 @@ def _save(im, fp, filename): shutil.rmtree(iconset) if retcode: - raise CalledProcessError(retcode, convert_cmd) + raise subprocess.CalledProcessError(retcode, convert_cmd) Image.register_open(IcnsImageFile.format, IcnsImageFile, lambda x: x[:4] == b"icns") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index cca9c8a91..27d61da6d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -32,6 +32,7 @@ import numbers import os import struct import sys +import tempfile import warnings # VERSION was removed in Pillow 6.0.0. @@ -639,8 +640,6 @@ class Image(object): self.load() def _dump(self, file=None, format=None, **options): - import tempfile - suffix = "" if format: suffix = "." + format diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 5cce9af8e..2546d2aa8 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -25,8 +25,10 @@ # 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 @@ -713,9 +715,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 9b4413536..e587d942d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -15,17 +15,14 @@ # 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") @@ -40,7 +37,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False): if bbox: im = im.crop(bbox) else: - offset, size, data = grabber(include_layered_windows, all_screens) + offset, size, data = Image.core.grabscreen(include_layered_windows, all_screens) im = Image.frombytes( "RGB", size, diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 020b95219..da0759129 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -36,7 +36,10 @@ from __future__ import print_function import array import io +import os import struct +import subprocess +import tempfile import warnings from . import Image, ImageFile, TiffImagePlugin @@ -444,10 +447,6 @@ class JpegImageFile(ImageFile.ImageFile): # 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): @@ -772,9 +771,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: From fc4dbf848406c43961c59584d669f7895bd5f45a Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 14 Sep 2019 12:29:33 +0300 Subject: [PATCH 051/186] Test Docker with GitHub Actions --- .github/workflows/test-docker.yml | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/test-docker.yml diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml new file mode 100644 index 000000000..09beb2fdd --- /dev/null +++ b/.github/workflows/test-docker.yml @@ -0,0 +1,40 @@ +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, + amazon-1-amd64, + amazon-2-amd64, + fedora-29-amd64, + fedora-30-amd64, + ] + dockerTag: [master] + + name: ${{ matrix.docker }} + + steps: + - uses: actions/checkout@v1 + + - 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 }} From fab0205abcc9a7412c2c761b239e1278736c9b32 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Oct 2019 21:12:15 +1100 Subject: [PATCH 052/186] Updated documentation [ci skip] --- docs/reference/ImageOps.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 From 84e53e375799639153756a586ddceab3a94539ad Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 8 Oct 2019 11:24:36 +0300 Subject: [PATCH 053/186] Simplify using subprocess.DEVNULL Co-Authored-By: Jon Dufresne --- Tests/helper.py | 9 ++++----- setup.py | 6 +++--- src/PIL/EpsImagePlugin.py | 3 +-- src/PIL/GifImagePlugin.py | 18 +++++++++--------- src/PIL/IcnsImagePlugin.py | 7 +++---- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 302e1f6ac..2c9ec331e 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -321,11 +321,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. """ - 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 diff --git a/setup.py b/setup.py index c72e02966..ff68b1e8b 100755 --- a/setup.py +++ b/setup.py @@ -164,10 +164,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() diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 6d85da988..e38de58cf 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -57,8 +57,7 @@ def has_ghostscript(): return True if not sys.platform.startswith("win"): try: - with open(os.devnull, "wb") as devnull: - subprocess.check_call(["gs", "--version"], stdout=devnull) + subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL) return True except OSError: # No Ghostscript diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 2024e025d..3079cc950 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -623,20 +623,20 @@ def _save_netpbm(im, fp, filename): with open(filename, "wb") as f: if im.mode != "RGB": - with open(os.devnull, "wb") as devnull: - subprocess.check_call(["ppmtogif", tempfile], stdout=f, stderr=devnull) + 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"] - with open(os.devnull, "wb") as devnull: - quant_proc = subprocess.Popen( - quant_cmd, stdout=subprocess.PIPE, stderr=devnull - ) - togif_proc = subprocess.Popen( - togif_cmd, stdin=quant_proc.stdout, stdout=f, stderr=devnull - ) + 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() diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index efa7f3c2b..b80ac2179 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -336,10 +336,9 @@ def _save(im, fp, filename): # iconutil -c icns -o {} {} convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] - with open(os.devnull, "wb") as devnull: - convert_proc = subprocess.Popen( - convert_cmd, stdout=subprocess.PIPE, stderr=devnull - ) + convert_proc = subprocess.Popen( + convert_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) convert_proc.stdout.close() From 3a34081db5762b18e630d7b313dc75e4bebe0c31 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 8 Oct 2019 16:32:42 +0300 Subject: [PATCH 054/186] Simplify temporary directory cleanup Co-Authored-By: Jon Dufresne --- Tests/test_file_pdf.py | 5 +--- src/PIL/IcnsImagePlugin.py | 55 +++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 0158807f7..3547a5938 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -162,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) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index b80ac2179..18e8403d1 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -314,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 {} {} + # iconutil -c icns -o {} {} - convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] - convert_proc = subprocess.Popen( - convert_cmd, stdout=subprocess.PIPE, stderr=subprocess.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 subprocess.CalledProcessError(retcode, convert_cmd) + if retcode: + raise subprocess.CalledProcessError(retcode, convert_cmd) Image.register_open(IcnsImageFile.format, IcnsImageFile, lambda x: x[:4] == b"icns") From 0caa48b179eb8d9b3baa26d1f4d9c44f3bd46101 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 8 Oct 2019 16:41:57 +0300 Subject: [PATCH 055/186] Remove redundant __future__ from docs Co-Authored-By: Jon Dufresne --- docs/handbook/tutorial.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 16090b040..e57bc440f 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -18,7 +18,6 @@ in the :py:mod:`~PIL.Image` module:: If successful, this function returns an :py:class:`~PIL.Image.Image` object. You can now use instance attributes to examine the file contents:: - >>> from __future__ import print_function >>> print(im.format, im.size, im.mode) PPM (512, 512) RGB @@ -67,7 +66,6 @@ Convert files to JPEG :: - from __future__ import print_function import os, sys from PIL import Image @@ -89,7 +87,6 @@ Create JPEG thumbnails :: - from __future__ import print_function import os, sys from PIL import Image @@ -120,7 +117,6 @@ Identify Image Files :: - from __future__ import print_function import sys from PIL import Image @@ -513,7 +509,6 @@ 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) From e118de943d20690e9957dc902c9ec44e4754e784 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 8 Oct 2019 16:55:22 +0300 Subject: [PATCH 056/186] Remove redundant __ne__ method Co-Authored-By: Jon Dufresne --- src/PIL/Image.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 494be337a..6468fbddb 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -646,10 +646,6 @@ class Image: 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__, From 3e24c5fea42a84fe18b7e6c08e6316addd1bb127 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 8 Oct 2019 17:01:11 +0300 Subject: [PATCH 057/186] Replace isStringType(t) with isinstance(t, str) Co-Authored-By: Jon Dufresne --- Tests/test_util.py | 20 -------------------- src/PIL/Image.py | 8 ++++---- src/PIL/ImageCms.py | 3 +-- src/PIL/ImageDraw.py | 5 ++--- src/PIL/ImageOps.py | 3 +-- src/PIL/JpegImagePlugin.py | 5 ++--- src/PIL/_util.py | 4 ---- 7 files changed, 10 insertions(+), 38 deletions(-) diff --git a/Tests/test_util.py b/Tests/test_util.py index 5ec21a77c..cb2101e1d 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -4,26 +4,6 @@ from .helper import PillowTestCase, unittest class TestUtil(PillowTestCase): - def test_is_string_type(self): - # Arrange - color = "red" - - # Act - it_is = _util.isStringType(color) - - # Assert - self.assertTrue(it_is) - - def test_is_not_string_type(self): - # Arrange - color = (255, 0, 0) - - # Act - it_is_not = _util.isStringType(color) - - # Assert - self.assertFalse(it_is_not) - def test_is_path(self): # Arrange fp = "filename.ext" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6468fbddb..26194b998 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -43,7 +43,7 @@ from pathlib import Path # Use __version__ instead. from . import ImageMode, TiffTags, __version__, _plugins from ._binary import i8, i32le -from ._util import deferred_error, isPath, isStringType +from ._util import deferred_error, isPath logger = logging.getLogger(__name__) @@ -1466,7 +1466,7 @@ class Image: 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) @@ -2120,7 +2120,7 @@ class Image: """ self.load() - if isStringType(channel): + if isinstance(channel, str): try: channel = self.getbands().index(channel) except ValueError: @@ -2447,7 +2447,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 diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 89a4426fa..d05b3102c 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -18,7 +18,6 @@ import sys from PIL import Image -from PIL._util import isStringType try: from PIL import _imagingcms @@ -159,7 +158,7 @@ class ImageCmsProfile: """ - 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())) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 65e4d7ac9..3ece20152 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 """ @@ -107,13 +106,13 @@ class ImageDraw: 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) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 5a06adaf3..3ffe50806 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) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 22ec5ceb9..17cb0c412 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -41,7 +41,6 @@ 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 @@ -638,7 +637,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": @@ -659,7 +658,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) diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 95bb457af..755b4b272 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -4,10 +4,6 @@ import sys py36 = sys.version_info[0:2] >= (3, 6) -def isStringType(t): - return isinstance(t, str) - - if py36: from pathlib import Path From 1c493fb4fa6fcab3417a3eb1cecd6847dfa2c8e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Oct 2019 06:31:18 +1100 Subject: [PATCH 058/186] Added Python 3.8 rc1 --- .appveyor.yml | 6 ++++++ winbuild/config.py | 1 + 2 files changed, 7 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index f299794b6..09951f0f8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -34,9 +34,15 @@ environment: EXECUTABLE: bin/pypy.exe PIP_DIR: bin VENV: YES + - PYTHON: C:\Python38rc1-x64 install: +- ps: | + if ($env:PYTHON -eq "C:\Python38rc1-x64") { + curl -o install_python.ps1 https://raw.githubusercontent.com/matthew-brett/multibuild/d0cf77e62028704875073e3dc4626f61d1c33b0e/install_python.ps1 + .\install_python.ps1 + } - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip - 7z x pillow-depends.zip -oc:\ - mv c:\pillow-depends-master c:\pillow-depends diff --git a/winbuild/config.py b/winbuild/config.py index 16a1d9cad..fb45f4527 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -10,6 +10,7 @@ pythons = { "36": {"compiler": 7.1, "vc": 2015}, "pypy3": {"compiler": 7.1, "vc": 2015}, "37": {"compiler": 7.1, "vc": 2015}, + "38rc1-x64": {"compiler": 7.1, "vc": 2015}, } VIRT_BASE = "c:/vp/" From f2abab474da584ec753b941a97f9843c10e5e40b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Oct 2019 22:04:21 +1100 Subject: [PATCH 059/186] Added GitHub Actions badges [ci skip] --- README.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 6b783a95a..e35091bae 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors Date: Fri, 11 Oct 2019 22:40:31 +1100 Subject: [PATCH 060/186] Added orientation note [ci skip] --- docs/handbook/concepts.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 582866345..7e782e743 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -104,6 +104,15 @@ the file format handler (see the chapter on :ref:`image-file-formats`). Most handlers add properties to the :py:attr:`~PIL.Image.Image.info` attribute when loading an image, but ignore it when saving images. +Orientation +----------- + +A common element of the :py:attr:`~PIL.Image.Image.info` attribute for JPG and +TIFF images is the EXIF orientation tag. This is an instruction for how the +image data should be oriented. For example, it may instruct an image to be +rotated by 90 degrees, or to be mirrored. To apply this information to an +image, :py:meth:`~PIL.ImageOps.exif_transpose` can be used. + .. _concept-filters: Filters From faa1d1f3268f3c1f9c8300853ad92f305e6efaf1 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 11 Oct 2019 19:05:20 +0300 Subject: [PATCH 061/186] Stop testing Python 2.7 --- .appveyor.yml | 6 ------ .travis.yml | 4 ---- 2 files changed, 10 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 09951f0f8..a8562378a 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,13 +13,7 @@ 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 diff --git a/.travis.yml b/.travis.yml index 286f828f2..3be6e065c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,15 +16,11 @@ matrix: - python: "3.6" name: "Lint" env: LINT="true" - - python: "pypy" - name: "PyPy2 Xenial" - python: "pypy3" name: "PyPy3 Xenial" - python: '3.7' name: "3.7 Xenial" services: xvfb - - python: '2.7' - name: "2.7 Xenial" - python: '3.6' name: "3.6 Xenial PYTHONOPTIMIZE=1" env: PYTHONOPTIMIZE=1 From e595ddbaa1eeeee88c11314cba3fd042c120d8ef Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 22 Sep 2019 17:09:27 +0300 Subject: [PATCH 062/186] Test with GitHub Actions --- .github/workflows/lint.yml | 8 +++--- .github/workflows/test.yml | 54 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4bd02b674..f72342c5c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,17 +8,17 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [3.7] + python-version: ["3.7"] - name: Python ${{ matrix.python }} + name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python }} + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: - python-version: ${{ matrix.python }} + python-version: ${{ matrix.python-version }} - name: Install dependencies run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..fb09b0ffa --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,54 @@ +name: Test + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ + "pypy3", + "pypy2", + "3.7", + "2.7", + "3.5", + "3.6", + ] + include: + - python-version: "3.5" + env: PYTHONOPTIMIZE=2 + - python-version: "3.6" + env: PYTHONOPTIMIZE=1 + 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: Install dependencies + run: | + .travis/install.sh + + - name: Test + run: | + .travis/script.sh + + - name: Docs + if: matrix.python-version == 2.7 + run: | + pip install sphinx-rtd-theme + make doccheck + + - name: After success + if: success() + run: | + .travis/after_success.sh + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} From 7c7c53fbb6db8028a09b46c788d89f7efde42e42 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 22 Sep 2019 23:09:59 +0300 Subject: [PATCH 063/186] Test macOS with GitHub Actions --- .github/workflows/macos-install.sh | 19 +++++++++++++++++++ .github/workflows/test.yml | 16 +++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100755 .github/workflows/macos-install.sh diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh new file mode 100755 index 000000000..473a1695e --- /dev/null +++ b/.github/workflows/macos-install.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +sudo installer -pkg /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -target / + +brew install libtiff libjpeg webp little-cms2 + +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/test.yml b/.github/workflows/test.yml index fb09b0ffa..10596655f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,9 +5,12 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest strategy: matrix: + os: [ + "ubuntu-18.04", + "macOS-10.14", + ] python-version: [ "pypy3", "pypy2", @@ -21,7 +24,8 @@ jobs: env: PYTHONOPTIMIZE=2 - python-version: "3.6" env: PYTHONOPTIMIZE=1 - name: Python ${{ matrix.python-version }} + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v1 @@ -31,10 +35,16 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install Linux dependencies + if: matrix.os == 'ubuntu-18.04' run: | .travis/install.sh + - name: Install macOS dependencies + if: matrix.os == 'macOS-10.14' + run: | + .github/workflows/macos-install.sh + - name: Test run: | .travis/script.sh From ac5642dc76e947b44bc593c0e6f6299ce64838e0 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 23 Sep 2019 17:02:09 +0300 Subject: [PATCH 064/186] Split script.sh into build.sh and test.sh --- .github/workflows/test.yml | 6 +++++- .travis.yml | 3 ++- .travis/build.sh | 7 +++++++ .travis/{script.sh => test.sh} | 4 ---- 4 files changed, 14 insertions(+), 6 deletions(-) create mode 100755 .travis/build.sh rename .travis/{script.sh => test.sh} (75%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 10596655f..dcfd4eb99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,9 +45,13 @@ jobs: run: | .github/workflows/macos-install.sh + - name: Build + run: | + .travis/build.sh + - name: Test run: | - .travis/script.sh + .travis/test.sh - name: Docs if: matrix.python-version == 2.7 diff --git a/.travis.yml b/.travis.yml index 3be6e065c..2d0f7ed40 100644 --- a/.travis.yml +++ b/.travis.yml @@ -64,7 +64,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/build.sh b/.travis/build.sh new file mode 100755 index 000000000..3b286076f --- /dev/null +++ b/.travis/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +coverage erase +make clean +make install-coverage diff --git a/.travis/script.sh b/.travis/test.sh similarity index 75% rename from .travis/script.sh rename to .travis/test.sh index af56cc6ab..67c390ebd 100755 --- a/.travis/script.sh +++ b/.travis/test.sh @@ -2,10 +2,6 @@ set -e -coverage erase -make clean -make install-coverage - python -m pytest -v -x --cov PIL --cov-report term Tests # Docs From 01373b4ed9445546c3272477505316cdc3ab67a9 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 29 Sep 2019 21:28:10 +0300 Subject: [PATCH 065/186] Add Codecov token to config --- .codecov.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.codecov.yml b/.codecov.yml index 3e147d151..a93095486 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -6,4 +6,6 @@ codecov: # https://docs.codecov.io/v4.3.6/docs/comparing-commits allow_coverage_offsets: true + token: 6dafc396-e7f5-4221-a38a-8b07a49fbdae + comment: off From cf8f8b17437d366e9a4fa0549865fdc5d7e09114 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 1 Oct 2019 18:27:22 +0300 Subject: [PATCH 066/186] Drop support for EOL Python 2.7 --- .github/workflows/test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dcfd4eb99..eddf4f8d3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,9 +13,7 @@ jobs: ] python-version: [ "pypy3", - "pypy2", "3.7", - "2.7", "3.5", "3.6", ] @@ -54,7 +52,7 @@ jobs: .travis/test.sh - name: Docs - if: matrix.python-version == 2.7 + if: matrix.python-version == 3.7 run: | pip install sphinx-rtd-theme make doccheck From 707c9910ab6ca5a285bc79f13d29c5e5b3cfeeb8 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 9 Oct 2019 15:10:27 +0300 Subject: [PATCH 067/186] Install OS dependencies without checking version number --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eddf4f8d3..55b280124 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,12 +34,12 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install Linux dependencies - if: matrix.os == 'ubuntu-18.04' + if: startsWith(matrix.os, 'ubuntu') run: | .travis/install.sh - name: Install macOS dependencies - if: matrix.os == 'macOS-10.14' + if: startsWith(matrix.os, 'macOS') run: | .github/workflows/macos-install.sh From a4d7861e342ce362120d1b1b5d66a05d8903514a Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 12 Oct 2019 12:14:49 +0300 Subject: [PATCH 068/186] Turn fail-fast off, so one failed job doesn't cancel others --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55b280124..d6676ace7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,7 @@ jobs: build: strategy: + fail-fast: false matrix: os: [ "ubuntu-18.04", From 8cdacf8871da5ddbb4505882085d985a8ab65f0e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Oct 2019 21:38:01 +1100 Subject: [PATCH 069/186] Added GitHub Actions badges for platform builds [ci skip] --- README.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e35091bae..e006f5bc7 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors Date: Sat, 12 Oct 2019 21:53:49 +1100 Subject: [PATCH 070/186] Rearranged badges [ci skip] Co-Authored-By: Hugo van Kemenade --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e006f5bc7..b8cad5e9d 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors Date: Sat, 12 Oct 2019 14:16:10 +0300 Subject: [PATCH 071/186] Remove outdated OS scripts, point docs to Dockerfiles --- depends/alpine_Dockerfile | 43 --------------------------------------- depends/debian_8.2.sh | 18 ---------------- depends/fedora_23.sh | 18 ---------------- depends/freebsd_10.sh | 13 ------------ depends/ubuntu_12.04.sh | 17 ---------------- depends/ubuntu_14.04.sh | 16 --------------- docs/installation.rst | 7 +++---- 7 files changed, 3 insertions(+), 129 deletions(-) delete mode 100644 depends/alpine_Dockerfile delete mode 100755 depends/debian_8.2.sh delete mode 100755 depends/fedora_23.sh delete mode 100755 depends/freebsd_10.sh delete mode 100755 depends/ubuntu_12.04.sh delete mode 100755 depends/ubuntu_14.04.sh 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/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/installation.rst b/docs/installation.rst index 35547cb55..baca3629b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -123,10 +123,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: From accbe58b5eaf86a1a372ebc1882899fe4264c0c9 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 12 Oct 2019 15:01:18 +0100 Subject: [PATCH 072/186] add Python version to selftest, rename brief parameter --- Tests/test_features.py | 13 ++++++++----- Tests/test_main.py | 13 ++++++++----- selftest.py | 2 +- src/PIL/features.py | 17 +++++++---------- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 64b0302ca..06da35ee4 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -70,11 +70,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_main.py b/Tests/test_main.py index 847def834..8258396c1 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -12,11 +12,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/selftest.py b/selftest.py index 817b2872a..aef950b7f 100755 --- a/selftest.py +++ b/selftest.py @@ -161,7 +161,7 @@ if __name__ == "__main__": exit_status = 0 - features.pilinfo(sys.stdout, True) + features.pilinfo(sys.stdout, False) # use doctest to make sure the test program behaves as documented! import doctest diff --git a/src/PIL/features.py b/src/PIL/features.py index e8ce1e4ff..7b007dc56 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -95,7 +95,7 @@ def get_supported(): return ret -def pilinfo(out=None, brief=False): +def pilinfo(out=None, supported_formats=True): if out is None: out = sys.stdout @@ -103,6 +103,10 @@ def pilinfo(out=None, brief=False): 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__)), @@ -114,13 +118,6 @@ def pilinfo(out=None, brief=False): ) print("-" * 68, file=out) - if not brief: - 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"), @@ -135,7 +132,7 @@ def pilinfo(out=None, brief=False): ("zlib", "ZLIB (PNG/ZIP)"), ("libtiff", "LIBTIFF"), ("raqm", "RAQM (Bidirectional Text)"), - ("libimagequant", "LIBIMAGEQUANT (quantization method)"), + ("libimagequant", "LIBIMAGEQUANT (Quantization method)"), ]: if check(name): print("---", feature, "support ok", file=out) @@ -143,7 +140,7 @@ def pilinfo(out=None, brief=False): print("***", feature, "support not installed", file=out) print("-" * 68, file=out) - if not brief: + if supported_formats: extensions = collections.defaultdict(list) for ext, i in Image.EXTENSION.items(): extensions[i].append(ext) From 911079cc68adfdb217f8d0b748e90eee5a3d58af Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 12 Oct 2019 07:56:29 -0700 Subject: [PATCH 073/186] Add PyPy3 to the tox test matrix Allows contributors to automatically test locally. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2dc920371..5fee6dbb9 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ [tox] envlist = lint - py{27,35,36,37} + py{27,35,36,37,py3} minversion = 1.9 [testenv] From 0affbacd5eebfafeac1d9ed1b249c96ab602de19 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 12 Oct 2019 08:17:20 -0700 Subject: [PATCH 074/186] Remove unused arguments from PillowTestCase.skipKnwonBadTest() --- Tests/helper.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 466d04bda..b112fec7a 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -202,24 +202,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_") From 4cd4adddc3019a11e1a2a084a64723b6c7c747ac Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 25 May 2019 09:30:58 -0700 Subject: [PATCH 075/186] Improve handling of file resources Follow Python's file object semantics. User code is responsible for closing resources (usually through a context manager) in a deterministic way. To achieve this, remove __del__ functions. These functions used to closed open file handlers in an attempt to silence Python ResourceWarnings. However, using __del__ has the following drawbacks: - __del__ isn't called until the object's reference count reaches 0. Therefore, resource handlers remain open or in use longer than necessary. - The __del__ method isn't guaranteed to execute on system exit. See the Python documentation: https://docs.python.org/3/reference/datamodel.html#object.__del__ > It is not guaranteed that __del__() methods are called for objects > that still exist when the interpreter exits. - Exceptions that occur inside __del__ are ignored instead of raised. This has the potential of hiding bugs. This is also in the Python documentation: > Warning: Due to the precarious circumstances under which __del__() > methods are invoked, exceptions that occur during their execution > are ignored, and a warning is printed to sys.stderr instead. Instead, always close resource handlers when they are no longer in use. This will close the file handler at a specified point in the user's code and not wait until the interpreter chooses to. It is always guaranteed to run. And, if an exception occurs while closing the file handler, the bug will not be ignored. Now, when code receives a ResourceWarning, it will highlight an area that is mishandling resources. It should not simply be silenced, but fixed by closing resources with a context manager. All warnings that were emitted during tests have been cleaned up. To enable warnings, I passed the `-Wa` CLI option to Python. This exposed some mishandling of resources in ImageFile.__init__() and SpiderImagePlugin.loadImageSeries(), they too were fixed. --- Tests/test_bmp_reference.py | 30 +- Tests/test_decompression_bomb.py | 17 +- Tests/test_file_blp.py | 12 +- Tests/test_file_bmp.py | 27 +- Tests/test_file_bufrstub.py | 18 +- Tests/test_file_container.py | 4 +- Tests/test_file_dcx.py | 66 +++-- Tests/test_file_eps.py | 172 +++++------ Tests/test_file_fitsstub.py | 34 +-- Tests/test_file_fli.py | 118 ++++---- Tests/test_file_gbr.py | 8 +- Tests/test_file_gd.py | 6 +- Tests/test_file_gif.py | 475 ++++++++++++++++--------------- Tests/test_file_gribstub.py | 18 +- Tests/test_file_hdf5stub.py | 34 +-- Tests/test_file_icns.py | 70 ++--- Tests/test_file_ico.py | 70 ++--- Tests/test_file_im.py | 60 ++-- Tests/test_file_iptc.py | 18 +- Tests/test_file_jpeg.py | 215 +++++++------- Tests/test_file_jpeg2k.py | 6 +- Tests/test_file_libtiff.py | 135 +++++---- Tests/test_file_mic.py | 42 +-- Tests/test_file_mpo.py | 190 +++++++------ Tests/test_file_pdf.py | 36 +-- Tests/test_file_png.py | 57 ++-- Tests/test_file_ppm.py | 8 +- Tests/test_file_psd.py | 108 ++++--- Tests/test_file_spider.py | 66 +++-- Tests/test_file_tar.py | 37 ++- Tests/test_file_tga.py | 66 ++--- Tests/test_file_tiff.py | 287 ++++++++++--------- Tests/test_file_tiff_metadata.py | 148 +++++----- Tests/test_file_webp.py | 14 +- Tests/test_file_webp_alpha.py | 6 +- Tests/test_file_webp_animated.py | 42 +-- Tests/test_file_webp_metadata.py | 48 ++-- Tests/test_file_wmf.py | 40 +-- Tests/test_file_xbm.py | 16 +- Tests/test_file_xpm.py | 8 +- Tests/test_image.py | 31 +- Tests/test_image_convert.py | 10 +- Tests/test_image_fromqimage.py | 15 +- Tests/test_image_mode.py | 4 +- Tests/test_image_resize.py | 4 +- Tests/test_image_thumbnail.py | 12 +- Tests/test_image_transform.py | 26 +- Tests/test_imagecms.py | 23 +- Tests/test_imagedraw.py | 6 +- Tests/test_imagefile.py | 72 ++--- Tests/test_imageops_usm.py | 50 ++-- Tests/test_imagesequence.py | 86 +++--- Tests/test_imageshow.py | 4 +- Tests/test_locale.py | 6 +- Tests/test_map.py | 6 +- Tests/test_mode_i16.py | 6 +- Tests/test_shell_injection.py | 17 +- Tests/test_tiff_ifdrational.py | 6 +- docs/handbook/tutorial.rst | 65 +++-- docs/reference/open_files.rst | 46 ++- selftest.py | 4 +- src/PIL/Image.py | 5 - src/PIL/ImageFile.py | 27 +- src/PIL/SpiderImagePlugin.py | 3 +- src/PIL/TarIO.py | 6 - 65 files changed, 1767 insertions(+), 1605 deletions(-) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index e6a75e2c3..f7a60cfd7 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -24,8 +24,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 +48,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 +89,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: diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 3afad674f..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,21 +26,28 @@ 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): @@ -53,6 +61,7 @@ class TestDecompressionBomb(PillowTestCase): 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_file_blp.py b/Tests/test_file_blp.py index 59951a890..8dbd82986 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -5,14 +5,14 @@ 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") diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 2180835ba..e7f2c9315 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -46,13 +46,12 @@ class TestFileBmp(PillowTestCase): 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 @@ -72,24 +71,24 @@ class TestFileBmp(PillowTestCase): 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") 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_load_dib(self): # test for #1293, Imagegrab returning Unsupported Bitfields Format 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_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_eps.py b/Tests/test_file_eps.py index 3459310df..9b1a1ec40 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -25,30 +25,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,43 +57,42 @@ 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): + target = Image.open("Tests/images/pil_sample_rgb.jpg") + self.assert_image_similar(cmyk_image, target, 10) @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") def test_showpage(self): # See https://github.com/python-pillow/Pillow/issues/2615 - plot_image = Image.open("Tests/images/reqd_showpage.eps") - target = Image.open("Tests/images/reqd_showpage.png") - - # should not crash/hang - plot_image.load() - # fonts could be slightly different - self.assert_image_similar(plot_image, target, 6) + 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 io.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): @@ -120,18 +119,18 @@ 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() + image1_scale1_compare = Image.open(file1_compare).convert("RGB") + image1_scale1_compare.load() + self.assert_image_similar(image1_scale1, image1_scale1_compare, 5) # Non-Zero bounding box - 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() + image2_scale1_compare = Image.open(file2_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 +140,44 @@ 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) + image1_scale2_compare = Image.open(file1_compare_scale2).convert("RGB") + image1_scale2_compare.load() + self.assert_image_similar(image1_scale2, image1_scale2_compare, 5) # Non-Zero bounding box - 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) + image2_scale2_compare = Image.open(file2_compare_scale2).convert("RGB") + image2_scale2_compare.load() + self.assert_image_similar(image2_scale2, image2_scale2_compare, 10) @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") def test_resize(self): - # 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 +225,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_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..9208dcbff 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -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") diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 4ff9727e1..7c1de5891 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -2,7 +2,7 @@ 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, unittest try: from PIL import _webp @@ -26,18 +26,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): @@ -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,18 +541,18 @@ 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") @@ -540,16 +560,18 @@ class TestFileGif(PillowTestCase): 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,8 +627,8 @@ 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): @@ -615,16 +637,16 @@ class TestFileGif(PillowTestCase): 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 @@ -642,9 +664,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 +676,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 +685,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,8 +695,8 @@ 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 @@ -686,9 +708,9 @@ class TestFileGif(PillowTestCase): 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 @@ -701,9 +723,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_palette_save_ImagePalette(self): # pass in a different palette, as an ImagePalette.ImagePalette @@ -715,9 +737,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,8 +749,8 @@ 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 @@ -759,14 +781,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..4d7f95ec3 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -15,14 +15,14 @@ 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): @@ -56,31 +56,31 @@ class TestFileIcns(PillowTestCase): 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..ac6b19041 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") @@ -46,22 +46,22 @@ class TestFileIco(PillowTestCase): 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..1dd18a759 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -8,20 +8,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 +30,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) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 5a34a3faa..35f2c0940 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -53,14 +53,14 @@ class TestFileJpeg(PillowTestCase): 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, @@ -99,20 +99,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 @@ -205,24 +205,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) @@ -256,17 +256,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()) @@ -329,13 +329,13 @@ 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 @@ -354,11 +354,13 @@ class TestFileJpeg(PillowTestCase): 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" @@ -370,14 +372,13 @@ class TestFileJpeg(PillowTestCase): 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() + # 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) @@ -483,9 +484,9 @@ class TestFileJpeg(PillowTestCase): @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): @@ -525,10 +526,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() @@ -558,106 +559,106 @@ class TestFileJpeg(PillowTestCase): 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") 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. - self.assertEqual(im.info.get("dpi"), (72, 72)) + # This should return the default, and not a SyntaxError or + # OSError for 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, + }, + ) # 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(is_win32(), "Windows only") diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 72b374a0b..dac1d0ec0 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -42,9 +42,9 @@ class TestFileJpeg2k(PillowTestCase): 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" diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 3372a68f0..e7c3615c2 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -144,15 +144,14 @@ class TestFileLibTiff(LibTiffTestCase): 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. @@ -163,11 +162,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: @@ -302,21 +301,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() @@ -343,8 +342,8 @@ 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") @@ -412,9 +411,9 @@ class TestFileLibTiff(LibTiffTestCase): 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 @@ -521,36 +520,36 @@ 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 @@ -656,9 +655,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 @@ -686,21 +685,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 @@ -836,8 +835,8 @@ class TestFileLibTiff(LibTiffTestCase): def test_no_rows_per_strip(self): # This image does not have a RowsPerStrip TIFF tag infile = "Tests/images/no_rows_per_strip.tif" - im = Image.open(infile) - im.load() + with Image.open(infile) as im: + im.load() self.assertEqual(im.size, (950, 975)) def test_orientation(self): diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 5ec110c80..f9ea7712c 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -16,42 +16,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..9c8a2b468 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,97 @@ 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_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 +133,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_pdf.py b/Tests/test_file_pdf.py index 25c2f6bf6..c4f64a0a0 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -82,27 +82,27 @@ 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): + for im in ims: + yield im - 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) @@ -116,10 +116,10 @@ class TestFilePdf(PillowTestCase): 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) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 07e84ef72..ee2a95255 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -81,12 +81,12 @@ 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) @@ -393,12 +393,12 @@ class TestFilePng(PillowTestCase): 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) @@ -462,8 +462,13 @@ class TestFilePng(PillowTestCase): 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( + chr(0x4E00) + + chr(0x66F0) + + chr(0x9FBA) # CJK + + chr(0x3042) + + chr(0xAC00) + ) rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042)) # Combined def test_scary(self): @@ -509,19 +514,19 @@ class TestFilePng(PillowTestCase): 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") @@ -614,8 +619,8 @@ class TestFilePng(PillowTestCase): 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): @@ -624,8 +629,8 @@ class TestFilePng(PillowTestCase): 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): @@ -634,8 +639,8 @@ class TestFilePng(PillowTestCase): 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" diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 5d2a0bc69..226687fa3 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -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 8381ceaef..f57deff6a 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,26 +1,45 @@ +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(): + im = Image.open(test_file) + im.load() + im.close() + self.assert_warning(None, open) def test_invalid_file(self): @@ -29,64 +48,63 @@ 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") - - self.assertNotIn("icc_profile", im.info) + with Image.open("Tests/images/hopper_merged.psd") as im: + self.assertNotIn("icc_profile", im.info) def test_combined_larger_than_size(self): # The 'combined' sizes of the individual parts is larger than the diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 340208486..5940c2ff2 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,26 +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): @@ -32,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 @@ -57,18 +74,18 @@ class TestImageSpider(PillowTestCase): 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 @@ -109,15 +126,14 @@ 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): diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index c4666a65a..f381eef7e 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: + im = Image.open(tar) + 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..bd5c32c5b 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -76,20 +76,20 @@ class TestFileTga(PillowTestCase): 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" @@ -99,14 +99,14 @@ class TestFileTga(PillowTestCase): # Save im.save(out) - test_im = Image.open(out) - self.assertEqual(test_im.size, (100, 100)) - self.assertEqual(test_im.info["id_section"], im.info["id_section"]) + with Image.open(out) as test_im: + 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)) + 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" @@ -116,27 +116,27 @@ class TestFileTga(PillowTestCase): # Check there is no id section im.save(out) - test_im = Image.open(out) - self.assertNotIn("id_section", test_im.info) + with Image.open(out) as test_im: + 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" @@ -146,8 +146,8 @@ class TestFileTga(PillowTestCase): out = self.tempfile("temp.tga") im.save(out, orientation=1) - test_im = Image.open(out) - self.assertEqual(test_im.info["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" @@ -158,19 +158,19 @@ class TestFileTga(PillowTestCase): # Save im.save(out) - test_im = Image.open(out) - self.assertEqual(test_im.size, (199, 199)) - self.assertEqual(test_im.info["compression"], "tga_rle") + with Image.open(out) as test_im: + 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) @@ -178,8 +178,8 @@ class TestFileTga(PillowTestCase): # Save with compression im.save(out, compression="tga_rle") - test_im = Image.open(out) - self.assertEqual(test_im.info["compression"], "tga_rle") + with Image.open(out) as test_im: + self.assertEqual(test_im.info["compression"], "tga_rle") def test_save_l_transparency(self): # There are 559 transparent pixels in la.tga. diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a335e5e7a..ccaa91920 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -6,7 +6,7 @@ from PIL import Image, TiffImagePlugin from PIL._util import py3 from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION -from .helper import PillowTestCase, hopper, is_win32, unittest +from .helper import PillowTestCase, hopper, is_pypy, is_win32, unittest logger = logging.getLogger(__name__) @@ -18,32 +18,53 @@ 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): @@ -75,55 +96,55 @@ class TestFileTiff(PillowTestCase): 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") @@ -155,9 +176,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") @@ -238,44 +259,44 @@ 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") @@ -285,62 +306,62 @@ class TestFileTiff(PillowTestCase): 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]: @@ -369,16 +390,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 @@ -439,15 +460,15 @@ class TestFileTiff(PillowTestCase): 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 @@ -480,9 +501,8 @@ class TestFileTiff(PillowTestCase): 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 \ @@ -545,9 +565,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 diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 170cac71e..09b510511 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -52,77 +52,81 @@ 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 """ @@ -131,10 +135,10 @@ class TestFileTiffMetadata(PillowTestCase): f = self.tempfile("temp.tiff") img.save(f, tiffinfo=img.tag) - loaded = Image.open(f) + with Image.open(f) as loaded: - original = img.tag_v2.named() - reloaded = loaded.tag_v2.named() + original = img.tag_v2.named() + reloaded = loaded.tag_v2.named() for k, v in original.items(): if isinstance(v, IFDRational): @@ -187,18 +191,18 @@ class TestFileTiffMetadata(PillowTestCase): out = self.tempfile("temp.tiff") 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") @@ -218,9 +222,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_empty_values(self): data = io.BytesIO( @@ -243,9 +247,9 @@ class TestFileTiffMetadata(PillowTestCase): self.assertIsInstance(im.tag_v2[34377][0], bytes) out = self.tempfile("temp.tiff") im.save(out) - reloaded = Image.open(out) - self.assertEqual(len(reloaded.tag_v2[34377]), 1) - self.assertIsInstance(reloaded.tag_v2[34377][0], bytes) + with Image.open(out) as reloaded: + 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..35ce3dba7 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -158,19 +158,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..9693f691f 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -103,7 +103,8 @@ class TestFileWebpAlpha(PillowTestCase): temp_file = self.tempfile("temp.webp") file_path = "Tests/images/transparent.gif" - Image.open(file_path).save(temp_file) + with Image.open(file_path) as im: + im.save(temp_file) image = Image.open(temp_file) self.assertEqual(image.mode, "RGBA") @@ -112,6 +113,7 @@ class TestFileWebpAlpha(PillowTestCase): image.load() image.getdata() - target = Image.open(file_path).convert("RGBA") + with Image.open(file_path) as im: + target = im.convert("RGBA") self.assert_image_similar(image, target, 25.0) diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index dec74d0d0..a5d71093d 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) + im = Image.open(temp_file) + self.assertEqual(im.n_frames, orig.n_frames) - # Compare first and last frames to the original animated GIF - orig.load() - im.load() - self.assert_image_similar(im, orig.convert("RGBA"), 25.0) - orig.seek(orig.n_frames - 1) - im.seek(im.n_frames - 1) - orig.load() - im.load() - self.assert_image_similar(im, orig.convert("RGBA"), 25.0) + # 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): """ diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 6351dc1e1..6a25ea261 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -24,21 +24,21 @@ 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): file_path = "Tests/images/flower.jpg" @@ -60,17 +60,17 @@ 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): file_path = "Tests/images/flower2.jpg" @@ -126,10 +126,10 @@ class TestFileWebpMetadata(PillowTestCase): 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..c07bbf60a 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 + imref = Image.open("Tests/images/drawing_emf_ref.png") + 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 + imref = Image.open("Tests/images/drawing_wmf_ref.png") + imref.load() + self.assert_image_similar(im, imref, 2.0) def test_register_handler(self): class TestHandler: @@ -46,12 +46,12 @@ 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_save(self): im = hopper() diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 3d2259a21..5a1eb54bc 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -42,11 +42,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 +54,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..a4ea8c491 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -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_image.py b/Tests/test_image.py index 54a7680c6..b494b17dc 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -97,9 +97,9 @@ class TestImage(PillowTestCase): 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") @@ -346,7 +346,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() @@ -448,10 +449,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) @@ -522,8 +523,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 @@ -587,12 +588,12 @@ class TestImage(PillowTestCase): def test_overrun(self): for file in ["fli_overrun.bin", "sgi_overrun.bin", "pcx_overrun.bin"]: - im = Image.open(os.path.join("Tests/images", file)) - try: - im.load() - self.assertFail() - except IOError as e: - self.assertEqual(str(e), "buffer overrun when reading image file") + with Image.open(os.path.join("Tests/images", file)) as im: + try: + im.load() + self.assertFail() + except IOError as e: + self.assertEqual(str(e), "buffer overrun when reading image file") class MockEncoder(object): diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index abbd2a45f..80fa9f513 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -144,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() diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index d7556a680..bf3a250f3 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(TestFromQImage, self).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_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_resize.py b/Tests/test_image_resize.py index 7c35be570..2538dd378 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -148,5 +148,5 @@ 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") diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index bd7c98c28..d1224f075 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -39,11 +39,11 @@ class TestImageThumbnail(PillowTestCase): 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.assert_image(im, im.mode, (64, 64)) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index a0e54176a..e00a22a3d 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -156,21 +156,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_imagecms.py b/Tests/test_imagecms.py index 10465e739..97f81fb3f 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" ) @@ -266,8 +267,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 bfc2c3c9c..75e658d65 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -45,10 +45,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() diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index a367f62df..dad408e93 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -115,13 +115,13 @@ class TestImageFile(PillowTestCase): 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() + # 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: @@ -258,12 +258,12 @@ class TestPyDecoder(PillowTestCase): 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") + 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() @@ -278,12 +278,12 @@ class TestPyDecoder(PillowTestCase): 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") + 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, @@ -300,11 +300,11 @@ class TestPyDecoder(PillowTestCase): 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") + 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() @@ -323,23 +323,13 @@ class TestPyDecoder(PillowTestCase): 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} - ) - - def test_exif_shared(self): - im = Image.open("Tests/images/exif.png") - exif = im.getexif() - self.assertIs(im.getexif(), exif) - - def test_exif_str(self): - im = Image.open("Tests/images/exif.png") - exif = im.getexif() - self.assertEqual(str(exif), "{274: 1}") + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + self.assertEqual( + exif.get_ifd(0xA005), {1: "R98", 2: b"0100", 4097: 2272, 4098: 1704} + ) diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 8340c5f0d..4ed8a83a7 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(TestImageOpsUsm, self).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_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 aac48d6c0..225c8a6a9 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -27,8 +27,8 @@ 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 diff --git a/Tests/test_locale.py b/Tests/test_locale.py index cbec8b965..e4b2806e3 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -26,13 +26,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") try: - Image.open(path) + with Image.open(path): + pass finally: locale.setlocale(locale.LC_ALL, (None, None)) diff --git a/Tests/test_map.py b/Tests/test_map.py index 8d4e32219..25e24e2fb 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -23,9 +23,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..649148699 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -43,10 +43,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_shell_injection.py b/Tests/test_shell_injection.py index 97774a0a4..3e74647eb 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -24,7 +24,8 @@ class TestShellInjection(PillowTestCase): dest_file = self.tempfile(filename) save_func(src_img, 0, dest_file) # If file can't be opened, shell injection probably occurred - Image.open(dest_file).load() + with Image.open(dest_file) as im: + im.load() @unittest.skipUnless(djpeg_available(), "djpeg not available") def test_load_djpeg_filename(self): @@ -32,8 +33,8 @@ class TestShellInjection(PillowTestCase): src_file = self.tempfile(filename) shutil.copy(TEST_JPG, src_file) - im = Image.open(src_file) - im.load_djpeg() + with Image.open(src_file) as im: + im.load_djpeg() @unittest.skipUnless(cjpeg_available(), "cjpeg not available") def test_save_cjpeg_filename(self): @@ -42,10 +43,12 @@ class TestShellInjection(PillowTestCase): @unittest.skipUnless(netpbm_available(), "netpbm not available") def test_save_netpbm_filename_bmp_mode(self): - im = Image.open(TEST_GIF).convert("RGB") - self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) + with Image.open(TEST_GIF) as im: + im = im.convert("RGB") + self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) @unittest.skipUnless(netpbm_available(), "netpbm not available") def test_save_netpbm_filename_l_mode(self): - im = Image.open(TEST_GIF).convert("L") - self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) + with Image.open(TEST_GIF) as im: + im = im.convert("L") + self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index f210c8737..dedbbfe6d 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -54,5 +54,7 @@ class Test_IFDRational(PillowTestCase): res = IFDRational(301, 1) im.save(out, dpi=(res, res), compression="raw") - reloaded = Image.open(out) - self.assertEqual(float(IFDRational(301, 1)), float(reloaded.tag_v2[282])) + with Image.open(out) as reloaded: + self.assertEqual( + float(IFDRational(301, 1)), float(reloaded.tag_v2[282]) + ) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 16090b040..e5da549c3 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -99,9 +99,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) @@ -267,7 +267,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 +384,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 +423,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 +516,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/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/selftest.py b/selftest.py index dcac54a5a..99ca940ec 100755 --- a/selftest.py +++ b/selftest.py @@ -42,8 +42,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)) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 27d61da6d..cf3e556d9 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -622,11 +622,6 @@ class Image(object): # object is gone. self.im = deferred_error(ValueError("Operation on closed image")) - if sys.version_info.major >= 3: - - def __del__(self): - self.__exit__() - def _copy(self): self.load() self.im = self.im.copy() diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 836e6318c..39a596ee7 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -103,21 +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") + raise def draft(self, mode, size): """Set draft mode""" diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index f1cae4d9f..c7b6fa5f8 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -219,7 +219,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") diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index e180b802c..c81633efb 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -15,7 +15,6 @@ # import io -import sys from . import ContainerIO @@ -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() From 998156898259379f87f3f628c3287522a9b53214 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 12 Oct 2019 10:39:21 -0700 Subject: [PATCH 076/186] Simpilify PillowTestCase.delete_tempfile for pytest As the test suite always runs with pytest now, self.currentResult is always None. Using this, can remove unused code. --- Tests/helper.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 466d04bda..3de828c3b 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -53,29 +53,11 @@ 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: From 3dac6e2c6284d5c013c6b4dce03b70070198892d Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 12 Oct 2019 21:40:11 +0300 Subject: [PATCH 077/186] Replace ImageShow.which() with stdlib Co-Authored-By: Jon Dufresne --- src/PIL/ImageShow.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 2999d2087..f7e809279 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -12,6 +12,7 @@ # See the README file for information on usage and redistribution. # import os +import shutil import subprocess import sys import tempfile @@ -145,16 +146,6 @@ 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} @@ -183,7 +174,7 @@ else: command = executable = "display" return command, executable - if which("display"): + if shutil.which("display"): register(DisplayViewer) class EogViewer(UnixViewer): @@ -191,7 +182,7 @@ else: command = executable = "eog" return command, executable - if which("eog"): + if shutil.which("eog"): register(EogViewer) class XVViewer(UnixViewer): @@ -203,7 +194,7 @@ else: command += " -name %s" % quote(title) return command, executable - if which("xv"): + if shutil.which("xv"): register(XVViewer) if __name__ == "__main__": From 7068225b231e6490b857ff6e6848230557a7540e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 13 Oct 2019 08:12:54 +1100 Subject: [PATCH 078/186] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3d532f5e8..d4b9170d9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 7.0.0 (unreleased) ------------------ +- Added pypy3 to tox envlist #4137 + [jdufresne] + - Drop support for EOL PyQt4 and PySide #4108 [hugovk, radarhere] From dcb732d110de9c6725519a075860aea49e1ee847 Mon Sep 17 00:00:00 2001 From: hugovk Date: Mon, 14 Oct 2019 23:22:21 +0300 Subject: [PATCH 079/186] Test pypy3.6-v7.2.0 on Windows --- .github/workflows/test-windows.yml | 17 ++++++++--------- winbuild/appveyor_install_pypy3.cmd | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 541fe021c..2c80090fb 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build: - + runs-on: windows-2016 strategy: fail-fast: false @@ -19,16 +19,15 @@ jobs: platform-vcvars: "x86_amd64" platform-msbuild: "x64" - python-version: "pypy3.6" - pypy-version: "pypy-c-jit-97588-7392d01b93d0-win32" - pypy-url: "http://buildbot.pypy.org/nightly/py3.6/pypy-c-jit-97588-7392d01b93d0-win32.zip" - # pypy-url: "https://bitbucket.org/pypy/pypy/downloads/${{ matrix.pypy-version }}.zip" + pypy-version: "pypy3.6-v7.2.0-win32" + pypy-url: "https://bitbucket.org/pypy/pypy/downloads/pypy3.6-v7.2.0-win32.zip" exclude: - python-version: "pypy3.6" architecture: "x64" timeout-minutes: 30 - + name: Python ${{ matrix.python-version }} ${{ matrix.architecture }} - + steps: - uses: actions/checkout@v1 @@ -59,7 +58,7 @@ jobs: run: | "%pythonLocation%\python.exe" -m pip install wheel pytest pytest-cov pip install codecov - + - name: Fetch dependencies run: | curl -fsSL -o nasm.zip https://www.nasm.us/pub/nasm/releasebuilds/2.14.02/win64/nasm-2.14.02-win64.zip @@ -324,7 +323,7 @@ jobs: rem Add GhostScript and Raqm binaries (copied to INCLIB) to PATH. path %INCLIB%;%PATH% %PYTHON%\python.exe selftest.py --installed - + - name: Test Pillow run: | set PYTHON=%pythonLocation% @@ -333,7 +332,7 @@ jobs: path %INCLIB%;%PATH% cd /D %GITHUB_WORKSPACE% %PYTHON%\python.exe -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests - + - name: Upload coverage run: 'codecov --file "%GITHUB_WORKSPACE%\coverage.xml" --name "%pythonLocation%"' diff --git a/winbuild/appveyor_install_pypy3.cmd b/winbuild/appveyor_install_pypy3.cmd index 63659b165..3622ed6ec 100644 --- a/winbuild/appveyor_install_pypy3.cmd +++ b/winbuild/appveyor_install_pypy3.cmd @@ -1,3 +1,3 @@ -curl -fsSL -o pypy3.zip http://buildbot.pypy.org/nightly/py3.6/pypy-c-jit-97588-7392d01b93d0-win32.zip +curl -fsSL -o pypy3.zip https://bitbucket.org/pypy/pypy/downloads/pypy3.6-v7.2.0-win32.zip 7z x pypy3.zip -oc:\ -c:\Python37\Scripts\virtualenv.exe -p c:\pypy-c-jit-97588-7392d01b93d0-win32\pypy3.exe c:\vp\pypy3 +c:\Python37\Scripts\virtualenv.exe -p c:\pypy3.6-v7.2.0-win32\pypy3.exe c:\vp\pypy3 From 2ab518edcb0bde133b9306d14e99fa51908fd91c Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 15 Oct 2019 12:44:21 +0300 Subject: [PATCH 080/186] Add support for Python 3.8 --- .appveyor.yml | 2 +- .travis.yml | 6 +++--- docs/installation.rst | 40 +++++++++++++++++++++------------------- setup.py | 3 ++- tox.ini | 2 +- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index a8562378a..afb0bcb69 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,6 +13,7 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: + - PYTHON: C:/Python38rc1-x64 - PYTHON: C:/Python37 - PYTHON: C:/Python37-x64 - PYTHON: C:/Python36 @@ -28,7 +29,6 @@ environment: EXECUTABLE: bin/pypy.exe PIP_DIR: bin VENV: YES - - PYTHON: C:\Python38rc1-x64 install: diff --git a/.travis.yml b/.travis.yml index 2d0f7ed40..0966547a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,9 @@ matrix: env: LINT="true" - python: "pypy3" name: "PyPy3 Xenial" + - python: "3.8-dev" + name: "3.8-dev Xenial" + services: xvfb - python: '3.7' name: "3.7 Xenial" services: xvfb @@ -29,9 +32,6 @@ matrix: name: "3.5 Xenial PYTHONOPTIMIZE=2" env: PYTHONOPTIMIZE=2 services: xvfb - - 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" diff --git a/docs/installation.rst b/docs/installation.rst index 35547cb55..0e7532845 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -15,23 +15,25 @@ 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** |**2.4**|**2.5**|**2.6**|**2.7**|**3.2**|**3.3**|**3.4**|**3.5**|**3.6**|**3.7**|**3.8**| ++-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow < 2 | Yes | Yes | Yes | Yes | | | | | | | | ++-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 2 - 3 | | | Yes | Yes | Yes | Yes | Yes | Yes | | | | ++-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 4 | | | | Yes | | Yes | Yes | Yes | Yes | | | ++-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 5.0 - 5.1 | | | | Yes | | | Yes | Yes | Yes | | | ++-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 5.2 - 5.4 | | | | Yes | | | Yes | Yes | Yes | Yes | | ++-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 6.0 - 6.2.0 | | | | Yes | | | | Yes | Yes | Yes | | ++-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 6.2.1 | | | | Yes | | | | Yes | Yes | Yes | Yes | ++-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow >= 7 | | | | | | | | Yes | Yes | Yes | Yes | ++-------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ Basic Installation ------------------ @@ -405,10 +407,10 @@ These platforms are built and tested for every change. +----------------------------------+-------------------------------+-----------------------+ | 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 | +| Ubuntu Linux 16.04 LTS | 2.7, 3.5, 3.6, 3.7, 3.8, |x86-64 | | | PyPy, PyPy3 | | +----------------------------------+-------------------------------+-----------------------+ -| Windows Server 2012 R2 | 2.7, 3.5, 3.6, 3.7 |x86, x86-64 | +| Windows Server 2012 R2 | 2.7, 3.5, 3.6, 3.7, 3.8 |x86, x86-64 | | +-------------------------------+-----------------------+ | | PyPy, 3.7/MinGW |x86 | +----------------------------------+-------------------------------+-----------------------+ diff --git a/setup.py b/setup.py index 76bdfb159..d54e30dfa 100755 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ from setuptools import Extension, setup # comment this out to disable multi threaded builds. import mp_compile -if sys.platform == "win32" and sys.version_info >= (3, 8): +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 " @@ -867,6 +867,7 @@ try: "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Multimedia :: Graphics", diff --git a/tox.ini b/tox.ini index 5fee6dbb9..3da68ba7b 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ [tox] envlist = lint - py{27,35,36,37,py3} + py{27,35,36,37,38,py3} minversion = 1.9 [testenv] From fd4707d9d3a6616107433715f3f7357ea6436080 Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 15 Oct 2019 20:57:29 +0300 Subject: [PATCH 081/186] Test on Python 3.8 final --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0966547a0..70833de17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,8 +18,8 @@ matrix: env: LINT="true" - python: "pypy3" name: "PyPy3 Xenial" - - python: "3.8-dev" - name: "3.8-dev Xenial" + - python: "3.8" + name: "3.8 Xenial" services: xvfb - python: '3.7' name: "3.7 Xenial" From 062c4013a48e9442060f8fda259051aa5b145bcb Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 15 Oct 2019 23:02:43 +0300 Subject: [PATCH 082/186] Consistent slash --- .appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index afb0bcb69..5352e9c73 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,7 +13,7 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/Python38rc1-x64 + - PYTHON: C:\Python38rc1-x64 - PYTHON: C:/Python37 - PYTHON: C:/Python37-x64 - PYTHON: C:/Python36 From 2b862103086f86fb144d4ff44d75ed13acb57f06 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 19 Oct 2019 09:58:13 +0300 Subject: [PATCH 083/186] Quotes and caps To exactly match the recommend text at https://tidelift.com/lifter/package/pypi/Pillow/tasks/2439 and hopefully mark the task as done. --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From aaf9720c58c5df5dfe46294acde21bdca27c1b33 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 20 Oct 2019 21:00:26 +1100 Subject: [PATCH 084/186] Updated CI target Python versions for macOS [ci skip] --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 0e7532845..13333e465 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -405,7 +405,7 @@ These platforms are built and tested for every change. +----------------------------------+-------------------------------+-----------------------+ | Fedora 30 | 2.7, 3.7 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| macOS 10.13 High Sierra* | 2.7, 3.5, 3.6, 3.7 |x86-64 | +| macOS 10.13 High Sierra* | 2.7, 3.5, 3.6, 3.7, 3.8 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ | Ubuntu Linux 16.04 LTS | 2.7, 3.5, 3.6, 3.7, 3.8, |x86-64 | | | PyPy, PyPy3 | | From 1bbacf94750f7f486f6c8231b92c55d6937b9466 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 20 Oct 2019 16:13:07 +0300 Subject: [PATCH 085/186] Update CHANGES.rst [CI skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d4b9170d9..d86f60210 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,12 @@ Changelog (Pillow) - Changed default frombuffer raw decoder args #1730 [radarhere] +6.2.1 (unreleased) +------------------ + +- Add support for Python 3.8 #4141 + [hugovk] + 6.2.0 (2019-10-01) ------------------ From 6f3464e1cbf52f1313ca97a5f327036ef809836c Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 20 Oct 2019 16:31:09 +0300 Subject: [PATCH 086/186] Add release notes for Pillow 6.2.1 --- docs/releasenotes/6.2.1.rst | 7 +++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 8 insertions(+) create mode 100644 docs/releasenotes/6.2.1.rst diff --git a/docs/releasenotes/6.2.1.rst b/docs/releasenotes/6.2.1.rst new file mode 100644 index 000000000..2c77fc44a --- /dev/null +++ b/docs/releasenotes/6.2.1.rst @@ -0,0 +1,7 @@ +6.2.1 +----- + +Support added for Python 3.8 +============================ + +Pillow 6.2.1 supports Python 3.8. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 76c0321e7..381643cf3 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,6 +6,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 6.2.1 6.2.0 6.1.0 6.0.0 From a80a45219f119717b5589673d3bf50944f31b030 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 20 Oct 2019 23:06:22 +0300 Subject: [PATCH 087/186] Pillow 6.2.1 is the last to support Python 2.7 --- CHANGES.rst | 6 +++--- docs/releasenotes/6.2.0.rst | 8 -------- docs/releasenotes/6.2.1.rst | 21 ++++++++++++++++++++- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d86f60210..1e878c229 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,17 +20,17 @@ Changelog (Pillow) - Changed default frombuffer raw decoder args #1730 [radarhere] -6.2.1 (unreleased) +6.2.1 (2019-10-20) ------------------ +- 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) ------------------ -- This is the last Pillow release to support Python 2.7 #3642 - - Catch buffer overruns #4104 [radarhere] diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 9576e6be8..c31ade197 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -62,14 +62,6 @@ shared instance of ``Image.Exif``. Deprecations ^^^^^^^^^^^^ -Python 2.7 -~~~~~~~~~~ - -Python 2.7 reaches end-of-life on 2020-01-01. - -Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python -2.7, making Pillow 6.2.x the last release series to support Python 2. - Image.frombuffer ~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/6.2.1.rst b/docs/releasenotes/6.2.1.rst index 2c77fc44a..ca298fa70 100644 --- a/docs/releasenotes/6.2.1.rst +++ b/docs/releasenotes/6.2.1.rst @@ -1,7 +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. From d8003c350f892c7e2392eef71b1aafa604c60a12 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 21 Oct 2019 21:57:59 +0300 Subject: [PATCH 088/186] Add a check for point releases --- RELEASING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASING.md b/RELEASING.md index feeb7c0d7..9614b133f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -51,6 +51,7 @@ Released as needed for security, installation or critical bug fixes. make sdist ``` * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions) +* [ ] Upload all binaries and source distributions e.g. `twine upload dist/Pillow-5.2.1*` * [ ] Create a [new release on GitHub](https://github.com/python-pillow/Pillow/releases/new) ## Embargoed Release From 2694564d08fd147b40641cee80fe594c8bba864c Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Mon, 21 Oct 2019 14:47:51 -0700 Subject: [PATCH 089/186] Do not destroy glyph while its bitmap is used --- src/_imagingft.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 7776e43f1..0e8622844 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -791,7 +791,7 @@ font_render(FontObject* self, PyObject* args) int index, error, ascender, horizontal_dir; int load_flags; unsigned char *source; - FT_Glyph glyph; + FT_Glyph glyph = NULL; FT_GlyphSlot glyph_slot; FT_Bitmap bitmap; FT_BitmapGlyph bitmap_glyph; @@ -890,8 +890,6 @@ font_render(FontObject* self, PyObject* args) bitmap = bitmap_glyph->bitmap; left = bitmap_glyph->left; - - FT_Done_Glyph(glyph); } else { bitmap = glyph_slot->bitmap; left = glyph_slot->bitmap_left; @@ -953,6 +951,10 @@ font_render(FontObject* self, PyObject* args) } x += glyph_info[i].x_advance; y -= glyph_info[i].y_advance; + if (glyph != NULL) { + FT_Done_Glyph(glyph); + glyph = NULL; + } } FT_Stroker_Done(stroker); From 41a77fcf27d22e0a0cc8543e0e3a4832ec0e8e1c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 22 Oct 2019 20:17:47 +1100 Subject: [PATCH 090/186] Updated 6.2.1 release date [ci skip] --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1e878c229..3a0bb1c1d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,7 @@ Changelog (Pillow) - Changed default frombuffer raw decoder args #1730 [radarhere] -6.2.1 (2019-10-20) +6.2.1 (2019-10-21) ------------------ - This is the last Pillow release to support Python 2.7 #3642 From cae17eb927a82c89ce096c07a3ec08cc6239872a Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 5 Nov 2016 10:31:11 -0700 Subject: [PATCH 091/186] Use more Pythonic super() instead of referencing parent class https://rhettinger.wordpress.com/2011/05/26/super-considered-super/ --- Tests/test_imageqt.py | 4 ++-- src/PIL/BdfFontFile.py | 3 +-- src/PIL/ImageFile.py | 2 +- src/PIL/ImageQt.py | 3 +-- src/PIL/ImageTk.py | 2 +- src/PIL/ImageWin.py | 2 +- src/PIL/PcfFontFile.py | 2 +- src/PIL/PngImagePlugin.py | 3 +-- src/PIL/TarIO.py | 2 +- src/PIL/TiffImagePlugin.py | 2 +- 10 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 9248291c3..5cf14adda 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -25,7 +25,7 @@ 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 @@ -37,7 +37,7 @@ class PillowQPixmapTestCase(PillowQtTestCase): self.app = QGuiApplication([]) def tearDown(self): - PillowQtTestCase.tearDown(self) + super().tearDown() self.app.quit() diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index fdf2c097e..53a890fb6 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -85,8 +85,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/ImageFile.py b/src/PIL/ImageFile.py index 836e6318c..a275f95d2 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -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 diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index da60cacd0..f49725b5f 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -180,8 +180,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/ImageTk.py b/src/PIL/ImageTk.py index fd480007a..bb825f353 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -296,7 +296,7 @@ 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") diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index ed2c18ec4..c654569c5 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -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/PcfFontFile.py b/src/PIL/PcfFontFile.py index 074124612..7b3075696 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 = {} diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index be237b3ee..84317ceb7 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -293,8 +293,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 = {} diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index e180b802c..92a08aefb 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -55,7 +55,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): diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index b0d465fe2..455fbd0ee 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -911,7 +911,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) From 3443c3679531d15b8fea338e9db68dd8f0a3ca6b Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 25 Oct 2019 14:16:51 +0300 Subject: [PATCH 092/186] Explicitly use cmd shell, as GHA changed the default to powershell --- .github/workflows/test-windows.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 2c80090fb..0e368a7c6 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -58,6 +58,7 @@ jobs: run: | "%pythonLocation%\python.exe" -m pip install wheel pytest pytest-cov pip install codecov + shell: cmd - name: Fetch dependencies run: | @@ -96,6 +97,7 @@ jobs: copy /Y /B j*.h %INCLIB% copy /Y /B *.lib %INCLIB% copy /Y /B *.exe %INCLIB% + shell: cmd - name: Build dependencies / libjpeg-turbo run: | @@ -114,6 +116,7 @@ jobs: 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: | @@ -128,6 +131,7 @@ jobs: 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: | @@ -143,6 +147,7 @@ jobs: 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: | @@ -157,6 +162,7 @@ jobs: 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: | @@ -176,6 +182,7 @@ jobs: %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: | @@ -193,6 +200,7 @@ jobs: %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: | @@ -211,6 +219,7 @@ jobs: 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 @@ -233,6 +242,7 @@ jobs: nmake -nologo -f Makefile copy /Y /B *.h %INCLIB% copy /Y /B *.lib %INCLIB% + shell: cmd # for Raqm - name: Build dependencies / HarfBuzz @@ -253,6 +263,7 @@ jobs: nmake -nologo -f Makefile harfbuzz copy /Y /B src\*.h %INCLIB% copy /Y /B *.lib %INCLIB% + shell: cmd # for Raqm - name: Build dependencies / FriBidi @@ -272,6 +283,7 @@ jobs: nmake -nologo -f Makefile fribidi copy /Y /B lib\*.h %INCLIB% copy /Y /B *.lib %INCLIB% + shell: cmd # failing with PyPy3 - name: Build dependencies / Raqm @@ -293,6 +305,7 @@ jobs: nmake -nologo -f Makefile libraqm copy /Y /B src\*.h %INCLIB% copy /Y /B libraqm.dll %INCLIB% + shell: cmd - name: Build dependencies / ghostscript run: | @@ -308,6 +321,7 @@ jobs: nmake -nologo -f psi\msvc.mak rem Add bin to PATH variable: Copy to INCLIB, then add INCLIB to PATH in Test step. copy /Y /B bin\* %INCLIB% + shell: cmd - name: Build Pillow run: | @@ -323,6 +337,7 @@ jobs: rem Add GhostScript and Raqm binaries (copied to INCLIB) to PATH. path %INCLIB%;%PATH% %PYTHON%\python.exe selftest.py --installed + shell: cmd - name: Test Pillow run: | @@ -332,9 +347,11 @@ jobs: path %INCLIB%;%PATH% cd /D %GITHUB_WORKSPACE% %PYTHON%\python.exe -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests + shell: cmd - name: Upload coverage run: 'codecov --file "%GITHUB_WORKSPACE%\coverage.xml" --name "%pythonLocation%"' + shell: cmd - name: Build wheel id: wheel @@ -351,6 +368,7 @@ jobs: set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 %PYTHON%\python.exe setup.py bdist_wheel + shell: cmd - uses: actions/upload-artifact@v1 if: "github.event_name == 'push' && !contains(matrix.python-version, 'pypy')" From 720a1738facdda36815e6659a9117a4c96e603e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2019 09:58:47 +1100 Subject: [PATCH 093/186] Replaced macOS Command Line Tools install with miniconda path --- .github/workflows/macos-install.sh | 2 -- .travis/build.sh | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 473a1695e..78eac6162 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,8 +2,6 @@ set -e -sudo installer -pkg /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -target / - brew install libtiff libjpeg webp little-cms2 PYTHONOPTIMIZE=0 pip install cffi diff --git a/.travis/build.sh b/.travis/build.sh index 3b286076f..a2e3041bd 100755 --- a/.travis/build.sh +++ b/.travis/build.sh @@ -3,5 +3,8 @@ set -e coverage erase +if [ $(uname) == "Darwin" ]; then + export CPPFLAGS="-I/usr/local/miniconda/include"; +fi make clean make install-coverage From ef0fa7a65172942119ae5cfeaef0844ac693e712 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 25 Oct 2019 11:40:11 +0300 Subject: [PATCH 094/186] Remove soon-EOL Fedora 29 --- .github/workflows/test-docker.yml | 1 - .travis.yml | 1 - azure-pipelines.yml | 5 ----- docs/installation.rst | 2 -- 4 files changed, 9 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 09beb2fdd..b24a4b9dc 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -19,7 +19,6 @@ jobs: centos-7-amd64, amazon-1-amd64, amazon-2-amd64, - fedora-29-amd64, fedora-30-amd64, ] dockerTag: [master] diff --git a/.travis.yml b/.travis.yml index 70833de17..5e6997e79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,7 +42,6 @@ matrix: - env: DOCKER="centos-7-amd64" DOCKER_TAG="master" - env: DOCKER="amazon-1-amd64" DOCKER_TAG="master" - env: DOCKER="amazon-2-amd64" DOCKER_TAG="master" - - env: DOCKER="fedora-29-amd64" DOCKER_TAG="master" - env: DOCKER="fedora-30-amd64" DOCKER_TAG="master" services: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f26f2c037..b1be91534 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -61,11 +61,6 @@ jobs: docker: 'amazon-2-amd64' name: 'amazon_2_amd64' -- template: .azure-pipelines/jobs/test-docker.yml - parameters: - docker: 'fedora-29-amd64' - name: 'fedora_29_amd64' - - template: .azure-pipelines/jobs/test-docker.yml parameters: docker: 'fedora-30-amd64' diff --git a/docs/installation.rst b/docs/installation.rst index 13333e465..f6cbc585d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -401,8 +401,6 @@ These platforms are built and tested for every change. +----------------------------------+-------------------------------+-----------------------+ | 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, 3.8 |x86-64 | From a6244ffbb108766d04cc18b6d5f422332763a752 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2019 17:31:50 +1100 Subject: [PATCH 095/186] Added OpenJPEG, libimagequant and freetype --- .github/workflows/macos-install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 78eac6162..6cd9dadf3 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,7 +2,7 @@ set -e -brew install libtiff libjpeg webp little-cms2 +brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype PYTHONOPTIMIZE=0 pip install cffi pip install coverage From 97ea6898cadb23d281fd5e2fd0167902c00ae671 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 27 Oct 2019 07:38:45 +1100 Subject: [PATCH 096/186] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3a0bb1c1d..816e0a4f1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 7.0.0 (unreleased) ------------------ +- Removed CI testing of Fedora 29 #4165 + [hugovk] + - Added pypy3 to tox envlist #4137 [jdufresne] From 204803917c2df413246bda8fd7ca941602bed543 Mon Sep 17 00:00:00 2001 From: mixmastamyk Date: Sun, 27 Oct 2019 23:36:33 -0700 Subject: [PATCH 097/186] Fix mismatched name, add explanation. Mention why this information is not available in the EXIF tag specified for this purpose. --- src/PIL/JpegPresets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 From 8d9f0d23741218040eb27108b700d3450bae92dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Oct 2019 21:17:32 +1100 Subject: [PATCH 098/186] Simplified commands --- depends/install_extra_test_images.sh | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) 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 From b8023838cc827aa93770d0292f9572ac22d1b393 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Oct 2019 19:07:45 +1100 Subject: [PATCH 099/186] Added GHA [ci skip] --- docs/about.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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/ From d9845c14c83ba60e45494338446ab8358685cf93 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Oct 2019 22:42:34 +1100 Subject: [PATCH 100/186] Lint fixes --- src/PIL/BlpImagePlugin.py | 38 +++++++++++++++++++------------------- src/PIL/DdsImagePlugin.py | 2 +- src/PIL/FtexImagePlugin.py | 2 +- src/PIL/Image.py | 4 ++-- src/PIL/TiffImagePlugin.py | 12 ++++++------ 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 7b97964a8..dd1745158 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(" 4: - offset, = struct.unpack("L", data) + (offset,) = struct.unpack(">L", data) self.fp.seek(offset) camerainfo = {"ModelID": self.fp.read(4)} diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 455fbd0ee..7f596b9ed 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -484,7 +484,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) @@ -595,7 +595,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( @@ -765,7 +765,7 @@ 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), @@ -797,7 +797,7 @@ class ImageFileDirectory_v2(MutableMapping): else: print("- value:", self[tag]) - self.next, = self._unpack("L", self._ensure_read(fp, 4)) + (self.next,) = self._unpack("L", self._ensure_read(fp, 4)) except IOError as msg: warnings.warn(str(msg)) return @@ -1796,11 +1796,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): From 47691906df10d0a7bfb485e97ba66475247189fd Mon Sep 17 00:00:00 2001 From: pwohlhart Date: Tue, 29 Oct 2019 10:12:03 -0700 Subject: [PATCH 101/186] Better error messaging in PIL.image.fromarray --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 474ca1e88..0c783a428 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2633,7 +2633,7 @@ def fromarray(obj, mode=None): typekey = (1, 1) + shape[2:], arr["typestr"] mode, rawmode = _fromarray_typemap[typekey] except KeyError: - raise TypeError("Cannot handle this data type") + raise TypeError("Cannot handle this data type: %s, %s" % typekey) else: rawmode = mode if mode in ["1", "L", "I", "P", "F"]: From 9b20276c45d7fdae34f24d9a803846b725deba82 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 30 Oct 2019 07:23:08 +1100 Subject: [PATCH 102/186] Allow for arr KeyError --- Tests/test_image_array.py | 5 +++++ src/PIL/Image.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 02e5c80f2..e2a79764e 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -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/src/PIL/Image.py b/src/PIL/Image.py index 0c783a428..4093c030c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2631,6 +2631,9 @@ def fromarray(obj, mode=None): if mode is None: try: typekey = (1, 1) + shape[2:], arr["typestr"] + 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) From 511aed922aed5d8a4d060f70f01bf9400d52cea6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Oct 2019 09:10:29 +1000 Subject: [PATCH 103/186] Fixed freeing unallocated pointer when resizing with height too large --- Tests/test_image_resample.py | 14 +++++++++----- src/libImaging/Resample.c | 2 -- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 7d1dc009d..854a64d01 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -11,11 +11,15 @@ 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() diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 4a98e8477..d1a89e2ce 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -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; } From 290189596ec77808d98ce6ea57fbf41f611a60dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Sep 2019 21:11:40 +1000 Subject: [PATCH 104/186] Removed redundant return --- src/PIL/TiffImagePlugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 7f596b9ed..ab53483c5 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -327,8 +327,6 @@ class IFDRational(Rational): if denominator == 0: self._val = float("nan") - return - elif denominator == 1: self._val = Fraction(value) else: From 887a7e503cbff8f42704d1323b01e46c4f1f2ce1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Sep 2019 21:18:52 +1000 Subject: [PATCH 105/186] Only assign once --- src/PIL/TiffImagePlugin.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ab53483c5..a685009e8 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -310,20 +310,18 @@ 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") From d716278d20e5d270babf88f92d57bcd49f02910e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2019 17:53:50 +1100 Subject: [PATCH 106/186] Corrected DdsImagePlugin setting info gamma --- Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds | Bin 0 -> 516 bytes Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.png | Bin 0 -> 106 bytes Tests/test_file_dds.py | 16 ++++++++++++++++ src/PIL/DdsImagePlugin.py | 2 +- 4 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds create mode 100644 Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.png 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 0000000000000000000000000000000000000000..9b4d8e21f646b498275ea69582fe02fe905ce404 GIT binary patch literal 516 zcmZ>930A0KU|?Vu;9?K}(jd&h2u2L7AT|j=0jQP*h+QHK4X|nB5OBZ{P6moFLq&ns zf`P#Q|Nre7)~s5!%4?E}=Oh&jK2X(x|LYO*=zL1`qnn4$r__8x`qAal?MLUMtCz3& r!1&?uLwUIa{D15}FbjbBzuDQ?e$+Ft7ce-izyA7bRilHDAOrvaQ6i>% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..57177fe2bb83d99a200a0008a94b4545cc6eb99a GIT binary patch literal 106 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`hMq2tAr*6y|NQ@N&un+##EFh@ zM&Y=-E)9XqY|V^4JUl!VkAMFE|G&qi^xz7{=7S6jY(X5-m*NjJ05vmsy85}Sb4q9e E01k&CqyPW_ literal 0 HcmV?d00001 diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 498c64f21..8ef90e86e 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" @@ -69,6 +70,21 @@ class TestFileDds(PillowTestCase): self.assert_image_equal(target, im) + def test_dx10_bc7_unorm_srgb(self): + """Check DX10 unsigned normalized integer images can be opened""" + + target = Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB.replace(".dds", ".png")) + + im = Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) + im.load() + + self.assertEqual(im.format, "DDS") + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (16, 16)) + self.assertEqual(im.info["gamma"], 1 / 2.2) + + self.assert_image_equal(target, im) + def test_unimplemented_dxgi_format(self): self.assertRaises( NotImplementedError, diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index e6622d14f..28a582ec4 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -155,7 +155,7 @@ class DdsImageFile(ImageFile.ImageFile): n = 7 elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB: self.pixel_format = "BC7" - self.im_info["gamma"] = 1 / 2.2 + self.info["gamma"] = 1 / 2.2 n = 7 else: raise NotImplementedError( From b65fcb280ab23cb1fc1e9eb22a4951415722d5ee Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Oct 2019 20:22:41 +1100 Subject: [PATCH 107/186] Copy info in transform --- Tests/test_image_transform.py | 14 +++++++++++--- src/PIL/Image.py | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index a0e54176a..33b8aaa7c 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" + + im = Image.open("Tests/images/hopper.gif") + 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 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 474ca1e88..edbb98e9c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2293,6 +2293,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: From 2296614e5f5de25e84234fa41e8a41b21e23e012 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Oct 2019 23:11:13 +1100 Subject: [PATCH 108/186] Use pillow-depends on HTTPError --- winbuild/build_dep.py | 4 +--- winbuild/config.py | 37 ++++++++++++++++++------------------- winbuild/fetch.py | 29 +++++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index d7df22584..0062ad0c9 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( diff --git a/winbuild/config.py b/winbuild/config.py index 37e45e8cb..72669f753 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -28,96 +28,95 @@ 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", + "filename": "tiff-4.0.10.tar.gz", "dir": "tiff-4.0.10", }, "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-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", }, "lcms-2.8": { "url": SF_MIRROR + "/project/lcms/lcms/2.8/lcms2-2.8.zip", - "filename": PILLOW_DEPENDS_DIR + "lcms2-2.8.zip", + "filename": "lcms2-2.8.zip", "dir": "lcms2-2.8", }, "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", + "filename": "ghostscript-9.27.tar.gz", "dir": "ghostscript-9.27", }, "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", + "filename": "tcl869-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", + "filename": "tk869-src.zip", "dir": "", "version": "8.6.9", }, "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": PILLOW_DEPENDS_DIR + "libjpeg-turbo-2.0.3.tar.gz", + "filename": "libjpeg-turbo-2.0.3.tar.gz", "dir": "libjpeg-turbo-2.0.3", }, # ba653c8: Merge tag '2.12.5' into msvc "imagequant": { "url": "https://github.com/ImageOptim/libimagequant/archive/ba653c8ccb34dde4e21c6076d85a72d21ed9d971.zip", # noqa: E501 - "filename": PILLOW_DEPENDS_DIR - + "libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971.zip", + "filename": "libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971.zip", "dir": "libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971", }, "harfbuzz": { "url": "https://github.com/harfbuzz/harfbuzz/archive/2.6.1.zip", - "filename": PILLOW_DEPENDS_DIR + "harfbuzz-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": PILLOW_DEPENDS_DIR + "fribidi-1.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": PILLOW_DEPENDS_DIR + "libraqm-0.7.0.zip", + "filename": "libraqm-0.7.0.zip", "dir": "libraqm-0.7.0", }, } 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) From 37f492cbbc715c3c88c17508b71103323cbda655 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 31 Oct 2019 12:35:26 +0200 Subject: [PATCH 109/186] Add support for Fedora 31 --- .github/workflows/test-docker.yml | 1 + .travis.yml | 1 + azure-pipelines.yml | 5 +++++ docs/installation.rst | 2 ++ 4 files changed, 9 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index b24a4b9dc..4121eb5e9 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -20,6 +20,7 @@ jobs: amazon-1-amd64, amazon-2-amd64, fedora-30-amd64, + fedora-31-amd64, ] dockerTag: [master] diff --git a/.travis.yml b/.travis.yml index 5e6997e79..56542996b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,6 +43,7 @@ matrix: - env: DOCKER="amazon-1-amd64" DOCKER_TAG="master" - env: DOCKER="amazon-2-amd64" DOCKER_TAG="master" - env: DOCKER="fedora-30-amd64" DOCKER_TAG="master" + - env: DOCKER="fedora-31-amd64" DOCKER_TAG="master" services: - docker diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b1be91534..38cf61f62 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -65,3 +65,8 @@ jobs: parameters: docker: 'fedora-30-amd64' name: 'fedora_30_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'fedora-31-amd64' + name: 'fedora_31_amd64' diff --git a/docs/installation.rst b/docs/installation.rst index f6cbc585d..4ccc47ae4 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -403,6 +403,8 @@ These platforms are built and tested for every change. +----------------------------------+-------------------------------+-----------------------+ | Fedora 30 | 2.7, 3.7 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ +| Fedora 31 | 3.7 |x86-64 | ++----------------------------------+-------------------------------+-----------------------+ | macOS 10.13 High Sierra* | 2.7, 3.5, 3.6, 3.7, 3.8 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ | Ubuntu Linux 16.04 LTS | 2.7, 3.5, 3.6, 3.7, 3.8, |x86-64 | From a2225ae961d16185d7191b5fbb325addc5f4048f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 Nov 2019 21:34:38 +1100 Subject: [PATCH 110/186] Employ same condition used to set glyph --- src/_imagingft.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 0e8622844..146df095b 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -791,7 +791,7 @@ font_render(FontObject* self, PyObject* args) int index, error, ascender, horizontal_dir; int load_flags; unsigned char *source; - FT_Glyph glyph = NULL; + FT_Glyph glyph; FT_GlyphSlot glyph_slot; FT_Bitmap bitmap; FT_BitmapGlyph bitmap_glyph; @@ -951,9 +951,8 @@ font_render(FontObject* self, PyObject* args) } x += glyph_info[i].x_advance; y -= glyph_info[i].y_advance; - if (glyph != NULL) { + if (stroker != NULL) { FT_Done_Glyph(glyph); - glyph = NULL; } } From c9c02c513b841e63c6947528881fb5deab8a1869 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 1 Nov 2019 13:42:12 +0200 Subject: [PATCH 111/186] Update docs for 7.0.0 --- docs/deprecations.rst | 46 ++++++++++++------------ docs/releasenotes/7.0.0.rst | 71 +++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 3 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 docs/releasenotes/7.0.0.rst diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 396300f1f..3a48f37ee 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 ~~~~~~~~~~ @@ -96,6 +73,29 @@ 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. + +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") + PILLOW_VERSION constant ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst new file mode 100644 index 000000000..4fb6773c4 --- /dev/null +++ b/docs/releasenotes/7.0.0.rst @@ -0,0 +1,71 @@ +7.0.0 +----- + +Backwards Incompatible Changes +============================== + +PILLOW_VERSION constant +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +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. + + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +TODO +~~~~ + +TODO + + +API Additions +============= + +TODO +^^^^ + +TODO + + +Other Changes +============= + +Image.__del__ +^^^^^^^^^^^^^ + +Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. +Use a context manager or call ``Image.close()`` instead to close the file in a +deterministic way. + +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") diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 381643cf3..6f8ecb95c 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,6 +6,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 7.0.0 6.2.1 6.2.0 6.1.0 From b4f93cf140faeaab59a46924d01694f106d165e2 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 1 Nov 2019 13:54:19 +0200 Subject: [PATCH 112/186] Upgrade Python syntax with pyupgrade --py3-plus --- Tests/test_image_fromqimage.py | 2 +- Tests/test_imageops_usm.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index bf3a250f3..0d961572c 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -6,7 +6,7 @@ from .test_imageqt import PillowQtTestCase class TestFromQImage(PillowQtTestCase, PillowTestCase): def setUp(self): - super(TestFromQImage, self).setUp() + super().setUp() self.files_to_test = [ hopper(), Image.open("Tests/images/transparent.png"), diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 4ed8a83a7..71e858681 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -5,7 +5,7 @@ from .helper import PillowTestCase class TestImageOpsUsm(PillowTestCase): def setUp(self): - super(TestImageOpsUsm, self).setUp() + super().setUp() self.im = Image.open("Tests/images/hopper.ppm") self.addCleanup(self.im.close) self.snakes = Image.open("Tests/images/color_snakes.png") From 41ddb1ef5db8a006eb46021cfb973805300dff22 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 Nov 2019 23:21:54 +1100 Subject: [PATCH 113/186] Removed unused variable --- winbuild/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/winbuild/config.py b/winbuild/config.py index 72669f753..1c5ee6c28 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -1,7 +1,6 @@ import os SF_MIRROR = "https://iweb.dl.sourceforge.net" -PILLOW_DEPENDS_DIR = "C:\\pillow-depends\\" pythons = { # for AppVeyor From 83235c4b06d3d5f4f88054ef550c9d4d8d61fa0a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Nov 2019 13:11:56 +1100 Subject: [PATCH 114/186] Enabled page heap verification --- .appveyor.yml | 1 + .github/workflows/test-windows.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index 5352e9c73..d4f2f74e3 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -85,6 +85,7 @@ build_script: test_script: - cd c:\pillow - '%PYTHON%\%PIP_DIR%\pip.exe install pytest pytest-cov' +- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests' #- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 0e368a7c6..c49d89335 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -346,6 +346,7 @@ jobs: rem Add GhostScript and Raqm binaries (copied to INCLIB) to PATH. path %INCLIB%;%PATH% cd /D %GITHUB_WORKSPACE% + c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\python.exe %PYTHON%\python.exe -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests shell: cmd From 579b6cac6045540afdd5903fc1e376e18f5f1ea6 Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Fri, 1 Nov 2019 23:06:51 -0700 Subject: [PATCH 115/186] Report details about Pillow when running tests --- Tests/conftest.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 Tests/conftest.py diff --git a/Tests/conftest.py b/Tests/conftest.py new file mode 100644 index 000000000..b5dc741e3 --- /dev/null +++ b/Tests/conftest.py @@ -0,0 +1,40 @@ +def pytest_report_header(config): + import os + + report = [] + + def append(*args): + report.append(" ".join(args)) + + try: + from PIL import Image, features + + append("-" * 68) + append("Pillow", Image.__version__) + append("-" * 68) + append("Python modules loaded from", os.path.dirname(Image.__file__)) + append("Binary modules loaded from", os.path.dirname(Image.core.__file__)) + append("-" * 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): + append("---", feature, "support ok") + else: + append("***", feature, "support not installed") + append("-" * 68) + except Exception as e: + return "pytest_report_header failed: %s" % str(e) + return "\n".join(report) From 2058e00e3e81e6e298a93d3f209d13568970fa7d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 2 Nov 2019 08:40:29 +0200 Subject: [PATCH 116/186] Update docs/deprecations.rst Co-Authored-By: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/deprecations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 3a48f37ee..144661820 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -82,7 +82,7 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Deprecated: +Previous method: .. code-block:: python From a3d16dd40ac1ae0487ff5467cac29c4c600d0ace Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 2 Nov 2019 08:40:40 +0200 Subject: [PATCH 117/186] Update docs/releasenotes/7.0.0.rst Co-Authored-By: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/releasenotes/7.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 4fb6773c4..a6d4a6d35 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -56,7 +56,7 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Deprecated: +Previous method: .. code-block:: python From b1ee44a74b0552c1d83fe9988687159ad915580b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Nov 2019 18:10:55 +1100 Subject: [PATCH 118/186] Ignore UserWarnings --- Tests/test_file_tiff.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index ccaa91920..146bf1851 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -2,6 +2,7 @@ import logging import os from io import BytesIO +import pytest from PIL import Image, TiffImagePlugin from PIL._util import py3 from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION @@ -594,6 +595,9 @@ class TestFileTiff(PillowTestCase): im.load() self.assertFalse(fp.closed) + # Ignore this UserWarning which triggers for four tags: + # "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..." + @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") def test_string_dimension(self): # Assert that an error is raised if one of the dimensions is a string with self.assertRaises(ValueError): From 47df9c2b0a04583d19e576a98524cadc8dce2c80 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Nov 2019 18:57:40 +1100 Subject: [PATCH 119/186] Disabled heap verification for pypy GHA --- .github/workflows/test-windows.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c49d89335..281d99681 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -339,6 +339,13 @@ jobs: %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% @@ -346,7 +353,6 @@ jobs: rem Add GhostScript and Raqm binaries (copied to INCLIB) to PATH. path %INCLIB%;%PATH% cd /D %GITHUB_WORKSPACE% - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\python.exe %PYTHON%\python.exe -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests shell: cmd From 690bd430b0d1883e2daf960a58cfcf06c12b2f44 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 2 Nov 2019 11:06:58 +0200 Subject: [PATCH 120/186] Update docs/releasenotes/7.0.0.rst Co-Authored-By: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/releasenotes/7.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index a6d4a6d35..48660b506 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -15,7 +15,7 @@ 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 +Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 or PySide2. Setting the size of TIFF images From 6f88d8dd6bf8526c760f311820296833807d18b0 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 2 Nov 2019 20:02:07 +0200 Subject: [PATCH 121/186] black --target-version py35 --- src/PIL/ImageDraw.py | 4 ++-- src/PIL/ImageFilter.py | 2 +- tox.ini | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 3ece20152..c6e12150e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -313,7 +313,7 @@ class ImageDraw: language=language, stroke_width=stroke_width, *args, - **kwargs + **kwargs, ) coord = coord[0] + offset[0], coord[1] + offset[1] except AttributeError: @@ -326,7 +326,7 @@ class ImageDraw: language, stroke_width, *args, - **kwargs + **kwargs, ) except TypeError: mask = font.getmask(text) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 9cb62ad27..6b0f5eb37 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -495,7 +495,7 @@ class Color3DLUT(MultibandFilter): r / (size1D - 1), g / (size2D - 1), b / (size3D - 1), - *values + *values, ) else: values = callback(*values) diff --git a/tox.ini b/tox.ini index 0bf833b81..07d75be64 100644 --- a/tox.ini +++ b/tox.ini @@ -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 From 5d10f8dff2b93914fa0de2e4aff1de1c412cb984 Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Sat, 2 Nov 2019 15:12:52 -0700 Subject: [PATCH 122/186] Use features.pilinfo to report details about Pillow --- Tests/conftest.py | 39 +++++---------------------------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/Tests/conftest.py b/Tests/conftest.py index b5dc741e3..2b796148d 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -1,40 +1,11 @@ def pytest_report_header(config): - import os - - report = [] - - def append(*args): - report.append(" ".join(args)) + import io try: - from PIL import Image, features + from PIL import features - append("-" * 68) - append("Pillow", Image.__version__) - append("-" * 68) - append("Python modules loaded from", os.path.dirname(Image.__file__)) - append("Binary modules loaded from", os.path.dirname(Image.core.__file__)) - append("-" * 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): - append("---", feature, "support ok") - else: - append("***", feature, "support not installed") - append("-" * 68) + 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) - return "\n".join(report) From 58e6d127b7631fcbab6e42263482b229c7316e35 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 3 Nov 2019 16:02:08 +0200 Subject: [PATCH 123/186] In Python 3, 'next' is renamed to '__next__' --- src/PIL/ImageSequence.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 28a54c7b0..4e9f5c210 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -52,9 +52,6 @@ class Iterator: except EOFError: raise StopIteration - def next(self): - return self.__next__() - def all_frames(im, func=None): """ From 4483642e45242b9c5d6da5a5cfbf5d0b8a7fc477 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 3 Nov 2019 07:35:48 -0800 Subject: [PATCH 124/186] Reuse deferred_error instead of _imaging_not_installed deferred_error is a general implementation of _imaging_not_installed. Can reuse rather than repeating the same logic. --- src/PIL/Image.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d65810d8b..42487ba93 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -69,12 +69,6 @@ class DecompressionBombError(Exception): pass -class _imaging_not_installed(object): - # module placeholder - def __getattr__(self, id): - raise ImportError("The _imaging C module is not installed") - - # Limit to around a quarter gigabyte for a 24 bit (3 bpp) image MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3) @@ -95,7 +89,7 @@ try: ) except ImportError as v: - core = _imaging_not_installed() + core = deferred_error(ImportError("The _imaging C module is not installed.")) # Explanations for ways that we know we might have an import error if str(v).startswith("Module use of python"): # The _imaging C module is present, but not compiled for From 24b8501d4b01f80181820aea8ed23472474db79a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 4 Nov 2019 00:32:33 +0200 Subject: [PATCH 125/186] Remove handling for pre-3.3 wide/narrow builds --- src/PIL/Image.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0374d1583..2d73d8f53 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -88,22 +88,6 @@ except ImportError as v: ) elif str(v).startswith("The _imaging extension"): warnings.warn(str(v), RuntimeWarning) - elif "Symbol not found: _PyUnicodeUCS2_" in str(v): - # should match _PyUnicodeUCS2_FromString and - # _PyUnicodeUCS2_AsLatin1String - warnings.warn( - "The _imaging extension was built for Python with UCS2 support; " - "recompile Pillow or build Python --without-wide-unicode. ", - RuntimeWarning, - ) - elif "Symbol not found: _PyUnicodeUCS4_" in str(v): - # should match _PyUnicodeUCS4_FromString and - # _PyUnicodeUCS4_AsLatin1String - warnings.warn( - "The _imaging extension was built for Python with UCS4 support; " - "recompile Pillow or build Python --with-wide-unicode. ", - RuntimeWarning, - ) # Fail here anyway. Don't let people run with a mostly broken Pillow. # see docs/porting.rst raise From 09ea81385aca79088ecf1c6d3b2d8c207cd597f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 4 Nov 2019 19:07:47 +1100 Subject: [PATCH 126/186] Updated libtiff to 4.1.0 --- .github/workflows/test-windows.yml | 2 +- winbuild/config.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 0e368a7c6..216bd26f3 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -138,7 +138,7 @@ jobs: 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.0.10 + cd /D %BUILD%\tiff-4.1.0 call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 echo on copy %GITHUB_WORKSPACE%\winbuild\tiff.opt nmake.opt diff --git a/winbuild/config.py b/winbuild/config.py index 1c5ee6c28..c909b436f 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -36,9 +36,9 @@ libs = { "dir": "jpeg-9c", }, "tiff": { - "url": "ftp://download.osgeo.org/libtiff/tiff-4.0.10.tar.gz", - "filename": "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 From 9a247bd59719b0fbcfa4cc6545fbe5d9ab5611ea Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 4 Nov 2019 22:55:50 +0200 Subject: [PATCH 127/186] Test Python 3.8 on GHA --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f72342c5c..195ea3865 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7"] + python-version: ["3.8"] name: Python ${{ matrix.python-version }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6676ace7..1b5943c7d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,9 +14,10 @@ jobs: ] python-version: [ "pypy3", + "3.8", "3.7", - "3.5", "3.6", + "3.5", ] include: - python-version: "3.5" @@ -53,7 +54,7 @@ jobs: .travis/test.sh - name: Docs - if: matrix.python-version == 3.7 + if: matrix.python-version == 3.8 run: | pip install sphinx-rtd-theme make doccheck From 0c5895470cf6b02aaa42bec77414781bfb0999e2 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 5 Nov 2019 17:00:34 +0200 Subject: [PATCH 128/186] Remove deprecated __version__ from plugins --- src/PIL/BmpImagePlugin.py | 4 ---- src/PIL/CurImagePlugin.py | 4 ---- src/PIL/DcxImagePlugin.py | 4 ---- src/PIL/EpsImagePlugin.py | 4 ---- src/PIL/FliImagePlugin.py | 5 ----- src/PIL/FpxImagePlugin.py | 4 ---- src/PIL/GdImageFile.py | 5 ----- src/PIL/GifImagePlugin.py | 5 ----- src/PIL/IcoImagePlugin.py | 4 ---- src/PIL/ImImagePlugin.py | 5 ----- src/PIL/ImtImagePlugin.py | 5 ----- src/PIL/IptcImagePlugin.py | 4 ---- src/PIL/Jpeg2KImagePlugin.py | 4 ---- src/PIL/JpegImagePlugin.py | 5 ----- src/PIL/McIdasImagePlugin.py | 4 ---- src/PIL/MicImagePlugin.py | 5 ----- src/PIL/MpegImagePlugin.py | 5 ----- src/PIL/MpoImagePlugin.py | 4 ---- src/PIL/MspImagePlugin.py | 5 ----- src/PIL/PalmImagePlugin.py | 4 ---- src/PIL/PcdImagePlugin.py | 5 ----- src/PIL/PcxImagePlugin.py | 4 ---- src/PIL/PdfImagePlugin.py | 9 ++------- src/PIL/PixarImagePlugin.py | 5 ----- src/PIL/PngImagePlugin.py | 4 ---- src/PIL/PpmImagePlugin.py | 4 ---- src/PIL/PsdImagePlugin.py | 4 ---- src/PIL/SgiImagePlugin.py | 4 ---- src/PIL/SunImagePlugin.py | 4 ---- src/PIL/TgaImagePlugin.py | 5 ----- src/PIL/TiffImagePlugin.py | 3 --- src/PIL/WmfImagePlugin.py | 4 ---- src/PIL/XVThumbImagePlugin.py | 4 ---- src/PIL/XbmImagePlugin.py | 4 ---- src/PIL/XpmImagePlugin.py | 4 ---- 35 files changed, 2 insertions(+), 154 deletions(-) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 8426e2497..e9c2afd10 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -27,10 +27,6 @@ from . import Image, ImageFile, ImagePalette from ._binary import i8, i16le as i16, i32le as i32, o8, o16le as o16, o32le as o32 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.7" - # # -------------------------------------------------------------------- # Read BMP file diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index 9e2d8c96f..50bf1a356 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -21,10 +21,6 @@ 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/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 721ad4c21..37cba79c3 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -30,10 +30,6 @@ import tempfile from . import Image, ImageFile from ._binary import i32le as i32 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.5" - # # -------------------------------------------------------------------- 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/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 15ebe0e3b..dfb509e15 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -22,10 +22,6 @@ 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 diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 2d492358c..80235bba6 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -26,11 +26,6 @@ from . import ImageFile, ImagePalette from ._binary import i8, i16be as i16, i32be as i32 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - - ## # Image plugin for the GD uncompressed format. Note that this format # is not supported by the standard Image.open function. To use diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index a92c142f0..ec0f1ee71 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,11 +31,6 @@ 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 diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 148e604f8..2e4ad1d65 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" - # # -------------------------------------------------------------------- diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 77127fae6..12c9237f0 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -31,11 +31,6 @@ import re from . import Image, ImageFile, ImagePalette from ._binary import i8 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.7" - - # -------------------------------------------------------------------- # Standard tags 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..cb39aab83 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -23,10 +23,6 @@ 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 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 da0759129..42ac025b3 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -47,11 +47,6 @@ 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 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..5396b2014 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" - - # # -------------------------------------------------------------------- diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 9c662fcc2..5988c5be2 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -17,11 +17,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" - - # # Bitstream parser diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 938f2a5a6..a63e76fe7 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) diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 7315ab66e..21e71e78b 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 diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index dd068d794..62f13f535 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), 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/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 397af8c10..6768a38f9 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] diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 1fd40f5ba..18de6f5a7 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 " + __version__) # # pages 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 84317ceb7..62a1d1a1e 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -40,10 +40,6 @@ 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__) is_cid = re.compile(br"\w\w\w\w").match diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index c3e9eed6d..67c672485 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" - # # -------------------------------------------------------------------- diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index f72ad5f44..cf1b4d94d 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 diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 99408fdc3..8b2268104 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -29,10 +29,6 @@ from . import Image, ImageFile from ._binary import i8, i16be as i16, o8 from ._util import py3 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.3" - def _accept(prefix): return len(prefix) >= 2 and i16(prefix) == 474 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/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index b1b351396..40cadbb6b 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 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 7f596b9ed..10faa7702 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -64,9 +64,6 @@ except ImportError: 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. diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 416af6fd7..151c48ae2 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -25,10 +25,6 @@ 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: 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..4ca101701 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]+" 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]*)') From 09e48ae768fb819c9e3c5ba83b455a8a9e3b9f16 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 5 Nov 2019 17:00:51 +0200 Subject: [PATCH 129/186] Remove deprecated __version__ from plugins --- docs/deprecations.rst | 50 +++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 144661820..db4219528 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -22,31 +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. -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__`` -=============================== ================================= ================================== - ImageCms.CmsProfile attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -103,6 +78,31 @@ PILLOW_VERSION constant ``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. + +=============================== ================================= ================================== +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__`` +=============================== ================================= ================================== + PyQt4 and PySide ~~~~~~~~~~~~~~~~ From 15ee91761bc0250fd7e43747312870d1edc929a9 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 5 Nov 2019 22:04:04 +0200 Subject: [PATCH 130/186] Remove deprecated __version__ from plugins --- docs/deprecations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index db4219528..a9b534dca 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -87,7 +87,7 @@ The version constants of individual plugins have been removed. Use ``PIL.__versi instead. =============================== ================================= ================================== -Deprecated Deprecated Deprecated +Removed Removed Removed =============================== ================================= ================================== ``BmpImagePlugin.__version__`` ``Jpeg2KImagePlugin.__version__`` ``PngImagePlugin.__version__`` ``CurImagePlugin.__version__`` ``JpegImagePlugin.__version__`` ``PpmImagePlugin.__version__`` From f273da1b3ef59e16da9c075f2dc77ff46e02857e Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 5 Nov 2019 22:06:57 +0200 Subject: [PATCH 131/186] Remove deprecated __version__ from plugins --- docs/releasenotes/7.0.0.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 48660b506..374a8d2dd 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -9,6 +9,29 @@ 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 ^^^^^^^^^^^^^^^^ From d79f2bb7dc9a49262b01077398dc95d92cffac9b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 Nov 2019 20:12:19 +1100 Subject: [PATCH 132/186] Corrected context manager test --- Tests/test_file_psd.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index f57deff6a..939485f67 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -36,9 +36,8 @@ class TestImagePsd(PillowTestCase): def test_context_manager(self): def open(): - im = Image.open(test_file) - im.load() - im.close() + with Image.open(test_file) as im: + im.load() self.assert_warning(None, open) From f4dbc0a664ba4442391f4ca9216bf5ee78aa4125 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 Nov 2019 20:44:43 +1100 Subject: [PATCH 133/186] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 816e0a4f1..70526ac55 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 7.0.0 (unreleased) ------------------ +- Improve handling of file resources #3577 + [jdufresne] + - Removed CI testing of Fedora 29 #4165 [hugovk] From 0c46177804a62e5d146e27077d64ecb77e8e1836 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 6 Nov 2019 18:27:06 +0200 Subject: [PATCH 134/186] Test on latest available Ubuntu and macOS --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b5943c7d..cab5b642a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,8 +9,8 @@ jobs: fail-fast: false matrix: os: [ - "ubuntu-18.04", - "macOS-10.14", + "ubuntu-latest", + "macOS-latest", ] python-version: [ "pypy3", From b3b7ff6d9ee9623d0e46a59732120a8b3b085d1d Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 2 Nov 2019 21:01:26 +0000 Subject: [PATCH 135/186] Update GHA to Windows Server 2019 --- .github/workflows/test-windows.yml | 44 ++++++++++++++++-------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 530423096..076fbddde 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -5,7 +5,7 @@ on: [push, pull_request] jobs: build: - runs-on: windows-2016 + runs-on: windows-2019 strategy: fail-fast: false matrix: @@ -89,7 +89,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + 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 @@ -105,7 +105,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.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% -DENABLE_SHARED:BOOL=OFF -DWITH_JPEG8:BOOL=TRUE -DWITH_CRT_DLL:BOOL=TRUE -DCMAKE_BUILD_TYPE=Release @@ -124,7 +124,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + 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 @@ -139,7 +139,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + 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 @@ -155,7 +155,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + 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 @@ -172,12 +172,12 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.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=v140 - set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\VC\VCTargets - set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" + 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% @@ -185,17 +185,18 @@ jobs: shell: cmd - name: Build dependencies / LCMS2 + if: false 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + 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\2017\Enterprise\Common7\IDE\VC\VCTargets - set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" + 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 Projects\VC2015\lcms2_static\lcms2_static.vcxproj) -replace 'MultiThreaded<', 'MultiThreadedDLL<' | Out-File -encoding ASCII Projects\VC2015\lcms2_static\lcms2_static.vcxproj" %MSBUILD% Projects\VC2015\lcms2.sln /t:Clean;lcms2_static /p:Configuration="Release" /p:Platform=${{ matrix.platform-msbuild }} /m xcopy /Y /E /Q include %INCLIB% @@ -208,7 +209,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.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% -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF @@ -230,7 +231,7 @@ jobs: set BUILD=%GITHUB_WORKSPACE%\winbuild\build rem ba653c8: Merge tag '2.12.5' into msvc cd /D %BUILD%\libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971 - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + 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 @@ -254,7 +255,7 @@ jobs: set INCLUDE=%INCLUDE%;%INCLIB% set LIB=%LIB%;%INCLIB% cd /D %BUILD%\harfbuzz-2.6.1 - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.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 @@ -273,7 +274,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + 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 @@ -295,7 +296,7 @@ jobs: set INCLUDE=%INCLUDE%;%INCLIB% set LIB=%LIB%;%INCLIB% cd /D %BUILD%\libraqm-0.7.0 - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + 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 @@ -308,12 +309,13 @@ jobs: shell: cmd - name: Build dependencies / ghostscript + if: false 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%\ghostscript-9.27 - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} echo on set MSVC_VERSION=14 set RCOMP="C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\RC.Exe" @@ -332,7 +334,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} %PYTHON%\python.exe setup.py build_ext install rem Add GhostScript and Raqm binaries (copied to INCLIB) to PATH. path %INCLIB%;%PATH% @@ -373,7 +375,7 @@ jobs: 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 14.0\VC\vcvarsall.bat" ${{ matrix.platform-vcvars }} 8.1 + 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 From 907ce50ba2f4fb3030747dbbd47ae3d580ecaf86 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 7 Nov 2019 17:26:04 +0000 Subject: [PATCH 136/186] override distutils msvc search --- .github/workflows/test-windows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 076fbddde..6419e0517 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -335,6 +335,8 @@ jobs: 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 DISTUTILS_USE_SDK=1 + set py_vcruntime_redist=true %PYTHON%\python.exe setup.py build_ext install rem Add GhostScript and Raqm binaries (copied to INCLIB) to PATH. path %INCLIB%;%PATH% From 641383f1e529e45a4b02ced7466d1d9c6955ebf9 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 7 Nov 2019 20:58:12 +0000 Subject: [PATCH 137/186] add lcms2 bulid script upgrade patch to gha --- .github/workflows/test-windows.yml | 3 +-- winbuild/lcms2_patch.ps1 | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 winbuild/lcms2_patch.ps1 diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6419e0517..cb712562b 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -185,7 +185,6 @@ jobs: shell: cmd - name: Build dependencies / LCMS2 - if: false run: | set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 @@ -197,7 +196,7 @@ jobs: 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 -Command "(gc Projects\VC2015\lcms2_static\lcms2_static.vcxproj) -replace 'MultiThreaded<', 'MultiThreadedDLL<' | Out-File -encoding ASCII Projects\VC2015\lcms2_static\lcms2_static.vcxproj" + 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% 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")) + } From 2d1192933397a5f3992c32dee612e33b8d765e62 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 7 Nov 2019 22:02:04 +0000 Subject: [PATCH 138/186] fix pypy3 pip on gha --- .github/workflows/test-windows.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index cb712562b..3da8baa64 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -43,6 +43,7 @@ jobs: 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 From 3c1f90efce54d7aaada8e88ce6d8001f9c00b4cc Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 7 Nov 2019 22:23:59 +0000 Subject: [PATCH 139/186] use gs binary on gha --- .github/workflows/test-windows.yml | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 3da8baa64..5755e089e 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -68,6 +68,12 @@ jobs: Write-Host "`#`#[add-path]$env:RUNNER_WORKSPACE\nasm-2.14.02" Write-Host "::add-path::$env:RUNNER_WORKSPACE\nasm-2.14.02" + # 32-bit should work on both platforms + curl -fsSL -o gs950.exe https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs950/gs950w32.exe + ./gs950.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 curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip 7z x pillow-depends.zip -oc:\ @@ -308,23 +314,6 @@ jobs: copy /Y /B libraqm.dll %INCLIB% shell: cmd - - name: Build dependencies / ghostscript - if: false - 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%\ghostscript-9.27 - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} - echo on - set MSVC_VERSION=14 - set RCOMP="C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\RC.Exe" - if "${{ matrix.architecture }}"=="x64" set WIN64="" - nmake -nologo -f psi\msvc.mak - rem Add bin to PATH variable: Copy to INCLIB, then add INCLIB to PATH in Test step. - copy /Y /B bin\* %INCLIB% - shell: cmd - - name: Build Pillow run: | set PYTHON=%pythonLocation% @@ -338,7 +327,7 @@ jobs: set DISTUTILS_USE_SDK=1 set py_vcruntime_redist=true %PYTHON%\python.exe setup.py build_ext install - rem Add GhostScript and Raqm binaries (copied to INCLIB) to PATH. + rem Add libraqm.dll (copied to INCLIB) to PATH. path %INCLIB%;%PATH% %PYTHON%\python.exe selftest.py --installed shell: cmd @@ -354,7 +343,7 @@ jobs: run: | set PYTHON=%pythonLocation% set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 - rem Add GhostScript and Raqm binaries (copied to INCLIB) to PATH. + rem Add libraqm.dll (copied to INCLIB) to PATH. path %INCLIB%;%PATH% cd /D %GITHUB_WORKSPACE% %PYTHON%\python.exe -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests From c62b5f90442afbdaf69d15bfa845ca86914af873 Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 8 Nov 2019 15:10:33 +0000 Subject: [PATCH 140/186] temporarily remove PyPy from GHA testing --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 5755e089e..50aff6bfe 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.5", "3.6", "3.7", "pypy3.6"] + python-version: ["3.5", "3.6", "3.7"] architecture: ["x86", "x64"] include: - architecture: "x86" From b5e3ef8540ffdce26d07dfa2b7ded6e424f06155 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 9 Nov 2019 10:09:16 +0200 Subject: [PATCH 141/186] Test on Python 3.8 --- .appveyor.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index d4f2f74e3..8ab594134 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,7 +13,8 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:\Python38rc1-x64 + - PYTHON: C:/Python38 + - PYTHON: C:/Python38-x64 - PYTHON: C:/Python37 - PYTHON: C:/Python37-x64 - PYTHON: C:/Python36 @@ -32,11 +33,6 @@ environment: install: -- ps: | - if ($env:PYTHON -eq "C:\Python38rc1-x64") { - curl -o install_python.ps1 https://raw.githubusercontent.com/matthew-brett/multibuild/d0cf77e62028704875073e3dc4626f61d1c33b0e/install_python.ps1 - .\install_python.ps1 - } - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip - 7z x pillow-depends.zip -oc:\ - mv c:\pillow-depends-master c:\pillow-depends From 9eb5cb8f25dbcddbad3d5ee0c753b969983a94af Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 9 Nov 2019 10:13:57 +0200 Subject: [PATCH 142/186] Test on Python 3.8 --- winbuild/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/config.py b/winbuild/config.py index c909b436f..c76d9d5bf 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -10,7 +10,7 @@ pythons = { "36": {"compiler": 7.1, "vc": 2015}, "pypy3": {"compiler": 7.1, "vc": 2015}, "37": {"compiler": 7.1, "vc": 2015}, - "38rc1-x64": {"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}, From 01b0dbd4df530087469ab1569f7dd4b7c55280b5 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 11 Nov 2019 16:00:17 +0200 Subject: [PATCH 143/186] Include Pillow version in Windows wheel warning --- setup.py | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/setup.py b/setup.py index d54e30dfa..d2f376d44 100755 --- a/setup.py +++ b/setup.py @@ -24,11 +24,31 @@ from setuptools import Extension, setup # comment this out to disable multi threaded builds. import mp_compile + +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 yet support Python {}.{} and does not yet 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, ) @@ -233,24 +253,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") From 3007f95c05e82bb5604ed69aba3bf8548e2ab259 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 11 Nov 2019 21:39:47 +0200 Subject: [PATCH 144/186] Update wording Co-Authored-By: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d2f376d44..02d2760c3 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ 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" + "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 ), From 9fd629efdbc4ce415267b9cd5ea58bd639478509 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 12 Nov 2019 21:16:36 +1100 Subject: [PATCH 145/186] Updated URL --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 38cf61f62..dd77f3278 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: From e57a5d85a49279b939d020e72efa0c9f566e6111 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 14 Nov 2019 20:01:38 +1100 Subject: [PATCH 146/186] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 70526ac55..eb96600ed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,18 @@ Changelog (Pillow) 7.0.0 (unreleased) ------------------ +- 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 + [radarhere] + - Improve handling of file resources #3577 [jdufresne] From 6213a701070e199cc9fa5028f4e4917163cb0090 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 15 Nov 2019 10:25:08 +0200 Subject: [PATCH 147/186] Improve wording Co-Authored-By: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 02d2760c3..2f18a0048 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ ZLIB_ROOT = None if sys.platform == "win32" and sys.version_info >= (3, 9): warnings.warn( - "Pillow {} does not support Python {}.{} and does not provide prebuilt" + "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 ), From 4eccafc5da8d9043a1f8363af25ed145b0cc4a27 Mon Sep 17 00:00:00 2001 From: Andriy Orehov Date: Fri, 15 Nov 2019 14:00:18 +0200 Subject: [PATCH 148/186] Add Documentation and Github URLs for PyPi --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2f18a0048..ce3fd5860 100755 --- a/setup.py +++ b/setup.py @@ -859,7 +859,11 @@ 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/en/stable/", + "Source Code": "https://github.com/python-pillow/Pillow", + }, classifiers=[ "Development Status :: 6 - Mature", "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", # noqa: E501 From 38ea1c6e03496b0d7a63848f471a39e57a116bf4 Mon Sep 17 00:00:00 2001 From: Andriy Orehov Date: Fri, 15 Nov 2019 18:25:20 +0200 Subject: [PATCH 149/186] use the more common option change docs url --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ce3fd5860..89641078e 100755 --- a/setup.py +++ b/setup.py @@ -861,8 +861,8 @@ try: author_email="aclark@python-pillow.org", url="https://python-pillow.org/", project_urls={ - "Documentation": "https://pillow.readthedocs.io/en/stable/", - "Source Code": "https://github.com/python-pillow/Pillow", + "Documentation": "https://pillow.readthedocs.io", + "Source": "https://github.com/python-pillow/Pillow", }, classifiers=[ "Development Status :: 6 - Mature", From defe8389177e181dbd3ec96eaed544bec077ac16 Mon Sep 17 00:00:00 2001 From: Andriy Orehov Date: Fri, 15 Nov 2019 22:48:26 +0200 Subject: [PATCH 150/186] add in a funding link for PyPi --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 89641078e..339132ff3 100755 --- a/setup.py +++ b/setup.py @@ -859,10 +859,11 @@ try: license="HPND", author="Alex Clark (PIL Fork Author)", author_email="aclark@python-pillow.org", - url="https://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", From 197fb91574310ce800419c8b7112ff4cc0cee27f Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 12 Oct 2019 11:59:03 +0100 Subject: [PATCH 151/186] upload image errors to GitHub Actions --- .github/workflows/test-windows.yml | 7 +++++++ .github/workflows/test.yml | 7 +++++++ Tests/helper.py | 18 ++++++++++++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 50aff6bfe..075707bf6 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -349,6 +349,13 @@ jobs: %PYTHON%\python.exe -m pytest -vx --cov PIL --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: Upload coverage run: 'codecov --file "%GITHUB_WORKSPACE%\coverage.xml" --name "%pythonLocation%"' shell: cmd diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cab5b642a..163250248 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,6 +53,13 @@ jobs: run: | .travis/test.sh + - name: Upload errors + uses: actions/upload-artifact@v1 + if: failure() + with: + name: errors + path: Tests/errors + - name: Docs if: matrix.python-version == 3.8 run: | diff --git a/Tests/helper.py b/Tests/helper.py index 93f6c6dda..57bbab0c9 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -24,12 +24,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 From b0d9fe6ce3bf9108a820d4a443292cecfc9e60d0 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 4 Nov 2019 09:06:09 +0200 Subject: [PATCH 152/186] Cache pip --- .github/workflows/test.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 163250248..9288553d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,20 @@ jobs: steps: - uses: actions/checkout@v1 + - name: Ubuntu cache + uses: actions/cache@preview + if: startsWith(matrix.os, 'ubuntu') + with: + path: ~/.cache/pip + key: ${{ github.workflow }}-${{ matrix.os }}-${{ matrix.python-version }} + + - name: macOS cache + uses: actions/cache@preview + if: startsWith(matrix.os, 'macOS') + with: + path: ~/Library/Caches/pip + key: ${{ github.workflow }}-${{ matrix.os }}-${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: From 45497c33b893212d71c1dfed3874e0868ff23050 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 17 Nov 2019 13:24:37 +0200 Subject: [PATCH 153/186] Add hashFiles to cache key, and add restore-keys --- .github/workflows/test.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9288553d9..68d2731bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,18 +31,24 @@ jobs: - uses: actions/checkout@v1 - name: Ubuntu cache - uses: actions/cache@preview + uses: actions/cache@v1 if: startsWith(matrix.os, 'ubuntu') with: path: ~/.cache/pip - key: ${{ github.workflow }}-${{ matrix.os }}-${{ matrix.python-version }} + key: + ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/.travis/*.sh') }} + restore-keys: | + ${{ matrix.os }}-${{ matrix.python-version }}- - name: macOS cache - uses: actions/cache@preview + uses: actions/cache@v1 if: startsWith(matrix.os, 'macOS') with: path: ~/Library/Caches/pip - key: ${{ github.workflow }}-${{ matrix.os }}-${{ matrix.python-version }} + 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 From 324f4856fabd42198547b0c131fdd9bcdcf05dc3 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 17 Nov 2019 13:51:42 +0200 Subject: [PATCH 154/186] Cache pip --- .github/workflows/test-windows.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 075707bf6..61dab9d6b 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -31,6 +31,15 @@ jobs: steps: - uses: actions/checkout@v1 + - name: Cache + uses: actions/cache@v1 + with: + path: ~\AppData\Local\pip\Cache + key: + ${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/.github/workflows/test-windows.yml') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.python-version }}- + - name: Install PyPy if: "contains(matrix.python-version, 'pypy')" run: | From f81c829e041b14f94750434aa609ba8b9ff9354f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 18 Nov 2019 12:07:02 +0200 Subject: [PATCH 155/186] Update word order Co-Authored-By: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/PdfImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 18de6f5a7..d9bbf6fab 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -77,7 +77,7 @@ def _save(im, fp, filename, save_all=False): existing_pdf.start_writing() existing_pdf.write_header() - existing_pdf.write_comment("created by Pillow PDF driver " + __version__) + existing_pdf.write_comment("created by Pillow {} PDF driver".format(__version__)) # # pages From 1c9275c2498bb84bd3f659de4ddd18f17b214067 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 19 Nov 2019 20:04:38 +1100 Subject: [PATCH 156/186] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index eb96600ed..9952c1166 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 7.0.0 (unreleased) ------------------ +- Remove deprecated __version__ from plugins #4197 + [hugovk, radarhere] + - Fixed freeing unallocated pointer when resizing with height too large #4116 [radarhere] From e05ef3fef37846316488f0360577a7a5f485ea6f Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 19 Nov 2019 11:55:49 +0200 Subject: [PATCH 157/186] Include architecture in cache key --- .github/workflows/test-windows.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 61dab9d6b..aa4761f68 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -36,8 +36,9 @@ jobs: with: path: ~\AppData\Local\pip\Cache key: - ${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/.github/workflows/test-windows.yml') }} + ${{ 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 From 40f891dfd70b484f7257fee455bd96f82f61d4e3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 19 Nov 2019 21:20:02 +1100 Subject: [PATCH 158/186] Added UnidentifiedImageError --- Tests/test_file_gd.py | 4 ++-- Tests/test_image.py | 7 +++++-- docs/releasenotes/7.0.0.rst | 8 ++++---- src/PIL/GdImageFile.py | 4 ++-- src/PIL/Image.py | 6 ++++-- src/PIL/__init__.py | 4 ++++ 6 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index 9208dcbff..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 @@ -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_image.py b/Tests/test_image.py index b494b17dc..90bf9d1cf 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -2,7 +2,7 @@ import os import shutil import tempfile -from PIL import Image +from PIL import Image, UnidentifiedImageError from PIL._util import py3 from .helper import PillowTestCase, hopper, is_win32, unittest @@ -48,6 +48,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)) @@ -88,7 +91,7 @@ class TestImage(PillowTestCase): import StringIO im = StringIO.StringIO("") - self.assertRaises(IOError, Image.open, im) + self.assertRaises(UnidentifiedImageError, Image.open, im) def test_bad_mode(self): self.assertRaises(ValueError, Image.open, "filename", "bad mode") diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 374a8d2dd..6d64cc7f9 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -63,11 +63,11 @@ TODO API Additions ============= -TODO -^^^^ - -TODO +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. Other Changes ============= diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 80235bba6..54c88712c 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -23,7 +23,7 @@ # purposes only. -from . import ImageFile, ImagePalette +from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i8, i16be as i16, i32be as i32 ## @@ -82,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/Image.py b/src/PIL/Image.py index f50b319d2..b04b4af93 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -38,7 +38,7 @@ import warnings # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 7.0.0. # Use __version__ instead. -from . import ImageMode, TiffTags, __version__, _plugins +from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins from ._binary import i8, i32le from ._util import deferred_error, isPath, isStringType, py3 @@ -2794,7 +2794,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) + ) # diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index fd02b40d9..e7f26488d 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -70,3 +70,7 @@ _plugins = [ "XpmImagePlugin", "XVThumbImagePlugin", ] + + +class UnidentifiedImageError(IOError): + pass From 699183c5dc63cc983c7656b307cf4b8fac726942 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 19 Nov 2019 21:41:40 +1100 Subject: [PATCH 159/186] Highlighted classes Co-Authored-By: Hugo van Kemenade --- docs/releasenotes/7.0.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 6d64cc7f9..386a26757 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -66,8 +66,8 @@ 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. +Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be +identified. For backwards compatibility, this will inherit from ``IOError``. Other Changes ============= From 1ff3a501ef31552e1ef279b8554655977f1c2e0e Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 20 Nov 2019 14:00:59 +0200 Subject: [PATCH 160/186] Use Codecov's GitHub action --- .codecov.yml | 2 -- .github/workflows/test-windows.yml | 10 ++++++---- .github/workflows/test.yml | 8 ++++++++ .travis/after_success.sh | 11 +++++++++-- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index a93095486..3e147d151 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -6,6 +6,4 @@ codecov: # https://docs.codecov.io/v4.3.6/docs/comparing-commits allow_coverage_offsets: true - token: 6dafc396-e7f5-4221-a38a-8b07a49fbdae - comment: off diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 075707bf6..30f302d20 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -55,10 +55,9 @@ jobs: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.architecture }} - - name: pip install wheel pytest pytest-cov codecov + - name: pip install wheel pytest pytest-cov run: | "%pythonLocation%\python.exe" -m pip install wheel pytest pytest-cov - pip install codecov shell: cmd - name: Fetch dependencies @@ -357,8 +356,11 @@ jobs: path: Tests/errors - name: Upload coverage - run: 'codecov --file "%GITHUB_WORKSPACE%\coverage.xml" --name "%pythonLocation%"' - shell: cmd + if: success() + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: ${{ runner.os }} Python ${{ matrix.python-version }} - name: Build wheel id: wheel diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 163250248..580aff824 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,5 +71,13 @@ jobs: run: | .travis/after_success.sh env: + MATRIX_OS: ${{ matrix.os }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + + - 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/after_success.sh b/.travis/after_success.sh index 721a469b6..ec4f027b4 100755 --- a/.travis/after_success.sh +++ b/.travis/after_success.sh @@ -1,7 +1,12 @@ #!/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 @@ -13,7 +18,9 @@ coverage report pip install codecov pip install coveralls-merge coveralls-merge coverage.c.json -codecov +if [[ $TRAVIS ]]; then + codecov +fi if [ "$TRAVIS_PYTHON_VERSION" == "3.7" ] && [ "$DOCKER" == "" ]; then # Coverage and quality reports on just the latest diff. From a1c6b5edf738f834cac268587eadf4c0d55df2e4 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 22 Oct 2019 16:37:04 +0300 Subject: [PATCH 161/186] Cover tests https://nedbatchelder.com/blog/201908/dont_omit_tests_from_coverage.html --- .appveyor.yml | 2 +- .github/workflows/test-windows.yml | 2 +- .travis/test.sh | 2 +- Tests/README.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 3183c3d1b..edfc75101 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -77,7 +77,7 @@ test_script: - cd c:\pillow - '%PYTHON%\%PIP_DIR%\pip.exe install pytest pytest-cov' - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% -- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests' +- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' #- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? after_test: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 075707bf6..05a04ca02 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -346,7 +346,7 @@ jobs: rem Add libraqm.dll (copied to INCLIB) to PATH. path %INCLIB%;%PATH% cd /D %GITHUB_WORKSPACE% - %PYTHON%\python.exe -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests + %PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests shell: cmd - name: Upload errors diff --git a/.travis/test.sh b/.travis/test.sh index a65e620b8..91dae9f8d 100755 --- a/.travis/test.sh +++ b/.travis/test.sh @@ -2,7 +2,7 @@ set -e -python -m pytest -v -x --cov PIL --cov-report term Tests +python -m pytest -v -x --cov PIL --cov Tests --cov-report term Tests # Docs if [ "$TRAVIS_PYTHON_VERSION" == "3.7" ]; then make doccheck; fi 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 From cccc535f66a306059f919dbb1f64547adc152490 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 27 Oct 2019 12:20:54 +0200 Subject: [PATCH 162/186] Omit Tests/check_ files from coverage --- .coveragerc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.coveragerc b/.coveragerc index ea79190ae..e93a2a426 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,3 +12,7 @@ exclude_lines = # Don't complain about debug code if Image.DEBUG: if DEBUG: + +[run] +omit = + Tests/check_*.py From b33df562b8a212bf499ee304efc847bc4d476503 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 27 Oct 2019 18:23:28 +0200 Subject: [PATCH 163/186] Matches 'omit:' from .coveragerc --- .codecov.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.codecov.yml b/.codecov.yml index a93095486..033786a49 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -9,3 +9,7 @@ codecov: token: 6dafc396-e7f5-4221-a38a-8b07a49fbdae comment: off + +# Matches 'omit:' in .coveragerc +ignore: + - "Tests/check_*.py" From 106fc4085f19c4c40a7c482d5c57a4fb86143e71 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 18 Nov 2019 13:48:37 +0200 Subject: [PATCH 164/186] Remove redundant files --- Tests/bench_get.py | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 Tests/bench_get.py 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") From 837d8ae984e5959633483191376b70d652e89379 Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 13 Oct 2019 00:11:48 +0100 Subject: [PATCH 165/186] fix support for extended unicode characters in PyPy --- Tests/test_imagefont.py | 2 +- src/_imagingft.c | 14 +------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index b7cfefacf..1c44e5221 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -463,7 +463,7 @@ class TestImageFont(PillowTestCase): with self.assertRaises(UnicodeEncodeError): font.getsize("’") - @unittest.skipIf(is_pypy(), "requires CPython") + @unittest.skipIf(is_pypy(), "failing on PyPy") def test_unicode_extended(self): # issue #3777 text = "A\u278A\U0001F12B" diff --git a/src/_imagingft.c b/src/_imagingft.c index 0a9c00a91..d89f9a758 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -326,24 +326,12 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) static int font_getchar(PyObject* string, int index, FT_ULong* char_out) { -#if (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; - } -#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; } @@ -363,7 +351,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * goto failed; } -#if (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); From 64317f8885b171f16938a6e8306ab068a8a97ce4 Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 15 Oct 2019 21:58:50 +0100 Subject: [PATCH 166/186] raqm now works with PyPy on Windows --- .github/workflows/test-windows.yml | 4 ---- src/_imagingft.c | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 075707bf6..af89d2d43 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -253,7 +253,6 @@ jobs: # for Raqm - name: Build dependencies / HarfBuzz - if: "!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 @@ -274,7 +273,6 @@ jobs: # for Raqm - name: Build dependencies / FriBidi - if: "!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 @@ -292,9 +290,7 @@ jobs: copy /Y /B *.lib %INCLIB% shell: cmd - # failing with PyPy3 - name: Build dependencies / Raqm - if: "!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 diff --git a/src/_imagingft.c b/src/_imagingft.c index d89f9a758..62a4c283e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -380,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"); From b0688d9aed1bc8d83ef33dc0cbf8287cd2733fa1 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 20 Nov 2019 17:13:18 +0000 Subject: [PATCH 167/186] Use prebuilt GhostScript on AppVeyor --- .appveyor.yml | 3 +++ winbuild/build_dep.py | 28 ---------------------------- winbuild/config.py | 5 ----- 3 files changed, 3 insertions(+), 33 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 3183c3d1b..9ec837996 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -56,6 +56,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: | diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index c2ed77ca0..5762de5e5 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -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 -nologo -f psi/msvc.mak -copy /Y /B bin\ C:\Python27\ -endlocal -""" - return script % compiler - - def add_compiler(compiler, bit): script.append(setup_compiler(compiler)) script.append(nmake_libs(compiler, bit)) @@ -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 826c6183c..0ecfaddeb 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -53,11 +53,6 @@ libs = { "filename": "lcms2-2.8.zip", "dir": "lcms2-2.8", }, - "ghostscript": { - "url": "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs927/ghostscript-9.27.tar.gz", # noqa: E501 - "filename": "ghostscript-9.27.tar.gz", - "dir": "ghostscript-9.27", - }, "tcl-8.5": { "url": SF_MIRROR + "/project/tcl/Tcl/8.5.19/tcl8519-src.zip", "filename": "tcl8519-src.zip", From fa40817efbbb7555c55210562e3304933a607c5e Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 20 Nov 2019 19:22:38 +0200 Subject: [PATCH 168/186] GHA: Install lcov on macOS to report coverage --- .github/workflows/test.yml | 1 + .travis/after_success.sh | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 163250248..06caa4316 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,5 +71,6 @@ jobs: run: | .travis/after_success.sh env: + MATRIX_OS: ${{ matrix.os }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} diff --git a/.travis/after_success.sh b/.travis/after_success.sh index 721a469b6..badf04484 100755 --- a/.travis/after_success.sh +++ b/.travis/after_success.sh @@ -1,7 +1,12 @@ #!/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 From d870f1ac2ee8de715c11cf7898e511e1861dbb18 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 20 Nov 2019 19:52:32 +0200 Subject: [PATCH 169/186] Revert --- .codecov.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.codecov.yml b/.codecov.yml index 3e147d151..a93095486 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -6,4 +6,6 @@ codecov: # https://docs.codecov.io/v4.3.6/docs/comparing-commits allow_coverage_offsets: true + token: 6dafc396-e7f5-4221-a38a-8b07a49fbdae + comment: off From 96bfc981335e23a32ff263ed9b78eb0e738d4cea Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 20 Nov 2019 18:20:01 +0000 Subject: [PATCH 170/186] use checkout action to fetch depends --- .github/workflows/test-windows.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 075707bf6..38c7a55ef 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -31,6 +31,11 @@ jobs: steps: - uses: actions/checkout@v1 + - uses: actions/checkout@v1 + with: + repository: python-pillow/pillow-depends + ref: master + - name: Install PyPy if: "contains(matrix.python-version, 'pypy')" run: | @@ -75,12 +80,9 @@ jobs: Write-Host "::add-path::C:\Program Files (x86)\gs\gs9.50\bin" $env:PYTHON=$env:pythonLocation - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip - 7z x pillow-depends.zip -oc:\ - mv c:\pillow-depends-master c:\pillow-depends - xcopy c:\pillow-depends\*.zip $env:GITHUB_WORKSPACE\winbuild\ - xcopy c:\pillow-depends\*.tar.gz $env:GITHUB_WORKSPACE\winbuild\ - xcopy /s c:\pillow-depends\test_images\* $env:GITHUB_WORKSPACE\tests\images\ + 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: From 416393885429240ea599411bd7386c3dc04e275c Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 20 Nov 2019 17:48:58 +0000 Subject: [PATCH 171/186] Test Python 3.8 on Windows with GitHub Actions --- .github/workflows/test-windows.yml | 2 +- winbuild/config.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 075707bf6..e5356b79f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.5", "3.6", "3.7"] + python-version: ["3.5", "3.6", "3.7", "3.8"] architecture: ["x86", "x64"] include: - architecture: "x86" diff --git a/winbuild/config.py b/winbuild/config.py index 826c6183c..999e7e5b8 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -13,6 +13,7 @@ pythons = { "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/" From bd6f00cf4394a77e608e53f6d04834dab3840a5f Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 20 Nov 2019 19:45:30 +0000 Subject: [PATCH 172/186] use cached binary dependency files --- .github/workflows/test-windows.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 38c7a55ef..b854c0a36 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -68,14 +68,11 @@ jobs: - name: Fetch dependencies run: | - curl -fsSL -o nasm.zip https://www.nasm.us/pub/nasm/releasebuilds/2.14.02/win64/nasm-2.14.02-win64.zip - 7z x nasm.zip "-o$env:RUNNER_WORKSPACE\" + 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" - # 32-bit should work on both platforms - curl -fsSL -o gs950.exe https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs950/gs950w32.exe - ./gs950.exe /S + ..\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" From 14f65284224d7c7a72d5bd0c35b7b85114d6817b Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 20 Nov 2019 21:11:51 +0000 Subject: [PATCH 173/186] add pypy to gha win --- .github/workflows/test-windows.yml | 3 ++- winbuild/config.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e5356b79f..08d1573d7 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.5", "3.6", "3.7", "3.8"] + python-version: ["3.5", "3.6", "3.7", "3.8", "pypy3.6"] architecture: ["x86", "x64"] include: - architecture: "x86" @@ -324,6 +324,7 @@ jobs: 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 diff --git a/winbuild/config.py b/winbuild/config.py index 999e7e5b8..7f7fc7430 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -3,10 +3,10 @@ import os SF_MIRROR = "https://iweb.dl.sourceforge.net" pythons = { + "pypy3": {"compiler": 7.1, "vc": 2015}, # for AppVeyor "35": {"compiler": 7.1, "vc": 2015}, "36": {"compiler": 7.1, "vc": 2015}, - "pypy3": {"compiler": 7.1, "vc": 2015}, "37": {"compiler": 7.1, "vc": 2015}, "38": {"compiler": 7.1, "vc": 2015}, # for GitHub Actions From 33dabf986f1b14557f8005334c70327a1f5740aa Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 20 Nov 2019 18:42:52 -0800 Subject: [PATCH 174/186] Import unittest from stdlib rather than helper.py The unittest in helper.py has not offered an interesting abstraction since dbe9f85c7d8f760756ebf8129195470700e63e3d so import from the more typical stdlib location. --- Tests/bench_cffi_access.py | 3 ++- Tests/check_fli_overflow.py | 4 +++- Tests/check_imaging_leaks.py | 4 +++- Tests/check_j2k_leaks.py | 3 ++- Tests/check_j2k_overflow.py | 4 +++- Tests/check_jpeg_leaks.py | 3 ++- Tests/check_large_memory.py | 3 ++- Tests/check_large_memory_numpy.py | 3 ++- Tests/check_libtiff_segfault.py | 4 +++- Tests/check_png_dos.py | 3 ++- Tests/test_color_lut.py | 3 ++- Tests/test_core_resources.py | 3 ++- Tests/test_features.py | 3 ++- Tests/test_file_eps.py | 3 ++- Tests/test_file_fpx.py | 4 +++- Tests/test_file_gif.py | 3 ++- Tests/test_file_icns.py | 3 ++- Tests/test_file_mic.py | 4 +++- Tests/test_file_msp.py | 3 ++- Tests/test_file_png.py | 3 ++- Tests/test_file_sun.py | 3 ++- Tests/test_file_tiff.py | 3 ++- Tests/test_file_webp.py | 4 +++- Tests/test_file_webp_alpha.py | 4 +++- Tests/test_font_leaks.py | 4 +++- Tests/test_image.py | 3 ++- Tests/test_image_access.py | 3 ++- Tests/test_image_resample.py | 3 ++- Tests/test_imagedraw.py | 3 ++- Tests/test_imagedraw2.py | 3 ++- Tests/test_imagefile.py | 3 ++- Tests/test_imagefont.py | 3 ++- Tests/test_imagefont_bitmap.py | 4 +++- Tests/test_imagefontctl.py | 4 +++- Tests/test_imageshow.py | 4 +++- Tests/test_imagetk.py | 4 +++- Tests/test_imagewin.py | 4 +++- Tests/test_lib_image.py | 4 +++- Tests/test_locale.py | 3 ++- Tests/test_main.py | 3 ++- Tests/test_map.py | 3 ++- Tests/test_numpy.py | 4 +++- Tests/test_pyroma.py | 4 +++- Tests/test_util.py | 4 +++- Tests/test_webp_leaks.py | 3 ++- 45 files changed, 108 insertions(+), 45 deletions(-) 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/check_fli_overflow.py b/Tests/check_fli_overflow.py index db6559f1e..fa6037c2e 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" diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 34a570bb1..2c1793a4f 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,7 +1,9 @@ #!/usr/bin/env python +import unittest + from PIL import Image -from .helper import PillowTestCase, is_win32, unittest +from .helper import PillowTestCase, is_win32 min_iterations = 100 max_iterations = 10000 diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index d9c6c68b7..1635f1001 100755 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,8 +1,9 @@ +import unittest from io import BytesIO from PIL import Image -from .helper import PillowTestCase, is_win32, unittest +from .helper import PillowTestCase, is_win32 # Limits for testing the leak mem_limit = 1024 * 1048576 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 8cb93f2dd..6b2801a21 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,6 +1,7 @@ +import unittest from io import BytesIO -from .helper import PillowTestCase, hopper, is_win32, unittest +from .helper import PillowTestCase, hopper, is_win32 iterations = 5000 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..a0263f725 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" 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/test_color_lut.py b/Tests/test_color_lut.py index 301a99a3f..96461ca3a 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,8 +1,9 @@ +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 8d5fa5479..ac2970de2 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,8 +1,9 @@ import sys +import unittest from PIL import Image -from .helper import PillowTestCase, is_pypy, unittest +from .helper import PillowTestCase, is_pypy class TestCoreStats(PillowTestCase): diff --git a/Tests/test_features.py b/Tests/test_features.py index 96030bf56..eb51407a1 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,8 +1,9 @@ import io +import unittest from PIL import features -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase try: from PIL import _webp diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 82c510b18..0ab14e4eb 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() 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_gif.py b/Tests/test_file_gif.py index 3ae960075..bed25c890 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, is_pypy, netpbm_available, unittest +from .helper import PillowTestCase, hopper, is_pypy, netpbm_available try: from PIL import _webp diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 4d7f95ec3..e258601ce 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" diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index f9ea7712c..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 diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 5d512047b..d717ade16 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" diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 24279732d..c9c337e64 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,9 +1,10 @@ +import unittest import zlib from io import BytesIO from PIL import Image, ImageFile, PngImagePlugin -from .helper import PillowLeakTestCase, PillowTestCase, hopper, is_win32, unittest +from .helper import PillowLeakTestCase, PillowTestCase, hopper, is_win32 try: from PIL import _webp diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 84d59e0c7..5fc171054 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" diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index dec3f5488..a4346a7b5 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,12 +1,13 @@ import logging import os +import unittest from io import BytesIO import pytest from PIL import Image, TiffImagePlugin from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION -from .helper import PillowTestCase, hopper, is_pypy, is_win32, unittest +from .helper import PillowTestCase, hopper, is_pypy, is_win32 logger = logging.getLogger(__name__) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 35ce3dba7..685f876e2 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 diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 9693f691f..e3c2b98b9 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 diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 06085392d..393ddb0fb 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image, ImageDraw, ImageFont, features -from .helper import PillowLeakTestCase, is_win32, unittest +from .helper import PillowLeakTestCase, is_win32 @unittest.skipIf(is_win32(), "requires Unix or macOS") diff --git a/Tests/test_image.py b/Tests/test_image.py index 74cac4914..15cf435b0 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,10 +1,11 @@ import os import shutil import tempfile +import unittest from PIL import Image, UnidentifiedImageError -from .helper import PillowTestCase, hopper, is_win32, unittest +from .helper import PillowTestCase, hopper, is_win32 class TestImage(PillowTestCase): diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index cd65e8dd0..57bac753d 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -2,11 +2,12 @@ import ctypes import os import subprocess import sys +import unittest from distutils import ccompiler, sysconfig from PIL import Image -from .helper import PillowTestCase, hopper, is_win32, on_ci, 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 diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 26c65d544..00c8fa5f2 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,8 +1,9 @@ +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): diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 75e658d65..50a774df2 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,8 +1,9 @@ import os.path +import unittest from PIL import Image, ImageColor, ImageDraw, ImageFont, 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_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_imagefile.py b/Tests/test_imagefile.py index dad408e93..228bbf929 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 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index b7cfefacf..e34756a79 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -4,11 +4,12 @@ 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, is_pypy, is_win32, unittest +from .helper import PillowTestCase, is_pypy, is_win32 FONT_PATH = "Tests/fonts/FreeMono.ttf" FONT_SIZE = 20 diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py index b7be8f723..eed0c70b6 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: diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 84bf1c58f..71efb897f 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,6 +1,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" diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 225c8a6a9..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, is_win32, on_ci, on_github_actions, unittest +from .helper import PillowTestCase, hopper, is_win32, on_ci, on_github_actions class TestImageShow(PillowTestCase): diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index 808ebd392..048efd639 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.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 ImageTk diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 2b2034187..1cd8c674e 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,6 +1,8 @@ +import unittest + from PIL import ImageWin -from .helper import PillowTestCase, hopper, is_win32, unittest +from .helper import PillowTestCase, hopper, is_win32 class TestImageWin(PillowTestCase): 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_locale.py b/Tests/test_locale.py index 69bce68cf..0b1f330ac 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,8 +1,9 @@ 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: diff --git a/Tests/test_main.py b/Tests/test_main.py index 15d4f7819..4f52149b6 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -1,9 +1,10 @@ import os import subprocess import sys +import unittest from unittest import TestCase -from .helper import is_pypy, is_win32, on_github_actions, unittest +from .helper import is_pypy, is_win32, on_github_actions class TestMain(TestCase): diff --git a/Tests/test_map.py b/Tests/test_map.py index 25e24e2fb..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, is_win32, unittest +from .helper import PillowTestCase, is_win32 try: import numpy diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 358180f3f..7de5c3bbd 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,6 +1,8 @@ +import unittest + from PIL import Image -from .helper import PillowTestCase, hopper, unittest +from .helper import PillowTestCase, hopper try: import numpy diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 3455a502b..accad5451 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,6 +1,8 @@ +import unittest + from PIL import __version__ -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase try: import pyroma diff --git a/Tests/test_util.py b/Tests/test_util.py index cb2101e1d..6a111b5b9 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,6 +1,8 @@ +import unittest + from PIL import _util -from .helper import PillowTestCase, unittest +from .helper import PillowTestCase class TestUtil(PillowTestCase): diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 93a6c2db0..713fc161e 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -1,8 +1,9 @@ +import unittest from io import BytesIO from PIL import Image, features -from .helper import PillowLeakTestCase, unittest +from .helper import PillowLeakTestCase test_file = "Tests/images/hopper.webp" From 2f5eacf0f9f03aa2f12f20301bb57e949c8db72f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 21 Nov 2019 18:55:58 +1100 Subject: [PATCH 175/186] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9952c1166..3fb4c2e0c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 7.0.0 (unreleased) ------------------ +- Drop support for EOL Python 2.7 #4109 + [hugovk, radarhere, jdufresne] + +- Added UnidentifiedImageError #4182 + [radarhere, hugovk] + - Remove deprecated __version__ from plugins #4197 [hugovk, radarhere] From e77398096100b458b439c784ed1ec68e3aac137c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Nov 2019 10:12:52 +1100 Subject: [PATCH 176/186] Updated URL [ci skip] --- src/PIL/GbrImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 2de56aadf..292de435c 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -14,7 +14,7 @@ # See the README file for information on usage and redistribution. # # -# See https://github.com/GNOME/gimp/blob/master/devel-docs/gbr.txt for +# See https://github.com/GNOME/gimp/blob/mainline/devel-docs/gbr.txt for # format documentation. # # This code Interprets version 1 and 2 .gbr files. From 0253bd59234a46797479908d38d11cc60b863789 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Nov 2019 19:44:06 +1100 Subject: [PATCH 177/186] Updated Tk Tcl to 8.6.10 --- winbuild/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/winbuild/config.py b/winbuild/config.py index 7f7fc7430..fad263755 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -71,15 +71,15 @@ libs = { "version": "8.5.19", }, "tcl-8.6": { - "url": SF_MIRROR + "/project/tcl/Tcl/8.6.9/tcl869-src.zip", - "filename": "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": "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", From 0cce22c68dd59befb797583dec485dd12a553621 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 23 Nov 2019 06:24:55 +1100 Subject: [PATCH 178/186] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3fb4c2e0c..d3922f328 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,7 +23,7 @@ Changelog (Pillow) - Corrected DdsImagePlugin setting info gamma #4171 [radarhere] -- Depends: Update libtiff to 4.1.0 #4195 +- Depends: Update libtiff to 4.1.0 #4195, Tk Tcl to 8.6.10 #4229 [radarhere] - Improve handling of file resources #3577 From c0048ad7de2b74e7b27a535ddff9c64484d859b2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 Nov 2019 07:03:23 +1100 Subject: [PATCH 179/186] Use context managers --- Tests/check_fli_overflow.py | 4 +- Tests/check_libtiff_segfault.py | 4 +- Tests/test_file_blp.py | 6 +- Tests/test_file_bmp.py | 82 +++--- Tests/test_file_cur.py | 15 +- Tests/test_file_dds.py | 95 +++--- Tests/test_file_eps.py | 27 +- Tests/test_file_ftex.py | 13 +- Tests/test_file_gif.py | 19 +- Tests/test_file_icns.py | 31 +- Tests/test_file_ico.py | 24 +- Tests/test_file_jpeg.py | 331 ++++++++++----------- Tests/test_file_jpeg2k.py | 116 ++++---- Tests/test_file_libtiff.py | 486 +++++++++++++++---------------- Tests/test_file_libtiff_small.py | 21 +- Tests/test_file_mcidas.py | 16 +- Tests/test_file_msp.py | 22 +- Tests/test_file_pcd.py | 4 +- Tests/test_file_pcx.py | 24 +- Tests/test_file_pdf.py | 4 +- Tests/test_file_pixar.py | 16 +- Tests/test_file_png.py | 270 ++++++++--------- Tests/test_file_ppm.py | 50 ++-- Tests/test_file_sgi.py | 50 ++-- Tests/test_file_spider.py | 12 +- Tests/test_file_sun.py | 14 +- Tests/test_file_tar.py | 10 +- Tests/test_file_tga.py | 156 +++++----- Tests/test_file_tiff.py | 167 +++++------ Tests/test_file_tiff_metadata.py | 38 ++- Tests/test_file_webp.py | 99 +++---- Tests/test_file_webp_alpha.py | 80 +++-- Tests/test_file_webp_animated.py | 153 +++++----- Tests/test_file_webp_lossless.py | 16 +- Tests/test_file_webp_metadata.py | 56 ++-- Tests/test_file_wmf.py | 12 +- Tests/test_file_xbm.py | 9 +- Tests/test_file_xpm.py | 14 +- Tests/test_file_xvthumb.py | 12 +- Tests/test_format_lab.py | 28 +- Tests/test_image.py | 38 +-- Tests/test_image_convert.py | 26 +- Tests/test_image_crop.py | 8 +- Tests/test_image_filter.py | 76 ++--- Tests/test_image_getextrema.py | 6 +- Tests/test_image_quantize.py | 9 +- Tests/test_image_resample.py | 28 +- Tests/test_image_rotate.py | 58 ++-- Tests/test_image_split.py | 4 +- Tests/test_image_transform.py | 8 +- Tests/test_imagechops.py | 142 ++++----- Tests/test_imagecms.py | 10 +- Tests/test_imagedraw.py | 282 +++++++++--------- Tests/test_imagefile.py | 132 +++++---- Tests/test_imagefont.py | 54 ++-- Tests/test_imagefontctl.py | 70 ++--- Tests/test_imagemorph.py | 4 +- Tests/test_imageops.py | 69 +++-- Tests/test_imagepalette.py | 5 +- Tests/test_imagetk.py | 22 +- Tests/test_numpy.py | 12 +- Tests/test_pickle.py | 42 +-- Tests/test_psdraw.py | 4 +- Tests/test_qt_image_toqimage.py | 4 +- Tests/test_shell_injection.py | 4 +- 65 files changed, 1837 insertions(+), 1886 deletions(-) diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index fa6037c2e..206a86007 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -11,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_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index a0263f725..b272c601c 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -14,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/test_file_blp.py b/Tests/test_file_blp.py index 8dbd82986..1e8dff184 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -15,6 +15,6 @@ class TestFileBlp(PillowTestCase): 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 e7f2c9315..76cd98aba 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,11 +36,10 @@ class TestFileBmp(PillowTestCase): im.save(output, "BMP") output.seek(0) - reloaded = Image.open(output) - - self.assertEqual(im.mode, reloaded.mode) - self.assertEqual(im.size, reloaded.size) - self.assertEqual(reloaded.format, "BMP") + with Image.open(output) as reloaded: + self.assertEqual(im.mode, reloaded.mode) + self.assertEqual(im.size, reloaded.size) + self.assertEqual(reloaded.format, "BMP") def test_dpi(self): dpi = (72, 72) @@ -57,17 +56,17 @@ class TestFileBmp(PillowTestCase): # 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 @@ -80,11 +79,10 @@ class TestFileBmp(PillowTestCase): def test_save_dpi_rounding(self): outfile = self.tempfile("temp.bmp") - im = Image.open("Tests/images/hopper.bmp") - - im.save(outfile, dpi=(72.2, 72.2)) - with Image.open(outfile) as reloaded: - self.assertEqual(reloaded.info["dpi"], (72, 72)) + 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.8, 72.8)) with Image.open(outfile) as reloaded: @@ -92,32 +90,32 @@ class TestFileBmp(PillowTestCase): 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_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_dds.py b/Tests/test_file_dds.py index 8ef90e86e..053d72568 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -17,73 +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)) - - self.assert_image_equal(target, im) + with Image.open(TEST_FILE_DX10_BC7.replace(".dds", ".png")) as target: + self.assert_image_equal(target, im) def test_dx10_bc7_unorm_srgb(self): """Check DX10 unsigned normalized integer images can be opened""" - target = Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB.replace(".dds", ".png")) + with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im: + im.load() - im = Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) - im.load() + self.assertEqual(im.format, "DDS") + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (16, 16)) + self.assertEqual(im.info["gamma"], 1 / 2.2) - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (16, 16)) - self.assertEqual(im.info["gamma"], 1 / 2.2) - - self.assert_image_equal(target, im) + 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( @@ -95,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""" @@ -145,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 0ab14e4eb..6fd201d48 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -68,8 +68,8 @@ class TestFileEps(PillowTestCase): 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) + 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): @@ -100,12 +100,13 @@ class TestFileEps(PillowTestCase): 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") @@ -122,14 +123,16 @@ class TestFileEps(PillowTestCase): # Zero bounding box with Image.open(file1) as image1_scale1: image1_scale1.load() - image1_scale1_compare = Image.open(file1_compare).convert("RGB") + with Image.open(file1_compare) as image1_scale1_compare: + image1_scale1_compare = image1_scale1_compare.convert("RGB") image1_scale1_compare.load() self.assert_image_similar(image1_scale1, image1_scale1_compare, 5) # Non-Zero bounding box with Image.open(file2) as image2_scale1: image2_scale1.load() - image2_scale1_compare = Image.open(file2_compare).convert("RGB") + with Image.open(file2_compare) as image2_scale1_compare: + image2_scale1_compare = image2_scale1_compare.convert("RGB") image2_scale1_compare.load() self.assert_image_similar(image2_scale1, image2_scale1_compare, 10) @@ -143,14 +146,16 @@ class TestFileEps(PillowTestCase): # Zero bounding box with Image.open(file1) as image1_scale2: image1_scale2.load(scale=2) - image1_scale2_compare = Image.open(file1_compare_scale2).convert("RGB") + with Image.open(file1_compare_scale2) as image1_scale2_compare: + image1_scale2_compare = image1_scale2_compare.convert("RGB") image1_scale2_compare.load() self.assert_image_similar(image1_scale2, image1_scale2_compare, 5) # Non-Zero bounding box with Image.open(file2) as image2_scale2: image2_scale2.load(scale=2) - image2_scale2_compare = Image.open(file2_compare_scale2).convert("RGB") + with Image.open(file2_compare_scale2) as image2_scale2_compare: + image2_scale2_compare = image2_scale2_compare.convert("RGB") image2_scale2_compare.load() self.assert_image_similar(image2_scale2, image2_scale2_compare, 10) 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_gif.py b/Tests/test_file_gif.py index bed25c890..b17459a8b 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -94,13 +94,14 @@ class TestFileGif(PillowTestCase): 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) @@ -554,9 +555,9 @@ class TestFileGif(PillowTestCase): 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): with Image.open(TEST_GIF) as im: diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index e258601ce..6b6543d8d 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -27,32 +27,31 @@ class TestFileIcns(PillowTestCase): @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 diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index ac6b19041..d8bb9630f 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -27,23 +27,23 @@ 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): with Image.open(TEST_ICO_FILE) as im: diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 35f2c0940..e43b27f52 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -44,12 +44,12 @@ 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) @@ -66,30 +66,34 @@ class TestFileJpeg(PillowTestCase): # 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)) @@ -140,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()) @@ -339,17 +343,17 @@ class TestFileJpeg(PillowTestCase): 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 @@ -365,10 +369,10 @@ class TestFileJpeg(PillowTestCase): def test_truncated_jpeg_should_read_all_the_data(self): filename = "Tests/images/truncated_jpeg.jpg" ImageFile.LOAD_TRUNCATED_IMAGES = True - im = Image.open(filename) - im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False - self.assertIsNotNone(im.getbbox()) + with Image.open(filename) as im: + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False + self.assertIsNotNone(im.getbbox()) def test_truncated_jpeg_throws_IOError(self): filename = "Tests/images/truncated_jpeg.jpg" @@ -381,106 +385,106 @@ class TestFileJpeg(PillowTestCase): 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): @@ -490,12 +494,11 @@ class TestFileJpeg(PillowTestCase): @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 @@ -512,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 """ @@ -547,15 +549,15 @@ 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 @@ -568,13 +570,14 @@ class TestFileJpeg(PillowTestCase): 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)) - with Image.open(outfile) as reloaded: - 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)) + with Image.open(outfile) as reloaded: self.assertEqual(reloaded.info["dpi"], (73, 73)) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index dac1d0ec0..37ce726db 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -33,13 +33,13 @@ 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): with Image.open("Tests/images/balloon.jpf") as im: @@ -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) @@ -110,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") @@ -132,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 dba053e60..c26c503c4 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -47,25 +47,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""" @@ -74,10 +72,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""" @@ -87,56 +84,51 @@ 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 """ @@ -204,44 +196,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 @@ -342,62 +334,58 @@ class TestFileLibTiff(LibTiffTestCase): 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) - self.assertEqual(b[0], ord(b"\xe0")) - self.assertEqual(b[1], ord(b"\x01")) + b = im.tobytes() + # Bytes are in image native order (little endian) + self.assertEqual(b[0], ord(b"\xe0")) + self.assertEqual(b[1], ord(b"\x01")) - out = self.tempfile("temp.tif") - # out = "temp.le.tif" - im.save(out) - reread = Image.open(out) - - self.assertEqual(reread.info["compression"], im.info["compression"]) - self.assertEqual(reread.getpixel((0, 0)), 480) + out = 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) + self.assertEqual(b[0], ord(b"\x01")) + self.assertEqual(b[1], ord(b"\xe0")) - # Bytes are in image native order (big endian) - self.assertEqual(b[0], ord(b"\x01")) - self.assertEqual(b[1], ord(b"\xe0")) - - out = 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) with Image.open(out) as reread: self.assertEqual("temp.tif", reread.tag_v2[269]) @@ -407,16 +395,16 @@ class TestFileLibTiff(LibTiffTestCase): """ 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 @@ -424,16 +412,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 @@ -446,18 +434,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) @@ -479,8 +467,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 @@ -543,10 +531,10 @@ class TestFileLibTiff(LibTiffTestCase): def test__next(self): TiffImagePlugin.READ_LIBTIFF = True - im = Image.open("Tests/images/hopper.tif") - self.assertFalse(im.tag.next) - im.load() - self.assertFalse(im.tag.next) + with Image.open("Tests/images/hopper.tif") as im: + self.assertFalse(im.tag.next) + im.load() + self.assertFalse(im.tag.next) def test_4bit(self): # Arrange @@ -555,13 +543,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 = ( @@ -586,15 +574,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 @@ -612,8 +600,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") @@ -625,12 +613,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): @@ -696,44 +684,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] @@ -743,82 +737,79 @@ 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) - - self.assert_image_equal_tofile( - im, "Tests/images/old-style-jpeg-compression.png" - ) + with Image.open(infile) as im: + 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 @@ -828,13 +819,12 @@ class TestFileLibTiff(LibTiffTestCase): self.assertEqual(im.size, (950, 975)) def test_orientation(self): - base_im = Image.open("Tests/images/g4_orientation_1.tif") + with Image.open("Tests/images/g4_orientation_1.tif") as base_im: + for i in range(2, 9): + with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: + im.load() - for i in range(2, 9): - im = Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") - im.load() - - self.assert_image_similar(base_im, im, 0.7) + 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 diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 2eabc60fd..a4e74896f 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -20,10 +20,9 @@ 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""" @@ -32,16 +31,14 @@ class TestFileLibTiffSmall(LibTiffTestCase): 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_msp.py b/Tests/test_file_msp.py index d717ade16..51189b83b 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -16,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" @@ -38,16 +38,16 @@ 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") def test_open_windows_v2(self): 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 4b5897589..9b8b789b9 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -107,8 +107,8 @@ class TestFilePdf(PillowTestCase): 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) 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 c9c337e64..4cd613785 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -91,10 +91,10 @@ class TestFilePng(PillowTestCase): 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" @@ -195,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 @@ -223,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 @@ -235,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 @@ -257,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)) @@ -290,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( @@ -322,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) @@ -350,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 @@ -386,9 +379,8 @@ 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): @@ -401,9 +393,8 @@ class TestFilePng(PillowTestCase): 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)) @@ -412,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"}) @@ -480,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)) @@ -498,10 +488,10 @@ 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 @@ -521,36 +511,35 @@ class TestFilePng(PillowTestCase): 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) @@ -575,61 +564,58 @@ 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") - - test_file = self.tempfile("temp.png") - im.save(test_file) + with Image.open("Tests/images/exif.png") as im: + test_file = self.tempfile("temp.png") + im.save(test_file) 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") - - test_file = self.tempfile("temp.png") - im.save(test_file) + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + test_file = self.tempfile("temp.png") + im.save(test_file) 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) - - test_file = self.tempfile("temp.png") - im.save(test_file, exif=b"exifstring") + with Image.open(TEST_PNG_FILE) as im: + test_file = self.tempfile("temp.png") + im.save(test_file, exif=b"exifstring") with Image.open(test_file) as reloaded: self.assertEqual(reloaded.info["exif"], b"Exif\x00\x00exifstring") @@ -638,12 +624,12 @@ class TestFilePng(PillowTestCase): 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(is_win32(), "requires Unix or macOS") diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 226687fa3..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") 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 5940c2ff2..4f617ce51 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -64,10 +64,10 @@ 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)) @@ -143,5 +143,5 @@ class TestImageSpider(PillowTestCase): im.save(data, format="SPIDER") data.seek(0) - im2 = Image.open(data) - self.assert_image_equal(im, im2) + 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 5fc171054..835aead79 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -15,20 +15,20 @@ 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") def test_others(self): diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index f381eef7e..a77e4e84c 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -22,11 +22,11 @@ class TestFileTar(PillowTestCase): ]: if codec in codecs: with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: - im = Image.open(tar) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, format) + 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): diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bd5c32c5b..01664cd6e 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -24,52 +24,57 @@ 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 @@ -93,29 +98,27 @@ class TestFileTga(PillowTestCase): 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) - 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"]) - - # RGBA save - im.convert("RGBA").save(out) + # 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) + # Check there is no id section + im.save(out) with Image.open(out) as test_im: self.assertNotIn("id_section", test_im.info) @@ -140,24 +143,23 @@ class TestFileTga(PillowTestCase): 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) + 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) + # 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") @@ -173,11 +175,11 @@ class TestFileTga(PillowTestCase): 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") + # Save with compression + im.save(out, compression="tga_rle") with Image.open(out) as test_im: self.assertEqual(test_im.info["compression"], "tga_rle") @@ -186,15 +188,15 @@ class TestFileTga(PillowTestCase): 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 a4346a7b5..2353fb34e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -72,22 +72,20 @@ class TestFileTiff(PillowTestCase): # 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() @@ -149,23 +147,22 @@ class TestFileTiff(PillowTestCase): 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" @@ -192,55 +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) 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) 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( @@ -292,10 +287,10 @@ class TestFileTiff(PillowTestCase): 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" @@ -411,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 = ( @@ -439,15 +434,15 @@ 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} @@ -467,29 +462,26 @@ class TestFileTiff(PillowTestCase): # 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 @@ -502,9 +494,8 @@ class TestFileTiff(PillowTestCase): # -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"]: @@ -513,8 +504,8 @@ 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): mp = BytesIO() @@ -532,8 +523,8 @@ class TestFileTiff(PillowTestCase): im.copy().save(mp, format="TIFF", save_all=True, append_images=ims) mp.seek(0, os.SEEK_SET) - reread = Image.open(mp) - self.assertEqual(reread.n_frames, 3) + with Image.open(mp) as reread: + self.assertEqual(reread.n_frames, 3) # Test appending using a generator def imGenerator(ims): @@ -543,8 +534,8 @@ class TestFileTiff(PillowTestCase): im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims)) mp.seek(0, os.SEEK_SET) - reread = Image.open(mp) - self.assertEqual(reread.n_frames, 3) + with Image.open(mp) as reread: + self.assertEqual(reread.n_frames, 3) def test_saving_icc_profile(self): # Tests saving TIFF with icc_profile set. diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 7393a147c..bf8d6e933 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -130,14 +130,13 @@ class TestFileTiffMetadata(PillowTestCase): def test_write_metadata(self): """ Test metadata writing through the python code """ - img = Image.open("Tests/images/hopper.tif") - - f = self.tempfile("temp.tiff") - img.save(f, tiffinfo=img.tag) - - with Image.open(f) as loaded: + with Image.open("Tests/images/hopper.tif") as img: + f = self.tempfile("temp.tiff") + img.save(f, tiffinfo=img.tag) original = img.tag_v2.named() + + with Image.open(f) as loaded: reloaded = loaded.tag_v2.named() for k, v in original.items(): @@ -187,10 +186,10 @@ 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) with Image.open(out) as reloaded: self.assertNotIsInstance(im.info["icc_profile"], tuple) self.assertEqual(im.info["icc_profile"], reloaded.info["icc_profile"]) @@ -205,14 +204,14 @@ class TestFileTiffMetadata(PillowTestCase): 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() @@ -241,12 +240,11 @@ class TestFileTiffMetadata(PillowTestCase): self.assertIn(33432, info) def test_PhotoshopInfo(self): - im = Image.open("Tests/images/issue_2278.tif") - - self.assertEqual(len(im.tag_v2[34377]), 1) - self.assertIsInstance(im.tag_v2[34377][0], bytes) - out = self.tempfile("temp.tiff") - im.save(out) + 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) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 685f876e2..a2fdbc333 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -41,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): """ @@ -64,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): """ @@ -93,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): """ @@ -113,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): """ @@ -145,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" diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index e3c2b98b9..85a1e3d2f 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -26,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): """ @@ -56,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): """ @@ -81,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): """ @@ -107,15 +106,14 @@ class TestFileWebpAlpha(PillowTestCase): file_path = "Tests/images/transparent.gif" with Image.open(file_path) as im: im.save(temp_file) - image = Image.open(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() - with Image.open(file_path) as im: - target = im.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 834080db8..bf425d079 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -48,18 +48,18 @@ class TestFileWebpAnimation(PillowTestCase): 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) + 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,39 +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): - yield from ims - - 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): """ @@ -110,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): """ @@ -141,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 e3acf745d..06c780299 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -42,17 +42,15 @@ class TestFileWebpMetadata(PillowTestCase): def test_write_exif_metadata(self): 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") @@ -74,17 +72,15 @@ class TestFileWebpMetadata(PillowTestCase): def test_write_icc_metadata(self): 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: @@ -94,17 +90,15 @@ class TestFileWebpMetadata(PillowTestCase): def test_read_no_exif(self): file_path = "Tests/images/flower.jpg" - image = Image.open(file_path) - self.assertIn("exif", image.info) - test_buffer = BytesIO() + 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: @@ -115,16 +109,16 @@ class TestFileWebpMetadata(PillowTestCase): 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, + ) with Image.open(temp_file) as image: self.assertIn("icc_profile", image.info) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index c07bbf60a..01a802bfb 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -12,9 +12,9 @@ class TestFileWmf(PillowTestCase): # Currently, support for WMF/EMF is Windows-only im.load() # Compare to reference rendering - imref = Image.open("Tests/images/drawing_emf_ref.png") - imref.load() - self.assert_image_similar(im, imref, 0) + with Image.open("Tests/images/drawing_emf_ref.png") as imref: + imref.load() + self.assert_image_similar(im, imref, 0) # Test basic WMF open and rendering with Image.open("Tests/images/drawing.wmf") as im: @@ -22,9 +22,9 @@ class TestFileWmf(PillowTestCase): # Currently, support for WMF/EMF is Windows-only im.load() # Compare to reference rendering - imref = Image.open("Tests/images/drawing_wmf_ref.png") - imref.load() - self.assert_image_similar(im, imref, 2.0) + with Image.open("Tests/images/drawing_wmf_ref.png") as imref: + imref.load() + self.assert_image_similar(im, imref, 2.0) def test_register_handler(self): class TestHandler: diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 5a1eb54bc..972c5abe0 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -30,11 +30,10 @@ static char basic_bits[] = { class TestFileXbm(PillowTestCase): def test_pil151(self): - 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 diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index a4ea8c491..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" 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_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 15cf435b0..83da76b96 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -98,14 +98,14 @@ class TestImage(PillowTestCase): 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") @@ -127,8 +127,8 @@ class TestImage(PillowTestCase): 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,9 +150,9 @@ class TestImage(PillowTestCase): 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)) @@ -365,8 +365,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 @@ -407,8 +407,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 @@ -475,7 +475,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): @@ -499,7 +500,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): diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 80fa9f513..5bbe3abfa 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -53,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") @@ -210,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_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_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_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_resample.py b/Tests/test_image_resample.py index 00c8fa5f2..3bb941438 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -450,25 +450,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_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_transform.py b/Tests/test_image_transform.py index 35f64bcc9..781ba7923 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -23,11 +23,11 @@ class TestImageTransform(PillowTestCase): def test_info(self): comment = b"File written by Adobe Photoshop\xa8 4.0" - im = Image.open("Tests/images/hopper.gif") - self.assertEqual(im.info["comment"], comment) + with Image.open("Tests/images/hopper.gif") as im: + self.assertEqual(im.info["comment"], comment) - transform = ImageTransform.ExtentTransform((0, 0, 0, 0)) - new_im = im.transform((100, 100), transform) + 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): 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 97f81fb3f..98b1a80ab 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -229,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. diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 50a774df2..d3791e0b1 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -157,12 +157,12 @@ class TestImageDraw(PillowTestCase): def test_bitmap(self): # Arrange - small = Image.open("Tests/images/pil123rgba.png").resize((50, 50)) im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) + with Image.open("Tests/images/pil123rgba.png").resize((50, 50)) as small: - # Act - draw.bitmap((10, 10), small) + # Act + draw.bitmap((10, 10), small) # Assert self.assert_image_equal(im, Image.open("Tests/images/imagedraw_bitmap.png")) @@ -523,8 +523,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) @@ -603,152 +603,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 diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 228bbf929..50c7d337b 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -128,33 +128,31 @@ class TestImageFile(PillowTestCase): 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): @@ -246,19 +244,19 @@ 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) + 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) @@ -266,19 +264,19 @@ class TestPyDecoder(PillowTestCase): 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) + 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) @@ -291,38 +289,38 @@ class TestPyDecoder(PillowTestCase): "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(): - 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") + 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) with Image.open(out) as reloaded: reloaded_exif = reloaded.getexif() diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 5a15eb535..1777e2978 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -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 @@ -703,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) @@ -733,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_imagefontctl.py b/Tests/test_imagefontctl.py index 71efb897f..c1f976dfa 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -25,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) @@ -37,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) @@ -49,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) @@ -60,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) @@ -72,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) @@ -84,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) @@ -96,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) @@ -112,9 +105,8 @@ class TestImagecomplextext(PillowTestCase): self.skipTest("libraqm 0.7 or greater not available") target = "Tests/images/test_direction_ttb.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 1.15) + with Image.open(target) as target_img: + 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) @@ -136,9 +128,8 @@ class TestImagecomplextext(PillowTestCase): self.skipTest("libraqm 0.7 or greater not available") target = "Tests/images/test_direction_ttb_stroke.png" - target_img = Image.open(target) - - self.assert_image_similar(im, target_img, 12.4) + 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) @@ -147,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)) @@ -162,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) @@ -180,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) @@ -192,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) @@ -204,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_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 d0fd73689..ea50a4c76 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -106,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) @@ -140,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") @@ -173,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( @@ -208,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( @@ -261,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_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_imagetk.py b/Tests/test_imagetk.py index 048efd639..5ee994dc7 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -31,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_numpy.py b/Tests/test_numpy.py index 7de5c3bbd..d3c4ce11d 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -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(" Date: Sat, 30 Nov 2019 10:08:32 +1100 Subject: [PATCH 180/186] Handle broken Photoshop data --- Tests/images/photoshop-200dpi-broken.jpg | Bin 0 -> 10953 bytes Tests/test_file_jpeg.py | 5 +++++ src/PIL/JpegImagePlugin.py | 5 ++++- 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Tests/images/photoshop-200dpi-broken.jpg diff --git a/Tests/images/photoshop-200dpi-broken.jpg b/Tests/images/photoshop-200dpi-broken.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a574872f267caea4bb2c0b185990f01a473f84de GIT binary patch literal 10953 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 literal 0 HcmV?d00001 diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 35f2c0940..04851d82c 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -656,6 +656,11 @@ class TestFileJpeg(PillowTestCase): }, ) + # 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 with Image.open("Tests/images/app13.jpg") as im: self.assertNotIn("photoshop", im.info) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index ea2f795a7..4286e1ae7 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -109,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]) From 47b2ae9a634842b1a49ee9a4e15249eef0544a48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Nov 2019 11:05:36 +1100 Subject: [PATCH 181/186] Raise a specific exception if no data is found for an MPO frame --- Tests/images/sugarshack_no_data.mpo | Bin 0 -> 120198 bytes Tests/test_file_mpo.py | 7 +++++++ src/PIL/MpoImagePlugin.py | 5 ++++- 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 Tests/images/sugarshack_no_data.mpo diff --git a/Tests/images/sugarshack_no_data.mpo b/Tests/images/sugarshack_no_data.mpo new file mode 100644 index 0000000000000000000000000000000000000000..d94bad53b1f813d180da0a38f9509eb8a2fc4085 GIT binary patch literal 120198 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 y`1P$7zJ)4hg6^ct`UVN>+~EFJ9t literal 0 HcmV?d00001 diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 9c8a2b468..cdcf2b041 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -113,6 +113,13 @@ class TestFileMpo(PillowTestCase): 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: with Image.open(test_file) as im: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index a63e76fe7..e97176d57 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -82,7 +82,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): self.offset = self.__mpoffsets[frame] self.fp.seek(self.offset + 2) # skip SOI marker - if i16(self.fp.read(2)) == 0xFFE1: # APP1 + segment = self.fp.read(2) + if not segment: + raise ValueError("No data found for frame") + if i16(segment) == 0xFFE1: # APP1 n = i16(self.fp.read(2)) - 2 self.info["exif"] = ImageFile._safe_read(self.fp, n) From 6745a193c22895d308d4f2450c0358104a181c26 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Dec 2019 00:00:04 +1100 Subject: [PATCH 182/186] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d3922f328..5af03f281 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 7.0.0 (unreleased) ------------------ +- 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] From ebed90c228e4c88b8bf7303a267e2714a5e3cd57 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 2 Dec 2019 15:26:51 +0200 Subject: [PATCH 183/186] Remove redundant files --- Tests/import_all.py | 14 --------- Tests/make_hash.py | 66 ------------------------------------------ Tests/threaded_save.py | 57 ------------------------------------ Tests/versions.py | 26 ----------------- 4 files changed, 163 deletions(-) delete mode 100644 Tests/import_all.py delete mode 100644 Tests/make_hash.py delete mode 100644 Tests/threaded_save.py delete mode 100644 Tests/versions.py diff --git a/Tests/import_all.py b/Tests/import_all.py deleted file mode 100644 index 33b07f9a2..000000000 --- a/Tests/import_all.py +++ /dev/null @@ -1,14 +0,0 @@ -import glob -import os -import sys -import traceback - -sys.path.insert(0, ".") - -for file in glob.glob("src/PIL/*.py"): - module = os.path.basename(file)[:-3] - try: - exec("from PIL import " + module) - except (ImportError, SyntaxError): - print("===", "failed to import", module) - traceback.print_exc() diff --git a/Tests/make_hash.py b/Tests/make_hash.py deleted file mode 100644 index 7199f8c7f..000000000 --- a/Tests/make_hash.py +++ /dev/null @@ -1,66 +0,0 @@ -# brute-force search for access descriptor hash table - -modes = [ - "1", - "L", - "LA", - "La", - "I", - "I;16", - "I;16L", - "I;16B", - "I;32L", - "I;32B", - "F", - "P", - "PA", - "RGB", - "RGBA", - "RGBa", - "RGBX", - "CMYK", - "YCbCr", - "LAB", - "HSV", -] - - -def hash(s, i): - # djb2 hash: multiply by 33 and xor character - for c in s: - i = (((i << 5) + i) ^ ord(c)) & 0xFFFFFFFF - return i - - -def check(size, i0): - h = [None] * size - for m in modes: - i = hash(m, i0) - i = i % size - if h[i]: - return 0 - h[i] = m - return h - - -min_start = 0 - -# 1) find the smallest table size with no collisions -for min_size in range(len(modes), 16384): - if check(min_size, 0): - print(len(modes), "modes fit in", min_size, "slots") - break - -# 2) see if we can do better with a different initial value -for i0 in range(65556): - for size in range(1, min_size): - if check(size, i0): - if size < min_size: - print(len(modes), "modes fit in", size, "slots with start", i0) - min_size = size - min_start = i0 - -print() - -print("#define ACCESS_TABLE_SIZE", min_size) -print("#define ACCESS_TABLE_HASH", min_start) diff --git a/Tests/threaded_save.py b/Tests/threaded_save.py deleted file mode 100644 index d47c2cf99..000000000 --- a/Tests/threaded_save.py +++ /dev/null @@ -1,57 +0,0 @@ -import io -import queue -import sys -import threading -import time - -from PIL import Image - -test_format = sys.argv[1] if len(sys.argv) > 1 else "PNG" - -im = Image.open("Tests/images/hopper.ppm") -im.load() - -queue = queue.Queue() - -result = [] - - -class Worker(threading.Thread): - def run(self): - while True: - im = queue.get() - if im is None: - queue.task_done() - sys.stdout.write("x") - break - f = io.BytesIO() - im.save(f, test_format, optimize=1) - data = f.getvalue() - result.append(len(data)) - im = Image.open(io.BytesIO(data)) - im.load() - sys.stdout.write(".") - queue.task_done() - - -t0 = time.time() - -threads = 20 -jobs = 100 - -for i in range(threads): - w = Worker() - w.start() - -for i in range(jobs): - queue.put(im) - -for i in range(threads): - queue.put(None) - -queue.join() - -print() -print(time.time() - t0) -print(len(result), sum(result)) -print(result) diff --git a/Tests/versions.py b/Tests/versions.py deleted file mode 100644 index d6433b063..000000000 --- a/Tests/versions.py +++ /dev/null @@ -1,26 +0,0 @@ -from PIL import Image - - -def version(module, version): - v = getattr(module.core, version + "_version", None) - if v: - print(version, v) - - -version(Image, "jpeglib") -version(Image, "zlib") -version(Image, "libtiff") - -try: - from PIL import ImageFont -except ImportError: - pass -else: - version(ImageFont, "freetype2") - -try: - from PIL import ImageCms -except ImportError: - pass -else: - version(ImageCms, "littlecms") From ce382e7858c32ef5e336509fd9e2375dc5412a1a Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 2 Dec 2019 15:37:20 +0200 Subject: [PATCH 184/186] Omit from coverage --- .codecov.yml | 3 +++ .coveragerc | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.codecov.yml b/.codecov.yml index 033786a49..d348345dc 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -12,4 +12,7 @@ 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 e93a2a426..f1f095806 100644 --- a/.coveragerc +++ b/.coveragerc @@ -15,4 +15,7 @@ exclude_lines = [run] omit = + Tests/32bit_segfault_check.py + Tests/bench_cffi_access.py Tests/check_*.py + Tests/createfontdatachunk.py From af37cf087392e1e6b8b4a80d5481dd9ef293daeb Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 20 Nov 2019 12:11:59 +0200 Subject: [PATCH 185/186] Python 3.5 and 3.6 are tested by GHA, remove from AppVeyor --- .appveyor.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 553ccdff1..2e3fd0a58 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -15,10 +15,6 @@ environment: matrix: - PYTHON: C:/Python38 - PYTHON: C:/Python38-x64 - - PYTHON: C:/Python37 - - PYTHON: C:/Python37-x64 - - PYTHON: C:/Python36 - - PYTHON: C:/Python36-x64 - PYTHON: C:/Python35 - PYTHON: C:/Python35-x64 - PYTHON: C:/msys64/mingw32 From a4a6a9e83a111e2234eb44092fbe326af4e89358 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 4 Dec 2019 22:47:15 +0300 Subject: [PATCH 186/186] Add La mode packing and unpacking --- Tests/test_lib_pack.py | 6 ++++++ src/libImaging/Pack.c | 3 +++ src/libImaging/Unpack.c | 4 +++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 45958d2eb..91479e343 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -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) @@ -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) 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/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},