diff --git a/.appveyor.yml b/.appveyor.yml index 0cf10597a..9ec837996 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,13 +13,9 @@ 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:/Python38 + - PYTHON: C:/Python38-x64 - PYTHON: C:/Python37 - - PYTHON: C:/Python27 - PYTHON: C:/Python37-x64 - PYTHON: C:/Python36 - PYTHON: C:/Python36-x64 @@ -30,6 +26,10 @@ environment: PIP_DIR: bin TEST_OPTIONS: --processes=0 DEPLOY: NO + - PYTHON: C:/vp/pypy3 + EXECUTABLE: bin/pypy.exe + PIP_DIR: bin + VENV: YES install: @@ -41,9 +41,9 @@ install: - xcopy /s c:\pillow-depends\test_images\* c:\pillow\tests\images - cd c:\pillow\winbuild\ - ps: | - if ($env:PYTHON -eq "c:/vp/pypy2") + if ($env:PYTHON -eq "c:/vp/pypy3") { - c:\pillow\winbuild\appveyor_install_pypy.cmd + c:\pillow\winbuild\appveyor_install_pypy3.cmd } - ps: | if ($env:PYTHON -eq "c:/msys64/mingw32") @@ -52,10 +52,13 @@ install: } else { - c:\python34\python.exe c:\pillow\winbuild\build_dep.py + c:\python37\python.exe c:\pillow\winbuild\build_dep.py c:\pillow\winbuild\build_deps.cmd $host.SetShouldExit(0) } +- curl -fsSL -o gs950.exe https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs950/gs950w32.exe +- gs950.exe /S +- path %path%;C:\Program Files (x86)\gs\gs9.50\bin build_script: - ps: | @@ -76,6 +79,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/.azure-pipelines/jobs/lint.yml b/.azure-pipelines/jobs/lint.yml new file mode 100644 index 000000000..d017590f8 --- /dev/null +++ b/.azure-pipelines/jobs/lint.yml @@ -0,0 +1,28 @@ +parameters: + name: '' # defaults for any parameters that aren't specified + vmImage: '' + +jobs: + +- job: ${{ parameters.name }} + pool: + vmImage: ${{ parameters.vmImage }} + + strategy: + matrix: + Python37: + python.version: '3.7' + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(python.version)' + architecture: 'x64' + + - script: | + python -m pip install --upgrade tox + displayName: 'Install dependencies' + + - script: | + tox -e lint + displayName: 'Lint' diff --git a/.azure-pipelines/jobs/test-docker.yml b/.azure-pipelines/jobs/test-docker.yml new file mode 100644 index 000000000..41dc2daec --- /dev/null +++ b/.azure-pipelines/jobs/test-docker.yml @@ -0,0 +1,22 @@ +parameters: + docker: '' # defaults for any parameters that aren't specified + dockerTag: 'master' + name: '' + vmImage: 'Ubuntu-16.04' + +jobs: + +- job: ${{ parameters.name }} + pool: + vmImage: ${{ parameters.vmImage }} + + steps: + - script: | + docker pull pythonpillow/${{ parameters.docker }}:${{ parameters.dockerTag }} + displayName: 'Docker pull' + + - script: | + # The Pillow user in the docker container is UID 1000 + sudo chown -R 1000 $(Build.SourcesDirectory) + docker run -v $(Build.SourcesDirectory):/Pillow pythonpillow/${{ parameters.docker }}:${{ parameters.dockerTag }} + displayName: 'Docker build' diff --git a/.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 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index eef2a30bd..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 [Coveralls](https://coveralls.io/repos/new) 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 @@ -34,6 +34,4 @@ The best reproductions are self-contained scripts with minimal dependencies. If ## Security vulnerabilities -To report sensitive vulnerability information, email security@python-pillow.org. - -If your organisation/employer is a distributor of Pillow and would like advance notification of security-related bugs, please let us know your preferred contact method. +Please see our [security policy](https://github.com/python-pillow/Pillow/blob/master/.github/SECURITY.md). diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..e0e6804bf --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +tidelift: "pypi/Pillow" diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 6cea87df2..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,19 +0,0 @@ -### What did you do? - -### What did you expect to happen? - -### What actually happened? - -### What are your OS, Python and Pillow versions? - -* OS: -* Python: -* Pillow: - -Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive. - -The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as plone, Django, or buildout, try to replicate the issue just using Pillow. - -```python -code goes here -``` diff --git a/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md b/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md new file mode 100644 index 000000000..115f6135d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md @@ -0,0 +1,59 @@ +--- +name: Issue report +about: Create a report to help us improve Pillow +--- + + + +### What did you do? + +### What did you expect to happen? + +### What actually happened? + +### What are your OS, Python and Pillow versions? + +* OS: +* Python: +* Pillow: + + + +```python +code goes here +``` diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 000000000..c6369fdef --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,5 @@ +# Security policy + +To report sensitive vulnerability information, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. + +If your organisation/employer is a distributor of Pillow and would like advance notification of security-related bugs, please let us know your preferred contact method. diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..195ea3865 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,29 @@ +name: Lint + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8"] + + name: Python ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v1 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade tox + + - name: Lint + run: tox -e lint diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh new file mode 100755 index 000000000..6cd9dadf3 --- /dev/null +++ b/.github/workflows/macos-install.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype + +PYTHONOPTIMIZE=0 pip install cffi +pip install coverage +pip install olefile +pip install -U pytest +pip install -U pytest-cov +pip install pyroma +pip install test-image-results +pip install numpy + +# extra test images +pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml new file mode 100644 index 000000000..4121eb5e9 --- /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-30-amd64, + fedora-31-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 }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml new file mode 100644 index 000000000..cc7d31ef0 --- /dev/null +++ b/.github/workflows/test-windows.yml @@ -0,0 +1,380 @@ +name: Test Windows + +on: [push, pull_request] + +jobs: + build: + + runs-on: windows-2019 + strategy: + fail-fast: false + matrix: + python-version: ["3.5", "3.6", "3.7", "3.8", "pypy3.6"] + architecture: ["x86", "x64"] + include: + - architecture: "x86" + platform-vcvars: "x86" + platform-msbuild: "Win32" + - architecture: "x64" + platform-vcvars: "x86_amd64" + platform-msbuild: "x64" + - python-version: "pypy3.6" + pypy-version: "pypy3.6-v7.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 + + - uses: actions/checkout@v1 + with: + repository: python-pillow/pillow-depends + ref: master + + - name: Install PyPy + if: "contains(matrix.python-version, 'pypy')" + run: | + curl -fsSL -o pypy3.zip "${{ matrix.pypy-url }}" + 7z x pypy3.zip "-o$env:RUNNER_WORKSPACE\" + mv "$env:RUNNER_WORKSPACE\${{ matrix.pypy-version }}" "$env:RUNNER_WORKSPACE\${{ matrix.python-version }}" + $env:PYTHON="$env:RUNNER_WORKSPACE\${{ matrix.python-version }}" + # set env: pythonLocation + Write-Host "`#`#[set-env name=pythonLocation;]$env:PYTHON" # old syntax https://github.com/actions/toolkit/issues/61 + Write-Host "::set-env name=pythonLocation::$env:PYTHON" # new syntax https://github.com/actions/toolkit/blob/5bb77ec03fea98332e41f9347c8fbb1ce1e48f4a/docs/commands.md + New-Item -ItemType SymbolicLink -Path "$env:PYTHON\python.exe" -Target "$env:PYTHON\pypy3.exe" + curl -fsSL -o get-pip.py https://bootstrap.pypa.io/get-pip.py + $env:PATH = "$env:PYTHON\bin;$env:PATH" + & $env:PYTHON\python.exe get-pip.py + shell: pwsh + + # sets env: pythonLocation + - name: Set up Python + if: "!contains(matrix.python-version, 'pypy')" + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + architecture: ${{ matrix.architecture }} + + - name: pip install wheel pytest pytest-cov codecov + run: | + "%pythonLocation%\python.exe" -m pip install wheel pytest pytest-cov + pip install codecov + shell: cmd + + - name: Fetch dependencies + run: | + 7z x ..\pillow-depends\nasm-2.14.02-win64.zip "-o$env:RUNNER_WORKSPACE\" + Write-Host "`#`#[add-path]$env:RUNNER_WORKSPACE\nasm-2.14.02" + Write-Host "::add-path::$env:RUNNER_WORKSPACE\nasm-2.14.02" + + ..\pillow-depends\gs950w32.exe /S + Write-Host "`#`#[add-path]C:\Program Files (x86)\gs\gs9.50\bin" + Write-Host "::add-path::C:\Program Files (x86)\gs\gs9.50\bin" + + $env:PYTHON=$env:pythonLocation + xcopy ..\pillow-depends\*.zip $env:GITHUB_WORKSPACE\winbuild\ + xcopy ..\pillow-depends\*.tar.gz $env:GITHUB_WORKSPACE\winbuild\ + xcopy /s ..\pillow-depends\test_images\* $env:GITHUB_WORKSPACE\tests\images\ + cd $env:GITHUB_WORKSPACE/winbuild/ + python.exe $env:GITHUB_WORKSPACE\winbuild\build_dep.py + env: + EXECUTABLE: bin\python.exe + shell: pwsh + + - name: Build dependencies / libjpeg + if: false + run: | + REM FIXME uses /MT not /MD, see makefile.vc and win32.mak for more info + + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\jpeg-9c + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + nmake -nologo -f makefile.vc setup-vc6 + nmake -nologo -f makefile.vc clean + nmake -nologo -f makefile.vc nodebug=1 libjpeg.lib cjpeg.exe djpeg.exe + copy /Y /B j*.h %INCLIB% + copy /Y /B *.lib %INCLIB% + copy /Y /B *.exe %INCLIB% + shell: cmd + + - name: Build dependencies / libjpeg-turbo + run: | + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\libjpeg-turbo-2.0.3 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF + set CMAKE=%CMAKE% -DENABLE_SHARED:BOOL=OFF -DWITH_JPEG8:BOOL=TRUE -DWITH_CRT_DLL:BOOL=TRUE -DCMAKE_BUILD_TYPE=Release + %CMAKE% -G "NMake Makefiles" . + nmake -nologo -f Makefile clean + nmake -nologo -f Makefile jpeg-static cjpeg-static djpeg-static + copy /Y /B j*.h %INCLIB% + copy /Y /B jpeg-static.lib %INCLIB%\libjpeg.lib + copy /Y /B cjpeg-static.exe %INCLIB%\cjpeg.exe + copy /Y /B djpeg-static.exe %INCLIB%\djpeg.exe + shell: cmd + + - name: Build dependencies / zlib + run: | + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\zlib-1.2.11 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + nmake -nologo -f win32\Makefile.msc clean + nmake -nologo -f win32\Makefile.msc zlib.lib + copy /Y /B z*.h %INCLIB% + copy /Y /B *.lib %INCLIB% + copy /Y /B zlib.lib %INCLIB%\z.lib + shell: cmd + + - name: Build dependencies / LibTIFF + run: | + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\tiff-4.1.0 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + copy %GITHUB_WORKSPACE%\winbuild\tiff.opt nmake.opt + nmake -nologo -f makefile.vc clean + nmake -nologo -f makefile.vc lib + copy /Y /B libtiff\tiff*.h %INCLIB% + copy /Y /B libtiff\*.dll %INCLIB% + copy /Y /B libtiff\*.lib %INCLIB% + shell: cmd + + - name: Build dependencies / WebP + run: | + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\libwebp-1.0.3 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + rmdir /S /Q output\release-static + nmake -nologo -f Makefile.vc CFG=release-static OBJDIR=output ARCH=${{ matrix.architecture }} all + mkdir %INCLIB%\webp + copy /Y /B src\webp\*.h %INCLIB%\webp + copy /Y /B output\release-static\${{ matrix.architecture }}\lib\* %INCLIB% + shell: cmd + + - name: Build dependencies / FreeType + run: | + REM Toolkit v100 not available; missing VCTargetsPath; Clean fails + + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\freetype-2.10.1 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + rmdir /S /Q objs + set DefaultPlatformToolset=v142 + set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Microsoft\VC\v160\ + set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe" + powershell -Command "(gc builds\windows\vc2010\freetype.vcxproj) -replace 'MultiThreaded<', 'MultiThreadedDLL<' | Out-File -encoding ASCII builds\windows\vc2010\freetype.vcxproj" + %MSBUILD% builds\windows\vc2010\freetype.sln /t:Build /p:Configuration="Release Static" /p:Platform=${{ matrix.platform-msbuild }} /m + xcopy /Y /E /Q include %INCLIB% + copy /Y /B "objs\${{ matrix.platform-msbuild }}\Release Static\freetype.lib" %INCLIB% + shell: cmd + + - name: Build dependencies / LCMS2 + run: | + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\lcms2-2.8 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + rmdir /S /Q Lib + rmdir /S /Q Projects\VC2015\Release + set VCTargetsPath=C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Microsoft\VC\v160\ + set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe" + powershell %GITHUB_WORKSPACE%\winbuild\lcms2_patch.ps1 + %MSBUILD% Projects\VC2015\lcms2.sln /t:Clean;lcms2_static /p:Configuration="Release" /p:Platform=${{ matrix.platform-msbuild }} /m + xcopy /Y /E /Q include %INCLIB% + copy /Y /B Lib\MS\*.lib %INCLIB% + shell: cmd + + - name: Build dependencies / OpenJPEG + run: | + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\openjpeg-2.3.1msvcr10-x32 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF + set CMAKE=%CMAKE% -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF + set CMAKE=%CMAKE% -DCMAKE_BUILD_TYPE=Release + %CMAKE% -G "NMake Makefiles" . + nmake -nologo -f Makefile clean + nmake -nologo -f Makefile + mkdir %INCLIB%\openjpeg-2.3.1 + copy /Y /B src\lib\openjp2\*.h %INCLIB%\openjpeg-2.3.1 + copy /Y /B bin\*.lib %INCLIB% + shell: cmd + + # GPL licensed; skip if building wheels + - name: Build dependencies / libimagequant + if: "github.event_name != 'push' || contains(matrix.python-version, 'pypy')" + run: | + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + rem ba653c8: Merge tag '2.12.5' into msvc + cd /D %BUILD%\libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + echo (gc CMakeLists.txt) -replace 'add_library', "add_compile_options(-openmp-)`r`nadd_library" ^| Out-File -encoding ASCII CMakeLists.txt > patch.ps1 + echo (gc CMakeLists.txt) -replace ' SHARED', ' STATIC' ^| Out-File -encoding ASCII CMakeLists.txt >> patch.ps1 + powershell .\patch.ps1 + set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF + set CMAKE=%CMAKE% -DCMAKE_BUILD_TYPE=Release + %CMAKE% -G "NMake Makefiles" . + nmake -nologo -f Makefile clean + nmake -nologo -f Makefile + copy /Y /B *.h %INCLIB% + copy /Y /B *.lib %INCLIB% + shell: cmd + + # for Raqm + - name: Build dependencies / HarfBuzz + run: | + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + set INCLUDE=%INCLUDE%;%INCLIB% + set LIB=%LIB%;%INCLIB% + cd /D %BUILD%\harfbuzz-2.6.1 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF + set CMAKE=%CMAKE% -DHB_HAVE_FREETYPE:BOOL=ON -DCMAKE_BUILD_TYPE=Release + %CMAKE% -G "NMake Makefiles" . + nmake -nologo -f Makefile clean + nmake -nologo -f Makefile harfbuzz + copy /Y /B src\*.h %INCLIB% + copy /Y /B *.lib %INCLIB% + shell: cmd + + # for Raqm + - name: Build dependencies / FriBidi + run: | + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + cd /D %BUILD%\fribidi-1.0.7 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + copy /Y /B %GITHUB_WORKSPACE%\winbuild\fribidi.cmake CMakeLists.txt + set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF + set CMAKE=%CMAKE% -DCMAKE_BUILD_TYPE=Release + %CMAKE% -G "NMake Makefiles" . + nmake -nologo -f Makefile clean + nmake -nologo -f Makefile fribidi + copy /Y /B lib\*.h %INCLIB% + copy /Y /B *.lib %INCLIB% + shell: cmd + + - name: Build dependencies / Raqm + run: | + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + set BUILD=%GITHUB_WORKSPACE%\winbuild\build + set INCLUDE=%INCLUDE%;%INCLIB% + set LIB=%LIB%;%INCLIB% + cd /D %BUILD%\libraqm-0.7.0 + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + echo on + copy /Y /B %GITHUB_WORKSPACE%\winbuild\raqm.cmake CMakeLists.txt + set CMAKE=cmake.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES:BOOL=OFF + set CMAKE=%CMAKE% -DCMAKE_BUILD_TYPE=Release + %CMAKE% -G "NMake Makefiles" . + nmake -nologo -f Makefile clean + nmake -nologo -f Makefile libraqm + copy /Y /B src\*.h %INCLIB% + copy /Y /B libraqm.dll %INCLIB% + shell: cmd + + - name: Build Pillow + run: | + set PYTHON=%pythonLocation% + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set MPLSRC=%GITHUB_WORKSPACE% + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + cd /D %GITHUB_WORKSPACE% + set LIB=%INCLIB%;%PYTHON%\tcl + set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + set MSSdk=1 + set DISTUTILS_USE_SDK=1 + set py_vcruntime_redist=true + %PYTHON%\python.exe setup.py build_ext install + rem Add libraqm.dll (copied to INCLIB) to PATH. + path %INCLIB%;%PATH% + %PYTHON%\python.exe selftest.py --installed + shell: cmd + + # failing with PyPy3 + - name: Enable heap verification + if: "!contains(matrix.python-version, 'pypy')" + run: | + c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\python.exe + shell: cmd + + - name: Test Pillow + run: | + set PYTHON=%pythonLocation% + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + rem Add libraqm.dll (copied to INCLIB) to PATH. + path %INCLIB%;%PATH% + cd /D %GITHUB_WORKSPACE% + %PYTHON%\python.exe -m pytest -vx --cov PIL --cov-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 + + - name: Build wheel + id: wheel + if: "github.event_name == 'push' && !contains(matrix.python-version, 'pypy')" + run: | + for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ##[set-output name=dist;]dist-%%a + for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a + set PYTHON=%pythonLocation% + set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\V7.1A\Include + set MPLSRC=%GITHUB_WORKSPACE% + set INCLIB=%GITHUB_WORKSPACE%\winbuild\depends\msvcr10-x32 + cd /D %GITHUB_WORKSPACE% + set LIB=%INCLIB%;%PYTHON%\tcl + set INCLUDE=%INCLIB%;%GITHUB_WORKSPACE%\depends\tcl86\include;%INCLUDE% + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.platform-vcvars }} + %PYTHON%\python.exe setup.py bdist_wheel + shell: cmd + + - uses: actions/upload-artifact@v1 + if: "github.event_name == 'push' && !contains(matrix.python-version, 'pypy')" + with: + name: ${{ steps.wheel.outputs.dist }} + path: dist diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..06caa4316 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,76 @@ +name: Test + +on: [push, pull_request] + +jobs: + build: + + strategy: + fail-fast: false + matrix: + os: [ + "ubuntu-latest", + "macOS-latest", + ] + python-version: [ + "pypy3", + "3.8", + "3.7", + "3.6", + "3.5", + ] + include: + - python-version: "3.5" + env: PYTHONOPTIMIZE=2 + - python-version: "3.6" + env: PYTHONOPTIMIZE=1 + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v1 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Linux dependencies + if: startsWith(matrix.os, 'ubuntu') + run: | + .travis/install.sh + + - name: Install macOS dependencies + if: startsWith(matrix.os, 'macOS') + run: | + .github/workflows/macos-install.sh + + - name: Build + run: | + .travis/build.sh + + - name: Test + run: | + .travis/test.sh + + - name: Upload errors + uses: actions/upload-artifact@v1 + if: failure() + with: + name: errors + path: Tests/errors + + - name: Docs + if: matrix.python-version == 3.8 + run: | + pip install sphinx-rtd-theme + make doccheck + + - name: After success + if: success() + 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/.gitignore b/.gitignore index 08955e34f..585120056 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,9 @@ pip-delete-this-directory.txt \#*# .#* +#VS Code +.vscode + #Komodo *.komodoproject diff --git a/.landscape.yaml b/.landscape.yaml deleted file mode 100644 index ddd9cef32..000000000 --- a/.landscape.yaml +++ /dev/null @@ -1,3 +0,0 @@ -strictness: medium -test-warnings: yes -max-line-length: 80 diff --git a/.travis.yml b/.travis.yml index 9550eb8f2..9dea5fb14 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,50 +16,34 @@ matrix: - python: "3.6" name: "Lint" env: LINT="true" - - python: "pypy2.7-6.0" - name: "PyPy2 Xenial" - dist: xenial - - python: "pypy3.5-6.0" + - python: "pypy3" name: "PyPy3 Xenial" - dist: xenial + - python: "3.8" + name: "3.8 Xenial" + services: xvfb - python: '3.7' name: "3.7 Xenial" - - python: '2.7' - name: "2.7 Xenial" - - python: '2.7' - name: "2.7 Trusty" - dist: trusty - - python: "2.7_with_system_site_packages" # For PyQt4 - name: "2.7_with_system_site_packages Xenial" services: xvfb - - python: "2.7_with_system_site_packages" # For PyQt4 - name: "2.7_with_system_site_packages Trusty" - dist: trusty - python: '3.6' - name: "3.6 Xenial" - - python: '3.6' - name: "3.6 Trusty PYTHONOPTIMIZE=1" - dist: trusty + name: "3.6 Xenial PYTHONOPTIMIZE=1" env: PYTHONOPTIMIZE=1 + services: xvfb - python: '3.5' - name: "3.5 Xenial" - - python: '3.5' - name: "3.5 Trusty PYTHONOPTIMIZE=2" - dist: trusty + name: "3.5 Xenial PYTHONOPTIMIZE=2" env: PYTHONOPTIMIZE=2 - - python: "3.8-dev" - name: "3.8-dev Xenial" + services: xvfb - env: DOCKER="alpine" DOCKER_TAG="master" - env: DOCKER="arch" DOCKER_TAG="master" # contains PyQt5 - - env: DOCKER="ubuntu-trusty-x86" DOCKER_TAG="master" - - env: DOCKER="ubuntu-xenial-amd64" DOCKER_TAG="master" - - env: DOCKER="debian-stretch-x86" DOCKER_TAG="master" + - env: DOCKER="ubuntu-16.04-xenial-amd64" DOCKER_TAG="master" + - env: DOCKER="ubuntu-18.04-bionic-amd64" DOCKER_TAG="master" + - env: DOCKER="debian-9-stretch-x86" DOCKER_TAG="master" + - env: DOCKER="debian-10-buster-x86" DOCKER_TAG="master" - env: DOCKER="centos-6-amd64" DOCKER_TAG="master" - env: DOCKER="centos-7-amd64" DOCKER_TAG="master" - env: DOCKER="amazon-1-amd64" DOCKER_TAG="master" - env: DOCKER="amazon-2-amd64" DOCKER_TAG="master" - - env: DOCKER="fedora-28-amd64" DOCKER_TAG="master" - - env: DOCKER="fedora-29-amd64" DOCKER_TAG="master" + - env: DOCKER="fedora-30-amd64" DOCKER_TAG="master" + - env: DOCKER="fedora-31-amd64" DOCKER_TAG="master" services: - docker @@ -75,20 +59,13 @@ install: .travis/install.sh; fi -before_script: -# Qt needs a display for some of the tests, and it's only run on the system site packages install -- | - if [ "$TRAVIS_JOB_NAME" == "2.7_with_system_site_packages Trusty" ]; then - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start - fi - script: - | if [ "$LINT" == "true" ]; then tox -e lint elif [ "$DOCKER" == "" ]; then - .travis/script.sh + .travis/build.sh + .travis/test.sh elif [ "$DOCKER" ]; then # the Pillow user in the docker container is UID 1000 sudo chown -R 1000 $TRAVIS_BUILD_DIR diff --git a/.travis/after_success.sh b/.travis/after_success.sh index ad1aeffa3..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 @@ -15,9 +20,8 @@ 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/build.sh b/.travis/build.sh new file mode 100755 index 000000000..a2e3041bd --- /dev/null +++ b/.travis/build.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +coverage erase +if [ $(uname) == "Darwin" ]; then + export CPPFLAGS="-I/usr/local/miniconda/include"; +fi +make clean +make install-coverage diff --git a/.travis/install.sh b/.travis/install.sh index b123fd302..747acb448 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -4,11 +4,10 @@ set -e sudo apt-get update sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk\ - python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick\ - libharfbuzz-dev libfribidi-dev + ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ + cmake imagemagick libharfbuzz-dev libfribidi-dev PYTHONOPTIMIZE=0 pip install cffi -pip install check-manifest pip install coverage pip install olefile pip install -U pytest @@ -16,22 +15,22 @@ 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 - -# clean checkout for manifest -mkdir /tmp/check-manifest && cp -a . /tmp/check-manifest +# 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 -# openjpeg -pushd depends && ./install_openjpeg.sh && popd - # libimagequant pushd depends && ./install_imagequant.sh && popd +# raqm +pushd depends && ./install_raqm.sh && popd + # extra test images pushd depends && ./install_extra_test_images.sh && popd - diff --git a/.travis/script.sh b/.travis/script.sh deleted file mode 100755 index d6e02f01d..000000000 --- a/.travis/script.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -set -e - -coverage erase -make clean -make install-coverage - -python selftest.py -python -m pytest -vx --cov PIL --cov-report term Tests - -pushd /tmp/check-manifest && check-manifest --ignore ".coveragerc,.editorconfig,*.yml,*.yaml,tox.ini" && popd - -# Docs -if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then make doccheck; fi diff --git a/.travis/test.sh b/.travis/test.sh new file mode 100755 index 000000000..a65e620b8 --- /dev/null +++ b/.travis/test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +python -m pytest -v -x --cov PIL --cov-report term Tests + +# Docs +if [ "$TRAVIS_PYTHON_VERSION" == "3.7" ]; then make doccheck; fi diff --git a/CHANGES.rst b/CHANGES.rst index e1c08d8bd..d3922f328 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,410 @@ Changelog (Pillow) ================== -6.0.0 (unreleased) +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] + +- Fixed freeing unallocated pointer when resizing with height too large #4116 + [radarhere] + +- Copy info in Image.transform #4128 + [radarhere] + +- Corrected DdsImagePlugin setting info gamma #4171 + [radarhere] + +- Depends: Update libtiff to 4.1.0 #4195, Tk Tcl to 8.6.10 #4229 + [radarhere] + +- Improve handling of file resources #3577 + [jdufresne] + +- Removed CI testing of Fedora 29 #4165 + [hugovk] + +- Added pypy3 to tox envlist #4137 + [jdufresne] + +- Drop support for EOL PyQt4 and PySide #4108 + [hugovk, radarhere] + +- Removed deprecated setting of TIFF image sizes #4114 + [radarhere] + +- Removed deprecated PILLOW_VERSION #4107 + [hugovk] + +- Changed default frombuffer raw decoder args #1730 + [radarhere] + +6.2.1 (2019-10-21) +------------------ + +- This is the last Pillow release to support Python 2.7 #3642 + +- Add support for Python 3.8 #4141 + [hugovk] + +6.2.0 (2019-10-01) +------------------ + +- Catch buffer overruns #4104 + [radarhere] + +- Initialize rows_per_strip when RowsPerStrip tag is missing #4034 + [cgohlke, radarhere] + +- Raise error if TIFF dimension is a string #4103 + [radarhere] + +- Added decompression bomb checks #4102 + [radarhere] + +- Fix ImageGrab.grab DPI scaling on Windows 10 version 1607+ #4000 + [nulano, radarhere] + +- Corrected negative seeks #4101 + [radarhere] + +- Added argument to capture all screens on Windows #3950 + [nulano, radarhere] + +- Updated warning to specify when Image.frombuffer defaults will change #4086 + [radarhere] + +- Changed WindowsViewer format to PNG #4080 + [radarhere] + +- Use TIFF orientation #4063 + [radarhere] + +- Raise the same error if a truncated image is loaded a second time #3965 + [radarhere] + +- Lazily use ImageFileDirectory_v1 values from Exif #4031 + [radarhere] + +- Improved HSV conversion #4004 + [radarhere] + +- Added text stroking #3978 + [radarhere, hugovk] + +- No more deprecated bdist_wininst .exe installers #4029 + [hugovk] + +- Do not allow floodfill to extend into negative coordinates #4017 + [radarhere] + +- Fixed arc drawing bug for a non-whole number of degrees #4014 + [radarhere] + +- Fix bug when merging identical images to GIF with a list of durations #4003 + [djy0, radarhere] + +- Fix bug in TIFF loading of BufferedReader #3998 + [chadawagner] + +- Added fallback for finding ld on MinGW Cygwin #4019 + [radarhere] + +- Remove indirect dependencies from requirements.txt #3976 + [hugovk] + +- Depends: Update libwebp to 1.0.3 #3983, libimagequant to 2.12.5 #3993, freetype to 2.10.1 #3991 + [radarhere] + +- Change overflow check to use PY_SSIZE_T_MAX #3964 + [radarhere] + +- Report reason for pytest skips #3942 + [hugovk] + +6.1.0 (2019-07-01) +------------------ + +- Deprecate Image.__del__ #3929 + [jdufresne] + +- Tiff: Add support for JPEG quality #3886 + [olt] + +- Respect the PKG_CONFIG environment variable when building #3928 + [chewi] + +- Use explicit memcpy() to avoid unaligned memory accesses #3225 + [DerDakon] + +- Improve encoding of TIFF tags #3861 + [olt] + +- Update Py_UNICODE to Py_UCS4 #3780 + [nulano] + +- Consider I;16 pixel size when drawing #3899 + [radarhere] + +- Add TIFFTAG_SAMPLEFORMAT to blocklist #3926 + [cgohlke, radarhere] + +- Create GIF deltas from background colour of GIF frames if disposal mode is 2 #3708 + [sircinnamon, radarhere] + +- Added ImageSequence all_frames #3778 + [radarhere] + +- Use unsigned int to store TIFF IFD offsets #3923 + [cgohlke] + +- Include CPPFLAGS when searching for libraries #3819 + [jefferyto] + +- Updated TIFF tile descriptors to match current decoding functionality #3795 + [dmnisson] + +- Added an ``image.entropy()`` method (second revision) #3608 + [fish2000] + +- Pass the correct types to PyArg_ParseTuple #3880 + [QuLogic] + +- Fixed crash when loading non-font bytes #3912 + [radarhere] + +- Fix SPARC memory alignment issues in Pack/Unpack functions #3858 + [kulikjak] + +- Added CMYK;16B and CMYK;16N unpackers #3913 + [radarhere] + +- Fixed bugs in calculating text size #3864 + [radarhere] + +- Add __main__.py to output basic format and support information #3870 + [jdufresne] + +- Added variation font support #3802 + [radarhere] + +- Do not down-convert if image is LA when showing with PNG format #3869 + [radarhere] + +- Improve handling of PSD frames #3759 + [radarhere] + +- Improved ICO and ICNS loading #3897 + [radarhere] + +- Changed Preview application path so that it is no longer static #3896 + [radarhere] + +- Corrected ttb text positioning #3856 + [radarhere] + +- Handle unexpected ICO image sizes #3836 + [radarhere] + +- Fixed bits value for RGB;16N unpackers #3837 + [kkopachev] + +- Travis CI: Add Fedora 30, remove Fedora 28 #3821 + [hugovk] + +- Added reading of CMYK;16L TIFF images #3817 + [radarhere] + +- Fixed dimensions of 1-bit PDFs #3827 + [radarhere] + +- Fixed opening mmap image through Path on Windows #3825 + [radarhere] + +- Fixed ImageDraw arc gaps #3824 + [radarhere] + +- Expand GIF to include frames with extents outside the image size #3822 + [radarhere] + +- Fixed ImageTk getimage #3814 + [radarhere] + +- Fixed bug in decoding large images #3791 + [radarhere] + +- Fixed reading APP13 marker without Photoshop data #3771 + [radarhere] + +- Added option to include layered windows in ImageGrab.grab on Windows #3808 + [radarhere] + +- Detect libimagequant when installed by pacman on MingW #3812 + [radarhere] + +- Fixed raqm layout bug #3787 + [radarhere] + +- Fixed loading font with non-Unicode path on Windows #3785 + [radarhere] + +- Travis CI: Upgrade PyPy from 6.0.0 to 7.1.1 #3783 + [hugovk, johnthagen] + +- Depends: Updated openjpeg to 2.3.1 #3794, raqm to 0.7.0 #3877, libimagequant to 2.12.3 #3889 + [radarhere] + +- Fix numpy bool bug #3790 + [radarhere] + +6.0.0 (2019-04-01) +------------------ + +- Python 2.7 support will be removed in Pillow 7.0.0 #3682 + [hugovk] + +- Add EXIF class #3625 + [radarhere] + +- Add ImageOps exif_transpose method #3687 + [radarhere] + +- Added warnings to deprecated CMSProfile attributes #3615 + [hugovk] + +- Documented reading TIFF multiframe images #3720 + [akuchling] + +- Improved speed of opening an MPO file #3658 + [Glandos] + +- Update palette in quantize #3721 + [radarhere] + +- Improvements to TIFF is_animated and n_frames #3714 + [radarhere] + +- Fixed incompatible pointer type warnings #3754 + [radarhere] + +- Improvements to PA and LA conversion and palette operations #3728 + [radarhere] + +- Consistent DPI rounding #3709 + [radarhere] + +- Change size of MPO image to match frame #3588 + [radarhere] + +- Read Photoshop resolution data #3701 + [radarhere] + +- Ensure image is mutable before saving #3724 + [radarhere] + +- Correct remap_palette documentation #3740 + [radarhere] + +- Promote P images to PA in putalpha #3726 + [radarhere] + +- Allow RGB and RGBA values for new P images #3719 + [radarhere] + +- Fixed TIFF bug when seeking backwards and then forwards #3713 + [radarhere] + +- Cache EXIF information #3498 + [Glandos] + +- Added transparency for all PNG greyscale modes #3744 + [radarhere] + +- Fix deprecation warnings in Python 3.8 #3749 + [radarhere] + +- Fixed GIF bug when rewinding to a non-zero frame #3716 + [radarhere] + +- Only close original fp in __del__ and __exit__ if original fp is exclusive #3683 + [radarhere] + +- Fix BytesWarning in Tests/test_numpy.py #3725 + [jdufresne] + +- Add missing MIME types and extensions #3520 + [pirate486743186] + +- Add I;16 PNG save #3566 + [radarhere] + +- Add support for BMP RGBA bitfield compression #3705 + [radarhere] + +- Added ability to set language for text rendering #3693 + [iwsfutcmd] + +- Only close exclusive fp on Image __exit__ #3698 + [radarhere] + +- Changed EPS subprocess stdout from devnull to None #3635 + [radarhere] + +- Add reading old-JPEG compressed TIFFs #3489 + [kkopachev] + +- Add EXIF support for PNG #3674 + [radarhere] + +- Add option to set dither param on quantize #3699 + [glasnt] + +- Add reading of DDS uncompressed RGB data #3673 + [radarhere] + +- Correct length of Tiff BYTE tags #3672 + [radarhere] + +- Add DIB saving and loading through Image open #3691 + [radarhere] + +- Removed deprecated VERSION #3624 + [hugovk] + +- Fix 'BytesWarning: Comparison between bytes and string' in PdfDict #3580 + [jdufresne] + +- Do not resize in Image.thumbnail if already the destination size #3632 + [radarhere] + +- Replace .seek() magic numbers with io.SEEK_* constants #3572 + [jdufresne] + +- Make ContainerIO.isatty() return a bool, not int #3568 + [jdufresne] + +- Add support to all transpose operations for I;16 modes #3563, #3741 + [radarhere] + +- Deprecate support for PyQt4 and PySide #3655 + [hugovk, radarhere] + +- Add TIFF compression codecs: LZMA, Zstd, WebP #3555 + [cgohlke] + +- Fixed pickling of iTXt class with protocol > 1 #3537 + [radarhere] + +- _util.isPath returns True for pathlib.Path objects #3616 + [wbadart] + - Remove unnecessary unittest.main() boilerplate from test files #3631 [jdufresne] @@ -407,7 +808,7 @@ Changelog (Pillow) - Enable background colour parameter on rotate #3057 [storesource] -- Remove unnecessary `#if 1` directive #3072 +- Remove unnecessary ``#if 1`` directive #3072 [jdufresne] - Remove unused Python class, Path #3070 @@ -944,7 +1345,7 @@ Changelog (Pillow) - Add decompression bomb check to Image.crop #2410 [wiredfool] -- ImageFile: Ensure that the `err_code` variable is initialized in case of exception. #2363 +- ImageFile: Ensure that the ``err_code`` variable is initialized in case of exception. #2363 [alexkiro] - Tiff: Support append_images for saving multipage TIFFs #2406 @@ -1181,7 +1582,7 @@ Changelog (Pillow) - Removed PIL 1.0 era TK readme that concerns Windows 95/NT #2360 [wiredfool] -- Prevent `nose -v` printing docstrings #2369 +- Prevent ``nose -v`` printing docstrings #2369 [hugovk] - Replaced absolute PIL imports with relative imports #2349 @@ -1270,7 +1671,7 @@ Changelog (Pillow) - Test: Faster assert_image_similar #2279 [homm] -- Removed depreciated internal "stretch" method #2276 +- Removed deprecated internal "stretch" method #2276 [homm] - Removed the handles_eof flag in decode.c #2223 @@ -1626,7 +2027,7 @@ Changelog (Pillow) - Changed depends/install_*.sh urls to point to github pillow-depends repo #1983 [wiredfool] -- Allow ICC profile from `encoderinfo` while saving PNGs #1909 +- Allow ICC profile from ``encoderinfo`` while saving PNGs #1909 [homm] - Fix integer overflow on ILP32 systems (32-bit Linux). #1975 @@ -2069,7 +2470,7 @@ Changelog (Pillow) - Added PDF multipage saving #1445 [radarhere] -- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype `file` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343 +- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype ``file`` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343 [radarhere] - Load more broken images #1428 @@ -2561,7 +2962,7 @@ Changelog (Pillow) - Doc cleanup [wiredfool] -- Fix `ImageStat` docs #796 +- Fix ``ImageStat`` docs #796 [akx] - Added docs for ExifTags #794 @@ -2998,7 +3399,7 @@ Changelog (Pillow) - Add RGBA support to ImageColor #309 [yoavweiss] -- Test for `str`, not `"utf-8"` #306 (fixes #304) +- Test for ``str``, not ``"utf-8"`` #306 (fixes #304) [mjpieters] - Fix missing import os in _util.py #303 @@ -3104,7 +3505,7 @@ Changelog (Pillow) - Partial work to add a wrapper for WebPGetFeatures to correctly support #220 (fixes #204) -- Significant performance improvement of `alpha_composite` function #156 +- Significant performance improvement of ``alpha_composite`` function #156 [homm] - Support explicitly disabling features via --disable-* options #240 diff --git a/MANIFEST.in b/MANIFEST.in index 809d0d667..79f4e2adb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ - include *.c include *.h include *.in @@ -9,23 +8,22 @@ include *.sh include *.txt include LICENSE include Makefile +include tox.ini graft Tests graft src graft depends graft winbuild graft docs -prune docs/_static # build/src control detritus exclude .appveyor.yml exclude .coveragerc exclude .codecov.yml exclude .editorconfig -exclude .landscape.yaml exclude .readthedocs.yml -exclude .travis -exclude .travis/* -exclude tox.ini +exclude azure-pipelines.yml global-exclude .git* global-exclude *.pyc global-exclude *.so +prune .azure-pipelines +prune .travis diff --git a/Makefile b/Makefile index 1803e617d..fdde3416b 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ .DEFAULT_GOAL := release-test clean: - python setup.py clean + python3 setup.py clean rm src/PIL/*.so || true rm -r build || true find . -name __pycache__ | xargs rm -r || true @@ -15,8 +15,8 @@ co: done coverage: - python selftest.py - python setup.py test + python3 selftest.py + python3 setup.py test rm -r htmlcov || true coverage report @@ -30,7 +30,7 @@ doccheck: $(MAKE) -C docs linkcheck || true docserve: - cd docs/_build/html && python -mSimpleHTTPServer 2> /dev/null& + cd docs/_build/html && python3 -mSimpleHTTPServer 2> /dev/null& help: @echo "Welcome to Pillow development. Please use \`make \` where is one of" @@ -50,22 +50,22 @@ help: @echo " upload-test build and upload sdists to test.pythonpackages.com" inplace: clean - python setup.py develop build_ext --inplace + python3 setup.py develop build_ext --inplace install: - python setup.py install - python selftest.py + python3 setup.py install + python3 selftest.py install-coverage: - CFLAGS="-coverage" python setup.py build_ext install - python selftest.py + CFLAGS="-coverage" python3 setup.py build_ext install + python3 selftest.py debug: # make a debug version if we don't have a -dbg python. Leaves in symbols # for our stuff, kills optimization, and redirects to dev null so we # see any build failures. make clean > /dev/null - CFLAGS='-g -O0' python setup.py build_ext install > /dev/null + CFLAGS='-g -O0' python3 setup.py build_ext install > /dev/null install-req: pip install -r requirements.txt @@ -76,17 +76,17 @@ install-venv: release-test: $(MAKE) install-req - python setup.py develop - python selftest.py - python -m pytest Tests - python setup.py install - python -m pytest -qq + python3 setup.py develop + python3 selftest.py + python3 -m pytest Tests + python3 setup.py install + python3 -m pytest -qq check-manifest pyroma . viewdoc sdist: - python setup.py sdist --format=gztar + python3 setup.py sdist --format=gztar test: pytest -qq @@ -97,10 +97,10 @@ upload-test: # username: # password: # repository = http://test.pythonpackages.com - python setup.py sdist --format=gztar upload -r test + python3 setup.py sdist --format=gztar upload -r test upload: - python setup.py sdist --format=gztar upload + python3 setup.py sdist --format=gztar upload readme: viewdoc diff --git a/README.rst b/README.rst index 381859c18..b8cad5e9d 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ Pillow Python Imaging Library (Fork) ----------------------------- -Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. As of 2019, Pillow development is `supported by Tidelift `_. .. start-badges @@ -14,58 +14,14 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors `_ - `Pre-fork `_ + +Report a Vulnerability +---------------------- + +To report a security vulnerability, please follow the procedure described in the `Tidelift security policy `_. + +.. |docs| image:: https://readthedocs.org/projects/pillow/badge/?version=latest + :target: https://pillow.readthedocs.io/?badge=latest + :alt: Documentation Status + +.. |linux| image:: https://img.shields.io/travis/python-pillow/Pillow/master.svg?label=Linux%20build + :target: https://travis-ci.org/python-pillow/Pillow + :alt: Travis CI build status (Linux) + +.. |macos| image:: https://img.shields.io/travis/python-pillow/pillow-wheels/master.svg?label=macOS%20build + :target: https://travis-ci.org/python-pillow/pillow-wheels + :alt: Travis CI build status (macOS) + +.. |windows| image:: https://img.shields.io/appveyor/ci/python-pillow/Pillow/master.svg?label=Windows%20build + :target: https://ci.appveyor.com/project/python-pillow/Pillow + :alt: AppVeyor CI build status (Windows) + +.. |gha_lint| image:: https://github.com/python-pillow/Pillow/workflows/Lint/badge.svg + :alt: GitHub Actions build status (Lint) + +.. |gha_docker| image:: https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg + :alt: GitHub Actions build status (Test Docker) + +.. |gha| image:: https://github.com/python-pillow/Pillow/workflows/Test/badge.svg + :alt: GitHub Actions build status (Test Linux and macOS) + +.. |gha_windows| image:: https://github.com/python-pillow/Pillow/workflows/Test%20Windows/badge.svg + :alt: GitHub Actions build status (Test Windows) + +.. |coverage| image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg + :target: https://codecov.io/gh/python-pillow/Pillow + :alt: Code coverage + +.. |zenodo| image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg + :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow + +.. |tidelift| image:: https://tidelift.com/badges/package/pypi/Pillow?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=referral&utm_campaign=readme + +.. |version| image:: https://img.shields.io/pypi/v/pillow.svg + :target: https://pypi.org/project/Pillow/ + :alt: Latest PyPI version + +.. |downloads| image:: https://img.shields.io/pypi/dm/pillow.svg + :target: https://pypi.org/project/Pillow/ + :alt: Number of PyPI downloads + +.. |gitter| image:: https://badges.gitter.im/python-pillow/Pillow.svg + :target: https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge + :alt: Join the chat at https://gitter.im/python-pillow/Pillow + +.. |twitter| image:: https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg + :target: https://twitter.com/PythonPillow + :alt: Follow on https://twitter.com/PythonPillow diff --git a/RELEASING.md b/RELEASING.md index 40a48c4d3..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 @@ -88,18 +89,11 @@ Released as needed privately to individual vendors for critical security-related ```bash git clone https://github.com/python-pillow/pillow-wheels cd pillow-wheels - git submodule init - git submodule update Pillow - cd Pillow - git fetch --all - git checkout [[release tag]] - cd .. - git commit -m "Pillow -> 5.2.0" Pillow - git push + ./update-pillow-tag.sh [[release tag]] ``` * [ ] Download distributions from the [Pillow Wheel Builder container](http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com/). ```bash - wget -m -A 'Pillow-*' \ + wget -m -A 'Pillow--*' \ http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com ``` @@ -109,4 +103,4 @@ Released as needed privately to individual vendors for critical security-related ## Documentation -* [ ] Make sure the default version for Read the Docs is the latest tagged release e.g. `d2d43879` (5.4.0) +* [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py index a601f762e..26a91d5cd 100755 --- a/Tests/32bit_segfault_check.py +++ b/Tests/32bit_segfault_check.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -from PIL import Image import sys +from PIL import Image -if sys.maxsize < 2**32: - im = Image.new('L', (999999, 999999), 0) +if sys.maxsize < 2 ** 32: + im = Image.new("L", (999999, 999999), 0) diff --git a/Tests/README.rst b/Tests/README.rst index d64a34bd7..da3297bce 100644 --- a/Tests/README.rst +++ b/Tests/README.rst @@ -4,7 +4,7 @@ Pillow Tests Test scripts are named ``test_xxx.py`` and use the ``unittest`` module. A base class and helper functions can be found in ``helper.py``. Dependencies ------------ +------------ Install:: diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 79427dca3..1797d34fc 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -1,10 +1,11 @@ -from .helper import unittest, PillowTestCase, hopper - -# Not running this test by default. No DOS against Travis CI. +import time +import unittest from PIL import PyAccess -import time +from .helper import PillowTestCase, hopper + +# Not running this test by default. No DOS against Travis CI. def iterate_get(size, access): @@ -26,18 +27,21 @@ def timer(func, label, *args): starttime = time.time() for x in range(iterations): func(*args) - if time.time()-starttime > 10: - print("%s: breaking at %s iterations, %.6f per iteration" % ( - label, x+1, (time.time()-starttime)/(x+1.0))) + if time.time() - starttime > 10: + print( + "%s: breaking at %s iterations, %.6f per iteration" + % (label, x + 1, (time.time() - starttime) / (x + 1.0)) + ) break - if x == iterations-1: + if x == iterations - 1: endtime = time.time() - print("%s: %.4f s %.6f per iteration" % ( - label, endtime-starttime, (endtime-starttime)/(x+1.0))) + print( + "%s: %.4f s %.6f per iteration" + % (label, endtime - starttime, (endtime - starttime) / (x + 1.0)) + ) class BenchCffiAccess(PillowTestCase): - def test_direct(self): im = hopper() im.load() @@ -48,11 +52,11 @@ class BenchCffiAccess(PillowTestCase): self.assertEqual(caccess[(0, 0)], access[(0, 0)]) print("Size: %sx%s" % im.size) - timer(iterate_get, 'PyAccess - get', im.size, access) - timer(iterate_set, 'PyAccess - set', im.size, access) - timer(iterate_get, 'C-api - get', im.size, caccess) - timer(iterate_set, 'C-api - set', im.size, caccess) + timer(iterate_get, "PyAccess - get", im.size, access) + timer(iterate_set, "PyAccess - set", im.size, access) + timer(iterate_get, "C-api - get", im.size, caccess) + timer(iterate_set, "C-api - set", im.size, caccess) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/Tests/bench_get.py b/Tests/bench_get.py index 68ac2c9a2..8a54ff921 100644 --- a/Tests/bench_get.py +++ b/Tests/bench_get.py @@ -1,7 +1,8 @@ -from . import helper +import sys import timeit -import sys +from . import helper + sys.path.insert(0, ".") diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 3f7c58015..fa6037c2e 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -1,6 +1,9 @@ -from .helper import unittest, PillowTestCase +import unittest + from PIL import Image +from .helper import PillowTestCase + TEST_FILE = "Tests/images/fli_overflow.fli" @@ -12,5 +15,5 @@ class TestFliOverflow(PillowTestCase): im.load() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/Tests/check_icns_dos.py b/Tests/check_icns_dos.py index d4c3cf7cb..3f4fb6518 100644 --- a/Tests/check_icns_dos.py +++ b/Tests/check_icns_dos.py @@ -1,12 +1,8 @@ # Tests potential DOS of IcnsImagePlugin with 0 length block. # Run from anywhere that PIL is importable. -from PIL import Image -from PIL._util import py3 from io import BytesIO -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'))) +from PIL import Image + +Image.open(BytesIO(b"icns\x00\x00\x00\x10hang\x00\x00\x00\x00")) diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 7fa0663e8..2c1793a4f 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,19 +1,19 @@ #!/usr/bin/env python +import unittest -from __future__ import division -from .helper import unittest, PillowTestCase -import sys from PIL import Image +from .helper import PillowTestCase, is_win32 + min_iterations = 100 max_iterations = 10000 -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or macOS") +@unittest.skipIf(is_win32(), "requires Unix or macOS") class TestImagingLeaks(PillowTestCase): - def _get_mem_usage(self): from resource import getpagesize, getrusage, RUSAGE_SELF + mem = getrusage(RUSAGE_SELF).ru_maxrss return mem * getpagesize() / 1024 / 1024 @@ -25,20 +25,22 @@ class TestImagingLeaks(PillowTestCase): if i < min_iterations: mem_limit = mem + 1 continue - msg = 'memory usage limit exceeded after %d iterations' % (i + 1) + msg = "memory usage limit exceeded after %d iterations" % (i + 1) self.assertLessEqual(mem, mem_limit, msg) def test_leak_putdata(self): - im = Image.new('RGB', (25, 25)) - self._test_leak(min_iterations, max_iterations, - im.putdata, im.getdata()) + im = Image.new("RGB", (25, 25)) + self._test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) def test_leak_getlist(self): - im = Image.new('P', (25, 25)) - self._test_leak(min_iterations, max_iterations, - # Pass a new list at each iteration. - lambda: im.point(range(256))) + im = Image.new("P", (25, 25)) + self._test_leak( + min_iterations, + max_iterations, + # Pass a new list at each iteration. + lambda: im.point(range(256)), + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/Tests/check_j2k_dos.py b/Tests/check_j2k_dos.py index 4ea31cec2..273c18585 100644 --- a/Tests/check_j2k_dos.py +++ b/Tests/check_j2k_dos.py @@ -1,15 +1,8 @@ # Tests potential DOS of Jpeg2kImagePlugin with 0 length block. # Run from anywhere that PIL is importable. -from PIL import Image -from PIL._util import py3 from io import BytesIO -if py3: - Image.open(BytesIO(bytes( - '\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang', - 'latin-1'))) +from PIL import Image -else: - Image.open(BytesIO(bytes( - '\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang'))) +Image.open(BytesIO(b"\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang")) diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index d87b7f041..1635f1001 100755 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,24 +1,27 @@ -from .helper import unittest, PillowTestCase -import sys -from PIL import Image +import unittest from io import BytesIO +from PIL import Image + +from .helper import PillowTestCase, is_win32 + # Limits for testing the leak -mem_limit = 1024*1048576 -stack_size = 8*1048576 -iterations = int((mem_limit/stack_size)*2) +mem_limit = 1024 * 1048576 +stack_size = 8 * 1048576 +iterations = int((mem_limit / stack_size) * 2) codecs = dir(Image.core) test_file = "Tests/images/rgb_trns_ycbc.jp2" -@unittest.skipIf(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: - self.skipTest('JPEG 2000 support not available') + self.skipTest("JPEG 2000 support not available") def test_leak_load(self): from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) for _ in range(iterations): @@ -27,6 +30,7 @@ class TestJpegLeaks(PillowTestCase): def test_leak_save(self): from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) for _ in range(iterations): @@ -38,5 +42,5 @@ class TestJpegLeaks(PillowTestCase): test_output.read() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index f456ebb32..d5b6e455f 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,15 +1,18 @@ +import unittest + from PIL import Image -from .helper import unittest, PillowTestCase + +from .helper import PillowTestCase class TestJ2kEncodeOverflow(PillowTestCase): def test_j2k_overflow(self): - im = Image.new('RGBA', (1024, 131584)) - target = self.tempfile('temp.jpc') + im = Image.new("RGBA", (1024, 131584)) + target = self.tempfile("temp.jpc") with self.assertRaises(IOError): im.save(target) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 97a5650e0..6b2801a21 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,6 +1,7 @@ -from .helper import unittest, PillowTestCase, hopper +import unittest from io import BytesIO -import sys + +from .helper import PillowTestCase, hopper, is_win32 iterations = 5000 @@ -14,7 +15,7 @@ valgrind --tool=massif python test-installed.py -s -v Tests/check_jpeg_leaks.py """ -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or macOS") +@unittest.skipIf(is_win32(), "requires Unix or macOS") class TestJpegLeaks(PillowTestCase): """ @@ -74,9 +75,11 @@ post-patch: """ def test_qtables_leak(self): - im = hopper('RGB') + im = hopper("RGB") - standard_l_qtable = [int(s) for s in """ + 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 @@ -85,9 +88,14 @@ post-patch: 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)] + """.split( + None + ) + ] - standard_chrominance_qtable = [int(s) for s in """ + 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 @@ -96,10 +104,12 @@ post-patch: 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)] + """.split( + None + ) + ] - qtables = [standard_l_qtable, - standard_chrominance_qtable] + qtables = [standard_l_qtable, standard_chrominance_qtable] for _ in range(iterations): test_output = BytesIO() @@ -161,8 +171,8 @@ post patch: 0 11.33 """ - im = hopper('RGB') - exif = b'12345678'*4096 + im = hopper("RGB") + exif = b"12345678" * 4096 for _ in range(iterations): test_output = BytesIO() @@ -195,12 +205,12 @@ base case: 0 +----------------------------------------------------------------------->Gi 0 7.882 """ - im = hopper('RGB') + im = hopper("RGB") for _ in range(iterations): test_output = BytesIO() im.save(test_output, "JPEG") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 24d687ea6..7fcaa4cf9 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,6 +1,9 @@ import sys +import unittest -from .helper import unittest, PillowTestCase +from PIL import Image + +from .helper import PillowTestCase # This test is not run automatically. # @@ -11,17 +14,21 @@ from .helper import unittest, PillowTestCase # Raspberry Pis). It does succeed on a 3gb Ubuntu 12.04x64 VM on Python # 2.7 and 3.2. -from PIL import Image + +try: + import numpy +except ImportError: + numpy = None + YDIM = 32769 XDIM = 48000 -@unittest.skipIf(sys.maxsize <= 2**32, "requires 64-bit system") +@unittest.skipIf(sys.maxsize <= 2 ** 32, "requires 64-bit system") class LargeMemoryTest(PillowTestCase): - def _write_png(self, xdim, ydim): - f = self.tempfile('temp.png') - im = Image.new('L', (xdim, ydim), 0) + f = self.tempfile("temp.png") + im = Image.new("L", (xdim, ydim), 0) im.save(f) def test_large(self): @@ -32,6 +39,11 @@ class LargeMemoryTest(PillowTestCase): """failed prepatch""" self._write_png(XDIM, XDIM) + @unittest.skipIf(numpy is None, "Numpy is not installed") + def test_size_greater_than_int(self): + arr = numpy.ndarray(shape=(16394, 16394)) + Image.fromarray(arr) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index b66988fd5..8e65dc1cb 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,6 +1,9 @@ import sys +import unittest -from .helper import unittest, PillowTestCase +from PIL import Image + +from .helper import PillowTestCase # This test is not run automatically. # @@ -10,7 +13,7 @@ from .helper import unittest, PillowTestCase # on any 32-bit machine, as well as any smallish things (like # Raspberry Pis). -from PIL import Image + try: import numpy as np except ImportError: @@ -20,14 +23,13 @@ YDIM = 32769 XDIM = 48000 -@unittest.skipIf(sys.maxsize <= 2**32, "requires 64-bit system") +@unittest.skipIf(sys.maxsize <= 2 ** 32, "requires 64-bit system") class LargeMemoryNumpyTest(PillowTestCase): - def _write_png(self, xdim, ydim): dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) - f = self.tempfile('temp.png') - im = Image.fromarray(a, 'L') + f = self.tempfile("temp.png") + im = Image.fromarray(a, "L") im.save(f) def test_large(self): @@ -39,5 +41,5 @@ class LargeMemoryNumpyTest(PillowTestCase): self._write_png(XDIM, XDIM) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index f8c4a3090..a0263f725 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -1,6 +1,9 @@ -from .helper import unittest, PillowTestCase +import unittest + from PIL import Image +from .helper import PillowTestCase + TEST_FILE = "Tests/images/libtiff_segfault.tif" @@ -15,5 +18,5 @@ class TestLibtiffSegfault(PillowTestCase): im.load() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 9a446bf84..f133b2695 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -1,7 +1,10 @@ -from .helper import unittest, PillowTestCase -from PIL import Image, PngImagePlugin, ImageFile -from io import BytesIO +import unittest import zlib +from io import BytesIO + +from PIL import Image, ImageFile, PngImagePlugin + +from .helper import PillowTestCase TEST_FILE = "Tests/images/png_decompression_dos.png" @@ -17,10 +20,10 @@ class TestPngDos(PillowTestCase): ImageFile.LOAD_TRUNCATED_IMAGES = False for s in im.text.values(): - self.assertLess(len(s), 1024*1024, "Text chunk larger than 1M") + self.assertLess(len(s), 1024 * 1024, "Text chunk larger than 1M") for s in im.info.values(): - self.assertLess(len(s), 1024*1024, "Text chunk larger than 1M") + self.assertLess(len(s), 1024 * 1024, "Text chunk larger than 1M") def test_dos_text(self): @@ -32,20 +35,20 @@ class TestPngDos(PillowTestCase): return for s in im.text.values(): - self.assertLess(len(s), 1024*1024, "Text chunk larger than 1M") + self.assertLess(len(s), 1024 * 1024, "Text chunk larger than 1M") def test_dos_total_memory(self): - im = Image.new('L', (1, 1)) - compressed_data = zlib.compress(b'a'*1024*1023) + im = Image.new("L", (1, 1)) + compressed_data = zlib.compress(b"a" * 1024 * 1023) info = PngImagePlugin.PngInfo() for x in range(64): - info.add_text('t%s' % x, compressed_data, zip=True) - info.add_itxt('i%s' % x, compressed_data, zip=True) + info.add_text("t%s" % x, compressed_data, zip=True) + info.add_itxt("i%s" % x, compressed_data, zip=True) b = BytesIO() - im.save(b, 'PNG', pnginfo=info) + im.save(b, "PNG", pnginfo=info) b.seek(0) try: @@ -57,9 +60,10 @@ class TestPngDos(PillowTestCase): total_len = 0 for txt in im2.text.values(): total_len += len(txt) - self.assertLess(total_len, 64*1024*1024, - "Total text chunks greater than 64M") + self.assertLess( + total_len, 64 * 1024 * 1024, "Total text chunks greater than 64M" + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/Tests/conftest.py b/Tests/conftest.py new file mode 100644 index 000000000..2b796148d --- /dev/null +++ b/Tests/conftest.py @@ -0,0 +1,11 @@ +def pytest_report_header(config): + import io + + try: + from PIL import features + + with io.StringIO() as out: + features.pilinfo(out=out, supported_formats=False) + return out.getvalue() + except Exception as e: + return "pytest_report_header failed: %s" % str(e) diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index bace38a23..c7055995e 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -from __future__ import print_function import base64 import os diff --git a/Tests/fonts/AdobeVFPrototype.ttf b/Tests/fonts/AdobeVFPrototype.ttf new file mode 100644 index 000000000..64f5ea8e1 Binary files /dev/null and b/Tests/fonts/AdobeVFPrototype.ttf differ diff --git a/Tests/fonts/ArefRuqaa-Regular.ttf b/Tests/fonts/ArefRuqaa-Regular.ttf new file mode 100644 index 000000000..940cb58f4 Binary files /dev/null and b/Tests/fonts/ArefRuqaa-Regular.ttf differ diff --git a/Tests/fonts/KhmerOSBattambang-Regular.ttf b/Tests/fonts/KhmerOSBattambang-Regular.ttf new file mode 100755 index 000000000..b812c0af1 Binary files /dev/null and b/Tests/fonts/KhmerOSBattambang-Regular.ttf differ diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index ee9daee59..726d5d797 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -1,13 +1,13 @@ -NotoNastaliqUrdu-Regular.ttf: +NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts +NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/ +AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype +TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny +ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa -(from https://github.com/googlei18n/noto-fonts) - -All Noto fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. +All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. -10x20-ISO8859-1.pcf - -(from https://packages.ubuntu.com/xenial/xfonts-base) +10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base "Public domain font. Share and enjoy." diff --git a/Tests/fonts/NotoSansJP-Regular.otf b/Tests/fonts/NotoSansJP-Regular.otf new file mode 100644 index 000000000..fbccd9f16 Binary files /dev/null and b/Tests/fonts/NotoSansJP-Regular.otf differ diff --git a/Tests/fonts/NotoSansSymbols-Regular.ttf b/Tests/fonts/NotoSansSymbols-Regular.ttf new file mode 100644 index 000000000..92accef72 Binary files /dev/null and b/Tests/fonts/NotoSansSymbols-Regular.ttf differ diff --git a/Tests/fonts/TINY5x3GX.ttf b/Tests/fonts/TINY5x3GX.ttf new file mode 100755 index 000000000..bd6e208de Binary files /dev/null and b/Tests/fonts/TINY5x3GX.ttf differ diff --git a/Tests/helper.py b/Tests/helper.py index b47604a60..0f8e05c19 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -1,33 +1,51 @@ """ Helper functions. """ -from __future__ import print_function -import sys -import tempfile -import os -import unittest - -from PIL import Image, ImageMath -from PIL._util import py3 import logging +import os +import subprocess +import sys +import tempfile +import unittest +from io import BytesIO + +from PIL import Image, ImageMath + logger = logging.getLogger(__name__) HAS_UPLOADER = False -if os.environ.get('SHOW_ERRORS', None): +if os.environ.get("SHOW_ERRORS", None): # local img.show for errors. 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 + HAS_UPLOADER = True except ImportError: pass @@ -35,72 +53,60 @@ else: def convert_to_comparable(a, b): new_a, new_b = a, b - if a.mode == 'P': - new_a = Image.new('L', a.size) - new_b = Image.new('L', b.size) + if a.mode == "P": + new_a = Image.new("L", a.size) + new_b = Image.new("L", b.size) new_a.putdata(a.getdata()) new_b.putdata(b.getdata()) - elif a.mode == 'I;16': - new_a = a.convert('I') - new_b = b.convert('I') + elif a.mode == "I;16": + new_a = a.convert("I") + new_b = b.convert("I") return new_a, new_b class PillowTestCase(unittest.TestCase): - - def __init__(self, *args, **kwargs): - unittest.TestCase.__init__(self, *args, **kwargs) - # holds last result object passed to run method: - self.currentResult = None - - def run(self, result=None): - self.currentResult = result # remember result for use later - unittest.TestCase.run(self, result) # call superclass run method - def delete_tempfile(self, path): try: - ok = self.currentResult.wasSuccessful() - except AttributeError: # for pytest - ok = True - - if ok: - # only clean out tempfiles if test passed - try: - os.remove(path) - except OSError: - pass # report? - else: - print("=== orphaned temp file: %s" % path) + os.remove(path) + except OSError: + pass # report? def assert_deep_equal(self, a, b, msg=None): try: self.assertEqual( - len(a), len(b), - msg or "got length %s, expected %s" % (len(a), len(b))) + len(a), + len(b), + msg or "got length {}, expected {}".format(len(a), len(b)), + ) self.assertTrue( all(x == y for x, y in zip(a, b)), - msg or "got %s, expected %s" % (a, b)) + msg or "got {}, expected {}".format(a, b), + ) except Exception: self.assertEqual(a, b, msg) def assert_image(self, im, mode, size, msg=None): if mode is not None: self.assertEqual( - im.mode, mode, - msg or "got mode %r, expected %r" % (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: try: @@ -120,26 +126,28 @@ 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) diff = 0 for ach, bch in zip(a.split(), b.split()): - chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert('L') + chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert("L") diff += sum(i * num for i, num in enumerate(chdiff.histogram())) - ave_diff = float(diff)/(a.size[0]*a.size[1]) + ave_diff = float(diff) / (a.size[0] * a.size[1]) try: self.assertGreaterEqual( - epsilon, ave_diff, - (msg or '') + - " average pixel value difference %.4f > epsilon %.4f" % ( - ave_diff, epsilon)) + epsilon, + ave_diff, + (msg or "") + + " average pixel value difference %.4f > epsilon %.4f" + % (ave_diff, epsilon), + ) except Exception as e: if HAS_UPLOADER: try: @@ -149,8 +157,7 @@ class PillowTestCase(unittest.TestCase): pass raise e - def assert_image_similar_tofile(self, a, filename, epsilon, msg=None, - mode=None): + def assert_image_similar_tofile(self, a, filename, epsilon, msg=None, mode=None): with Image.open(filename) as img: if mode: img = img.convert(mode) @@ -168,9 +175,9 @@ class PillowTestCase(unittest.TestCase): # Verify some things. if warn_class is None: - self.assertEqual(len(w), 0, - "Expected no warnings, got %s" % - [v.category for v in w]) + self.assertEqual( + len(w), 0, "Expected no warnings, got %s" % [v.category for v in w] + ) else: self.assertGreaterEqual(len(w), 1) found = False @@ -192,29 +199,17 @@ class PillowTestCase(unittest.TestCase): value = True for i, target in enumerate(targets): - value *= (target - threshold <= actuals[i] <= target + threshold) + value *= target - threshold <= actuals[i] <= target + threshold - self.assertTrue(value, - msg + ': ' + repr(actuals) + ' != ' + repr(targets)) + self.assertTrue(value, msg + ": " + repr(actuals) + " != " + repr(targets)) - def skipKnownBadTest(self, msg=None, platform=None, - travis=None, interpreter=None): - # Skip if platform/travis matches, and - # PILLOW_RUN_KNOWN_BAD is not true in the environment. - if os.environ.get('PILLOW_RUN_KNOWN_BAD', False): - print(os.environ.get('PILLOW_RUN_KNOWN_BAD', False)) + 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_") @@ -226,15 +221,15 @@ 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") +@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") class PillowLeakTestCase(PillowTestCase): # requires unix/macOS iterations = 100 # count @@ -249,8 +244,9 @@ class PillowLeakTestCase(PillowTestCase): """ from resource import getrusage, RUSAGE_SELF + mem = getrusage(RUSAGE_SELF).ru_maxrss - if sys.platform == 'darwin': + if sys.platform == "darwin": # man 2 getrusage: # ru_maxrss # This is the maximum resident set size utilized (in bytes). @@ -266,26 +262,19 @@ class PillowLeakTestCase(PillowTestCase): start_mem = self._get_mem_usage() for cycle in range(self.iterations): core() - mem = (self._get_mem_usage() - start_mem) - msg = 'memory usage limit exceeded in iteration %d' % cycle + mem = self._get_mem_usage() - start_mem + msg = "memory usage limit exceeded in iteration %d" % cycle self.assertLess(mem, self.mem_limit, msg) # helpers -if not py3: - # Remove DeprecationWarning in Python 3 - PillowTestCase.assertRaisesRegex = PillowTestCase.assertRaisesRegexp - PillowTestCase.assertRegex = PillowTestCase.assertRegexpMatches - def fromstring(data): - from io import BytesIO return Image.open(BytesIO(data)) def tostring(im, string_format, **options): - from io import BytesIO out = BytesIO() im.save(out, string_format, **options) return out.getvalue() @@ -317,53 +306,73 @@ def command_succeeds(cmd): Runs the command, which must be a list of strings. Returns True if the command succeeds, or False if an OSError was raised by subprocess.Popen. """ - import subprocess - with open(os.devnull, 'wb') as f: - try: - subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT) - except OSError: - return False + try: + subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + except OSError: + return False return True def djpeg_available(): - return command_succeeds(['djpeg', '-version']) + return command_succeeds(["djpeg", "-version"]) def cjpeg_available(): - return command_succeeds(['cjpeg', '-version']) + return command_succeeds(["cjpeg", "-version"]) def netpbm_available(): - return (command_succeeds(["ppmquant", "--version"]) and - command_succeeds(["ppmtogif", "--version"])) + return command_succeeds(["ppmquant", "--version"]) and command_succeeds( + ["ppmtogif", "--version"] + ) def imagemagick_available(): - return IMCONVERT and command_succeeds([IMCONVERT, '-version']) + return IMCONVERT and command_succeeds([IMCONVERT, "-version"]) def on_appveyor(): - return 'APPVEYOR' in os.environ + return "APPVEYOR" in os.environ -if sys.platform == 'win32': - IMCONVERT = os.environ.get('MAGICK_HOME', '') +def on_github_actions(): + return "GITHUB_ACTIONS" in os.environ + + +def on_ci(): + # Travis and AppVeyor have "CI" + # Azure Pipelines has "TF_BUILD" + # GitHub Actions has "GITHUB_ACTIONS" + return ( + "CI" in os.environ or "TF_BUILD" in os.environ or "GITHUB_ACTIONS" in os.environ + ) + + +def is_win32(): + return sys.platform.startswith("win32") + + +def is_pypy(): + return hasattr(sys, "pypy_translation_info") + + +if sys.platform == "win32": + IMCONVERT = os.environ.get("MAGICK_HOME", "") if IMCONVERT: - IMCONVERT = os.path.join(IMCONVERT, 'convert.exe') + IMCONVERT = os.path.join(IMCONVERT, "convert.exe") else: - IMCONVERT = 'convert' + IMCONVERT = "convert" def distro(): - if os.path.exists('/etc/os-release'): - with open('/etc/os-release', 'r') as f: + if os.path.exists("/etc/os-release"): + with open("/etc/os-release", "r") as f: for line in f: - if 'ID=' in line: - return line.strip().split('=')[1] + if "ID=" in line: + return line.strip().split("=")[1] -class cached_property(object): +class cached_property: def __init__(self, func): self.func = func diff --git a/Tests/images/1_trns.png b/Tests/images/1_trns.png new file mode 100644 index 000000000..c9a271b40 Binary files /dev/null and b/Tests/images/1_trns.png differ diff --git a/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds b/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds new file mode 100644 index 000000000..9b4d8e21f Binary files /dev/null and b/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds differ diff --git a/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.png b/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.png new file mode 100644 index 000000000..57177fe2b Binary files /dev/null and b/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.png differ diff --git a/Tests/images/app13.jpg b/Tests/images/app13.jpg new file mode 100644 index 000000000..b02d71b40 Binary files /dev/null and b/Tests/images/app13.jpg differ diff --git a/Tests/images/combined_larger_than_size.psd b/Tests/images/combined_larger_than_size.psd new file mode 100644 index 000000000..2e6caef39 Binary files /dev/null and b/Tests/images/combined_larger_than_size.psd differ diff --git a/Tests/images/decompression_bomb.gif b/Tests/images/decompression_bomb.gif new file mode 100644 index 000000000..3ca21b60a Binary files /dev/null and b/Tests/images/decompression_bomb.gif differ diff --git a/Tests/images/decompression_bomb.ico b/Tests/images/decompression_bomb.ico new file mode 100644 index 000000000..0efc9eaf7 Binary files /dev/null and b/Tests/images/decompression_bomb.ico differ diff --git a/Tests/images/drawing_roundDown.emf b/Tests/images/drawing_roundDown.emf new file mode 100644 index 000000000..6c3e20248 Binary files /dev/null and b/Tests/images/drawing_roundDown.emf differ diff --git a/Tests/images/exif.png b/Tests/images/exif.png new file mode 100644 index 000000000..0388b6b8a Binary files /dev/null and b/Tests/images/exif.png differ diff --git a/Tests/images/fli_overrun.bin b/Tests/images/fli_overrun.bin new file mode 100644 index 000000000..e1e8c5901 Binary files /dev/null and b/Tests/images/fli_overrun.bin differ diff --git a/Tests/images/fujifilm.mpo b/Tests/images/fujifilm.mpo new file mode 100644 index 000000000..ff0deb8a6 Binary files /dev/null and b/Tests/images/fujifilm.mpo differ diff --git a/Tests/images/g4_orientation_1.tif b/Tests/images/g4_orientation_1.tif new file mode 100755 index 000000000..8ab0f1d0d Binary files /dev/null and b/Tests/images/g4_orientation_1.tif differ diff --git a/Tests/images/g4_orientation_2.tif b/Tests/images/g4_orientation_2.tif new file mode 100755 index 000000000..4ab085641 Binary files /dev/null and b/Tests/images/g4_orientation_2.tif differ diff --git a/Tests/images/g4_orientation_3.tif b/Tests/images/g4_orientation_3.tif new file mode 100755 index 000000000..ca0d0fe29 Binary files /dev/null and b/Tests/images/g4_orientation_3.tif differ diff --git a/Tests/images/g4_orientation_4.tif b/Tests/images/g4_orientation_4.tif new file mode 100755 index 000000000..166381fb7 Binary files /dev/null and b/Tests/images/g4_orientation_4.tif differ diff --git a/Tests/images/g4_orientation_5.tif b/Tests/images/g4_orientation_5.tif new file mode 100755 index 000000000..9fecaad65 Binary files /dev/null and b/Tests/images/g4_orientation_5.tif differ diff --git a/Tests/images/g4_orientation_6.tif b/Tests/images/g4_orientation_6.tif new file mode 100755 index 000000000..6abc001eb Binary files /dev/null and b/Tests/images/g4_orientation_6.tif differ diff --git a/Tests/images/g4_orientation_7.tif b/Tests/images/g4_orientation_7.tif new file mode 100755 index 000000000..0babc9108 Binary files /dev/null and b/Tests/images/g4_orientation_7.tif differ diff --git a/Tests/images/g4_orientation_8.tif b/Tests/images/g4_orientation_8.tif new file mode 100755 index 000000000..3216a3725 Binary files /dev/null and b/Tests/images/g4_orientation_8.tif differ diff --git a/Tests/images/hopper.pnm b/Tests/images/hopper.pnm new file mode 100644 index 000000000..52368b2e2 Binary files /dev/null and b/Tests/images/hopper.pnm differ diff --git a/Tests/images/hopper_draw.ico b/Tests/images/hopper_draw.ico new file mode 100644 index 000000000..014711896 Binary files /dev/null and b/Tests/images/hopper_draw.ico differ diff --git a/Tests/images/hopper_orientation_2.jpg b/Tests/images/hopper_orientation_2.jpg new file mode 100644 index 000000000..02b4f392e Binary files /dev/null and b/Tests/images/hopper_orientation_2.jpg differ diff --git a/Tests/images/hopper_orientation_2.webp b/Tests/images/hopper_orientation_2.webp new file mode 100644 index 000000000..43381d2ba Binary files /dev/null and b/Tests/images/hopper_orientation_2.webp differ diff --git a/Tests/images/hopper_orientation_3.jpg b/Tests/images/hopper_orientation_3.jpg new file mode 100644 index 000000000..01717d980 Binary files /dev/null and b/Tests/images/hopper_orientation_3.jpg differ diff --git a/Tests/images/hopper_orientation_3.webp b/Tests/images/hopper_orientation_3.webp new file mode 100644 index 000000000..9537ff68e Binary files /dev/null and b/Tests/images/hopper_orientation_3.webp differ diff --git a/Tests/images/hopper_orientation_4.jpg b/Tests/images/hopper_orientation_4.jpg new file mode 100644 index 000000000..3e0bb4e1a Binary files /dev/null and b/Tests/images/hopper_orientation_4.jpg differ diff --git a/Tests/images/hopper_orientation_4.webp b/Tests/images/hopper_orientation_4.webp new file mode 100644 index 000000000..ca7b8cd30 Binary files /dev/null and b/Tests/images/hopper_orientation_4.webp differ diff --git a/Tests/images/hopper_orientation_5.jpg b/Tests/images/hopper_orientation_5.jpg new file mode 100644 index 000000000..fd32afc27 Binary files /dev/null and b/Tests/images/hopper_orientation_5.jpg differ diff --git a/Tests/images/hopper_orientation_5.webp b/Tests/images/hopper_orientation_5.webp new file mode 100644 index 000000000..a3164a90d Binary files /dev/null and b/Tests/images/hopper_orientation_5.webp differ diff --git a/Tests/images/hopper_orientation_6.jpg b/Tests/images/hopper_orientation_6.jpg new file mode 100644 index 000000000..22a096198 Binary files /dev/null and b/Tests/images/hopper_orientation_6.jpg differ diff --git a/Tests/images/hopper_orientation_6.webp b/Tests/images/hopper_orientation_6.webp new file mode 100644 index 000000000..3e24c5bcb Binary files /dev/null and b/Tests/images/hopper_orientation_6.webp differ diff --git a/Tests/images/hopper_orientation_7.jpg b/Tests/images/hopper_orientation_7.jpg new file mode 100644 index 000000000..a7c45146a Binary files /dev/null and b/Tests/images/hopper_orientation_7.jpg differ diff --git a/Tests/images/hopper_orientation_7.webp b/Tests/images/hopper_orientation_7.webp new file mode 100644 index 000000000..f78163aed Binary files /dev/null and b/Tests/images/hopper_orientation_7.webp differ diff --git a/Tests/images/hopper_orientation_8.jpg b/Tests/images/hopper_orientation_8.jpg new file mode 100644 index 000000000..e6b8c2c1c Binary files /dev/null and b/Tests/images/hopper_orientation_8.jpg differ diff --git a/Tests/images/hopper_orientation_8.webp b/Tests/images/hopper_orientation_8.webp new file mode 100644 index 000000000..3cce80a47 Binary files /dev/null and b/Tests/images/hopper_orientation_8.webp differ diff --git a/Tests/images/hopper_roundDown.bmp b/Tests/images/hopper_roundDown.bmp new file mode 100644 index 000000000..62aada050 Binary files /dev/null and b/Tests/images/hopper_roundDown.bmp differ diff --git a/Tests/images/hopper_roundDown_2.tif b/Tests/images/hopper_roundDown_2.tif new file mode 100644 index 000000000..ac8cd057d Binary files /dev/null and b/Tests/images/hopper_roundDown_2.tif differ diff --git a/Tests/images/hopper_roundDown_3.tif b/Tests/images/hopper_roundDown_3.tif new file mode 100644 index 000000000..0542fab9a Binary files /dev/null and b/Tests/images/hopper_roundDown_3.tif differ diff --git a/Tests/images/hopper_roundDown_None.tif b/Tests/images/hopper_roundDown_None.tif new file mode 100644 index 000000000..21c40e8fe Binary files /dev/null and b/Tests/images/hopper_roundDown_None.tif differ diff --git a/Tests/images/hopper_roundUp_2.tif b/Tests/images/hopper_roundUp_2.tif new file mode 100644 index 000000000..e38541c5d Binary files /dev/null and b/Tests/images/hopper_roundUp_2.tif differ diff --git a/Tests/images/hopper_roundUp_3.tif b/Tests/images/hopper_roundUp_3.tif new file mode 100644 index 000000000..af6c96bd4 Binary files /dev/null and b/Tests/images/hopper_roundUp_3.tif differ diff --git a/Tests/images/hopper_roundUp_None.tif b/Tests/images/hopper_roundUp_None.tif new file mode 100644 index 000000000..b98635108 Binary files /dev/null and b/Tests/images/hopper_roundUp_None.tif differ diff --git a/Tests/images/hopper_unexpected.ico b/Tests/images/hopper_unexpected.ico new file mode 100644 index 000000000..639828ae0 Binary files /dev/null and b/Tests/images/hopper_unexpected.ico differ diff --git a/Tests/images/i_trns.png b/Tests/images/i_trns.png new file mode 100644 index 000000000..ef63d33b0 Binary files /dev/null and b/Tests/images/i_trns.png differ diff --git a/Tests/images/imagedraw_arc_width_non_whole_angle.png b/Tests/images/imagedraw_arc_width_non_whole_angle.png new file mode 100644 index 000000000..1fb9a3c86 Binary files /dev/null and b/Tests/images/imagedraw_arc_width_non_whole_angle.png differ diff --git a/Tests/images/imagedraw_arc_width_pieslice.png b/Tests/images/imagedraw_arc_width_pieslice.png new file mode 100644 index 000000000..950d95dd6 Binary files /dev/null and b/Tests/images/imagedraw_arc_width_pieslice.png differ diff --git a/Tests/images/imagedraw_ellipse_width_large.png b/Tests/images/imagedraw_ellipse_width_large.png new file mode 100644 index 000000000..9d3c3326b Binary files /dev/null and b/Tests/images/imagedraw_ellipse_width_large.png differ diff --git a/Tests/images/imagedraw_floodfill_not_negative.png b/Tests/images/imagedraw_floodfill_not_negative.png new file mode 100644 index 000000000..c3f34a174 Binary files /dev/null and b/Tests/images/imagedraw_floodfill_not_negative.png differ diff --git a/Tests/images/imagedraw_rectangle_I.png b/Tests/images/imagedraw_rectangle_I.png new file mode 100644 index 000000000..4e94f6943 Binary files /dev/null and b/Tests/images/imagedraw_rectangle_I.png differ diff --git a/Tests/images/imagedraw_stroke_different.png b/Tests/images/imagedraw_stroke_different.png new file mode 100644 index 000000000..e58cbdc4e Binary files /dev/null and b/Tests/images/imagedraw_stroke_different.png differ diff --git a/Tests/images/imagedraw_stroke_multiline.png b/Tests/images/imagedraw_stroke_multiline.png new file mode 100644 index 000000000..fc5e07c86 Binary files /dev/null and b/Tests/images/imagedraw_stroke_multiline.png differ diff --git a/Tests/images/imagedraw_stroke_same.png b/Tests/images/imagedraw_stroke_same.png new file mode 100644 index 000000000..8f2f3abe1 Binary files /dev/null and b/Tests/images/imagedraw_stroke_same.png differ diff --git a/Tests/images/iptc_roundDown.jpg b/Tests/images/iptc_roundDown.jpg new file mode 100644 index 000000000..f98206f18 Binary files /dev/null and b/Tests/images/iptc_roundDown.jpg differ diff --git a/Tests/images/iptc_roundUp.jpg b/Tests/images/iptc_roundUp.jpg new file mode 100644 index 000000000..68ac20b71 Binary files /dev/null and b/Tests/images/iptc_roundUp.jpg differ diff --git a/Tests/images/itxt_chunks.png b/Tests/images/itxt_chunks.png new file mode 100644 index 000000000..ca098440c Binary files /dev/null and b/Tests/images/itxt_chunks.png differ diff --git a/Tests/images/no_rows_per_strip.tif b/Tests/images/no_rows_per_strip.tif new file mode 100644 index 000000000..67942aec4 Binary files /dev/null and b/Tests/images/no_rows_per_strip.tif differ diff --git a/Tests/images/old-style-jpeg-compression.png b/Tests/images/old-style-jpeg-compression.png new file mode 100644 index 000000000..c035542ea Binary files /dev/null and b/Tests/images/old-style-jpeg-compression.png differ diff --git a/Tests/images/old-style-jpeg-compression.tif b/Tests/images/old-style-jpeg-compression.tif new file mode 100644 index 000000000..8d726c404 Binary files /dev/null and b/Tests/images/old-style-jpeg-compression.tif differ diff --git a/Tests/images/pcx_overrun.bin b/Tests/images/pcx_overrun.bin new file mode 100644 index 000000000..ea46d2c11 Binary files /dev/null and b/Tests/images/pcx_overrun.bin differ diff --git a/Tests/images/raw_negative_stride.bin b/Tests/images/raw_negative_stride.bin new file mode 100644 index 000000000..312e82a5f Binary files /dev/null and b/Tests/images/raw_negative_stride.bin differ diff --git a/Tests/images/rgb32bf-rgba.bmp b/Tests/images/rgb32bf-rgba.bmp new file mode 100644 index 000000000..467c2570b Binary files /dev/null and b/Tests/images/rgb32bf-rgba.bmp differ diff --git a/Tests/images/sgi_overrun.bin b/Tests/images/sgi_overrun.bin new file mode 100644 index 000000000..9a45d065a Binary files /dev/null and b/Tests/images/sgi_overrun.bin differ diff --git a/Tests/images/string_dimension.tiff b/Tests/images/string_dimension.tiff new file mode 100644 index 000000000..d0b558301 Binary files /dev/null and b/Tests/images/string_dimension.tiff differ diff --git a/Tests/images/sugarshack_frame_size.mpo b/Tests/images/sugarshack_frame_size.mpo new file mode 100644 index 000000000..81d58e64b Binary files /dev/null and b/Tests/images/sugarshack_frame_size.mpo differ diff --git a/Tests/images/test_complex_unicode_text2.png b/Tests/images/test_complex_unicode_text2.png new file mode 100644 index 000000000..543b174c0 Binary files /dev/null and b/Tests/images/test_complex_unicode_text2.png differ diff --git a/Tests/images/test_direction_ttb.png b/Tests/images/test_direction_ttb.png new file mode 100644 index 000000000..825f3213e Binary files /dev/null and b/Tests/images/test_direction_ttb.png differ diff --git a/Tests/images/test_direction_ttb_stroke.png b/Tests/images/test_direction_ttb_stroke.png new file mode 100644 index 000000000..3fa844e9a Binary files /dev/null and b/Tests/images/test_direction_ttb_stroke.png differ diff --git a/Tests/images/test_extents.gif b/Tests/images/test_extents.gif new file mode 100644 index 000000000..03c436435 Binary files /dev/null and b/Tests/images/test_extents.gif differ diff --git a/Tests/images/test_language.png b/Tests/images/test_language.png new file mode 100644 index 000000000..8daf007b0 Binary files /dev/null and b/Tests/images/test_language.png differ diff --git a/Tests/images/test_x_max_and_y_offset.png b/Tests/images/test_x_max_and_y_offset.png new file mode 100644 index 000000000..f8bec3e95 Binary files /dev/null and b/Tests/images/test_x_max_and_y_offset.png differ diff --git a/Tests/images/test_y_offset.png b/Tests/images/test_y_offset.png index 5a166be8c..2d57890cb 100644 Binary files a/Tests/images/test_y_offset.png and b/Tests/images/test_y_offset.png differ diff --git a/Tests/images/tiff_16bit_RGB.tiff b/Tests/images/tiff_16bit_RGB.tiff new file mode 100644 index 000000000..5eb7c73c2 Binary files /dev/null and b/Tests/images/tiff_16bit_RGB.tiff differ diff --git a/Tests/images/tiff_16bit_RGB_target.png b/Tests/images/tiff_16bit_RGB_target.png new file mode 100644 index 000000000..923580004 Binary files /dev/null and b/Tests/images/tiff_16bit_RGB_target.png differ diff --git a/Tests/images/tiff_strip_cmyk_16l_jpeg.tif b/Tests/images/tiff_strip_cmyk_16l_jpeg.tif new file mode 100644 index 000000000..8bfd8bd6a Binary files /dev/null and b/Tests/images/tiff_strip_cmyk_16l_jpeg.tif differ diff --git a/Tests/images/uncompressed_rgb.dds b/Tests/images/uncompressed_rgb.dds new file mode 100755 index 000000000..cd5189532 Binary files /dev/null and b/Tests/images/uncompressed_rgb.dds differ diff --git a/Tests/images/uncompressed_rgb.png b/Tests/images/uncompressed_rgb.png new file mode 100644 index 000000000..50bca09ee Binary files /dev/null and b/Tests/images/uncompressed_rgb.png differ diff --git a/Tests/images/unicode_extended.png b/Tests/images/unicode_extended.png new file mode 100644 index 000000000..c0ffad3c6 Binary files /dev/null and b/Tests/images/unicode_extended.png differ diff --git a/Tests/images/unimplemented_dxgi_format.dds b/Tests/images/unimplemented_dxgi_format.dds new file mode 100644 index 000000000..5ecb42006 Binary files /dev/null and b/Tests/images/unimplemented_dxgi_format.dds differ diff --git a/Tests/images/unimplemented_pixel_format.dds b/Tests/images/unimplemented_pixel_format.dds new file mode 100755 index 000000000..41a343886 Binary files /dev/null and b/Tests/images/unimplemented_pixel_format.dds differ diff --git a/Tests/images/variation_adobe.png b/Tests/images/variation_adobe.png new file mode 100644 index 000000000..71b879bc5 Binary files /dev/null and b/Tests/images/variation_adobe.png differ diff --git a/Tests/images/variation_adobe_axes.png b/Tests/images/variation_adobe_axes.png new file mode 100644 index 000000000..9376c1d7b Binary files /dev/null and b/Tests/images/variation_adobe_axes.png differ diff --git a/Tests/images/variation_adobe_name.png b/Tests/images/variation_adobe_name.png new file mode 100644 index 000000000..9e5fe70e5 Binary files /dev/null and b/Tests/images/variation_adobe_name.png differ diff --git a/Tests/images/variation_tiny.png b/Tests/images/variation_tiny.png new file mode 100644 index 000000000..a0ff3f594 Binary files /dev/null and b/Tests/images/variation_tiny.png differ diff --git a/Tests/images/variation_tiny_axes.png b/Tests/images/variation_tiny_axes.png new file mode 100644 index 000000000..d06ac7a60 Binary files /dev/null and b/Tests/images/variation_tiny_axes.png differ diff --git a/Tests/images/variation_tiny_name.png b/Tests/images/variation_tiny_name.png new file mode 100644 index 000000000..a0c6ffe3f Binary files /dev/null and b/Tests/images/variation_tiny_name.png differ diff --git a/Tests/import_all.py b/Tests/import_all.py index 11682237b..33b07f9a2 100644 --- a/Tests/import_all.py +++ b/Tests/import_all.py @@ -1,10 +1,8 @@ -from __future__ import print_function - import glob import os +import sys import traceback -import sys sys.path.insert(0, ".") for file in glob.glob("src/PIL/*.py"): diff --git a/Tests/make_hash.py b/Tests/make_hash.py index c52e009ec..7199f8c7f 100644 --- a/Tests/make_hash.py +++ b/Tests/make_hash.py @@ -1,24 +1,34 @@ # brute-force search for access descriptor hash table -from __future__ import print_function - modes = [ "1", - "L", "LA", "La", - "I", "I;16", "I;16L", "I;16B", "I;32L", "I;32B", + "L", + "LA", + "La", + "I", + "I;16", + "I;16L", + "I;16B", + "I;32L", + "I;32B", "F", - "P", "PA", - "RGB", "RGBA", "RGBa", "RGBX", + "P", + "PA", + "RGB", + "RGBA", + "RGBa", + "RGBX", "CMYK", "YCbCr", - "LAB", "HSV", - ] + "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 + i = (((i << 5) + i) ^ ord(c)) & 0xFFFFFFFF return i diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index 321a6b3ce..a6143f084 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -1,21 +1,18 @@ -from .helper import PillowTestCase - import PIL import PIL.Image +from .helper import PillowTestCase + class TestSanity(PillowTestCase): - def test_sanity(self): # Make sure we have the binary extension PIL.Image.core.new("L", (100, 100)) - self.assertEqual(PIL.Image.VERSION[:3], '1.1') - # Create an image and do stuff with it. im = PIL.Image.new("1", (100, 100)) - self.assertEqual((im.mode, im.size), ('1', (100, 100))) + self.assertEqual((im.mode, im.size), ("1", (100, 100))) self.assertEqual(len(im.tobytes()), 1300) # Create images in all remaining major modes. diff --git a/Tests/test_binary.py b/Tests/test_binary.py index bf9ba1e5f..79d5d2bcb 100644 --- a/Tests/test_binary.py +++ b/Tests/test_binary.py @@ -1,24 +1,23 @@ -from .helper import PillowTestCase - from PIL import _binary +from .helper import PillowTestCase + class TestBinary(PillowTestCase): - def test_standard(self): - self.assertEqual(_binary.i8(b'*'), 42) - self.assertEqual(_binary.o8(42), b'*') + self.assertEqual(_binary.i8(b"*"), 42) + self.assertEqual(_binary.o8(42), b"*") def test_little_endian(self): - self.assertEqual(_binary.i16le(b'\xff\xff\x00\x00'), 65535) - self.assertEqual(_binary.i32le(b'\xff\xff\x00\x00'), 65535) + self.assertEqual(_binary.i16le(b"\xff\xff\x00\x00"), 65535) + self.assertEqual(_binary.i32le(b"\xff\xff\x00\x00"), 65535) - self.assertEqual(_binary.o16le(65535), b'\xff\xff') - self.assertEqual(_binary.o32le(65535), b'\xff\xff\x00\x00') + self.assertEqual(_binary.o16le(65535), b"\xff\xff") + self.assertEqual(_binary.o32le(65535), b"\xff\xff\x00\x00") def test_big_endian(self): - self.assertEqual(_binary.i16be(b'\x00\x00\xff\xff'), 0) - self.assertEqual(_binary.i32be(b'\x00\x00\xff\xff'), 65535) + self.assertEqual(_binary.i16be(b"\x00\x00\xff\xff"), 0) + self.assertEqual(_binary.i32be(b"\x00\x00\xff\xff"), 65535) - self.assertEqual(_binary.o16be(65535), b'\xff\xff') - self.assertEqual(_binary.o32be(65535), b'\x00\x00\xff\xff') + self.assertEqual(_binary.o16be(65535), b"\xff\xff") + self.assertEqual(_binary.o32be(65535), b"\x00\x00\xff\xff") diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 0e32c93dd..3a33d2c8a 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,26 +1,29 @@ -from __future__ import print_function -from .helper import PillowTestCase - -from PIL import Image import os -base = os.path.join('Tests', 'images', 'bmp') +from PIL import Image + +from .helper import PillowTestCase + +base = os.path.join("Tests", "images", "bmp") class TestBmpReference(PillowTestCase): - - def get_files(self, d, ext='.bmp'): - return [os.path.join(base, d, f) for f - in os.listdir(os.path.join(base, d)) if ext in f] + def get_files(self, d, ext=".bmp"): + return [ + os.path.join(base, d, f) + for f in os.listdir(os.path.join(base, d)) + if ext in f + ] def test_bad(self): """ These shouldn't crash/dos, but they shouldn't return anything either """ - for f in self.get_files('b'): + for f in self.get_files("b"): + def open(f): try: - im = Image.open(f) - im.load() + with Image.open(f) as im: + im.load() except Exception: # as msg: pass @@ -41,13 +44,12 @@ class TestBmpReference(PillowTestCase): "pal8os2sp.bmp", "rgb32bf-xbgr.bmp", ] - for f in self.get_files('q'): + 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) + print("Please add %s to the partially supported bmp specs." % f) except Exception: # as msg: if os.path.basename(f) in supported: raise @@ -57,49 +59,52 @@ class TestBmpReference(PillowTestCase): html directory that we can compare against. """ # Target files, if they're not just replacing the extension - file_map = {'pal1wb.bmp': 'pal1.png', - 'pal4rle.bmp': 'pal4.png', - 'pal8-0.bmp': 'pal8.png', - 'pal8rle.bmp': 'pal8.png', - 'pal8topdown.bmp': 'pal8.png', - 'pal8nonsquare.bmp': 'pal8nonsquare-v.png', - 'pal8os2.bmp': 'pal8.png', - 'pal8os2sp.bmp': 'pal8.png', - 'pal8os2v2.bmp': 'pal8.png', - 'pal8os2v2-16.bmp': 'pal8.png', - 'pal8v4.bmp': 'pal8.png', - 'pal8v5.bmp': 'pal8.png', - 'rgb16-565pal.bmp': 'rgb16-565.png', - 'rgb24pal.bmp': 'rgb24.png', - 'rgb32.bmp': 'rgb24.png', - 'rgb32bf.bmp': 'rgb24.png' - } + file_map = { + "pal1wb.bmp": "pal1.png", + "pal4rle.bmp": "pal4.png", + "pal8-0.bmp": "pal8.png", + "pal8rle.bmp": "pal8.png", + "pal8topdown.bmp": "pal8.png", + "pal8nonsquare.bmp": "pal8nonsquare-v.png", + "pal8os2.bmp": "pal8.png", + "pal8os2sp.bmp": "pal8.png", + "pal8os2v2.bmp": "pal8.png", + "pal8os2v2-16.bmp": "pal8.png", + "pal8v4.bmp": "pal8.png", + "pal8v5.bmp": "pal8.png", + "rgb16-565pal.bmp": "rgb16-565.png", + "rgb24pal.bmp": "rgb24.png", + "rgb32.bmp": "rgb24.png", + "rgb32bf.bmp": "rgb24.png", + } def get_compare(f): name = os.path.split(f)[1] if name in file_map: - return os.path.join(base, 'html', file_map[name]) + return os.path.join(base, "html", file_map[name]) name = os.path.splitext(name)[0] - return os.path.join(base, 'html', "%s.png" % name) + return os.path.join(base, "html", "%s.png" % name) - for f in self.get_files('g'): + 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: - unsupported = (os.path.join(base, 'g', 'rgb32bf.bmp'), - os.path.join(base, 'g', 'pal8rle.bmp'), - os.path.join(base, 'g', 'pal4rle.bmp')) + unsupported = ( + os.path.join(base, "g", "rgb32bf.bmp"), + os.path.join(base, "g", "pal8rle.bmp"), + os.path.join(base, "g", "pal4rle.bmp"), + ) if f not in unsupported: - self.fail("Unsupported Image %s: %s" % (f, msg)) + self.fail("Unsupported Image {}: {}".format(f, msg)) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index b9939c5f2..c17e7996d 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -1,9 +1,9 @@ -from .helper import PillowTestCase - from PIL import Image, ImageFilter +from .helper import PillowTestCase sample = Image.new("L", (7, 5)) +# fmt: off sample.putdata(sum([ [210, 50, 20, 10, 220, 230, 80], [190, 210, 20, 180, 170, 40, 110], @@ -11,10 +11,10 @@ sample.putdata(sum([ [220, 40, 230, 80, 130, 250, 40], [250, 0, 80, 30, 60, 20, 110], ], [])) +# fmt: on class TestBoxBlurApi(PillowTestCase): - def test_imageops_box_blur(self): i = sample.filter(ImageFilter.BoxBlur(1)) self.assertEqual(i.mode, sample.mode) @@ -23,7 +23,6 @@ class TestBoxBlurApi(PillowTestCase): class TestBoxBlur(PillowTestCase): - def box_blur(self, image, radius=1, n=1): return image._new(image.im.box_blur(radius, n)) @@ -32,8 +31,7 @@ class TestBoxBlur(PillowTestCase): for data_row in data: im_row = [next(it) for _ in range(im.size[0])] if any( - abs(data_v - im_v) > delta - for data_v, im_v in zip(data_row, im_row) + abs(data_v - im_v) > delta for data_v, im_v in zip(data_row, im_row) ): self.assertEqual(im_row, data_row) self.assertRaises(StopIteration, next, it) @@ -41,7 +39,7 @@ class TestBoxBlur(PillowTestCase): def assertBlur(self, im, radius, data, passes=1, delta=0): # check grayscale image self.assertImage(self.box_blur(im, radius, passes), data, delta) - rgba = Image.merge('RGBA', (im, im, im, im)) + rgba = Image.merge("RGBA", (im, im, im, im)) for band in self.box_blur(rgba, radius, passes).split(): self.assertImage(band, data, delta) @@ -61,110 +59,135 @@ class TestBoxBlur(PillowTestCase): def test_radius_0(self): self.assertBlur( - sample, 0, + sample, + 0, [ + # fmt: off [210, 50, 20, 10, 220, 230, 80], [190, 210, 20, 180, 170, 40, 110], [120, 210, 250, 60, 220, 0, 220], [220, 40, 230, 80, 130, 250, 40], [250, 0, 80, 30, 60, 20, 110], - ] + # fmt: on + ], ) def test_radius_0_02(self): self.assertBlur( - sample, 0.02, + sample, + 0.02, [ + # fmt: off [206, 55, 20, 17, 215, 223, 83], [189, 203, 31, 171, 169, 46, 110], [125, 206, 241, 69, 210, 13, 210], [215, 49, 221, 82, 131, 235, 48], [244, 7, 80, 32, 60, 27, 107], + # fmt: on ], delta=2, ) def test_radius_0_05(self): self.assertBlur( - sample, 0.05, + sample, + 0.05, [ + # fmt: off [202, 62, 22, 27, 209, 215, 88], [188, 194, 44, 161, 168, 56, 111], [131, 201, 229, 81, 198, 31, 198], [209, 62, 209, 86, 133, 216, 59], [237, 17, 80, 36, 60, 35, 103], + # fmt: on ], delta=2, ) def test_radius_0_1(self): self.assertBlur( - sample, 0.1, + sample, + 0.1, [ + # fmt: off [196, 72, 24, 40, 200, 203, 93], [187, 183, 62, 148, 166, 68, 111], [139, 193, 213, 96, 182, 54, 182], [201, 78, 193, 91, 133, 191, 73], [227, 31, 80, 42, 61, 47, 99], + # fmt: on ], delta=1, ) def test_radius_0_5(self): self.assertBlur( - sample, 0.5, + sample, + 0.5, [ + # fmt: off [176, 101, 46, 83, 163, 165, 111], [176, 149, 108, 122, 144, 120, 117], [164, 171, 159, 141, 134, 119, 129], [170, 136, 133, 114, 116, 124, 109], [184, 95, 72, 70, 69, 81, 89], + # fmt: on ], delta=1, ) def test_radius_1(self): self.assertBlur( - sample, 1, + sample, + 1, [ + # fmt: off [170, 109, 63, 97, 146, 153, 116], [168, 142, 112, 128, 126, 143, 121], [169, 166, 142, 149, 126, 131, 114], [159, 156, 109, 127, 94, 117, 112], [164, 128, 63, 87, 76, 89, 90], + # fmt: on ], delta=1, ) def test_radius_1_5(self): self.assertBlur( - sample, 1.5, + sample, + 1.5, [ + # fmt: off [155, 120, 105, 112, 124, 137, 130], [160, 136, 124, 125, 127, 134, 130], [166, 147, 130, 125, 120, 121, 119], [168, 145, 119, 109, 103, 105, 110], [168, 134, 96, 85, 85, 89, 97], + # fmt: on ], delta=1, ) def test_radius_bigger_then_half(self): self.assertBlur( - sample, 3, + sample, + 3, [ + # fmt: off [144, 145, 142, 128, 114, 115, 117], [148, 145, 137, 122, 109, 111, 112], [152, 145, 131, 117, 103, 107, 108], [156, 144, 126, 111, 97, 102, 103], [160, 144, 121, 106, 92, 98, 99], + # fmt: on ], delta=1, ) def test_radius_bigger_then_width(self): self.assertBlur( - sample, 10, + sample, + 10, [ [158, 153, 147, 141, 135, 129, 123], [159, 153, 147, 141, 136, 130, 124], @@ -175,9 +198,10 @@ class TestBoxBlur(PillowTestCase): delta=0, ) - def test_exteme_large_radius(self): + def test_extreme_large_radius(self): self.assertBlur( - sample, 600, + sample, + 600, [ [162, 162, 162, 162, 162, 162, 162], [162, 162, 162, 162, 162, 162, 162], @@ -190,13 +214,16 @@ class TestBoxBlur(PillowTestCase): def test_two_passes(self): self.assertBlur( - sample, 1, + sample, + 1, [ + # fmt: off [153, 123, 102, 109, 132, 135, 129], [159, 138, 123, 121, 133, 131, 126], [162, 147, 136, 124, 127, 121, 121], [159, 140, 125, 108, 111, 106, 108], [154, 126, 105, 87, 94, 93, 97], + # fmt: on ], passes=2, delta=1, @@ -204,13 +231,16 @@ class TestBoxBlur(PillowTestCase): def test_three_passes(self): self.assertBlur( - sample, 1, + sample, + 1, [ + # fmt: off [146, 131, 116, 118, 126, 131, 130], [151, 138, 125, 123, 126, 128, 127], [154, 143, 129, 123, 120, 120, 119], [152, 139, 122, 113, 108, 108, 108], [148, 132, 112, 102, 97, 99, 100], + # fmt: on ], passes=3, delta=1, diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 97035c793..96461ca3a 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,9 +1,9 @@ -from __future__ import division - +import unittest from array import array from PIL import Image, ImageFilter -from .helper import unittest, PillowTestCase + +from .helper import PillowTestCase try: import numpy @@ -20,185 +20,197 @@ class TestColorLut3DCoreAPI(PillowTestCase): table = [ [ - r / float(size1D-1) if size1D != 1 else 0, - g / float(size2D-1) if size2D != 1 else 0, - b / float(size3D-1) if size3D != 1 else 0, - r / float(size1D-1) if size1D != 1 else 0, - g / float(size2D-1) if size2D != 1 else 0, + r / float(size1D - 1) if size1D != 1 else 0, + g / float(size2D - 1) if size2D != 1 else 0, + b / float(size3D - 1) if size3D != 1 else 0, + r / float(size1D - 1) if size1D != 1 else 0, + g / float(size2D - 1) if size2D != 1 else 0, ][:channels] for b in range(size3D) for g in range(size2D) for r in range(size1D) ] return ( - channels, size1D, size2D, size3D, - [item for sublist in table for item in sublist]) + channels, + size1D, + size2D, + size3D, + [item for sublist in table for item in sublist], + ) def test_wrong_args(self): - im = Image.new('RGB', (10, 10), 0) + im = Image.new("RGB", (10, 10), 0) with self.assertRaisesRegex(ValueError, "filter"): - im.im.color_lut_3d('RGB', - Image.CUBIC, - *self.generate_identity_table(3, 3)) + im.im.color_lut_3d("RGB", Image.CUBIC, *self.generate_identity_table(3, 3)) with self.assertRaisesRegex(ValueError, "image mode"): - im.im.color_lut_3d('wrong', - Image.LINEAR, - *self.generate_identity_table(3, 3)) + im.im.color_lut_3d( + "wrong", Image.LINEAR, *self.generate_identity_table(3, 3) + ) with self.assertRaisesRegex(ValueError, "table_channels"): - im.im.color_lut_3d('RGB', - Image.LINEAR, - *self.generate_identity_table(5, 3)) + im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(5, 3)) with self.assertRaisesRegex(ValueError, "table_channels"): - im.im.color_lut_3d('RGB', - Image.LINEAR, - *self.generate_identity_table(1, 3)) + im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(1, 3)) with self.assertRaisesRegex(ValueError, "table_channels"): - im.im.color_lut_3d('RGB', - Image.LINEAR, - *self.generate_identity_table(2, 3)) + im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(2, 3)) with self.assertRaisesRegex(ValueError, "Table size"): - im.im.color_lut_3d('RGB', - Image.LINEAR, - *self.generate_identity_table(3, (1, 3, 3))) + im.im.color_lut_3d( + "RGB", Image.LINEAR, *self.generate_identity_table(3, (1, 3, 3)) + ) with self.assertRaisesRegex(ValueError, "Table size"): - im.im.color_lut_3d('RGB', - Image.LINEAR, - *self.generate_identity_table(3, (66, 3, 3))) + im.im.color_lut_3d( + "RGB", Image.LINEAR, *self.generate_identity_table(3, (66, 3, 3)) + ) with self.assertRaisesRegex(ValueError, r"size1D \* size2D \* size3D"): - im.im.color_lut_3d('RGB', - Image.LINEAR, - 3, 2, 2, 2, [0, 0, 0] * 7) + im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 7) with self.assertRaisesRegex(ValueError, r"size1D \* size2D \* size3D"): - im.im.color_lut_3d('RGB', - Image.LINEAR, - 3, 2, 2, 2, [0, 0, 0] * 9) + im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 9) with self.assertRaises(TypeError): - im.im.color_lut_3d('RGB', - Image.LINEAR, - 3, 2, 2, 2, [0, 0, "0"] * 8) + im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8) with self.assertRaises(TypeError): - im.im.color_lut_3d('RGB', - Image.LINEAR, - 3, 2, 2, 2, 16) + im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, 16) def test_correct_args(self): - im = Image.new('RGB', (10, 10), 0) + im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d('RGB', Image.LINEAR, - *self.generate_identity_table(3, 3)) + im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3)) - im.im.color_lut_3d('CMYK', Image.LINEAR, - *self.generate_identity_table(4, 3)) + im.im.color_lut_3d("CMYK", Image.LINEAR, *self.generate_identity_table(4, 3)) - im.im.color_lut_3d('RGB', Image.LINEAR, - *self.generate_identity_table(3, (2, 3, 3))) + im.im.color_lut_3d( + "RGB", Image.LINEAR, *self.generate_identity_table(3, (2, 3, 3)) + ) - im.im.color_lut_3d('RGB', Image.LINEAR, - *self.generate_identity_table(3, (65, 3, 3))) + im.im.color_lut_3d( + "RGB", Image.LINEAR, *self.generate_identity_table(3, (65, 3, 3)) + ) - im.im.color_lut_3d('RGB', Image.LINEAR, - *self.generate_identity_table(3, (3, 65, 3))) + im.im.color_lut_3d( + "RGB", Image.LINEAR, *self.generate_identity_table(3, (3, 65, 3)) + ) - im.im.color_lut_3d('RGB', Image.LINEAR, - *self.generate_identity_table(3, (3, 3, 65))) + im.im.color_lut_3d( + "RGB", Image.LINEAR, *self.generate_identity_table(3, (3, 3, 65)) + ) def test_wrong_mode(self): with self.assertRaisesRegex(ValueError, "wrong mode"): - im = Image.new('L', (10, 10), 0) - im.im.color_lut_3d('RGB', Image.LINEAR, - *self.generate_identity_table(3, 3)) + im = Image.new("L", (10, 10), 0) + im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3)) with self.assertRaisesRegex(ValueError, "wrong mode"): - im = Image.new('RGB', (10, 10), 0) - im.im.color_lut_3d('L', Image.LINEAR, - *self.generate_identity_table(3, 3)) + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3)) with self.assertRaisesRegex(ValueError, "wrong mode"): - im = Image.new('L', (10, 10), 0) - im.im.color_lut_3d('L', Image.LINEAR, - *self.generate_identity_table(3, 3)) + im = Image.new("L", (10, 10), 0) + im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3)) with self.assertRaisesRegex(ValueError, "wrong mode"): - im = Image.new('RGB', (10, 10), 0) - im.im.color_lut_3d('RGBA', Image.LINEAR, - *self.generate_identity_table(3, 3)) + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "RGBA", Image.LINEAR, *self.generate_identity_table(3, 3) + ) with self.assertRaisesRegex(ValueError, "wrong mode"): - im = Image.new('RGB', (10, 10), 0) - im.im.color_lut_3d('RGB', Image.LINEAR, - *self.generate_identity_table(4, 3)) + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(4, 3)) def test_correct_mode(self): - im = Image.new('RGBA', (10, 10), 0) - im.im.color_lut_3d('RGBA', Image.LINEAR, - *self.generate_identity_table(3, 3)) + im = Image.new("RGBA", (10, 10), 0) + im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(3, 3)) - im = Image.new('RGBA', (10, 10), 0) - im.im.color_lut_3d('RGBA', Image.LINEAR, - *self.generate_identity_table(4, 3)) + im = Image.new("RGBA", (10, 10), 0) + im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(4, 3)) - im = Image.new('RGB', (10, 10), 0) - im.im.color_lut_3d('HSV', Image.LINEAR, - *self.generate_identity_table(3, 3)) + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d("HSV", Image.LINEAR, *self.generate_identity_table(3, 3)) - im = Image.new('RGB', (10, 10), 0) - im.im.color_lut_3d('RGBA', Image.LINEAR, - *self.generate_identity_table(4, 3)) + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(4, 3)) def test_identities(self): - g = Image.linear_gradient('L') - im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), - g.transpose(Image.ROTATE_180)]) + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + ) # Fast test with small cubes for size in [2, 3, 5, 7, 11, 16, 17]: - self.assert_image_equal(im, im._new( - im.im.color_lut_3d('RGB', Image.LINEAR, - *self.generate_identity_table(3, size)))) + self.assert_image_equal( + im, + im._new( + im.im.color_lut_3d( + "RGB", Image.LINEAR, *self.generate_identity_table(3, size) + ) + ), + ) # Not so fast - self.assert_image_equal(im, im._new( - im.im.color_lut_3d('RGB', Image.LINEAR, - *self.generate_identity_table(3, (2, 2, 65))))) + self.assert_image_equal( + im, + im._new( + im.im.color_lut_3d( + "RGB", Image.LINEAR, *self.generate_identity_table(3, (2, 2, 65)) + ) + ), + ) def test_identities_4_channels(self): - g = Image.linear_gradient('L') - im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), - g.transpose(Image.ROTATE_180)]) + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + ) # Red channel copied to alpha self.assert_image_equal( - Image.merge('RGBA', (im.split()*2)[:4]), - im._new(im.im.color_lut_3d('RGBA', Image.LINEAR, - *self.generate_identity_table(4, 17)))) + Image.merge("RGBA", (im.split() * 2)[:4]), + im._new( + im.im.color_lut_3d( + "RGBA", Image.LINEAR, *self.generate_identity_table(4, 17) + ) + ), + ) def test_copy_alpha_channel(self): - g = Image.linear_gradient('L') - im = Image.merge('RGBA', [g, g.transpose(Image.ROTATE_90), - g.transpose(Image.ROTATE_180), - g.transpose(Image.ROTATE_270)]) + g = Image.linear_gradient("L") + im = Image.merge( + "RGBA", + [ + g, + g.transpose(Image.ROTATE_90), + g.transpose(Image.ROTATE_180), + g.transpose(Image.ROTATE_270), + ], + ) - self.assert_image_equal(im, im._new( - im.im.color_lut_3d('RGBA', Image.LINEAR, - *self.generate_identity_table(3, 17)))) + self.assert_image_equal( + im, + im._new( + im.im.color_lut_3d( + "RGBA", Image.LINEAR, *self.generate_identity_table(3, 17) + ) + ), + ) def test_channels_order(self): - g = Image.linear_gradient('L') - im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), - g.transpose(Image.ROTATE_180)]) + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + ) # Reverse channels by splitting and using table + # fmt: off self.assert_image_equal( Image.merge('RGB', im.split()[::-1]), im._new(im.im.color_lut_3d('RGB', Image.LINEAR, @@ -209,12 +221,15 @@ class TestColorLut3DCoreAPI(PillowTestCase): 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, ]))) + # fmt: on def test_overflow(self): - g = Image.linear_gradient('L') - im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), - g.transpose(Image.ROTATE_180)]) + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + ) + # fmt: off transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, 3, 2, 2, 2, [ @@ -224,6 +239,7 @@ class TestColorLut3DCoreAPI(PillowTestCase): -1, -1, 2, 2, -1, 2, -1, 2, 2, 2, 2, 2, ])).load() + # fmt: on self.assertEqual(transformed[0, 0], (0, 0, 255)) self.assertEqual(transformed[50, 50], (0, 0, 255)) self.assertEqual(transformed[255, 0], (0, 255, 255)) @@ -233,6 +249,7 @@ class TestColorLut3DCoreAPI(PillowTestCase): self.assertEqual(transformed[255, 255], (255, 255, 0)) self.assertEqual(transformed[205, 205], (255, 255, 0)) + # fmt: off transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, 3, 2, 2, 2, [ @@ -242,6 +259,7 @@ class TestColorLut3DCoreAPI(PillowTestCase): -3, -3, 5, 5, -3, 5, -3, 5, 5, 5, 5, 5, ])).load() + # fmt: on self.assertEqual(transformed[0, 0], (0, 0, 255)) self.assertEqual(transformed[50, 50], (0, 0, 255)) self.assertEqual(transformed[255, 0], (0, 255, 255)) @@ -286,14 +304,15 @@ class TestColorLut3DFilter(PillowTestCase): self.assertEqual(tuple(lut.size), (2, 2, 2)) self.assertEqual(lut.name, "Color 3D LUT") + # fmt: off lut = ImageFilter.Color3DLUT((2, 2, 2), [ (0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11), (12, 13, 14), (15, 16, 17), (18, 19, 20), (21, 22, 23)]) + # fmt: on self.assertEqual(tuple(lut.size), (2, 2, 2)) self.assertEqual(lut.table, list(range(24))) - lut = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8, - channels=4) + lut = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8, channels=4) self.assertEqual(tuple(lut.size), (2, 2, 2)) self.assertEqual(lut.table, list(range(4)) * 8) @@ -318,7 +337,7 @@ class TestColorLut3DFilter(PillowTestCase): self.assertEqual(lut.table.shape, (table.size,)) # Check application - Image.new('RGB', (10, 10), 0).filter(lut) + Image.new("RGB", (10, 10), 0).filter(lut) # Check copy table[0] = 33 @@ -332,40 +351,34 @@ class TestColorLut3DFilter(PillowTestCase): @unittest.skipIf(numpy is None, "Numpy is not installed") def test_numpy_formats(self): - g = Image.linear_gradient('L') - im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), - g.transpose(Image.ROTATE_180)]) + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + ) - lut = ImageFilter.Color3DLUT.generate((7, 9, 11), - lambda r, g, b: (r, g, b)) + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float32)[:-1] with self.assertRaisesRegex(ValueError, "should have table_channels"): im.filter(lut) - lut = ImageFilter.Color3DLUT.generate((7, 9, 11), - lambda r, g, b: (r, g, b)) - lut.table = (numpy.array(lut.table, dtype=numpy.float32) - .reshape((7 * 9 * 11), 3)) + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float32).reshape((7 * 9 * 11), 3) with self.assertRaisesRegex(ValueError, "should have table_channels"): im.filter(lut) - lut = ImageFilter.Color3DLUT.generate((7, 9, 11), - lambda r, g, b: (r, g, b)) + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float16) self.assert_image_equal(im, im.filter(lut)) - lut = ImageFilter.Color3DLUT.generate((7, 9, 11), - lambda r, g, b: (r, g, b)) + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float32) self.assert_image_equal(im, im.filter(lut)) - lut = ImageFilter.Color3DLUT.generate((7, 9, 11), - lambda r, g, b: (r, g, b)) + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float64) self.assert_image_equal(im, im.filter(lut)) - lut = ImageFilter.Color3DLUT.generate((7, 9, 11), - lambda r, g, b: (r, g, b)) + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.int32) im.filter(lut) lut.table = numpy.array(lut.table, dtype=numpy.int8) @@ -373,54 +386,65 @@ class TestColorLut3DFilter(PillowTestCase): def test_repr(self): lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) - self.assertEqual(repr(lut), - "") + self.assertEqual(repr(lut), "") lut = ImageFilter.Color3DLUT( - (3, 4, 5), array('f', [0, 0, 0, 0] * (3 * 4 * 5)), - channels=4, target_mode='YCbCr', _copy_table=False) + (3, 4, 5), + array("f", [0, 0, 0, 0] * (3 * 4 * 5)), + channels=4, + target_mode="YCbCr", + _copy_table=False, + ) self.assertEqual( - repr(lut), - "") + repr(lut), "" + ) class TestGenerateColorLut3D(PillowTestCase): def test_wrong_channels_count(self): with self.assertRaisesRegex(ValueError, "3 or 4 output channels"): ImageFilter.Color3DLUT.generate( - 5, channels=2, callback=lambda r, g, b: (r, g, b)) + 5, channels=2, callback=lambda r, g, b: (r, g, b) + ) with self.assertRaisesRegex(ValueError, "should have either channels"): ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b, r)) with self.assertRaisesRegex(ValueError, "should have either channels"): ImageFilter.Color3DLUT.generate( - 5, channels=4, callback=lambda r, g, b: (r, g, b)) + 5, channels=4, callback=lambda r, g, b: (r, g, b) + ) def test_3_channels(self): lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) self.assertEqual(tuple(lut.size), (5, 5, 5)) self.assertEqual(lut.name, "Color 3D LUT") + # fmt: off self.assertEqual(lut.table[:24], [ 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.5, 0.0, 0.0, 0.75, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0]) + # fmt: on def test_4_channels(self): lut = ImageFilter.Color3DLUT.generate( - 5, channels=4, callback=lambda r, g, b: (b, r, g, (r+g+b) / 2)) + 5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2) + ) self.assertEqual(tuple(lut.size), (5, 5, 5)) self.assertEqual(lut.name, "Color 3D LUT") + # fmt: off self.assertEqual(lut.table[:24], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.125, 0.0, 0.5, 0.0, 0.25, 0.0, 0.75, 0.0, 0.375, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 0.25, 0.125 ]) + # fmt: on def test_apply(self): lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) - g = Image.linear_gradient('L') - im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), - g.transpose(Image.ROTATE_180)]) + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + ) self.assertEqual(im, im.filter(lut)) @@ -442,80 +466,96 @@ class TestTransformColorLut3D(PillowTestCase): def test_target_mode(self): source = ImageFilter.Color3DLUT.generate( - 2, lambda r, g, b: (r, g, b), target_mode='HSV') + 2, lambda r, g, b: (r, g, b), target_mode="HSV" + ) lut = source.transform(lambda r, g, b: (r, g, b)) - self.assertEqual(lut.mode, 'HSV') + self.assertEqual(lut.mode, "HSV") - lut = source.transform(lambda r, g, b: (r, g, b), target_mode='RGB') - self.assertEqual(lut.mode, 'RGB') + lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB") + self.assertEqual(lut.mode, "RGB") def test_3_to_3_channels(self): - source = ImageFilter.Color3DLUT.generate( - (3, 4, 5), lambda r, g, b: (r, g, b)) - lut = source.transform(lambda r, g, b: (r*r, g*g, b*b)) + source = ImageFilter.Color3DLUT.generate((3, 4, 5), lambda r, g, b: (r, g, b)) + lut = source.transform(lambda r, g, b: (r * r, g * g, b * b)) self.assertEqual(tuple(lut.size), tuple(source.size)) self.assertEqual(len(lut.table), len(source.table)) self.assertNotEqual(lut.table, source.table) - self.assertEqual(lut.table[0:10], [ - 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + self.assertEqual( + lut.table[0:10], [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] + ) def test_3_to_4_channels(self): - source = ImageFilter.Color3DLUT.generate( - (6, 5, 4), lambda r, g, b: (r, g, b)) - lut = source.transform(lambda r, g, b: (r*r, g*g, b*b, 1), channels=4) + source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b)) + lut = source.transform(lambda r, g, b: (r * r, g * g, b * b, 1), channels=4) self.assertEqual(tuple(lut.size), tuple(source.size)) self.assertNotEqual(len(lut.table), len(source.table)) self.assertNotEqual(lut.table, source.table) + # fmt: off self.assertEqual(lut.table[0:16], [ 0.0, 0.0, 0.0, 1, 0.2**2, 0.0, 0.0, 1, 0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1]) + # fmt: on def test_4_to_3_channels(self): source = ImageFilter.Color3DLUT.generate( - (3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4) - lut = source.transform(lambda r, g, b, a: (a - r*r, a - g*g, a - b*b), - channels=3) + (3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4 + ) + lut = source.transform( + lambda r, g, b, a: (a - r * r, a - g * g, a - b * b), channels=3 + ) self.assertEqual(tuple(lut.size), tuple(source.size)) self.assertNotEqual(len(lut.table), len(source.table)) self.assertNotEqual(lut.table, source.table) + # fmt: off self.assertEqual(lut.table[0:18], [ 1.0, 1.0, 1.0, 0.75, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0]) + # fmt: on def test_4_to_4_channels(self): source = ImageFilter.Color3DLUT.generate( - (6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4) - lut = source.transform(lambda r, g, b, a: (r*r, g*g, b*b, a - 0.5)) + (6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4 + ) + lut = source.transform(lambda r, g, b, a: (r * r, g * g, b * b, a - 0.5)) self.assertEqual(tuple(lut.size), tuple(source.size)) self.assertEqual(len(lut.table), len(source.table)) self.assertNotEqual(lut.table, source.table) + # fmt: off self.assertEqual(lut.table[0:16], [ 0.0, 0.0, 0.0, 0.5, 0.2**2, 0.0, 0.0, 0.5, 0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5]) + # fmt: on def test_with_normals_3_channels(self): source = ImageFilter.Color3DLUT.generate( - (6, 5, 4), lambda r, g, b: (r*r, g*g, b*b)) + (6, 5, 4), lambda r, g, b: (r * r, g * g, b * b) + ) lut = source.transform( - lambda nr, ng, nb, r, g, b: (nr - r, ng - g, nb - b), - with_normals=True) + lambda nr, ng, nb, r, g, b: (nr - r, ng - g, nb - b), with_normals=True + ) self.assertEqual(tuple(lut.size), tuple(source.size)) self.assertEqual(len(lut.table), len(source.table)) self.assertNotEqual(lut.table, source.table) + # fmt: off self.assertEqual(lut.table[0:18], [ 0.0, 0.0, 0.0, 0.16, 0.0, 0.0, 0.24, 0.0, 0.0, 0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0]) + # fmt: on def test_with_normals_4_channels(self): source = ImageFilter.Color3DLUT.generate( - (3, 6, 5), lambda r, g, b: (r*r, g*g, b*b, 1), channels=4) + (3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4 + ) lut = source.transform( - lambda nr, ng, nb, r, g, b, a: (nr - r, ng - g, nb - b, a-0.5), - with_normals=True) + lambda nr, ng, nb, r, g, b, a: (nr - r, ng - g, nb - b, a - 0.5), + with_normals=True, + ) self.assertEqual(tuple(lut.size), tuple(source.size)) self.assertEqual(len(lut.table), len(source.table)) self.assertNotEqual(lut.table, source.table) + # fmt: off self.assertEqual(lut.table[0:16], [ 0.0, 0.0, 0.0, 0.5, 0.25, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.16, 0.0, 0.5]) + # fmt: on diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index fe9f5ccc4..ac2970de2 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,44 +1,41 @@ -from __future__ import division, print_function - import sys +import unittest -from .helper import unittest, PillowTestCase from PIL import Image - -is_pypy = hasattr(sys, 'pypy_version_info') +from .helper import PillowTestCase, is_pypy class TestCoreStats(PillowTestCase): def test_get_stats(self): # Create at least one image - Image.new('RGB', (10, 10)) + Image.new("RGB", (10, 10)) stats = Image.core.get_stats() - self.assertIn('new_count', stats) - self.assertIn('reused_blocks', stats) - self.assertIn('freed_blocks', stats) - self.assertIn('allocated_blocks', stats) - self.assertIn('reallocated_blocks', stats) - self.assertIn('blocks_cached', stats) + self.assertIn("new_count", stats) + self.assertIn("reused_blocks", stats) + self.assertIn("freed_blocks", stats) + self.assertIn("allocated_blocks", stats) + self.assertIn("reallocated_blocks", stats) + self.assertIn("blocks_cached", stats) def test_reset_stats(self): Image.core.reset_stats() stats = Image.core.get_stats() - self.assertEqual(stats['new_count'], 0) - self.assertEqual(stats['reused_blocks'], 0) - self.assertEqual(stats['freed_blocks'], 0) - self.assertEqual(stats['allocated_blocks'], 0) - self.assertEqual(stats['reallocated_blocks'], 0) - self.assertEqual(stats['blocks_cached'], 0) + self.assertEqual(stats["new_count"], 0) + self.assertEqual(stats["reused_blocks"], 0) + self.assertEqual(stats["freed_blocks"], 0) + self.assertEqual(stats["allocated_blocks"], 0) + self.assertEqual(stats["reallocated_blocks"], 0) + self.assertEqual(stats["blocks_cached"], 0) class TestCoreMemory(PillowTestCase): def tearDown(self): # Restore default values Image.core.set_alignment(1) - Image.core.set_block_size(1024*1024) + Image.core.set_block_size(1024 * 1024) Image.core.set_blocks_max(0) Image.core.clear_cache() @@ -54,7 +51,7 @@ class TestCoreMemory(PillowTestCase): self.assertEqual(alignment, i) # Try to construct new image - Image.new('RGB', (10, 10)) + Image.new("RGB", (10, 10)) self.assertRaises(ValueError, Image.core.set_alignment, 0) self.assertRaises(ValueError, Image.core.set_alignment, -1) @@ -66,13 +63,13 @@ class TestCoreMemory(PillowTestCase): self.assertGreaterEqual(block_size, 4096) def test_set_block_size(self): - for i in [4096, 2*4096, 3*4096]: + for i in [4096, 2 * 4096, 3 * 4096]: Image.core.set_block_size(i) block_size = Image.core.get_block_size() self.assertEqual(block_size, i) # Try to construct new image - Image.new('RGB', (10, 10)) + Image.new("RGB", (10, 10)) self.assertRaises(ValueError, Image.core.set_block_size, 0) self.assertRaises(ValueError, Image.core.set_block_size, -1) @@ -82,13 +79,13 @@ class TestCoreMemory(PillowTestCase): Image.core.reset_stats() Image.core.set_blocks_max(0) Image.core.set_block_size(4096) - Image.new('RGB', (256, 256)) + Image.new("RGB", (256, 256)) stats = Image.core.get_stats() - self.assertGreaterEqual(stats['new_count'], 1) - self.assertGreaterEqual(stats['allocated_blocks'], 64) - if not is_pypy: - self.assertGreaterEqual(stats['freed_blocks'], 64) + self.assertGreaterEqual(stats["new_count"], 1) + self.assertGreaterEqual(stats["allocated_blocks"], 64) + if not is_pypy(): + self.assertGreaterEqual(stats["freed_blocks"], 64) def test_get_blocks_max(self): blocks_max = Image.core.get_blocks_max() @@ -102,80 +99,82 @@ class TestCoreMemory(PillowTestCase): self.assertEqual(blocks_max, i) # Try to construct new image - Image.new('RGB', (10, 10)) + Image.new("RGB", (10, 10)) self.assertRaises(ValueError, Image.core.set_blocks_max, -1) + 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) Image.core.set_block_size(4096) - Image.new('RGB', (256, 256)) - Image.new('RGB', (256, 256)) + Image.new("RGB", (256, 256)) + Image.new("RGB", (256, 256)) stats = Image.core.get_stats() - self.assertGreaterEqual(stats['new_count'], 2) - self.assertGreaterEqual(stats['allocated_blocks'], 64) - self.assertGreaterEqual(stats['reused_blocks'], 64) - self.assertEqual(stats['freed_blocks'], 0) - self.assertEqual(stats['blocks_cached'], 64) + self.assertGreaterEqual(stats["new_count"], 2) + self.assertGreaterEqual(stats["allocated_blocks"], 64) + self.assertGreaterEqual(stats["reused_blocks"], 64) + self.assertEqual(stats["freed_blocks"], 0) + self.assertEqual(stats["blocks_cached"], 64) - @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() Image.core.set_blocks_max(128) Image.core.set_block_size(4096) - Image.new('RGB', (256, 256)) - Image.new('RGB', (256, 256)) + Image.new("RGB", (256, 256)) + Image.new("RGB", (256, 256)) # Keep 16 blocks in cache Image.core.clear_cache(16) stats = Image.core.get_stats() - self.assertGreaterEqual(stats['new_count'], 2) - self.assertGreaterEqual(stats['allocated_blocks'], 64) - self.assertGreaterEqual(stats['reused_blocks'], 64) - self.assertGreaterEqual(stats['freed_blocks'], 48) - self.assertEqual(stats['blocks_cached'], 16) + self.assertGreaterEqual(stats["new_count"], 2) + self.assertGreaterEqual(stats["allocated_blocks"], 64) + self.assertGreaterEqual(stats["reused_blocks"], 64) + self.assertGreaterEqual(stats["freed_blocks"], 48) + self.assertEqual(stats["blocks_cached"], 16) def test_large_images(self): Image.core.reset_stats() Image.core.set_blocks_max(0) Image.core.set_block_size(4096) - Image.new('RGB', (2048, 16)) + Image.new("RGB", (2048, 16)) Image.core.clear_cache() stats = Image.core.get_stats() - self.assertGreaterEqual(stats['new_count'], 1) - self.assertGreaterEqual(stats['allocated_blocks'], 16) - self.assertGreaterEqual(stats['reused_blocks'], 0) - self.assertEqual(stats['blocks_cached'], 0) - if not is_pypy: - self.assertGreaterEqual(stats['freed_blocks'], 16) + self.assertGreaterEqual(stats["new_count"], 1) + self.assertGreaterEqual(stats["allocated_blocks"], 16) + self.assertGreaterEqual(stats["reused_blocks"], 0) + self.assertEqual(stats["blocks_cached"], 0) + if not is_pypy(): + self.assertGreaterEqual(stats["freed_blocks"], 16) class TestEnvVars(PillowTestCase): def tearDown(self): # Restore default values Image.core.set_alignment(1) - Image.core.set_block_size(1024*1024) + Image.core.set_block_size(1024 * 1024) Image.core.set_blocks_max(0) Image.core.clear_cache() def test_units(self): - Image._apply_env_variables({'PILLOW_BLOCKS_MAX': '2K'}) - self.assertEqual(Image.core.get_blocks_max(), 2*1024) - Image._apply_env_variables({'PILLOW_BLOCK_SIZE': '2m'}) - self.assertEqual(Image.core.get_block_size(), 2*1024*1024) + Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"}) + self.assertEqual(Image.core.get_blocks_max(), 2 * 1024) + Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) + self.assertEqual(Image.core.get_block_size(), 2 * 1024 * 1024) def test_warnings(self): self.assert_warning( - UserWarning, Image._apply_env_variables, - {'PILLOW_ALIGNMENT': '15'}) + UserWarning, Image._apply_env_variables, {"PILLOW_ALIGNMENT": "15"} + ) self.assert_warning( - UserWarning, Image._apply_env_variables, - {'PILLOW_BLOCK_SIZE': '1024'}) + UserWarning, Image._apply_env_variables, {"PILLOW_BLOCK_SIZE": "1024"} + ) self.assert_warning( - UserWarning, Image._apply_env_variables, - {'PILLOW_BLOCKS_MAX': 'wat'}) + UserWarning, Image._apply_env_variables, {"PILLOW_BLOCKS_MAX": "wat"} + ) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index bc0bab525..5d0d37099 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -1,21 +1,21 @@ -from .helper import PillowTestCase, hopper - from PIL import Image +from .helper import PillowTestCase, hopper + TEST_FILE = "Tests/images/hopper.ppm" ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS class TestDecompressionBomb(PillowTestCase): - def tearDown(self): Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT def test_no_warning_small_file(self): # Implicit assert: no warning. # A warning would cause a failure. - Image.open(TEST_FILE) + with Image.open(TEST_FILE): + pass def test_no_warning_no_limit(self): # Arrange @@ -26,29 +26,42 @@ class TestDecompressionBomb(PillowTestCase): # Act / Assert # Implicit assert: no warning. # A warning would cause a failure. - Image.open(TEST_FILE) + with Image.open(TEST_FILE): + pass def test_warning(self): # Set limit to trigger warning on the test file Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 self.assertEqual(Image.MAX_IMAGE_PIXELS, 128 * 128 - 1) - self.assert_warning(Image.DecompressionBombWarning, - Image.open, TEST_FILE) + def open(): + with Image.open(TEST_FILE): + pass + + self.assert_warning(Image.DecompressionBombWarning, open) def test_exception(self): # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 self.assertEqual(Image.MAX_IMAGE_PIXELS, 64 * 128 - 1) - self.assertRaises(Image.DecompressionBombError, - lambda: Image.open(TEST_FILE)) + with self.assertRaises(Image.DecompressionBombError): + with Image.open(TEST_FILE): + pass + + def test_exception_ico(self): + with self.assertRaises(Image.DecompressionBombError): + Image.open("Tests/images/decompression_bomb.ico") + + def test_exception_gif(self): + with self.assertRaises(Image.DecompressionBombError): + Image.open("Tests/images/decompression_bomb.gif") class TestDecompressionCrop(PillowTestCase): - def setUp(self): self.src = hopper() + self.addCleanup(self.src.close) Image.MAX_IMAGE_PIXELS = self.src.height * self.src.width * 4 - 1 def tearDown(self): @@ -58,21 +71,17 @@ class TestDecompressionCrop(PillowTestCase): # Crops can extend the extents, therefore we should have the # same decompression bomb warnings on them. box = (0, 0, self.src.width * 2, self.src.height * 2) - self.assert_warning(Image.DecompressionBombWarning, - self.src.crop, box) + self.assert_warning(Image.DecompressionBombWarning, self.src.crop, box) def test_crop_decompression_checks(self): im = Image.new("RGB", (100, 100)) - good_values = ((-9999, -9999, -9990, -9990), - (-999, -999, -990, -990)) + good_values = ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)) - warning_values = ((-160, -160, 99, 99), - (160, 160, -99, -99)) + warning_values = ((-160, -160, 99, 99), (160, 160, -99, -99)) - error_values = ((-99909, -99990, 99999, 99999), - (99909, 99990, -99999, -99999)) + error_values = ((-99909, -99990, 99999, 99999), (99909, 99990, -99999, -99999)) for value in good_values: self.assertEqual(im.crop(value).size, (9, 9)) diff --git a/Tests/test_features.py b/Tests/test_features.py index 15b5982ce..eb51407a1 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,44 +1,42 @@ -from .helper import unittest, PillowTestCase +import io +import unittest from PIL import features +from .helper import PillowTestCase + try: from PIL import _webp + HAVE_WEBP = True except ImportError: HAVE_WEBP = False class TestFeatures(PillowTestCase): - def test_check(self): # Check the correctness of the convenience function for module in features.modules: - self.assertEqual(features.check_module(module), - features.check(module)) + self.assertEqual(features.check_module(module), features.check(module)) for codec in features.codecs: - self.assertEqual(features.check_codec(codec), - features.check(codec)) + self.assertEqual(features.check_codec(codec), features.check(codec)) for feature in features.features: - self.assertEqual(features.check_feature(feature), - features.check(feature)) + self.assertEqual(features.check_feature(feature), features.check(feature)) @unittest.skipUnless(HAVE_WEBP, "WebP not available") def test_webp_transparency(self): - self.assertEqual(features.check('transp_webp'), - not _webp.WebPDecoderBuggyAlpha()) - self.assertEqual(features.check('transp_webp'), - _webp.HAVE_TRANSPARENCY) + self.assertEqual( + features.check("transp_webp"), not _webp.WebPDecoderBuggyAlpha() + ) + self.assertEqual(features.check("transp_webp"), _webp.HAVE_TRANSPARENCY) @unittest.skipUnless(HAVE_WEBP, "WebP not available") def test_webp_mux(self): - self.assertEqual(features.check('webp_mux'), - _webp.HAVE_WEBPMUX) + self.assertEqual(features.check("webp_mux"), _webp.HAVE_WEBPMUX) @unittest.skipUnless(HAVE_WEBP, "WebP not available") def test_webp_anim(self): - self.assertEqual(features.check('webp_anim'), - _webp.HAVE_WEBPANIM) + self.assertEqual(features.check("webp_anim"), _webp.HAVE_WEBPANIM) def test_check_modules(self): for feature in features.modules: @@ -63,3 +61,30 @@ class TestFeatures(PillowTestCase): module = "unsupported_module" # Act / Assert self.assertRaises(ValueError, features.check_module, module) + + def test_pilinfo(self): + buf = io.StringIO() + features.pilinfo(buf) + out = buf.getvalue() + lines = out.splitlines() + self.assertEqual(lines[0], "-" * 68) + self.assertTrue(lines[1].startswith("Pillow ")) + self.assertTrue(lines[2].startswith("Python ")) + lines = lines[3:] + while lines[0].startswith(" "): + lines = lines[1:] + self.assertEqual(lines[0], "-" * 68) + self.assertTrue(lines[1].startswith("Python modules loaded from ")) + self.assertTrue(lines[2].startswith("Binary modules loaded from ")) + self.assertEqual(lines[3], "-" * 68) + jpeg = ( + "\n" + + "-" * 68 + + "\n" + + "JPEG image/jpeg\n" + + "Extensions: .jfif, .jpe, .jpeg, .jpg\n" + + "Features: open, save\n" + + "-" * 68 + + "\n" + ) + self.assertIn(jpeg, out) 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 c51e2bb4e..e7f2c9315 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,21 +1,22 @@ -from .helper import PillowTestCase, hopper - -from PIL import Image, BmpImagePlugin import io +from PIL import BmpImagePlugin, Image + +from .helper import PillowTestCase, hopper + class TestFileBmp(PillowTestCase): - def roundtrip(self, im): outfile = self.tempfile("temp.bmp") - im.save(outfile, 'BMP') + im.save(outfile, "BMP") reloaded = Image.open(outfile) reloaded.load() self.assertEqual(im.mode, reloaded.mode) self.assertEqual(im.size, reloaded.size) self.assertEqual(reloaded.format, "BMP") + self.assertEqual(reloaded.get_format_mimetype(), "image/bmp") def test_sanity(self): self.roundtrip(hopper()) @@ -27,8 +28,7 @@ class TestFileBmp(PillowTestCase): def test_invalid_file(self): with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, - BmpImagePlugin.BmpImageFile, fp) + self.assertRaises(SyntaxError, BmpImagePlugin.BmpImageFile, fp) def test_save_to_bytes(self): output = io.BytesIO() @@ -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 @@ -61,17 +60,64 @@ class TestFileBmp(PillowTestCase): im = Image.open("Tests/images/hopper.bmp") # Act - im.save(outfile, 'JPEG', dpi=im.info['dpi']) + 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.info["dpi"], reloaded.info["dpi"]) self.assertEqual(im.size, reloaded.size) self.assertEqual(reloaded.format, "JPEG") + def test_load_dpi_rounding(self): + # Round up + with Image.open("Tests/images/hopper.bmp") as im: + self.assertEqual(im.info["dpi"], (96, 96)) + + # Round down + with Image.open("Tests/images/hopper_roundDown.bmp") as im: + self.assertEqual(im.info["dpi"], (72, 72)) + + def test_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)) + + im.save(outfile, dpi=(72.8, 72.8)) + with Image.open(outfile) as reloaded: + self.assertEqual(reloaded.info["dpi"], (73, 73)) + def test_load_dib(self): # test for #1293, Imagegrab returning Unsupported Bitfields Format - im = BmpImagePlugin.DibImageFile('Tests/images/clipboard.dib') - target = Image.open('Tests/images/clipboard_target.png') + im = Image.open("Tests/images/clipboard.dib") + self.assertEqual(im.format, "DIB") + self.assertEqual(im.get_format_mimetype(), "image/bmp") + + target = Image.open("Tests/images/clipboard_target.png") + self.assert_image_equal(im, target) + + def test_save_dib(self): + outfile = self.tempfile("temp.dib") + + im = Image.open("Tests/images/clipboard.dib") + 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) + + 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") + + # 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) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 307029136..540d89719 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,37 +1,37 @@ -from .helper import PillowTestCase, hopper - from PIL import BufrStubImagePlugin, Image +from .helper import PillowTestCase, hopper + 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 invalid_file = "Tests/images/flower.jpg" # Act / Assert - self.assertRaises(SyntaxError, - BufrStubImagePlugin.BufrStubImageFile, invalid_file) + self.assertRaises( + SyntaxError, BufrStubImagePlugin.BufrStubImageFile, invalid_file + ) 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 8ca3310e5..2f931fb68 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,22 +1,20 @@ -from .helper import PillowTestCase, hopper +from PIL import ContainerIO, Image -from PIL import Image -from PIL import ContainerIO +from .helper import PillowTestCase, hopper TEST_FILE = "Tests/images/dummy.container" class TestFileContainer(PillowTestCase): - def test_sanity(self): dir(Image) 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.assertEqual(container.isatty(), 0) + self.assertFalse(container.isatty()) def test_seek_mode_0(self): # Arrange @@ -106,14 +104,16 @@ class TestFileContainer(PillowTestCase): def test_readlines(self): # Arrange - expected = ["This is line 1\n", - "This is line 2\n", - "This is line 3\n", - "This is line 4\n", - "This is line 5\n", - "This is line 6\n", - "This is line 7\n", - "This is line 8\n"] + expected = [ + "This is line 1\n", + "This is line 2\n", + "This is line 3\n", + "This is line 4\n", + "This is line 5\n", + "This is line 6\n", + "This is line 7\n", + "This is line 8\n", + ] with open(TEST_FILE) as fh: container = ContainerIO.ContainerIO(fh, 0, 120) diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 734c330e6..0b2f7a98c 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -1,12 +1,11 @@ -from .helper import PillowTestCase +from PIL import CurImagePlugin, Image -from PIL import Image, CurImagePlugin +from .helper import PillowTestCase TEST_FILE = "Tests/images/deerstalker.cur" class TestFileCur(PillowTestCase): - def test_sanity(self): im = Image.open(TEST_FILE) @@ -20,8 +19,7 @@ class TestFileCur(PillowTestCase): def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - CurImagePlugin.CurImageFile, invalid_file) + self.assertRaises(SyntaxError, CurImagePlugin.CurImageFile, invalid_file) no_cursors_file = "Tests/images/no_cursors.cur" diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 0da364648..e9411dbf8 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,66 +1,83 @@ -from .helper import PillowTestCase, hopper +import unittest -from PIL import Image, DcxImagePlugin +from PIL import DcxImagePlugin, Image + +from .helper import PillowTestCase, hopper, is_pypy # Created with ImageMagick: convert hopper.ppm hopper.dcx TEST_FILE = "Tests/images/hopper.dcx" class TestFileDcx(PillowTestCase): - def test_sanity(self): # Arrange # 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): with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, - DcxImagePlugin.DcxImageFile, fp) + self.assertRaises(SyntaxError, DcxImagePlugin.DcxImageFile, fp) def test_tell(self): # Arrange - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: - # Act - frame = im.tell() + # Act + frame = im.tell() - # Assert - self.assertEqual(frame, 0) + # Assert + self.assertEqual(frame, 0) def test_n_frames(self): - im = Image.open(TEST_FILE) - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) + with Image.open(TEST_FILE) as im: + self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) def test_eoferror(self): - im = Image.open(TEST_FILE) - n_frames = im.n_frames + with Image.open(TEST_FILE) as im: + n_frames = im.n_frames - # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + # Test seeking past the last frame + self.assertRaises(EOFError, im.seek, n_frames) + self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames-1) + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) def test_seek_too_far(self): # Arrange - im = Image.open(TEST_FILE) - frame = 999 # too big on purpose + with Image.open(TEST_FILE) as im: + frame = 999 # too big on purpose # Act / Assert self.assertRaises(EOFError, im.seek, frame) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index af2524c4a..8ef90e86e 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,12 +1,15 @@ from io import BytesIO +from PIL import DdsImagePlugin, Image + from .helper import PillowTestCase -from PIL import Image, DdsImagePlugin TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" +TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" +TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/uncompressed_rgb.dds" class TestFileDds(PillowTestCase): @@ -14,7 +17,7 @@ class TestFileDds(PillowTestCase): def test_sanity_dxt1(self): """Check DXT1 images can be opened""" - target = Image.open(TEST_FILE_DXT1.replace('.dds', '.png')) + target = Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) im = Image.open(TEST_FILE_DXT1) im.load() @@ -23,12 +26,12 @@ class TestFileDds(PillowTestCase): self.assertEqual(im.mode, "RGBA") self.assertEqual(im.size, (256, 256)) - self.assert_image_equal(target.convert('RGBA'), im) + self.assert_image_equal(target.convert("RGBA"), im) def test_sanity_dxt5(self): """Check DXT5 images can be opened""" - target = Image.open(TEST_FILE_DXT5.replace('.dds', '.png')) + target = Image.open(TEST_FILE_DXT5.replace(".dds", ".png")) im = Image.open(TEST_FILE_DXT5) im.load() @@ -42,7 +45,7 @@ class TestFileDds(PillowTestCase): def test_sanity_dxt3(self): """Check DXT3 images can be opened""" - target = Image.open(TEST_FILE_DXT3.replace('.dds', '.png')) + target = Image.open(TEST_FILE_DXT3.replace(".dds", ".png")) im = Image.open(TEST_FILE_DXT3) im.load() @@ -56,7 +59,7 @@ class TestFileDds(PillowTestCase): def test_dx10_bc7(self): """Check DX10 images can be opened""" - target = Image.open(TEST_FILE_DX10_BC7.replace('.dds', '.png')) + target = Image.open(TEST_FILE_DX10_BC7.replace(".dds", ".png")) im = Image.open(TEST_FILE_DX10_BC7) im.load() @@ -67,6 +70,42 @@ 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, + Image.open, + "Tests/images/unimplemented_dxgi_format.dds", + ) + + def test_uncompressed_rgb(self): + """Check uncompressed RGB images can be opened""" + + target = Image.open(TEST_FILE_UNCOMPRESSED_RGB.replace(".dds", ".png")) + + im = Image.open(TEST_FILE_UNCOMPRESSED_RGB) + im.load() + + self.assertEqual(im.format, "DDS") + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (800, 600)) + + self.assert_image_equal(target, im) + def test__validate_true(self): """Check valid prefix""" # Arrange @@ -91,7 +130,7 @@ class TestFileDds(PillowTestCase): def test_short_header(self): """ Check a short header""" - with open(TEST_FILE_DXT5, 'rb') as f: + with open(TEST_FILE_DXT5, "rb") as f: img_file = f.read() def short_header(): @@ -102,7 +141,7 @@ class TestFileDds(PillowTestCase): def test_short_file(self): """ Check that the appropriate error is thrown for a short file""" - with open(TEST_FILE_DXT5, 'rb') as f: + with open(TEST_FILE_DXT5, "rb") as f: img_file = f.read() def short_file(): @@ -110,3 +149,10 @@ class TestFileDds(PillowTestCase): im.load() self.assertRaises(IOError, short_file) + + def test_unimplemented_pixel_format(self): + self.assertRaises( + NotImplementedError, + Image.open, + "Tests/images/unimplemented_pixel_format.dds", + ) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 1f6025d69..0ab14e4eb 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,7 +1,9 @@ -from .helper import unittest, PillowTestCase, hopper - -from PIL import Image, EpsImagePlugin import io +import unittest + +from PIL import EpsImagePlugin, Image + +from .helper import PillowTestCase, hopper HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() @@ -21,84 +23,81 @@ file3 = "Tests/images/binary_preview_map.eps" class TestFileEps(PillowTestCase): - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") def test_sanity(self): # Regular scale - image1 = Image.open(file1) - 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" - self.assertRaises(SyntaxError, - EpsImagePlugin.EpsImageFile, invalid_file) + self.assertRaises(SyntaxError, EpsImagePlugin.EpsImageFile, invalid_file) @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 open(self.tempfile("temp_iobase.eps"), "wb") as fh: + image1.save(fh, "EPS") @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") def test_bytesio_object(self): - with open(file1, 'rb') as f: + with open(file1, "rb") as f: img_bytes = io.BytesIO(f.read()) img = Image.open(img_bytes) @@ -110,7 +109,7 @@ class TestFileEps(PillowTestCase): def test_image_mode_not_supported(self): im = hopper("RGBA") - tmpfile = self.tempfile('temp.eps') + tmpfile = self.tempfile("temp.eps") self.assertRaises(ValueError, im.save, tmpfile) @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") @@ -121,18 +120,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): @@ -142,86 +141,73 @@ 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" % ("".join( - "%s" % ord(s) - for s in ending)) - self.assertEqual(t.readline().strip('\r\n'), 'something', ending) - self.assertEqual(t.readline().strip('\r\n'), 'else', ending) - self.assertEqual(t.readline().strip('\r\n'), 'baz', ending) - self.assertEqual(t.readline().strip('\r\n'), 'bif', ending) + ending = "Failure with line ending: %s" % ( + "".join("%s" % ord(s) for s in ending) + ) + self.assertEqual(t.readline().strip("\r\n"), "something", ending) + self.assertEqual(t.readline().strip("\r\n"), "else", ending) + self.assertEqual(t.readline().strip("\r\n"), "baz", ending) + self.assertEqual(t.readline().strip("\r\n"), "bif", ending) def _test_readline_io_psfile(self, test_string, ending): - f = io.BytesIO(test_string.encode('latin-1')) + f = io.BytesIO(test_string.encode("latin-1")) t = EpsImagePlugin.PSFile(f) self._test_readline(t, ending) def _test_readline_file_psfile(self, test_string, ending): - f = self.tempfile('temp.txt') - with open(f, 'wb') as w: - w.write(test_string.encode('latin-1')) + f = self.tempfile("temp.txt") + with open(f, "wb") as w: + w.write(test_string.encode("latin-1")) - with open(f, 'rb') as r: + with open(f, "rb") as r: t = EpsImagePlugin.PSFile(r) self._test_readline(t, ending) def test_readline(self): # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' - line_endings = ['\r\n', '\n', '\n\r', '\r'] - strings = ['something', 'else', 'baz', 'bif'] + line_endings = ["\r\n", "\n", "\n\r", "\r"] + strings = ["something", "else", "baz", "bif"] for ending in line_endings: s = ending.join(strings) @@ -231,23 +217,25 @@ class TestFileEps(PillowTestCase): def test_open_eps(self): # https://github.com/python-pillow/Pillow/issues/1104 # Arrange - FILES = ["Tests/images/illu10_no_preview.eps", - "Tests/images/illu10_preview.eps", - "Tests/images/illuCS6_no_preview.eps", - "Tests/images/illuCS6_preview.eps"] + FILES = [ + "Tests/images/illu10_no_preview.eps", + "Tests/images/illu10_preview.eps", + "Tests/images/illuCS6_no_preview.eps", + "Tests/images/illuCS6_preview.eps", + ] # Act / Assert for filename in FILES: - img = Image.open(filename) - self.assertEqual(img.mode, "RGB") + 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 6cb2de110..933e0fd12 100644 --- a/Tests/test_file_fitsstub.py +++ b/Tests/test_file_fitsstub.py @@ -1,46 +1,46 @@ -from .helper import PillowTestCase - from PIL import FitsStubImagePlugin, Image +from .helper import PillowTestCase + TEST_FILE = "Tests/images/hopper.fits" class TestFileFitsStub(PillowTestCase): - def test_open(self): # Act - im = Image.open(TEST_FILE) + 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 invalid_file = "Tests/images/flower.jpg" # Act / Assert - self.assertRaises(SyntaxError, - FitsStubImagePlugin.FITSStubImageFile, invalid_file) + self.assertRaises( + SyntaxError, FitsStubImagePlugin.FITSStubImageFile, invalid_file + ) 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 f67f0ada1..895942d70 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,6 +1,8 @@ -from .helper import PillowTestCase +import unittest -from PIL import Image, FliImagePlugin +from PIL import FliImagePlugin, Image + +from .helper import PillowTestCase, is_pypy # created as an export of a palette image from Gimp2.6 # save as...-> hopper.fli, default options. @@ -11,89 +13,104 @@ 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" - self.assertRaises(SyntaxError, - FliImagePlugin.FliImageFile, invalid_file) + self.assertRaises(SyntaxError, FliImagePlugin.FliImageFile, invalid_file) def test_n_frames(self): - im = Image.open(static_test_file) - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) + with Image.open(static_test_file) as im: + self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) - im = Image.open(animated_test_file) - self.assertEqual(im.n_frames, 384) - self.assertTrue(im.is_animated) + with Image.open(animated_test_file) as im: + self.assertEqual(im.n_frames, 384) + self.assertTrue(im.is_animated) def test_eoferror(self): - im = Image.open(animated_test_file) - n_frames = im.n_frames + with Image.open(animated_test_file) as im: + n_frames = im.n_frames - # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + # Test seeking past the last frame + self.assertRaises(EOFError, im.seek, n_frames) + self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames-1) + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) def test_seek_tell(self): - im = Image.open(animated_test_file) + with Image.open(animated_test_file) as im: - layer_number = im.tell() - self.assertEqual(layer_number, 0) + layer_number = im.tell() + self.assertEqual(layer_number, 0) - im.seek(0) - layer_number = im.tell() - self.assertEqual(layer_number, 0) + im.seek(0) + layer_number = im.tell() + self.assertEqual(layer_number, 0) - im.seek(1) - layer_number = im.tell() - self.assertEqual(layer_number, 1) + im.seek(1) + layer_number = im.tell() + self.assertEqual(layer_number, 1) - im.seek(2) - layer_number = im.tell() - self.assertEqual(layer_number, 2) + im.seek(2) + layer_number = im.tell() + self.assertEqual(layer_number, 2) - im.seek(1) - layer_number = im.tell() - self.assertEqual(layer_number, 1) + im.seek(1) + layer_number = im.tell() + self.assertEqual(layer_number, 1) def test_seek(self): - im = Image.open(animated_test_file) - im.seek(50) + with Image.open(animated_test_file) as im: + im.seek(50) - expected = Image.open("Tests/images/a_fli.png") - self.assert_image_equal(im, expected) + with Image.open("Tests/images/a_fli.png") as expected: + self.assert_image_equal(im, expected) diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index 7283ba088..7c985be30 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -1,4 +1,6 @@ -from .helper import unittest, PillowTestCase +import unittest + +from .helper import PillowTestCase try: from PIL import FpxImagePlugin @@ -10,14 +12,11 @@ else: @unittest.skipUnless(olefile_installed, "olefile package not installed") class TestFileFpx(PillowTestCase): - def test_invalid_file(self): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - FpxImagePlugin.FpxImageFile, invalid_file) + self.assertRaises(SyntaxError, FpxImagePlugin.FpxImageFile, invalid_file) # Test a valid OLE file, but not an FPX file ole_file = "Tests/images/test-ole-file.doc" - self.assertRaises(SyntaxError, - FpxImagePlugin.FpxImageFile, ole_file) + self.assertRaises(SyntaxError, FpxImagePlugin.FpxImageFile, ole_file) diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index 07e29d7e0..7d30042ca 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,16 +1,16 @@ -from .helper import PillowTestCase from PIL import Image +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') + im = Image.open("Tests/images/ftex_uncompressed.ftu") + target = Image.open("Tests/images/ftex_uncompressed.png") 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) + 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) diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index 1eb66264d..4c26579a8 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -1,19 +1,15 @@ -from .helper import PillowTestCase +from PIL import GbrImagePlugin, Image -from PIL import Image, GbrImagePlugin +from .helper import PillowTestCase class TestFileGbr(PillowTestCase): - def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - GbrImagePlugin.GbrImageFile, invalid_file) + 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 67c9fba7b..0b7543a31 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -1,22 +1,20 @@ -from .helper import PillowTestCase +from PIL import GdImageFile, UnidentifiedImageError -from PIL import GdImageFile +from .helper import PillowTestCase 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') + self.assertRaises(ValueError, GdImageFile.open, TEST_GD_FILE, "bad mode") def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(IOError, GdImageFile.open, invalid_file) + self.assertRaises(UnidentifiedImageError, GdImageFile.open, invalid_file) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 6a4b14d40..bed25c890 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,11 +1,13 @@ -from .helper import unittest, PillowTestCase, hopper, netpbm_available - -from PIL import Image, ImagePalette, GifImagePlugin - +import unittest from io import BytesIO +from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette + +from .helper import PillowTestCase, hopper, is_pypy, netpbm_available + try: from PIL import _webp + HAVE_WEBP = True except ImportError: HAVE_WEBP = False @@ -20,30 +22,45 @@ with open(TEST_GIF, "rb") as f: class TestFileGif(PillowTestCase): - def setUp(self): if "gif_encoder" not in codecs or "gif_decoder" not in codecs: self.skipTest("gif support not available") # can this happen? def test_sanity(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): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - GifImagePlugin.GifImageFile, invalid_file) + self.assertRaises(SyntaxError, GifImagePlugin.GifImageFile, invalid_file) def test_optimize(self): def test_grayscale(optimize): @@ -59,7 +76,7 @@ class TestFileGif(PillowTestCase): return len(test_file.getvalue()) self.assertEqual(test_grayscale(0), 800) - self.assertEqual(test_grayscale(1), 38) + self.assertEqual(test_grayscale(1), 44) self.assertEqual(test_bilevel(0), 800) self.assertEqual(test_bilevel(1), 800) @@ -70,19 +87,20 @@ class TestFileGif(PillowTestCase): # Check for correctness after conversion back to RGB def check(colors, size, expected_palette_length): # make an image with empty colors in the start of the palette range - im = Image.frombytes('P', (colors, colors), - bytes(bytearray(range(256-colors, 256))*colors)) + im = Image.frombytes( + "P", (colors, colors), bytes(range(256 - colors, 256)) * colors + ) im = im.resize((size, size)) outfile = BytesIO() - im.save(outfile, 'GIF') + im.save(outfile, "GIF") outfile.seek(0) reloaded = Image.open(outfile) # check palette length - palette_length = max(i+1 for i, v in enumerate(reloaded.histogram()) if v) + 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) @@ -100,378 +118,491 @@ 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") def test_roundtrip(self): - out = self.tempfile('temp.gif') + 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) + out = self.tempfile("temp.gif") + 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') + 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'] + 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 def roundtrip(im, *args, **kwargs): - out = self.tempfile('temp.gif') + out = self.tempfile("temp.gif") im.copy().save(out, *args, **kwargs) reloaded = Image.open(out) 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): + with Image.open("Tests/images/iss634.gif") as im: + im.seek(2) + im.seek(1) + + with Image.open("Tests/images/iss634.gif") as expected: + expected.seek(1) + self.assert_image_equal(im, expected) def test_n_frames(self): - for path, n_frames in [ - [TEST_GIF, 1], - ['Tests/images/iss634.gif', 42] - ]: + 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') + out = self.tempfile("temp.gif") im_list = [ - Image.new('L', (100, 100), '#000'), - Image.new('L', (100, 100), '#111'), - Image.new('L', (100, 100), '#222'), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + Image.new("L", (100, 100), "#222"), ] for method in range(0, 4): im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - disposal=method + 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( out, save_all=True, append_images=im_list[1:], - disposal=tuple(range(len(im_list))) - ) + 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") + + # 4 backgrounds: White, Grey, Black, Red + circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] + + im_list = [] + for circle in circles: + img = Image.new("RGB", (100, 100), (255, 0, 0)) + + # Red circle in center of each frame + d = ImageDraw.Draw(img) + d.ellipse([(40, 40), (60, 60)], fill=circle) + + im_list.append(img) + + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) + + with Image.open(out) as img: + for i, circle in enumerate(circles): + img.seek(i) + rgb_img = img.convert("RGB") + + # Check top left pixel matches background + self.assertEqual(rgb_img.getpixel((0, 0)), (255, 0, 0)) + + # Center remains red every frame + self.assertEqual(rgb_img.getpixel((50, 50)), circle) + + def test_dispose2_diff(self): + out = self.tempfile("temp.gif") + + # 4 frames: red/blue, red/red, blue/blue, red/blue + circles = [ + ((255, 0, 0, 255), (0, 0, 255, 255)), + ((255, 0, 0, 255), (255, 0, 0, 255)), + ((0, 0, 255, 255), (0, 0, 255, 255)), + ((255, 0, 0, 255), (0, 0, 255, 255)), + ] + + im_list = [] + for i in range(len(circles)): + # Transparent BG + img = Image.new("RGBA", (100, 100), (255, 255, 255, 0)) + + # Two circles per frame + d = ImageDraw.Draw(img) + d.ellipse([(0, 30), (40, 70)], fill=circles[i][0]) + d.ellipse([(60, 30), (100, 70)], fill=circles[i][1]) + + im_list.append(img) + + im_list[0].save( + out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0 + ) + + with Image.open(out) as img: + for i, colours in enumerate(circles): + img.seek(i) + rgb_img = img.convert("RGBA") + + # Check left circle is correct colour + self.assertEqual(rgb_img.getpixel((20, 50)), colours[0]) + + # Check right circle is correct colour + self.assertEqual(rgb_img.getpixel((80, 50)), colours[1]) + + # Check BG is correct colour + self.assertEqual(rgb_img.getpixel((1, 1)), (255, 255, 255, 0)) + + def test_dispose2_background(self): + out = self.tempfile("temp.gif") + + im_list = [] + + im = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im) + d.rectangle([(50, 0), (100, 100)], fill="#f00") + d.rectangle([(0, 0), (50, 100)], fill="#0f0") + im_list.append(im) + + im = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im) + d.rectangle([(0, 0), (100, 50)], fill="#f00") + d.rectangle([(0, 50), (100, 100)], fill="#0f0") + im_list.append(im) + + im_list[0].save( + out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1 + ) + + with Image.open(out) as im: + im.seek(1) + self.assertEqual(im.getpixel((0, 0)), 0) def test_iss634(self): - 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 - out = self.tempfile('temp.gif') - im = Image.new('L', (100, 100), '#000') + out = self.tempfile("temp.gif") + im = Image.new("L", (100, 100), "#000") # Check that the argument has priority over the info settings - im.info['duration'] = 100 + 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] - out = self.tempfile('temp.gif') + out = self.tempfile("temp.gif") im_list = [ - Image.new('L', (100, 100), '#000'), - Image.new('L', (100, 100), '#111'), - Image.new('L', (100, 100), '#222') + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + Image.new("L", (100, 100), "#222"), ] # duration as list im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - duration=duration_list + 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) + 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] - out = self.tempfile('temp.gif') + out = self.tempfile("temp.gif") im_list = [ - Image.new('L', (100, 100), '#000'), - Image.new('L', (100, 100), '#000'), - Image.new('L', (100, 100), '#000'), - Image.new('L', (100, 100), '#111') + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), ] # duration as list im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - duration=duration_list + out, save_all=True, append_images=im_list[1:], duration=duration_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): + out = self.tempfile("temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + ] + + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=duration + ) + with Image.open(out) as reread: + # Assert that all frames were combined + self.assertEqual(reread.n_frames, 1) + + # 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 - out = self.tempfile('temp.gif') - im = Image.new('L', (100, 100), '#000') + 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 + out = self.tempfile("temp.gif") + im = Image.new("L", (100, 100), "#000") + im.info["background"] = 1 im.save(out) - reread = Image.open(out) + with Image.open(out) as reread: - self.assertEqual(reread.info['background'], im.info['background']) + self.assertEqual(reread.info["background"], im.info["background"]) if HAVE_WEBP and _webp.HAVE_WEBPANIM: im = Image.open("Tests/images/hopper.webp") - self.assertIsInstance(im.info['background'], tuple) + self.assertIsInstance(im.info["background"], tuple) im.save(out) def test_comment(self): - im = Image.open(TEST_GIF) - self.assertEqual(im.info['comment'], - b"File written by Adobe Photoshop\xa8 4.0") + with Image.open(TEST_GIF) as im: + self.assertEqual( + im.info["comment"], b"File written by Adobe Photoshop\xa8 4.0" + ) - out = self.tempfile('temp.gif') - im = Image.new('L', (100, 100), '#000') - im.info['comment'] = b"Test comment text" - im.save(out) - reread = Image.open(out) + out = self.tempfile("temp.gif") + im = Image.new("L", (100, 100), "#000") + im.info["comment"] = b"Test comment text" + im.save(out) + with Image.open(out) as reread: - self.assertEqual(reread.info['comment'], im.info['comment']) + self.assertEqual(reread.info["comment"], im.info["comment"]) def test_comment_over_255(self): - out = self.tempfile('temp.gif') - im = Image.new('L', (100, 100), '#000') + out = self.tempfile("temp.gif") + im = Image.new("L", (100, 100), "#000") comment = b"Test comment text" while len(comment) < 256: comment += comment - im.info['comment'] = comment + im.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') + 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') + im = Image.new("L", (100, 100), "#000") assertVersionAfterSave(im, b"GIF87a") # Test setting the version to 89a - im = Image.new('L', (100, 100), '#000') + im = Image.new("L", (100, 100), "#000") im.info["version"] = b"89a" assertVersionAfterSave(im, b"GIF89a") @@ -480,41 +611,40 @@ 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') + out = self.tempfile("temp.gif") # Test appending single frame images - im = Image.new('RGB', (100, 100), '#f00') - ims = [Image.new('RGB', (100, 100), color) for color - in ['#0f0', '#00f']] + im = Image.new("RGB", (100, 100), "#f00") + ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] im.copy().save(out, save_all=True, append_images=ims) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 3) + with Image.open(out) as reread: + self.assertEqual(reread.n_frames, 3) # Tests appending using a generator def imGenerator(ims): - for im in ims: - yield im + yield from ims + im.save(out, save_all=True, append_images=imGenerator(ims)) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 3) + with Image.open(out) as reread: + self.assertEqual(reread.n_frames, 3) # Tests appending single and multiple frame images - im = Image.open("Tests/images/dispose_none.gif") - ims = [Image.open("Tests/images/dispose_prev.gif")] - im.save(out, save_all=True, append_images=ims) + with Image.open("Tests/images/dispose_none.gif") as im: + with Image.open("Tests/images/dispose_prev.gif") as im2: + im.save(out, save_all=True, append_images=[im2]) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 10) + with Image.open(out) as reread: + self.assertEqual(reread.n_frames, 10) def test_transparent_optimize(self): # from issue #2195, if the transparent color is incorrectly @@ -523,112 +653,111 @@ 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))) - palette = ImagePalette.ImagePalette("RGB", list(range(256))*3) + data = bytes(range(1, 254)) + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - im = Image.new('L', (253, 1)) + im = Image.new("L", (253, 1)) im.frombytes(data) im.putpalette(palette) - out = self.tempfile('temp.gif') + 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') + out = self.tempfile("temp.gif") # Single frame - im = Image.new('RGB', (1, 1)) - im.info['transparency'] = (255, 0, 0) + im = Image.new("RGB", (1, 1)) + 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)) - im.info['transparency'] = b"" - ims = [Image.new('RGB', (1, 1))] - self.assert_warning(UserWarning, - im.save, out, save_all=True, append_images=ims) + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = b"" + ims = [Image.new("RGB", (1, 1))] + self.assert_warning(UserWarning, im.save, out, save_all=True, append_images=ims) - 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') + out = self.tempfile("temp.gif") - im = Image.new('RGB', (100, 100), '#fff') - ims = [Image.new("RGB", (100, 100), '#000')] + im = Image.new("RGB", (100, 100), "#fff") + ims = [Image.new("RGB", (100, 100), "#000")] im.save(out, save_all=True, append_images=ims) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 2) + with Image.open(out) as reread: + self.assertEqual(reread.n_frames, 2) def test_palette_save_L(self): # generate an L mode image with a separate palette - im = hopper('P') - im_l = Image.frombytes('L', im.size, im.tobytes()) - palette = bytes(bytearray(im.getpalette())) + im = hopper("P") + im_l = Image.frombytes("L", im.size, im.tobytes()) + palette = bytes(im.getpalette()) - out = self.tempfile('temp.gif') + 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 # would look like. # Forcing a non-straight grayscale palette. - im = hopper('P') - palette = bytes(bytearray([255-i//3 for i in range(768)])) + im = hopper("P") + palette = bytes([255 - i // 3 for i in range(768)]) - out = self.tempfile('temp.gif') + 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 # effectively the same as test_palette_save_P - im = hopper('P') - palette = ImagePalette.ImagePalette('RGB', list(range(256))[::-1]*3) + im = hopper("P") + palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) - out = self.tempfile('temp.gif') + 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' - im = hopper('I') + im = hopper("I") - out = self.tempfile('temp.gif') + out = self.tempfile("temp.gif") im.save(out) - reloaded = Image.open(out) - self.assert_image_equal(reloaded.convert('L'), im.convert('L')) + with Image.open(out) as reloaded: + self.assert_image_equal(reloaded.convert("L"), im.convert("L")) def test_getdata(self): # test getheader/getdata against legacy values # Create a 'P' image with holes in the palette im = Image._wedge().resize((16, 16)) - im.putpalette(ImagePalette.ImagePalette('RGB')) - im.info = {'background': 0} + 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: @@ -636,10 +765,11 @@ class TestFileGif(PillowTestCase): d = GifImagePlugin.getdata(im) import pickle + # Enable to get target values on pre-refactor version # with open('Tests/images/gif_header_data.pkl', 'wb') as f: # pickle.dump((h, d), f, 1) - with open('Tests/images/gif_header_data.pkl', 'rb') as f: + with open("Tests/images/gif_header_data.pkl", "rb") as f: (h_target, d_target) = pickle.load(f) self.assertEqual(h, h_target) @@ -649,8 +779,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') + 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() - self.assertEqual(im.tile[0][3][0], 11) # LZW bits - # codec error prepatch - im.load() + def test_extents(self): + with Image.open("Tests/images/test_extents.gif") as im: + self.assertEqual(im.size, (100, 100)) + im.seek(1) + self.assertEqual(im.size, (150, 150)) diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index c89440239..bafee79a3 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -1,10 +1,9 @@ -from .helper import PillowTestCase - from PIL import GimpGradientFile +from .helper import PillowTestCase + class TestImage(PillowTestCase): - def test_linear_pos_le_middle(self): # Arrange middle = 0.5 @@ -96,6 +95,7 @@ class TestImage(PillowTestCase): def test_load_via_imagepalette(self): # Arrange from PIL import ImagePalette + test_file = "Tests/images/gimp_gradient.ggr" # Act @@ -109,6 +109,7 @@ class TestImage(PillowTestCase): def test_load_1_3_via_imagepalette(self): # Arrange from PIL import ImagePalette + # GIMP 1.3 gradient files contain a name field test_file = "Tests/images/gimp_gradient_with_name.ggr" diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index 1ebac44e7..a1677f0cb 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,26 +1,25 @@ -from .helper import PillowTestCase - from PIL.GimpPaletteFile import GimpPaletteFile +from .helper import PillowTestCase + class TestImage(PillowTestCase): - def test_sanity(self): - with open('Tests/images/test.gpl', 'rb') as fp: + with open("Tests/images/test.gpl", "rb") as fp: GimpPaletteFile(fp) - with open('Tests/images/hopper.jpg', 'rb') as fp: + with open("Tests/images/hopper.jpg", "rb") as fp: self.assertRaises(SyntaxError, GimpPaletteFile, fp) - with open('Tests/images/bad_palette_file.gpl', 'rb') as fp: + with open("Tests/images/bad_palette_file.gpl", "rb") as fp: self.assertRaises(SyntaxError, GimpPaletteFile, fp) - with open('Tests/images/bad_palette_entry.gpl', 'rb') as fp: + with open("Tests/images/bad_palette_entry.gpl", "rb") as fp: self.assertRaises(ValueError, GimpPaletteFile, fp) def test_get_palette(self): # Arrange - with open('Tests/images/custom_gimp_palette.gpl', 'rb') as fp: + with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: palette_file = GimpPaletteFile(fp) # Act diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 79e826945..92d112ced 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,37 +1,37 @@ -from .helper import PillowTestCase, hopper - from PIL import GribStubImagePlugin, Image +from .helper import PillowTestCase, hopper + 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 invalid_file = "Tests/images/flower.jpg" # Act / Assert - self.assertRaises(SyntaxError, - GribStubImagePlugin.GribStubImageFile, invalid_file) + self.assertRaises( + SyntaxError, GribStubImagePlugin.GribStubImageFile, invalid_file + ) 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 598ef0c5c..cdaad0cf7 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,46 +1,46 @@ -from .helper import PillowTestCase - from PIL import Hdf5StubImagePlugin, Image +from .helper import PillowTestCase + TEST_FILE = "Tests/images/hdf5.h5" class TestFileHdf5Stub(PillowTestCase): - def test_open(self): # Act - im = Image.open(TEST_FILE) + 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 invalid_file = "Tests/images/flower.jpg" # Act / Assert - self.assertRaises(SyntaxError, - Hdf5StubImagePlugin.HDF5StubImageFile, invalid_file) + self.assertRaises( + SyntaxError, Hdf5StubImagePlugin.HDF5StubImageFile, invalid_file + ) 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 ac60731f9..e258601ce 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,31 +1,31 @@ -from .helper import unittest, PillowTestCase - -from PIL import Image, IcnsImagePlugin - import io import sys +import unittest + +from PIL import IcnsImagePlugin, Image + +from .helper import PillowTestCase # sample icon file TEST_FILE = "Tests/images/pillow.icns" -enable_jpeg2k = hasattr(Image.core, 'jp2klib_version') +enable_jpeg2k = hasattr(Image.core, "jp2klib_version") class TestFileIcns(PillowTestCase): - def test_sanity(self): # Loading this icon by default should result in the largest size # (512x512@2x) being loaded - im = Image.open(TEST_FILE) + 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") + @unittest.skipIf(sys.platform != "darwin", "requires macOS") def test_save(self): im = Image.open(TEST_FILE) @@ -38,12 +38,12 @@ class TestFileIcns(PillowTestCase): self.assertEqual(reread.size, (1024, 1024)) self.assertEqual(reread.format, "ICNS") - @unittest.skipIf(sys.platform != 'darwin', "requires macOS") + @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)) + provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) im.save(temp_file, append_images=[provided_im]) reread = Image.open(temp_file) @@ -57,32 +57,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 - im2 = Image.open(TEST_FILE) - im2.size = (w, h, r) - im2.load() - self.assertEqual(im2.mode, 'RGBA') - self.assertEqual(im2.size, (wr, hr)) + with Image.open(TEST_FILE) as im: + 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 @@ -95,18 +94,18 @@ 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: + with open(TEST_FILE, "rb") as fp: icns_file = IcnsImagePlugin.IcnsFile(fp) im = icns_file.getimage() @@ -118,6 +117,5 @@ class TestFileIcns(PillowTestCase): self.assertEqual(im.size, (512, 512)) def test_not_an_icns_file(self): - with io.BytesIO(b'invalid\n') as fp: - self.assertRaises(SyntaxError, - IcnsImagePlugin.IcnsFile, fp) + with io.BytesIO(b"invalid\n") as fp: + self.assertRaises(SyntaxError, IcnsImagePlugin.IcnsFile, fp) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index b0d33e33f..ac6b19041 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,24 +1,24 @@ -from .helper import PillowTestCase, hopper - import io -from PIL import Image, IcoImagePlugin + +from PIL import IcoImagePlugin, Image, ImageDraw + +from .helper import PillowTestCase, hopper 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") + self.assertEqual(im.get_format_mimetype(), "image/x-icon") def test_invalid_file(self): with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, - IcoImagePlugin.IcoImageFile, fp) + self.assertRaises(SyntaxError, IcoImagePlugin.IcoImageFile, fp) def test_save_to_bytes(self): output = io.BytesIO() @@ -28,13 +28,12 @@ class TestFileIco(PillowTestCase): # the default image output.seek(0) reloaded = Image.open(output) - self.assertEqual(reloaded.info['sizes'], {(32, 32), (64, 64)}) + 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.assert_image_equal(reloaded, hopper().resize((64, 64), Image.LANCZOS)) # the other one output.seek(0) @@ -44,26 +43,25 @@ class TestFileIco(PillowTestCase): 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.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 @@ -71,14 +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) + with Image.open(outfile) as im_saved: + # Assert + self.assertEqual( + im_saved.info["sizes"], {(16, 16), (24, 24), (32, 32), (48, 48)} + ) - # 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 + 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): + 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) + + 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 8e774ce0a..1a5638523 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,71 +1,88 @@ -from .helper import PillowTestCase, hopper +import unittest from PIL import Image, ImImagePlugin +from .helper import PillowTestCase, hopper, is_pypy + # sample im 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"]: - out = self.tempfile('temp.im') + 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') + out = self.tempfile("temp.im") im = hopper("HSV") self.assertRaises(ValueError, im.save, out) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - ImImagePlugin.ImImageFile, invalid_file) + self.assertRaises(SyntaxError, ImImagePlugin.ImImageFile, invalid_file) def test_number(self): self.assertEqual(1.2, ImImagePlugin.number("1.2")) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 83b735464..b6cc4c225 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,28 +1,30 @@ -from .helper import PillowTestCase, hopper +import sys +from io import StringIO from PIL import Image, IptcImagePlugin +from .helper import PillowTestCase, hopper + 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) @@ -31,10 +33,10 @@ class TestFileIptc(PillowTestCase): def test_getiptcinfo_tiff_none(self): # Arrange - im = Image.open("Tests/images/hopper.tif") + with Image.open("Tests/images/hopper.tif") as im: - # Act - iptc = IptcImagePlugin.getiptcinfo(im) + # Act + iptc = IptcImagePlugin.getiptcinfo(im) # Assert self.assertIsNone(iptc) @@ -53,11 +55,6 @@ class TestFileIptc(PillowTestCase): # Arrange c = b"abc" # Temporarily redirect stdout - try: - from cStringIO import StringIO - except ImportError: - from io import StringIO - import sys old_stdout = sys.stdout sys.stdout = mystdout = StringIO() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index cbe894f34..35f2c0940 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,13 +1,16 @@ -from .helper import unittest, PillowTestCase, hopper -from .helper import djpeg_available, cjpeg_available - -from io import BytesIO import os -import sys +from io import BytesIO -from PIL import Image -from PIL import ImageFile -from PIL import JpegImagePlugin +from PIL import Image, ImageFile, JpegImagePlugin + +from .helper import ( + PillowTestCase, + cjpeg_available, + djpeg_available, + hopper, + is_win32, + unittest, +) codecs = dir(Image.core) @@ -15,7 +18,6 @@ TEST_FILE = "Tests/images/hopper.jpg" class TestFileJpeg(PillowTestCase): - def setUp(self): if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: self.skipTest("jpeg support not available") @@ -29,14 +31,13 @@ class TestFileJpeg(PillowTestCase): im.bytes = test_bytes # for testing only return im - def gen_random_image(self, size, mode='RGB'): + def gen_random_image(self, size, mode="RGB"): """ Generates a very hard to compress file :param size: tuple :param mode: optional image mode """ - return Image.frombytes(mode, size, - os.urandom(size[0]*size[1]*len(mode))) + return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) def test_sanity(self): @@ -52,13 +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, @@ -72,8 +74,7 @@ class TestFileJpeg(PillowTestCase): 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))] + 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) @@ -82,8 +83,7 @@ class TestFileJpeg(PillowTestCase): 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))] + 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): @@ -91,6 +91,7 @@ class TestFileJpeg(PillowTestCase): im = Image.open(TEST_FILE) im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) return im.info.get("dpi") + self.assertEqual(test(72), (72, 72)) self.assertEqual(test(300), (300, 300)) self.assertEqual(test(100, 200), (100, 200)) @@ -98,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 @@ -119,31 +120,38 @@ class TestFileJpeg(PillowTestCase): # The ICC APP marker can store 65519 bytes per marker, so # using a 4-byte test code should allow us to detect out of # order issues. - icc_profile = (b"Test"*int(n/4+1))[:n] + icc_profile = (b"Test" * int(n / 4 + 1))[:n] self.assertEqual(len(icc_profile), n) # sanity im1 = self.roundtrip(hopper(), icc_profile=icc_profile) self.assertEqual(im1.info.get("icc_profile"), icc_profile or None) + test(0) test(1) test(3) test(4) test(5) - test(65533-14) # full JPEG marker block - test(65533-14+1) # full block plus one byte + test(65533 - 14) # full JPEG marker block + test(65533 - 14 + 1) # full block plus one byte test(ImageFile.MAXBLOCK) # full buffer block - test(ImageFile.MAXBLOCK+1) # full buffer block plus one byte - test(ImageFile.MAXBLOCK*4+3) # large block + test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte + test(ImageFile.MAXBLOCK * 4 + 3) # large block def test_large_icc_meta(self): # 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') + 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) + im.save( + f, + format="JPEG", + progressive=True, + quality=95, + icc_profile=icc_profile, + optimize=True, + ) def test_optimize(self): im1 = self.roundtrip(hopper()) @@ -156,9 +164,9 @@ class TestFileJpeg(PillowTestCase): def test_optimize_large_buffer(self): # https://github.com/python-pillow/Pillow/issues/148 - f = self.tempfile('temp.jpg') + f = self.tempfile("temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK - im = Image.new("RGB", (4096, 4096), 0xff3333) + im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", optimize=True) def test_progressive(self): @@ -173,13 +181,13 @@ class TestFileJpeg(PillowTestCase): self.assertGreaterEqual(im1.bytes, im3.bytes) def test_progressive_large_buffer(self): - f = self.tempfile('temp.jpg') + f = self.tempfile("temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK - im = Image.new("RGB", (4096, 4096), 0xff3333) + im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", progressive=True) def test_progressive_large_buffer_highest_quality(self): - f = self.tempfile('temp.jpg') + f = self.tempfile("temp.jpg") im = self.gen_random_image((255, 255)) # this requires more bytes than pixels in the image im.save(f, format="JPEG", progressive=True, quality=100) @@ -187,33 +195,34 @@ class TestFileJpeg(PillowTestCase): def test_progressive_cmyk_buffer(self): # Issue 2272, quality 90 cmyk image is tripping the large buffer bug. f = BytesIO() - im = self.gen_random_image((256, 256), 'CMYK') - im.save(f, format='JPEG', progressive=True, quality=94) + im = self.gen_random_image((256, 256), "CMYK") + im.save(f, format="JPEG", progressive=True, quality=94) def test_large_exif(self): # https://github.com/python-pillow/Pillow/issues/148 - f = self.tempfile('temp.jpg') + f = self.tempfile("temp.jpg") im = hopper() - im.save(f, 'JPEG', quality=90, exif=b"1"*65532) + 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) @@ -222,38 +231,42 @@ class TestFileJpeg(PillowTestCase): # rolling back exif support in 3.1 to pre-3.0 formatting. # expected from 2.9, with b/u qualifiers switched for 3.2 compatibility # this test passes on 2.9 and 3.1, but not 3.0 - expected_exif = {34867: 4294967295, - 258: (24, 24, 24), - 36867: '2099:09:29 10:10:10', - 34853: {0: b'\x00\x00\x00\x01', - 2: (4294967295, 1), - 5: b'\x01', - 30: 65535, - 29: '1999:99:99 99:99:99'}, - 296: 65535, - 34665: 185, - 41994: 65535, - 514: 4294967295, - 271: 'Make', - 272: 'XXX-XXX', - 305: 'PIL', - 42034: ((1, 1), (1, 1), (1, 1), (1, 1)), - 42035: 'LensMake', - 34856: b'\xaa\xaa\xaa\xaa\xaa\xaa', - 282: (4294967295, 1), - 33434: (4294967295, 1)} + expected_exif = { + 34867: 4294967295, + 258: (24, 24, 24), + 36867: "2099:09:29 10:10:10", + 34853: { + 0: b"\x00\x00\x00\x01", + 2: (4294967295, 1), + 5: b"\x01", + 30: 65535, + 29: "1999:99:99 99:99:99", + }, + 296: 65535, + 34665: 185, + 41994: 65535, + 514: 4294967295, + 271: "Make", + 272: "XXX-XXX", + 305: "PIL", + 42034: ((1, 1), (1, 1), (1, 1), (1, 1)), + 42035: "LensMake", + 34856: b"\xaa\xaa\xaa\xaa\xaa\xaa", + 282: (4294967295, 1), + 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()) @@ -291,6 +304,7 @@ class TestFileJpeg(PillowTestCase): def getsampling(im): layer = im.layer return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] + # experimental API im = self.roundtrip(hopper(), subsampling=-1) # default self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) @@ -312,40 +326,41 @@ class TestFileJpeg(PillowTestCase): im = self.roundtrip(hopper(), subsampling="4:1:1") self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) - self.assertRaises( - TypeError, self.roundtrip, hopper(), subsampling="1:1:1") + self.assertRaises(TypeError, self.roundtrip, hopper(), subsampling="1:1:1") def test_exif(self): - im = Image.open("Tests/images/pil_sample_rgb.jpg") - info = im._getexif() - self.assertEqual(info[305], 'Adobe Photoshop CS Macintosh') + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + info = im._getexif() + self.assertEqual(info[305], "Adobe Photoshop CS Macintosh") def test_mp(self): - im = Image.open("Tests/images/pil_sample_rgb.jpg") - self.assertIsNone(im._getmp()) + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + self.assertIsNone(im._getmp()) def test_quality_keep(self): # RGB im = Image.open("Tests/images/hopper.jpg") - f = self.tempfile('temp.jpg') - im.save(f, quality='keep') + 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') + 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') + f = self.tempfile("temp.jpg") + im.save(f, quality="keep") def test_junk_jpeg_header(self): # https://github.com/python-pillow/Pillow/issues/630 filename = "Tests/images/junk_jpeg_header.jpg" - Image.open(filename) + with Image.open(filename): + pass def test_ff00_jpeg_header(self): filename = "Tests/images/jpeg_ff00_header.jpg" - Image.open(filename) + with Image.open(filename): + pass def test_truncated_jpeg_should_read_all_the_data(self): filename = "Tests/images/truncated_jpeg.jpg" @@ -357,15 +372,18 @@ 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() 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) + 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") @@ -376,18 +394,18 @@ class TestFileJpeg(PillowTestCase): 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) + self.assert_image_similar(im, self.roundtrip(im, qtables="web_low"), 30) + self.assert_image_similar(im, self.roundtrip(im, qtables="web_high"), 30) + self.assert_image_similar(im, self.roundtrip(im, qtables="keep"), 30) # valid bounds for baseline qtable bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)] self.roundtrip(im, qtables=[bounds_qtable]) # values from wizard.txt in jpeg9-a src package. - standard_l_qtable = [int(s) for s in """ + 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 @@ -396,9 +414,14 @@ class TestFileJpeg(PillowTestCase): 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)] + """.split( + None + ) + ] - standard_chrominance_qtable = [int(s) for s in """ + 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 @@ -407,25 +430,36 @@ class TestFileJpeg(PillowTestCase): 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)] + """.split( + None + ) + ] # list of qtable lists self.assert_image_similar( - im, self.roundtrip( - im, qtables=[standard_l_qtable, standard_chrominance_qtable]), - 30) + 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) + im, + self.roundtrip( + im, qtables=(standard_l_qtable, standard_chrominance_qtable) + ), + 30, + ) # dict of qtable lists - self.assert_image_similar(im, - self.roundtrip(im, qtables={ - 0: standard_l_qtable, - 1: standard_chrominance_qtable - }), 30) + self.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") @@ -437,24 +471,22 @@ class TestFileJpeg(PillowTestCase): self._n_qtables_helper(4, "Tests/images/pil_sample_cmyk.jpg") # not a sequence - self.assertRaises(ValueError, self.roundtrip, im, qtables='a') + 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]) + 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]]) + self.assertRaises(ValueError, self.roundtrip, im, qtables=[[1, 2, 3, 4]]) @unittest.skipUnless(djpeg_available(), "djpeg not available") def test_load_djpeg(self): - img = Image.open(TEST_FILE) - img.load_djpeg() - self.assert_image_similar(img, Image.open(TEST_FILE), 0) + with Image.open(TEST_FILE) as img: + img.load_djpeg() + self.assert_image_similar(img, Image.open(TEST_FILE), 0) @unittest.skipUnless(cjpeg_available(), "cjpeg not available") def test_save_cjpeg(self): @@ -468,11 +500,12 @@ class TestFileJpeg(PillowTestCase): def test_no_duplicate_0x1001_tag(self): # Arrange from PIL import ExifTags + tag_ids = {v: k for k, v in ExifTags.TAGS.items()} # Assert - self.assertEqual(tag_ids['RelatedImageWidth'], 0x1001) - self.assertEqual(tag_ids['RelatedImageLength'], 0x1002) + self.assertEqual(tag_ids["RelatedImageWidth"], 0x1001) + self.assertEqual(tag_ids["RelatedImageLength"], 0x1002) def test_MAXBLOCK_scaling(self): im = self.gen_random_image((512, 512)) @@ -482,9 +515,9 @@ class TestFileJpeg(PillowTestCase): 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) + 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 """ @@ -493,21 +526,21 @@ 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() - for mode in ['1', 'L', 'RGB', 'RGBX', 'CMYK', 'YCbCr']: + for mode in ["1", "L", "RGB", "RGBX", "CMYK", "YCbCr"]: img = Image.new(mode, (20, 20)) img.save(out, "JPEG") def test_save_wrong_modes(self): # ref https://github.com/python-pillow/Pillow/issues/2005 out = BytesIO() - for mode in ['LA', 'La', 'RGBA', 'RGBa', 'P']: + for mode in ["LA", "La", "RGBA", "RGBa", "P"]: img = Image.new(mode, (20, 20)) self.assertRaises(IOError, img.save, out, "JPEG") @@ -517,81 +550,118 @@ class TestFileJpeg(PillowTestCase): im = Image.open("Tests/images/hopper.tif") # Act - im.save(outfile, 'JPEG', dpi=im.info['dpi']) + 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.info["dpi"], reloaded.info["dpi"]) + + def test_load_dpi_rounding(self): + # Round up + with Image.open("Tests/images/iptc_roundUp.jpg") as im: + self.assertEqual(im.info["dpi"], (44, 44)) + + # Round down + 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)) + 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)) 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): + 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 + with Image.open("Tests/images/app13.jpg") as im: + self.assertNotIn("photoshop", im.info) -@unittest.skipUnless(sys.platform.startswith('win32'), "Windows only") +@unittest.skipUnless(is_win32(), "Windows only") class TestFileCloseW32(PillowTestCase): def setUp(self): if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 4b34354e2..dac1d0ec0 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,11 +1,12 @@ -from .helper import PillowTestCase +from io import BytesIO from PIL import Image, Jpeg2KImagePlugin -from io import BytesIO + +from .helper import PillowTestCase codecs = dir(Image.core) -test_card = Image.open('Tests/images/test-card.png') +test_card = Image.open("Tests/images/test-card.png") test_card.load() # OpenJPEG 2.0.0 outputs this debugging message sometimes; we should @@ -14,10 +15,9 @@ test_card.load() class TestFileJpeg2k(PillowTestCase): - def setUp(self): if "jpeg2k_encoder" not in codecs or "jpeg2k_decoder" not in codecs: - self.skipTest('JPEG 2000 support not available') + self.skipTest("JPEG 2000 support not available") def roundtrip(self, im, **options): out = BytesIO() @@ -31,29 +31,28 @@ class TestFileJpeg2k(PillowTestCase): def test_sanity(self): # Internal version number - self.assertRegex(Image.core.jp2klib_version, r'\d+\.\d+\.\d+$') + self.assertRegex(Image.core.jp2klib_version, r"\d+\.\d+\.\d+$") - im = Image.open('Tests/images/test-card-lossless.jp2') + 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.mode, "RGB") self.assertEqual(im.size, (640, 480)) - self.assertEqual(im.format, 'JPEG2000') - self.assertEqual(im.get_format_mimetype(), 'image/jp2') + self.assertEqual(im.format, "JPEG2000") + self.assertEqual(im.get_format_mimetype(), "image/jp2") def test_jpf(self): - im = Image.open('Tests/images/balloon.jpf') - self.assertEqual(im.format, 'JPEG2000') - self.assertEqual(im.get_format_mimetype(), 'image/jpx') + with Image.open("Tests/images/balloon.jpf") as im: + self.assertEqual(im.format, "JPEG2000") + self.assertEqual(im.get_format_mimetype(), "image/jpx") def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - Jpeg2KImagePlugin.Jpeg2KImageFile, invalid_file) + self.assertRaises(SyntaxError, Jpeg2KImagePlugin.Jpeg2KImageFile, invalid_file) def test_bytesio(self): - with open('Tests/images/test-card-lossless.jp2', 'rb') as f: + with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) im = Image.open(data) im.load() @@ -63,14 +62,14 @@ class TestFileJpeg2k(PillowTestCase): # PIL (they were made using Adobe Photoshop) def test_lossless(self): - im = Image.open('Tests/images/test-card-lossless.jp2') + im = Image.open("Tests/images/test-card-lossless.jp2") im.load() - outfile = self.tempfile('temp_test-card.png') + 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 = Image.open("Tests/images/test-card-lossy-tiled.jp2") im.load() self.assert_image_similar(im, test_card, 2.0) @@ -88,49 +87,49 @@ class TestFileJpeg2k(PillowTestCase): def test_tiled_offset_rt(self): im = self.roundtrip( - test_card, tile_size=(128, 128), - tile_offset=(0, 0), offset=(32, 32)) + test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32) + ) self.assert_image_equal(im, test_card) + def test_tiled_offset_too_small(self): + with self.assertRaises(ValueError): + self.roundtrip( + test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32) + ) + def test_irreversible_rt(self): im = self.roundtrip(test_card, irreversible=True, quality_layers=[20]) self.assert_image_similar(im, test_card, 2.0) def test_prog_qual_rt(self): - im = self.roundtrip( - test_card, quality_layers=[60, 40, 20], progression='LRCP') + im = self.roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") self.assert_image_similar(im, test_card, 2.0) def test_prog_res_rt(self): - im = self.roundtrip(test_card, num_resolutions=8, progression='RLCP') + im = self.roundtrip(test_card, num_resolutions=8, progression="RLCP") self.assert_image_equal(im, test_card) def test_reduce(self): - im = Image.open('Tests/images/test-card-lossless.jp2') + im = Image.open("Tests/images/test-card-lossless.jp2") im.reduce = 2 im.load() self.assertEqual(im.size, (160, 120)) def test_layers_type(self): - outfile = self.tempfile('temp_layers.jp2') - for quality_layers in [ - [100, 50, 10], - (100, 50, 10), - None - ]: + outfile = self.tempfile("temp_layers.jp2") + for quality_layers in [[100, 50, 10], (100, 50, 10), None]: test_card.save(outfile, quality_layers=quality_layers) - for quality_layers in [ - 'quality_layers', - ('100', '50', '10') - ]: - self.assertRaises(ValueError, test_card.save, outfile, - quality_layers=quality_layers) + for quality_layers in ["quality_layers", ("100", "50", "10")]: + self.assertRaises( + ValueError, test_card.save, outfile, quality_layers=quality_layers + ) def test_layers(self): out = BytesIO() - test_card.save(out, 'JPEG2000', quality_layers=[100, 50, 10], - progression='LRCP') + test_card.save( + out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP" + ) out.seek(0) im = Image.open(out) @@ -146,49 +145,49 @@ class TestFileJpeg2k(PillowTestCase): def test_rgba(self): # Arrange - j2k = Image.open('Tests/images/rgb_trns_ycbc.j2k') - jp2 = Image.open('Tests/images/rgb_trns_ycbc.jp2') + j2k = Image.open("Tests/images/rgb_trns_ycbc.j2k") + jp2 = Image.open("Tests/images/rgb_trns_ycbc.jp2") # Act j2k.load() jp2.load() # Assert - self.assertEqual(j2k.mode, 'RGBA') - self.assertEqual(jp2.mode, 'RGBA') + self.assertEqual(j2k.mode, "RGBA") + self.assertEqual(jp2.mode, "RGBA") def test_16bit_monochrome_has_correct_mode(self): - j2k = Image.open('Tests/images/16bit.cropped.j2k') - jp2 = Image.open('Tests/images/16bit.cropped.jp2') + 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') + self.assertEqual(j2k.mode, "I;16") + 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') + tiff_16bit = Image.open("Tests/images/16bit.cropped.tif") + jp2 = Image.open("Tests/images/16bit.cropped.jp2") self.assert_image_similar(jp2, tiff_16bit, 1e-3) def test_16bit_monochrome_j2k_like_tiff(self): - tiff_16bit = Image.open('Tests/images/16bit.cropped.tif') - j2k = Image.open('Tests/images/16bit.cropped.j2k') + tiff_16bit = Image.open("Tests/images/16bit.cropped.tif") + j2k = Image.open("Tests/images/16bit.cropped.j2k") self.assert_image_similar(j2k, tiff_16bit, 1e-3) def test_16bit_j2k_roundtrips(self): - j2k = Image.open('Tests/images/16bit.cropped.j2k') + j2k = Image.open("Tests/images/16bit.cropped.j2k") im = self.roundtrip(j2k) self.assert_image_equal(im, j2k) def test_16bit_jp2_roundtrips(self): - jp2 = Image.open('Tests/images/16bit.cropped.jp2') + jp2 = Image.open("Tests/images/16bit.cropped.jp2") im = self.roundtrip(jp2) self.assert_image_equal(im, jp2) @@ -196,12 +195,13 @@ class TestFileJpeg2k(PillowTestCase): # prepatch, a malformed jp2 file could cause an UnboundLocalError # exception. with self.assertRaises(IOError): - Image.open('Tests/images/unbound_variable.jp2') + Image.open("Tests/images/unbound_variable.jp2") def test_parser_feed(self): # Arrange from PIL import ImageFile - with open('Tests/images/test-card-lossless.jp2', 'rb') as f: + + with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = f.read() # Act diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 56564ebde..dba053e60 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,24 +1,22 @@ -from __future__ import print_function -from .helper import PillowTestCase, hopper -from PIL import features -from PIL._util import py3 - -from ctypes import c_float -import io -import logging -import itertools -import os +import base64 import distutils.version +import io +import itertools +import logging +import os +from collections import namedtuple +from ctypes import c_float -from PIL import Image, TiffImagePlugin, TiffTags +from PIL import Image, TiffImagePlugin, TiffTags, features + +from .helper import PillowTestCase, hopper logger = logging.getLogger(__name__) class LibTiffTestCase(PillowTestCase): - def setUp(self): - if not features.check('libtiff'): + if not features.check("libtiff"): self.skipTest("tiff support not available") def _assert_noerr(self, im): @@ -31,7 +29,7 @@ class LibTiffTestCase(PillowTestCase): im.getdata() try: - self.assertEqual(im._compression, 'group4') + self.assertEqual(im._compression, "group4") except AttributeError: print("No _compression") print(dir(im)) @@ -41,11 +39,10 @@ class LibTiffTestCase(PillowTestCase): im.save(out) out_bytes = io.BytesIO() - im.save(out_bytes, format='tiff', compression='group4') + im.save(out_bytes, format="tiff", compression="group4") class TestFileLibTiff(LibTiffTestCase): - def test_g4_tiff(self): """Test the ordinary file path load path""" @@ -64,7 +61,7 @@ class TestFileLibTiff(LibTiffTestCase): """Testing the string load path""" test_file = "Tests/images/hopper_g4_500.tif" - with open(test_file, 'rb') as f: + with open(test_file, "rb") as f: im = Image.open(f) self.assertEqual(im.size, (500, 500)) @@ -74,7 +71,7 @@ class TestFileLibTiff(LibTiffTestCase): """Testing the stringio loading code path""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() - with open(test_file, 'rb') as f: + with open(test_file, "rb") as f: s.write(f.read()) s.seek(0) im = Image.open(s) @@ -82,18 +79,31 @@ class TestFileLibTiff(LibTiffTestCase): 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""" + test_file = "Tests/images/hopper_g4_500.tif" + s = io.BytesIO() + with open(test_file, "rb") as f: + s.write(f.read()) + s.seek(0) + r = io.BufferedReader(s) + im = Image.open(r) + + 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') + png = Image.open("Tests/images/hopper_bw_500.png") + g4 = Image.open("Tests/images/hopper_g4_500.tif") 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') + png = Image.open("Tests/images/g4-fillorder-test.png") + g4 = Image.open("Tests/images/g4-fillorder-test.tif") self.assert_image_equal(g4, png) @@ -111,9 +121,9 @@ class TestFileLibTiff(LibTiffTestCase): 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"], "group4") - self.assertEqual(reread.info['compression'], orig.info['compression']) + self.assertEqual(reread.info["compression"], orig.info["compression"]) self.assertNotEqual(orig.tobytes(), reread.tobytes()) @@ -123,59 +133,59 @@ class TestFileLibTiff(LibTiffTestCase): self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (278, 374)) - self.assertEqual( - im.tile[0][:3], ('tiff_adobe_deflate', (0, 0, 278, 374), 0)) + self.assertEqual(im.tile[0][:3], ("libtiff", (0, 0, 278, 374), 0)) im.load() - self.assert_image_equal_tofile(im, - 'Tests/images/tiff_adobe_deflate.png') + self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") def test_write_metadata(self): """ Test metadata writing through libtiff """ for legacy_api in [False, True]: - img = Image.open('Tests/images/hopper_g4.tif') - f = self.tempfile('temp.tiff') + 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. - ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', - 'PhotometricInterpretation'] + ignored = [ + "StripByteCounts", + "RowsPerStrip", + "PageNumber", + "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()): + for tag, value in itertools.chain(reloaded.items(), original.items()): if tag not in ignored: val = original[tag] - if tag.endswith('Resolution'): + if tag.endswith("Resolution"): if legacy_api: self.assertEqual( c_float(val[0][0] / val[0][1]).value, c_float(value[0][0] / value[0][1]).value, - msg="%s didn't roundtrip" % tag) + msg="%s didn't roundtrip" % tag, + ) else: self.assertEqual( - c_float(val).value, c_float(value).value, - msg="%s didn't roundtrip" % tag) + c_float(val).value, + c_float(value).value, + msg="%s didn't roundtrip" % tag, + ) else: - self.assertEqual( - val, value, msg="%s didn't roundtrip" % tag) + self.assertEqual(val, value, msg="%s didn't roundtrip" % tag) # https://github.com/python-pillow/Pillow/issues/1561 - requested_fields = ['StripByteCounts', - 'RowsPerStrip', - 'StripOffsets'] + requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"] for field in requested_fields: self.assertIn(field, reloaded, "%s not in metadata" % field) @@ -186,13 +196,15 @@ class TestFileLibTiff(LibTiffTestCase): # Get the list of the ones that we should be able to write - core_items = {tag: info for tag, info in ((s, TiffTags.lookup(s)) for s - in TiffTags.LIBTIFF_CORE) - if info.type is not None} + core_items = { + tag: info + for tag, info in ((s, TiffTags.lookup(s)) for s in TiffTags.LIBTIFF_CORE) + if info.type is not None + } # Exclude ones that have special meaning # that we're already testing them - im = Image.open('Tests/images/hopper_g4.tif') + im = Image.open("Tests/images/hopper_g4.tif") for tag in im.tag_v2: try: del core_items[tag] @@ -206,11 +218,13 @@ class TestFileLibTiff(LibTiffTestCase): # 5: "rational", # 12: "double", # Type: dummy value - values = {2: 'test', - 3: 1, - 4: 2**20, - 5: TiffImagePlugin.IFDRational(100, 1), - 12: 1.05} + 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(): @@ -219,8 +233,7 @@ class TestFileLibTiff(LibTiffTestCase): 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[tag] = tuple(values[info.type] for _ in range(info.length)) # Extra samples really doesn't make sense in this application. del new_ifd[338] @@ -233,100 +246,147 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False def test_custom_metadata(self): + tc = namedtuple("test_case", "value,type,supported_by_default") custom = { - 37000: 4, - 37001: 4.2, - 37002: 'custom tag value', - 37003: u'custom tag value', - 37004: b'custom tag value' + 37000 + k: v + for k, v in enumerate( + [ + tc(4, TiffTags.SHORT, True), + tc(123456789, TiffTags.LONG, True), + tc(-4, TiffTags.SIGNED_BYTE, False), + tc(-4, TiffTags.SIGNED_SHORT, False), + tc(-123456789, TiffTags.SIGNED_LONG, False), + tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True), + tc(4.25, TiffTags.FLOAT, True), + tc(4.25, TiffTags.DOUBLE, 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), + tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False), + tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False), + tc( + (-123456789, 9, 34, 234, 219387, -92432323), + TiffTags.SIGNED_LONG, + False, + ), + tc((4.25, 5.25), TiffTags.FLOAT, True), + tc((4.25, 5.25), TiffTags.DOUBLE, True), + # array of TIFF_BYTE requires bytes instead of tuple for backwards + # compatibility + tc(bytes([4]), TiffTags.BYTE, True), + tc(bytes((4, 9, 10)), TiffTags.BYTE, True), + ] + ) } libtiff_version = TiffImagePlugin._libtiff_version() libtiffs = [False] - if distutils.version.StrictVersion(libtiff_version) >= \ - distutils.version.StrictVersion("4.0"): + if distutils.version.StrictVersion( + libtiff_version + ) >= distutils.version.StrictVersion("4.0"): libtiffs.append(True) for libtiff in libtiffs: TiffImagePlugin.WRITE_LIBTIFF = libtiff - im = hopper() + def check_tags(tiffinfo): + im = hopper() - out = self.tempfile("temp.tif") - im.save(out, tiffinfo=custom) - TiffImagePlugin.WRITE_LIBTIFF = False + out = self.tempfile("temp.tif") + im.save(out, tiffinfo=tiffinfo) - reloaded = Image.open(out) - for tag, value in custom.items(): - if libtiff and isinstance(value, bytes): - value = value.decode() - self.assertEqual(reloaded.tag_v2[tag], value) + 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() + + self.assertEqual(reloaded_value, value) + + # Test with types + ifd = TiffImagePlugin.ImageFileDirectory_v2() + for tag, tagdata in custom.items(): + ifd[tag] = tagdata.value + ifd.tagtype[tag] = tagdata.type + check_tags(ifd) + + # Test without types. This only works for some types, int for example are + # always encoded as LONG and not SIGNED_LONG. + check_tags( + { + tag: tagdata.value + for tag, tagdata in custom.items() + if tagdata.supported_by_default + } + ) + TiffImagePlugin.WRITE_LIBTIFF = False def test_int_dpi(self): # issue #1765 - im = hopper('RGB') - out = self.tempfile('temp.tif') + im = hopper("RGB") + out = self.tempfile("temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True im.save(out, dpi=(72, 72)) TiffImagePlugin.WRITE_LIBTIFF = False - reloaded = Image.open(out) - self.assertEqual(reloaded.info['dpi'], (72.0, 72.0)) + 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') + i = Image.open("Tests/images/hopper_g4_500.tif") out = self.tempfile("temp.tif") - i.save(out, compression='group3') + i.save(out, compression="group3") reread = Image.open(out) - self.assertEqual(reread.info['compression'], 'group3') + 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') + im = Image.open("Tests/images/16bit.deflate.tif") self.assertEqual(im.getpixel((0, 0)), 480) - self.assertEqual(im.mode, 'I;16') + self.assertEqual(im.mode, "I;16") b = im.tobytes() # Bytes are in image native order (little endian) - if py3: - self.assertEqual(b[0], ord(b'\xe0')) - self.assertEqual(b[1], ord(b'\x01')) - else: - self.assertEqual(b[0], b'\xe0') - self.assertEqual(b[1], b'\x01') + 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.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') + im = Image.open("Tests/images/16bit.MM.deflate.tif") self.assertEqual(im.getpixel((0, 0)), 480) - self.assertEqual(im.mode, 'I;16B') + self.assertEqual(im.mode, "I;16B") b = im.tobytes() # Bytes are in image native order (big endian) - if py3: - self.assertEqual(b[0], ord(b'\x01')) - self.assertEqual(b[1], ord(b'\xe0')) - else: - self.assertEqual(b[0], b'\x01') - self.assertEqual(b[1], b'\xe0') + self.assertEqual(b[0], ord(b"\x01")) + self.assertEqual(b[1], ord(b"\xe0")) out = self.tempfile("temp.tif") im.save(out) reread = Image.open(out) - self.assertEqual(reread.info['compression'], im.info['compression']) + self.assertEqual(reread.info["compression"], im.info["compression"]) self.assertEqual(reread.getpixel((0, 0)), 480) def test_g4_string_info(self): @@ -336,18 +396,18 @@ class TestFileLibTiff(LibTiffTestCase): out = self.tempfile("temp.tif") - orig.tag[269] = 'temp.tif' + orig.tag[269] = "temp.tif" orig.save(out) - reread = Image.open(out) - self.assertEqual('temp.tif', reread.tag_v2[269]) - self.assertEqual('temp.tif', reread.tag[269][0]) + with Image.open(out) as reread: + self.assertEqual("temp.tif", reread.tag_v2[269]) + self.assertEqual("temp.tif", reread.tag[269][0]) def test_12bit_rawmode(self): """ Are we generating the same interpretation of the image as Imagemagick is? """ TiffImagePlugin.READ_LIBTIFF = True - im = Image.open('Tests/images/12bit.cropped.tif') + im = Image.open("Tests/images/12bit.cropped.tif") im.load() TiffImagePlugin.READ_LIBTIFF = False # to make the target -- @@ -356,18 +416,19 @@ class TestFileLibTiff(LibTiffTestCase): # 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 # and save to compressed tif. from PIL import ImageFilter - out = self.tempfile('temp.tif') - im = Image.open('Tests/images/pport_g4.tif') - im = im.convert('L') + + out = self.tempfile("temp.tif") + im = Image.open("Tests/images/pport_g4.tif") + im = im.convert("L") im = im.filter(ImageFilter.GaussianBlur(4)) - im.save(out, compression='tiff_adobe_deflate') + im.save(out, compression="tiff_adobe_deflate") im2 = Image.open(out) im2.load() @@ -375,23 +436,49 @@ class TestFileLibTiff(LibTiffTestCase): self.assert_image_equal(im, im2) def test_compressions(self): - im = hopper('RGB') - out = self.tempfile('temp.tif') + # Test various tiff compressions and assert similar image content but reduced + # file sizes. + im = hopper("RGB") + out = self.tempfile("temp.tif") + im.save(out) + size_raw = os.path.getsize(out) - for compression in ('packbits', 'tiff_lzw'): + 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) - im.save(out, compression='jpeg') + im.save(out, compression="jpeg") + size_jpeg = os.path.getsize(out) im2 = Image.open(out) self.assert_image_similar(im, im2, 30) - def test_cmyk_save(self): - im = hopper('CMYK') - out = self.tempfile('temp.tif') + 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) - im.save(out, compression='tiff_adobe_deflate') + self.assertGreater(size_raw, size_compressed) + self.assertGreater(size_compressed, size_jpeg) + self.assertGreater(size_jpeg, size_jpeg_30) + + def test_quality(self): + im = hopper("RGB") + out = self.tempfile("temp.tif") + + self.assertRaises(ValueError, im.save, out, compression="tiff_lzw", quality=50) + self.assertRaises(ValueError, im.save, out, compression="jpeg", quality=-1) + self.assertRaises(ValueError, im.save, out, compression="jpeg", quality=101) + self.assertRaises(ValueError, im.save, out, compression="jpeg", quality="good") + im.save(out, compression="jpeg", quality=0) + im.save(out, compression="jpeg", quality=100) + + def test_cmyk_save(self): + im = hopper("CMYK") + out = self.tempfile("temp.tif") + + im.save(out, compression="tiff_adobe_deflate") im2 = Image.open(out) self.assert_image_equal(im, im2) @@ -400,12 +487,12 @@ class TestFileLibTiff(LibTiffTestCase): to output on stderr from the error thrown by libtiff. We need to capture that but not now""" - im = hopper('RGB') - out = self.tempfile('temp.tif') + im = hopper("RGB") + out = self.tempfile("temp.tif") - self.assertRaises(IOError, im.save, out, compression='tiff_ccitt') - self.assertRaises(IOError, im.save, out, compression='group3') - self.assertRaises(IOError, im.save, out, compression='group4') + self.assertRaises(IOError, im.save, out, compression="tiff_ccitt") + self.assertRaises(IOError, im.save, out, compression="group3") + self.assertRaises(IOError, im.save, out, compression="group4") def test_fp_leak(self): im = Image.open("Tests/images/hopper_g4_500.tif") @@ -421,42 +508,42 @@ class TestFileLibTiff(LibTiffTestCase): def test_multipage(self): # issue #862 TiffImagePlugin.READ_LIBTIFF = True - im = Image.open('Tests/images/multipage.tiff') - # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue + with Image.open("Tests/images/multipage.tiff") as im: + # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue - im.seek(0) - self.assertEqual(im.size, (10, 10)) - self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 128, 0)) - self.assertTrue(im.tag.next) + im.seek(0) + self.assertEqual(im.size, (10, 10)) + self.assertEqual(im.convert("RGB").getpixel((0, 0)), (0, 128, 0)) + self.assertTrue(im.tag.next) - im.seek(1) - self.assertEqual(im.size, (10, 10)) - self.assertEqual(im.convert('RGB').getpixel((0, 0)), (255, 0, 0)) - self.assertTrue(im.tag.next) + im.seek(1) + self.assertEqual(im.size, (10, 10)) + self.assertEqual(im.convert("RGB").getpixel((0, 0)), (255, 0, 0)) + self.assertTrue(im.tag.next) - im.seek(2) - self.assertFalse(im.tag.next) - self.assertEqual(im.size, (20, 20)) - self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 0, 255)) + im.seek(2) + self.assertFalse(im.tag.next) + self.assertEqual(im.size, (20, 20)) + self.assertEqual(im.convert("RGB").getpixel((0, 0)), (0, 0, 255)) TiffImagePlugin.READ_LIBTIFF = False def test_multipage_nframes(self): # issue #862 TiffImagePlugin.READ_LIBTIFF = True - im = Image.open('Tests/images/multipage.tiff') - frames = im.n_frames - self.assertEqual(frames, 3) - for _ in range(frames): - im.seek(0) - # Should not raise ValueError: I/O operation on closed file - im.load() + with Image.open("Tests/images/multipage.tiff") as im: + frames = im.n_frames + self.assertEqual(frames, 3) + for _ in range(frames): + im.seek(0) + # Should not raise ValueError: I/O operation on closed file + im.load() TiffImagePlugin.READ_LIBTIFF = False def test__next(self): TiffImagePlugin.READ_LIBTIFF = True - im = Image.open('Tests/images/hopper.tif') + im = Image.open("Tests/images/hopper.tif") self.assertFalse(im.tag.next) im.load() self.assertFalse(im.tag.next) @@ -485,7 +572,7 @@ class TestFileLibTiff(LibTiffTestCase): "Tests/images/tiff_gray_2_4_bpp/hopper2I.tif", "Tests/images/tiff_gray_2_4_bpp/hopper2R.tif", "Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif", - ) + ), ), ( 7.3, # epsilon @@ -494,7 +581,7 @@ class TestFileLibTiff(LibTiffTestCase): "Tests/images/tiff_gray_2_4_bpp/hopper4I.tif", "Tests/images/tiff_gray_2_4_bpp/hopper4R.tif", "Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif", - ) + ), ), ) original = hopper("L") @@ -529,7 +616,7 @@ class TestFileLibTiff(LibTiffTestCase): self.assert_image_similar(pilim, pilim_load, 0) save_bytesio() - save_bytesio('raw') + save_bytesio("raw") save_bytesio("packbits") save_bytesio("tiff_lzw") @@ -538,12 +625,12 @@ class TestFileLibTiff(LibTiffTestCase): def test_crashing_metadata(self): # issue 1597 - im = Image.open('Tests/images/rdf.tif') - out = self.tempfile('temp.tif') + im = Image.open("Tests/images/rdf.tif") + out = self.tempfile("temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True # this shouldn't crash - im.save(out, format='TIFF') + im.save(out, format="TIFF") TiffImagePlugin.WRITE_LIBTIFF = False def test_page_number_x_0(self): @@ -556,16 +643,16 @@ 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 tmpfile = self.tempfile("temp.tif") - with open(tmpfile, 'wb') as f: - with open("Tests/images/g4-multi.tiff", 'rb') as src: + with open(tmpfile, "wb") as f: + with open("Tests/images/g4-multi.tiff", "rb") as src: f.write(src.read()) im = Image.open(tmpfile) @@ -576,31 +663,31 @@ class TestFileLibTiff(LibTiffTestCase): def test_read_icc(self): with Image.open("Tests/images/hopper.iccprofile.tif") as img: - icc = img.info.get('icc_profile') + icc = img.info.get("icc_profile") self.assertIsNotNone(icc) TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/hopper.iccprofile.tif") as img: - icc_libtiff = img.info.get('icc_profile') + icc_libtiff = img.info.get("icc_profile") self.assertIsNotNone(icc_libtiff) TiffImagePlugin.READ_LIBTIFF = False 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 @@ -615,6 +702,26 @@ class TestFileLibTiff(LibTiffTestCase): # Should not raise UnicodeDecodeError or anything else im.save(outfile) + def test_16bit_RGB_tiff(self): + im = Image.open("Tests/images/tiff_16bit_RGB.tiff") + + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (100, 40)) + self.assertEqual( + im.tile, + [ + ( + "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") + def test_16bit_RGBa_tiff(self): im = Image.open("Tests/images/tiff_16bit_RGBa.tiff") @@ -622,12 +729,11 @@ class TestFileLibTiff(LibTiffTestCase): self.assertEqual(im.size, (100, 40)) self.assertEqual( im.tile, - [('tiff_lzw', (0, 0, 100, 40), 0, ('RGBa;16N', 'tiff_lzw', False))] + [("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] @@ -642,7 +748,7 @@ class TestFileLibTiff(LibTiffTestCase): self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (256, 256)) self.assertEqual( - im.tile, [('jpeg', (0, 0, 256, 256), 0, ('RGB', 'jpeg', False))] + im.tile, [("libtiff", (0, 0, 256, 256), 0, ("RGB", "jpeg", False, 5122))] ) im.load() @@ -651,15 +757,14 @@ class TestFileLibTiff(LibTiffTestCase): def test_sampleformat(self): # https://github.com/python-pillow/Pillow/issues/1466 im = Image.open("Tests/images/copyleft.tiff") - self.assertEqual(im.mode, 'RGB') + 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.mode, "RGB") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "TIFF") im2 = hopper() @@ -671,6 +776,12 @@ class TestFileLibTiff(LibTiffTestCase): 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) + def test_strip_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" im = Image.open(infile) @@ -700,3 +811,48 @@ class TestFileLibTiff(LibTiffTestCase): im = Image.open(infile) 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" + ) + + def test_no_rows_per_strip(self): + # This image does not have a RowsPerStrip TIFF tag + infile = "Tests/images/no_rows_per_strip.tif" + with Image.open(infile) as im: + im.load() + self.assertEqual(im.size, (950, 975)) + + def test_orientation(self): + base_im = Image.open("Tests/images/g4_orientation_1.tif") + + for i in range(2, 9): + im = Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") + im.load() + + self.assert_image_similar(base_im, im, 0.7) + + def test_sampleformat_not_corrupted(self): + # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted + # when saving to a new file. + # Pillow 6.0 fails with "OSError: cannot identify image file". + tiff = io.BytesIO( + base64.b64decode( + b"SUkqAAgAAAAPAP4ABAABAAAAAAAAAAABBAABAAAAAQAAAAEBBAABAAAAAQAA" + b"AAIBAwADAAAAwgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAA" + b"4AAAABUBAwABAAAAAwAAABYBBAABAAAAAQAAABcBBAABAAAACwAAABoBBQAB" + b"AAAAyAAAABsBBQABAAAA0AAAABwBAwABAAAAAQAAACgBAwABAAAAAQAAAFMB" + b"AwADAAAA2AAAAAAAAAAIAAgACAABAAAAAQAAAAEAAAABAAAAAQABAAEAAAB4" + b"nGNgYAAAAAMAAQ==" + ) + ) + out = io.BytesIO() + with Image.open(tiff) as im: + im.save(out, format="tiff") + out.seek(0) + with Image.open(out) as im: + im.load() diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 6b379718e..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 @@ -17,7 +19,7 @@ class TestFileLibTiffSmall(LibTiffTestCase): """Testing the open file load path""" test_file = "Tests/images/hopper_g4.tif" - with open(test_file, 'rb') as f: + with open(test_file, "rb") as f: im = Image.open(f) self.assertEqual(im.size, (128, 128)) @@ -25,10 +27,9 @@ 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: + with open(test_file, "rb") as f: s.write(f.read()) s.seek(0) im = Image.open(s) diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index e273faad9..acc4ddb91 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -1,15 +1,13 @@ -from .helper import PillowTestCase - from PIL import Image, McIdasImagePlugin +from .helper import PillowTestCase + class TestFileMcIdas(PillowTestCase): - def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - McIdasImagePlugin.McIdasImageFile, invalid_file) + self.assertRaises(SyntaxError, McIdasImagePlugin.McIdasImageFile, invalid_file) def test_valid_file(self): # Arrange diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 3a7daa459..00f42fa4a 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -1,7 +1,9 @@ -from .helper import unittest, PillowTestCase, hopper +import unittest from PIL import Image, ImagePalette, features +from .helper import PillowTestCase, hopper + try: from PIL import MicImagePlugin except ImportError: @@ -13,54 +15,51 @@ TEST_FILE = "Tests/images/hopper.mic" @unittest.skipUnless(olefile_installed, "olefile package not installed") -@unittest.skipUnless(features.check('libtiff'), "libtiff not installed") +@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 invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - MicImagePlugin.MicImageFile, invalid_file) + self.assertRaises(SyntaxError, MicImagePlugin.MicImageFile, invalid_file) # Test a valid OLE file, but not a MIC file ole_file = "Tests/images/test-ole-file.doc" - self.assertRaises(SyntaxError, - MicImagePlugin.MicImageFile, ole_file) + self.assertRaises(SyntaxError, MicImagePlugin.MicImageFile, ole_file) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 45172472a..9c8a2b468 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,13 +1,14 @@ -from .helper import PillowTestCase +import unittest from io import BytesIO + from PIL import Image +from .helper import PillowTestCase, is_pypy test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] class TestFileMpo(PillowTestCase): - def setUp(self): codecs = dir(Image.core) if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: @@ -25,128 +26,169 @@ 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 + with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: + self.assertEqual(im.size, (640, 480)) + + im.seek(1) + self.assertEqual(im.size, (680, 480)) + + def test_parallax(self): + # Nintendo + with Image.open("Tests/images/sugarshack.mpo") as im: + exif = im.getexif() + self.assertEqual( + exif.get_ifd(0x927C)[0x1101]["Parallax"], -44.798187255859375 + ) + + # Fujifilm + with Image.open("Tests/images/fujifilm.mpo") as im: + im.seek(1) + exif = im.getexif() + self.assertEqual(exif.get_ifd(0x927C)[0xB211], -3.125) def test_mp(self): for test_file in test_files: - 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'] + mpattr = mpentry["Attribute"] if frameNumber: - self.assertFalse(mpattr['RepresentativeImageFlag']) + self.assertFalse(mpattr["RepresentativeImageFlag"]) else: - self.assertTrue(mpattr['RepresentativeImageFlag']) - self.assertFalse(mpattr['DependentParentImageFlag']) - self.assertFalse(mpattr['DependentChildImageFlag']) - self.assertEqual(mpattr['ImageDataFormat'], 'JPEG') - self.assertEqual(mpattr['MPType'], - 'Multi-Frame Image: (Disparity)') - self.assertEqual(mpattr['Reserved'], 0) + self.assertTrue(mpattr["RepresentativeImageFlag"]) + self.assertFalse(mpattr["DependentParentImageFlag"]) + self.assertFalse(mpattr["DependentChildImageFlag"]) + self.assertEqual(mpattr["ImageDataFormat"], "JPEG") + self.assertEqual(mpattr["MPType"], "Multi-Frame Image: (Disparity)") + self.assertEqual(mpattr["Reserved"], 0) frameNumber += 1 def test_seek(self): for test_file in test_files: - im = Image.open(test_file) - self.assertEqual(im.tell(), 0) - # prior to first image raises an error, both blatant and borderline - self.assertRaises(EOFError, im.seek, -1) - self.assertRaises(EOFError, im.seek, -523) - # after the final image raises an error, - # both blatant and borderline - self.assertRaises(EOFError, im.seek, 2) - self.assertRaises(EOFError, im.seek, 523) - # bad calls shouldn't change the frame - self.assertEqual(im.tell(), 0) - # this one will work - im.seek(1) - self.assertEqual(im.tell(), 1) - # and this one, too - im.seek(0) - self.assertEqual(im.tell(), 0) + with Image.open(test_file) as im: + self.assertEqual(im.tell(), 0) + # prior to first image raises an error, both blatant and borderline + self.assertRaises(EOFError, im.seek, -1) + self.assertRaises(EOFError, im.seek, -523) + # after the final image raises an error, + # both blatant and borderline + self.assertRaises(EOFError, im.seek, 2) + self.assertRaises(EOFError, im.seek, 523) + # bad calls shouldn't change the frame + self.assertEqual(im.tell(), 0) + # this one will work + im.seek(1) + self.assertEqual(im.tell(), 1) + # and this one, too + im.seek(0) + self.assertEqual(im.tell(), 0) def test_n_frames(self): - im = Image.open("Tests/images/sugarshack.mpo") - self.assertEqual(im.n_frames, 2) - self.assertTrue(im.is_animated) + with Image.open("Tests/images/sugarshack.mpo") as im: + self.assertEqual(im.n_frames, 2) + self.assertTrue(im.is_animated) def test_eoferror(self): - im = Image.open("Tests/images/sugarshack.mpo") - n_frames = im.n_frames + with Image.open("Tests/images/sugarshack.mpo") as im: + n_frames = im.n_frames - # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + # Test seeking past the last frame + self.assertRaises(EOFError, im.seek, n_frames) + self.assertLess(im.tell(), n_frames) - # Test that seeking to the last frame does not raise an error - im.seek(n_frames-1) + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) def test_image_grab(self): for test_file in test_files: - im = Image.open(test_file) - self.assertEqual(im.tell(), 0) - im0 = im.tobytes() - im.seek(1) - self.assertEqual(im.tell(), 1) - im1 = im.tobytes() - im.seek(0) - self.assertEqual(im.tell(), 0) - im02 = im.tobytes() - self.assertEqual(im0, im02) - self.assertNotEqual(im0, im1) + with Image.open(test_file) as im: + self.assertEqual(im.tell(), 0) + im0 = im.tobytes() + im.seek(1) + self.assertEqual(im.tell(), 1) + im1 = im.tobytes() + im.seek(0) + self.assertEqual(im.tell(), 0) + im02 = im.tobytes() + self.assertEqual(im0, im02) + self.assertNotEqual(im0, im1) def test_save(self): # Note that only individual frames can be saved at present for test_file in test_files: - im = Image.open(test_file) - self.assertEqual(im.tell(), 0) - jpg0 = self.frame_roundtrip(im) - self.assert_image_similar(im, jpg0, 30) - im.seek(1) - self.assertEqual(im.tell(), 1) - jpg1 = self.frame_roundtrip(im) - self.assert_image_similar(im, jpg1, 30) + with Image.open(test_file) as im: + self.assertEqual(im.tell(), 0) + jpg0 = self.frame_roundtrip(im) + self.assert_image_similar(im, jpg0, 30) + im.seek(1) + self.assertEqual(im.tell(), 1) + jpg1 = self.frame_roundtrip(im) + self.assert_image_similar(im, jpg1, 30) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 66af65fcd..d717ade16 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,8 +1,9 @@ -from .helper import unittest, PillowTestCase, hopper +import os +import unittest from PIL import Image, MspImagePlugin -import os +from .helper import PillowTestCase, hopper TEST_FILE = "Tests/images/hopper.msp" EXTRA_DIR = "Tests/images/picins" @@ -10,13 +11,12 @@ YA_EXTRA_DIR = "Tests/images/msp" class TestFileMsp(PillowTestCase): - def test_sanity(self): - file = self.tempfile("temp.msp") + test_file = self.tempfile("temp.msp") - hopper("1").save(file) + hopper("1").save(test_file) - im = Image.open(file) + im = Image.open(test_file) im.load() self.assertEqual(im.mode, "1") self.assertEqual(im.size, (128, 128)) @@ -25,8 +25,7 @@ class TestFileMsp(PillowTestCase): def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - MspImagePlugin.MspImageFile, invalid_file) + self.assertRaises(SyntaxError, MspImagePlugin.MspImageFile, invalid_file) def test_bad_checksum(self): # Arrange @@ -34,8 +33,7 @@ class TestFileMsp(PillowTestCase): bad_checksum = "Tests/images/hopper_bad_checksum.msp" # Act / Assert - self.assertRaises(SyntaxError, - MspImagePlugin.MspImageFile, bad_checksum) + self.assertRaises(SyntaxError, MspImagePlugin.MspImageFile, bad_checksum) def test_open_windows_v1(self): # Arrange @@ -51,25 +49,26 @@ class TestFileMsp(PillowTestCase): target = Image.open(target_path) self.assert_image_equal(im, target) - @unittest.skipIf(not os.path.exists(EXTRA_DIR), - "Extra image files not installed") + @unittest.skipIf(not os.path.exists(EXTRA_DIR), "Extra image files not installed") def test_open_windows_v2(self): - files = (os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) - if os.path.splitext(f)[1] == '.msp') + files = ( + os.path.join(EXTRA_DIR, f) + for f in os.listdir(EXTRA_DIR) + if os.path.splitext(f)[1] == ".msp" + ) for path in files: - self._assert_file_image_equal(path, - path.replace('.msp', '.png')) + self._assert_file_image_equal(path, path.replace(".msp", ".png")) - @unittest.skipIf(not os.path.exists(YA_EXTRA_DIR), - "Even More Extra image files not installed") + @unittest.skipIf( + not os.path.exists(YA_EXTRA_DIR), "Even More Extra image files not installed" + ) def test_msp_v2(self): for f in os.listdir(YA_EXTRA_DIR): - if '.MSP' not in f: + if ".MSP" not in f: continue path = os.path.join(YA_EXTRA_DIR, f) - self._assert_file_image_equal(path, - path.replace('.MSP', '.png')) + self._assert_file_image_equal(path, path.replace(".MSP", ".png")) def test_cannot_save_wrong_mode(self): # Arrange diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index a6491634a..fbfd89661 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,7 +1,7 @@ -from .helper import PillowTestCase, hopper, imagemagick_available - import os.path +from .helper import PillowTestCase, hopper, imagemagick_available + class TestFilePalm(PillowTestCase): _roundtrip = imagemagick_available() @@ -46,6 +46,13 @@ class TestFilePalm(PillowTestCase): self.skipKnownBadTest("Palm P image is wrong") self.roundtrip(mode) + def test_l_ioerror(self): + # Arrange + mode = "L" + + # Act / Assert + self.assertRaises(IOError, self.helper_save_as_palm, mode) + def test_rgb_ioerror(self): # Arrange mode = "RGB" diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 7296a303f..b23328ba5 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,11 +1,11 @@ -from .helper import PillowTestCase from PIL import Image +from .helper import PillowTestCase + class TestFilePcd(PillowTestCase): - def test_load_raw(self): - im = Image.open('Tests/images/hopper.pcd') + im = Image.open("Tests/images/hopper.pcd") im.load() # should not segfault. # Note that this image was created with a resized hopper diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 9e42a86f7..eb2c7d611 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,10 +1,9 @@ -from .helper import PillowTestCase, hopper - from PIL import Image, ImageFile, PcxImagePlugin +from .helper import PillowTestCase, hopper + class TestFilePcx(PillowTestCase): - def _roundtrip(self, im): f = self.tempfile("temp.pcx") im.save(f) @@ -13,10 +12,11 @@ class TestFilePcx(PillowTestCase): 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'): + for mode in ("1", "L", "P", "RGB"): self._roundtrip(hopper(mode)) # Test an unsupported mode @@ -27,14 +27,13 @@ class TestFilePcx(PillowTestCase): def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - PcxImagePlugin.PcxImageFile, invalid_file) + self.assertRaises(SyntaxError, PcxImagePlugin.PcxImageFile, invalid_file) def test_odd(self): # see issue #523, odd sized images should have a stride that's even. # not that imagemagick or gimp write pcx that way. # we were not handling properly. - for mode in ('1', 'L', 'P', 'RGB'): + for mode in ("1", "L", "P", "RGB"): # larger, odd sized images are better here to ensure that # we handle interrupted scan lines properly. self._roundtrip(hopper(mode).resize((511, 511))) @@ -49,17 +48,17 @@ class TestFilePcx(PillowTestCase): 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) + self.assertEqual(im.histogram()[0] + im.histogram()[255], 447 * 144) def test_1px_width(self): - im = Image.new('L', (1, 256)) + im = Image.new("L", (1, 256)) px = im.load() for y in range(256): px[0, y] = y self._roundtrip(im) def test_large_count(self): - im = Image.new('L', (256, 1)) + im = Image.new("L", (256, 1)) px = im.load() for x in range(256): px[x, 0] = x // 67 * 67 @@ -74,7 +73,7 @@ class TestFilePcx(PillowTestCase): ImageFile.MAXBLOCK = _last def test_break_in_count_overflow(self): - im = Image.new('L', (256, 5)) + im = Image.new("L", (256, 5)) px = im.load() for y in range(4): for x in range(256): @@ -82,7 +81,7 @@ class TestFilePcx(PillowTestCase): self._test_buffer_overflow(im) def test_break_one_in_loop(self): - im = Image.new('L', (256, 5)) + im = Image.new("L", (256, 5)) px = im.load() for y in range(5): for x in range(256): @@ -90,7 +89,7 @@ class TestFilePcx(PillowTestCase): self._test_buffer_overflow(im) def test_break_many_in_loop(self): - im = Image.new('L', (256, 5)) + im = Image.new("L", (256, 5)) px = im.load() for y in range(4): for x in range(256): @@ -100,7 +99,7 @@ class TestFilePcx(PillowTestCase): self._test_buffer_overflow(im) def test_break_one_at_end(self): - im = Image.new('L', (256, 5)) + im = Image.new("L", (256, 5)) px = im.load() for y in range(5): for x in range(256): @@ -109,7 +108,7 @@ class TestFilePcx(PillowTestCase): self._test_buffer_overflow(im) def test_break_many_at_end(self): - im = Image.new('L', (256, 5)) + im = Image.new("L", (256, 5)) px = im.load() for y in range(5): for x in range(256): @@ -120,7 +119,7 @@ class TestFilePcx(PillowTestCase): self._test_buffer_overflow(im) def test_break_padding(self): - im = Image.new('L', (257, 5)) + im = Image.new("L", (257, 5)) px = im.load() for y in range(5): for x in range(257): diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 7b024426b..4b5897589 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,14 +1,15 @@ -from .helper import PillowTestCase, hopper -from PIL import Image, PdfParser import io import os import os.path import tempfile import time +from PIL import Image, PdfParser + +from .helper import PillowTestCase, hopper + class TestFilePdf(PillowTestCase): - def helper_save_as_pdf(self, mode, **kwargs): # Arrange im = hopper(mode) @@ -21,11 +22,17 @@ class TestFilePdf(PillowTestCase): self.assertTrue(os.path.isfile(outfile)) self.assertGreater(os.path.getsize(outfile), 0) with PdfParser.PdfParser(outfile) as pdf: - if kwargs.get("append_images", False) or \ - kwargs.get("append", False): + if kwargs.get("append_images", False) or kwargs.get("append", False): self.assertGreater(len(pdf.pages), 1) else: self.assertGreater(len(pdf.pages), 0) + with open(outfile, "rb") as fp: + contents = fp.read() + size = tuple( + int(d) + for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + self.assertEqual(im.size, size) return outfile @@ -75,26 +82,26 @@ 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 - im.save(outfile, save_all=True, append_images=imGenerator(ims)) + # Test appending using a generator + def imGenerator(ims): + yield from ims + + im.save(outfile, save_all=True, append_images=imGenerator(ims)) self.assertTrue(os.path.isfile(outfile)) self.assertGreater(os.path.getsize(outfile), 0) @@ -108,10 +115,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) @@ -119,8 +126,8 @@ class TestFilePdf(PillowTestCase): def test_pdf_open(self): # fail on a buffer full of null bytes self.assertRaises( - PdfParser.PdfFormatError, - PdfParser.PdfParser, buf=bytearray(65536)) + PdfParser.PdfFormatError, PdfParser.PdfParser, buf=bytearray(65536) + ) # make an empty PDF object with PdfParser.PdfParser() as empty_pdf: @@ -155,14 +162,10 @@ class TestFilePdf(PillowTestCase): def test_pdf_append_fails_on_nonexistent_file(self): im = hopper("RGB") - temp_dir = tempfile.mkdtemp() - try: - self.assertRaises(IOError, - im.save, - os.path.join(temp_dir, "nonexistent.pdf"), - append=True) - finally: - os.rmdir(temp_dir) + with tempfile.TemporaryDirectory() as temp_dir: + self.assertRaises( + IOError, im.save, os.path.join(temp_dir, "nonexistent.pdf"), append=True + ) def check_pdf_pages_consistency(self, pdf): pages_info = pdf.read_indirect(pdf.pages_ref) @@ -189,9 +192,9 @@ class TestFilePdf(PillowTestCase): with PdfParser.PdfParser(pdf_filename, mode="r+b") as pdf: self.assertEqual(len(pdf.pages), 1) self.assertEqual(len(pdf.info), 4) - self.assertEqual(pdf.info.Title, os.path.splitext( - os.path.basename(pdf_filename) - )[0]) + self.assertEqual( + pdf.info.Title, os.path.splitext(os.path.basename(pdf_filename))[0] + ) self.assertEqual(pdf.info.Producer, "PdfParser") self.assertIn(b"CreationDate", pdf.info) self.assertIn(b"ModDate", pdf.info) @@ -200,7 +203,7 @@ class TestFilePdf(PillowTestCase): # append some info pdf.info.Title = "abc" pdf.info.Author = "def" - pdf.info.Subject = u"ghi\uABCD" + pdf.info.Subject = "ghi\uABCD" pdf.info.Keywords = "qw)e\\r(ty" pdf.info.Creator = "hopper()" pdf.start_writing() @@ -218,8 +221,7 @@ class TestFilePdf(PillowTestCase): # append two images mode_CMYK = hopper("CMYK") mode_P = hopper("P") - mode_CMYK.save(pdf_filename, - append=True, save_all=True, append_images=[mode_P]) + mode_CMYK.save(pdf_filename, append=True, save_all=True, append_images=[mode_P]) # open the PDF again, check pages and info again with PdfParser.PdfParser(pdf_filename) as pdf: @@ -229,7 +231,7 @@ class TestFilePdf(PillowTestCase): self.assertEqual(pdf.info.Title, "abc") self.assertEqual(pdf.info.Producer, "PdfParser") self.assertEqual(pdf.info.Keywords, "qw)e\\r(ty") - self.assertEqual(pdf.info.Subject, u"ghi\uABCD") + self.assertEqual(pdf.info.Subject, "ghi\uABCD") self.assertIn(b"CreationDate", pdf.info) self.assertIn(b"ModDate", pdf.info) self.check_pdf_pages_consistency(pdf) @@ -237,10 +239,16 @@ class TestFilePdf(PillowTestCase): def test_pdf_info(self): # make a PDF file pdf_filename = self.helper_save_as_pdf( - "RGB", title="title", author="author", subject="subject", - keywords="keywords", creator="creator", producer="producer", + "RGB", + title="title", + author="author", + subject="subject", + keywords="keywords", + creator="creator", + producer="producer", creationDate=time.strptime("2000", "%Y"), - modDate=time.strptime("2001", "%Y")) + modDate=time.strptime("2001", "%Y"), + ) # open it, check pages and info with PdfParser.PdfParser(pdf_filename) as pdf: @@ -251,8 +259,7 @@ class TestFilePdf(PillowTestCase): self.assertEqual(pdf.info.Keywords, "keywords") self.assertEqual(pdf.info.Creator, "creator") self.assertEqual(pdf.info.Producer, "producer") - self.assertEqual(pdf.info.CreationDate, - time.strptime("2000", "%Y")) + self.assertEqual(pdf.info.CreationDate, time.strptime("2000", "%Y")) self.assertEqual(pdf.info.ModDate, time.strptime("2001", "%Y")) self.check_pdf_pages_consistency(pdf) diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index 3b998c0b5..c744932d4 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -1,12 +1,11 @@ -from .helper import hopper, PillowTestCase - from PIL import Image, PixarImagePlugin +from .helper import PillowTestCase, hopper + TEST_FILE = "Tests/images/hopper.pxr" class TestFilePixar(PillowTestCase): - def test_sanity(self): im = Image.open(TEST_FILE) im.load() @@ -21,6 +20,4 @@ class TestFilePixar(PillowTestCase): def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises( - SyntaxError, - PixarImagePlugin.PixarImageFile, invalid_file) + self.assertRaises(SyntaxError, PixarImagePlugin.PixarImageFile, invalid_file) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 2b80bf357..c9c337e64 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,13 +1,14 @@ -from .helper import unittest, PillowTestCase, PillowLeakTestCase, hopper -from PIL import Image, ImageFile, PngImagePlugin -from PIL._util import py3 - -from io import BytesIO +import unittest import zlib -import sys +from io import BytesIO + +from PIL import Image, ImageFile, PngImagePlugin + +from .helper import PillowLeakTestCase, PillowTestCase, hopper, is_win32 try: from PIL import _webp + HAVE_WEBP = True except ImportError: HAVE_WEBP = False @@ -32,7 +33,7 @@ def chunk(cid, *data): o32 = PngImagePlugin.o32 -IHDR = chunk(b"IHDR", o32(1), o32(1), b'\x08\x02', b'\0\0\0') +IHDR = chunk(b"IHDR", o32(1), o32(1), b"\x08\x02", b"\0\0\0") IDAT = chunk(b"IDAT") IEND = chunk(b"IEND") @@ -52,7 +53,6 @@ def roundtrip(im, **options): class TestFilePng(PillowTestCase): - def setUp(self): if "zip_encoder" not in codecs or "zip_decoder" not in codecs: self.skipTest("zip/deflate support not available") @@ -81,33 +81,25 @@ 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") - hopper("1").save(test_file) - Image.open(test_file) - - hopper("L").save(test_file) - Image.open(test_file) - - hopper("P").save(test_file) - Image.open(test_file) - - hopper("RGB").save(test_file) - Image.open(test_file) - - hopper("I").save(test_file) - Image.open(test_file) + 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) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - PngImagePlugin.PngImageFile, invalid_file) + self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, invalid_file) def test_broken(self): # Check reading of totally broken files. In this case, the test @@ -119,76 +111,83 @@ class TestFilePng(PillowTestCase): def test_bad_text(self): # Make sure PIL can read malformed tEXt chunks (@PIL152) - im = load(HEAD + chunk(b'tEXt') + TAIL) + im = load(HEAD + chunk(b"tEXt") + TAIL) self.assertEqual(im.info, {}) - im = load(HEAD + chunk(b'tEXt', b'spam') + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load(HEAD + chunk(b"tEXt", b"spam") + TAIL) + self.assertEqual(im.info, {"spam": ""}) - im = load(HEAD + chunk(b'tEXt', b'spam\0') + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load(HEAD + chunk(b"tEXt", b"spam\0") + TAIL) + self.assertEqual(im.info, {"spam": ""}) - im = load(HEAD + chunk(b'tEXt', b'spam\0egg') + TAIL) - self.assertEqual(im.info, {'spam': 'egg'}) + im = load(HEAD + chunk(b"tEXt", b"spam\0egg") + TAIL) + self.assertEqual(im.info, {"spam": "egg"}) - im = load(HEAD + chunk(b'tEXt', b'spam\0egg\0') + TAIL) - self.assertEqual(im.info, {'spam': 'egg\x00'}) + im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL) + self.assertEqual(im.info, {"spam": "egg\x00"}) def test_bad_ztxt(self): # Test reading malformed zTXt chunks (python-pillow/Pillow#318) - im = load(HEAD + chunk(b'zTXt') + TAIL) + im = load(HEAD + chunk(b"zTXt") + TAIL) self.assertEqual(im.info, {}) - im = load(HEAD + chunk(b'zTXt', b'spam') + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load(HEAD + chunk(b"zTXt", b"spam") + TAIL) + self.assertEqual(im.info, {"spam": ""}) - im = load(HEAD + chunk(b'zTXt', b'spam\0') + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load(HEAD + chunk(b"zTXt", b"spam\0") + TAIL) + self.assertEqual(im.info, {"spam": ""}) - im = load(HEAD + chunk(b'zTXt', b'spam\0\0') + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load(HEAD + chunk(b"zTXt", b"spam\0\0") + TAIL) + self.assertEqual(im.info, {"spam": ""}) - im = load(HEAD + chunk( - b'zTXt', b'spam\0\0' + zlib.compress(b'egg')[:1]) + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")[:1]) + TAIL) + self.assertEqual(im.info, {"spam": ""}) - im = load( - HEAD + chunk(b'zTXt', b'spam\0\0' + zlib.compress(b'egg')) + TAIL) - self.assertEqual(im.info, {'spam': 'egg'}) + im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL) + self.assertEqual(im.info, {"spam": "egg"}) def test_bad_itxt(self): - im = load(HEAD + chunk(b'iTXt') + TAIL) + im = load(HEAD + chunk(b"iTXt") + TAIL) self.assertEqual(im.info, {}) - im = load(HEAD + chunk(b'iTXt', b'spam') + TAIL) + im = load(HEAD + chunk(b"iTXt", b"spam") + TAIL) self.assertEqual(im.info, {}) - im = load(HEAD + chunk(b'iTXt', b'spam\0') + TAIL) + im = load(HEAD + chunk(b"iTXt", b"spam\0") + TAIL) self.assertEqual(im.info, {}) - im = load(HEAD + chunk(b'iTXt', b'spam\0\x02') + TAIL) + im = load(HEAD + chunk(b"iTXt", b"spam\0\x02") + TAIL) self.assertEqual(im.info, {}) - im = load(HEAD + chunk(b'iTXt', b'spam\0\0\0foo\0') + TAIL) + im = load(HEAD + chunk(b"iTXt", b"spam\0\0\0foo\0") + TAIL) self.assertEqual(im.info, {}) - im = load(HEAD + chunk(b'iTXt', b'spam\0\0\0en\0Spam\0egg') + TAIL) + im = load(HEAD + chunk(b"iTXt", b"spam\0\0\0en\0Spam\0egg") + TAIL) self.assertEqual(im.info, {"spam": "egg"}) self.assertEqual(im.info["spam"].lang, "en") self.assertEqual(im.info["spam"].tkey, "Spam") - im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + - zlib.compress(b"egg")[:1]) + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load( + HEAD + + chunk(b"iTXt", b"spam\0\1\0en\0Spam\0" + zlib.compress(b"egg")[:1]) + + TAIL + ) + self.assertEqual(im.info, {"spam": ""}) - im = load(HEAD + chunk(b'iTXt', b'spam\0\1\1en\0Spam\0' + - zlib.compress(b"egg")) + TAIL) + im = load( + HEAD + + chunk(b"iTXt", b"spam\0\1\1en\0Spam\0" + zlib.compress(b"egg")) + + TAIL + ) self.assertEqual(im.info, {}) - im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + - zlib.compress(b"egg")) + TAIL) + im = load( + HEAD + + chunk(b"iTXt", b"spam\0\1\0en\0Spam\0" + zlib.compress(b"egg")) + + TAIL + ) self.assertEqual(im.info, {"spam": "egg"}) self.assertEqual(im.info["spam"].lang, "en") self.assertEqual(im.info["spam"].tkey, "Spam") @@ -220,7 +219,7 @@ class TestFilePng(PillowTestCase): self.assert_image(im, "RGBA", (162, 150)) # image has 124 unique alpha values - self.assertEqual(len(im.getchannel('A').getcolors()), 124) + self.assertEqual(len(im.getchannel("A").getcolors()), 124) def test_load_transparent_rgb(self): test_file = "Tests/images/rgb_trns.png" @@ -232,7 +231,7 @@ class TestFilePng(PillowTestCase): self.assert_image(im, "RGBA", (64, 64)) # image has 876 transparent pixels - self.assertEqual(im.getchannel('A').getcolors()[0][0], 876) + self.assertEqual(im.getchannel("A").getcolors()[0][0], 876) def test_save_p_transparent_palette(self): in_file = "Tests/images/pil123p.png" @@ -254,7 +253,7 @@ class TestFilePng(PillowTestCase): self.assert_image(im, "RGBA", (162, 150)) # image has 124 unique alpha values - self.assertEqual(len(im.getchannel('A').getcolors()), 124) + self.assertEqual(len(im.getchannel("A").getcolors()), 124) def test_save_p_single_transparency(self): in_file = "Tests/images/p_trns_single.png" @@ -278,7 +277,7 @@ class TestFilePng(PillowTestCase): self.assertEqual(im.getpixel((31, 31)), (0, 255, 52, 0)) # image has 876 transparent pixels - self.assertEqual(im.getchannel('A').getcolors()[0][0], 876) + self.assertEqual(im.getchannel("A").getcolors()[0][0], 876) def test_save_p_transparent_black(self): # check if solid black image with full transparency @@ -298,30 +297,28 @@ class TestFilePng(PillowTestCase): self.assert_image(im, "RGBA", (10, 10)) self.assertEqual(im.getcolors(), [(100, (0, 0, 0, 0))]) - def test_save_l_transparency(self): - # There are 559 transparent pixels in l_trns.png. - num_transparent = 559 + 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) - in_file = "Tests/images/l_trns.png" - im = Image.open(in_file) - self.assertEqual(im.mode, "L") - self.assertEqual(im.info["transparency"], 255) + im_rgba = im.convert("RGBA") + self.assertEqual(im_rgba.getchannel("A").getcolors()[0][0], num_transparent) - im_rgba = im.convert('RGBA') - self.assertEqual( - im_rgba.getchannel("A").getcolors()[0][0], num_transparent) + test_file = self.tempfile("temp.png") + im.save(test_file) - 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) - test_im = Image.open(test_file) - self.assertEqual(test_im.mode, "L") - self.assertEqual(test_im.info["transparency"], 255) - self.assert_image_equal(im, test_im) - - test_im_rgba = test_im.convert('RGBA') - self.assertEqual( - test_im_rgba.getchannel('A').getcolors()[0][0], num_transparent) + test_im_rgba = test_im.convert("RGBA") + self.assertEqual( + test_im_rgba.getchannel("A").getcolors()[0][0], num_transparent + ) def test_save_rgb_single_transparency(self): in_file = "Tests/images/caption_6_33_22.png" @@ -350,7 +347,7 @@ class TestFilePng(PillowTestCase): # -14: malformed chunk for offset in (-10, -13, -14): - with open(TEST_PNG_FILE, 'rb') as f: + with open(TEST_PNG_FILE, "rb") as f: test_file = f.read()[:offset] im = Image.open(BytesIO(test_file)) @@ -360,12 +357,11 @@ class TestFilePng(PillowTestCase): def test_verify_ignores_crc_error(self): # check ignores crc errors in ancillary chunks - chunk_data = chunk(b'tEXt', b'spam') - broken_crc_chunk_data = chunk_data[:-1] + b'q' # break CRC + chunk_data = chunk(b"tEXt", b"spam") + broken_crc_chunk_data = chunk_data[:-1] + b"q" # break CRC image_data = HEAD + broken_crc_chunk_data + TAIL - self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, - BytesIO(image_data)) + self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, BytesIO(image_data)) ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -377,12 +373,13 @@ class TestFilePng(PillowTestCase): def test_verify_not_ignores_crc_error_in_required_chunk(self): # check does not ignore crc errors in required chunks - image_data = MAGIC + IHDR[:-1] + b'q' + TAIL + image_data = MAGIC + IHDR[:-1] + b"q" + TAIL ImageFile.LOAD_TRUNCATED_IMAGES = True try: - self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, - BytesIO(image_data)) + self.assertRaises( + SyntaxError, PngImagePlugin.PngImageFile, BytesIO(image_data) + ) finally: ImageFile.LOAD_TRUNCATED_IMAGES = False @@ -394,6 +391,24 @@ class TestFilePng(PillowTestCase): im = roundtrip(im, dpi=(100, 100)) self.assertEqual(im.info["dpi"], (100, 100)) + def test_load_dpi_rounding(self): + # Round up + with Image.open(TEST_PNG_FILE) as im: + self.assertEqual(im.info["dpi"], (96, 96)) + + # Round down + with Image.open("Tests/images/icc_profile_none.png") as im: + self.assertEqual(im.info["dpi"], (72, 72)) + + def test_save_dpi_rounding(self): + im = Image.open(TEST_PNG_FILE) + + im = roundtrip(im, dpi=(72.2, 72.2)) + self.assertEqual(im.info["dpi"], (72, 72)) + + im = roundtrip(im, dpi=(72.8, 72.8)) + self.assertEqual(im.info["dpi"], (73, 73)) + def test_roundtrip_text(self): # Check text roundtripping @@ -404,8 +419,8 @@ class TestFilePng(PillowTestCase): info.add_text("ZIP", "VALUE", zip=True) im = roundtrip(im, pnginfo=info) - self.assertEqual(im.info, {'TXT': 'VALUE', 'ZIP': 'VALUE'}) - self.assertEqual(im.text, {'TXT': 'VALUE', 'ZIP': 'VALUE'}) + self.assertEqual(im.info, {"TXT": "VALUE", "ZIP": "VALUE"}) + self.assertEqual(im.text, {"TXT": "VALUE", "ZIP": "VALUE"}) def test_roundtrip_itxt(self): # Check iTXt roundtripping @@ -413,8 +428,7 @@ class TestFilePng(PillowTestCase): im = Image.new("RGB", (32, 32)) info = PngImagePlugin.PngInfo() info.add_itxt("spam", "Eggs", "en", "Spam") - info.add_text("eggs", PngImagePlugin.iTXt("Spam", "en", "Eggs"), - zip=True) + info.add_text("eggs", PngImagePlugin.iTXt("Spam", "en", "Eggs"), zip=True) im = roundtrip(im, pnginfo=info) self.assertEqual(im.info, {"spam": "Eggs", "eggs": "Spam"}) @@ -434,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)) @@ -445,12 +457,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 - rt_text(chr(0x4e00) + chr(0x66f0) + # CJK - 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: @@ -458,8 +469,8 @@ class TestFilePng(PillowTestCase): # The first byte is removed from pngtest_bad.png # to avoid classification as malware. - with open("Tests/images/pngtest_bad.png.bin", 'rb') as fd: - data = b'\x89' + fd.read() + with open("Tests/images/pngtest_bad.png.bin", "rb") as fd: + data = b"\x89" + fd.read() pngfile = BytesIO(data) self.assertRaises(IOError, Image.open, pngfile) @@ -481,60 +492,59 @@ class TestFilePng(PillowTestCase): def test_trns_p(self): # Check writing a transparency of 0, issue #528 - im = hopper('P') - im.info['transparency'] = 0 + im = hopper("P") + im.info["transparency"] = 0 f = self.tempfile("temp.png") im.save(f) im2 = Image.open(f) - self.assertIn('transparency', im2.info) + self.assertIn("transparency", im2.info) - self.assert_image_equal(im2.convert('RGBA'), - im.convert('RGBA')) + self.assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) def test_trns_null(self): # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" - im = Image.open(test_file) + with Image.open(test_file) as im: - self.assertEqual(im.info["transparency"], 0) + self.assertEqual(im.info["transparency"], 0) def test_save_icc_profile(self): - im = Image.open("Tests/images/icc_profile_none.png") - self.assertIsNone(im.info['icc_profile']) + with Image.open("Tests/images/icc_profile_none.png") as im: + self.assertIsNone(im.info["icc_profile"]) - with_icc = Image.open("Tests/images/icc_profile.png") - expected_icc = with_icc.info['icc_profile'] + with Image.open("Tests/images/icc_profile.png") as with_icc: + expected_icc = with_icc.info["icc_profile"] - im = roundtrip(im, icc_profile=expected_icc) - self.assertEqual(im.info['icc_profile'], expected_icc) + im = roundtrip(im, icc_profile=expected_icc) + self.assertEqual(im.info["icc_profile"], expected_icc) def test_discard_icc_profile(self): - im = Image.open('Tests/images/icc_profile.png') + im = Image.open("Tests/images/icc_profile.png") im = roundtrip(im, icc_profile=None) - self.assertNotIn('icc_profile', im.info) + 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'] + im = Image.open("Tests/images/icc_profile.png") + expected_icc = im.info["icc_profile"] im = roundtrip(im) - self.assertEqual(im.info['icc_profile'], expected_icc) + 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']) + self.assertIsNone(im.info["icc_profile"]) im = roundtrip(im) - self.assertNotIn('icc_profile', im.info) + 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.assertEqual(repr_png.format, "PNG") self.assert_image_equal(im, repr_png) def test_chunk_order(self): @@ -566,10 +576,10 @@ class TestFilePng(PillowTestCase): def test_textual_chunks_after_idat(self): im = Image.open("Tests/images/hopper.png") - self.assertIn('comment', im.text.keys()) + 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', + "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) @@ -588,22 +598,57 @@ class TestFilePng(PillowTestCase): # 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'}) + self.assertEqual(im.text, {"TXT": "VALUE", "ZIP": "VALUE"}) - @unittest.skipUnless(HAVE_WEBP and _webp.HAVE_WEBPANIM, - "WebP support not installed with animation") + def test_exif(self): + im = Image.open("Tests/images/exif.png") + exif = im._getexif() + self.assertEqual(exif[274], 1) + + def test_exif_save(self): + im = Image.open("Tests/images/exif.png") + + test_file = self.tempfile("temp.png") + im.save(test_file) + + 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(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_file) as reloaded: + self.assertEqual(reloaded.info["exif"], b"Exif\x00\x00exifstring") + + @unittest.skipUnless( + HAVE_WEBP and _webp.HAVE_WEBPANIM, "WebP support not installed with animation" + ) def test_apng(self): im = Image.open("Tests/images/iss634.apng") - self.assertEqual(im.get_format_mimetype(), 'image/apng') + 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) -@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 + mem_limit = 2 * 1024 # max increase in K iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs def setUp(self): @@ -611,7 +656,7 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): self.skipTest("zip/deflate support not available") def test_leak_load(self): - with open('Tests/images/hopper.png', 'rb') as f: + with open("Tests/images/hopper.png", "rb") as f: DATA = BytesIO(f.read(16 * 1024)) ImageFile.LOAD_TRUNCATED_IMAGES = True diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 47f8b845e..226687fa3 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,43 +1,54 @@ -from .helper import PillowTestCase - from PIL import Image +from .helper import PillowTestCase, hopper + # sample ppm stream 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") def test_16bit_pgm(self): - im = Image.open('Tests/images/16_bit_binary.pgm') + im = Image.open("Tests/images/16_bit_binary.pgm") im.load() - self.assertEqual(im.mode, 'I') + 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') + tgt = Image.open("Tests/images/16_bit_binary_pgm.png") self.assert_image_equal(im, tgt) def test_16bit_pgm_write(self): - im = Image.open('Tests/images/16_bit_binary.pgm') + im = Image.open("Tests/images/16_bit_binary.pgm") 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) + + def test_pnm(self): + im = Image.open("Tests/images/hopper.pnm") + self.assert_image_similar(im, hopper(), 0.0001) + + f = self.tempfile("temp.pnm") + im.save(f) reloaded = Image.open(f) self.assert_image_equal(im, reloaded) def test_truncated_file(self): - path = self.tempfile('temp.pgm') - with open(path, 'w') as f: - f.write('P6') + path = self.tempfile("temp.pgm") + with open(path, "w") as f: + f.write("P6") self.assertRaises(ValueError, Image.open, path) @@ -48,4 +59,17 @@ class TestFilePpm(PillowTestCase): # sizes. with self.assertRaises(IOError): - Image.open('Tests/images/negative_size.ppm') + Image.open("Tests/images/negative_size.ppm") + + def test_mimetypes(self): + path = self.tempfile("temp.pgm") + + with open(path, "w") as f: + f.write("P4\n128 128\n255") + with Image.open(path) as im: + self.assertEqual(im.get_format_mimetype(), "image/x-portable-bitmap") + + with open(path, "w") as f: + f.write("PyCMYK\n128 128\n255") + with Image.open(path) as im: + self.assertEqual(im.get_format_mimetype(), "image/x-portable-anymap") diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 3b8a7add6..939485f67 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,78 +1,115 @@ -from .helper import hopper, PillowTestCase +import unittest from PIL import Image, PsdImagePlugin +from .helper import PillowTestCase, hopper, is_pypy + test_file = "Tests/images/hopper.psd" class TestImagePsd(PillowTestCase): - def test_sanity(self): - im = Image.open(test_file) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PSD") + with Image.open(test_file) as im: + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "PSD") - im2 = hopper() - self.assert_image_similar(im, im2, 4.8) + im2 = hopper() + self.assert_image_similar(im, im2, 4.8) + + @unittest.skipIf(is_pypy(), "Requires CPython") + def test_unclosed_file(self): + def open(): + im = Image.open(test_file) + im.load() + + self.assert_warning(ResourceWarning, open) + + def test_closed_file(self): + def open(): + im = Image.open(test_file) + im.load() + im.close() + + self.assert_warning(None, open) + + def test_context_manager(self): + def open(): + with Image.open(test_file) as im: + im.load() + + self.assert_warning(None, open) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - PsdImagePlugin.PsdImageFile, invalid_file) + 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): + with Image.open(test_file) as im: + im.load() + im.seek(im.tell() + 1) + im.load() def test_icc_profile(self): - im = Image.open(test_file) - self.assertIn("icc_profile", im.info) + with Image.open(test_file) as im: + self.assertIn("icc_profile", im.info) - icc_profile = im.info["icc_profile"] - self.assertEqual(len(icc_profile), 3144) + icc_profile = im.info["icc_profile"] + self.assertEqual(len(icc_profile), 3144) def test_no_icc_profile(self): - im = Image.open("Tests/images/hopper_merged.psd") + with Image.open("Tests/images/hopper_merged.psd") as im: + self.assertNotIn("icc_profile", im.info) - self.assertNotIn("icc_profile", im.info) + def test_combined_larger_than_size(self): + # The 'combined' sizes of the individual parts is larger than the + # declared 'size' of the extra data field, resulting in a backwards seek. + + # If we instead take the 'size' of the extra data field as the source of truth, + # then the seek can't be negative + with self.assertRaises(IOError): + Image.open("Tests/images/combined_larger_than_size.psd") diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 1ad70e24f..ff3aea1d5 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,10 +1,9 @@ -from .helper import PillowTestCase, hopper - from PIL import Image, SgiImagePlugin +from .helper import PillowTestCase, hopper + class TestFileSgi(PillowTestCase): - def test_rgb(self): # Created with ImageMagick then renamed: # convert hopper.ppm -compress None sgi:hopper.rgb @@ -12,7 +11,7 @@ class TestFileSgi(PillowTestCase): im = Image.open(test_file) self.assert_image_equal(im, hopper()) - self.assertEqual(im.get_format_mimetype(), 'image/rgb') + self.assertEqual(im.get_format_mimetype(), "image/rgb") def test_rgb16(self): test_file = "Tests/images/hopper16.rgb" @@ -26,8 +25,8 @@ class TestFileSgi(PillowTestCase): 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') + self.assert_image_similar(im, hopper("L"), 2) + self.assertEqual(im.get_format_mimetype(), "image/sgi") def test_rgba(self): # Created with ImageMagick: @@ -35,9 +34,9 @@ class TestFileSgi(PillowTestCase): test_file = "Tests/images/transparent.sgi" im = Image.open(test_file) - target = Image.open('Tests/images/transparent.png') + target = Image.open("Tests/images/transparent.png") self.assert_image_equal(im, target) - self.assertEqual(im.get_format_mimetype(), 'image/sgi') + self.assertEqual(im.get_format_mimetype(), "image/sgi") def test_rle(self): # Created with ImageMagick: @@ -45,47 +44,46 @@ class TestFileSgi(PillowTestCase): test_file = "Tests/images/hopper.sgi" im = Image.open(test_file) - target = Image.open('Tests/images/hopper.rgb') + target = Image.open("Tests/images/hopper.rgb") 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') + target = Image.open("Tests/images/tv.rgb") self.assert_image_equal(im, target) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(ValueError, - SgiImagePlugin.SgiImageFile, invalid_file) + self.assertRaises(ValueError, SgiImagePlugin.SgiImageFile, invalid_file) def test_write(self): def roundtrip(img): - out = self.tempfile('temp.sgi') - img.save(out, format='sgi') + out = self.tempfile("temp.sgi") + img.save(out, format="sgi") reloaded = Image.open(out) self.assert_image_equal(img, reloaded) - for mode in ('L', 'RGB', 'RGBA'): + for mode in ("L", "RGB", "RGBA"): roundtrip(hopper(mode)) # Test 1 dimension for an L mode image - roundtrip(Image.new('L', (10, 1))) + roundtrip(Image.new("L", (10, 1))) 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) + out = self.tempfile("temp.sgi") + im.save(out, format="sgi", bpc=2) reloaded = Image.open(out) self.assert_image_equal(im, reloaded) def test_unsupported_mode(self): - im = hopper('LA') - out = self.tempfile('temp.sgi') + im = hopper("LA") + out = self.tempfile("temp.sgi") - self.assertRaises(ValueError, im.save, out, format='sgi') + self.assertRaises(ValueError, im.save, out, format="sgi") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index f160272fd..5940c2ff2 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,42 +1,58 @@ -from .helper import PillowTestCase, hopper - -from PIL import Image -from PIL import ImageSequence -from PIL import SpiderImagePlugin - import tempfile +import unittest +from io import BytesIO + +from PIL import Image, ImageSequence, SpiderImagePlugin + +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): # Arrange - temp = self.tempfile('temp.spider') + temp = self.tempfile("temp.spider") im = hopper() # Act 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 @@ -58,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 @@ -110,12 +126,22 @@ class TestImageSpider(PillowTestCase): self.assertRaises(IOError, Image.open, invalid_file) def test_nonstack_file(self): - im = Image.open(TEST_FILE) - - self.assertRaises(EOFError, im.seek, 0) + with Image.open(TEST_FILE) as im: + self.assertRaises(EOFError, im.seek, 0) def test_nonstack_dos(self): - im = Image.open(TEST_FILE) - for i, frame in enumerate(ImageSequence.Iterator(im)): - if i > 1: - self.fail("Non-stack DOS file test failed") + with Image.open(TEST_FILE) as im: + for i, frame in enumerate(ImageSequence.Iterator(im)): + if i > 1: + self.fail("Non-stack DOS file test failed") + + # for issue #4093 + def test_odd_size(self): + data = BytesIO() + width = 100 + im = Image.new("F", (width, 64)) + im.save(data, format="SPIDER") + + data.seek(0) + im2 = Image.open(data) + self.assert_image_equal(im, im2) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 65c00eea2..5fc171054 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,14 +1,14 @@ -from .helper import unittest, PillowTestCase, hopper +import os +import unittest from PIL import Image, SunImagePlugin -import os +from .helper import PillowTestCase, hopper -EXTRA_DIR = 'Tests/images/sunraster' +EXTRA_DIR = "Tests/images/sunraster" class TestFileSun(PillowTestCase): - def test_sanity(self): # Arrange # Created with ImageMagick: convert hopper.jpg hopper.ras @@ -23,20 +23,20 @@ class TestFileSun(PillowTestCase): self.assert_image_similar(im, hopper(), 5) # visually verified invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - SunImagePlugin.SunImageFile, invalid_file) + 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') + im = Image.open("Tests/images/sunraster.im1") + target = Image.open("Tests/images/sunraster.im1.png") self.assert_image_equal(im, target) - @unittest.skipIf(not os.path.exists(EXTRA_DIR), - "Extra image files not installed") + @unittest.skipIf(not os.path.exists(EXTRA_DIR), "Extra image files not installed") def test_others(self): - files = (os.path.join(EXTRA_DIR, f) for f in - os.listdir(EXTRA_DIR) if os.path.splitext(f)[1] - in ('.sun', '.SUN', '.ras')) + files = ( + os.path.join(EXTRA_DIR, f) + for f in os.listdir(EXTRA_DIR) + if os.path.splitext(f)[1] in (".sun", ".SUN", ".ras") + ) for path in files: with Image.open(path) as im: im.load() diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index cd2b95778..f381eef7e 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,7 +1,9 @@ -from .helper import PillowTestCase +import unittest from PIL import Image, TarIO +from .helper import PillowTestCase, is_pypy + codecs = dir(Image.core) # Sample tar archive @@ -9,28 +11,40 @@ TEST_TAR_FILE = "Tests/images/hopper.tar" class TestFileTar(PillowTestCase): - def setUp(self): if "zip_decoder" not in codecs and "jpeg_decoder" not in codecs: self.skipTest("neither jpeg nor zip support available") def test_sanity(self): for codec, test_path, format in [ - ['zip_decoder', 'hopper.png', 'PNG'], - ['jpeg_decoder', 'hopper.jpg', 'JPEG'] + ["zip_decoder", "hopper.png", "PNG"], + ["jpeg_decoder", "hopper.jpg", "JPEG"], ]: if codec in codecs: - tar = TarIO.TarIO(TEST_TAR_FILE, test_path) - im = Image.open(tar) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, format) + 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 0dbfb309c..bd5c32c5b 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -2,10 +2,9 @@ import os from glob import glob from itertools import product -from .helper import PillowTestCase - from PIL import Image +from .helper import PillowTestCase _TGA_DIR = os.path.join("Tests", "images", "tga") _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") @@ -16,16 +15,13 @@ class TestFileTga(PillowTestCase): _MODES = ("L", "LA", "P", "RGB", "RGBA") _ORIGINS = ("tl", "bl") - _ORIGIN_TO_ORIENTATION = { - "tl": 1, - "bl": -1 - } + _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} def test_sanity(self): for mode in self._MODES: png_paths = glob( - os.path.join( - _TGA_DIR_COMMON, "*x*_{}.png".format(mode.lower()))) + os.path.join(_TGA_DIR_COMMON, "*x*_{}.png".format(mode.lower())) + ) for png_path in png_paths: reference_im = Image.open(png_path) @@ -34,19 +30,22 @@ class TestFileTga(PillowTestCase): 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") + 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["compression"], "tga_rle") self.assertEqual( original_im.info["orientation"], - self._ORIGIN_TO_ORIENTATION[origin]) + self._ORIGIN_TO_ORIENTATION[origin], + ) if mode == "P": self.assertEqual( - original_im.getpalette(), - reference_im.getpalette()) + original_im.getpalette(), reference_im.getpalette() + ) self.assert_image_equal(original_im, reference_im) @@ -60,14 +59,15 @@ class TestFileTga(PillowTestCase): if rle: self.assertEqual( saved_im.info["compression"], - original_im.info["compression"]) + original_im.info["compression"], + ) self.assertEqual( - saved_im.info["orientation"], - original_im.info["orientation"]) + saved_im.info["orientation"], original_im.info["orientation"] + ) if mode == "P": self.assertEqual( - saved_im.getpalette(), - original_im.getpalette()) + saved_im.getpalette(), original_im.getpalette() + ) self.assert_image_equal(saved_im, original_im) @@ -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,28 +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]) + self.assert_warning(UserWarning, lambda: im.save(out, id_section=id_section)) + with Image.open(out) as test_im: + self.assertEqual(test_im.info["id_section"], id_section[:255]) 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" @@ -147,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" @@ -159,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) @@ -179,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. @@ -189,15 +188,13 @@ class TestFileTga(PillowTestCase): 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) + self.assertEqual(im.getchannel("A").getcolors()[0][0], num_transparent) 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) + self.assertEqual(test_im.getchannel("A").getcolors()[0][0], num_transparent) self.assert_image_equal(im, test_im) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 9a4104b78..a4346a7b5 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,49 +1,71 @@ import logging +import os +import unittest from io import BytesIO -import sys - -from .helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image, TiffImagePlugin -from PIL._util import py3 -from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION, RESOLUTION_UNIT +from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION + +from .helper import PillowTestCase, hopper, is_pypy, is_win32 logger = logging.getLogger(__name__) class TestFileTiff(PillowTestCase): - def test_sanity(self): filename = self.tempfile("temp.tif") 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): @@ -54,7 +76,7 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.mode, "RGBA") self.assertEqual(im.size, (55, 43)) - self.assertEqual(im.tile, [('raw', (0, 0, 55, 43), 8, ('RGBa', 0, 1))]) + 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) @@ -64,72 +86,83 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.mode, "RGBA") self.assertEqual(im.size, (52, 53)) - self.assertEqual(im.tile, - [('raw', (0, 0, 52, 53), 160, ('RGBA', 0, 1))]) + 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() with self.assertRaises(Exception) as e: ifd.legacy_api = None - self.assertEqual(str(e.exception), - "Not allowing setting of legacy api") - - def test_size(self): - filename = "Tests/images/pil168.tif" - im = Image.open(filename) - - def set_size(): - im.size = (256, 256) - self.assert_warning(DeprecationWarning, set_size) + self.assertEqual(str(e.exception), "Not allowing setting of legacy api") def test_xyres_tiff(self): filename = "Tests/images/pil168.tif" - im = Image.open(filename) + with Image.open(filename) as im: - # legacy api - self.assertIsInstance(im.tag[X_RESOLUTION][0], tuple) - self.assertIsInstance(im.tag[Y_RESOLUTION][0], tuple) + # legacy api + 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., 72.)) + 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., 100.)) - # Fallback "inch". - self.assertEqual(im.info['dpi'], (100., 100.)) + # 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., 71.)) + # 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))): + with Image.open( + "Tests/images/hopper_roundDown_" + str(resolutionUnit) + ".tif" + ) as im: + self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit) + self.assertEqual(im.info["dpi"], (dpi[0], dpi[0])) + + with Image.open( + "Tests/images/hopper_roundUp_" + str(resolutionUnit) + ".tif" + ) as im: + self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit) + self.assertEqual(im.info["dpi"], (dpi[1], dpi[1])) + + def test_save_dpi_rounding(self): + outfile = self.tempfile("temp.tif") + im = Image.open("Tests/images/hopper.tif") + + 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"]) def test_save_setting_missing_resolution(self): b = BytesIO() Image.open("Tests/images/10ct_32bit_128.tiff").save( - b, format="tiff", resolution=123.45) + 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) @@ -137,18 +170,16 @@ class TestFileTiff(PillowTestCase): def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - TiffImagePlugin.TiffImageFile, invalid_file) + self.assertRaises(SyntaxError, TiffImagePlugin.TiffImageFile, invalid_file) TiffImagePlugin.PREFIXES.append(b"\xff\xd8\xff\xe0") - self.assertRaises(SyntaxError, - TiffImagePlugin.TiffImageFile, invalid_file) + self.assertRaises(SyntaxError, TiffImagePlugin.TiffImageFile, invalid_file) TiffImagePlugin.PREFIXES.pop() def test_bad_exif(self): - i = Image.open('Tests/images/hopper_bad_exif.jpg') - # Should not raise struct.error. - self.assert_warning(UserWarning, i._getexif) + with Image.open("Tests/images/hopper_bad_exif.jpg") as i: + # Should not raise struct.error. + self.assert_warning(UserWarning, i._getexif) def test_save_rgba(self): im = hopper("RGBA") @@ -161,38 +192,30 @@ class TestFileTiff(PillowTestCase): self.assertRaises(IOError, im.save, outfile) def test_little_endian(self): - im = Image.open('Tests/images/16bit.cropped.tif') + im = Image.open("Tests/images/16bit.cropped.tif") self.assertEqual(im.getpixel((0, 0)), 480) - self.assertEqual(im.mode, 'I;16') + self.assertEqual(im.mode, "I;16") b = im.tobytes() # Bytes are in image native order (little endian) - if py3: - self.assertEqual(b[0], ord(b'\xe0')) - self.assertEqual(b[1], ord(b'\x01')) - else: - self.assertEqual(b[0], b'\xe0') - self.assertEqual(b[1], b'\x01') + 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') + im = Image.open("Tests/images/16bit.MM.cropped.tif") self.assertEqual(im.getpixel((0, 0)), 480) - self.assertEqual(im.mode, 'I;16B') + self.assertEqual(im.mode, "I;16B") b = im.tobytes() # Bytes are in image native order (big endian) - if py3: - self.assertEqual(b[0], ord(b'\x01')) - self.assertEqual(b[1], ord(b'\xe0')) - else: - self.assertEqual(b[0], b'\x01') - self.assertEqual(b[1], b'\xe0') + self.assertEqual(b[0], ord(b"\x01")) + self.assertEqual(b[1], ord(b"\xe0")) def test_16bit_s(self): - im = Image.open('Tests/images/16bit.s.tif') + im = Image.open("Tests/images/16bit.s.tif") im.load() - self.assertEqual(im.mode, 'I') + self.assertEqual(im.mode, "I") self.assertEqual(im.getpixel((0, 0)), 32767) self.assertEqual(im.getpixel((0, 1)), 0) @@ -200,7 +223,7 @@ class TestFileTiff(PillowTestCase): """ Are we generating the same interpretation of the image as Imagemagick is? """ - im = Image.open('Tests/images/12bit.cropped.tif') + im = Image.open("Tests/images/12bit.cropped.tif") # to make the target -- # convert 12bit.cropped.tif -depth 16 tmp.tif @@ -208,107 +231,130 @@ class TestFileTiff(PillowTestCase): # 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' + path = "Tests/images/10ct_32bit_128.tiff" im = Image.open(path) im.load() self.assertEqual(im.getpixel((0, 0)), -0.4526388943195343) - self.assertEqual( - im.getextrema(), (-3.140936851501465, 3.140684127807617)) + self.assertEqual(im.getextrema(), (-3.140936851501465, 3.140684127807617)) def test_unknown_pixel_mode(self): self.assertRaises( - IOError, Image.open, 'Tests/images/hopper_unknown_pixel_mode.tif') + IOError, Image.open, "Tests/images/hopper_unknown_pixel_mode.tif" + ) def test_n_frames(self): for path, n_frames in [ - ['Tests/images/multipage-lastframe.tif', 1], - ['Tests/images/multipage.tiff', 3] + ["Tests/images/multipage-lastframe.tif", 1], + ["Tests/images/multipage.tiff", 3], ]: - # Test is_animated before n_frames - im = Image.open(path) - self.assertEqual(im.is_animated, n_frames != 1) - - # Test is_animated after n_frames - im = Image.open(path) - self.assertEqual(im.n_frames, n_frames) - self.assertEqual(im.is_animated, n_frames != 1) + 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(2) - im.load() - self.assertEqual(im.size, (20, 20)) - self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 0, 255)) + 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)) def test_multipage_last_frame(self): - im = Image.open('Tests/images/multipage-lastframe.tif') + 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)) + self.assertEqual(im.convert("RGB").getpixel((0, 0)), (0, 0, 255)) def test___str__(self): filename = "Tests/images/pil136.tiff" - im = Image.open(filename) + with Image.open(filename) as im: - # Act - ret = str(im.ifd) + # Act + ret = str(im.ifd) - # Assert - self.assertIsInstance(ret, str) + # Assert + self.assertIsInstance(ret, str) def test_dict(self): # Arrange filename = "Tests/images/pil136.tiff" - im = Image.open(filename) + with Image.open(filename) as im: - # v2 interface - v2_tags = {256: 55, 257: 43, 258: (8, 8, 8, 8), 259: 1, - 262: 2, 296: 2, 273: (8,), 338: (1,), 277: 4, - 279: (9460,), 282: 72.0, 283: 72.0, 284: 1} - self.assertEqual(dict(im.tag_v2), v2_tags) + # v2 interface + v2_tags = { + 256: 55, + 257: 43, + 258: (8, 8, 8, 8), + 259: 1, + 262: 2, + 296: 2, + 273: (8,), + 338: (1,), + 277: 4, + 279: (9460,), + 282: 72.0, + 283: 72.0, + 284: 1, + } + self.assertEqual(dict(im.tag_v2), v2_tags) - # legacy interface - legacy_tags = {256: (55,), 257: (43,), 258: (8, 8, 8, 8), 259: (1,), - 262: (2,), 296: (2,), 273: (8,), 338: (1,), 277: (4,), - 279: (9460,), 282: ((720000, 10000),), - 283: ((720000, 10000),), 284: (1,)} - self.assertEqual(dict(im.tag), legacy_tags) + # legacy interface + legacy_tags = { + 256: (55,), + 257: (43,), + 258: (8, 8, 8, 8), + 259: (1,), + 262: (2,), + 296: (2,), + 273: (8,), + 338: (1,), + 277: (4,), + 279: (9460,), + 282: ((720000, 10000),), + 283: ((720000, 10000),), + 284: (1,), + } + self.assertEqual(dict(im.tag), legacy_tags) def test__delitem__(self): filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - len_before = len(dict(im.ifd)) - del im.ifd[256] - len_after = len(dict(im.ifd)) - self.assertEqual(len_before, len_after + 1) + with Image.open(filename) as im: + len_before = len(dict(im.ifd)) + del im.ifd[256] + len_after = len(dict(im.ifd)) + self.assertEqual(len_before, len_after + 1) def test_load_byte(self): for legacy_api in [False, True]: @@ -327,35 +373,37 @@ class TestFileTiff(PillowTestCase): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" ret = ifd.load_float(data, False) - self.assertEqual(ret, (1.6777999408082104e+22, 1.6777999408082104e+22)) + self.assertEqual(ret, (1.6777999408082104e22, 1.6777999408082104e22)) def test_load_double(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" ret = ifd.load_double(data, False) - self.assertEqual(ret, (8.540883223036124e+194, 8.540883223036124e+194)) + self.assertEqual(ret, (8.540883223036124e194, 8.540883223036124e194)) def test_seek(self): filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - im.seek(0) - self.assertEqual(im.tell(), 0) + with Image.open(filename) as im: + im.seek(0) + 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 + value = 34 ret = _limit_rational(value, 65536) self.assertEqual(ret, (34, 1)) def test__limit_rational_float(self): from PIL.TiffImagePlugin import _limit_rational + value = 22.3 ret = _limit_rational(value, 65536) self.assertEqual(ret, (223, 10)) @@ -377,7 +425,7 @@ class TestFileTiff(PillowTestCase): "Tests/images/tiff_gray_2_4_bpp/hopper2I.tif", "Tests/images/tiff_gray_2_4_bpp/hopper2R.tif", "Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif", - ) + ), ), ( 7.3, # epsilon @@ -386,7 +434,7 @@ class TestFileTiff(PillowTestCase): "Tests/images/tiff_gray_2_4_bpp/hopper4I.tif", "Tests/images/tiff_gray_2_4_bpp/hopper4R.tif", "Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif", - ) + ), ), ) original = hopper("L") @@ -402,20 +450,18 @@ class TestFileTiff(PillowTestCase): self.assert_image_equal(im, im2) def test_with_underscores(self): - kwargs = {'resolution_unit': 'inch', - 'x_resolution': 72, - 'y_resolution': 36} + 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 @@ -435,8 +481,7 @@ class TestFileTiff(PillowTestCase): infile = "Tests/images/tiff_strip_raw.tif" im = Image.open(infile) - 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_strip_planar_raw(self): # gdal_translate -of GTiff -co INTERLEAVE=BAND \ @@ -444,16 +489,13 @@ class TestFileTiff(PillowTestCase): infile = "Tests/images/tiff_strip_planar_raw.tif" im = Image.open(infile) - 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_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 \ @@ -462,14 +504,20 @@ class TestFileTiff(PillowTestCase): infile = "Tests/images/tiff_tiled_planar_raw.tif" im = Image.open(infile) - 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_palette(self): + for mode in ["P", "PA"]: + outfile = self.tempfile("temp.tif") + + im = hopper(mode) + im.save(outfile) + + reloaded = Image.open(outfile) + 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) @@ -478,10 +526,9 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.n_frames, 3) # Test appending images - mp = io.BytesIO() - im = Image.new('RGB', (100, 100), '#f00') - ims = [Image.new('RGB', (100, 100), color) for color - in ['#0f0', '#00f']] + mp = BytesIO() + im = Image.new("RGB", (100, 100), "#f00") + ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] im.copy().save(mp, format="TIFF", save_all=True, append_images=ims) mp.seek(0, os.SEEK_SET) @@ -490,11 +537,10 @@ class TestFileTiff(PillowTestCase): # Test appending using a generator def imGenerator(ims): - for im in ims: - yield im - mp = io.BytesIO() - im.save(mp, format="TIFF", save_all=True, - append_images=imGenerator(ims)) + yield from ims + + mp = BytesIO() + im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims)) mp.seek(0, os.SEEK_SET) reread = Image.open(mp) @@ -505,15 +551,14 @@ class TestFileTiff(PillowTestCase): # At the time of writing this will only work for non-compressed tiffs # as libtiff does not support embedded ICC profiles, # ImageFile._save(..) however does. - im = Image.new('RGB', (1, 1)) - im.info['icc_profile'] = 'Dummy value' + im = Image.new("RGB", (1, 1)) + im.info["icc_profile"] = "Dummy value" # 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']) + tmpfile = self.tempfile("temp.tif") + im.save(tmpfile, "TIFF", compression="raw") + 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 @@ -534,19 +579,26 @@ class TestFileTiff(PillowTestCase): with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) - with open(tmpfile, 'rb') as f: + with open(tmpfile, "rb") as f: im = Image.open(f) fp = im.fp self.assertFalse(fp.closed) 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): + Image.open("Tests/images/string_dimension.tiff") -@unittest.skipUnless(sys.platform.startswith('win32'), "Windows only") + +@unittest.skipUnless(is_win32(), "Windows only") class TestFileTiffW32(PillowTestCase): def test_fd_leak(self): tmpfile = self.tempfile("temp.tif") - import os # this is an mmaped file. with Image.open("Tests/images/uint16_1_4660.tif") as im: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 14ec3ab6c..7393a147c 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,16 +1,15 @@ import io import struct -from .helper import PillowTestCase, hopper - from PIL import Image, TiffImagePlugin, TiffTags -from PIL.TiffImagePlugin import _limit_rational, IFDRational +from PIL.TiffImagePlugin import IFDRational, _limit_rational + +from .helper import PillowTestCase, hopper tag_ids = {info.name: info.value for info in TiffTags.TAGS_V2.values()} class TestFileTiffMetadata(PillowTestCase): - def test_rt_metadata(self): """ Test writing arbitrary metadata into the tiff image directory Use case is ImageJ private tags, one numeric, one arbitrary @@ -29,23 +28,23 @@ class TestFileTiffMetadata(PillowTestCase): # the tiff file format can't take 8 bit bytes in that field. basetextdata = "This is some arbitrary metadata for a text field" - bindata = basetextdata.encode('ascii') + b" \xff" + bindata = basetextdata.encode("ascii") + b" \xff" textdata = basetextdata + " " + chr(255) reloaded_textdata = basetextdata + " ?" floatdata = 12.345 doubledata = 67.89 info = TiffImagePlugin.ImageFileDirectory() - ImageJMetaData = tag_ids['ImageJMetaData'] - ImageJMetaDataByteCounts = tag_ids['ImageJMetaDataByteCounts'] - ImageDescription = tag_ids['ImageDescription'] + ImageJMetaData = tag_ids["ImageJMetaData"] + ImageJMetaDataByteCounts = tag_ids["ImageJMetaDataByteCounts"] + ImageDescription = tag_ids["ImageDescription"] info[ImageJMetaDataByteCounts] = len(bindata) info[ImageJMetaData] = bindata - info[tag_ids['RollAngle']] = floatdata - info.tagtype[tag_ids['RollAngle']] = 11 - info[tag_ids['YawAngle']] = doubledata - info.tagtype[tag_ids['YawAngle']] = 12 + info[tag_ids["RollAngle"]] = floatdata + info.tagtype[tag_ids["RollAngle"]] = 11 + info[tag_ids["YawAngle"]] = doubledata + info.tagtype[tag_ids["YawAngle"]] = 12 info[ImageDescription] = textdata @@ -53,123 +52,134 @@ class TestFileTiffMetadata(PillowTestCase): img.save(f, tiffinfo=info) - loaded = Image.open(f) + with Image.open(f) as loaded: - self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], - (len(bindata),)) + self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) + self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (len(bindata),)) - self.assertEqual(loaded.tag[ImageJMetaData], bindata) - self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata) + self.assertEqual(loaded.tag[ImageJMetaData], bindata) + self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata) - self.assertEqual(loaded.tag[ImageDescription], (reloaded_textdata,)) - self.assertEqual(loaded.tag_v2[ImageDescription], reloaded_textdata) + self.assertEqual(loaded.tag[ImageDescription], (reloaded_textdata,)) + self.assertEqual(loaded.tag_v2[ImageDescription], reloaded_textdata) - loaded_float = loaded.tag[tag_ids['RollAngle']][0] - self.assertAlmostEqual(loaded_float, floatdata, places=5) - loaded_double = loaded.tag[tag_ids['YawAngle']][0] - self.assertAlmostEqual(loaded_double, doubledata) + loaded_float = loaded.tag[tag_ids["RollAngle"]][0] + self.assertAlmostEqual(loaded_float, floatdata, places=5) + loaded_double = loaded.tag[tag_ids["YawAngle"]][0] + self.assertAlmostEqual(loaded_double, doubledata) # check with 2 element ImageJMetaDataByteCounts, issue #2006 info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8) img.save(f, tiffinfo=info) - loaded = Image.open(f) + with Image.open(f) as loaded: - self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], - (8, len(bindata) - 8)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], - (8, len(bindata) - 8)) + self.assertEqual( + loaded.tag[ImageJMetaDataByteCounts], (8, len(bindata) - 8) + ) + self.assertEqual( + loaded.tag_v2[ImageJMetaDataByteCounts], (8, len(bindata) - 8) + ) def test_read_metadata(self): - img = Image.open('Tests/images/hopper_g4.tif') + with Image.open("Tests/images/hopper_g4.tif") as img: - self.assertEqual({'YResolution': IFDRational(4294967295, 113653537), - 'PlanarConfiguration': 1, - 'BitsPerSample': (1,), - 'ImageLength': 128, - 'Compression': 4, - 'FillOrder': 1, - 'RowsPerStrip': 128, - 'ResolutionUnit': 3, - 'PhotometricInterpretation': 0, - 'PageNumber': (0, 1), - 'XResolution': IFDRational(4294967295, 113653537), - 'ImageWidth': 128, - 'Orientation': 1, - 'StripByteCounts': (1968,), - 'SamplesPerPixel': 1, - 'StripOffsets': (8,) - }, img.tag_v2.named()) + self.assertEqual( + { + "YResolution": IFDRational(4294967295, 113653537), + "PlanarConfiguration": 1, + "BitsPerSample": (1,), + "ImageLength": 128, + "Compression": 4, + "FillOrder": 1, + "RowsPerStrip": 128, + "ResolutionUnit": 3, + "PhotometricInterpretation": 0, + "PageNumber": (0, 1), + "XResolution": IFDRational(4294967295, 113653537), + "ImageWidth": 128, + "Orientation": 1, + "StripByteCounts": (1968,), + "SamplesPerPixel": 1, + "StripOffsets": (8,), + }, + img.tag_v2.named(), + ) - self.assertEqual({'YResolution': ((4294967295, 113653537),), - 'PlanarConfiguration': (1,), - 'BitsPerSample': (1,), - 'ImageLength': (128,), - 'Compression': (4,), - 'FillOrder': (1,), - 'RowsPerStrip': (128,), - 'ResolutionUnit': (3,), - 'PhotometricInterpretation': (0,), - 'PageNumber': (0, 1), - 'XResolution': ((4294967295, 113653537),), - 'ImageWidth': (128,), - 'Orientation': (1,), - 'StripByteCounts': (1968,), - 'SamplesPerPixel': (1,), - 'StripOffsets': (8,) - }, img.tag.named()) + self.assertEqual( + { + "YResolution": ((4294967295, 113653537),), + "PlanarConfiguration": (1,), + "BitsPerSample": (1,), + "ImageLength": (128,), + "Compression": (4,), + "FillOrder": (1,), + "RowsPerStrip": (128,), + "ResolutionUnit": (3,), + "PhotometricInterpretation": (0,), + "PageNumber": (0, 1), + "XResolution": ((4294967295, 113653537),), + "ImageWidth": (128,), + "Orientation": (1,), + "StripByteCounts": (1968,), + "SamplesPerPixel": (1,), + "StripOffsets": (8,), + }, + img.tag.named(), + ) def test_write_metadata(self): """ Test metadata writing through the python code """ - img = Image.open('Tests/images/hopper.tif') + img = Image.open("Tests/images/hopper.tif") - f = self.tempfile('temp.tiff') + 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): - original[k] = IFDRational(*_limit_rational(v, 2**31)) + original[k] = IFDRational(*_limit_rational(v, 2 ** 31)) elif isinstance(v, tuple) and isinstance(v[0], IFDRational): - original[k] = tuple(IFDRational(*_limit_rational(elt, 2**31)) - for elt in v) + original[k] = tuple( + IFDRational(*_limit_rational(elt, 2 ** 31)) for elt in v + ) - ignored = ['StripByteCounts', 'RowsPerStrip', - 'PageNumber', 'StripOffsets'] + ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] for tag, value in reloaded.items(): if tag in ignored: continue - if (isinstance(original[tag], tuple) - and isinstance(original[tag][0], IFDRational)): + if isinstance(original[tag], tuple) and isinstance( + original[tag][0], IFDRational + ): # Need to compare element by element in the tuple, # not comparing tuples of object references - self.assert_deep_equal(original[tag], - value, - "%s didn't roundtrip, %s, %s" % - (tag, original[tag], value)) + self.assert_deep_equal( + 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)) + self.assertEqual( + original[tag], + value, + "{} didn't roundtrip, {}, {}".format(tag, original[tag], value), + ) for tag, value in original.items(): if tag not in ignored: - self.assertEqual( - value, reloaded[tag], "%s didn't roundtrip" % tag) + self.assertEqual(value, reloaded[tag], "%s didn't roundtrip" % tag) def test_no_duplicate_50741_tag(self): - self.assertEqual(tag_ids['MakerNoteSafety'], 50741) - self.assertEqual(tag_ids['BestQualityScale'], 50780) + self.assertEqual(tag_ids["MakerNoteSafety"], 50741) + self.assertEqual(tag_ids["BestQualityScale"], 50780) def test_empty_metadata(self): - f = io.BytesIO(b'II*\x00\x08\x00\x00\x00') + f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) # Should not raise struct.error. @@ -177,31 +187,31 @@ 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') + im = Image.open("Tests/images/hopper.iccprofile.tif") + 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') - outfile = self.tempfile('temp.png') + im = Image.open("Tests/images/hopper.iccprofile.tif") + 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 = Image.open("Tests/images/hopper.iccprofile_binary.tif") + outfile = self.tempfile("temp.png") im.save(outfile) def test_exif_div_zero(self): @@ -209,19 +219,20 @@ class TestFileTiffMetadata(PillowTestCase): info = TiffImagePlugin.ImageFileDirectory_v2() info[41988] = TiffImagePlugin.IFDRational(0, 0) - out = self.tempfile('temp.tiff') - im.save(out, tiffinfo=info, compression='raw') + 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_expty_values(self): + def test_empty_values(self): data = io.BytesIO( - b'II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x98\x82\x02\x00\x07\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00a ' - b'text\x00\x00') + b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x98\x82\x02\x00\x07\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00a " + b"text\x00\x00" + ) head = data.read(8) info = TiffImagePlugin.ImageFileDirectory_v2(head) info.load(data) @@ -230,19 +241,21 @@ class TestFileTiffMetadata(PillowTestCase): self.assertIn(33432, info) def test_PhotoshopInfo(self): - im = Image.open('Tests/images/issue_2278.tif') + im = Image.open("Tests/images/issue_2278.tif") - self.assertIsInstance(im.tag_v2[34377], bytes) - out = self.tempfile('temp.tiff') + self.assertEqual(len(im.tag_v2[34377]), 1) + self.assertIsInstance(im.tag_v2[34377][0], bytes) + out = self.tempfile("temp.tiff") im.save(out) - reloaded = Image.open(out) - self.assertIsInstance(reloaded.tag_v2[34377], 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() # 277: ("SamplesPerPixel", SHORT, 1), - ifd._tagdata[277] = struct.pack('hh', 4, 4) + ifd._tagdata[277] = struct.pack("hh", 4, 4) ifd.tagtype[277] = TiffTags.SHORT # Should not raise ValueError. diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index 1e0a835d2..74c238fc1 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -1,10 +1,9 @@ -from .helper import PillowTestCase - from PIL import WalImageFile +from .helper import PillowTestCase + class TestFileWal(PillowTestCase): - def test_open(self): # Arrange TEST_FILE = "Tests/images/hopper.wal" diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 7e6fad93c..685f876e2 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,9 +1,12 @@ -from .helper import unittest, PillowTestCase, hopper +import unittest from PIL import Image, WebPImagePlugin +from .helper import PillowTestCase, hopper + try: from PIL import _webp + HAVE_WEBP = True except ImportError: HAVE_WEBP = False @@ -16,8 +19,7 @@ class TestUnsupportedWebp(PillowTestCase): file_path = "Tests/images/hopper.webp" self.assert_warning( - UserWarning, - lambda: self.assertRaises(IOError, Image.open, file_path) + UserWarning, lambda: self.assertRaises(IOError, Image.open, file_path) ) if HAVE_WEBP: @@ -26,7 +28,6 @@ class TestUnsupportedWebp(PillowTestCase): @unittest.skipIf(not HAVE_WEBP, "WebP support not installed") class TestFileWebp(PillowTestCase): - def setUp(self): self.rgb_mode = "RGB" @@ -51,7 +52,8 @@ class TestFileWebp(PillowTestCase): # 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) + image, "Tests/images/hopper_webp_bits.ppm", 1.0 + ) def test_write_rgb(self): """ @@ -72,7 +74,8 @@ class TestFileWebp(PillowTestCase): # 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) + 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, @@ -149,26 +152,28 @@ class TestFileWebp(PillowTestCase): def test_file_pointer_could_be_reused(self): file_path = "Tests/images/hopper.webp" - with open(file_path, 'rb') as blob: + with open(file_path, "rb") as blob: Image.open(blob).load() Image.open(blob).load() - @unittest.skipUnless(HAVE_WEBP and _webp.HAVE_WEBPANIM, - "WebP save all not available") + @unittest.skipUnless( + HAVE_WEBP and _webp.HAVE_WEBPANIM, "WebP save all not available" + ) def test_background_from_gif(self): - im = Image.open("Tests/images/chi.gif") - original_value = im.convert("RGB").getpixel((1, 1)) + 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)) - difference = sum([abs(original_value[i] - reread_value[i]) - for i in range(0, 3)]) + with Image.open(out_gif) as reread: + reread_value = reread.convert("RGB").getpixel((1, 1)) + difference = sum( + [abs(original_value[i] - reread_value[i]) for i in range(0, 3)] + ) self.assertLess(difference, 5) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index c868fa1df..e3c2b98b9 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,7 +1,9 @@ -from .helper import unittest, PillowTestCase, hopper +import unittest from PIL import Image +from .helper import PillowTestCase, hopper + try: from PIL import _webp except ImportError: @@ -10,11 +12,11 @@ except ImportError: @unittest.skipIf(_webp is None, "WebP support not installed") class TestFileWebpAlpha(PillowTestCase): - def setUp(self): if _webp.WebPDecoderBuggyAlpha(self): - self.skipTest("Buggy early version of WebP installed, " - "not testing transparency") + self.skipTest( + "Buggy early version of WebP installed, not testing transparency" + ) def test_read_rgba(self): """ @@ -34,7 +36,7 @@ class TestFileWebpAlpha(PillowTestCase): image.tobytes() - target = Image.open('Tests/images/transparent.png') + target = Image.open("Tests/images/transparent.png") self.assert_image_similar(image, target, 20.0) def test_write_lossless_rgb(self): @@ -46,7 +48,7 @@ class TestFileWebpAlpha(PillowTestCase): temp_file = self.tempfile("temp.webp") # temp_file = "temp.webp" - pil_image = hopper('RGBA') + pil_image = hopper("RGBA") mask = Image.new("RGBA", (64, 64), (128, 128, 128, 128)) # Add some partially transparent bits: @@ -103,7 +105,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 +115,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 c751545c7..834080db8 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,24 +1,26 @@ -from .helper import PillowTestCase - from PIL import Image +from .helper import PillowTestCase + try: from PIL import _webp + HAVE_WEBP = True except ImportError: HAVE_WEBP = False class TestFileWebpAnimation(PillowTestCase): - def setUp(self): if not HAVE_WEBP: - self.skipTest('WebP support not installed') + self.skipTest("WebP support not installed") return if not _webp.HAVE_WEBPANIM: - self.skipTest("WebP library does not contain animation support, " - "not testing animation") + self.skipTest( + "WebP library does not contain animation support, " + "not testing animation" + ) def test_n_frames(self): """ @@ -26,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): """ @@ -41,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): """ @@ -78,23 +80,26 @@ class TestFileWebpAnimation(PillowTestCase): 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') + frame1 = Image.open("Tests/images/anim_frame1.webp") + frame2 = Image.open("Tests/images/anim_frame2.webp") temp_file1 = self.tempfile("temp.webp") - frame1.copy().save(temp_file1, - save_all=True, append_images=[frame2], - lossless=True) + frame1.copy().save( + temp_file1, save_all=True, append_images=[frame2], lossless=True + ) check(temp_file1) # Tests appending using a generator def imGenerator(ims): - for im in ims: - yield im + yield from ims + temp_file2 = self.tempfile("temp_generator.webp") - frame1.copy().save(temp_file2, - save_all=True, append_images=imGenerator([frame2]), - lossless=True) + frame1.copy().save( + temp_file2, + save_all=True, + append_images=imGenerator([frame2]), + lossless=True, + ) check(temp_file2) def test_timestamp_and_duration(self): @@ -105,11 +110,14 @@ 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) + frame1 = Image.open("Tests/images/anim_frame1.webp") + frame2 = Image.open("Tests/images/anim_frame2.webp") + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2, frame1], + duration=durations, + ) im = Image.open(temp_file) self.assertEqual(im.n_frames, 5) @@ -133,18 +141,21 @@ 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) + frame1 = Image.open("Tests/images/anim_frame1.webp") + frame2 = Image.open("Tests/images/anim_frame2.webp") + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2, frame1], + duration=dur, + ) im = Image.open(temp_file) self.assertEqual(im.n_frames, 5) self.assertTrue(im.is_animated) # Traverse frames in reverse, checking timestamps and durations - ts = dur * (im.n_frames-1) + ts = dur * (im.n_frames - 1) for frame in reversed(range(im.n_frames)): im.seek(frame) im.load() diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 528c9177d..2eff41529 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,23 +1,23 @@ -from .helper import PillowTestCase, hopper - from PIL import Image +from .helper import PillowTestCase, hopper + try: from PIL import _webp + HAVE_WEBP = True except ImportError: HAVE_WEBP = False class TestFileWebpLossless(PillowTestCase): - def setUp(self): if not HAVE_WEBP: - self.skipTest('WebP support not installed') + self.skipTest("WebP support not installed") return if _webp.WebPDecoderVersion() < 0x0200: - self.skipTest('lossless not included') + self.skipTest("lossless not included") self.rgb_mode = "RGB" diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 402b6ce42..e3acf745d 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,49 +1,49 @@ -from .helper import PillowTestCase +from io import BytesIO from PIL import Image +from .helper import PillowTestCase + try: from PIL import _webp + HAVE_WEBP = True except ImportError: HAVE_WEBP = False class TestFileWebpMetadata(PillowTestCase): - def setUp(self): if not HAVE_WEBP: - self.skipTest('WebP support not installed') + self.skipTest("WebP support not installed") return if not _webp.HAVE_WEBPMUX: - self.skipTest('WebPMux support not installed') + self.skipTest("WebPMux support not installed") def test_read_exif_metadata(self): file_path = "Tests/images/flower.webp" - image = Image.open(file_path) + with Image.open(file_path) as image: - self.assertEqual(image.format, "WEBP") - exif_data = image.info.get("exif", None) - self.assertTrue(exif_data) + self.assertEqual(image.format, "WEBP") + exif_data = image.info.get("exif", None) + self.assertTrue(exif_data) - exif = image._getexif() + exif = image._getexif() - # camera make - self.assertEqual(exif[271], "Canon") + # camera make + self.assertEqual(exif[271], "Canon") - jpeg_image = Image.open('Tests/images/flower.jpg') - expected_exif = jpeg_image.info['exif'] + with Image.open("Tests/images/flower.jpg") as jpeg_image: + expected_exif = jpeg_image.info["exif"] - self.assertEqual(exif_data, expected_exif) + self.assertEqual(exif_data, expected_exif) def test_write_exif_metadata(self): - from io import BytesIO - file_path = "Tests/images/flower.jpg" image = Image.open(file_path) - expected_exif = image.info['exif'] + expected_exif = image.info["exif"] test_buffer = BytesIO() @@ -52,33 +52,30 @@ class TestFileWebpMetadata(PillowTestCase): test_buffer.seek(0) webp_image = Image.open(test_buffer) - webp_exif = webp_image.info.get('exif', None) + 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") + self.assertEqual(webp_exif, expected_exif, "WebP EXIF didn't match") def test_read_icc_profile(self): file_path = "Tests/images/flower2.webp" - image = Image.open(file_path) + with Image.open(file_path) as image: - self.assertEqual(image.format, "WEBP") - self.assertTrue(image.info.get("icc_profile", None)) + self.assertEqual(image.format, "WEBP") + self.assertTrue(image.info.get("icc_profile", None)) - icc = image.info['icc_profile'] + icc = image.info["icc_profile"] - jpeg_image = Image.open('Tests/images/flower2.jpg') - expected_icc = jpeg_image.info['icc_profile'] + with Image.open("Tests/images/flower2.jpg") as jpeg_image: + expected_icc = jpeg_image.info["icc_profile"] - self.assertEqual(icc, expected_icc) + self.assertEqual(icc, expected_icc) def test_write_icc_metadata(self): - from io import BytesIO - file_path = "Tests/images/flower2.jpg" image = Image.open(file_path) - expected_icc_profile = image.info['icc_profile'] + expected_icc_profile = image.info["icc_profile"] test_buffer = BytesIO() @@ -87,20 +84,18 @@ class TestFileWebpMetadata(PillowTestCase): test_buffer.seek(0) webp_image = Image.open(test_buffer) - webp_icc_profile = webp_image.info.get('icc_profile', None) + webp_icc_profile = webp_image.info.get("icc_profile", None) self.assertTrue(webp_icc_profile) if webp_icc_profile: self.assertEqual( - webp_icc_profile, expected_icc_profile, - "Webp ICC didn't match") + webp_icc_profile, expected_icc_profile, "Webp ICC didn't match" + ) 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) + self.assertIn("exif", image.info) test_buffer = BytesIO() @@ -113,23 +108,28 @@ class TestFileWebpMetadata(PillowTestCase): def test_write_animated_metadata(self): if not _webp.HAVE_WEBPANIM: - self.skipTest('WebP animation support not available') + self.skipTest("WebP animation support not available") - iccp_data = ''.encode('utf-8') - exif_data = ''.encode('utf-8') - xmp_data = ''.encode('utf-8') + iccp_data = b"" + exif_data = b"" + xmp_data = b"" temp_file = self.tempfile("temp.webp") - frame1 = Image.open('Tests/images/anim_frame1.webp') - frame2 = Image.open('Tests/images/anim_frame2.webp') - frame1.save(temp_file, save_all=True, - append_images=[frame2, frame1, frame2], - icc_profile=iccp_data, exif=exif_data, xmp=xmp_data) + frame1 = Image.open("Tests/images/anim_frame1.webp") + frame2 = Image.open("Tests/images/anim_frame2.webp") + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2], + icc_profile=iccp_data, + exif=exif_data, + xmp=xmp_data, + ) - image = Image.open(temp_file) - self.assertIn('icc_profile', image.info) - self.assertIn('exif', image.info) - self.assertIn('xmp', image.info) - self.assertEqual(iccp_data, image.info.get('icc_profile', None)) - self.assertEqual(exif_data, image.info.get('exif', None)) - self.assertEqual(xmp_data, image.info.get('xmp', None)) + 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 146888491..c07bbf60a 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,32 +1,30 @@ -from .helper import PillowTestCase, hopper +from PIL import Image, WmfImagePlugin -from PIL import Image -from PIL import WmfImagePlugin +from .helper import PillowTestCase, hopper 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: @@ -34,6 +32,7 @@ class TestFileWmf(PillowTestCase): def save(self, im, fp, filename): self.methodCalled = True + handler = TestHandler() WmfImagePlugin.register_handler(handler) @@ -45,9 +44,18 @@ class TestFileWmf(PillowTestCase): # Restore the state before this test WmfImagePlugin.register_handler(None) + def test_load_dpi_rounding(self): + # Round up + with Image.open("Tests/images/drawing.emf") as im: + self.assertEqual(im.info["dpi"], 1424) + + # Round down + with Image.open("Tests/images/drawing_roundDown.emf") as im: + self.assertEqual(im.info["dpi"], 1426) + def test_save(self): im = hopper() for ext in [".wmf", ".emf"]: - tmpfile = self.tempfile("temp"+ext) + tmpfile = self.tempfile("temp" + ext) self.assertRaises(IOError, im.save, tmpfile) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index fbcb30e90..5a1eb54bc 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,7 +1,9 @@ -from .helper import PillowTestCase +from io import BytesIO from PIL import Image +from .helper import PillowTestCase + PIL151 = b""" #define basic_width 32 #define basic_height 32 @@ -27,14 +29,11 @@ static char basic_bits[] = { class TestFileXbm(PillowTestCase): - def test_pil151(self): - from io import BytesIO - im = Image.open(BytesIO(PIL151)) im.load() - self.assertEqual(im.mode, '1') + self.assertEqual(im.mode, "1") self.assertEqual(im.size, (32, 32)) def test_open(self): @@ -43,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 @@ -55,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 b57cda2e3..a4ea8c491 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,12 +1,11 @@ -from .helper import PillowTestCase, hopper - from PIL import Image, XpmImagePlugin +from .helper import PillowTestCase, hopper + TEST_FILE = "Tests/images/hopper.xpm" class TestFileXpm(PillowTestCase): - def test_sanity(self): im = Image.open(TEST_FILE) im.load() @@ -15,21 +14,20 @@ class TestFileXpm(PillowTestCase): self.assertEqual(im.format, "XPM") # large error due to quantization->44 colors. - self.assert_image_similar(im.convert('RGB'), hopper('RGB'), 60) + self.assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - XpmImagePlugin.XpmImageFile, invalid_file) + self.assertRaises(SyntaxError, XpmImagePlugin.XpmImageFile, invalid_file) def test_load_read(self): # Arrange - im = Image.open(TEST_FILE) - dummy_bytes = 1 + with Image.open(TEST_FILE) as im: + dummy_bytes = 1 - # Act - data = im.load_read(dummy_bytes) + # Act + data = im.load_read(dummy_bytes) # Assert self.assertEqual(len(data), 16384) diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index 11672ebae..f8b6d3531 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -1,12 +1,11 @@ -from .helper import hopper, PillowTestCase - from PIL import Image, XVThumbImagePlugin +from .helper import PillowTestCase, hopper + TEST_FILE = "Tests/images/hopper.p7" class TestFileXVThumb(PillowTestCase): - def test_open(self): # Act im = Image.open(TEST_FILE) @@ -24,13 +23,13 @@ class TestFileXVThumb(PillowTestCase): bad_file = "Tests/images/hopper_bad.p7" # Act / Assert - self.assertRaises(SyntaxError, - XVThumbImagePlugin.XVThumbImageFile, bad_file) + self.assertRaises(SyntaxError, XVThumbImagePlugin.XVThumbImageFile, bad_file) def test_invalid_file(self): # Arrange invalid_file = "Tests/images/flower.jpg" # Act / Assert - self.assertRaises(SyntaxError, - XVThumbImagePlugin.XVThumbImageFile, invalid_file) + self.assertRaises( + SyntaxError, XVThumbImagePlugin.XVThumbImageFile, invalid_file + ) diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index be49e818e..9b3a342d0 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,12 +1,11 @@ -from .helper import PillowTestCase +from PIL import BdfFontFile, FontFile -from PIL import FontFile, BdfFontFile +from .helper import PillowTestCase filename = "Tests/images/courB08.bdf" class TestFontBdf(PillowTestCase): - def test_sanity(self): with open(filename, "rb") as test_file: diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 119012bc5..393ddb0fb 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,25 +1,28 @@ -from __future__ import division -from .helper import unittest, PillowLeakTestCase -import sys -from PIL import Image, features, ImageDraw, ImageFont +import unittest + +from PIL import Image, ImageDraw, ImageFont, features + +from .helper import PillowLeakTestCase, is_win32 -@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 mem_limit = 4096 # k def _test_font(self, font): - im = Image.new('RGB', (255, 255), 'white') + im = Image.new("RGB", (255, 255), "white") draw = ImageDraw.ImageDraw(im) - self._test_leak(lambda: draw.text((0, 0), "some text "*1024, # ~10k - font=font, fill="black")) + self._test_leak( + lambda: draw.text( + (0, 0), "some text " * 1024, font=font, fill="black" # ~10k + ) + ) - @unittest.skipIf(not features.check('freetype2'), - "Test requires freetype2") + @unittest.skipIf(not features.check("freetype2"), "Test requires freetype2") def test_leak(self): - ttype = ImageFont.truetype('Tests/fonts/FreeMono.ttf', 20) + ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) self._test_font(ttype) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index d092634f3..e37f43207 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,8 +1,6 @@ -from .helper import PillowTestCase +from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile -from PIL import Image, FontFile, PcfFontFile -from PIL import ImageFont, ImageDraw -from PIL._util import py3 +from .helper import PillowTestCase codecs = dir(Image.core) @@ -12,7 +10,6 @@ message = "hello, world" class TestFontPcf(PillowTestCase): - def setUp(self): if "zip_encoder" not in codecs or "zip_decoder" not in codecs: self.skipTest("zlib support not available") @@ -25,15 +22,15 @@ class TestFontPcf(PillowTestCase): self.assertEqual(len([_f for _f in font.glyph if _f]), 223) tempname = self.tempfile("temp.pil") - self.addCleanup(self.delete_tempfile, tempname[:-4]+'.pbm') + self.addCleanup(self.delete_tempfile, tempname[:-4] + ".pbm") font.save(tempname) - with Image.open(tempname.replace('.pil', '.pbm')) as loaded: - with Image.open('Tests/fonts/10x20.pbm') as target: + with Image.open(tempname.replace(".pil", ".pbm")) as loaded: + with Image.open("Tests/fonts/10x20.pbm") as target: self.assert_image_equal(loaded, target) - with open(tempname, 'rb') as f_loaded: - with open('Tests/fonts/10x20.pil', 'rb') as f_target: + with open(tempname, "rb") as f_loaded: + with open("Tests/fonts/10x20.pil", "rb") as f_target: self.assertEqual(f_loaded.read(), f_target.read()) return tempname @@ -49,8 +46,8 @@ class TestFontPcf(PillowTestCase): font = ImageFont.load(tempname) im = Image.new("L", (130, 30), "white") draw = ImageDraw.Draw(im) - draw.text((0, 0), message, 'black', font=font) - with Image.open('Tests/images/test_draw_pbm_target.png') as target: + draw.text((0, 0), message, "black", font=font) + with Image.open("Tests/images/test_draw_pbm_target.png") as target: self.assert_image_similar(im, target, 0) def test_textsize(self): @@ -61,8 +58,8 @@ class TestFontPcf(PillowTestCase): self.assertEqual(dy, 20) self.assertIn(dx, (0, 10)) for l in range(len(message)): - msg = message[:l+1] - self.assertEqual(font.getsize(msg), (len(msg)*10, 20)) + msg = message[: l + 1] + self.assertEqual(font.getsize(msg), (len(msg) * 10, 20)) def _test_high_characters(self, message): tempname = self.save_font() @@ -70,12 +67,11 @@ class TestFontPcf(PillowTestCase): im = Image.new("L", (750, 30), "white") draw = ImageDraw.Draw(im) draw.text((0, 0), message, "black", font=font) - with Image.open('Tests/images/high_ascii_chars.png') as target: + with Image.open("Tests/images/high_ascii_chars.png") as target: self.assert_image_similar(im, target, 0) def test_high_characters(self): - message = "".join(chr(i+1) for i in range(140, 232)) + 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 3e1c00c9e..e80e16ffe 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -1,26 +1,24 @@ -from .helper import PillowTestCase, hopper - -from PIL import Image -from PIL._util import py3 - import colorsys import itertools +from PIL import Image + +from .helper import PillowTestCase, hopper + class TestFormatHSV(PillowTestCase): - def int_to_float(self, i): - return float(i)/255.0 + return float(i) / 255.0 def str_to_float(self, i): - return float(ord(i))/255.0 + return float(ord(i)) / 255.0 def tuple_to_ints(self, tp): x, y, z = tp - return int(x*255.0), int(y*255.0), int(z*255.0) + return int(x * 255.0), int(y * 255.0), int(z * 255.0) def test_sanity(self): - Image.new('HSV', (100, 100)) + Image.new("HSV", (100, 100)) def wedge(self): w = Image._wedge() @@ -28,7 +26,7 @@ class TestFormatHSV(PillowTestCase): (px, h) = w.size - r = Image.new('L', (px*3, h)) + r = Image.new("L", (px * 3, h)) g = r.copy() b = r.copy() @@ -36,12 +34,12 @@ class TestFormatHSV(PillowTestCase): r.paste(w90, (px, 0)) g.paste(w90, (0, 0)) - g.paste(w, (2*px, 0)) + g.paste(w, (2 * px, 0)) b.paste(w, (px, 0)) - b.paste(w90, (2*px, 0)) + b.paste(w90, (2 * px, 0)) - img = Image.merge('RGB', (r, g, b)) + img = Image.merge("RGB", (r, g, b)) return img @@ -50,82 +48,97 @@ class TestFormatHSV(PillowTestCase): (r, g, b) = im.split() - if py3: - conv_func = self.int_to_float - else: - conv_func = self.str_to_float + conv_func = self.int_to_float - if hasattr(itertools, 'izip'): - iter_helper = itertools.izip - else: - iter_helper = itertools.zip_longest + converted = [ + self.tuple_to_ints(func(conv_func(_r), conv_func(_g), conv_func(_b))) + for (_r, _g, _b) in itertools.zip_longest( + r.tobytes(), g.tobytes(), b.tobytes() + ) + ] - converted = [self.tuple_to_ints(func(conv_func(_r), conv_func(_g), - conv_func(_b))) - for (_r, _g, _b) in iter_helper(r.tobytes(), g.tobytes(), - b.tobytes())] - - if py3: - new_bytes = b''.join(bytes(chr(h)+chr(s)+chr(v), 'latin-1') for ( - h, s, v) in converted) - else: - new_bytes = b''.join(chr(h)+chr(s)+chr(v) for ( - h, s, v) in converted) + new_bytes = b"".join( + bytes(chr(h) + chr(s) + chr(v), "latin-1") for (h, s, v) in converted + ) hsv = Image.frombytes(mode, r.size, new_bytes) return hsv def to_hsv_colorsys(self, im): - return self.to_xxx_colorsys(im, colorsys.rgb_to_hsv, 'HSV') + return self.to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") def to_rgb_colorsys(self, im): - return self.to_xxx_colorsys(im, colorsys.hsv_to_rgb, 'RGB') + return self.to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") def test_wedge(self): - src = self.wedge().resize((3*32, 32), Image.BILINEAR) - im = src.convert('HSV') + src = self.wedge().resize((3 * 32, 32), Image.BILINEAR) + im = src.convert("HSV") comparable = self.to_hsv_colorsys(src) - self.assert_image_similar(im.getchannel(0), comparable.getchannel(0), - 1, "Hue conversion is wrong") - self.assert_image_similar(im.getchannel(1), comparable.getchannel(1), - 1, "Saturation conversion is wrong") - self.assert_image_similar(im.getchannel(2), comparable.getchannel(2), - 1, "Value conversion is wrong") + self.assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" + ) + self.assert_image_similar( + im.getchannel(1), + comparable.getchannel(1), + 1, + "Saturation conversion is wrong", + ) + self.assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" + ) comparable = src - im = im.convert('RGB') + im = im.convert("RGB") - self.assert_image_similar(im.getchannel(0), comparable.getchannel(0), - 3, "R conversion is wrong") - self.assert_image_similar(im.getchannel(1), comparable.getchannel(1), - 3, "G conversion is wrong") - self.assert_image_similar(im.getchannel(2), comparable.getchannel(2), - 3, "B conversion is wrong") + self.assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong" + ) + self.assert_image_similar( + im.getchannel(1), comparable.getchannel(1), 3, "G conversion is wrong" + ) + self.assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 3, "B conversion is wrong" + ) def test_convert(self): - im = hopper('RGB').convert('HSV') - comparable = self.to_hsv_colorsys(hopper('RGB')) + im = hopper("RGB").convert("HSV") + comparable = self.to_hsv_colorsys(hopper("RGB")) - self.assert_image_similar(im.getchannel(0), comparable.getchannel(0), - 1, "Hue conversion is wrong") - self.assert_image_similar(im.getchannel(1), comparable.getchannel(1), - 1, "Saturation conversion is wrong") - self.assert_image_similar(im.getchannel(2), comparable.getchannel(2), - 1, "Value conversion is wrong") + self.assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" + ) + self.assert_image_similar( + im.getchannel(1), + comparable.getchannel(1), + 1, + "Saturation conversion is wrong", + ) + self.assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" + ) def test_hsv_to_rgb(self): - comparable = self.to_hsv_colorsys(hopper('RGB')) - converted = comparable.convert('RGB') + comparable = self.to_hsv_colorsys(hopper("RGB")) + converted = comparable.convert("RGB") comparable = self.to_rgb_colorsys(comparable) - self.assert_image_similar(converted.getchannel(0), - comparable.getchannel(0), - 3, "R conversion is wrong") - self.assert_image_similar(converted.getchannel(1), - comparable.getchannel(1), - 3, "G conversion is wrong") - self.assert_image_similar(converted.getchannel(2), - comparable.getchannel(2), - 3, "B conversion is wrong") + self.assert_image_similar( + converted.getchannel(0), + comparable.getchannel(0), + 3, + "R conversion is wrong", + ) + self.assert_image_similar( + converted.getchannel(1), + comparable.getchannel(1), + 3, + "G conversion is wrong", + ) + self.assert_image_similar( + converted.getchannel(2), + comparable.getchannel(2), + 3, + "B conversion is wrong", + ) diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index 8a096e672..a98c20579 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -1,18 +1,17 @@ -from .helper import PillowTestCase - from PIL import Image +from .helper import PillowTestCase + class TestFormatLab(PillowTestCase): - def test_white(self): - i = Image.open('Tests/images/lab.tif') + i = Image.open("Tests/images/lab.tif") 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)) self.assertEqual(k, (255, 128, 128)) @@ -21,14 +20,14 @@ class TestFormatLab(PillowTestCase): 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) + self.assertEqual(list(L), [255] * 100) + self.assertEqual(list(a), [128] * 100) + self.assertEqual(list(b), [128] * 100) def test_green(self): # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS # == RGB: 0, 152, 117 - i = Image.open('Tests/images/lab-green.tif') + i = Image.open("Tests/images/lab-green.tif") k = i.getpixel((0, 0)) self.assertEqual(k, (128, 28, 128)) @@ -36,7 +35,7 @@ class TestFormatLab(PillowTestCase): 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') + i = Image.open("Tests/images/lab-red.tif") k = i.getpixel((0, 0)) self.assertEqual(k, (128, 228, 128)) diff --git a/Tests/test_image.py b/Tests/test_image.py index 330048057..15cf435b0 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,42 +1,65 @@ -from .helper import unittest, PillowTestCase, hopper - -from PIL import Image -from PIL._util import py3 import os +import shutil +import tempfile +import unittest + +from PIL import Image, UnidentifiedImageError + +from .helper import PillowTestCase, hopper, is_win32 class TestImage(PillowTestCase): - def test_image_modes_success(self): for mode in [ - '1', 'P', 'PA', - 'L', 'LA', 'La', - 'F', 'I', 'I;16', 'I;16L', 'I;16B', 'I;16N', - 'RGB', 'RGBX', 'RGBA', 'RGBa', - 'CMYK', 'YCbCr', 'LAB', 'HSV', + "1", + "P", + "PA", + "L", + "LA", + "La", + "F", + "I", + "I;16", + "I;16L", + "I;16B", + "I;16N", + "RGB", + "RGBX", + "RGBA", + "RGBa", + "CMYK", + "YCbCr", + "LAB", + "HSV", ]: Image.new(mode, (1, 1)) def test_image_modes_fail(self): for mode in [ - '', 'bad', 'very very long', - 'BGR;15', 'BGR;16', 'BGR;24', 'BGR;32' + "", + "bad", + "very very long", + "BGR;15", + "BGR;16", + "BGR;24", + "BGR;32", ]: with self.assertRaises(ValueError) as e: Image.new(mode, (1, 1)) - self.assertEqual(str(e.exception), 'unrecognized image mode') + 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)) - self.assertEqual( - repr(im)[:45], "= 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")); @@ -339,30 +360,31 @@ 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; } - """ % sys.prefix.replace('\\', '\\\\')) + """ + % sys.prefix.replace("\\", "\\\\") + ) compiler = ccompiler.new_compiler() compiler.add_include_dir(sysconfig.get_python_inc()) - libdir = (sysconfig.get_config_var('LIBDIR') or - sysconfig.get_python_inc().replace('include', 'libs')) + libdir = sysconfig.get_config_var( + "LIBDIR" + ) or sysconfig.get_python_inc().replace("include", "libs") print(libdir) compiler.add_library_dir(libdir) - objects = compiler.compile(['embed_pil.c']) - compiler.link_executable(objects, 'embed_pil') + objects = compiler.compile(["embed_pil.c"]) + compiler.link_executable(objects, "embed_pil") env = os.environ.copy() - env["PATH"] = sys.prefix + ';' + env["PATH"] + env["PATH"] = sys.prefix + ";" + env["PATH"] # do not display the Windows Error Reporting dialog ctypes.windll.kernel32.SetErrorMode(0x0002) - process = subprocess.Popen(['embed_pil.exe'], env=env) + process = subprocess.Popen(["embed_pil.exe"], env=env) process.communicate() self.assertEqual(process.returncode, 0) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index cf104217a..8277e42af 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,34 +1,31 @@ -from .helper import PillowTestCase, hopper - from PIL import Image +from .helper import PillowTestCase, hopper + im = hopper().resize((128, 100)) class TestImageArray(PillowTestCase): - def test_toarray(self): def test(mode): ai = im.convert(mode).__array_interface__ - return ai['version'], ai["shape"], ai["typestr"], len(ai["data"]) + return ai["version"], ai["shape"], ai["typestr"], len(ai["data"]) + # self.assertEqual(test("1"), (3, (100, 128), '|b1', 1600)) - self.assertEqual(test("L"), (3, (100, 128), '|u1', 12800)) + self.assertEqual(test("L"), (3, (100, 128), "|u1", 12800)) # FIXME: wrong? - self.assertEqual(test("I"), (3, (100, 128), - Image._ENDIAN + 'i4', 51200)) + self.assertEqual(test("I"), (3, (100, 128), Image._ENDIAN + "i4", 51200)) # FIXME: wrong? - self.assertEqual(test("F"), (3, (100, 128), - Image._ENDIAN + 'f4', 51200)) + self.assertEqual(test("F"), (3, (100, 128), Image._ENDIAN + "f4", 51200)) - self.assertEqual(test("LA"), (3, (100, 128, 2), '|u1', 25600)) - self.assertEqual(test("RGB"), (3, (100, 128, 3), '|u1', 38400)) - self.assertEqual(test("RGBA"), (3, (100, 128, 4), '|u1', 51200)) - self.assertEqual(test("RGBX"), (3, (100, 128, 4), '|u1', 51200)) + self.assertEqual(test("LA"), (3, (100, 128, 2), "|u1", 25600)) + self.assertEqual(test("RGB"), (3, (100, 128, 3), "|u1", 38400)) + self.assertEqual(test("RGBA"), (3, (100, 128, 4), "|u1", 51200)) + self.assertEqual(test("RGBX"), (3, (100, 128, 4), "|u1", 51200)) def test_fromarray(self): - - class Wrapper(object): + class Wrapper: """ Class with API matching Image.fromarray """ def __init__(self, img, arr_params): diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 1ba1794eb..80fa9f513 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,18 +1,30 @@ -from .helper import PillowTestCase, hopper - from PIL import Image +from .helper import PillowTestCase, hopper + class TestImageConvert(PillowTestCase): - def test_sanity(self): - def convert(im, mode): out = im.convert(mode) self.assertEqual(out.mode, mode) self.assertEqual(out.size, im.size) - modes = "1", "L", "I", "F", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr" + modes = ( + "1", + "L", + "LA", + "P", + "PA", + "I", + "F", + "RGB", + "RGBA", + "RGBX", + "CMYK", + "YCbCr", + "HSV", + ) for mode in modes: im = hopper(mode) @@ -37,162 +49,159 @@ class TestImageConvert(PillowTestCase): def _test_float_conversion(self, im): orig = im.getpixel((5, 5)) - converted = im.convert('F').getpixel((5, 5)) + converted = im.convert("F").getpixel((5, 5)) self.assertEqual(orig, converted) def test_8bit(self): - im = Image.open('Tests/images/hopper.jpg') - self._test_float_conversion(im.convert('L')) + im = Image.open("Tests/images/hopper.jpg") + self._test_float_conversion(im.convert("L")) def test_16bit(self): - im = Image.open('Tests/images/16bit.cropped.tif') + im = Image.open("Tests/images/16bit.cropped.tif") self._test_float_conversion(im) def test_16bit_workaround(self): - im = Image.open('Tests/images/16bit.cropped.tif') - self._test_float_conversion(im.convert('I')) + im = Image.open("Tests/images/16bit.cropped.tif") + self._test_float_conversion(im.convert("I")) def test_rgba_p(self): - im = hopper('RGBA') - im.putalpha(hopper('L')) + im = hopper("RGBA") + im.putalpha(hopper("L")) - converted = im.convert('P') - comparable = converted.convert('RGBA') + converted = im.convert("P") + comparable = converted.convert("RGBA") self.assert_image_similar(im, comparable, 20) def test_trns_p(self): - im = hopper('P') - im.info['transparency'] = 0 + im = hopper("P") + im.info["transparency"] = 0 - f = self.tempfile('temp.png') + f = self.tempfile("temp.png") - im_l = im.convert('L') - self.assertEqual(im_l.info['transparency'], 0) # undone + im_l = im.convert("L") + self.assertEqual(im_l.info["transparency"], 0) # undone im_l.save(f) - im_rgb = im.convert('RGB') - self.assertEqual(im_rgb.info['transparency'], (0, 0, 0)) # undone + im_rgb = im.convert("RGB") + self.assertEqual(im_rgb.info["transparency"], (0, 0, 0)) # undone im_rgb.save(f) # ref https://github.com/python-pillow/Pillow/issues/664 def test_trns_p_rgba(self): # Arrange - im = hopper('P') - im.info['transparency'] = 128 + im = hopper("P") + im.info["transparency"] = 128 # Act - im_rgba = im.convert('RGBA') + im_rgba = im.convert("RGBA") # Assert - self.assertNotIn('transparency', im_rgba.info) + self.assertNotIn("transparency", im_rgba.info) # https://github.com/python-pillow/Pillow/issues/2702 self.assertIsNone(im_rgba.palette) def test_trns_l(self): - im = hopper('L') - im.info['transparency'] = 128 + im = hopper("L") + im.info["transparency"] = 128 - f = self.tempfile('temp.png') + f = self.tempfile("temp.png") - im_rgb = im.convert('RGB') - self.assertEqual(im_rgb.info['transparency'], - (128, 128, 128)) # undone + im_rgb = im.convert("RGB") + self.assertEqual(im_rgb.info["transparency"], (128, 128, 128)) # undone im_rgb.save(f) - im_p = im.convert('P') - self.assertIn('transparency', im_p.info) + im_p = im.convert("P") + self.assertIn("transparency", im_p.info) im_p.save(f) - im_p = self.assert_warning( - UserWarning, - im.convert, 'P', palette=Image.ADAPTIVE) - self.assertNotIn('transparency', im_p.info) + im_p = self.assert_warning(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) + self.assertNotIn("transparency", im_p.info) im_p.save(f) def test_trns_RGB(self): - im = hopper('RGB') - im.info['transparency'] = im.getpixel((0, 0)) + im = hopper("RGB") + im.info["transparency"] = im.getpixel((0, 0)) - f = self.tempfile('temp.png') + f = self.tempfile("temp.png") - im_l = im.convert('L') - self.assertEqual(im_l.info['transparency'], - im_l.getpixel((0, 0))) # undone + im_l = im.convert("L") + self.assertEqual(im_l.info["transparency"], im_l.getpixel((0, 0))) # undone im_l.save(f) - im_p = im.convert('P') - self.assertIn('transparency', im_p.info) + im_p = im.convert("P") + self.assertIn("transparency", im_p.info) im_p.save(f) - im_rgba = im.convert('RGBA') - self.assertNotIn('transparency', im_rgba.info) + im_rgba = im.convert("RGBA") + self.assertNotIn("transparency", im_rgba.info) im_rgba.save(f) - im_p = self.assert_warning( - UserWarning, - im.convert, 'P', palette=Image.ADAPTIVE) - self.assertNotIn('transparency', im_p.info) + im_p = self.assert_warning(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) + self.assertNotIn("transparency", im_p.info) im_p.save(f) def test_gif_with_rgba_palette_to_p(self): # See https://github.com/python-pillow/Pillow/issues/2433 - 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() def test_p_la(self): - im = hopper('RGBA') - alpha = hopper('L') + im = hopper("RGBA") + alpha = hopper("L") im.putalpha(alpha) - comparable = im.convert('P').convert('LA').getchannel('A') + comparable = im.convert("P").convert("LA").getchannel("A") self.assert_image_similar(alpha, comparable, 5) def test_matrix_illegal_conversion(self): # Arrange - im = hopper('CMYK') + im = hopper("CMYK") + # fmt: off matrix = ( 0.412453, 0.357580, 0.180423, 0, 0.212671, 0.715160, 0.072169, 0, 0.019334, 0.119193, 0.950227, 0) - self.assertNotEqual(im.mode, 'RGB') + # fmt: on + self.assertNotEqual(im.mode, "RGB") # Act / Assert - self.assertRaises(ValueError, - im.convert, mode='CMYK', matrix=matrix) + self.assertRaises(ValueError, im.convert, mode="CMYK", matrix=matrix) def test_matrix_wrong_mode(self): # Arrange - im = hopper('L') + im = hopper("L") + # fmt: off matrix = ( 0.412453, 0.357580, 0.180423, 0, 0.212671, 0.715160, 0.072169, 0, 0.019334, 0.119193, 0.950227, 0) - self.assertEqual(im.mode, 'L') + # fmt: on + self.assertEqual(im.mode, "L") # Act / Assert - self.assertRaises(ValueError, - im.convert, mode='L', matrix=matrix) + self.assertRaises(ValueError, im.convert, mode="L", matrix=matrix) def test_matrix_xyz(self): - def matrix_convert(mode): # Arrange - im = hopper('RGB') - im.info['transparency'] = (255, 0, 0) + im = hopper("RGB") + im.info["transparency"] = (255, 0, 0) + # fmt: off matrix = ( 0.412453, 0.357580, 0.180423, 0, 0.212671, 0.715160, 0.072169, 0, 0.019334, 0.119193, 0.950227, 0) - self.assertEqual(im.mode, 'RGB') + # fmt: on + self.assertEqual(im.mode, "RGB") # Act # Convert an RGB image to the CIE XYZ colour space @@ -201,31 +210,31 @@ 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': + 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)) + 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) + self.assert_image_similar(converted_im, target.getchannel(0), 1) + self.assertEqual(converted_im.info["transparency"], 105) - matrix_convert('RGB') - matrix_convert('L') + matrix_convert("RGB") + matrix_convert("L") def test_matrix_identity(self): # Arrange - im = hopper('RGB') + im = hopper("RGB") + # fmt: off identity_matrix = ( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0) - self.assertEqual(im.mode, 'RGB') + # fmt: on + self.assertEqual(im.mode, "RGB") # Act # Convert with an identity matrix - converted_im = im.convert(mode='RGB', matrix=identity_matrix) + converted_im = im.convert(mode="RGB", matrix=identity_matrix) # Assert # No change diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 19679fe22..653159e14 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,12 +1,11 @@ -from .helper import PillowTestCase, hopper +import copy from PIL import Image -import copy +from .helper import PillowTestCase, hopper class TestImageCopy(PillowTestCase): - def test_copy(self): croppedCoordinates = (10, 10, 20, 20) croppedSize = (10, 10) @@ -36,7 +35,7 @@ class TestImageCopy(PillowTestCase): self.assertEqual(out.size, croppedSize) def test_copy_zero(self): - im = Image.new('RGB', (0, 0)) + im = Image.new("RGB", (0, 0)) out = im.copy() self.assertEqual(out.mode, im.mode) self.assertEqual(out.size, im.size) diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 45123a631..6a604f494 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -1,10 +1,9 @@ -from .helper import PillowTestCase, hopper - from PIL import Image +from .helper import PillowTestCase, hopper + class TestImageCrop(PillowTestCase): - def test_crop(self): def crop(mode): im = hopper(mode) @@ -13,11 +12,11 @@ class TestImageCrop(PillowTestCase): cropped = im.crop((50, 50, 100, 100)) self.assertEqual(cropped.mode, mode) self.assertEqual(cropped.size, (50, 50)) + for mode in "1", "P", "L", "RGB", "I", "F": crop(mode) def test_wide_crop(self): - def crop(*bbox): i = im.crop(bbox) h = i.histogram() @@ -74,7 +73,7 @@ class TestImageCrop(PillowTestCase): # apparently a use after free on windows, see # https://github.com/python-pillow/Pillow/issues/1077 - test_img = 'Tests/images/bmp/g/pal8-0.bmp' + test_img = "Tests/images/bmp/g/pal8-0.bmp" extents = (1, 1, 10, 10) # works prepatch img = Image.open(test_img) @@ -88,7 +87,7 @@ class TestImageCrop(PillowTestCase): def test_crop_zero(self): - im = Image.new('RGB', (0, 0), 'white') + im = Image.new("RGB", (0, 0), "white") cropped = im.crop((0, 0, 0, 0)) self.assertEqual(cropped.size, (0, 0)) @@ -97,7 +96,7 @@ class TestImageCrop(PillowTestCase): self.assertEqual(cropped.size, (10, 10)) self.assertEqual(cropped.getdata()[0], (0, 0, 0)) - im = Image.new('RGB', (0, 0)) + im = Image.new("RGB", (0, 0)) cropped = im.crop((10, 10, 20, 20)) self.assertEqual(cropped.size, (10, 10)) diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 9ebd68945..5d92ee797 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -1,7 +1,7 @@ -from .helper import PillowTestCase, fromstring, tostring - from PIL import Image +from .helper import PillowTestCase, fromstring, tostring + class TestImageDraft(PillowTestCase): def setUp(self): @@ -11,7 +11,7 @@ class TestImageDraft(PillowTestCase): def draft_roundtrip(self, in_mode, in_size, req_mode, req_size): im = Image.new(in_mode, in_size) - data = tostring(im, 'JPEG') + data = tostring(im, "JPEG") im = fromstring(data) im.draft(req_mode, req_size) return im @@ -23,7 +23,6 @@ class TestImageDraft(PillowTestCase): ((128, 128), (64, 64), (64, 64)), ((128, 128), (32, 32), (32, 32)), ((128, 128), (16, 16), (16, 16)), - # large requested width ((435, 361), (218, 128), (435, 361)), # almost 2x ((435, 361), (217, 128), (218, 181)), # more than 2x @@ -32,7 +31,6 @@ class TestImageDraft(PillowTestCase): ((435, 361), (55, 32), (109, 91)), # almost 8x ((435, 361), (54, 32), (55, 46)), # more than 8x ((435, 361), (27, 16), (55, 46)), # more than 16x - # and vice versa ((435, 361), (128, 181), (435, 361)), # almost 2x ((435, 361), (128, 180), (218, 181)), # more than 2x @@ -42,7 +40,7 @@ class TestImageDraft(PillowTestCase): ((435, 361), (32, 45), (55, 46)), # more than 8x ((435, 361), (16, 22), (55, 46)), # more than 16x ]: - im = self.draft_roundtrip('L', in_size, None, req_size) + im = self.draft_roundtrip("L", in_size, None, req_size) im.load() self.assertEqual(im.size, out_size) @@ -66,6 +64,6 @@ class TestImageDraft(PillowTestCase): self.assertEqual(im.mode, out_mode) def test_several_drafts(self): - im = self.draft_roundtrip('L', (128, 128), None, (64, 64)) + im = self.draft_roundtrip("L", (128, 128), None, (64, 64)) im.draft(None, (64, 64)) im.load() diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py new file mode 100644 index 000000000..bc792bca6 --- /dev/null +++ b/Tests/test_image_entropy.py @@ -0,0 +1,17 @@ +from .helper import PillowTestCase, hopper + + +class TestImageEntropy(PillowTestCase): + def test_entropy(self): + def entropy(mode): + return hopper(mode).entropy() + + self.assertAlmostEqual(entropy("1"), 0.9138803254693582) + self.assertAlmostEqual(entropy("L"), 7.06650513081286) + self.assertAlmostEqual(entropy("I"), 7.06650513081286) + self.assertAlmostEqual(entropy("F"), 7.06650513081286) + self.assertAlmostEqual(entropy("P"), 5.0530452472519745) + self.assertAlmostEqual(entropy("RGB"), 8.821286587714319) + self.assertAlmostEqual(entropy("RGBA"), 7.42724306524488) + self.assertAlmostEqual(entropy("CMYK"), 7.4272430652448795) + self.assertAlmostEqual(entropy("YCbCr"), 7.698360534903628) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index a91387f81..bbd10e6e5 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,12 +1,10 @@ -from .helper import PillowTestCase, hopper - from PIL import Image, ImageFilter +from .helper import PillowTestCase, hopper + class TestImageFilter(PillowTestCase): - def test_sanity(self): - def filter(filter): for mode in ["L", "RGB", "CMYK"]: im = hopper(mode) @@ -49,7 +47,6 @@ class TestImageFilter(PillowTestCase): im.filter(ImageFilter.SMOOTH) def test_modefilter(self): - def modefilter(mode): im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) @@ -68,7 +65,6 @@ class TestImageFilter(PillowTestCase): self.assertEqual(modefilter("RGB"), ((4, 0, 0), (0, 0, 0))) def test_rankfilter(self): - def rankfilter(mode): im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) @@ -100,39 +96,48 @@ class TestImageFilter(PillowTestCase): self.assertRaises(ValueError, builtinFilter.filter, hopper("P")) def test_kernel_not_enough_coefficients(self): - self.assertRaises(ValueError, - lambda: ImageFilter.Kernel((3, 3), (0, 0))) + 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((3, 3), # noqa: E127 - (-1, -1, 0, - -1, 0, 1, - 0, 1, 1), .3) + 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']: + 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)]), + 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((5, 5), # noqa: E127 - (-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), 0.3) + 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']: + 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)]), + Image.merge(mode, source[: len(mode)]).filter(kernel), + Image.merge(mode, reference[: len(mode)]), ) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 3b224e71a..21d466d02 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,10 +1,9 @@ -from .helper import PillowTestCase, hopper - from PIL import Image +from .helper import PillowTestCase, hopper + class TestImageFromBytes(PillowTestCase): - def test_sanity(self): im1 = hopper() im2 = Image.frombytes(im1.mode, im1.size, im1.tobytes()) diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index 8a29a2715..0d961572c 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -1,16 +1,19 @@ +from PIL import Image, ImageQt + from .helper import PillowTestCase, hopper from .test_imageqt import PillowQtTestCase -from PIL import ImageQt, Image - class TestFromQImage(PillowQtTestCase, PillowTestCase): - - files_to_test = [ - hopper(), - Image.open('Tests/images/transparent.png'), - Image.open('Tests/images/7x13.png'), - ] + def setUp(self): + super().setUp() + self.files_to_test = [ + hopper(), + Image.open("Tests/images/transparent.png"), + Image.open("Tests/images/7x13.png"), + ] + for im in self.files_to_test: + self.addCleanup(im.close) def roundtrip(self, expected): # PIL -> Qt @@ -19,26 +22,26 @@ class TestFromQImage(PillowQtTestCase, PillowTestCase): result = ImageQt.fromqimage(intermediate) if intermediate.hasAlphaChannel(): - self.assert_image_equal(result, expected.convert('RGBA')) + self.assert_image_equal(result, expected.convert("RGBA")) else: - self.assert_image_equal(result, expected.convert('RGB')) + self.assert_image_equal(result, expected.convert("RGB")) def test_sanity_1(self): for im in self.files_to_test: - self.roundtrip(im.convert('1')) + self.roundtrip(im.convert("1")) def test_sanity_rgb(self): for im in self.files_to_test: - self.roundtrip(im.convert('RGB')) + self.roundtrip(im.convert("RGB")) def test_sanity_rgba(self): for im in self.files_to_test: - self.roundtrip(im.convert('RGBA')) + self.roundtrip(im.convert("RGBA")) def test_sanity_l(self): for im in self.files_to_test: - self.roundtrip(im.convert('L')) + self.roundtrip(im.convert("L")) def test_sanity_p(self): for im in self.files_to_test: - self.roundtrip(im.convert('P')) + self.roundtrip(im.convert("P")) diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index 6d79bf280..785b2ae42 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -1,10 +1,9 @@ -from .helper import PillowTestCase - from PIL import Image +from .helper import PillowTestCase + class TestImageGetBands(PillowTestCase): - def test_getbands(self): self.assertEqual(Image.new("1", (1, 1)).getbands(), ("1",)) self.assertEqual(Image.new("L", (1, 1)).getbands(), ("L",)) @@ -12,9 +11,6 @@ class TestImageGetBands(PillowTestCase): self.assertEqual(Image.new("F", (1, 1)).getbands(), ("F",)) self.assertEqual(Image.new("P", (1, 1)).getbands(), ("P",)) self.assertEqual(Image.new("RGB", (1, 1)).getbands(), ("R", "G", "B")) - self.assertEqual( - Image.new("RGBA", (1, 1)).getbands(), ("R", "G", "B", "A")) - self.assertEqual( - Image.new("CMYK", (1, 1)).getbands(), ("C", "M", "Y", "K")) - self.assertEqual( - Image.new("YCbCr", (1, 1)).getbands(), ("Y", "Cb", "Cr")) + self.assertEqual(Image.new("RGBA", (1, 1)).getbands(), ("R", "G", "B", "A")) + self.assertEqual(Image.new("CMYK", (1, 1)).getbands(), ("C", "M", "Y", "K")) + self.assertEqual(Image.new("YCbCr", (1, 1)).getbands(), ("Y", "Cb", "Cr")) diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index 9bf39752b..2df9f20f1 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,10 +1,9 @@ -from .helper import PillowTestCase, hopper - from PIL import Image +from .helper import PillowTestCase, hopper + class TestImageGetBbox(PillowTestCase): - def test_sanity(self): bbox = hopper().getbbox() diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index aa2a40976..f1abf0287 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -2,9 +2,7 @@ from .helper import PillowTestCase, hopper class TestImageGetColors(PillowTestCase): - def test_getcolors(self): - def getcolors(mode, limit=None): im = hopper(mode) if limit: @@ -43,9 +41,11 @@ class TestImageGetColors(PillowTestCase): im = hopper().quantize(3).convert("RGB") - expected = [(4039, (172, 166, 181)), - (4385, (124, 113, 134)), - (7960, (31, 20, 33))] + expected = [ + (4039, (172, 166, 181)), + (4385, (124, 113, 134)), + (7960, (31, 20, 33)), + ] A = im.getcolors(maxcolors=2) self.assertIsNone(A) diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index fe8f9adcd..d9bfcc7dd 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -2,7 +2,6 @@ from .helper import PillowTestCase, hopper class TestImageGetData(PillowTestCase): - def test_sanity(self): data = hopper().getdata() @@ -13,7 +12,6 @@ class TestImageGetData(PillowTestCase): self.assertEqual(data[0], (20, 20, 70)) def test_roundtrip(self): - def getdata(mode): im = hopper(mode).resize((32, 30)) data = im.getdata() diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 1689744af..1944b041c 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -1,11 +1,10 @@ from PIL import Image + from .helper import PillowTestCase, hopper class TestImageGetExtrema(PillowTestCase): - def test_extrema(self): - def extrema(mode): return hopper(mode).getextrema() @@ -14,16 +13,13 @@ class TestImageGetExtrema(PillowTestCase): self.assertEqual(extrema("I"), (0, 255)) self.assertEqual(extrema("F"), (0, 255)) self.assertEqual(extrema("P"), (0, 225)) # fixed palette - self.assertEqual( - extrema("RGB"), ((0, 255), (0, 255), (0, 255))) - self.assertEqual( - extrema("RGBA"), ((0, 255), (0, 255), (0, 255), (255, 255))) - self.assertEqual( - extrema("CMYK"), ((0, 255), (0, 255), (0, 255), (0, 0))) + self.assertEqual(extrema("RGB"), ((0, 255), (0, 255), (0, 255))) + self.assertEqual(extrema("RGBA"), ((0, 255), (0, 255), (0, 255), (255, 255))) + self.assertEqual(extrema("CMYK"), ((0, 255), (0, 255), (0, 255), (0, 0))) self.assertEqual(extrema("I;16"), (0, 255)) def test_true_16(self): im = Image.open("Tests/images/16_bit_noise.tif") - self.assertEqual(im.mode, 'I;16') + self.assertEqual(im.mode, "I;16") extrema = im.getextrema() self.assertEqual(extrema, (106, 285)) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 6d3682caf..f6908af4b 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,14 +1,10 @@ from .helper import PillowTestCase, hopper -from PIL._util import py3 class TestImageGetIm(PillowTestCase): - def test_sanity(self): im = hopper() type_repr = repr(type(im.getim())) - if py3: - self.assertIn("PyCapsule", type_repr) - + self.assertIn("PyCapsule", type_repr) self.assertIsInstance(im.im.id, int) diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 98f8142dc..7beeeff58 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -2,13 +2,13 @@ from .helper import PillowTestCase, hopper class TestImageGetPalette(PillowTestCase): - def test_palette(self): def palette(mode): p = hopper(mode).getpalette() if p: return p[:10] return None + self.assertIsNone(palette("1")) self.assertIsNone(palette("L")) self.assertIsNone(palette("I")) diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index 85d40e859..3b8bca64f 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -1,10 +1,9 @@ -from .helper import PillowTestCase, hopper - from PIL import Image +from .helper import PillowTestCase, hopper + class TestImageGetProjection(PillowTestCase): - def test_sanity(self): im = hopper() diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 0a023cd69..8d34658b8 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -2,9 +2,7 @@ from .helper import PillowTestCase, hopper class TestImageHistogram(PillowTestCase): - def test_histogram(self): - def histogram(mode): h = hopper(mode).histogram() return len(h), min(h), max(h) diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index d8f323eb8..2770126c4 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,12 +1,11 @@ -from .helper import PillowTestCase, hopper +import os from PIL import Image -import os +from .helper import PillowTestCase, hopper class TestImageLoad(PillowTestCase): - def test_sanity(self): im = hopper() @@ -28,3 +27,10 @@ class TestImageLoad(PillowTestCase): os.fstat(fn) self.assertRaises(OSError, os.fstat, fn) + + def test_contextmanager_non_exclusive_fp(self): + with open("Tests/images/hopper.gif", "rb") as fp: + with Image.open(fp): + pass + + self.assertFalse(fp.closed) diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 26266611d..1d41d8609 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,14 +1,13 @@ -from .helper import PillowTestCase, hopper - from PIL import Image +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 @@ -26,6 +25,23 @@ class TestImageMode(PillowTestCase): self.assertEqual(m.basemode, "L") self.assertEqual(m.basetype, "L") + for mode in ( + "I;16", + "I;16S", + "I;16L", + "I;16LS", + "I;16B", + "I;16BS", + "I;16N", + "I;16NS", + ): + m = ImageMode.getmode(mode) + self.assertEqual(m.mode, mode) + self.assertEqual(str(m), mode) + self.assertEqual(m.bands, ("I",)) + self.assertEqual(m.basemode, "L") + self.assertEqual(m.basetype, "L") + m = ImageMode.getmode("RGB") self.assertEqual(m.mode, "RGB") self.assertEqual(str(m), "RGB") @@ -36,13 +52,16 @@ class TestImageMode(PillowTestCase): def test_properties(self): def check(mode, *result): signature = ( - Image.getmodebase(mode), Image.getmodetype(mode), - Image.getmodebands(mode), Image.getmodebandnames(mode), - ) + Image.getmodebase(mode), + Image.getmodetype(mode), + Image.getmodebands(mode), + Image.getmodebandnames(mode), + ) self.assertEqual(signature, result) + check("1", "L", "L", 1, ("1",)) check("L", "L", "L", 1, ("L",)) - check("P", "RGB", "L", 1, ("P",)) + check("P", "P", "L", 1, ("P",)) check("I", "L", "I", 1, ("I",)) check("F", "L", "F", 1, ("F",)) check("RGB", "RGB", "L", 3, ("R", "G", "B")) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 5586e8618..3139db664 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,7 +1,7 @@ -from .helper import PillowTestCase, cached_property - from PIL import Image +from .helper import PillowTestCase, cached_property + class TestImagingPaste(PillowTestCase): masks = {} @@ -9,10 +9,7 @@ class TestImagingPaste(PillowTestCase): def assert_9points_image(self, im, expected): expected = [ - point[0] - if im.mode == 'L' else - point[:len(im.mode)] - for point in expected + point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected ] px = im.load() actual = [ @@ -39,7 +36,7 @@ class TestImagingPaste(PillowTestCase): @cached_property def mask_1(self): - mask = Image.new('1', (self.size, self.size)) + mask = Image.new("1", (self.size, self.size)) px = mask.load() for y in range(mask.height): for x in range(mask.width): @@ -52,7 +49,7 @@ class TestImagingPaste(PillowTestCase): @cached_property def gradient_L(self): - gradient = Image.new('L', (self.size, self.size)) + gradient = Image.new("L", (self.size, self.size)) px = gradient.load() for y in range(gradient.height): for x in range(gradient.width): @@ -61,34 +58,43 @@ class TestImagingPaste(PillowTestCase): @cached_property def gradient_RGB(self): - return Image.merge('RGB', [ - self.gradient_L, - self.gradient_L.transpose(Image.ROTATE_90), - self.gradient_L.transpose(Image.ROTATE_180), - ]) + return Image.merge( + "RGB", + [ + self.gradient_L, + self.gradient_L.transpose(Image.ROTATE_90), + self.gradient_L.transpose(Image.ROTATE_180), + ], + ) @cached_property def gradient_RGBA(self): - return Image.merge('RGBA', [ - self.gradient_L, - self.gradient_L.transpose(Image.ROTATE_90), - self.gradient_L.transpose(Image.ROTATE_180), - self.gradient_L.transpose(Image.ROTATE_270), - ]) + return Image.merge( + "RGBA", + [ + self.gradient_L, + self.gradient_L.transpose(Image.ROTATE_90), + self.gradient_L.transpose(Image.ROTATE_180), + self.gradient_L.transpose(Image.ROTATE_270), + ], + ) @cached_property def gradient_RGBa(self): - return Image.merge('RGBa', [ - self.gradient_L, - self.gradient_L.transpose(Image.ROTATE_90), - self.gradient_L.transpose(Image.ROTATE_180), - self.gradient_L.transpose(Image.ROTATE_270), - ]) + return Image.merge( + "RGBa", + [ + self.gradient_L, + self.gradient_L.transpose(Image.ROTATE_90), + self.gradient_L.transpose(Image.ROTATE_180), + self.gradient_L.transpose(Image.ROTATE_270), + ], + ) def test_image_solid(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), 'red') - im2 = getattr(self, 'gradient_' + mode) + for mode in ("RGBA", "RGB", "L"): + im = Image.new(mode, (200, 200), "red") + im2 = getattr(self, "gradient_" + mode) im.paste(im2, (12, 23)) @@ -96,79 +102,99 @@ class TestImagingPaste(PillowTestCase): self.assert_image_equal(im, im2) def test_image_mask_1(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), 'white') - im2 = getattr(self, 'gradient_' + mode) + for mode in ("RGBA", "RGB", "L"): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) - self.assert_9points_paste(im, im2, self.mask_1, [ - (255, 255, 255, 255), - (255, 255, 255, 255), - (127, 254, 127, 0), - (255, 255, 255, 255), - (255, 255, 255, 255), - (191, 190, 63, 64), - (127, 0, 127, 254), - (191, 64, 63, 190), - (255, 255, 255, 255), - ]) + self.assert_9points_paste( + im, + im2, + self.mask_1, + [ + (255, 255, 255, 255), + (255, 255, 255, 255), + (127, 254, 127, 0), + (255, 255, 255, 255), + (255, 255, 255, 255), + (191, 190, 63, 64), + (127, 0, 127, 254), + (191, 64, 63, 190), + (255, 255, 255, 255), + ], + ) def test_image_mask_L(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), 'white') - im2 = getattr(self, 'gradient_' + mode) + for mode in ("RGBA", "RGB", "L"): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) - self.assert_9points_paste(im, im2, self.mask_L, [ - (128, 191, 255, 191), - (208, 239, 239, 208), - (255, 255, 255, 255), - (112, 111, 206, 207), - (192, 191, 191, 191), - (239, 239, 207, 207), - (128, 1, 128, 254), - (207, 113, 112, 207), - (255, 191, 128, 191), - ]) + self.assert_9points_paste( + im, + im2, + self.mask_L, + [ + (128, 191, 255, 191), + (208, 239, 239, 208), + (255, 255, 255, 255), + (112, 111, 206, 207), + (192, 191, 191, 191), + (239, 239, 207, 207), + (128, 1, 128, 254), + (207, 113, 112, 207), + (255, 191, 128, 191), + ], + ) def test_image_mask_RGBA(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), 'white') - im2 = getattr(self, 'gradient_' + mode) + for mode in ("RGBA", "RGB", "L"): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) - self.assert_9points_paste(im, im2, self.gradient_RGBA, [ - (128, 191, 255, 191), - (208, 239, 239, 208), - (255, 255, 255, 255), - (112, 111, 206, 207), - (192, 191, 191, 191), - (239, 239, 207, 207), - (128, 1, 128, 254), - (207, 113, 112, 207), - (255, 191, 128, 191), - ]) + self.assert_9points_paste( + im, + im2, + self.gradient_RGBA, + [ + (128, 191, 255, 191), + (208, 239, 239, 208), + (255, 255, 255, 255), + (112, 111, 206, 207), + (192, 191, 191, 191), + (239, 239, 207, 207), + (128, 1, 128, 254), + (207, 113, 112, 207), + (255, 191, 128, 191), + ], + ) def test_image_mask_RGBa(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), 'white') - im2 = getattr(self, 'gradient_' + mode) + for mode in ("RGBA", "RGB", "L"): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) - self.assert_9points_paste(im, im2, self.gradient_RGBa, [ - (128, 255, 126, 255), - (0, 127, 126, 255), - (126, 253, 126, 255), - (128, 127, 254, 255), - (0, 255, 254, 255), - (126, 125, 254, 255), - (128, 1, 128, 255), - (0, 129, 128, 255), - (126, 255, 128, 255), - ]) + self.assert_9points_paste( + im, + im2, + self.gradient_RGBa, + [ + (128, 255, 126, 255), + (0, 127, 126, 255), + (126, 253, 126, 255), + (128, 127, 254, 255), + (0, 255, 254, 255), + (126, 125, 254, 255), + (128, 1, 128, 255), + (0, 129, 128, 255), + (126, 255, 128, 255), + ], + ) def test_color_solid(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), 'black') + for mode in ("RGBA", "RGB", "L"): + im = Image.new(mode, (200, 200), "black") rect = (12, 23, 128 + 12, 128 + 23) - im.paste('white', rect) + im.paste("white", rect) hist = im.crop(rect).histogram() while hist: @@ -177,76 +203,96 @@ class TestImagingPaste(PillowTestCase): self.assertEqual(sum(head[:255]), 0) def test_color_mask_1(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), (50, 60, 70, 80)[:len(mode)]) - color = (10, 20, 30, 40)[:len(mode)] + for mode in ("RGBA", "RGB", "L"): + im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) + color = (10, 20, 30, 40)[: len(mode)] - self.assert_9points_paste(im, color, self.mask_1, [ - (50, 60, 70, 80), - (50, 60, 70, 80), - (10, 20, 30, 40), - (50, 60, 70, 80), - (50, 60, 70, 80), - (10, 20, 30, 40), - (10, 20, 30, 40), - (10, 20, 30, 40), - (50, 60, 70, 80), - ]) + self.assert_9points_paste( + im, + color, + self.mask_1, + [ + (50, 60, 70, 80), + (50, 60, 70, 80), + (10, 20, 30, 40), + (50, 60, 70, 80), + (50, 60, 70, 80), + (10, 20, 30, 40), + (10, 20, 30, 40), + (10, 20, 30, 40), + (50, 60, 70, 80), + ], + ) def test_color_mask_L(self): - for mode in ('RGBA', 'RGB', 'L'): - im = getattr(self, 'gradient_' + mode).copy() - color = 'white' + for mode in ("RGBA", "RGB", "L"): + im = getattr(self, "gradient_" + mode).copy() + color = "white" - self.assert_9points_paste(im, color, self.mask_L, [ - (127, 191, 254, 191), - (111, 207, 206, 110), - (127, 254, 127, 0), - (207, 207, 239, 239), - (191, 191, 190, 191), - (207, 206, 111, 112), - (254, 254, 254, 255), - (239, 206, 206, 238), - (254, 191, 127, 191), - ]) + self.assert_9points_paste( + im, + color, + self.mask_L, + [ + (127, 191, 254, 191), + (111, 207, 206, 110), + (127, 254, 127, 0), + (207, 207, 239, 239), + (191, 191, 190, 191), + (207, 206, 111, 112), + (254, 254, 254, 255), + (239, 206, 206, 238), + (254, 191, 127, 191), + ], + ) def test_color_mask_RGBA(self): - for mode in ('RGBA', 'RGB', 'L'): - im = getattr(self, 'gradient_' + mode).copy() - color = 'white' + for mode in ("RGBA", "RGB", "L"): + im = getattr(self, "gradient_" + mode).copy() + color = "white" - self.assert_9points_paste(im, color, self.gradient_RGBA, [ - (127, 191, 254, 191), - (111, 207, 206, 110), - (127, 254, 127, 0), - (207, 207, 239, 239), - (191, 191, 190, 191), - (207, 206, 111, 112), - (254, 254, 254, 255), - (239, 206, 206, 238), - (254, 191, 127, 191), - ]) + self.assert_9points_paste( + im, + color, + self.gradient_RGBA, + [ + (127, 191, 254, 191), + (111, 207, 206, 110), + (127, 254, 127, 0), + (207, 207, 239, 239), + (191, 191, 190, 191), + (207, 206, 111, 112), + (254, 254, 254, 255), + (239, 206, 206, 238), + (254, 191, 127, 191), + ], + ) def test_color_mask_RGBa(self): - for mode in ('RGBA', 'RGB', 'L'): - im = getattr(self, 'gradient_' + mode).copy() - color = 'white' + for mode in ("RGBA", "RGB", "L"): + im = getattr(self, "gradient_" + mode).copy() + color = "white" - self.assert_9points_paste(im, color, self.gradient_RGBa, [ - (255, 63, 126, 63), - (47, 143, 142, 46), - (126, 253, 126, 255), - (15, 15, 47, 47), - (63, 63, 62, 63), - (142, 141, 46, 47), - (255, 255, 255, 0), - (48, 15, 15, 47), - (126, 63, 255, 63) - ]) + self.assert_9points_paste( + im, + color, + self.gradient_RGBa, + [ + (255, 63, 126, 63), + (47, 143, 142, 46), + (126, 253, 126, 255), + (15, 15, 47, 47), + (63, 63, 62, 63), + (142, 141, 46, 47), + (255, 255, 255, 0), + (48, 15, 15, 47), + (126, 63, 255, 63), + ], + ) def test_different_sizes(self): - im = Image.new('RGB', (100, 100)) - im2 = Image.new('RGB', (50, 50)) + im = Image.new("RGB", (100, 100)) + im2 = Image.new("RGB", (50, 50)) im.copy().paste(im2) im.copy().paste(im2, (0, 0)) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 90498fcff..56ed46488 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -2,39 +2,38 @@ from .helper import PillowTestCase, hopper class TestImagePoint(PillowTestCase): - def test_sanity(self): im = hopper() self.assertRaises(ValueError, im.point, list(range(256))) - im.point(list(range(256))*3) + im.point(list(range(256)) * 3) im.point(lambda x: x) im = im.convert("I") self.assertRaises(ValueError, im.point, list(range(256))) - im.point(lambda x: x*1) - im.point(lambda x: x+1) - im.point(lambda x: x*1+1) - self.assertRaises(TypeError, im.point, lambda x: x-1) - self.assertRaises(TypeError, im.point, lambda x: x/1) + im.point(lambda x: x * 1) + im.point(lambda x: x + 1) + im.point(lambda x: x * 1 + 1) + self.assertRaises(TypeError, im.point, lambda x: x - 1) + self.assertRaises(TypeError, im.point, lambda x: x / 1) def test_16bit_lut(self): """ Tests for 16 bit -> 8 bit lut for converting I->L images see https://github.com/python-pillow/Pillow/issues/440 """ im = hopper("I") - im.point(list(range(256))*256, 'L') + im.point(list(range(256)) * 256, "L") def test_f_lut(self): """ Tests for floating point lut of 8bit gray image """ - im = hopper('L') + im = hopper("L") lut = [0.5 * float(x) for x in range(256)] - out = im.point(lut, 'F') + out = im.point(lut, "F") - int_lut = [x//2 for x in range(256)] - self.assert_image_equal(out.convert('L'), im.point(int_lut, 'L')) + int_lut = [x // 2 for x in range(256)] + self.assert_image_equal(out.convert("L"), im.point(int_lut, "L")) def test_f_mode(self): - im = hopper('F') + im = hopper("F") self.assertRaises(ValueError, im.point, None) diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index 7b66b8833..6dc802598 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -1,10 +1,9 @@ -from .helper import PillowTestCase - from PIL import Image +from .helper import PillowTestCase + class TestImagePutAlpha(PillowTestCase): - def test_interface(self): im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) @@ -25,14 +24,21 @@ class TestImagePutAlpha(PillowTestCase): self.assertEqual(im.getpixel((0, 0)), 1) im.putalpha(2) - self.assertEqual(im.mode, 'LA') + self.assertEqual(im.mode, "LA") + self.assertEqual(im.getpixel((0, 0)), (1, 2)) + + im = Image.new("P", (1, 1), 1) + self.assertEqual(im.getpixel((0, 0)), 1) + + im.putalpha(2) + self.assertEqual(im.mode, "PA") self.assertEqual(im.getpixel((0, 0)), (1, 2)) im = Image.new("RGB", (1, 1), (1, 2, 3)) self.assertEqual(im.getpixel((0, 0)), (1, 2, 3)) im.putalpha(4) - self.assertEqual(im.mode, 'RGBA') + self.assertEqual(im.mode, "RGBA") self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) def test_readonly(self): @@ -42,5 +48,5 @@ class TestImagePutAlpha(PillowTestCase): im.putalpha(4) self.assertFalse(im.readonly) - self.assertEqual(im.mode, 'RGBA') + self.assertEqual(im.mode, "RGBA") self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 1b57e38b7..a213fbf88 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,13 +1,12 @@ -from .helper import PillowTestCase, hopper -from array import array - import sys +from array import array from PIL import Image +from .helper import PillowTestCase, hopper + class TestImagePutData(PillowTestCase): - def test_sanity(self): im1 = hopper() @@ -33,32 +32,33 @@ class TestImagePutData(PillowTestCase): im = Image.new("RGBA", (1, 1)) im.putdata([value]) return im.getpixel((0, 0)) + self.assertEqual(put(0xFFFFFFFF), (255, 255, 255, 255)) self.assertEqual(put(0xFFFFFFFF), (255, 255, 255, 255)) self.assertEqual(put(-1), (255, 255, 255, 255)) self.assertEqual(put(-1), (255, 255, 255, 255)) - if sys.maxsize > 2**32: + if sys.maxsize > 2 ** 32: self.assertEqual(put(sys.maxsize), (255, 255, 255, 255)) else: self.assertEqual(put(sys.maxsize), (255, 255, 255, 127)) def test_pypy_performance(self): - im = Image.new('L', (256, 256)) - im.putdata(list(range(256))*256) + im = Image.new("L", (256, 256)) + im.putdata(list(range(256)) * 256) def test_mode_i(self): - src = hopper('L') + src = hopper("L") data = list(src.getdata()) - im = Image.new('I', src.size, 0) + im = Image.new("I", src.size, 0) im.putdata(data, 2, 256) target = [2 * elt + 256 for elt in data] self.assertEqual(list(im.getdata()), target) def test_mode_F(self): - src = hopper('L') + src = hopper("L") data = list(src.getdata()) - im = Image.new('F', src.size, 0) + im = Image.new("F", src.size, 0) im.putdata(data, 2.0, 256.0) target = [2.0 * float(elt) + 256.0 for elt in data] @@ -68,8 +68,8 @@ class TestImagePutData(PillowTestCase): # shouldn't segfault # see https://github.com/python-pillow/Pillow/issues/1008 - arr = array('B', [0])*15000 - im = Image.new('L', (150, 100)) + arr = array("B", [0]) * 15000 + im = Image.new("L", (150, 100)) im.putdata(arr) self.assertEqual(len(im.getdata()), len(arr)) @@ -78,8 +78,8 @@ class TestImagePutData(PillowTestCase): # shouldn't segfault # see https://github.com/python-pillow/Pillow/issues/1008 - im = Image.new('F', (150, 100)) - arr = array('f', [0.0])*15000 + im = Image.new("F", (150, 100)) + arr = array("f", [0.0]) * 15000 im.putdata(arr) self.assertEqual(len(im.getdata()), len(arr)) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 34b585f55..68cfc4efe 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,21 +1,24 @@ -from .helper import PillowTestCase, hopper - from PIL import ImagePalette +from .helper import PillowTestCase, hopper + class TestImagePutPalette(PillowTestCase): - def test_putpalette(self): def palette(mode): im = hopper(mode).copy() - im.putpalette(list(range(256))*3) + im.putpalette(list(range(256)) * 3) p = im.getpalette() if p: return im.mode, p[:10] return im.mode + self.assertRaises(ValueError, palette, "1") - self.assertEqual(palette("L"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) - self.assertEqual(palette("P"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) + for mode in ["L", "LA", "P", "PA"]: + self.assertEqual( + palette(mode), + ("PA" if "A" in mode else "P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), + ) self.assertRaises(ValueError, palette, "I") self.assertRaises(ValueError, palette, "F") self.assertRaises(ValueError, palette, "RGB") diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 2f0b65758..2be5b74fc 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,48 +1,64 @@ -from .helper import PillowTestCase, hopper - from PIL import Image +from .helper import PillowTestCase, hopper + class TestImageQuantize(PillowTestCase): - def test_sanity(self): image = hopper() converted = image.quantize() - self.assert_image(converted, 'P', converted.size) - self.assert_image_similar(converted.convert('RGB'), image, 10) + self.assert_image(converted, "P", converted.size) + self.assert_image_similar(converted.convert("RGB"), image, 10) image = hopper() - converted = image.quantize(palette=hopper('P')) - self.assert_image(converted, 'P', converted.size) - self.assert_image_similar(converted.convert('RGB'), image, 60) + converted = image.quantize(palette=hopper("P")) + self.assert_image(converted, "P", converted.size) + self.assert_image_similar(converted.convert("RGB"), image, 60) def test_libimagequant_quantize(self): image = hopper() try: converted = image.quantize(100, Image.LIBIMAGEQUANT) except ValueError as ex: - if 'dependency' in str(ex).lower(): - self.skipTest('libimagequant support not available') + if "dependency" in str(ex).lower(): + self.skipTest("libimagequant support not available") else: raise - self.assert_image(converted, 'P', converted.size) - self.assert_image_similar(converted.convert('RGB'), image, 15) + self.assert_image(converted, "P", converted.size) + self.assert_image_similar(converted.convert("RGB"), image, 15) self.assertEqual(len(converted.getcolors()), 100) def test_octree_quantize(self): image = hopper() converted = image.quantize(100, Image.FASTOCTREE) - self.assert_image(converted, 'P', converted.size) - self.assert_image_similar(converted.convert('RGB'), image, 20) + self.assert_image(converted, "P", converted.size) + self.assert_image_similar(converted.convert("RGB"), image, 20) self.assertEqual(len(converted.getcolors()), 100) def test_rgba_quantize(self): - image = hopper('RGBA') - image.quantize() + image = hopper("RGBA") self.assertRaises(ValueError, image.quantize, method=0) + self.assertEqual(image.quantize().convert().mode, "RGBA") + def test_quantize(self): - image = Image.open('Tests/images/caption_6_33_22.png').convert('RGB') + image = Image.open("Tests/images/caption_6_33_22.png").convert("RGB") converted = image.quantize() - self.assert_image(converted, 'P', converted.size) - self.assert_image_similar(converted.convert('RGB'), image, 1) + self.assert_image(converted, "P", converted.size) + self.assert_image_similar(converted.convert("RGB"), image, 1) + + def test_quantize_no_dither(self): + image = hopper() + palette = Image.open("Tests/images/caption_6_33_22.png").convert("P") + + converted = image.quantize(dither=0, palette=palette) + self.assert_image(converted, "P", converted.size) + + def test_quantize_dither_diff(self): + image = hopper() + palette = Image.open("Tests/images/caption_6_33_22.png").convert("P") + + dither = image.quantize(dither=1, palette=palette) + nodither = image.quantize(dither=0, palette=palette) + + self.assertNotEqual(dither.tobytes(), nodither.tobytes()) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 9ab8280ed..00c8fa5f2 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,20 +1,24 @@ -from __future__ import division, print_function - +import unittest from contextlib import contextmanager -from .helper import unittest, PillowTestCase, hopper from PIL import Image, ImageDraw +from .helper import PillowTestCase, hopper + class TestImagingResampleVulnerability(PillowTestCase): # see https://github.com/python-pillow/Pillow/issues/1710 def test_overflow(self): - im = hopper('L') - xsize = 0x100000008 // 4 - ysize = 1000 # unimportant - with self.assertRaises(MemoryError): - # any resampling filter will do here - im.im.resize((xsize, ysize), Image.BILINEAR) + im = hopper("L") + 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() @@ -29,11 +33,11 @@ class TestImagingResampleVulnerability(PillowTestCase): im.resize((100, -100)) def test_modify_after_resizing(self): - im = hopper('RGB') + im = hopper("RGB") # get copy with same size copy = im.resize(im.size) # some in-place operation - copy.paste('black', (0, 0, im.width // 2, im.height // 2)) + copy.paste("black", (0, 0, im.width // 2, im.height // 2)) # image should be different self.assertNotEqual(im.tobytes(), copy.tobytes()) @@ -47,7 +51,7 @@ class TestImagingCoreResampleAccuracy(PillowTestCase): 1f 1f e0 e0 1f 1f e0 e0 """ - case = Image.new('L', size, 255 - color) + case = Image.new("L", size, 255 - color) rectangle = ImageDraw.Draw(case).rectangle rectangle((0, 0, size[0] // 2 - 1, size[1] // 2 - 1), color) rectangle((size[0] // 2, size[1] // 2, size[0], size[1]), color) @@ -58,13 +62,13 @@ class TestImagingCoreResampleAccuracy(PillowTestCase): """Restores a sample image from given data string which contains hex-encoded pixels from the top left fourth of a sample. """ - data = data.replace(' ', '') - sample = Image.new('L', size) + data = data.replace(" ", "") + sample = Image.new("L", size) s_px = sample.load() w, h = size[0] // 2, size[1] // 2 for y in range(h): for x in range(w): - val = int(data[(y * w + x) * 2:(y * w + x + 1) * 2], 16) + val = int(data[(y * w + x) * 2 : (y * w + x + 1) * 2], 16) s_px[x, y] = val s_px[size[0] - x - 1, size[1] - y - 1] = val s_px[x, size[1] - y - 1] = 255 - val @@ -77,118 +81,134 @@ class TestImagingCoreResampleAccuracy(PillowTestCase): for y in range(case.size[1]): for x in range(case.size[0]): if c_px[x, y] != s_px[x, y]: - message = '\nHave: \n{}\n\nExpected: \n{}'.format( - self.serialize_image(case), - self.serialize_image(sample), + message = "\nHave: \n{}\n\nExpected: \n{}".format( + self.serialize_image(case), self.serialize_image(sample) ) self.assertEqual(s_px[x, y], c_px[x, y], message) def serialize_image(self, image): s_px = image.load() - return '\n'.join( - ' '.join( - '{:02x}'.format(s_px[x, y]) - for x in range(image.size[0]) - ) + return "\n".join( + " ".join("{:02x}".format(s_px[x, y]) for x in range(image.size[0])) for y in range(image.size[1]) ) def test_reduce_box(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (8, 8), 0xe1) + for mode in ["RGBX", "RGB", "La", "L"]: + case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.BOX) - data = ('e1 e1' - 'e1 e1') + # fmt: off + data = ("e1 e1" + "e1 e1") + # fmt: on for channel in case.split(): self.check_case(channel, self.make_sample(data, (4, 4))) def test_reduce_bilinear(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (8, 8), 0xe1) + for mode in ["RGBX", "RGB", "La", "L"]: + case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.BILINEAR) - data = ('e1 c9' - 'c9 b7') + # fmt: off + data = ("e1 c9" + "c9 b7") + # fmt: on for channel in case.split(): self.check_case(channel, self.make_sample(data, (4, 4))) def test_reduce_hamming(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (8, 8), 0xe1) + for mode in ["RGBX", "RGB", "La", "L"]: + case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.HAMMING) - data = ('e1 da' - 'da d3') + # fmt: off + data = ("e1 da" + "da d3") + # fmt: on for channel in case.split(): self.check_case(channel, self.make_sample(data, (4, 4))) def test_reduce_bicubic(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (12, 12), 0xe1) + for mode in ["RGBX", "RGB", "La", "L"]: + case = self.make_case(mode, (12, 12), 0xE1) case = case.resize((6, 6), Image.BICUBIC) - data = ('e1 e3 d4' - 'e3 e5 d6' - 'd4 d6 c9') + # fmt: off + data = ("e1 e3 d4" + "e3 e5 d6" + "d4 d6 c9") + # fmt: on for channel in case.split(): self.check_case(channel, self.make_sample(data, (6, 6))) def test_reduce_lanczos(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (16, 16), 0xe1) + for mode in ["RGBX", "RGB", "La", "L"]: + case = self.make_case(mode, (16, 16), 0xE1) case = case.resize((8, 8), Image.LANCZOS) - data = ('e1 e0 e4 d7' - 'e0 df e3 d6' - 'e4 e3 e7 da' - 'd7 d6 d9 ce') + # fmt: off + data = ("e1 e0 e4 d7" + "e0 df e3 d6" + "e4 e3 e7 da" + "d7 d6 d9 ce") + # fmt: on for channel in case.split(): self.check_case(channel, self.make_sample(data, (8, 8))) def test_enlarge_box(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (2, 2), 0xe1) + for mode in ["RGBX", "RGB", "La", "L"]: + case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.BOX) - data = ('e1 e1' - 'e1 e1') + # fmt: off + data = ("e1 e1" + "e1 e1") + # fmt: on for channel in case.split(): self.check_case(channel, self.make_sample(data, (4, 4))) def test_enlarge_bilinear(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (2, 2), 0xe1) + for mode in ["RGBX", "RGB", "La", "L"]: + case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.BILINEAR) - data = ('e1 b0' - 'b0 98') + # fmt: off + data = ("e1 b0" + "b0 98") + # fmt: on for channel in case.split(): self.check_case(channel, self.make_sample(data, (4, 4))) def test_enlarge_hamming(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (2, 2), 0xe1) + for mode in ["RGBX", "RGB", "La", "L"]: + case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.HAMMING) - data = ('e1 d2' - 'd2 c5') + # fmt: off + data = ("e1 d2" + "d2 c5") + # fmt: on for channel in case.split(): self.check_case(channel, self.make_sample(data, (4, 4))) def test_enlarge_bicubic(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (4, 4), 0xe1) + for mode in ["RGBX", "RGB", "La", "L"]: + case = self.make_case(mode, (4, 4), 0xE1) case = case.resize((8, 8), Image.BICUBIC) - data = ('e1 e5 ee b9' - 'e5 e9 f3 bc' - 'ee f3 fd c1' - 'b9 bc c1 a2') + # fmt: off + data = ("e1 e5 ee b9" + "e5 e9 f3 bc" + "ee f3 fd c1" + "b9 bc c1 a2") + # fmt: on for channel in case.split(): self.check_case(channel, self.make_sample(data, (8, 8))) def test_enlarge_lanczos(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (6, 6), 0xe1) + for mode in ["RGBX", "RGB", "La", "L"]: + case = self.make_case(mode, (6, 6), 0xE1) case = case.resize((12, 12), Image.LANCZOS) - data = ('e1 e0 db ed f5 b8' - 'e0 df da ec f3 b7' - 'db db d6 e7 ee b5' - 'ed ec e6 fb ff bf' - 'f5 f4 ee ff ff c4' - 'b8 b7 b4 bf c4 a0') + data = ( + "e1 e0 db ed f5 b8" + "e0 df da ec f3 b7" + "db db d6 e7 ee b5" + "ed ec e6 fb ff bf" + "f5 f4 ee ff ff c4" + "b8 b7 b4 bf c4 a0" + ) for channel in case.split(): self.check_case(channel, self.make_sample(data, (12, 12))) @@ -204,29 +224,28 @@ class CoreResampleConsistencyTest(PillowTestCase): for x in range(channel.size[0]): for y in range(channel.size[1]): if px[x, y] != color: - message = "{} != {} for pixel {}".format( - px[x, y], color, (x, y)) + message = "{} != {} for pixel {}".format(px[x, y], color, (x, y)) self.assertEqual(px[x, y], color, message) def test_8u(self): - im, color = self.make_case('RGB', (0, 64, 255)) + im, color = self.make_case("RGB", (0, 64, 255)) r, g, b = im.split() self.run_case((r, color[0])) self.run_case((g, color[1])) self.run_case((b, color[2])) - self.run_case(self.make_case('L', 12)) + self.run_case(self.make_case("L", 12)) def test_32i(self): - self.run_case(self.make_case('I', 12)) - self.run_case(self.make_case('I', 0x7fffffff)) - self.run_case(self.make_case('I', -12)) - self.run_case(self.make_case('I', -1 << 31)) + self.run_case(self.make_case("I", 12)) + self.run_case(self.make_case("I", 0x7FFFFFFF)) + self.run_case(self.make_case("I", -12)) + self.run_case(self.make_case("I", -1 << 31)) def test_32f(self): - self.run_case(self.make_case('F', 1)) - self.run_case(self.make_case('F', 3.40282306074e+38)) - self.run_case(self.make_case('F', 1.175494e-38)) - self.run_case(self.make_case('F', 1.192093e-07)) + self.run_case(self.make_case("F", 1)) + self.run_case(self.make_case("F", 3.40282306074e38)) + self.run_case(self.make_case("F", 1.175494e-38)) + self.run_case(self.make_case("F", 1.192093e-07)) class CoreResampleAlphaCorrectTest(PillowTestCase): @@ -244,13 +263,16 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): px = i.load() for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} - self.assertEqual(256, len(used_colors), - 'All colors should present in resized image. ' - 'Only {} on {} line.'.format(len(used_colors), y)) + self.assertEqual( + 256, + len(used_colors), + "All colors should present in resized image. " + "Only {} on {} line.".format(len(used_colors), y), + ) @unittest.skip("current implementation isn't precise enough") def test_levels_rgba(self): - case = self.make_levels_case('RGBA') + case = self.make_levels_case("RGBA") self.run_levels_case(case.resize((512, 32), Image.BOX)) self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) self.run_levels_case(case.resize((512, 32), Image.HAMMING)) @@ -259,7 +281,7 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): @unittest.skip("current implementation isn't precise enough") def test_levels_la(self): - case = self.make_levels_case('LA') + case = self.make_levels_case("LA") self.run_levels_case(case.resize((512, 32), Image.BOX)) self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) self.run_levels_case(case.resize((512, 32), Image.HAMMING)) @@ -281,24 +303,21 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): for y in range(i.size[1]): for x in range(i.size[0]): if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel: - message = 'pixel at ({}, {}) is differ:\n{}\n{}'\ - .format(x, y, px[x, y], clean_pixel) + message = "pixel at ({}, {}) is differ:\n{}\n{}".format( + x, y, px[x, y], clean_pixel + ) self.assertEqual(px[x, y][:3], clean_pixel, message) def test_dirty_pixels_rgba(self): - case = self.make_dirty_case('RGBA', (255, 255, 0, 128), (0, 0, 255, 0)) + case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) self.run_dirty_case(case.resize((20, 20), Image.BOX), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), - (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.HAMMING), - (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), - (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), - (255, 255, 0)) + self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255, 255, 0)) + self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255, 255, 0)) + self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255, 255, 0)) + self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255, 255, 0)) def test_dirty_pixels_la(self): - case = self.make_dirty_case('LA', (255, 128), (0, 0)) + case = self.make_dirty_case("LA", (255, 128), (0, 0)) self.run_dirty_case(case.resize((20, 20), Image.BOX), (255,)) self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255,)) self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255,)) @@ -309,27 +328,27 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): class CoreResamplePassesTest(PillowTestCase): @contextmanager def count(self, diff): - count = Image.core.get_stats()['new_count'] + count = Image.core.get_stats()["new_count"] yield - self.assertEqual(Image.core.get_stats()['new_count'] - count, diff) + self.assertEqual(Image.core.get_stats()["new_count"] - count, diff) def test_horizontal(self): - im = hopper('L') + im = hopper("L") with self.count(1): im.resize((im.size[0] - 10, im.size[1]), Image.BILINEAR) def test_vertical(self): - im = hopper('L') + im = hopper("L") with self.count(1): im.resize((im.size[0], im.size[1] - 10), Image.BILINEAR) def test_both(self): - im = hopper('L') + im = hopper("L") with self.count(2): im.resize((im.size[0] - 10, im.size[1] - 10), Image.BILINEAR) def test_box_horizontal(self): - im = hopper('L') + im = hopper("L") box = (20, 0, im.size[0] - 20, im.size[1]) with self.count(1): # the same size, but different box @@ -339,7 +358,7 @@ class CoreResamplePassesTest(PillowTestCase): self.assert_image_similar(with_box, cropped, 0.1) def test_box_vertical(self): - im = hopper('L') + im = hopper("L") box = (0, 20, im.size[0], im.size[1] - 20) with self.count(1): # the same size, but different box @@ -354,7 +373,7 @@ class CoreResampleCoefficientsTest(PillowTestCase): test_color = 254 for size in range(400000, 400010, 2): - i = Image.new('L', (size, 1), 0) + i = Image.new("L", (size, 1), 0) draw = ImageDraw.Draw(i) draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) @@ -365,7 +384,7 @@ class CoreResampleCoefficientsTest(PillowTestCase): def test_nonzero_coefficients(self): # regression test for the wrong coefficients calculation # due to bug https://github.com/python-pillow/Pillow/issues/2161 - im = Image.new('RGBA', (1280, 1280), (0x20, 0x40, 0x60, 0xff)) + im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) histogram = im.resize((256, 256), Image.BICUBIC).histogram() # first channel @@ -375,21 +394,26 @@ class CoreResampleCoefficientsTest(PillowTestCase): # third channel self.assertEqual(histogram[0x100 * 2 + 0x60], 0x10000) # fourth channel - self.assertEqual(histogram[0x100 * 3 + 0xff], 0x10000) + self.assertEqual(histogram[0x100 * 3 + 0xFF], 0x10000) class CoreResampleBoxTest(PillowTestCase): def test_wrong_arguments(self): im = hopper() - for resample in (Image.NEAREST, Image.BOX, Image.BILINEAR, - Image.HAMMING, Image.BICUBIC, Image.LANCZOS): + for resample in ( + Image.NEAREST, + Image.BOX, + Image.BILINEAR, + Image.HAMMING, + Image.BICUBIC, + Image.LANCZOS, + ): im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height)) im.resize((32, 32), resample, (20, 20, 20, 100)) im.resize((32, 32), resample, (20, 20, 100, 20)) - with self.assertRaisesRegex(TypeError, - "must be sequence of length 4"): + with self.assertRaisesRegex(TypeError, "must be sequence of length 4"): im.resize((32, 32), resample, (im.width, im.height)) with self.assertRaisesRegex(ValueError, "can't be negative"): @@ -420,8 +444,7 @@ class CoreResampleBoxTest(PillowTestCase): for y0, y1 in split_range(dst_size[1], ytiles): for x0, x1 in split_range(dst_size[0], xtiles): - box = (x0 * scale[0], y0 * scale[1], - x1 * scale[0], y1 * scale[1]) + box = (x0 * scale[0], y0 * scale[1], x1 * scale[0], y1 * scale[1]) tile = im.resize((x1 - x0, y1 - y0), Image.BICUBIC, box) tiled.paste(tile, (x0, y0)) return tiled @@ -447,8 +470,7 @@ class CoreResampleBoxTest(PillowTestCase): # 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)) + with_box = supersampled.resize(dst_size, Image.BICUBIC, (0, 0, 59.125, 44.125)) without_box = supersampled.resize(dst_size, Image.BICUBIC) # error with box should be much smaller than without @@ -458,7 +480,7 @@ class CoreResampleBoxTest(PillowTestCase): def test_formats(self): for resample in [Image.NEAREST, Image.BILINEAR]: - for mode in ['RGB', 'L', 'RGBA', 'LA', 'I', '']: + for mode in ["RGB", "L", "RGBA", "LA", "I", ""]: im = hopper(mode) box = (20, 20, im.size[0] - 20, im.size[1] - 20) with_box = im.resize((32, 32), resample, box) @@ -480,7 +502,7 @@ class CoreResampleBoxTest(PillowTestCase): self.assertEqual(res.size, size) self.assert_image_equal(res, im.crop(box)) except AssertionError: - print('>>>', size, box) + print(">>>", size, box) raise def test_no_passthrough(self): @@ -500,7 +522,7 @@ class CoreResampleBoxTest(PillowTestCase): # check that the difference at least that much self.assert_image_similar(res, im.crop(box), 20) except AssertionError: - print('>>>', size, box) + print(">>>", size, box) raise def test_skip_horizontal(self): @@ -518,10 +540,9 @@ class CoreResampleBoxTest(PillowTestCase): res = im.resize(size, flt, box) self.assertEqual(res.size, size) # Borders should be slightly different - self.assert_image_similar( - res, im.crop(box).resize(size, flt), 0.4) + self.assert_image_similar(res, im.crop(box).resize(size, flt), 0.4) except AssertionError: - print('>>>', size, box, flt) + print(">>>", size, box, flt) raise def test_skip_vertical(self): @@ -539,8 +560,7 @@ class CoreResampleBoxTest(PillowTestCase): res = im.resize(size, flt, box) self.assertEqual(res.size, size) # Borders should be slightly different - self.assert_image_similar( - res, im.crop(box).resize(size, flt), 0.4) + self.assert_image_similar(res, im.crop(box).resize(size, flt), 0.4) except AssertionError: - print('>>>', size, box, flt) + print(">>>", size, box, flt) raise diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index c47c7317b..2538dd378 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -3,21 +3,30 @@ Tests for resize functionality. """ from itertools import permutations -from .helper import PillowTestCase, hopper - from PIL import Image +from .helper import PillowTestCase, hopper + class TestImagingCoreResize(PillowTestCase): - def resize(self, im, size, f): # Image class independent version of resize. im.load() return im._new(im.im.resize(size, f)) def test_nearest_mode(self): - for mode in ["1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", - "I;16"]: # exotic mode + for mode in [ + "1", + "P", + "L", + "I", + "F", + "RGB", + "RGBA", + "CMYK", + "YCbCr", + "I;16", + ]: # exotic mode im = hopper(mode) r = self.resize(im, (15, 12), Image.NEAREST) self.assertEqual(r.mode, mode) @@ -25,12 +34,15 @@ class TestImagingCoreResize(PillowTestCase): self.assertEqual(r.im.bands, im.im.bands) def test_convolution_modes(self): - self.assertRaises(ValueError, self.resize, hopper("1"), - (15, 12), Image.BILINEAR) - self.assertRaises(ValueError, self.resize, hopper("P"), - (15, 12), Image.BILINEAR) - self.assertRaises(ValueError, self.resize, hopper("I;16"), - (15, 12), Image.BILINEAR) + self.assertRaises( + ValueError, self.resize, hopper("1"), (15, 12), Image.BILINEAR + ) + self.assertRaises( + ValueError, self.resize, hopper("P"), (15, 12), Image.BILINEAR + ) + self.assertRaises( + ValueError, self.resize, hopper("I;16"), (15, 12), Image.BILINEAR + ) for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]: im = hopper(mode) r = self.resize(im, (15, 12), Image.BILINEAR) @@ -39,15 +51,27 @@ class TestImagingCoreResize(PillowTestCase): self.assertEqual(r.im.bands, im.im.bands) def test_reduce_filters(self): - for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, - Image.HAMMING, Image.BICUBIC, Image.LANCZOS]: + for f in [ + Image.NEAREST, + Image.BOX, + Image.BILINEAR, + Image.HAMMING, + Image.BICUBIC, + Image.LANCZOS, + ]: r = self.resize(hopper("RGB"), (15, 12), f) self.assertEqual(r.mode, "RGB") self.assertEqual(r.size, (15, 12)) def test_enlarge_filters(self): - for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, - Image.HAMMING, Image.BICUBIC, Image.LANCZOS]: + for f in [ + Image.NEAREST, + Image.BOX, + Image.BILINEAR, + Image.HAMMING, + Image.BICUBIC, + Image.LANCZOS, + ]: r = self.resize(hopper("RGB"), (212, 195), f) self.assertEqual(r.mode, "RGB") self.assertEqual(r.size, (212, 195)) @@ -60,24 +84,29 @@ class TestImagingCoreResize(PillowTestCase): # an endianness issues. samples = { - 'blank': Image.new('L', (2, 2), 0), - 'filled': Image.new('L', (2, 2), 255), - 'dirty': Image.new('L', (2, 2), 0), + "blank": Image.new("L", (2, 2), 0), + "filled": Image.new("L", (2, 2), 255), + "dirty": Image.new("L", (2, 2), 0), } - samples['dirty'].putpixel((1, 1), 128) + samples["dirty"].putpixel((1, 1), 128) - for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, - Image.HAMMING, Image.BICUBIC, Image.LANCZOS]: + for f in [ + Image.NEAREST, + Image.BOX, + Image.BILINEAR, + Image.HAMMING, + Image.BICUBIC, + Image.LANCZOS, + ]: # samples resized with current filter references = { - name: self.resize(ch, (4, 4), f) - for name, ch in samples.items() + name: self.resize(ch, (4, 4), f) for name, ch in samples.items() } for mode, channels_set in [ - ('RGB', ('blank', 'filled', 'dirty')), - ('RGBA', ('blank', 'blank', 'filled', 'dirty')), - ('LA', ('filled', 'dirty')), + ("RGB", ("blank", "filled", "dirty")), + ("RGBA", ("blank", "blank", "filled", "dirty")), + ("LA", ("filled", "dirty")), ]: for channels in set(permutations(channels_set)): # compile image from different channels permutations @@ -90,9 +119,15 @@ class TestImagingCoreResize(PillowTestCase): self.assert_image_equal(ch, references[channels[i]]) def test_enlarge_zero(self): - for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, - Image.HAMMING, Image.BICUBIC, Image.LANCZOS]: - r = self.resize(Image.new('RGB', (0, 0), "white"), (212, 195), f) + for f in [ + Image.NEAREST, + Image.BOX, + Image.BILINEAR, + Image.HAMMING, + Image.BICUBIC, + Image.LANCZOS, + ]: + r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), f) self.assertEqual(r.mode, "RGB") self.assertEqual(r.size, (212, 195)) self.assertEqual(r.getdata()[0], (0, 0, 0)) @@ -102,16 +137,16 @@ class TestImagingCoreResize(PillowTestCase): class TestImageResize(PillowTestCase): - def test_resize(self): def resize(mode, size): out = hopper(mode).resize(size) self.assertEqual(out.mode, mode) self.assertEqual(out.size, size) + for mode in "1", "P", "L", "RGB", "I", "F": resize(mode, (112, 103)) resize(mode, (188, 214)) # Test unknown resampling filter - im = hopper() - self.assertRaises(ValueError, im.resize, (10, 10), "unknown") + with hopper() as im: + self.assertRaises(ValueError, im.resize, (10, 10), "unknown") diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 05043cd06..9c62e7362 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,9 +1,9 @@ -from .helper import PillowTestCase, hopper from PIL import Image +from .helper import PillowTestCase, hopper + class TestImageRotate(PillowTestCase): - def rotate(self, im, mode, angle, center=None, translate=None): out = im.rotate(angle, center=center, translate=translate) self.assertEqual(out.mode, mode) @@ -24,12 +24,12 @@ class TestImageRotate(PillowTestCase): def test_angle(self): for angle in (0, 90, 180, 270): - im = Image.open('Tests/images/test-card.png') + im = Image.open("Tests/images/test-card.png") self.rotate(im, im.mode, angle) def test_zero(self): for angle in (0, 45, 90, 180, 270): - im = Image.new('RGB', (0, 0)) + im = Image.new("RGB", (0, 0)) self.rotate(im, im.mode, angle) def test_resample(self): @@ -38,18 +38,20 @@ 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)): + 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) def test_center_0(self): im = hopper() - target = Image.open('Tests/images/hopper_45.png') - target_origin = target.size[1]/2 + 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) @@ -58,7 +60,7 @@ class TestImageRotate(PillowTestCase): def test_center_14(self): im = hopper() - target = Image.open('Tests/images/hopper_45.png') + 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)) @@ -68,10 +70,11 @@ class TestImageRotate(PillowTestCase): def test_translate(self): im = hopper() - target = Image.open('Tests/images/hopper_45.png') + 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)) + target = target.crop( + (target_origin, target_origin, target_origin + 128, target_origin + 128) + ) im = im.rotate(45, translate=(5, 5), resample=Image.BICUBIC) @@ -82,44 +85,43 @@ class TestImageRotate(PillowTestCase): # resulting image should be black for angle in (90, 180, 270): im = hopper().rotate(angle, center=(-1, -1)) - self.assert_image_equal(im, Image.new('RGB', im.size, 'black')) + self.assert_image_equal(im, Image.new("RGB", im.size, "black")) def test_fastpath_translate(self): # if we post-translate by -128 # resulting image should be black for angle in (0, 90, 180, 270): im = hopper().rotate(angle, translate=(-128, -128)) - self.assert_image_equal(im, Image.new('RGB', im.size, 'black')) + self.assert_image_equal(im, Image.new("RGB", im.size, "black")) def test_center(self): im = hopper() self.rotate(im, im.mode, 45, center=(0, 0)) - self.rotate(im, im.mode, 45, translate=(im.size[0]/2, 0)) - self.rotate(im, im.mode, 45, center=(0, 0), - translate=(im.size[0]/2, 0)) + self.rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0)) + self.rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0)) def test_rotate_no_fill(self): - im = Image.new('RGB', (100, 100), 'green') - target = Image.open('Tests/images/rotate_45_no_fill.png') + im = Image.new("RGB", (100, 100), "green") + target = Image.open("Tests/images/rotate_45_no_fill.png") im = im.rotate(45) self.assert_image_equal(im, target) def test_rotate_with_fill(self): - im = Image.new('RGB', (100, 100), 'green') - target = Image.open('Tests/images/rotate_45_with_fill.png') - im = im.rotate(45, fillcolor='white') + im = Image.new("RGB", (100, 100), "green") + target = Image.open("Tests/images/rotate_45_with_fill.png") + im = im.rotate(45, fillcolor="white") self.assert_image_equal(im, target) def test_alpha_rotate_no_fill(self): # Alpha images are handled differently internally - im = Image.new('RGBA', (10, 10), 'green') + im = Image.new("RGBA", (10, 10), "green") im = im.rotate(45, expand=1) corner = im.getpixel((0, 0)) self.assertEqual(corner, (0, 0, 0, 0)) def test_alpha_rotate_with_fill(self): # Alpha images are handled differently internally - im = Image.new('RGBA', (10, 10), 'green') + im = Image.new("RGBA", (10, 10), "green") im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) corner = im.getpixel((0, 0)) self.assertEqual(corner, (255, 0, 0, 255)) diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 2d97dabc2..a19878aae 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,36 +1,38 @@ -from .helper import PillowTestCase, hopper - from PIL import Image +from .helper import PillowTestCase, hopper + class TestImageSplit(PillowTestCase): - def test_split(self): def split(mode): layers = hopper(mode).split() return [(i.mode, i.size[0], i.size[1]) for i in layers] - self.assertEqual(split("1"), [('1', 128, 128)]) - self.assertEqual(split("L"), [('L', 128, 128)]) - self.assertEqual(split("I"), [('I', 128, 128)]) - self.assertEqual(split("F"), [('F', 128, 128)]) - self.assertEqual(split("P"), [('P', 128, 128)]) + + self.assertEqual(split("1"), [("1", 128, 128)]) + self.assertEqual(split("L"), [("L", 128, 128)]) + self.assertEqual(split("I"), [("I", 128, 128)]) + self.assertEqual(split("F"), [("F", 128, 128)]) + self.assertEqual(split("P"), [("P", 128, 128)]) self.assertEqual( - split("RGB"), [('L', 128, 128), ('L', 128, 128), ('L', 128, 128)]) + split("RGB"), [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] + ) self.assertEqual( split("RGBA"), - [('L', 128, 128), ('L', 128, 128), - ('L', 128, 128), ('L', 128, 128)]) + [("L", 128, 128), ("L", 128, 128), ("L", 128, 128), ("L", 128, 128)], + ) self.assertEqual( split("CMYK"), - [('L', 128, 128), ('L', 128, 128), - ('L', 128, 128), ('L', 128, 128)]) + [("L", 128, 128), ("L", 128, 128), ("L", 128, 128), ("L", 128, 128)], + ) self.assertEqual( - split("YCbCr"), - [('L', 128, 128), ('L', 128, 128), ('L', 128, 128)]) + split("YCbCr"), [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] + ) def test_split_merge(self): def split_merge(mode): return Image.merge(mode, hopper(mode).split()) + self.assert_image_equal(hopper("1"), split_merge("1")) self.assert_image_equal(hopper("L"), split_merge("L")) self.assert_image_equal(hopper("I"), split_merge("I")) @@ -44,7 +46,7 @@ class TestImageSplit(PillowTestCase): def test_split_open(self): codecs = dir(Image.core) - if 'zip_encoder' in codecs: + if "zip_encoder" in codecs: test_file = self.tempfile("temp.png") else: test_file = self.tempfile("temp.pcx") @@ -53,9 +55,10 @@ class TestImageSplit(PillowTestCase): hopper(mode).save(test_file) im = Image.open(test_file) return len(im.split()) + self.assertEqual(split_open("1"), 1) self.assertEqual(split_open("L"), 1) self.assertEqual(split_open("P"), 1) self.assertEqual(split_open("RGB"), 3) - if 'zip_encoder' in codecs: + if "zip_encoder" in codecs: self.assertEqual(split_open("RGBA"), 4) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index d03ea4141..d1224f075 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,8 +1,9 @@ +from PIL import Image + from .helper import PillowTestCase, hopper class TestImageThumbnail(PillowTestCase): - def test_sanity(self): im = hopper() @@ -35,3 +36,14 @@ class TestImageThumbnail(PillowTestCase): im = hopper().resize((128, 128)) im.thumbnail((100, 100)) self.assert_image(im, im.mode, (100, 100)) + + def test_no_resize(self): + # Check that draft() can resize the image to the destination size + with Image.open("Tests/images/hopper.jpg") as im: + im.draft(None, (64, 64)) + self.assertEqual(im.size, (64, 64)) + + # Test thumbnail(), where only draft() is necessary to resize the image + with Image.open("Tests/images/hopper.jpg") as im: + im.thumbnail((64, 64)) + self.assert_image(im, im.mode, (64, 64)) diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index c5c06ba01..d7c879a25 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -1,8 +1,7 @@ -from .helper import PillowTestCase, hopper, fromstring +from .helper import PillowTestCase, fromstring, hopper class TestImageToBitmap(PillowTestCase): - def test_sanity(self): self.assertRaises(ValueError, lambda: hopper().tobitmap()) diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index 2bfee2da3..d21ef7f6f 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -2,7 +2,6 @@ from .helper import PillowTestCase, hopper class TestImageToBytes(PillowTestCase): - def test_sanity(self): data = hopper().tobytes() self.assertIsInstance(data, bytes) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index ae1cf6e3d..35f64bcc9 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,15 +1,12 @@ import math +from PIL import Image, ImageTransform + from .helper import PillowTestCase, hopper -from PIL import Image - class TestImageTransform(PillowTestCase): - def test_sanity(self): - from PIL import ImageTransform - im = Image.new("L", (100, 100)) seq = tuple(range(10)) @@ -23,55 +20,72 @@ 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') + im = hopper("RGB") (w, h) = im.size + # fmt: off transformed = im.transform(im.size, Image.EXTENT, (0, 0, w//2, h//2), # ul -> lr Image.BILINEAR) + # fmt: on - scaled = im.resize((w*2, h*2), Image.BILINEAR).crop((0, 0, w, h)) + scaled = im.resize((w * 2, h * 2), Image.BILINEAR).crop((0, 0, w, h)) # undone -- precision? self.assert_image_similar(transformed, scaled, 23) def test_quad(self): # one simple quad transform, equivalent to scale & crop upper left quad - im = hopper('RGB') + im = hopper("RGB") (w, h) = im.size + # fmt: off transformed = im.transform(im.size, Image.QUAD, (0, 0, 0, h//2, # ul -> ccw around quad: w//2, h//2, w//2, 0), Image.BILINEAR) + # fmt: on - scaled = im.transform((w, h), Image.AFFINE, - (.5, 0, 0, 0, .5, 0), - Image.BILINEAR) + scaled = im.transform( + (w, h), Image.AFFINE, (0.5, 0, 0, 0, 0.5, 0), Image.BILINEAR + ) self.assert_image_equal(transformed, scaled) def test_fill(self): for mode, pixel in [ - ['RGB', (255, 0, 0)], - ['RGBA', (255, 0, 0, 255)], - ['LA', (76, 0)] + ["RGB", (255, 0, 0)], + ["RGBA", (255, 0, 0, 255)], + ["LA", (76, 0)], ]: im = hopper(mode) (w, h) = im.size - transformed = im.transform(im.size, Image.EXTENT, - (0, 0, - w*2, h*2), - Image.BILINEAR, - fillcolor='red') + transformed = im.transform( + im.size, + Image.EXTENT, + (0, 0, w * 2, h * 2), + Image.BILINEAR, + fillcolor="red", + ) - self.assertEqual(transformed.getpixel((w-1, h-1)), pixel) + self.assertEqual(transformed.getpixel((w - 1, h - 1)), pixel) def test_mesh(self): # this should be a checkerboard of halfsized hoppers in ul, lr - im = hopper('RGBA') + im = hopper("RGBA") (w, h) = im.size + # fmt: off transformed = im.transform(im.size, Image.MESH, [((0, 0, w//2, h//2), # box (0, 0, 0, h, @@ -80,54 +94,50 @@ class TestImageTransform(PillowTestCase): (0, 0, 0, h, w, h, w, 0))], # ul -> ccw around quad Image.BILINEAR) + # fmt: on - scaled = im.transform((w//2, h//2), Image.AFFINE, - (2, 0, 0, 0, 2, 0), - Image.BILINEAR) + scaled = im.transform( + (w // 2, h // 2), Image.AFFINE, (2, 0, 0, 0, 2, 0), Image.BILINEAR + ) - checker = Image.new('RGBA', im.size) + checker = Image.new("RGBA", im.size) checker.paste(scaled, (0, 0)) - checker.paste(scaled, (w//2, h//2)) + checker.paste(scaled, (w // 2, h // 2)) self.assert_image_equal(transformed, checker) # now, check to see that the extra area is (0, 0, 0, 0) - blank = Image.new('RGBA', (w//2, h//2), (0, 0, 0, 0)) + blank = Image.new("RGBA", (w // 2, h // 2), (0, 0, 0, 0)) - self.assert_image_equal(blank, transformed.crop((w//2, 0, w, h//2))) - self.assert_image_equal(blank, transformed.crop((0, h//2, w//2, h))) + self.assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) + self.assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) def _test_alpha_premult(self, op): # create image with half white, half black, # with the black half transparent. # do op, # there should be no darkness in the white section. - im = Image.new('RGBA', (10, 10), (0, 0, 0, 0)) - im2 = Image.new('RGBA', (5, 10), (255, 255, 255, 255)) + im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) + im2 = Image.new("RGBA", (5, 10), (255, 255, 255, 255)) im.paste(im2, (0, 0)) im = op(im, (40, 10)) - im_background = Image.new('RGB', (40, 10), (255, 255, 255)) + im_background = Image.new("RGB", (40, 10), (255, 255, 255)) im_background.paste(im, (0, 0), im) hist = im_background.histogram() - self.assertEqual(40*10, hist[-1]) + self.assertEqual(40 * 10, hist[-1]) def test_alpha_premult_resize(self): - def op(im, sz): return im.resize(sz, Image.BILINEAR) self._test_alpha_premult(op) def test_alpha_premult_transform(self): - def op(im, sz): (w, h) = im.size - return im.transform(sz, Image.EXTENT, - (0, 0, - w, h), - Image.BILINEAR) + return im.transform(sz, Image.EXTENT, (0, 0, w, h), Image.BILINEAR) self._test_alpha_premult(op) @@ -146,10 +156,7 @@ class TestImageTransform(PillowTestCase): # Running by default, but I'd totally understand not doing it in # the future - pattern = [ - Image.new('RGBA', (1024, 1024), (a, a, a, a)) - for a in range(1, 65) - ] + pattern = [Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)] # Yeah. Watch some JIT optimize this out. pattern = None # noqa: F841 @@ -157,25 +164,44 @@ 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): + 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): transform = Image.AFFINE def _test_image(self): - im = hopper('RGB') + im = hopper("RGB") return im.crop((10, 20, im.width - 10, im.height - 20)) def _test_rotate(self, deg, transpose): im = self._test_image() - angle = - math.radians(deg) + angle = -math.radians(deg) matrix = [ - round(math.cos(angle), 15), round(math.sin(angle), 15), 0.0, - round(-math.sin(angle), 15), round(math.cos(angle), 15), 0.0, - 0, 0] + round(math.cos(angle), 15), + round(math.sin(angle), 15), + 0.0, + round(-math.sin(angle), 15), + round(math.cos(angle), 15), + 0.0, + 0, + 0, + ] matrix[2] = (1 - matrix[0] - matrix[1]) * im.width / 2 matrix[5] = (1 - matrix[3] - matrix[4]) * im.height / 2 @@ -185,8 +211,9 @@ class TestImageTransformAffine(PillowTestCase): transposed = im for resample in [Image.NEAREST, Image.BILINEAR, Image.BICUBIC]: - transformed = im.transform(transposed.size, self.transform, - matrix, resample) + transformed = im.transform( + transposed.size, self.transform, matrix, resample + ) self.assert_image_equal(transposed, transformed) def test_rotate_0_deg(self): @@ -205,21 +232,18 @@ class TestImageTransformAffine(PillowTestCase): im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) - matrix_up = [ - 1 / scale, 0, 0, - 0, 1 / scale, 0, - 0, 0] - matrix_down = [ - scale, 0, 0, - 0, scale, 0, - 0, 0] + matrix_up = [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0] + matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0] - for resample, epsilon in [(Image.NEAREST, 0), - (Image.BILINEAR, 2), (Image.BICUBIC, 1)]: - transformed = im.transform( - size_up, self.transform, matrix_up, resample) + for resample, epsilon in [ + (Image.NEAREST, 0), + (Image.BILINEAR, 2), + (Image.BICUBIC, 1), + ]: + transformed = im.transform(size_up, self.transform, matrix_up, resample) transformed = transformed.transform( - im.size, self.transform, matrix_down, resample) + im.size, self.transform, matrix_down, resample + ) self.assert_image_similar(transformed, im, epsilon * epsilonscale) def test_resize_1_1x(self): @@ -241,28 +265,25 @@ class TestImageTransformAffine(PillowTestCase): im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) - matrix_up = [ - 1, 0, -x, - 0, 1, -y, - 0, 0] - matrix_down = [ - 1, 0, x, - 0, 1, y, - 0, 0] + matrix_up = [1, 0, -x, 0, 1, -y, 0, 0] + matrix_down = [1, 0, x, 0, 1, y, 0, 0] - for resample, epsilon in [(Image.NEAREST, 0), - (Image.BILINEAR, 1.5), (Image.BICUBIC, 1)]: - transformed = im.transform( - size_up, self.transform, matrix_up, resample) + for resample, epsilon in [ + (Image.NEAREST, 0), + (Image.BILINEAR, 1.5), + (Image.BICUBIC, 1), + ]: + transformed = im.transform(size_up, self.transform, matrix_up, resample) transformed = transformed.transform( - im.size, self.transform, matrix_down, resample) + im.size, self.transform, matrix_down, resample + ) self.assert_image_similar(transformed, im, epsilon * epsilonscale) def test_translate_0_1(self): - self._test_translate(.1, 0, 3.7) + self._test_translate(0.1, 0, 3.7) def test_translate_0_6(self): - self._test_translate(.6, 0, 9.1) + self._test_translate(0.6, 0, 9.1) def test_translate_50(self): self._test_translate(50, 50, 0) diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 8ffb9e9bf..f5e8746ee 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,15 +1,22 @@ +from PIL.Image import ( + FLIP_LEFT_RIGHT, + FLIP_TOP_BOTTOM, + ROTATE_90, + ROTATE_180, + ROTATE_270, + TRANSPOSE, + TRANSVERSE, +) + from . import helper from .helper import PillowTestCase -from PIL.Image import (FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, ROTATE_90, ROTATE_180, - ROTATE_270, TRANSPOSE, TRANSVERSE) - class TestImageTranspose(PillowTestCase): hopper = { - 'L': helper.hopper('L').crop((0, 0, 121, 127)).copy(), - 'RGB': helper.hopper('RGB').crop((0, 0, 121, 127)).copy(), + mode: helper.hopper(mode).crop((0, 0, 121, 127)).copy() + for mode in ["L", "RGB", "I;16", "I;16L", "I;16B"] } def test_flip_left_right(self): @@ -20,12 +27,12 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(out.size, im.size) x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((x-2, 1))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, y-2))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, y-2))) + self.assertEqual(im.getpixel((1, 1)), out.getpixel((x - 2, 1))) + self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((1, 1))) + self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((x - 2, y - 2))) + self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((1, y - 2))) - for mode in ("L", "RGB"): + for mode in self.hopper: transpose(mode) def test_flip_top_bottom(self): @@ -36,12 +43,12 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(out.size, im.size) x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, y-2))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((x-2, y-2))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((x-2, 1))) + self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, y - 2))) + self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((x - 2, y - 2))) + self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((1, 1))) + self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((x - 2, 1))) - for mode in ("L", "RGB"): + for mode in self.hopper: transpose(mode) def test_rotate_90(self): @@ -52,12 +59,12 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(out.size, im.size[::-1]) x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, x-2))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, x-2))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, 1))) + self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, x - 2))) + self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((1, 1))) + self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((y - 2, x - 2))) + self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((y - 2, 1))) - for mode in ("L", "RGB"): + for mode in self.hopper: transpose(mode) def test_rotate_180(self): @@ -68,12 +75,12 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(out.size, im.size) x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((x-2, y-2))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, y-2))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, 1))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1))) + self.assertEqual(im.getpixel((1, 1)), out.getpixel((x - 2, y - 2))) + self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((1, y - 2))) + self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((x - 2, 1))) + self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((1, 1))) - for mode in ("L", "RGB"): + for mode in self.hopper: transpose(mode) def test_rotate_270(self): @@ -84,12 +91,12 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(out.size, im.size[::-1]) x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((y-2, 1))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((y-2, x-2))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, x-2))) + self.assertEqual(im.getpixel((1, 1)), out.getpixel((y - 2, 1))) + self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((y - 2, x - 2))) + self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((1, 1))) + self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((1, x - 2))) - for mode in ("L", "RGB"): + for mode in self.hopper: transpose(mode) def test_transpose(self): @@ -101,11 +108,11 @@ class TestImageTranspose(PillowTestCase): x, y = im.size self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, x-2))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, 1))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, x-2))) + self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((1, x - 2))) + self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((y - 2, 1))) + self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((y - 2, x - 2))) - for mode in ("L", "RGB"): + for mode in self.hopper: transpose(mode) def test_tranverse(self): @@ -116,33 +123,37 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(out.size, im.size[::-1]) x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((y-2, x-2))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((y-2, 1))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, x-2))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1))) + self.assertEqual(im.getpixel((1, 1)), out.getpixel((y - 2, x - 2))) + self.assertEqual(im.getpixel((x - 2, 1)), out.getpixel((y - 2, 1))) + self.assertEqual(im.getpixel((1, y - 2)), out.getpixel((1, x - 2))) + self.assertEqual(im.getpixel((x - 2, y - 2)), out.getpixel((1, 1))) - for mode in ("L", "RGB"): + for mode in self.hopper: transpose(mode) def test_roundtrip(self): - im = self.hopper['L'] + for mode in self.hopper: + im = self.hopper[mode] - def transpose(first, second): - return im.transpose(first).transpose(second) + def transpose(first, second): + return im.transpose(first).transpose(second) - self.assert_image_equal( - im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT)) - self.assert_image_equal( - im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM)) - self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270)) - self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180)) - self.assert_image_equal( - im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM)) - self.assert_image_equal( - im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT)) - self.assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT)) - self.assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM)) - self.assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE)) + self.assert_image_equal(im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT)) + self.assert_image_equal(im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM)) + self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270)) + self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180)) + self.assert_image_equal( + im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM) + ) + self.assert_image_equal( + im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT) + ) + self.assert_image_equal( + im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT) + ) + self.assert_image_equal( + im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM) + ) + self.assert_image_equal( + im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE) + ) diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 8aa784cdd..6f42a28df 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,7 +1,6 @@ -from .helper import PillowTestCase, hopper +from PIL import Image, ImageChops -from PIL import Image -from PIL import ImageChops +from .helper import PillowTestCase, hopper BLACK = (0, 0, 0) BROWN = (127, 64, 0) @@ -15,7 +14,6 @@ GREY = 128 class TestImageChops(PillowTestCase): - def test_sanity(self): im = hopper("L") @@ -264,11 +262,12 @@ class TestImageChops(PillowTestCase): # Assert self.assertEqual(new.getbbox(), (0, 45, 100, 96)) self.assertEqual(new.getpixel((50, 50)), BLACK) - self.assertEqual(new.getpixel((50+xoffset, 50+yoffset)), DARK_GREEN) + self.assertEqual(new.getpixel((50 + xoffset, 50 + yoffset)), DARK_GREEN) # Test no yoffset - self.assertEqual(ImageChops.offset(im, xoffset), - ImageChops.offset(im, xoffset, xoffset)) + self.assertEqual( + ImageChops.offset(im, xoffset), ImageChops.offset(im, xoffset, xoffset) + ) def test_screen(self): # Arrange @@ -343,7 +342,6 @@ class TestImageChops(PillowTestCase): self.assertEqual(new.getpixel((50, 50)), (241, 167, 127)) def test_logical(self): - def table(op, a, b): out = [] for x in (a, b): @@ -353,23 +351,14 @@ class TestImageChops(PillowTestCase): out.append(op(imx, imy).getpixel((0, 0))) return tuple(out) - self.assertEqual( - table(ImageChops.logical_and, 0, 1), (0, 0, 0, 255)) - self.assertEqual( - table(ImageChops.logical_or, 0, 1), (0, 255, 255, 255)) - self.assertEqual( - table(ImageChops.logical_xor, 0, 1), (0, 255, 255, 0)) + self.assertEqual(table(ImageChops.logical_and, 0, 1), (0, 0, 0, 255)) + self.assertEqual(table(ImageChops.logical_or, 0, 1), (0, 255, 255, 255)) + self.assertEqual(table(ImageChops.logical_xor, 0, 1), (0, 255, 255, 0)) - self.assertEqual( - table(ImageChops.logical_and, 0, 128), (0, 0, 0, 255)) - self.assertEqual( - table(ImageChops.logical_or, 0, 128), (0, 255, 255, 255)) - self.assertEqual( - table(ImageChops.logical_xor, 0, 128), (0, 255, 255, 0)) + self.assertEqual(table(ImageChops.logical_and, 0, 128), (0, 0, 0, 255)) + self.assertEqual(table(ImageChops.logical_or, 0, 128), (0, 255, 255, 255)) + self.assertEqual(table(ImageChops.logical_xor, 0, 128), (0, 255, 255, 0)) - self.assertEqual( - table(ImageChops.logical_and, 0, 255), (0, 0, 0, 255)) - self.assertEqual( - table(ImageChops.logical_or, 0, 255), (0, 255, 255, 255)) - self.assertEqual( - table(ImageChops.logical_xor, 0, 255), (0, 255, 255, 0)) + self.assertEqual(table(ImageChops.logical_and, 0, 255), (0, 0, 0, 255)) + self.assertEqual(table(ImageChops.logical_or, 0, 255), (0, 255, 255, 255)) + self.assertEqual(table(ImageChops.logical_xor, 0, 255), (0, 255, 255, 0)) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index cdd6f00c3..97f81fb3f 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,14 +1,15 @@ -from .helper import PillowTestCase, hopper import datetime +import os +from io import BytesIO from PIL import Image, ImageMode -from io import BytesIO -import os +from .helper import PillowTestCase, hopper try: from PIL import ImageCms from PIL.ImageCms import ImageCmsProfile + ImageCms.core.profile_open except ImportError: # Skipped via setUp() @@ -20,10 +21,10 @@ HAVE_PROFILE = os.path.exists(SRGB) class TestImageCms(PillowTestCase): - def setUp(self): try: from PIL import ImageCms + # need to hit getattr to trigger the delayed import error ImageCms.core.profile_open except ImportError as v: @@ -39,7 +40,7 @@ class TestImageCms(PillowTestCase): # this mostly follows the cms_test outline. v = ImageCms.versions() # should return four strings - self.assertEqual(v[0], '1.0.0 pil') + self.assertEqual(v[0], "1.0.0 pil") self.assertEqual(list(map(type, v)), [str, str, str, str]) # internal version number @@ -57,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) @@ -82,66 +83,80 @@ class TestImageCms(PillowTestCase): # get profile information for file self.assertEqual( ImageCms.getProfileName(SRGB).strip(), - 'IEC 61966-2-1 Default RGB Colour Space - sRGB') + "IEC 61966-2-1 Default RGB Colour Space - sRGB", + ) def test_info(self): self.skip_missing() self.assertEqual( - ImageCms.getProfileInfo(SRGB).splitlines(), [ - 'sRGB IEC61966-2-1 black scaled', '', - 'Copyright International Color Consortium, 2009', '']) + ImageCms.getProfileInfo(SRGB).splitlines(), + [ + "sRGB IEC61966-2-1 black scaled", + "", + "Copyright International Color Consortium, 2009", + "", + ], + ) def test_copyright(self): self.skip_missing() self.assertEqual( ImageCms.getProfileCopyright(SRGB).strip(), - 'Copyright International Color Consortium, 2009') + "Copyright International Color Consortium, 2009", + ) def test_manufacturer(self): self.skip_missing() - self.assertEqual( - ImageCms.getProfileManufacturer(SRGB).strip(), - '') + self.assertEqual(ImageCms.getProfileManufacturer(SRGB).strip(), "") def test_model(self): self.skip_missing() self.assertEqual( ImageCms.getProfileModel(SRGB).strip(), - 'IEC 61966-2-1 Default RGB Colour Space - sRGB') + "IEC 61966-2-1 Default RGB Colour Space - sRGB", + ) def test_description(self): self.skip_missing() self.assertEqual( ImageCms.getProfileDescription(SRGB).strip(), - 'sRGB IEC61966-2-1 black scaled') + "sRGB IEC61966-2-1 black scaled", + ) def test_intent(self): self.skip_missing() self.assertEqual(ImageCms.getDefaultIntent(SRGB), 0) - self.assertEqual(ImageCms.isIntentSupported( - SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, - ImageCms.DIRECTION_INPUT), 1) + self.assertEqual( + ImageCms.isIntentSupported( + SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT + ), + 1, + ) def test_profile_object(self): # same, using profile object p = ImageCms.createProfile("sRGB") - # self.assertEqual(ImageCms.getProfileName(p).strip(), - # 'sRGB built-in - (lcms internal)') - # self.assertEqual(ImageCms.getProfileInfo(p).splitlines(), - # ['sRGB built-in', '', 'WhitePoint : D65 (daylight)', '', '']) + # self.assertEqual(ImageCms.getProfileName(p).strip(), + # 'sRGB built-in - (lcms internal)') + # self.assertEqual(ImageCms.getProfileInfo(p).splitlines(), + # ['sRGB built-in', '', 'WhitePoint : D65 (daylight)', '', '']) self.assertEqual(ImageCms.getDefaultIntent(p), 0) - self.assertEqual(ImageCms.isIntentSupported( - p, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, - ImageCms.DIRECTION_INPUT), 1) + self.assertEqual( + ImageCms.isIntentSupported( + p, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT + ), + 1, + ) 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') + "IEC 61966-2.1 Default RGB colour space - sRGB", + ) def test_exceptions(self): # Test mode mismatch @@ -151,19 +166,18 @@ class TestImageCms(PillowTestCase): self.assertRaises(ValueError, t.apply_in_place, hopper("RGBA")) # the procedural pyCMS API uses PyCMSError for all sorts of errors + with hopper() as im: + self.assertRaises( + ImageCms.PyCMSError, ImageCms.profileToProfile, im, "foo", "bar" + ) self.assertRaises( - ImageCms.PyCMSError, - ImageCms.profileToProfile, hopper(), "foo", "bar") - self.assertRaises( - ImageCms.PyCMSError, - ImageCms.buildTransform, "foo", "bar", "RGB", "RGB") - self.assertRaises( - ImageCms.PyCMSError, - ImageCms.getProfileName, None) + ImageCms.PyCMSError, ImageCms.buildTransform, "foo", "bar", "RGB", "RGB" + ) + self.assertRaises(ImageCms.PyCMSError, ImageCms.getProfileName, None) self.skip_missing() self.assertRaises( - ImageCms.PyCMSError, - ImageCms.isIntentSupported, SRGB, None, None) + ImageCms.PyCMSError, ImageCms.isIntentSupported, SRGB, None, None + ) def test_display_profile(self): # try fetching the profile for the current display device @@ -174,15 +188,13 @@ class TestImageCms(PillowTestCase): ImageCms.createProfile("LAB", 6500) def test_unsupported_color_space(self): - self.assertRaises(ImageCms.PyCMSError, - ImageCms.createProfile, "unsupported") + self.assertRaises(ImageCms.PyCMSError, ImageCms.createProfile, "unsupported") def test_invalid_color_temperature(self): - self.assertRaises(ImageCms.PyCMSError, - ImageCms.createProfile, "LAB", "invalid") + self.assertRaises(ImageCms.PyCMSError, ImageCms.createProfile, "LAB", "invalid") def test_simple_lab(self): - i = Image.new('RGB', (10, 10), (128, 128, 128)) + i = Image.new("RGB", (10, 10), (128, 128, 128)) psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") @@ -190,7 +202,7 @@ class TestImageCms(PillowTestCase): i_lab = ImageCms.applyTransform(i, t) - self.assertEqual(i_lab.mode, 'LAB') + self.assertEqual(i_lab.mode, "LAB") k = i_lab.getpixel((0, 0)) # not a linear luminance map. so L != 128: @@ -217,7 +229,7 @@ class TestImageCms(PillowTestCase): # i.save('temp.lab.tif') # visually verified vs PS. - target = Image.open('Tests/images/hopper.Lab.tif') + target = Image.open("Tests/images/hopper.Lab.tif") self.assert_image_similar(i, target, 3.5) @@ -226,17 +238,17 @@ class TestImageCms(PillowTestCase): pLab = ImageCms.createProfile("LAB") t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - img = Image.open('Tests/images/hopper.Lab.tif') + img = Image.open("Tests/images/hopper.Lab.tif") img_srgb = ImageCms.applyTransform(img, t) # img_srgb.save('temp.srgb.tif') # visually verified vs ps. self.assert_image_similar(hopper(), img_srgb, 30) - self.assertTrue(img_srgb.info['icc_profile']) + self.assertTrue(img_srgb.info["icc_profile"]) - profile = ImageCmsProfile(BytesIO(img_srgb.info['icc_profile'])) - self.assertIn('sRGB', ImageCms.getProfileDescription(profile)) + profile = ImageCmsProfile(BytesIO(img_srgb.info["icc_profile"])) + self.assertIn("sRGB", ImageCms.getProfileDescription(profile)) def test_lab_roundtrip(self): # check to see if we're at least internally consistent. @@ -248,26 +260,25 @@ class TestImageCms(PillowTestCase): i = ImageCms.applyTransform(hopper(), t) - self.assertEqual(i.info['icc_profile'], - ImageCmsProfile(pLab).tobytes()) + self.assertEqual(i.info["icc_profile"], ImageCmsProfile(pLab).tobytes()) out = ImageCms.applyTransform(i, t2) 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())) # not the same bytes as the original icc_profile, # but it does roundtrip self.assertEqual(p.tobytes(), p2.tobytes()) - self.assertEqual(ImageCms.getProfileName(p), - ImageCms.getProfileName(p2)) - self.assertEqual(ImageCms.getProfileDescription(p), - ImageCms.getProfileDescription(p2)) + self.assertEqual(ImageCms.getProfileName(p), ImageCms.getProfileName(p2)) + self.assertEqual( + ImageCms.getProfileDescription(p), ImageCms.getProfileDescription(p2) + ) def test_extended_information(self): self.skip_missing() @@ -281,117 +292,184 @@ class TestImageCms(PillowTestCase): def truncate_tuple(tuple_or_float): return tuple( - truncate_tuple(val) if isinstance(val, tuple) - else int(val * power) / power for val in tuple_or_float) + truncate_tuple(val) + if isinstance(val, tuple) + else int(val * power) / power + for val in tuple_or_float + ) + self.assertEqual(truncate_tuple(tup1), truncate_tuple(tup2)) self.assertEqual(p.attributes, 4294967296) assert_truncated_tuple_equal( p.blue_colorant, - ((0.14306640625, 0.06060791015625, 0.7140960693359375), - (0.1558847490315394, 0.06603820639433387, 0.06060791015625))) + ( + (0.14306640625, 0.06060791015625, 0.7140960693359375), + (0.1558847490315394, 0.06603820639433387, 0.06060791015625), + ), + ) assert_truncated_tuple_equal( p.blue_primary, - ((0.14306641366715667, 0.06060790921083026, 0.7140960805782015), - (0.15588475410450106, 0.06603820408959558, 0.06060790921083026))) + ( + (0.14306641366715667, 0.06060790921083026, 0.7140960805782015), + (0.15588475410450106, 0.06603820408959558, 0.06060790921083026), + ), + ) assert_truncated_tuple_equal( p.chromatic_adaptation, - (((1.04791259765625, 0.0229339599609375, -0.050201416015625), - (0.02960205078125, 0.9904632568359375, -0.0170745849609375), - (-0.009246826171875, 0.0150604248046875, 0.7517852783203125)), - ((1.0267159024652783, 0.022470062342089134, 0.0229339599609375), - (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), - (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875)))) + ( + ( + (1.04791259765625, 0.0229339599609375, -0.050201416015625), + (0.02960205078125, 0.9904632568359375, -0.0170745849609375), + (-0.009246826171875, 0.0150604248046875, 0.7517852783203125), + ), + ( + (1.0267159024652783, 0.022470062342089134, 0.0229339599609375), + (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), + (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875), + ), + ), + ) self.assertIsNone(p.chromaticity) - self.assertEqual(p.clut, { - 0: (False, False, True), - 1: (False, False, True), - 2: (False, False, True), - 3: (False, False, True) - }) - self.assertEqual(p.color_space, 'RGB') + self.assertEqual( + p.clut, + { + 0: (False, False, True), + 1: (False, False, True), + 2: (False, False, True), + 3: (False, False, True), + }, + ) + self.assertIsNone(p.colorant_table) self.assertIsNone(p.colorant_table_out) self.assertIsNone(p.colorimetric_intent) - self.assertEqual(p.connection_space, 'XYZ ') - self.assertEqual(p.copyright, - 'Copyright International Color Consortium, 2009') - self.assertEqual(p.creation_date, - datetime.datetime(2009, 2, 27, 21, 36, 31)) - self.assertEqual(p.device_class, 'mntr') + self.assertEqual(p.connection_space, "XYZ ") + self.assertEqual(p.copyright, "Copyright International Color Consortium, 2009") + self.assertEqual(p.creation_date, datetime.datetime(2009, 2, 27, 21, 36, 31)) + self.assertEqual(p.device_class, "mntr") assert_truncated_tuple_equal( p.green_colorant, - ((0.3851470947265625, 0.7168731689453125, 0.097076416015625), - (0.32119769927720654, 0.5978443449048152, 0.7168731689453125))) + ( + (0.3851470947265625, 0.7168731689453125, 0.097076416015625), + (0.32119769927720654, 0.5978443449048152, 0.7168731689453125), + ), + ) assert_truncated_tuple_equal( p.green_primary, - ((0.3851470888162112, 0.7168731974161346, 0.09707641738998518), - (0.32119768793686687, 0.5978443567149709, 0.7168731974161346))) + ( + (0.3851470888162112, 0.7168731974161346, 0.09707641738998518), + (0.32119768793686687, 0.5978443567149709, 0.7168731974161346), + ), + ) self.assertEqual(p.header_flags, 0) - self.assertEqual(p.header_manufacturer, '\x00\x00\x00\x00') - self.assertEqual(p.header_model, '\x00\x00\x00\x00') - self.assertEqual(p.icc_measurement_condition, { - 'backing': (0.0, 0.0, 0.0), - 'flare': 0.0, - 'geo': 'unknown', - 'observer': 1, - 'illuminant_type': 'D65' - }) + self.assertEqual(p.header_manufacturer, "\x00\x00\x00\x00") + self.assertEqual(p.header_model, "\x00\x00\x00\x00") + self.assertEqual( + p.icc_measurement_condition, + { + "backing": (0.0, 0.0, 0.0), + "flare": 0.0, + "geo": "unknown", + "observer": 1, + "illuminant_type": "D65", + }, + ) self.assertEqual(p.icc_version, 33554432) self.assertIsNone(p.icc_viewing_condition) - self.assertEqual(p.intent_supported, { - 0: (True, True, True), - 1: (True, True, True), - 2: (True, True, True), - 3: (True, True, True) - }) + self.assertEqual( + p.intent_supported, + { + 0: (True, True, True), + 1: (True, True, True), + 2: (True, True, True), + 3: (True, True, True), + }, + ) self.assertTrue(p.is_matrix_shaper) self.assertEqual(p.luminance, ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0))) self.assertIsNone(p.manufacturer) assert_truncated_tuple_equal( p.media_black_point, - ((0.012054443359375, 0.0124969482421875, 0.01031494140625), - (0.34573304157549234, 0.35842450765864337, 0.0124969482421875))) + ( + (0.012054443359375, 0.0124969482421875, 0.01031494140625), + (0.34573304157549234, 0.35842450765864337, 0.0124969482421875), + ), + ) assert_truncated_tuple_equal( p.media_white_point, - ((0.964202880859375, 1.0, 0.8249053955078125), - (0.3457029219802284, 0.3585375327567059, 1.0))) + ( + (0.964202880859375, 1.0, 0.8249053955078125), + (0.3457029219802284, 0.3585375327567059, 1.0), + ), + ) assert_truncated_tuple_equal( - (p.media_white_point_temperature,), - (5000.722328847392,)) - self.assertEqual(p.model, - 'IEC 61966-2-1 Default RGB Colour Space - sRGB') - self.assertEqual(p.pcs, 'XYZ') + (p.media_white_point_temperature,), (5000.722328847392,) + ) + self.assertEqual(p.model, "IEC 61966-2-1 Default RGB Colour Space - sRGB") + self.assertIsNone(p.perceptual_rendering_intent_gamut) - self.assertEqual(p.product_copyright, - 'Copyright International Color Consortium, 2009') - self.assertEqual(p.product_desc, 'sRGB IEC61966-2-1 black scaled') - self.assertEqual(p.product_description, - 'sRGB IEC61966-2-1 black scaled') - self.assertEqual(p.product_manufacturer, '') - self.assertEqual( - p.product_model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') - self.assertEqual( - p.profile_description, 'sRGB IEC61966-2-1 black scaled') - self.assertEqual( - p.profile_id, b')\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r') + + self.assertEqual(p.profile_description, "sRGB IEC61966-2-1 black scaled") + self.assertEqual(p.profile_id, b")\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r") assert_truncated_tuple_equal( p.red_colorant, - ((0.436065673828125, 0.2224884033203125, 0.013916015625), - (0.6484536316398539, 0.3308524880306778, 0.2224884033203125))) + ( + (0.436065673828125, 0.2224884033203125, 0.013916015625), + (0.6484536316398539, 0.3308524880306778, 0.2224884033203125), + ), + ) assert_truncated_tuple_equal( p.red_primary, - ((0.43606566581047446, 0.22248840582960838, 0.013916015621759925), - (0.6484536250319214, 0.3308524944738204, 0.22248840582960838))) + ( + (0.43606566581047446, 0.22248840582960838, 0.013916015621759925), + (0.6484536250319214, 0.3308524944738204, 0.22248840582960838), + ), + ) self.assertEqual(p.rendering_intent, 0) self.assertIsNone(p.saturation_rendering_intent_gamut) self.assertIsNone(p.screening_description) self.assertIsNone(p.target) - self.assertEqual(p.technology, 'CRT ') + self.assertEqual(p.technology, "CRT ") self.assertEqual(p.version, 2.0) - self.assertEqual(p.viewing_condition, - 'Reference Viewing Condition in IEC 61966-2-1') - self.assertEqual(p.xcolor_space, 'RGB ') + self.assertEqual( + p.viewing_condition, "Reference Viewing Condition in IEC 61966-2-1" + ) + self.assertEqual(p.xcolor_space, "RGB ") + + def test_deprecations(self): + self.skip_missing() + o = ImageCms.getOpenProfile(SRGB) + p = o.profile + + def helper_deprecated(attr, expected): + result = self.assert_warning(DeprecationWarning, getattr, p, attr) + self.assertEqual(result, expected) + + # p.color_space + helper_deprecated("color_space", "RGB") + + # p.pcs + helper_deprecated("pcs", "XYZ") + + # p.product_copyright + helper_deprecated( + "product_copyright", "Copyright International Color Consortium, 2009" + ) + + # p.product_desc + helper_deprecated("product_desc", "sRGB IEC61966-2-1 black scaled") + + # p.product_description + helper_deprecated("product_description", "sRGB IEC61966-2-1 black scaled") + + # p.product_manufacturer + helper_deprecated("product_manufacturer", "") + + # p.product_model + helper_deprecated( + "product_model", "IEC 61966-2-1 Default RGB Colour Space - sRGB" + ) def test_profile_typesafety(self): """ Profile init type safety @@ -404,34 +482,36 @@ class TestImageCms(PillowTestCase): with self.assertRaises(TypeError): ImageCms.ImageCmsProfile(1).tobytes() - def assert_aux_channel_preserved(self, mode, - transform_in_place, preserved_channel): + def assert_aux_channel_preserved(self, mode, transform_in_place, preserved_channel): def create_test_image(): - # set up test image with something interesting in the tested aux - # channel. - nine_grid_deltas = [ # noqa: E128 + # set up test image with something interesting in the tested aux channel. + # fmt: off + nine_grid_deltas = [ # noqa: E131 (-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 0), (0, 1), (1, -1), (1, 0), (1, 1), ] + # fmt: on chans = [] bands = ImageMode.getmode(mode).bands for band_ndx in range(len(bands)): - channel_type = 'L' # 8-bit unorm + channel_type = "L" # 8-bit unorm channel_pattern = hopper(channel_type) # paste pattern with varying offsets to avoid correlation # potentially hiding some bugs (like channels getting mixed). paste_offset = ( int(band_ndx / float(len(bands)) * channel_pattern.size[0]), - int(band_ndx / float(len(bands) * 2) * channel_pattern.size[1]) + int(band_ndx / float(len(bands) * 2) * channel_pattern.size[1]), ) channel_data = Image.new(channel_type, channel_pattern.size) for delta in nine_grid_deltas: channel_data.paste( channel_pattern, - tuple(paste_offset[c] + delta[c] * channel_pattern.size[c] - for c in range(2)), + tuple( + paste_offset[c] + delta[c] * channel_pattern.size[c] + for c in range(2) + ), ) chans.append(channel_data) return Image.merge(mode, chans) @@ -443,42 +523,46 @@ class TestImageCms(PillowTestCase): source_profile = ImageCms.createProfile("sRGB") destination_profile = ImageCms.createProfile("sRGB") t = ImageCms.buildTransform( - source_profile, destination_profile, inMode=mode, outMode=mode) + source_profile, destination_profile, inMode=mode, outMode=mode + ) # apply transform if transform_in_place: ImageCms.applyTransform(source_image, t, inPlace=True) result_image = source_image else: - result_image = ImageCms.applyTransform( - source_image, t, inPlace=False) + result_image = ImageCms.applyTransform(source_image, t, inPlace=False) result_image_aux = result_image.getchannel(preserved_channel) self.assert_image_equal(source_image_aux, result_image_aux) def test_preserve_auxiliary_channels_rgba(self): self.assert_aux_channel_preserved( - mode='RGBA', transform_in_place=False, preserved_channel='A') + mode="RGBA", transform_in_place=False, preserved_channel="A" + ) def test_preserve_auxiliary_channels_rgba_in_place(self): self.assert_aux_channel_preserved( - mode='RGBA', transform_in_place=True, preserved_channel='A') + mode="RGBA", transform_in_place=True, preserved_channel="A" + ) def test_preserve_auxiliary_channels_rgbx(self): self.assert_aux_channel_preserved( - mode='RGBX', transform_in_place=False, preserved_channel='X') + mode="RGBX", transform_in_place=False, preserved_channel="X" + ) def test_preserve_auxiliary_channels_rgbx_in_place(self): self.assert_aux_channel_preserved( - mode='RGBX', transform_in_place=True, preserved_channel='X') + mode="RGBX", transform_in_place=True, preserved_channel="X" + ) def test_auxiliary_channels_isolated(self): # test data in aux channels does not affect non-aux channels aux_channel_formats = [ # format, profile, color-only format, source test image - ('RGBA', 'sRGB', 'RGB', hopper('RGBA')), - ('RGBX', 'sRGB', 'RGB', hopper('RGBX')), - ('LAB', 'LAB', 'LAB', Image.open('Tests/images/hopper.Lab.tif')), + ("RGBA", "sRGB", "RGB", hopper("RGBA")), + ("RGBX", "sRGB", "RGB", hopper("RGBX")), + ("LAB", "LAB", "LAB", Image.open("Tests/images/hopper.Lab.tif")), ] for src_format in aux_channel_formats: for dst_format in aux_channel_formats: @@ -492,25 +576,34 @@ class TestImageCms(PillowTestCase): destination_profile = ImageCms.createProfile(dst_format[1]) source_image = src_format[3] test_transform = ImageCms.buildTransform( - source_profile, destination_profile, - inMode=src_format[0], outMode=dst_format[0]) + source_profile, + destination_profile, + inMode=src_format[0], + outMode=dst_format[0], + ) # test conversion from aux-ful source if transform_in_place: test_image = source_image.copy() ImageCms.applyTransform( - test_image, test_transform, inPlace=True) + test_image, test_transform, inPlace=True + ) else: test_image = ImageCms.applyTransform( - source_image, test_transform, inPlace=False) + source_image, test_transform, inPlace=False + ) # reference conversion from aux-less source reference_transform = ImageCms.buildTransform( - source_profile, destination_profile, - inMode=src_format[2], outMode=dst_format[2]) + source_profile, + destination_profile, + inMode=src_format[2], + outMode=dst_format[2], + ) reference_image = ImageCms.applyTransform( - source_image.convert(src_format[2]), - reference_transform) + source_image.convert(src_format[2]), reference_transform + ) - self.assert_image_equal(test_image.convert(dst_format[2]), - reference_image) + self.assert_image_equal( + test_image.convert(dst_format[2]), reference_image + ) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index cb9c9843c..e4a7b7dfe 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -1,11 +1,9 @@ -from .helper import PillowTestCase +from PIL import Image, ImageColor -from PIL import Image -from PIL import ImageColor +from .helper import PillowTestCase class TestImageColor(PillowTestCase): - def test_hash(self): # short 3 components self.assertEqual((255, 0, 0), ImageColor.getrgb("#f00")) @@ -31,12 +29,9 @@ class TestImageColor(PillowTestCase): # case insensitivity self.assertEqual(ImageColor.getrgb("#DEF"), ImageColor.getrgb("#def")) - self.assertEqual(ImageColor.getrgb("#CDEF"), - ImageColor.getrgb("#cdef")) - self.assertEqual(ImageColor.getrgb("#DEFDEF"), - ImageColor.getrgb("#defdef")) - self.assertEqual(ImageColor.getrgb("#CDEFCDEF"), - ImageColor.getrgb("#cdefcdef")) + self.assertEqual(ImageColor.getrgb("#CDEF"), ImageColor.getrgb("#cdef")) + self.assertEqual(ImageColor.getrgb("#DEFDEF"), ImageColor.getrgb("#defdef")) + self.assertEqual(ImageColor.getrgb("#CDEFCDEF"), ImageColor.getrgb("#cdefcdef")) # not a number self.assertRaises(ValueError, ImageColor.getrgb, "#fo0") @@ -81,49 +76,48 @@ class TestImageColor(PillowTestCase): self.assertEqual((255, 0, 0), ImageColor.getrgb("hsv(0,100%,100%)")) self.assertEqual((255, 0, 0), ImageColor.getrgb("hsv(360,100%,100%)")) - self.assertEqual((0, 255, 255), - ImageColor.getrgb("hsv(180,100%,100%)")) + self.assertEqual((0, 255, 255), ImageColor.getrgb("hsv(180,100%,100%)")) # alternate format - self.assertEqual(ImageColor.getrgb("hsb(0,100%,50%)"), - ImageColor.getrgb("hsv(0,100%,50%)")) + self.assertEqual( + ImageColor.getrgb("hsb(0,100%,50%)"), ImageColor.getrgb("hsv(0,100%,50%)") + ) # floats - self.assertEqual((254, 3, 3), - ImageColor.getrgb("hsl(0.1,99.2%,50.3%)")) - self.assertEqual((255, 0, 0), - ImageColor.getrgb("hsl(360.,100.0%,50%)")) + self.assertEqual((254, 3, 3), ImageColor.getrgb("hsl(0.1,99.2%,50.3%)")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("hsl(360.,100.0%,50%)")) - self.assertEqual((253, 2, 2), - ImageColor.getrgb("hsv(0.1,99.2%,99.3%)")) - self.assertEqual((255, 0, 0), - ImageColor.getrgb("hsv(360.,100.0%,100%)")) + self.assertEqual((253, 2, 2), ImageColor.getrgb("hsv(0.1,99.2%,99.3%)")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("hsv(360.,100.0%,100%)")) # case insensitivity - self.assertEqual(ImageColor.getrgb("RGB(255,0,0)"), - ImageColor.getrgb("rgb(255,0,0)")) - self.assertEqual(ImageColor.getrgb("RGB(100%,0%,0%)"), - ImageColor.getrgb("rgb(100%,0%,0%)")) - self.assertEqual(ImageColor.getrgb("RGBA(255,0,0,0)"), - ImageColor.getrgb("rgba(255,0,0,0)")) - self.assertEqual(ImageColor.getrgb("HSL(0,100%,50%)"), - ImageColor.getrgb("hsl(0,100%,50%)")) - self.assertEqual(ImageColor.getrgb("HSV(0,100%,50%)"), - ImageColor.getrgb("hsv(0,100%,50%)")) - self.assertEqual(ImageColor.getrgb("HSB(0,100%,50%)"), - ImageColor.getrgb("hsb(0,100%,50%)")) + self.assertEqual( + ImageColor.getrgb("RGB(255,0,0)"), ImageColor.getrgb("rgb(255,0,0)") + ) + self.assertEqual( + ImageColor.getrgb("RGB(100%,0%,0%)"), ImageColor.getrgb("rgb(100%,0%,0%)") + ) + self.assertEqual( + ImageColor.getrgb("RGBA(255,0,0,0)"), ImageColor.getrgb("rgba(255,0,0,0)") + ) + self.assertEqual( + ImageColor.getrgb("HSL(0,100%,50%)"), ImageColor.getrgb("hsl(0,100%,50%)") + ) + self.assertEqual( + ImageColor.getrgb("HSV(0,100%,50%)"), ImageColor.getrgb("hsv(0,100%,50%)") + ) + self.assertEqual( + ImageColor.getrgb("HSB(0,100%,50%)"), ImageColor.getrgb("hsb(0,100%,50%)") + ) # space agnosticism - self.assertEqual((255, 0, 0), - ImageColor.getrgb("rgb( 255 , 0 , 0 )")) - self.assertEqual((255, 0, 0), - ImageColor.getrgb("rgb( 100% , 0% , 0% )")) - self.assertEqual((255, 0, 0, 0), - ImageColor.getrgb("rgba( 255 , 0 , 0 , 0 )")) - self.assertEqual((255, 0, 0), - ImageColor.getrgb("hsl( 0 , 100% , 50% )")) - self.assertEqual((255, 0, 0), - ImageColor.getrgb("hsv( 0 , 100% , 100% )")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("rgb( 255 , 0 , 0 )")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("rgb( 100% , 0% , 0% )")) + self.assertEqual( + (255, 0, 0, 0), ImageColor.getrgb("rgba( 255 , 0 , 0 , 0 )") + ) + self.assertEqual((255, 0, 0), ImageColor.getrgb("hsl( 0 , 100% , 50% )")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("hsv( 0 , 100% , 100% )")) # wrong number of components self.assertRaises(ValueError, ImageColor.getrgb, "rgb(255,0)") @@ -153,35 +147,32 @@ class TestImageColor(PillowTestCase): def test_rounding_errors(self): for color in ImageColor.colormap: - expected = Image.new( - "RGB", (1, 1), color).convert("L").getpixel((0, 0)) - actual = ImageColor.getcolor(color, 'L') + expected = Image.new("RGB", (1, 1), color).convert("L").getpixel((0, 0)) + actual = ImageColor.getcolor(color, "L") self.assertEqual(expected, actual) self.assertEqual( - (0, 255, 115), ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGB")) + (0, 255, 115), ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGB") + ) Image.new("RGB", (1, 1), "white") self.assertEqual((0, 0, 0, 255), ImageColor.getcolor("black", "RGBA")) + self.assertEqual((255, 255, 255, 255), ImageColor.getcolor("white", "RGBA")) self.assertEqual( - (255, 255, 255, 255), ImageColor.getcolor("white", "RGBA")) - self.assertEqual( - (0, 255, 115, 33), - ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGBA")) + (0, 255, 115, 33), ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGBA") + ) Image.new("RGBA", (1, 1), "white") self.assertEqual(0, ImageColor.getcolor("black", "L")) self.assertEqual(255, ImageColor.getcolor("white", "L")) - self.assertEqual(162, - ImageColor.getcolor("rgba(0, 255, 115, 33)", "L")) + self.assertEqual(162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "L")) Image.new("L", (1, 1), "white") self.assertEqual(0, ImageColor.getcolor("black", "1")) self.assertEqual(255, ImageColor.getcolor("white", "1")) # The following test is wrong, but is current behavior # The correct result should be 255 due to the mode 1 - self.assertEqual( - 162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) + self.assertEqual(162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) # Correct behavior # self.assertEqual( # 255, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) @@ -189,6 +180,5 @@ class TestImageColor(PillowTestCase): self.assertEqual((0, 255), ImageColor.getcolor("black", "LA")) self.assertEqual((255, 255), ImageColor.getcolor("white", "LA")) - self.assertEqual( - (162, 33), ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA")) + self.assertEqual((162, 33), ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA")) Image.new("LA", (1, 1), "white") diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index bceb0e3d4..50a774df2 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,13 +1,15 @@ import os.path +import unittest + +from PIL import Image, ImageColor, ImageDraw, ImageFont, features from .helper import PillowTestCase, hopper -from PIL import Image, ImageColor, ImageDraw BLACK = (0, 0, 0) WHITE = (255, 255, 255) GRAY = (190, 190, 190) -DEFAULT_MODE = 'RGB' -IMAGES_PATH = os.path.join('Tests', 'images', 'imagedraw') +DEFAULT_MODE = "RGB" +IMAGES_PATH = os.path.join("Tests", "images", "imagedraw") # Image size W, H = 100, 100 @@ -28,9 +30,10 @@ POINTS2 = [10, 10, 20, 40, 30, 30] KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] +HAS_FREETYPE = features.check("freetype2") + class TestImageDraw(PillowTestCase): - def test_sanity(self): im = hopper("RGB").copy() @@ -43,10 +46,10 @@ class TestImageDraw(PillowTestCase): draw.rectangle(list(range(4))) def test_valueerror(self): - im = Image.open("Tests/images/chi.gif") + with Image.open("Tests/images/chi.gif") as im: - draw = ImageDraw.Draw(im) - draw.line((0, 0), fill=(0, 0, 0)) + draw = ImageDraw.Draw(im) + draw.line((0, 0), fill=(0, 0, 0)) def test_mode_mismatch(self): im = hopper("RGB").copy() @@ -62,8 +65,7 @@ class TestImageDraw(PillowTestCase): draw.arc(bbox, start, end) # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_arc.png"), 1) + self.assert_image_similar(im, Image.open("Tests/images/imagedraw_arc.png"), 1) def test_arc1(self): self.helper_arc(BBOX1, 0, 180) @@ -85,7 +87,8 @@ class TestImageDraw(PillowTestCase): # Assert self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_arc_end_le_start.png")) + im, Image.open("Tests/images/imagedraw_arc_end_le_start.png") + ) def test_arc_no_loops(self): # No need to go in loops @@ -100,7 +103,8 @@ class TestImageDraw(PillowTestCase): # Assert self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_arc_no_loops.png"), 1) + im, Image.open("Tests/images/imagedraw_arc_no_loops.png"), 1 + ) def test_arc_width(self): # Arrange @@ -114,6 +118,19 @@ class TestImageDraw(PillowTestCase): # Assert self.assert_image_similar(im, Image.open(expected), 1) + def test_arc_width_pieslice_large(self): + # Tests an arc with a large enough width that it is a pieslice + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_arc_width_pieslice.png" + + # Act + draw.arc(BBOX1, 10, 260, fill="yellow", width=100) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + def test_arc_width_fill(self): # Arrange im = Image.new("RGB", (W, H)) @@ -126,6 +143,18 @@ class TestImageDraw(PillowTestCase): # Assert self.assert_image_similar(im, Image.open(expected), 1) + def test_arc_width_non_whole_angle(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png" + + # Act + draw.arc(BBOX1, 10, 259.5, width=5) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + def test_bitmap(self): # Arrange small = Image.open("Tests/images/pil123rgba.png").resize((50, 50)) @@ -136,8 +165,7 @@ class TestImageDraw(PillowTestCase): draw.bitmap((10, 10), small) # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_bitmap.png")) + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_bitmap.png")) def helper_chord(self, mode, bbox, start, end): # Arrange @@ -211,17 +239,15 @@ class TestImageDraw(PillowTestCase): draw = ImageDraw.Draw(im) # Act - draw.ellipse(((0, 0), (W-1, H)), fill="white") + draw.ellipse(((0, 0), (W - 1, H)), fill="white") # Assert self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) + im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1 + ) def test_ellipse_symmetric(self): - for bbox in [ - (25, 25, 76, 76), - (25, 25, 75, 75) - ]: + for bbox in [(25, 25, 76, 76), (25, 25, 75, 75)]: im = Image.new("RGB", (101, 101)) draw = ImageDraw.Draw(im) draw.ellipse(bbox, fill="green", outline="blue") @@ -239,6 +265,18 @@ class TestImageDraw(PillowTestCase): # Assert self.assert_image_similar(im, Image.open(expected), 1) + def test_ellipse_width_large(self): + # Arrange + im = Image.new("RGB", (500, 500)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_ellipse_width_large.png" + + # Act + draw.ellipse((25, 25, 475, 475), outline="blue", width=75) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + def test_ellipse_width_fill(self): # Arrange im = Image.new("RGB", (W, H)) @@ -260,8 +298,7 @@ class TestImageDraw(PillowTestCase): draw.line(points, fill="yellow", width=2) # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_line.png")) + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_line.png")) def test_line1(self): self.helper_line(POINTS1) @@ -287,8 +324,7 @@ class TestImageDraw(PillowTestCase): draw.shape(s, fill=1) # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_shape1.png")) + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_shape1.png")) def test_shape2(self): # Arrange @@ -308,8 +344,7 @@ class TestImageDraw(PillowTestCase): draw.shape(s, outline="blue") # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_shape2.png")) + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_shape2.png")) def helper_pieslice(self, bbox, start, end): # Arrange @@ -321,7 +356,8 @@ class TestImageDraw(PillowTestCase): # Assert self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_pieslice.png"), 1) + im, Image.open("Tests/images/imagedraw_pieslice.png"), 1 + ) def test_pieslice1(self): self.helper_pieslice(BBOX1, -90, 45) @@ -364,8 +400,7 @@ class TestImageDraw(PillowTestCase): draw.point(points, fill="yellow") # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_point.png")) + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_point.png")) def test_point1(self): self.helper_point(POINTS1) @@ -382,8 +417,7 @@ class TestImageDraw(PillowTestCase): draw.polygon(points, fill="red", outline="blue") # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_polygon.png")) + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_polygon.png")) def test_polygon1(self): self.helper_polygon(POINTS1) @@ -398,8 +432,7 @@ class TestImageDraw(PillowTestCase): # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_polygon_kite_{}.png".format( - mode) + expected = "Tests/images/imagedraw_polygon_kite_{}.png".format(mode) # Act draw.polygon(KITE_POINTS, fill="blue", outline="yellow") @@ -416,8 +449,7 @@ class TestImageDraw(PillowTestCase): draw.rectangle(bbox, fill="black", outline="green") # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_rectangle.png")) + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_rectangle.png")) def test_rectangle1(self): self.helper_rectangle(BBOX1) @@ -429,7 +461,7 @@ class TestImageDraw(PillowTestCase): # Test drawing a rectangle bigger than the image # Arrange im = Image.new("RGB", (W, H)) - bbox = [(-1, -1), (W+1, H+1)] + bbox = [(-1, -1), (W + 1, H + 1)] draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_big_rectangle.png" @@ -463,25 +495,34 @@ class TestImageDraw(PillowTestCase): # Assert self.assert_image_equal(im, Image.open(expected)) + def test_rectangle_I16(self): + # Arrange + im = Image.new("I;16", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(BBOX1, fill="black", outline="green") + + # Assert + self.assert_image_equal( + im.convert("I"), Image.open("Tests/images/imagedraw_rectangle_I.png") + ) + def test_floodfill(self): red = ImageColor.getrgb("red") - for mode, value in [ - ("L", 1), - ("RGBA", (255, 0, 0, 0)), - ("RGB", red) - ]: + for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) draw.rectangle(BBOX2, outline="yellow", fill="green") - centre_point = (int(W/2), int(H/2)) + centre_point = (int(W / 2), int(H / 2)) # Act ImageDraw.floodfill(im, centre_point, value) # Assert - expected = "Tests/images/imagedraw_floodfill_"+mode+".png" + expected = "Tests/images/imagedraw_floodfill_" + mode + ".png" im_floodfill = Image.open(expected) self.assert_image_equal(im, im_floodfill) @@ -505,16 +546,18 @@ class TestImageDraw(PillowTestCase): im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) draw.rectangle(BBOX2, outline="yellow", fill="green") - centre_point = (int(W/2), int(H/2)) + centre_point = (int(W / 2), int(H / 2)) # Act ImageDraw.floodfill( - im, centre_point, ImageColor.getrgb("red"), - border=ImageColor.getrgb("black")) + im, + centre_point, + ImageColor.getrgb("red"), + border=ImageColor.getrgb("black"), + ) # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_floodfill2.png")) + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_floodfill2.png")) def test_floodfill_thresh(self): # floodfill() is experimental @@ -523,21 +566,35 @@ class TestImageDraw(PillowTestCase): im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) draw.rectangle(BBOX2, outline="darkgreen", fill="green") - centre_point = (int(W/2), int(H/2)) + centre_point = (int(W / 2), int(H / 2)) # Act - ImageDraw.floodfill( - im, centre_point, ImageColor.getrgb("red"), - thresh=30) + ImageDraw.floodfill(im, centre_point, ImageColor.getrgb("red"), thresh=30) + + # Assert + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_floodfill2.png")) + + def test_floodfill_not_negative(self): + # floodfill() is experimental + # Test that floodfill does not extend into negative coordinates + + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.line((W / 2, 0, W / 2, H / 2), fill="green") + draw.line((0, H / 2, W / 2, H / 2), fill="green") + + # Act + ImageDraw.floodfill(im, (int(W / 4), int(H / 4)), ImageColor.getrgb("red")) # Assert self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_floodfill2.png")) + im, Image.open("Tests/images/imagedraw_floodfill_not_negative.png") + ) - def create_base_image_draw(self, size, - mode=DEFAULT_MODE, - background1=WHITE, - background2=GRAY): + def create_base_image_draw( + self, size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY + ): img = Image.new(mode, size, background1) for x in range(0, size[0]): for y in range(0, size[1]): @@ -546,140 +603,152 @@ class TestImageDraw(PillowTestCase): return img, ImageDraw.Draw(img) def test_square(self): - expected = Image.open(os.path.join(IMAGES_PATH, 'square.png')) + 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') + 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') + 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') + 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') + 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 = 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') + self.assert_image_equal(img, expected, "triangle right failed") def test_line_horizontal(self): - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_horizontal_w2px_normal.png')) + expected = 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(os.path.join(IMAGES_PATH, - 'line_horizontal_w2px_inverted.png')) + img, expected, "line straight horizontal normal 2px wide failed" + ) + expected = 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')) + 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, 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')) + 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') + img, expected, "line straight horizontal 101px wide failed" + ) def test_line_h_s1_w2(self): - self.skipTest('failing') - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_horizontal_slope1px_w2px.png')) + self.skipTest("failing") + expected = 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') + img, expected, "line horizontal 1px slope 2px wide failed" + ) def test_line_vertical(self): - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_vertical_w2px_normal.png')) + expected = 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(os.path.join(IMAGES_PATH, - 'line_vertical_w2px_inverted.png')) + img, expected, "line straight vertical normal 2px wide failed" + ) + expected = 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')) + 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, 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')) + 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(os.path.join(IMAGES_PATH, - 'line_vertical_slope1px_w2px.png')) + self.assert_image_equal( + img, expected, "line straight vertical 101px wide failed" + ) + expected = Image.open( + os.path.join(IMAGES_PATH, "line_vertical_slope1px_w2px.png") + ) 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') + 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 = 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') + 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')) + 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') + 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') + 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 @@ -701,9 +770,22 @@ class TestImageDraw(PillowTestCase): expected = "Tests/images/imagedraw_line_joint_curve.png" # Act - xy = [(400, 280), (380, 280), (450, 280), (440, 120), (350, 200), - (310, 280), (300, 280), (250, 280), (250, 200), (150, 200), - (150, 260), (50, 200), (150, 50), (250, 100)] + xy = [ + (400, 280), + (380, 280), + (450, 280), + (440, 120), + (350, 200), + (310, 280), + (300, 280), + (250, 280), + (250, 200), + (150, 200), + (150, 260), + (50, 200), + (150, 50), + (250, 100), + ] draw.line(xy, GRAY, 50, "curve") # Assert @@ -722,6 +804,54 @@ class TestImageDraw(PillowTestCase): draw.textsize("\n") draw.textsize("test\n") + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_textsize_stroke(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) + + # Act / Assert + self.assertEqual(draw.textsize("A", font, stroke_width=2), (16, 20)) + self.assertEqual( + draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2), (52, 44) + ) + + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_stroke(self): + for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.text( + (10, 10), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill + ) + + # Assert + self.assert_image_similar( + im, Image.open("Tests/images/imagedraw_stroke_" + suffix + ".png"), 3.1 + ) + + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_stroke_multiline(self): + # Arrange + im = Image.new("RGB", (100, 250)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.multiline_text( + (10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0" + ) + + # Assert + self.assert_image_similar( + im, Image.open("Tests/images/imagedraw_stroke_multiline.png"), 3.3 + ) + def test_same_color_outline(self): # Prepare shape x0, y0 = 5, 5 @@ -736,18 +866,14 @@ class TestImageDraw(PillowTestCase): # Begin for mode in ["RGB", "L"]: - for fill, outline in [ - ["red", None], - ["red", "red"], - ["red", "#f00"] - ]: + for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: for operation, args in { - 'chord': [BBOX1, 0, 180], - 'ellipse': [BBOX1], - 'shape': [s], - 'pieslice': [BBOX1, -90, 45], - 'polygon': [[(18, 30), (85, 30), (60, 72)]], - 'rectangle': [BBOX1] + "chord": [BBOX1, 0, 180], + "ellipse": [BBOX1], + "shape": [s], + "pieslice": [BBOX1, -90, 45], + "polygon": [[(18, 30), (85, 30), (60, 72)]], + "rectangle": [BBOX1], }.items(): # Arrange im = Image.new(mode, (W, H)) @@ -759,6 +885,7 @@ class TestImageDraw(PillowTestCase): draw_method(*args) # Assert - expected = ("Tests/images/imagedraw_outline" - "_{}_{}.png".format(operation, mode)) + expected = "Tests/images/imagedraw_outline_{}_{}.png".format( + operation, mode + ) self.assert_image_similar(im, Image.open(expected), 1) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 97033c8a7..577727664 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -1,8 +1,10 @@ import os.path +import unittest -from .helper import PillowTestCase, hopper, unittest from PIL import Image, ImageDraw2, features +from .helper import PillowTestCase, hopper + BLACK = (0, 0, 0) WHITE = (255, 255, 255) GRAY = (190, 190, 190) @@ -33,7 +35,6 @@ FONT_PATH = "Tests/fonts/FreeMono.ttf" class TestImageDraw(PillowTestCase): - def test_sanity(self): im = hopper("RGB").copy() @@ -74,11 +75,12 @@ class TestImageDraw(PillowTestCase): brush = ImageDraw2.Brush("white") # Act - draw.ellipse(((0, 0), (W-1, H)), brush) + draw.ellipse(((0, 0), (W - 1, H)), brush) # Assert self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) + im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1 + ) def helper_line(self, points): # Arrange @@ -90,8 +92,7 @@ class TestImageDraw(PillowTestCase): draw.line(points, pen) # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_line.png")) + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_line.png")) def test_line1_pen(self): self.helper_line(POINTS1) @@ -111,8 +112,7 @@ class TestImageDraw(PillowTestCase): draw.line(POINTS1, pen, brush) # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_line.png")) + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_line.png")) def helper_polygon(self, points): # Arrange @@ -125,8 +125,7 @@ class TestImageDraw(PillowTestCase): draw.polygon(points, pen, brush) # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_polygon.png")) + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_polygon.png")) def test_polygon1(self): self.helper_polygon(POINTS1) @@ -145,8 +144,7 @@ class TestImageDraw(PillowTestCase): draw.rectangle(bbox, pen, brush) # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_rectangle.png")) + self.assert_image_equal(im, Image.open("Tests/images/imagedraw_rectangle.png")) def test_rectangle1(self): self.helper_rectangle(BBOX1) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index 0e4e8c4f3..d0d994eee 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,11 +1,9 @@ -from .helper import PillowTestCase, hopper +from PIL import Image, ImageEnhance -from PIL import Image -from PIL import ImageEnhance +from .helper import PillowTestCase, hopper class TestImageEnhance(PillowTestCase): - def test_sanity(self): # FIXME: assert_image @@ -23,10 +21,10 @@ class TestImageEnhance(PillowTestCase): def _half_transparent_image(self): # returns an image, half transparent, half solid - im = hopper('RGB') + im = hopper("RGB") - transparent = Image.new('L', im.size, 0) - solid = Image.new('L', (im.size[0]//2, im.size[1]), 255) + transparent = Image.new("L", im.size, 0) + solid = Image.new("L", (im.size[0] // 2, im.size[1]), 255) transparent.paste(solid, (0, 0)) im.putalpha(transparent) @@ -34,8 +32,11 @@ class TestImageEnhance(PillowTestCase): def _check_alpha(self, im, original, op, amount): self.assertEqual(im.getbands(), original.getbands()) - self.assert_image_equal(im.getchannel('A'), original.getchannel('A'), - "Diff on %s: %s" % (op, amount)) + self.assert_image_equal( + im.getchannel("A"), + original.getchannel("A"), + "Diff on {}: {}".format(op, amount), + ) def test_alpha(self): # Issue https://github.com/python-pillow/Pillow/issues/899 @@ -43,8 +44,11 @@ class TestImageEnhance(PillowTestCase): original = self._half_transparent_image() - for op in ['Color', 'Brightness', 'Contrast', 'Sharpness']: + for op in ["Color", "Brightness", "Contrast", "Sharpness"]: for amount in [0, 0.5, 1.0]: self._check_alpha( getattr(ImageEnhance, op)(original).enhance(amount), - original, op, amount) + original, + op, + amount, + ) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 5853fb28f..228bbf929 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,10 +1,16 @@ -from .helper import PillowTestCase, hopper, fromstring, tostring - +import unittest from io import BytesIO -from PIL import Image -from PIL import ImageFile -from PIL import EpsImagePlugin +from PIL import EpsImagePlugin, Image, ImageFile + +from .helper import PillowTestCase, fromstring, hopper, tostring + +try: + from PIL import _webp + + HAVE_WEBP = True +except ImportError: + HAVE_WEBP = False codecs = dir(Image.core) @@ -15,9 +21,7 @@ SAFEBLOCK = ImageFile.SAFEBLOCK class TestImageFile(PillowTestCase): - def test_parser(self): - def roundtrip(format): im = hopper("L").resize((1000, 1000)) @@ -38,7 +42,7 @@ class TestImageFile(PillowTestCase): self.assert_image_equal(*roundtrip("BMP")) im1, im2 = roundtrip("GIF") - self.assert_image_similar(im1.convert('P'), im2, 1) + self.assert_image_similar(im1.convert("P"), im2, 1) self.assert_image_equal(*roundtrip("IM")) self.assert_image_equal(*roundtrip("MSP")) if "zip_encoder" in codecs: @@ -63,7 +67,7 @@ class TestImageFile(PillowTestCase): # md5sum: ba974835ff2d6f3f2fd0053a23521d4a # EPS comes back in RGB: - self.assert_image_similar(im1, im2.convert('L'), 20) + self.assert_image_similar(im1, im2.convert("L"), 20) if "jpeg_encoder" in codecs: im1, im2 = roundtrip("JPEG") # lossy compression @@ -72,7 +76,7 @@ class TestImageFile(PillowTestCase): self.assertRaises(IOError, roundtrip, "PDF") def test_ico(self): - with open('Tests/images/python.ico', 'rb') as f: + with open("Tests/images/python.ico", "rb") as f: data = f.read() with ImageFile.Parser() as p: p.feed(data) @@ -100,13 +104,25 @@ class TestImageFile(PillowTestCase): parser = ImageFile.Parser() parser.feed(1) + def test_negative_stride(self): + with open("Tests/images/raw_negative_stride.bin", "rb") as f: + input = f.read() + p = ImageFile.Parser() + p.feed(input) + with self.assertRaises(IOError): + p.close() + def test_truncated_with_errors(self): if "zip_encoder" not in codecs: self.skipTest("PNG (zlib) encoder not available") - im = Image.open("Tests/images/truncated_image.png") - with self.assertRaises(IOError): - im.load() + with Image.open("Tests/images/truncated_image.png") as im: + with self.assertRaises(IOError): + im.load() + + # Test that the error is raised if loaded a second time + with self.assertRaises(IOError): + im.load() def test_truncated_without_errors(self): if "zip_encoder" not in codecs: @@ -152,14 +168,13 @@ xoff, yoff, xsize, ysize = 10, 20, 100, 100 class MockImageFile(ImageFile.ImageFile): def _open(self): - self.rawmode = 'RGBA' - self.mode = 'RGBA' + self.rawmode = "RGBA" + self.mode = "RGBA" self._size = (200, 200) - self.tile = [("MOCK", (xoff, yoff, xoff+xsize, yoff+ysize), 32, None)] + self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] class TestPyDecoder(PillowTestCase): - def get_decoder(self): decoder = MockPyDecoder(None) @@ -167,11 +182,11 @@ class TestPyDecoder(PillowTestCase): decoder.__init__(mode, *args) return decoder - Image.register_decoder('MOCK', closure) + Image.register_decoder("MOCK", closure) return decoder def test_setimage(self): - buf = BytesIO(b'\x00'*255) + buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) d = self.get_decoder() @@ -183,10 +198,10 @@ class TestPyDecoder(PillowTestCase): self.assertEqual(d.state.xsize, xsize) self.assertEqual(d.state.ysize, ysize) - self.assertRaises(ValueError, d.set_as_raw, b'\x00') + self.assertRaises(ValueError, d.set_as_raw, b"\x00") def test_extents_none(self): - buf = BytesIO(b'\x00'*255) + buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) im.tile = [("MOCK", None, 32, None)] @@ -200,36 +215,122 @@ class TestPyDecoder(PillowTestCase): self.assertEqual(d.state.ysize, 200) def test_negsize(self): - buf = BytesIO(b'\x00'*255) + buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [("MOCK", (xoff, yoff, -10, yoff+ysize), 32, None)] + im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] self.get_decoder() self.assertRaises(ValueError, im.load) - im.tile = [("MOCK", (xoff, yoff, xoff+xsize, -10), 32, None)] + im.tile = [("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)] self.assertRaises(ValueError, im.load) def test_oversize(self): - buf = BytesIO(b'\x00'*255) + buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [ - ("MOCK", (xoff, yoff, xoff+xsize + 100, yoff+ysize), 32, None) - ] + im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] self.get_decoder() self.assertRaises(ValueError, im.load) - im.tile = [ - ("MOCK", (xoff, yoff, xoff+xsize, yoff+ysize + 100), 32, None) - ] + im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)] self.assertRaises(ValueError, im.load) def test_no_format(self): - buf = BytesIO(b'\x00'*255) + buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) self.assertIsNone(im.format) self.assertIsNone(im.get_format_mimetype()) + + def test_exif_jpeg(self): + im = Image.open("Tests/images/exif-72dpi-int.jpg") # Little endian + exif = im.getexif() + self.assertNotIn(258, exif) + self.assertIn(40960, exif) + self.assertEqual(exif[40963], 450) + self.assertEqual(exif[11], "gThumb 3.0.1") + + out = self.tempfile("temp.jpg") + exif[258] = 8 + del exif[40960] + exif[40963] = 455 + exif[11] = "Pillow test" + im.save(out, exif=exif) + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + self.assertEqual(reloaded_exif[258], 8) + self.assertNotIn(40960, exif) + self.assertEqual(reloaded_exif[40963], 455) + self.assertEqual(exif[11], "Pillow test") + + im = Image.open("Tests/images/no-dpi-in-exif.jpg") # Big endian + exif = im.getexif() + self.assertNotIn(258, exif) + self.assertIn(40962, exif) + self.assertEqual(exif[40963], 200) + self.assertEqual(exif[305], "Adobe Photoshop CC 2017 (Macintosh)") + + out = self.tempfile("temp.jpg") + exif[258] = 8 + del exif[34665] + exif[40963] = 455 + exif[305] = "Pillow test" + im.save(out, exif=exif) + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + self.assertEqual(reloaded_exif[258], 8) + self.assertNotIn(40960, exif) + self.assertEqual(reloaded_exif[40963], 455) + self.assertEqual(exif[305], "Pillow test") + + @unittest.skipIf( + not HAVE_WEBP or not _webp.HAVE_WEBPANIM, + "WebP support not installed with animation", + ) + def test_exif_webp(self): + im = Image.open("Tests/images/hopper.webp") + exif = im.getexif() + self.assertEqual(exif, {}) + + out = self.tempfile("temp.webp") + exif[258] = 8 + exif[40963] = 455 + exif[305] = "Pillow test" + + def check_exif(): + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + self.assertEqual(reloaded_exif[258], 8) + self.assertEqual(reloaded_exif[40963], 455) + self.assertEqual(exif[305], "Pillow test") + + im.save(out, exif=exif) + check_exif() + im.save(out, exif=exif, save_all=True) + check_exif() + + def test_exif_png(self): + im = Image.open("Tests/images/exif.png") + exif = im.getexif() + self.assertEqual(exif, {274: 1}) + + out = self.tempfile("temp.png") + exif[258] = 8 + del exif[274] + exif[40963] = 455 + exif[305] = "Pillow test" + im.save(out, exif=exif) + + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + self.assertEqual(reloaded_exif, {258: 8, 40963: 455, 305: "Pillow test"}) + + def test_exif_interop(self): + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + self.assertEqual( + exif.get_ifd(0xA005), {1: "R98", 2: b"0100", 4097: 2272, 4098: 1704} + ) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index be8667211..5a15eb535 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,22 +1,26 @@ -# -*- coding: utf-8 -*- -from .helper import unittest, PillowTestCase +import copy +import distutils.version +import os +import re +import shutil +import sys +import unittest +from io import BytesIO from PIL import Image, ImageDraw, ImageFont, features -from io import BytesIO -import os -import sys -import copy + +from .helper import PillowTestCase, is_pypy, is_win32 FONT_PATH = "Tests/fonts/FreeMono.ttf" FONT_SIZE = 20 TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" -HAS_FREETYPE = features.check('freetype2') -HAS_RAQM = features.check('raqm') +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 @@ -49,33 +53,37 @@ class TestImageFont(PillowTestCase): # Freetype has different metrics depending on the version. # (and, other things, but first things first) METRICS = { - ('2', '3'): {'multiline': 30, - 'textsize': 12, - 'getters': (13, 16)}, - ('2', '7'): {'multiline': 6.2, - 'textsize': 2.5, - 'getters': (12, 16)}, - ('2', '8'): {'multiline': 6.2, - 'textsize': 2.5, - 'getters': (12, 16)}, - ('2', '9'): {'multiline': 6.2, - 'textsize': 2.5, - 'getters': (12, 16)}, - 'Default': {'multiline': 0.5, - 'textsize': 0.5, - 'getters': (12, 16)}, - } + (">=2.3", "<2.4"): {"multiline": 30, "textsize": 12, "getters": (13, 16)}, + (">=2.7",): {"multiline": 6.2, "textsize": 2.5, "getters": (12, 16)}, + "Default": {"multiline": 0.5, "textsize": 0.5, "getters": (12, 16)}, + } def setUp(self): - freetype_version = tuple( - ImageFont.core.freetype2_version.split('.') - )[:2] - self.metrics = self.METRICS.get(freetype_version, - self.METRICS['Default']) + freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) + + self.metrics = self.METRICS["Default"] + for conditions, metrics in self.METRICS.items(): + if not isinstance(conditions, tuple): + continue + + for condition in conditions: + version = re.sub("[<=>]", "", condition) + if (condition.startswith(">=") and freetype >= version) or ( + condition.startswith("<") and freetype < version + ): + # Condition was met + continue + + # Condition failed + break + else: + # All conditions were met + self.metrics = metrics def get_font(self): - return ImageFont.truetype(FONT_PATH, FONT_SIZE, - layout_engine=self.LAYOUT_ENGINE) + return ImageFont.truetype( + FONT_PATH, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE + ) def test_sanity(self): self.assertRegex(ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") @@ -89,8 +97,8 @@ class TestImageFont(PillowTestCase): self.assertEqual(ttf_copy.path, FONT_PATH) self.assertEqual(ttf_copy.size, FONT_SIZE) - ttf_copy = ttf.font_variant(size=FONT_SIZE+1) - self.assertEqual(ttf_copy.size, FONT_SIZE+1) + ttf_copy = ttf.font_variant(size=FONT_SIZE + 1) + self.assertEqual(ttf_copy.size, FONT_SIZE + 1) second_font_path = "Tests/fonts/DejaVuSans.ttf" ttf_copy = ttf.font_variant(font=second_font_path) @@ -101,13 +109,14 @@ class TestImageFont(PillowTestCase): self._render(FONT_PATH) def _font_as_bytes(self): - with open(FONT_PATH, 'rb') as f: + with open(FONT_PATH, "rb") as f: font_bytes = BytesIO(f.read()) return font_bytes def test_font_with_filelike(self): - ImageFont.truetype(self._font_as_bytes(), FONT_SIZE, - layout_engine=self.LAYOUT_ENGINE) + ImageFont.truetype( + self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE + ) self._render(self._font_as_bytes()) # Usage note: making two fonts from the same buffer fails. # shared_bytes = self._font_as_bytes() @@ -115,31 +124,52 @@ class TestImageFont(PillowTestCase): # self.assertRaises(Exception, _render, shared_bytes) def test_font_with_open_file(self): - with open(FONT_PATH, 'rb') as f: + with open(FONT_PATH, "rb") as f: self._render(f) + def test_non_unicode_path(self): + try: + tempfile = self.tempfile("temp_" + chr(128) + ".ttf") + except UnicodeEncodeError: + self.skipTest("Unicode path could not be created") + shutil.copy(FONT_PATH, tempfile) + + ImageFont.truetype(tempfile, FONT_SIZE) + + def test_unavailable_layout_engine(self): + have_raqm = ImageFont.core.HAVE_RAQM + ImageFont.core.HAVE_RAQM = False + + try: + ttf = ImageFont.truetype( + FONT_PATH, FONT_SIZE, layout_engine=ImageFont.LAYOUT_RAQM + ) + finally: + ImageFont.core.HAVE_RAQM = have_raqm + + self.assertEqual(ttf.layout_engine, ImageFont.LAYOUT_BASIC) + def _render(self, font): txt = "Hello World!" - ttf = ImageFont.truetype(font, FONT_SIZE, - layout_engine=self.LAYOUT_ENGINE) + ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE) ttf.getsize(txt) img = Image.new("RGB", (256, 64), "white") d = ImageDraw.Draw(img) - d.text((10, 10), txt, font=ttf, fill='black') + d.text((10, 10), txt, font=ttf, fill="black") return img def test_render_equal(self): img_path = self._render(FONT_PATH) - with open(FONT_PATH, 'rb') as f: + with open(FONT_PATH, "rb") as f: font_filelike = BytesIO(f.read()) img_filelike = self._render(font_filelike) self.assert_image_equal(img_path, img_filelike) def test_textsize_equal(self): - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) ttf = self.get_font() @@ -148,96 +178,101 @@ class TestImageFont(PillowTestCase): draw.text((10, 10), txt, font=ttf) draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) - target = 'Tests/images/rectangle_surrounding_text.png' + target = "Tests/images/rectangle_surrounding_text.png" target_img = Image.open(target) # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics['textsize']) + self.assert_image_similar(im, target_img, self.metrics["textsize"]) def test_render_multiline(self): - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) ttf = self.get_font() - line_spacing = draw.textsize('A', font=ttf)[1] + 4 + line_spacing = draw.textsize("A", font=ttf)[1] + 4 lines = TEST_TEXT.split("\n") y = 0 for line in lines: draw.text((0, y), line, font=ttf) y += line_spacing - target = 'Tests/images/multiline_text.png' + target = "Tests/images/multiline_text.png" target_img = Image.open(target) # 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']) + self.assert_image_similar(im, target_img, self.metrics["multiline"]) def test_render_multiline_text(self): ttf = self.get_font() # Test that text() correctly connects to multiline_text() # and that align defaults to left - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.text((0, 0), TEST_TEXT, font=ttf) - target = 'Tests/images/multiline_text.png' + target = "Tests/images/multiline_text.png" target_img = Image.open(target) # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics['multiline']) + self.assert_image_similar(im, target_img, self.metrics["multiline"]) # Test that text() can pass on additional arguments # to multiline_text() - draw.text((0, 0), TEST_TEXT, fill=None, font=ttf, anchor=None, - spacing=4, align="left") + draw.text( + (0, 0), TEST_TEXT, fill=None, font=ttf, anchor=None, spacing=4, align="left" + ) draw.text((0, 0), TEST_TEXT, None, ttf, None, 4, "left") # Test align center and right - for align, ext in {"center": "_center", - "right": "_right"}.items(): - im = Image.new(mode='RGB', size=(300, 100)) + for align, ext in {"center": "_center", "right": "_right"}.items(): + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align) - target = 'Tests/images/multiline_text'+ext+'.png' + target = "Tests/images/multiline_text" + ext + ".png" target_img = Image.open(target) # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, - self.metrics['multiline']) + self.assert_image_similar(im, target_img, self.metrics["multiline"]) def test_unknown_align(self): - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) ttf = self.get_font() # Act/Assert self.assertRaises( ValueError, - draw.multiline_text, (0, 0), TEST_TEXT, font=ttf, align="unknown") + draw.multiline_text, + (0, 0), + TEST_TEXT, + font=ttf, + align="unknown", + ) def test_draw_align(self): - im = Image.new('RGB', (300, 100), 'white') + im = Image.new("RGB", (300, 100), "white") draw = ImageDraw.Draw(im) ttf = self.get_font() line = "some text" - draw.text((100, 40), line, (0, 0, 0), font=ttf, align='left') + draw.text((100, 40), line, (0, 0, 0), font=ttf, align="left") def test_multiline_size(self): ttf = self.get_font() - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) # Test that textsize() correctly connects to multiline_textsize() - self.assertEqual(draw.textsize(TEST_TEXT, font=ttf), - draw.multiline_textsize(TEST_TEXT, font=ttf)) + self.assertEqual( + draw.textsize(TEST_TEXT, font=ttf), + draw.multiline_textsize(TEST_TEXT, font=ttf), + ) # Test that multiline_textsize corresponds to ImageFont.textsize() # for single line text - self.assertEqual(ttf.getsize('A'), - draw.multiline_textsize('A', font=ttf)) + self.assertEqual(ttf.getsize("A"), draw.multiline_textsize("A", font=ttf)) # Test that textsize() can pass on additional arguments # to multiline_textsize() @@ -246,25 +281,26 @@ class TestImageFont(PillowTestCase): def test_multiline_width(self): ttf = self.get_font() - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - self.assertEqual(draw.textsize("longest line", font=ttf)[0], - draw.multiline_textsize("longest line\nline", - font=ttf)[0]) + self.assertEqual( + draw.textsize("longest line", font=ttf)[0], + draw.multiline_textsize("longest line\nline", font=ttf)[0], + ) def test_multiline_spacing(self): ttf = self.get_font() - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) - target = 'Tests/images/multiline_text_spacing.png' + target = "Tests/images/multiline_text_spacing.png" target_img = Image.open(target) # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics['multiline']) + self.assert_image_similar(im, target_img, self.metrics["multiline"]) def test_rotated_transposed_font(self): img_grey = Image.new("L", (100, 100)) @@ -273,8 +309,7 @@ class TestImageFont(PillowTestCase): font = self.get_font() orientation = Image.ROTATE_90 - transposed_font = ImageFont.TransposedFont( - font, orientation=orientation) + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) # Original font draw.font = font @@ -295,8 +330,7 @@ class TestImageFont(PillowTestCase): font = self.get_font() orientation = None - transposed_font = ImageFont.TransposedFont( - font, orientation=orientation) + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) # Original font draw.font = font @@ -314,8 +348,7 @@ class TestImageFont(PillowTestCase): text = "mask this" font = self.get_font() orientation = Image.ROTATE_90 - transposed_font = ImageFont.TransposedFont( - font, orientation=orientation) + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) # Act mask = transposed_font.getmask(text) @@ -328,8 +361,7 @@ class TestImageFont(PillowTestCase): text = "mask this" font = self.get_font() orientation = None - transposed_font = ImageFont.TransposedFont( - font, orientation=orientation) + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) # Act mask = transposed_font.getmask(text) @@ -345,7 +377,7 @@ class TestImageFont(PillowTestCase): name = font.getname() # Assert - self.assertEqual(('FreeMono', 'Regular'), name) + self.assertEqual(("FreeMono", "Regular"), name) def test_free_type_font_get_metrics(self): # Arrange @@ -387,14 +419,19 @@ class TestImageFont(PillowTestCase): # Act/Assert self.assertRaises(IOError, ImageFont.load_path, filename) + self.assertRaises(IOError, ImageFont.truetype, filename) + + def test_load_non_font_bytes(self): + with open("Tests/images/hopper.jpg", "rb") as f: + self.assertRaises(IOError, ImageFont.truetype, f) def test_default_font(self): # Arrange txt = 'This is a "better than nothing" default font.' - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - target = 'Tests/images/default_font.png' + target = "Tests/images/default_font.png" target_img = Image.open(target) # Act @@ -408,16 +445,16 @@ class TestImageFont(PillowTestCase): # issue #2614 font = self.get_font() # should not crash. - self.assertEqual((0, 0), font.getsize('')) + self.assertEqual((0, 0), font.getsize("")) def test_render_empty(self): # issue 2666 font = self.get_font() - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) target = im.copy() draw = ImageDraw.Draw(im) # should not crash here. - draw.text((10, 10), '', font=font) + draw.text((10, 10), "", font=font) self.assert_image_equal(im, target) def test_unicode_pilfont(self): @@ -425,81 +462,121 @@ class TestImageFont(PillowTestCase): # issue #2826 font = ImageFont.load_default() with self.assertRaises(UnicodeEncodeError): - font.getsize(u"’") + font.getsize("’") + + @unittest.skipIf(is_pypy(), "failing on PyPy") + def test_unicode_extended(self): + # issue #3777 + text = "A\u278A\U0001F12B" + target = "Tests/images/unicode_extended.png" + + ttf = ImageFont.truetype( + "Tests/fonts/NotoSansSymbols-Regular.ttf", + FONT_SIZE, + layout_engine=self.LAYOUT_ENGINE, + ) + img = Image.new("RGB", (100, 60)) + d = ImageDraw.Draw(img) + d.text((10, 10), text, font=ttf) + + self.assert_image_similar_tofile(img, target, self.metrics["multiline"]) def _test_fake_loading_font(self, path_to_fake, fontname): # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) - with SimplePatcher(ImageFont, '_FreeTypeFont', free_type_font): - def loadable_font(filepath, size, index, encoding, - *args, **kwargs): + with SimplePatcher(ImageFont, "_FreeTypeFont", free_type_font): + + def loadable_font(filepath, size, index, encoding, *args, **kwargs): if filepath == path_to_fake: - return ImageFont._FreeTypeFont(FONT_PATH, size, index, - encoding, *args, **kwargs) - return ImageFont._FreeTypeFont(filepath, size, index, - encoding, *args, **kwargs) - with SimplePatcher(ImageFont, 'FreeTypeFont', loadable_font): + return ImageFont._FreeTypeFont( + FONT_PATH, size, index, encoding, *args, **kwargs + ) + return ImageFont._FreeTypeFont( + filepath, size, index, encoding, *args, **kwargs + ) + + with SimplePatcher(ImageFont, "FreeTypeFont", loadable_font): font = ImageFont.truetype(fontname) # Make sure it's loaded name = font.getname() - self.assertEqual(('FreeMono', 'Regular'), name) + 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 - font_directory = '/usr/local/share/fonts' - with SimplePatcher(sys, 'platform', 'linux'): + font_directory = "/usr/local/share/fonts" + with SimplePatcher(sys, "platform", "linux"): patched_env = copy.deepcopy(os.environ) - patched_env['XDG_DATA_DIRS'] = '/usr/share/:/usr/local/share/' - with SimplePatcher(os, 'environ', patched_env): + patched_env["XDG_DATA_DIRS"] = "/usr/share/:/usr/local/share/" + with SimplePatcher(os, "environ", patched_env): + def fake_walker(path): if path == font_directory: - return [(path, [], [ - 'Arial.ttf', 'Single.otf', 'Duplicate.otf', - 'Duplicate.ttf'], )] - return [(path, [], ['some_random_font.ttf'], )] - with SimplePatcher(os, 'walk', fake_walker): + return [ + ( + path, + [], + [ + "Arial.ttf", + "Single.otf", + "Duplicate.otf", + "Duplicate.ttf", + ], + ) + ] + return [(path, [], ["some_random_font.ttf"])] + + with SimplePatcher(os, "walk", fake_walker): # Test that the font loads both with and without the # extension self._test_fake_loading_font( - font_directory+'/Arial.ttf', 'Arial.ttf') - self._test_fake_loading_font( - font_directory+'/Arial.ttf', 'Arial') + font_directory + "/Arial.ttf", "Arial.ttf" + ) + self._test_fake_loading_font(font_directory + "/Arial.ttf", "Arial") # Test that non-ttf fonts can be found without the # extension self._test_fake_loading_font( - font_directory+'/Single.otf', 'Single') + font_directory + "/Single.otf", "Single" + ) # Test that ttf fonts are preferred if the extension is # not specified self._test_fake_loading_font( - font_directory+'/Duplicate.ttf', 'Duplicate') + 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. - font_directory = '/System/Library/Fonts' - with SimplePatcher(sys, 'platform', 'darwin'): + font_directory = "/System/Library/Fonts" + with SimplePatcher(sys, "platform", "darwin"): + def fake_walker(path): if path == font_directory: - return [(path, [], - ['Arial.ttf', 'Single.otf', - 'Duplicate.otf', 'Duplicate.ttf'], )] - return [(path, [], ['some_random_font.ttf'], )] - with SimplePatcher(os, 'walk', fake_walker): + return [ + ( + path, + [], + [ + "Arial.ttf", + "Single.otf", + "Duplicate.otf", + "Duplicate.ttf", + ], + ) + ] + return [(path, [], ["some_random_font.ttf"])] + + with SimplePatcher(os, "walk", fake_walker): + self._test_fake_loading_font(font_directory + "/Arial.ttf", "Arial.ttf") + self._test_fake_loading_font(font_directory + "/Arial.ttf", "Arial") + self._test_fake_loading_font(font_directory + "/Single.otf", "Single") self._test_fake_loading_font( - font_directory+'/Arial.ttf', 'Arial.ttf') - self._test_fake_loading_font( - font_directory+'/Arial.ttf', 'Arial') - self._test_fake_loading_font( - font_directory+'/Single.otf', 'Single') - self._test_fake_loading_font( - font_directory+'/Duplicate.ttf', 'Duplicate') + font_directory + "/Duplicate.ttf", "Duplicate" + ) def test_imagefont_getters(self): # Arrange @@ -513,17 +590,159 @@ class TestImageFont(PillowTestCase): self.assertEqual(t.font.x_ppem, 20) self.assertEqual(t.font.y_ppem, 20) self.assertEqual(t.font.glyphs, 4177) - self.assertEqual(t.getsize('A'), (12, 16)) - self.assertEqual(t.getsize('AB'), (24, 16)) - self.assertEqual(t.getsize('M'), self.metrics['getters']) - self.assertEqual(t.getsize('y'), (12, 20)) - self.assertEqual(t.getsize('a'), (12, 16)) - self.assertEqual(t.getsize_multiline('A'), (12, 16)) - self.assertEqual(t.getsize_multiline('AB'), (24, 16)) - self.assertEqual(t.getsize_multiline('a'), (12, 16)) - self.assertEqual(t.getsize_multiline('ABC\n'), (36, 36)) - self.assertEqual(t.getsize_multiline('ABC\nA'), (36, 36)) - self.assertEqual(t.getsize_multiline('ABC\nAaaa'), (48, 36)) + self.assertEqual(t.getsize("A"), (12, 16)) + self.assertEqual(t.getsize("AB"), (24, 16)) + self.assertEqual(t.getsize("M"), self.metrics["getters"]) + self.assertEqual(t.getsize("y"), (12, 20)) + self.assertEqual(t.getsize("a"), (12, 16)) + self.assertEqual(t.getsize_multiline("A"), (12, 16)) + self.assertEqual(t.getsize_multiline("AB"), (24, 16)) + self.assertEqual(t.getsize_multiline("a"), (12, 16)) + self.assertEqual(t.getsize_multiline("ABC\n"), (36, 36)) + self.assertEqual(t.getsize_multiline("ABC\nA"), (36, 36)) + self.assertEqual(t.getsize_multiline("ABC\nAaaa"), (48, 36)) + + def test_getsize_stroke(self): + # Arrange + t = self.get_font() + + # Act / Assert + for stroke_width in [0, 2]: + self.assertEqual( + t.getsize("A", stroke_width=stroke_width), + (12 + stroke_width * 2, 16 + stroke_width * 2), + ) + self.assertEqual( + t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width), + (48 + stroke_width * 2, 36 + stroke_width * 4), + ) + + def test_complex_font_settings(self): + # Arrange + t = self.get_font() + # Act / Assert + if t.layout_engine == ImageFont.LAYOUT_BASIC: + self.assertRaises(KeyError, t.getmask, "абвг", direction="rtl") + self.assertRaises(KeyError, t.getmask, "абвг", features=["-kern"]) + self.assertRaises(KeyError, t.getmask, "абвг", language="sr") + + def test_variation_get(self): + font = self.get_font() + + freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) + if freetype < "2.9.1": + self.assertRaises(NotImplementedError, font.get_variation_names) + self.assertRaises(NotImplementedError, font.get_variation_axes) + return + + self.assertRaises(IOError, font.get_variation_names) + self.assertRaises(IOError, font.get_variation_axes) + + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf") + self.assertEqual( + font.get_variation_names(), + [ + b"ExtraLight", + b"Light", + b"Regular", + b"Semibold", + b"Bold", + b"Black", + b"Black Medium Contrast", + b"Black High Contrast", + b"Default", + ], + ) + self.assertEqual( + font.get_variation_axes(), + [ + {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389}, + {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0}, + ], + ) + + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf") + self.assertEqual( + font.get_variation_names(), + [ + b"20", + b"40", + b"60", + b"80", + b"100", + b"120", + b"140", + b"160", + b"180", + b"200", + b"220", + b"240", + b"260", + b"280", + b"300", + b"Regular", + ], + ) + self.assertEqual( + font.get_variation_axes(), + [{"name": b"Size", "minimum": 0, "maximum": 300, "default": 0}], + ) + + def test_variation_set_by_name(self): + font = self.get_font() + + freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) + if freetype < "2.9.1": + self.assertRaises(NotImplementedError, font.set_variation_by_name, "Bold") + return + + self.assertRaises(IOError, font.set_variation_by_name, "Bold") + + def _check_text(font, path, epsilon): + im = Image.new("RGB", (100, 75), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), "Text", font=font, fill="black") + + expected = Image.open(path) + self.assert_image_similar(im, expected, epsilon) + + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) + _check_text(font, "Tests/images/variation_adobe.png", 11) + for name in ["Bold", b"Bold"]: + font.set_variation_by_name(name) + _check_text(font, "Tests/images/variation_adobe_name.png", 11) + + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) + _check_text(font, "Tests/images/variation_tiny.png", 40) + for name in ["200", b"200"]: + font.set_variation_by_name(name) + _check_text(font, "Tests/images/variation_tiny_name.png", 40) + + def test_variation_set_by_axes(self): + font = self.get_font() + + freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) + if freetype < "2.9.1": + self.assertRaises(NotImplementedError, font.set_variation_by_axes, [100]) + return + + self.assertRaises(IOError, font.set_variation_by_axes, [500, 50]) + + def _check_text(font, path, epsilon): + im = Image.new("RGB", (100, 75), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), "Text", font=font, fill="black") + + expected = Image.open(path) + self.assert_image_similar(im, expected, epsilon) + + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) + font.set_variation_by_axes([500, 50]) + _check_text(font, "Tests/images/variation_adobe_axes.png", 5.1) + + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) + font.set_variation_by_axes([100]) + _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) @unittest.skipUnless(HAS_RAQM, "Raqm not Available") diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py index eb44957e4..eed0c70b6 100644 --- a/Tests/test_imagefont_bitmap.py +++ b/Tests/test_imagefont_bitmap.py @@ -1,6 +1,8 @@ -from .helper import unittest, PillowTestCase -from PIL import Image, ImageFont, ImageDraw +import unittest +from PIL import Image, ImageDraw, ImageFont + +from .helper import PillowTestCase image_font_installed = True try: @@ -12,16 +14,18 @@ except ImportError: @unittest.skipIf(not image_font_installed, "image font not installed") class TestImageFontBitmap(PillowTestCase): def test_similar(self): - text = 'EmbeddedBitmap' - font_outline = ImageFont.truetype( - font='Tests/fonts/DejaVuSans.ttf', size=24) + text = "EmbeddedBitmap" + font_outline = ImageFont.truetype(font="Tests/fonts/DejaVuSans.ttf", size=24) font_bitmap = ImageFont.truetype( - font='Tests/fonts/DejaVuSans-bitmap.ttf', size=24) + font="Tests/fonts/DejaVuSans-bitmap.ttf", size=24 + ) size_outline = font_outline.getsize(text) size_bitmap = font_bitmap.getsize(text) - size_final = (max(size_outline[0], size_bitmap[0]), - max(size_outline[1], size_bitmap[1])) - im_bitmap = Image.new('RGB', size_final, (255, 255, 255)) + size_final = ( + max(size_outline[0], size_bitmap[0]), + max(size_outline[1], size_bitmap[1]), + ) + im_bitmap = Image.new("RGB", size_final, (255, 255, 255)) im_outline = im_bitmap.copy() draw_bitmap = ImageDraw.Draw(im_bitmap) draw_outline = ImageDraw.Draw(im_outline) @@ -29,8 +33,13 @@ class TestImageFontBitmap(PillowTestCase): # Metrics are different on the bitmap and ttf fonts, # more so on some platforms and versions of freetype than others. # Mac has a 1px difference, linux doesn't. - draw_bitmap.text((0, size_final[1] - size_bitmap[1]), - text, fill=(0, 0, 0), font=font_bitmap) - draw_outline.text((0, size_final[1] - size_outline[1]), - text, fill=(0, 0, 0), font=font_outline) + draw_bitmap.text( + (0, size_final[1] - size_bitmap[1]), text, fill=(0, 0, 0), font=font_bitmap + ) + draw_outline.text( + (0, size_final[1] - size_outline[1]), + text, + fill=(0, 0, 0), + font=font_outline, + ) self.assert_image_similar(im_bitmap, im_outline, 20) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index d23f6d86f..71efb897f 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,43 +1,42 @@ -# -*- coding: utf-8 -*- -from .helper import unittest, PillowTestCase +import unittest + from PIL import Image, ImageDraw, ImageFont, features +from .helper import PillowTestCase FONT_SIZE = 20 FONT_PATH = "Tests/fonts/DejaVuSans.ttf" -@unittest.skipUnless(features.check('raqm'), "Raqm Library is not installed.") +@unittest.skipUnless(features.check("raqm"), "Raqm Library is not installed.") class TestImagecomplextext(PillowTestCase): - def test_english(self): # smoke test, this should not fail ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'TEST', font=ttf, fill=500, direction='ltr') + draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr") def test_complex_text(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'اهلا عمان', font=ttf, fill=500) + draw.text((0, 0), "اهلا عمان", font=ttf, fill=500) - target = 'Tests/images/test_text.png' + target = "Tests/images/test_text.png" target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) + self.assert_image_similar(im, target_img, 0.5) def test_y_offset(self): - ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", - FONT_SIZE) + ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'العالم العربي', font=ttf, fill=500) + draw.text((0, 0), "العالم العربي", font=ttf, fill=500) - target = 'Tests/images/test_y_offset.png' + target = "Tests/images/test_y_offset.png" target_img = Image.open(target) self.assert_image_similar(im, target_img, 1.7) @@ -45,88 +44,166 @@ class TestImagecomplextext(PillowTestCase): def test_complex_unicode_text(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'السلام عليكم', font=ttf, fill=500) + draw.text((0, 0), "السلام عليكم", font=ttf, fill=500) - target = 'Tests/images/test_complex_unicode_text.png' + target = "Tests/images/test_complex_unicode_text.png" target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) + self.assert_image_similar(im, target_img, 0.5) + + ttf = ImageFont.truetype("Tests/fonts/KhmerOSBattambang-Regular.ttf", FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "លោកុប្បត្តិ", font=ttf, fill=500) + + target = "Tests/images/test_complex_unicode_text2.png" + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, 2.3) def test_text_direction_rtl(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'English عربي', font=ttf, fill=500, direction='rtl') + draw.text((0, 0), "English عربي", font=ttf, fill=500, direction="rtl") - target = 'Tests/images/test_direction_rtl.png' + target = "Tests/images/test_direction_rtl.png" target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) + self.assert_image_similar(im, target_img, 0.5) def test_text_direction_ltr(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'سلطنة عمان Oman', - font=ttf, fill=500, direction='ltr') + draw.text((0, 0), "سلطنة عمان Oman", font=ttf, fill=500, direction="ltr") - target = 'Tests/images/test_direction_ltr.png' + target = "Tests/images/test_direction_ltr.png" target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) + self.assert_image_similar(im, target_img, 0.5) def test_text_direction_rtl2(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'Oman سلطنة عمان', - font=ttf, fill=500, direction='rtl') + draw.text((0, 0), "Oman سلطنة عمان", font=ttf, fill=500, direction="rtl") - target = 'Tests/images/test_direction_ltr.png' + target = "Tests/images/test_direction_ltr.png" target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) + 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) + + im = Image.new(mode="RGB", size=(100, 300)) + draw = ImageDraw.Draw(im) + try: + draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + self.skipTest("libraqm 0.7 or greater not available") + + target = "Tests/images/test_direction_ttb.png" + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, 1.15) + + def test_text_direction_ttb_stroke(self): + ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) + + im = Image.new(mode="RGB", size=(100, 300)) + draw = ImageDraw.Draw(im) + try: + draw.text( + (25, 25), + "あい", + font=ttf, + fill=500, + direction="ttb", + stroke_width=2, + stroke_fill="#0f0", + ) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + self.skipTest("libraqm 0.7 or greater not available") + + target = "Tests/images/test_direction_ttb_stroke.png" + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, 12.4) def test_ligature_features(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'filling', font=ttf, fill=500, features=['-liga']) - target = 'Tests/images/test_ligature_features.png' + 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, .5) + self.assert_image_similar(im, target_img, 0.5) - liga_size = ttf.getsize('fi', features=['-liga']) + liga_size = ttf.getsize("fi", features=["-liga"]) self.assertEqual(liga_size, (13, 19)) def test_kerning_features(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'TeToAV', font=ttf, fill=500, features=['-kern']) + draw.text((0, 0), "TeToAV", font=ttf, fill=500, features=["-kern"]) - target = 'Tests/images/test_kerning_features.png' + target = "Tests/images/test_kerning_features.png" target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) + self.assert_image_similar(im, target_img, 0.5) def test_arabictext_features(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) + im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'اللغة العربية', font=ttf, fill=500, - features=['-fina', '-init', '-medi']) + draw.text( + (0, 0), + "اللغة العربية", + font=ttf, + fill=500, + features=["-fina", "-init", "-medi"], + ) - target = 'Tests/images/test_arabictext_features.png' + target = "Tests/images/test_arabictext_features.png" target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) + 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) + + im = Image.new(mode="RGB", size=(50, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "لح", font=ttf, fill=500) + + target = "Tests/images/test_x_max_and_y_offset.png" + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, 0.5) + + def test_language(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "абвг", font=ttf, fill=500, language="sr") + + target = "Tests/images/test_language.png" + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, 0.5) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index a2e7a028d..bea7f68b3 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,40 +1,47 @@ -from .helper import PillowTestCase - -import sys import subprocess +import sys + +from .helper import PillowTestCase try: from PIL import ImageGrab class TestImageGrab(PillowTestCase): - def test_grab(self): - im = ImageGrab.grab() - self.assert_image(im, im.mode, im.size) + for im in [ + ImageGrab.grab(), + ImageGrab.grab(include_layered_windows=True), + ImageGrab.grab(all_screens=True), + ]: + self.assert_image(im, im.mode, im.size) def test_grabclipboard(self): if sys.platform == "darwin": - subprocess.call(['screencapture', '-cx']) + subprocess.call(["screencapture", "-cx"]) else: - p = subprocess.Popen(['powershell', '-command', '-'], - stdin=subprocess.PIPE) - p.stdin.write(b'''[Reflection.Assembly]::LoadWithPartialName("System.Drawing") + p = subprocess.Popen( + ["powershell", "-command", "-"], stdin=subprocess.PIPE + ) + p.stdin.write( + b"""[Reflection.Assembly]::LoadWithPartialName("System.Drawing") [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") $bmp = New-Object Drawing.Bitmap 200, 200 -[Windows.Forms.Clipboard]::SetImage($bmp)''') +[Windows.Forms.Clipboard]::SetImage($bmp)""" + ) p.communicate() im = ImageGrab.grabclipboard() self.assert_image(im, im.mode, im.size) + except ImportError: + class TestImageGrab(PillowTestCase): def test_skip(self): self.skipTest("ImportError") class TestImageGrabImport(PillowTestCase): - def test_import(self): # Arrange exception = None @@ -42,6 +49,7 @@ class TestImageGrabImport(PillowTestCase): # Act try: from PIL import ImageGrab + ImageGrab.__name__ # dummy to prevent Pyflakes warning except Exception as e: exception = e @@ -51,5 +59,4 @@ class TestImageGrabImport(PillowTestCase): self.assertIsNone(exception) else: self.assertIsInstance(exception, ImportError) - self.assertEqual(str(exception), - "ImageGrab is macOS and Windows only") + self.assertEqual(str(exception), "ImageGrab is macOS and Windows only") diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 8273b63c1..8d2b94226 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,13 +1,11 @@ -from __future__ import print_function -from .helper import PillowTestCase +from PIL import Image, ImageMath -from PIL import Image -from PIL import ImageMath +from .helper import PillowTestCase def pixel(im): if hasattr(im, "im"): - return "%s %r" % (im.mode, im.getpixel((0, 0))) + return "{} {!r}".format(im.mode, im.getpixel((0, 0))) else: if isinstance(im, int): return int(im) # hack to deal with booleans @@ -27,15 +25,13 @@ images = {"A": A, "B": B, "F": F, "I": I} class TestImageMath(PillowTestCase): - def test_sanity(self): self.assertEqual(ImageMath.eval("1"), 1) self.assertEqual(ImageMath.eval("1+A", A=2), 3) self.assertEqual(pixel(ImageMath.eval("A+B", A=A, B=B)), "I 3") self.assertEqual(pixel(ImageMath.eval("A+B", images)), "I 3") self.assertEqual(pixel(ImageMath.eval("float(A)+B", images)), "F 3.0") - self.assertEqual(pixel( - ImageMath.eval("int(float(A)+B)", images)), "I 3") + self.assertEqual(pixel(ImageMath.eval("int(float(A)+B)", images)), "I 3") def test_ops(self): @@ -47,16 +43,16 @@ class TestImageMath(PillowTestCase): self.assertEqual(pixel(ImageMath.eval("A*B", images)), "I 2") self.assertEqual(pixel(ImageMath.eval("A/B", images)), "I 0") self.assertEqual(pixel(ImageMath.eval("B**2", images)), "I 4") - self.assertEqual(pixel( - ImageMath.eval("B**33", images)), "I 2147483647") + self.assertEqual(pixel(ImageMath.eval("B**33", images)), "I 2147483647") self.assertEqual(pixel(ImageMath.eval("float(A)+B", images)), "F 3.0") self.assertEqual(pixel(ImageMath.eval("float(A)-B", images)), "F -1.0") self.assertEqual(pixel(ImageMath.eval("float(A)*B", images)), "F 2.0") self.assertEqual(pixel(ImageMath.eval("float(A)/B", images)), "F 0.5") self.assertEqual(pixel(ImageMath.eval("float(B)**2", images)), "F 4.0") - self.assertEqual(pixel( - ImageMath.eval("float(B)**33", images)), "F 8589934592.0") + self.assertEqual( + pixel(ImageMath.eval("float(B)**33", images)), "F 8589934592.0" + ) def test_logical(self): self.assertEqual(pixel(ImageMath.eval("not A", images)), 0) @@ -64,12 +60,11 @@ class TestImageMath(PillowTestCase): self.assertEqual(pixel(ImageMath.eval("A or B", images)), "L 1") def test_convert(self): - self.assertEqual(pixel( - ImageMath.eval("convert(A+B, 'L')", images)), "L 3") - self.assertEqual(pixel( - ImageMath.eval("convert(A+B, '1')", images)), "1 0") - self.assertEqual(pixel( - ImageMath.eval("convert(A+B, 'RGB')", images)), "RGB (3, 3, 3)") + self.assertEqual(pixel(ImageMath.eval("convert(A+B, 'L')", images)), "L 3") + self.assertEqual(pixel(ImageMath.eval("convert(A+B, '1')", images)), "1 0") + self.assertEqual( + pixel(ImageMath.eval("convert(A+B, 'RGB')", images)), "RGB (3, 3, 3)" + ) def test_compare(self): self.assertEqual(pixel(ImageMath.eval("min(A, B)", images)), "I 1") @@ -176,9 +171,6 @@ class TestImageMath(PillowTestCase): self.assertEqual(pixel(ImageMath.eval("notequal(A, A)", A=A)), "I 0") self.assertEqual(pixel(ImageMath.eval("notequal(B, B)", B=B)), "I 0") self.assertEqual(pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)), "I 0") - self.assertEqual( - pixel(ImageMath.eval("notequal(A, B)", A=A, B=B)), "I 1") - self.assertEqual( - pixel(ImageMath.eval("notequal(B, A)", A=A, B=B)), "I 1") - self.assertEqual( - pixel(ImageMath.eval("notequal(A, Z)", A=A, Z=Z)), "I 1") + self.assertEqual(pixel(ImageMath.eval("notequal(A, B)", A=A, B=B)), "I 1") + self.assertEqual(pixel(ImageMath.eval("notequal(B, A)", A=A, B=B)), "I 1") + self.assertEqual(pixel(ImageMath.eval("notequal(A, Z)", A=A, Z=Z)), "I 1") diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 0cf15bd6c..1492872b6 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,11 +1,10 @@ # Test the ImageMorphology functionality -from .helper import PillowTestCase, hopper - from PIL import Image, ImageMorph, _imagingmorph +from .helper import PillowTestCase, hopper + class MorphTests(PillowTestCase): - def setUp(self): self.A = self.string_to_img( """ @@ -17,27 +16,27 @@ class MorphTests(PillowTestCase): ....... ....... """ - ) + ) def img_to_string(self, im): """Turn a (small) binary image into a string representation""" - chars = '.1' + chars = ".1" width, height = im.size - return '\n'.join( - ''.join(chars[im.getpixel((c, r)) > 0] for c in range(width)) - for r in range(height)) + return "\n".join( + "".join(chars[im.getpixel((c, r)) > 0] for c in range(width)) + for r in range(height) + ) def string_to_img(self, image_string): """Turn a string image representation into a binary image""" - rows = [s for s in image_string.replace(' ', '').split('\n') - if len(s)] + rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] height = len(rows) width = len(rows[0]) - im = Image.new('L', (width, height)) + im = Image.new("L", (width, height)) for i in range(width): for j in range(height): c = rows[j][i] - v = c in 'X1' + v = c in "X1" im.putpixel((i, j), v) return im @@ -49,55 +48,50 @@ class MorphTests(PillowTestCase): self.assertEqual(self.img_to_string(A), self.img_to_string(B)) def assert_img_equal_img_string(self, A, Bstring): - self.assertEqual( - self.img_to_string(A), - self.img_string_normalize(Bstring)) + 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') + im = Image.open("Tests/images/morph_a.png") self.assert_image_equal(self.A, im) def create_lut(self): - for op in ( - 'corner', 'dilation4', 'dilation8', - 'erosion4', 'erosion8', 'edge'): + for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): lb = ImageMorph.LutBuilder(op_name=op) lut = lb.build_lut() - with open('Tests/images/%s.lut' % op, 'wb') as f: + with open("Tests/images/%s.lut" % op, "wb") as f: f.write(lut) # create_lut() def test_lut(self): - for op in ( - 'corner', 'dilation4', 'dilation8', - 'erosion4', 'erosion8', 'edge'): + for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): lb = ImageMorph.LutBuilder(op_name=op) self.assertIsNone(lb.get_lut()) lut = lb.build_lut() - with open('Tests/images/%s.lut' % op, 'rb') as f: + with open("Tests/images/%s.lut" % op, "rb") as f: self.assertEqual(lut, bytearray(f.read())) def test_no_operator_loaded(self): mop = ImageMorph.MorphOp() with self.assertRaises(Exception) as e: mop.apply(None) - self.assertEqual(str(e.exception), 'No operator loaded') + self.assertEqual(str(e.exception), "No operator loaded") with self.assertRaises(Exception) as e: mop.match(None) - self.assertEqual(str(e.exception), 'No operator loaded') + self.assertEqual(str(e.exception), "No operator loaded") with self.assertRaises(Exception) as e: mop.save_lut(None) - self.assertEqual(str(e.exception), 'No operator loaded') + self.assertEqual(str(e.exception), "No operator loaded") # Test the named patterns def test_erosion8(self): # erosion8 - mop = ImageMorph.MorphOp(op_name='erosion8') + mop = ImageMorph.MorphOp(op_name="erosion8") count, Aout = mop.apply(self.A) self.assertEqual(count, 8) - self.assert_img_equal_img_string(Aout, - """ + self.assert_img_equal_img_string( + Aout, + """ ....... ....... ....... @@ -105,15 +99,17 @@ class MorphTests(PillowTestCase): ....... ....... ....... - """) + """, + ) def test_dialation8(self): # dialation8 - mop = ImageMorph.MorphOp(op_name='dilation8') + mop = ImageMorph.MorphOp(op_name="dilation8") count, Aout = mop.apply(self.A) self.assertEqual(count, 16) - self.assert_img_equal_img_string(Aout, - """ + self.assert_img_equal_img_string( + Aout, + """ ....... .11111. .11111. @@ -121,15 +117,17 @@ class MorphTests(PillowTestCase): .11111. .11111. ....... - """) + """, + ) def test_erosion4(self): # erosion4 - mop = ImageMorph.MorphOp(op_name='dilation4') + mop = ImageMorph.MorphOp(op_name="dilation4") count, Aout = mop.apply(self.A) self.assertEqual(count, 12) - self.assert_img_equal_img_string(Aout, - """ + self.assert_img_equal_img_string( + Aout, + """ ....... ..111.. .11111. @@ -137,15 +135,17 @@ class MorphTests(PillowTestCase): .11111. ..111.. ....... - """) + """, + ) def test_edge(self): # edge - mop = ImageMorph.MorphOp(op_name='edge') + mop = ImageMorph.MorphOp(op_name="edge") count, Aout = mop.apply(self.A) self.assertEqual(count, 1) - self.assert_img_equal_img_string(Aout, - """ + self.assert_img_equal_img_string( + Aout, + """ ....... ....... ..111.. @@ -153,16 +153,17 @@ class MorphTests(PillowTestCase): ..111.. ....... ....... - """) + """, + ) def test_corner(self): # Create a corner detector pattern - mop = ImageMorph.MorphOp(patterns=['1:(... ... ...)->0', - '4:(00. 01. ...)->1']) + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) count, Aout = mop.apply(self.A) self.assertEqual(count, 5) - self.assert_img_equal_img_string(Aout, - """ + self.assert_img_equal_img_string( + Aout, + """ ....... ....... ..1.1.. @@ -170,7 +171,8 @@ class MorphTests(PillowTestCase): ..1.1.. ....... ....... - """) + """, + ) # Test the coordinate counting with the same operator coords = mop.match(self.A) @@ -183,12 +185,12 @@ class MorphTests(PillowTestCase): def test_mirroring(self): # Test 'M' for mirroring - mop = ImageMorph.MorphOp(patterns=['1:(... ... ...)->0', - 'M:(00. 01. ...)->1']) + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"]) count, Aout = mop.apply(self.A) self.assertEqual(count, 7) - self.assert_img_equal_img_string(Aout, - """ + self.assert_img_equal_img_string( + Aout, + """ ....... ....... ..1.1.. @@ -196,16 +198,17 @@ class MorphTests(PillowTestCase): ....... ....... ....... - """) + """, + ) def test_negate(self): # Test 'N' for negate - mop = ImageMorph.MorphOp(patterns=['1:(... ... ...)->0', - 'N:(00. 01. ...)->1']) + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"]) count, Aout = mop.apply(self.A) self.assertEqual(count, 8) - self.assert_img_equal_img_string(Aout, - """ + self.assert_img_equal_img_string( + Aout, + """ ....... ....... ..1.... @@ -213,32 +216,34 @@ class MorphTests(PillowTestCase): ....... ....... ....... - """) + """, + ) def test_non_binary_images(self): - im = hopper('RGB') + im = hopper("RGB") mop = ImageMorph.MorphOp(op_name="erosion8") with self.assertRaises(Exception) as e: mop.apply(im) - self.assertEqual(str(e.exception), - 'Image must be binary, meaning it must use mode L') + self.assertEqual( + str(e.exception), "Image must be binary, meaning it must use mode L" + ) with self.assertRaises(Exception) as e: mop.match(im) - self.assertEqual(str(e.exception), - 'Image must be binary, meaning it must use mode L') + self.assertEqual( + str(e.exception), "Image must be binary, meaning it must use mode L" + ) with self.assertRaises(Exception) as e: mop.get_on_pixels(im) - self.assertEqual(str(e.exception), - 'Image must be binary, meaning it must use mode L') + self.assertEqual( + str(e.exception), "Image must be binary, meaning it must use mode L" + ) def test_add_patterns(self): # Arrange - lb = ImageMorph.LutBuilder(op_name='corner') - self.assertEqual(lb.patterns, ['1:(... ... ...)->0', - '4:(00. 01. ...)->1']) - new_patterns = ['M:(00. 01. ...)->1', - 'N:(00. 01. ...)->1'] + lb = ImageMorph.LutBuilder(op_name="corner") + self.assertEqual(lb.patterns, ["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) + new_patterns = ["M:(00. 01. ...)->1", "N:(00. 01. ...)->1"] # Act lb.add_patterns(new_patterns) @@ -246,44 +251,44 @@ class MorphTests(PillowTestCase): # Assert self.assertEqual( lb.patterns, - ['1:(... ... ...)->0', - '4:(00. 01. ...)->1', - 'M:(00. 01. ...)->1', - 'N:(00. 01. ...)->1']) + [ + "1:(... ... ...)->0", + "4:(00. 01. ...)->1", + "M:(00. 01. ...)->1", + "N:(00. 01. ...)->1", + ], + ) def test_unknown_pattern(self): - self.assertRaises( - Exception, - ImageMorph.LutBuilder, op_name='unknown') + self.assertRaises(Exception, ImageMorph.LutBuilder, op_name="unknown") def test_pattern_syntax_error(self): # Arrange - lb = ImageMorph.LutBuilder(op_name='corner') - new_patterns = ['a pattern with a syntax error'] + lb = ImageMorph.LutBuilder(op_name="corner") + new_patterns = ["a pattern with a syntax error"] lb.add_patterns(new_patterns) # Act / Assert with self.assertRaises(Exception) as e: lb.build_lut() self.assertEqual( - str(e.exception), - 'Syntax error in pattern "a pattern with a syntax error"') + str(e.exception), 'Syntax error in pattern "a pattern with a syntax error"' + ) def test_load_invalid_mrl(self): # Arrange - invalid_mrl = 'Tests/images/hopper.png' + invalid_mrl = "Tests/images/hopper.png" mop = ImageMorph.MorphOp() # Act / Assert with self.assertRaises(Exception) as e: mop.load_lut(invalid_mrl) - self.assertEqual(str(e.exception), - 'Wrong size operator file!') + self.assertEqual(str(e.exception), "Wrong size operator file!") def test_roundtrip_mrl(self): # Arrange - tempfile = self.tempfile('temp.mrl') - mop = ImageMorph.MorphOp(op_name='corner') + tempfile = self.tempfile("temp.mrl") + mop = ImageMorph.MorphOp(op_name="corner") initial_lut = mop.lut # Act @@ -295,7 +300,7 @@ class MorphTests(PillowTestCase): def test_set_lut(self): # Arrange - lb = ImageMorph.LutBuilder(op_name='corner') + lb = ImageMorph.LutBuilder(op_name="corner") lut = lb.build_lut() mop = ImageMorph.MorphOp() @@ -306,9 +311,9 @@ class MorphTests(PillowTestCase): self.assertEqual(mop.lut, lut) def test_wrong_mode(self): - lut = ImageMorph.LutBuilder(op_name='corner').build_lut() - imrgb = Image.new('RGB', (10, 10)) - iml = Image.new('L', (10, 10)) + lut = ImageMorph.LutBuilder(op_name="corner").build_lut() + imrgb = Image.new("RGB", (10, 10)) + iml = Image.new("L", (10, 10)) with self.assertRaises(RuntimeError): _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index c5e48c431..d0fd73689 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,12 +1,17 @@ +from PIL import Image, ImageOps + from .helper import PillowTestCase, hopper -from PIL import ImageOps -from PIL import Image +try: + from PIL import _webp + + HAVE_WEBP = True +except ImportError: + HAVE_WEBP = False 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))] @@ -62,6 +67,9 @@ class TestImageOps(PillowTestCase): ImageOps.solarize(hopper("L")) ImageOps.solarize(hopper("RGB")) + ImageOps.exif_transpose(hopper("L")) + ImageOps.exif_transpose(hopper("RGB")) + def test_1pxfit(self): # Division by zero in equalize if image is 1 pixel high newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) @@ -73,6 +81,16 @@ class TestImageOps(PillowTestCase): newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) self.assertEqual(newimg.size, (35, 35)) + def test_fit_same_ratio(self): + # The ratio for this image is 1000.0 / 755 = 1.3245033112582782 + # If the ratios are not acknowledged to be the same, + # and Pillow attempts to adjust the width to + # 1.3245033112582782 * 755 = 1000.0000000000001 + # then centering this greater width causes a negative x offset when cropping + with Image.new("RGB", (1000, 755)) as im: + new_im = ImageOps.fit(im, (1000, 755)) + self.assertEqual(new_im.size, (1000, 755)) + def test_pad(self): # Same ratio im = hopper() @@ -82,15 +100,15 @@ class TestImageOps(PillowTestCase): for label, color, new_size in [ ("h", None, (im.width * 4, im.height * 2)), - ("v", "#f00", (im.width * 2, im.height * 4)) + ("v", "#f00", (im.width * 2, im.height * 4)), ]: for i, centering in enumerate([(0, 0), (0.5, 0.5), (1, 1)]): - new_im = ImageOps.pad(im, new_size, - color=color, centering=centering) + new_im = ImageOps.pad(im, new_size, color=color, centering=centering) self.assertEqual(new_im.size, new_size) target = Image.open( - "Tests/images/imageops_pad_"+label+"_"+str(i)+".jpg") + "Tests/images/imageops_pad_" + label + "_" + str(i) + ".jpg" + ) self.assert_image_similar(new_im, target, 6) def test_pil163(self): @@ -126,24 +144,30 @@ class TestImageOps(PillowTestCase): im = im.convert("L") # Create image with original 2-color functionality - im_test = ImageOps.colorize(im, 'red', 'green') + im_test = ImageOps.colorize(im, "red", "green") # Test output image (2-color) left = (0, 1) middle = (127, 1) right = (255, 1) - self.assert_tuple_approx_equal(im_test.getpixel(left), - (255, 0, 0), - threshold=1, - msg='black test pixel incorrect') - self.assert_tuple_approx_equal(im_test.getpixel(middle), - (127, 63, 0), - threshold=1, - msg='mid test pixel incorrect') - self.assert_tuple_approx_equal(im_test.getpixel(right), - (0, 127, 0), - threshold=1, - msg='white test pixel incorrect') + self.assert_tuple_approx_equal( + im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg="black test pixel incorrect", + ) + self.assert_tuple_approx_equal( + im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg="mid test pixel incorrect", + ) + self.assert_tuple_approx_equal( + im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg="white test pixel incorrect", + ) def test_colorize_2color_offset(self): # Test the colorizing function with 2-color functionality and offset @@ -153,28 +177,32 @@ class TestImageOps(PillowTestCase): im = im.convert("L") # Create image with original 2-color functionality with offsets - im_test = ImageOps.colorize(im, - black='red', - white='green', - blackpoint=50, - whitepoint=100) + im_test = ImageOps.colorize( + im, black="red", white="green", blackpoint=50, whitepoint=100 + ) # Test output image (2-color) with offsets left = (25, 1) middle = (75, 1) right = (125, 1) - self.assert_tuple_approx_equal(im_test.getpixel(left), - (255, 0, 0), - threshold=1, - msg='black test pixel incorrect') - self.assert_tuple_approx_equal(im_test.getpixel(middle), - (127, 63, 0), - threshold=1, - msg='mid test pixel incorrect') - self.assert_tuple_approx_equal(im_test.getpixel(right), - (0, 127, 0), - threshold=1, - msg='white test pixel incorrect') + self.assert_tuple_approx_equal( + im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg="black test pixel incorrect", + ) + self.assert_tuple_approx_equal( + im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg="mid test pixel incorrect", + ) + self.assert_tuple_approx_equal( + im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg="white test pixel incorrect", + ) def test_colorize_3color_offset(self): # Test the colorizing function with 3-color functionality and offset @@ -184,13 +212,15 @@ class TestImageOps(PillowTestCase): im = im.convert("L") # Create image with new three color functionality with offsets - im_test = ImageOps.colorize(im, - black='red', - white='green', - mid='blue', - blackpoint=50, - whitepoint=200, - midpoint=100) + im_test = ImageOps.colorize( + im, + black="red", + white="green", + mid="blue", + blackpoint=50, + whitepoint=200, + midpoint=100, + ) # Test output image (3-color) with offsets left = (25, 1) @@ -198,23 +228,60 @@ class TestImageOps(PillowTestCase): middle = (100, 1) right_middle = (150, 1) right = (225, 1) - self.assert_tuple_approx_equal(im_test.getpixel(left), - (255, 0, 0), - threshold=1, - msg='black test pixel incorrect') - self.assert_tuple_approx_equal(im_test.getpixel(left_middle), - (127, 0, 127), - threshold=1, - msg='low-mid test pixel incorrect') - self.assert_tuple_approx_equal(im_test.getpixel(middle), - (0, 0, 255), - threshold=1, - msg='mid incorrect') - self.assert_tuple_approx_equal(im_test.getpixel(right_middle), - (0, 63, 127), - threshold=1, - msg='high-mid test pixel incorrect') - self.assert_tuple_approx_equal(im_test.getpixel(right), - (0, 127, 0), - threshold=1, - msg='white test pixel incorrect') + self.assert_tuple_approx_equal( + im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg="black test pixel incorrect", + ) + self.assert_tuple_approx_equal( + im_test.getpixel(left_middle), + (127, 0, 127), + threshold=1, + msg="low-mid test pixel incorrect", + ) + self.assert_tuple_approx_equal( + im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect" + ) + self.assert_tuple_approx_equal( + im_test.getpixel(right_middle), + (0, 63, 127), + threshold=1, + msg="high-mid test pixel incorrect", + ) + self.assert_tuple_approx_equal( + im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg="white test pixel incorrect", + ) + + def test_exif_transpose(self): + exts = [".jpg"] + if HAVE_WEBP and _webp.HAVE_WEBPANIM: + exts.append(".webp") + for ext in exts: + base_im = Image.open("Tests/images/hopper" + ext) + + orientations = [base_im] + for i in range(2, 9): + im = Image.open("Tests/images/hopper_orientation_" + str(i) + ext) + orientations.append(im) + for i, orientation_im in enumerate(orientations): + for im in [orientation_im, orientation_im.copy()]: # ImageFile # Image + if i == 0: + self.assertNotIn("exif", im.info) + 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) + + 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) diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index a867e5430..71e858681 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,70 +1,84 @@ +from PIL import Image, ImageFilter + from .helper import PillowTestCase -from PIL import Image -from PIL import ImageFilter - -im = Image.open("Tests/images/hopper.ppm") -snakes = Image.open("Tests/images/color_snakes.png") - class TestImageOpsUsm(PillowTestCase): + def setUp(self): + super().setUp() + self.im = Image.open("Tests/images/hopper.ppm") + self.addCleanup(self.im.close) + self.snakes = Image.open("Tests/images/color_snakes.png") + self.addCleanup(self.snakes.close) def test_filter_api(self): test_filter = ImageFilter.GaussianBlur(2.0) - i = im.filter(test_filter) + i = self.im.filter(test_filter) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) test_filter = ImageFilter.UnsharpMask(2.0, 125, 8) - i = im.filter(test_filter) + i = self.im.filter(test_filter) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) def test_usm_formats(self): usm = ImageFilter.UnsharpMask - self.assertRaises(ValueError, im.convert("1").filter, usm) - im.convert("L").filter(usm) - self.assertRaises(ValueError, im.convert("I").filter, usm) - self.assertRaises(ValueError, im.convert("F").filter, usm) - im.convert("RGB").filter(usm) - im.convert("RGBA").filter(usm) - im.convert("CMYK").filter(usm) - self.assertRaises(ValueError, im.convert("YCbCr").filter, usm) + self.assertRaises(ValueError, self.im.convert("1").filter, usm) + self.im.convert("L").filter(usm) + self.assertRaises(ValueError, self.im.convert("I").filter, usm) + self.assertRaises(ValueError, self.im.convert("F").filter, usm) + self.im.convert("RGB").filter(usm) + self.im.convert("RGBA").filter(usm) + self.im.convert("CMYK").filter(usm) + self.assertRaises(ValueError, self.im.convert("YCbCr").filter, usm) def test_blur_formats(self): blur = ImageFilter.GaussianBlur - self.assertRaises(ValueError, im.convert("1").filter, blur) - blur(im.convert("L")) - self.assertRaises(ValueError, im.convert("I").filter, blur) - self.assertRaises(ValueError, im.convert("F").filter, blur) - im.convert("RGB").filter(blur) - im.convert("RGBA").filter(blur) - im.convert("CMYK").filter(blur) - self.assertRaises(ValueError, im.convert("YCbCr").filter, blur) + self.assertRaises(ValueError, self.im.convert("1").filter, blur) + blur(self.im.convert("L")) + self.assertRaises(ValueError, self.im.convert("I").filter, blur) + self.assertRaises(ValueError, self.im.convert("F").filter, blur) + self.im.convert("RGB").filter(blur) + self.im.convert("RGBA").filter(blur) + self.im.convert("CMYK").filter(blur) + self.assertRaises(ValueError, self.im.convert("YCbCr").filter, blur) def test_usm_accuracy(self): - src = snakes.convert('RGB') + src = self.snakes.convert("RGB") i = src.filter(ImageFilter.UnsharpMask(5, 1024, 0)) # Image should not be changed because it have only 0 and 255 levels. self.assertEqual(i.tobytes(), src.tobytes()) def test_blur_accuracy(self): - i = snakes.filter(ImageFilter.GaussianBlur(.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 [(1, 0, 1), (2, 0, 1), (7, 8, 1), (8, 8, 1), (2, 9, 1), - (7, 3, 0), (8, 3, 0), (5, 8, 0), (5, 9, 0), (1, 3, 0), - (4, 3, 2), (4, 2, 2)]: + for x, y, c in [ + (1, 0, 1), + (2, 0, 1), + (7, 8, 1), + (8, 8, 1), + (2, 9, 1), + (7, 3, 0), + (8, 3, 0), + (5, 8, 0), + (5, 9, 0), + (1, 3, 0), + (4, 3, 2), + (4, 2, 2), + ]: self.assertGreaterEqual(i.im.getpixel((x, y))[c], 250) # Fuzzy match. def gp(x, y): return i.im.getpixel((x, y)) + self.assertTrue(236 <= gp(7, 4)[0] <= 239) self.assertTrue(236 <= gp(7, 5)[2] <= 239) self.assertTrue(236 <= gp(7, 6)[2] <= 239) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index e4b5b7f72..1297712ef 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,15 +1,15 @@ -from .helper import PillowTestCase +from PIL import Image, ImagePalette -from PIL import ImagePalette, Image +from .helper import PillowTestCase class TestImagePalette(PillowTestCase): - def test_sanity(self): - ImagePalette.ImagePalette("RGB", list(range(256))*3) - self.assertRaises(ValueError, - ImagePalette.ImagePalette, "RGB", list(range(256))*2) + ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + self.assertRaises( + ValueError, ImagePalette.ImagePalette, "RGB", list(range(256)) * 2 + ) def test_getcolor(self): @@ -27,7 +27,7 @@ class TestImagePalette(PillowTestCase): def test_file(self): - palette = ImagePalette.ImagePalette("RGB", list(range(256))*3) + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) f = self.tempfile("temp.lut") @@ -65,8 +65,9 @@ class TestImagePalette(PillowTestCase): white = 255 # Act - self.assertRaises(NotImplementedError, - ImagePalette.make_linear_lut, black, white) + self.assertRaises( + NotImplementedError, ImagePalette.make_linear_lut, black, white + ) def test_make_gamma_lut(self): # Arrange @@ -87,7 +88,7 @@ class TestImagePalette(PillowTestCase): def test_rawmode_valueerrors(self): # Arrange - palette = ImagePalette.raw("RGB", list(range(256))*3) + palette = ImagePalette.raw("RGB", list(range(256)) * 3) # Act / Assert self.assertRaises(ValueError, palette.tobytes) @@ -97,7 +98,7 @@ class TestImagePalette(PillowTestCase): def test_getdata(self): # Arrange - data_in = list(range(256))*3 + data_in = list(range(256)) * 3 palette = ImagePalette.ImagePalette("RGB", data_in) # Act @@ -108,7 +109,7 @@ class TestImagePalette(PillowTestCase): def test_rawmode_getdata(self): # Arrange - data_in = list(range(256))*3 + data_in = list(range(256)) * 3 palette = ImagePalette.raw("RGB", data_in) # Act @@ -120,17 +121,16 @@ class TestImagePalette(PillowTestCase): def test_2bit_palette(self): # issue #2258, 2 bit palettes are corrupted. - outfile = self.tempfile('temp.png') + outfile = self.tempfile("temp.png") - rgb = b'\x00' * 2 + b'\x01' * 2 + b'\x02' * 2 - img = Image.frombytes('P', (6, 1), rgb) - img.putpalette(b'\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF') # RGB - img.save(outfile, format='PNG') + rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2 + img = Image.frombytes("P", (6, 1), rgb) + img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB + img.save(outfile, format="PNG") reloaded = Image.open(outfile) self.assert_image_equal(img, reloaded) def test_invalid_palette(self): - self.assertRaises(IOError, - ImagePalette.load, "Tests/images/hopper.jpg") + self.assertRaises(IOError, ImagePalette.load, "Tests/images/hopper.jpg") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 8cf88b7c1..35c78cd3b 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,14 +1,12 @@ -from .helper import PillowTestCase - -from PIL import ImagePath, Image -from PIL._util import py3 - import array import struct +from PIL import Image, ImagePath + +from .helper import PillowTestCase + class TestImagePath(PillowTestCase): - def test_path(self): p = ImagePath.Path(list(range(10))) @@ -19,21 +17,19 @@ class TestImagePath(PillowTestCase): self.assertEqual(p[-1], (8.0, 9.0)) self.assertEqual(list(p[:1]), [(0.0, 1.0)]) with self.assertRaises(TypeError) as cm: - p['foo'] + p["foo"] + self.assertEqual(str(cm.exception), "Path indices must be integers, not str") self.assertEqual( - str(cm.exception), - "Path indices must be integers, not str") - self.assertEqual( - list(p), - [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)]) + list(p), [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)] + ) # method sanity check self.assertEqual( - p.tolist(), - [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)]) + p.tolist(), [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)] + ) self.assertEqual( - p.tolist(1), - [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]) + p.tolist(1), [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + ) self.assertEqual(p.getbbox(), (0.0, 1.0, 8.0, 9.0)) @@ -62,7 +58,7 @@ class TestImagePath(PillowTestCase): self.assertEqual(list(p), [(0.0, 1.0)]) arr = array.array("f", [0, 1]) - if hasattr(arr, 'tobytes'): + if hasattr(arr, "tobytes"): p = ImagePath.Path(arr.tobytes()) else: p = ImagePath.Path(arr.tostring()) @@ -78,10 +74,7 @@ class TestImagePath(PillowTestCase): # This fails due to the invalid malloc above, # and segfaults for i in range(200000): - if py3: - x[i] = b'0'*16 - else: - x[i] = "0"*16 + x[i] = b"0" * 16 class evil: diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 2ded37c09..f69a21d2a 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,20 +1,21 @@ -from .helper import PillowTestCase, hopper - from PIL import ImageQt +from .helper import PillowTestCase, hopper if ImageQt.qt_is_installed: from PIL.ImageQt import qRgba def skip_if_qt_is_not_installed(_): pass + + else: + def skip_if_qt_is_not_installed(test_case): - test_case.skipTest('Qt bindings are not installed') + test_case.skipTest("Qt bindings are not installed") -class PillowQtTestCase(object): - +class PillowQtTestCase: def setUp(self): skip_if_qt_is_not_installed(self) @@ -23,52 +24,42 @@ class PillowQtTestCase(object): class PillowQPixmapTestCase(PillowQtTestCase): - def setUp(self): - PillowQtTestCase.setUp(self) + super().setUp() try: - if ImageQt.qt_version == '5': + 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': + elif ImageQt.qt_version == "side2": from PySide2.QtGui import QGuiApplication except ImportError: - self.skipTest('QGuiApplication not installed') + self.skipTest("QGuiApplication not installed") self.app = QGuiApplication([]) def tearDown(self): - PillowQtTestCase.tearDown(self) + super().tearDown() self.app.quit() class TestImageQt(PillowQtTestCase, PillowTestCase): - def test_rgb(self): # from https://doc.qt.io/archives/qt-4.8/qcolor.html # typedef QRgb # An ARGB quadruplet on the format #AARRGGBB, # equivalent to an unsigned int. - if ImageQt.qt_version == '5': + 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': + elif ImageQt.qt_version == "side2": from PySide2.QtGui import qRgb self.assertEqual(qRgb(0, 0, 0), qRgba(0, 0, 0, 255)) def checkrgb(r, g, b): val = ImageQt.rgb(r, g, b) - val = val % 2**24 # drop the alpha + val = val % 2 ** 24 # drop the alpha self.assertEqual(val >> 16, r) - self.assertEqual(((val >> 8) % 2**8), g) - self.assertEqual(val % 2**8, b) + self.assertEqual(((val >> 8) % 2 ** 8), g) + self.assertEqual(val % 2 ** 8, b) checkrgb(0, 0, 0) checkrgb(255, 0, 0) @@ -76,5 +67,5 @@ class TestImageQt(PillowQtTestCase, PillowTestCase): checkrgb(0, 0, 255) def test_image(self): - for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): + for mode in ("1", "RGB", "RGBA", "L", "P"): ImageQt.ImageQt(hopper(mode)) diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 9fbf3fed8..17f8100b7 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,10 +1,9 @@ -from .helper import PillowTestCase, hopper - from PIL import Image, ImageSequence, TiffImagePlugin +from .helper import PillowTestCase, hopper + class TestImageSequence(PillowTestCase): - def test_sanity(self): test_file = self.tempfile("temp.im") @@ -25,19 +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): + 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() @@ -53,19 +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 + 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) + + 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 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 899c057d6..858714e57 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,11 +1,11 @@ -from .helper import PillowTestCase, hopper +import unittest -from PIL import Image -from PIL import ImageShow +from PIL import Image, ImageShow + +from .helper import PillowTestCase, hopper, is_win32, on_ci, on_github_actions class TestImageShow(PillowTestCase): - def test_sanity(self): dir(Image) dir(ImageShow) @@ -17,23 +17,34 @@ class TestImageShow(PillowTestCase): # Restore original state ImageShow._viewers.pop() - def test_show(self): - class TestViewer: + def test_viewer_show(self): + class TestViewer(ImageShow.Viewer): methodCalled = False - def show(self, image, title=None, **options): + def show_image(self, image, **options): self.methodCalled = True return True + viewer = TestViewer() ImageShow.register(viewer, -1) - im = hopper() - self.assertTrue(ImageShow.show(im)) - self.assertTrue(viewer.methodCalled) + for mode in ("1", "I;16", "LA", "RGB", "RGBA"): + with hopper() as im: + self.assertTrue(ImageShow.show(im)) + self.assertTrue(viewer.methodCalled) # Restore original state ImageShow._viewers.pop(0) + @unittest.skipUnless( + on_ci() and not (is_win32() and on_github_actions()), + "Only run on CIs; hangs on Windows on GitHub Actions", + ) + def test_show(self): + for mode in ("1", "I;16", "LA", "RGB", "RGBA"): + im = hopper(mode) + self.assertTrue(ImageShow.show(im)) + def test_viewer(self): viewer = ImageShow.Viewer() @@ -43,4 +54,4 @@ class TestImageShow(PillowTestCase): def test_viewers(self): for viewer in ImageShow._viewers: - viewer.get_command('test.jpg') + viewer.get_command("test.jpg") diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index c2580a1b1..d6c6a7a55 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,11 +1,9 @@ -from .helper import PillowTestCase, hopper +from PIL import Image, ImageStat -from PIL import Image -from PIL import ImageStat +from .helper import PillowTestCase, hopper class TestImageStat(PillowTestCase): - def test_sanity(self): im = hopper() @@ -48,8 +46,8 @@ class TestImageStat(PillowTestCase): st = ImageStat.Stat(im) self.assertEqual(st.extrema[0], (128, 128)) - self.assertEqual(st.sum[0], 128**3) - self.assertEqual(st.sum2[0], 128**4) + self.assertEqual(st.sum[0], 128 ** 3) + self.assertEqual(st.sum2[0], 128 ** 4) self.assertEqual(st.mean[0], 128) self.assertEqual(st.median[0], 128) self.assertEqual(st.rms[0], 128) diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index a6a4dd4ea..048efd639 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,26 +1,25 @@ -from .helper import unittest, PillowTestCase, hopper -from PIL import Image -from PIL._util import py3 +import unittest +from PIL import Image + +from .helper import PillowTestCase, hopper try: from PIL import ImageTk - if py3: - import tkinter as tk - else: - import Tkinter as tk + + import tkinter as tk + dir(ImageTk) HAS_TK = True except (OSError, ImportError): # Skipped via setUp() HAS_TK = False -TK_MODES = ('1', 'L', 'P', 'RGB', 'RGBA') +TK_MODES = ("1", "L", "P", "RGB", "RGBA") @unittest.skipIf(not HAS_TK, "Tk not installed") class TestImageTk(PillowTestCase): - def setUp(self): try: # setup tk @@ -34,7 +33,7 @@ class TestImageTk(PillowTestCase): TEST_PNG = "Tests/images/hopper.png" im1 = Image.open(TEST_JPG) im2 = Image.open(TEST_PNG) - with open(TEST_PNG, 'rb') as fp: + with open(TEST_PNG, "rb") as fp: data = fp.read() kw = {"file": TEST_JPG, "data": data} @@ -61,9 +60,8 @@ class TestImageTk(PillowTestCase): self.assertEqual(im_tk.width(), im.width) self.assertEqual(im_tk.height(), im.height) - # _tkinter.TclError: this function is not yet supported - # reloaded = ImageTk.getimage(im_tk) - # self.assert_image_equal(reloaded, im) + reloaded = ImageTk.getimage(im_tk) + self.assert_image_equal(reloaded, im.convert("RGBA")) def test_photoimage_blank(self): # test a image using mode/size: @@ -77,7 +75,7 @@ class TestImageTk(PillowTestCase): # self.assert_image_equal(reloaded, im) def test_bitmapimage(self): - im = hopper('1') + im = hopper("1") # this should not crash im_tk = ImageTk.BitmapImage(im) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 16d681f2c..1cd8c674e 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,11 +1,11 @@ -from .helper import unittest, PillowTestCase, hopper +import unittest from PIL import ImageWin -import sys + +from .helper import PillowTestCase, hopper, is_win32 class TestImageWin(PillowTestCase): - def test_sanity(self): dir(ImageWin) @@ -32,9 +32,8 @@ 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 im = hopper() diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index 64f921916..d7c22a209 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,39 +1,39 @@ -from .helper import PillowTestCase, hopper -from PIL import Image, ImageWin - -import sys import ctypes from io import BytesIO +from PIL import Image, ImageWin + +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): _pack_ = 2 _fields_ = [ - ('bfType', ctypes.wintypes.WORD), - ('bfSize', ctypes.wintypes.DWORD), - ('bfReserved1', ctypes.wintypes.WORD), - ('bfReserved2', ctypes.wintypes.WORD), - ('bfOffBits', ctypes.wintypes.DWORD), + ("bfType", ctypes.wintypes.WORD), + ("bfSize", ctypes.wintypes.DWORD), + ("bfReserved1", ctypes.wintypes.WORD), + ("bfReserved2", ctypes.wintypes.WORD), + ("bfOffBits", ctypes.wintypes.DWORD), ] class BITMAPINFOHEADER(ctypes.Structure): _pack_ = 2 _fields_ = [ - ('biSize', ctypes.wintypes.DWORD), - ('biWidth', ctypes.wintypes.LONG), - ('biHeight', ctypes.wintypes.LONG), - ('biPlanes', ctypes.wintypes.WORD), - ('biBitCount', ctypes.wintypes.WORD), - ('biCompression', ctypes.wintypes.DWORD), - ('biSizeImage', ctypes.wintypes.DWORD), - ('biXPelsPerMeter', ctypes.wintypes.LONG), - ('biYPelsPerMeter', ctypes.wintypes.LONG), - ('biClrUsed', ctypes.wintypes.DWORD), - ('biClrImportant', ctypes.wintypes.DWORD), + ("biSize", ctypes.wintypes.DWORD), + ("biWidth", ctypes.wintypes.LONG), + ("biHeight", ctypes.wintypes.LONG), + ("biPlanes", ctypes.wintypes.WORD), + ("biBitCount", ctypes.wintypes.WORD), + ("biCompression", ctypes.wintypes.DWORD), + ("biSizeImage", ctypes.wintypes.DWORD), + ("biXPelsPerMeter", ctypes.wintypes.LONG), + ("biYPelsPerMeter", ctypes.wintypes.LONG), + ("biClrUsed", ctypes.wintypes.DWORD), + ("biClrImportant", ctypes.wintypes.DWORD), ] BI_RGB = 0 @@ -57,15 +57,19 @@ if sys.platform.startswith('win32'): DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ] CreateDIBSection = ctypes.windll.gdi32.CreateDIBSection - CreateDIBSection.argtypes = [ctypes.wintypes.HDC, ctypes.c_void_p, - ctypes.c_uint, - ctypes.POINTER(ctypes.c_void_p), - ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD] + CreateDIBSection.argtypes = [ + ctypes.wintypes.HDC, + ctypes.c_void_p, + ctypes.c_uint, + ctypes.POINTER(ctypes.c_void_p), + ctypes.wintypes.HANDLE, + ctypes.wintypes.DWORD, + ] CreateDIBSection.restype = ctypes.wintypes.HBITMAP def serialize_dib(bi, pixels): bf = BITMAPFILEHEADER() - bf.bfType = 0x4d42 + bf.bfType = 0x4D42 bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize bf.bfSize = bf.bfOffBits + bi.biSizeImage bf.bfReserved1 = bf.bfReserved2 = 0 @@ -81,7 +85,7 @@ if sys.platform.startswith('win32'): def test_pointer(self): im = hopper() (width, height) = im.size - opath = self.tempfile('temp.png') + opath = self.tempfile("temp.png") imdib = ImageWin.Dib(im) hdr = BITMAPINFOHEADER() @@ -97,8 +101,9 @@ if sys.platform.startswith('win32'): hdc = CreateCompatibleDC(None) pixels = ctypes.c_void_p() - dib = CreateDIBSection(hdc, ctypes.byref(hdr), DIB_RGB_COLORS, - ctypes.byref(pixels), None, 0) + dib = CreateDIBSection( + hdc, ctypes.byref(hdr), DIB_RGB_COLORS, ctypes.byref(pixels), None, 0 + ) SelectObject(hdc, dib) imdib.expose(hdc) diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 466c43f88..240783960 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,10 +1,11 @@ -from .helper import unittest, PillowTestCase +import unittest from PIL import Image +from .helper import PillowTestCase + class TestLibImage(PillowTestCase): - def test_setmode(self): im = Image.new("L", (1, 1), 255) @@ -33,5 +34,5 @@ class TestLibImage(PillowTestCase): self.assertRaises(ValueError, im.im.setmode, "RGBABCDE") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 543d151ac..45958d2eb 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,9 +1,8 @@ import sys -from .helper import PillowTestCase - from PIL import Image +from .helper import PillowTestCase X = 255 @@ -19,37 +18,36 @@ 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)) def test_1(self): - self.assert_pack("1", "1", b'\x01', 0, 0, 0, 0, 0, 0, 0, X) - self.assert_pack("1", "1;I", b'\x01', X, X, X, X, X, X, X, 0) - self.assert_pack("1", "1;R", b'\x01', X, 0, 0, 0, 0, 0, 0, 0) - self.assert_pack("1", "1;IR", b'\x01', 0, X, X, X, X, X, X, X) + self.assert_pack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) + self.assert_pack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) + self.assert_pack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) + self.assert_pack("1", "1;IR", b"\x01", 0, X, X, X, X, X, X, X) - self.assert_pack("1", "1", b'\xaa', X, 0, X, 0, X, 0, X, 0) - self.assert_pack("1", "1;I", b'\xaa', 0, X, 0, X, 0, X, 0, X) - self.assert_pack("1", "1;R", b'\xaa', 0, X, 0, X, 0, X, 0, X) - self.assert_pack("1", "1;IR", b'\xaa', X, 0, X, 0, X, 0, X, 0) + self.assert_pack("1", "1", b"\xaa", X, 0, X, 0, X, 0, X, 0) + self.assert_pack("1", "1;I", b"\xaa", 0, X, 0, X, 0, X, 0, X) + self.assert_pack("1", "1;R", b"\xaa", 0, X, 0, X, 0, X, 0, X) + self.assert_pack("1", "1;IR", b"\xaa", X, 0, X, 0, X, 0, X, 0) - self.assert_pack( - "1", "L", b'\xff\x00\x00\xff\x00\x00', X, 0, 0, X, 0, 0) + self.assert_pack("1", "L", b"\xff\x00\x00\xff\x00\x00", X, 0, 0, X, 0, 0) def test_L(self): self.assert_pack("L", "L", 1, 1, 2, 3, 4) - self.assert_pack("L", "L;16", b'\x00\xc6\x00\xaf', 198, 175) - self.assert_pack("L", "L;16B", b'\xc6\x00\xaf\x00', 198, 175) + self.assert_pack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) + self.assert_pack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) def test_LA(self): 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_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) - self.assert_pack("P", "P;4", b'\x02\xef', 0, 2, 14, 15) + self.assert_pack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 255, 0, 0) + self.assert_pack("P", "P;2", b"\xe4", 3, 2, 1, 0) + self.assert_pack("P", "P;4", b"\x02\xef", 0, 2, 14, 15) self.assert_pack("P", "P", 1, 1, 2, 3, 4) def test_PA(self): @@ -59,116 +57,118 @@ class TestLibPack(PillowTestCase): def test_RGB(self): self.assert_pack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack( - "RGB", "RGBX", - b'\x01\x02\x03\xff\x05\x06\x07\xff', (1, 2, 3), (5, 6, 7)) + "RGB", "RGBX", b"\x01\x02\x03\xff\x05\x06\x07\xff", (1, 2, 3), (5, 6, 7) + ) self.assert_pack( - "RGB", "XRGB", - b'\x00\x02\x03\x04\x00\x06\x07\x08', (2, 3, 4), (6, 7, 8)) + "RGB", "XRGB", b"\x00\x02\x03\x04\x00\x06\x07\x08", (2, 3, 4), (6, 7, 8) + ) self.assert_pack("RGB", "BGR", 3, (3, 2, 1), (6, 5, 4), (9, 8, 7)) self.assert_pack( - "RGB", "BGRX", - b'\x01\x02\x03\x00\x05\x06\x07\x00', (3, 2, 1), (7, 6, 5)) + "RGB", "BGRX", b"\x01\x02\x03\x00\x05\x06\x07\x00", (3, 2, 1), (7, 6, 5) + ) self.assert_pack( - "RGB", "XBGR", - b'\x00\x02\x03\x04\x00\x06\x07\x08', (4, 3, 2), (8, 7, 6)) + "RGB", "XBGR", b"\x00\x02\x03\x04\x00\x06\x07\x08", (4, 3, 2), (8, 7, 6) + ) self.assert_pack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_pack("RGB", "R", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) self.assert_pack("RGB", "G", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("RGB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) def test_RGBA(self): + self.assert_pack("RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( - "RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_pack("RGBA", "RGB", 3, (1, 2, 3, 14), (4, 5, 6, 15), (7, 8, 9, 16)) + self.assert_pack("RGBA", "BGR", 3, (3, 2, 1, 14), (6, 5, 4, 15), (9, 8, 7, 16)) + self.assert_pack("RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) + self.assert_pack("RGBA", "ABGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) self.assert_pack( - "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) - self.assert_pack( - "RGBA", "RGB", 3, (1, 2, 3, 14), (4, 5, 6, 15), (7, 8, 9, 16)) - self.assert_pack( - "RGBA", "BGR", 3, (3, 2, 1, 14), (6, 5, 4, 15), (9, 8, 7, 16)) - self.assert_pack( - "RGBA", "BGRA", 4, - (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) - self.assert_pack( - "RGBA", "ABGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) - self.assert_pack( - "RGBA", "BGRa", 4, - (191, 127, 63, 4), (223, 191, 159, 8), (233, 212, 191, 12)) - self.assert_pack( - "RGBA", "R", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) - self.assert_pack( - "RGBA", "G", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) - self.assert_pack( - "RGBA", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) - self.assert_pack( - "RGBA", "A", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) + "RGBA", + "BGRa", + 4, + (191, 127, 63, 4), + (223, 191, 159, 8), + (233, 212, 191, 12), + ) + self.assert_pack("RGBA", "R", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) + self.assert_pack("RGBA", "G", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) + self.assert_pack("RGBA", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) + self.assert_pack("RGBA", "A", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) def test_RGBa(self): - self.assert_pack( - "RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_pack( - "RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) - self.assert_pack( - "RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) + self.assert_pack("RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack("RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) + self.assert_pack("RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) def test_RGBX(self): + self.assert_pack("RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( - "RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_pack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) + self.assert_pack("RGBX", "BGR", 3, (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) self.assert_pack( - "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) + "RGBX", + "BGRX", + b"\x01\x02\x03\x00\x05\x06\x07\x00\t\n\x0b\x00", + (3, 2, 1, X), + (7, 6, 5, X), + (11, 10, 9, X), + ) self.assert_pack( - "RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) - self.assert_pack( - "RGBX", "BGR", 3, (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) - self.assert_pack( - "RGBX", "BGRX", - b'\x01\x02\x03\x00\x05\x06\x07\x00\t\n\x0b\x00', - (3, 2, 1, X), (7, 6, 5, X), (11, 10, 9, X)) - self.assert_pack( - "RGBX", "XBGR", - b'\x00\x02\x03\x04\x00\x06\x07\x08\x00\n\x0b\x0c', - (4, 3, 2, X), (8, 7, 6, X), (12, 11, 10, X)) - self.assert_pack("RGBX", "R", 1, - (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) - self.assert_pack("RGBX", "G", 1, - (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) - self.assert_pack("RGBX", "B", 1, - (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) - self.assert_pack("RGBX", "X", 1, - (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) + "RGBX", + "XBGR", + b"\x00\x02\x03\x04\x00\x06\x07\x08\x00\n\x0b\x0c", + (4, 3, 2, X), + (8, 7, 6, X), + (12, 11, 10, X), + ) + self.assert_pack("RGBX", "R", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) + self.assert_pack("RGBX", "G", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) + self.assert_pack("RGBX", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) + self.assert_pack("RGBX", "X", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) def test_CMYK(self): - self.assert_pack("CMYK", "CMYK", 4, - (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack("CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( - "CMYK", "CMYK;I", 4, - (254, 253, 252, 251), (250, 249, 248, 247), (246, 245, 244, 243)) + "CMYK", + "CMYK;I", + 4, + (254, 253, 252, 251), + (250, 249, 248, 247), + (246, 245, 244, 243), + ) self.assert_pack( - "CMYK", "CMYK;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) - self.assert_pack("CMYK", "K", 1, - (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) + "CMYK", "CMYK;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_pack("CMYK", "K", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) def test_YCbCr(self): self.assert_pack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - self.assert_pack("YCbCr", "YCbCr;L", 3, - (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_pack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_pack( - "YCbCr", "YCbCrX", - b'\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff', - (1, 2, 3), (5, 6, 7), (9, 10, 11)) + "YCbCr", + "YCbCrX", + b"\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff", + (1, 2, 3), + (5, 6, 7), + (9, 10, 11), + ) self.assert_pack( - "YCbCr", "YCbCrK", - b'\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff', - (1, 2, 3), (5, 6, 7), (9, 10, 11)) - self.assert_pack("YCbCr", "Y", 1, - (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) - self.assert_pack("YCbCr", "Cb", 1, - (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) - self.assert_pack("YCbCr", "Cr", 1, - (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) + "YCbCr", + "YCbCrK", + b"\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff", + (1, 2, 3), + (5, 6, 7), + (9, 10, 11), + ) + self.assert_pack("YCbCr", "Y", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) + self.assert_pack("YCbCr", "Cb", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) + self.assert_pack("YCbCr", "Cr", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) def test_LAB(self): - self.assert_pack( - "LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) + self.assert_pack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) self.assert_pack("LAB", "L", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) self.assert_pack("LAB", "A", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("LAB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) @@ -182,34 +182,41 @@ class TestLibPack(PillowTestCase): def test_I(self): self.assert_pack("I", "I;16B", 2, 0x0102, 0x0304) self.assert_pack( - "I", "I;32S", - b'\x83\x00\x00\x01\x01\x00\x00\x83', 0x01000083, -2097151999) + "I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999 + ) - if sys.byteorder == 'little': + if sys.byteorder == "little": self.assert_pack("I", "I", 4, 0x04030201, 0x08070605) self.assert_pack( - "I", "I;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', 0x01000083, -2097151999) + "I", + "I;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + 0x01000083, + -2097151999, + ) else: self.assert_pack("I", "I", 4, 0x01020304, 0x05060708) self.assert_pack( - "I", "I;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', -2097151999, 0x01000083) + "I", + "I;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + -2097151999, + 0x01000083, + ) def test_F_float(self): - self.assert_pack( - "F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) + self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) - if sys.byteorder == 'little': + if sys.byteorder == "little": + self.assert_pack("F", "F", 4, 1.539989614439558e-36, 4.063216068939723e-34) self.assert_pack( - "F", "F", 4, 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_pack( - "F", "F;32NF", 4, 1.539989614439558e-36, 4.063216068939723e-34) + "F", "F;32NF", 4, 1.539989614439558e-36, 4.063216068939723e-34 + ) else: + self.assert_pack("F", "F", 4, 2.387939260590663e-38, 6.301941157072183e-36) self.assert_pack( - "F", "F", 4, 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_pack( - "F", "F;32NF", 4, 2.387939260590663e-38, 6.301941157072183e-36) + "F", "F;32NF", 4, 2.387939260590663e-38, 6.301941157072183e-36 + ) class TestLibUnpack(PillowTestCase): @@ -219,56 +226,55 @@ 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) + im = Image.frombytes(mode, (len(pixels), 1), data, "raw", rawmode, 0, 1) for x, pixel in enumerate(pixels): self.assertEqual(pixel, im.getpixel((x, 0))) def test_1(self): - self.assert_unpack("1", "1", b'\x01', 0, 0, 0, 0, 0, 0, 0, X) - self.assert_unpack("1", "1;I", b'\x01', X, X, X, X, X, X, X, 0) - self.assert_unpack("1", "1;R", b'\x01', X, 0, 0, 0, 0, 0, 0, 0) - self.assert_unpack("1", "1;IR", b'\x01', 0, X, X, X, X, X, X, X) + self.assert_unpack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) + self.assert_unpack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) + self.assert_unpack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) + self.assert_unpack("1", "1;IR", b"\x01", 0, X, X, X, X, X, X, X) - self.assert_unpack("1", "1", b'\xaa', X, 0, X, 0, X, 0, X, 0) - self.assert_unpack("1", "1;I", b'\xaa', 0, X, 0, X, 0, X, 0, X) - self.assert_unpack("1", "1;R", b'\xaa', 0, X, 0, X, 0, X, 0, X) - self.assert_unpack("1", "1;IR", b'\xaa', X, 0, X, 0, X, 0, X, 0) + self.assert_unpack("1", "1", b"\xaa", X, 0, X, 0, X, 0, X, 0) + self.assert_unpack("1", "1;I", b"\xaa", 0, X, 0, X, 0, X, 0, X) + self.assert_unpack("1", "1;R", b"\xaa", 0, X, 0, X, 0, X, 0, X) + self.assert_unpack("1", "1;IR", b"\xaa", X, 0, X, 0, X, 0, X, 0) - self.assert_unpack("1", "1;8", b'\x00\x01\x02\xff', 0, X, X, X) + self.assert_unpack("1", "1;8", b"\x00\x01\x02\xff", 0, X, X, X) def test_L(self): - self.assert_unpack("L", "L;2", b'\xe4', 255, 170, 85, 0) - self.assert_unpack("L", "L;2I", b'\xe4', 0, 85, 170, 255) - self.assert_unpack("L", "L;2R", b'\xe4', 0, 170, 85, 255) - self.assert_unpack("L", "L;2IR", b'\xe4', 255, 85, 170, 0) + self.assert_unpack("L", "L;2", b"\xe4", 255, 170, 85, 0) + self.assert_unpack("L", "L;2I", b"\xe4", 0, 85, 170, 255) + self.assert_unpack("L", "L;2R", b"\xe4", 0, 170, 85, 255) + self.assert_unpack("L", "L;2IR", b"\xe4", 255, 85, 170, 0) - self.assert_unpack("L", "L;4", b'\x02\xef', 0, 34, 238, 255) - self.assert_unpack("L", "L;4I", b'\x02\xef', 255, 221, 17, 0) - self.assert_unpack("L", "L;4R", b'\x02\xef', 68, 0, 255, 119) - self.assert_unpack("L", "L;4IR", b'\x02\xef', 187, 255, 0, 136) + self.assert_unpack("L", "L;4", b"\x02\xef", 0, 34, 238, 255) + self.assert_unpack("L", "L;4I", b"\x02\xef", 255, 221, 17, 0) + self.assert_unpack("L", "L;4R", b"\x02\xef", 68, 0, 255, 119) + self.assert_unpack("L", "L;4IR", b"\x02\xef", 187, 255, 0, 136) self.assert_unpack("L", "L", 1, 1, 2, 3, 4) self.assert_unpack("L", "L;I", 1, 254, 253, 252, 251) self.assert_unpack("L", "L;R", 1, 128, 64, 192, 32) self.assert_unpack("L", "L;16", 2, 2, 4, 6, 8) self.assert_unpack("L", "L;16B", 2, 1, 3, 5, 7) - self.assert_unpack("L", "L;16", b'\x00\xc6\x00\xaf', 198, 175) - self.assert_unpack("L", "L;16B", b'\xc6\x00\xaf\x00', 198, 175) + self.assert_unpack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) + self.assert_unpack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) def test_LA(self): 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_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) + self.assert_unpack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 1, 0, 0) + self.assert_unpack("P", "P;2", b"\xe4", 3, 2, 1, 0) # erroneous? # self.assert_unpack("P", "P;2L", b'\xe4', 1, 1, 1, 0) - self.assert_unpack("P", "P;4", b'\x02\xef', 0, 2, 14, 15) + self.assert_unpack("P", "P;4", b"\x02\xef", 0, 2, 14, 15) # erroneous? # self.assert_unpack("P", "P;4L", b'\x02\xef', 2, 10, 10, 0) self.assert_unpack("P", "P", 1, 1, 2, 3, 4) @@ -293,195 +299,256 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack("RGB", "RGBX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) self.assert_unpack("RGB", "RGBX;L", 4, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_unpack("RGB", "BGRX", 4, (3, 2, 1), (7, 6, 5), (11, 10, 9)) + self.assert_unpack("RGB", "XRGB", 4, (2, 3, 4), (6, 7, 8), (10, 11, 12)) + self.assert_unpack("RGB", "XBGR", 4, (4, 3, 2), (8, 7, 6), (12, 11, 10)) self.assert_unpack( - "RGB", "XRGB", 4, (2, 3, 4), (6, 7, 8), (10, 11, 12)) - self.assert_unpack( - "RGB", "XBGR", 4, (4, 3, 2), (8, 7, 6), (12, 11, 10)) - self.assert_unpack( - "RGB", "YCC;P", - b'D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12', # random data - (127, 102, 0), (192, 227, 0), (213, 255, 170), (98, 255, 133)) + "RGB", + "YCC;P", + b"D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12", # random data + (127, 102, 0), + (192, 227, 0), + (213, 255, 170), + (98, 255, 133), + ) self.assert_unpack("RGB", "R", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) self.assert_unpack("RGB", "G", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) self.assert_unpack("RGB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) def test_RGBA(self): + self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) self.assert_unpack( - "RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) + "RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11) + ) self.assert_unpack( - "RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11)) + "RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) + ) self.assert_unpack( - "RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + "RGBA", "RGBAX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14) + ) self.assert_unpack( - "RGBA", "RGBAX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14)) + "RGBA", "RGBAXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16) + ) self.assert_unpack( - "RGBA", "RGBAXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16)) + "RGBA", + "RGBa", + 4, + (63, 127, 191, 4), + (159, 191, 223, 8), + (191, 212, 233, 12), + ) self.assert_unpack( - "RGBA", "RGBa", 4, - (63, 127, 191, 4), (159, 191, 223, 8), (191, 212, 233, 12)) + "RGBA", + "RGBa", + b"\x01\x02\x03\x00\x10\x20\x30\x7f\x10\x20\x30\xff", + (0, 0, 0, 0), + (32, 64, 96, 127), + (16, 32, 48, 255), + ) self.assert_unpack( - "RGBA", "RGBa", - b'\x01\x02\x03\x00\x10\x20\x30\x7f\x10\x20\x30\xff', - (0, 0, 0, 0), (32, 64, 96, 127), (16, 32, 48, 255)) + "RGBA", + "RGBaX", + b"\x01\x02\x03\x00-\x10\x20\x30\x7f-\x10\x20\x30\xff-", + (0, 0, 0, 0), + (32, 64, 96, 127), + (16, 32, 48, 255), + ) self.assert_unpack( - "RGBA", "RGBaX", - b'\x01\x02\x03\x00-\x10\x20\x30\x7f-\x10\x20\x30\xff-', - (0, 0, 0, 0), (32, 64, 96, 127), (16, 32, 48, 255)) + "RGBA", + "RGBaXX", + b"\x01\x02\x03\x00==\x10\x20\x30\x7f!!\x10\x20\x30\xff??", + (0, 0, 0, 0), + (32, 64, 96, 127), + (16, 32, 48, 255), + ) self.assert_unpack( - "RGBA", "RGBaXX", - b'\x01\x02\x03\x00==\x10\x20\x30\x7f!!\x10\x20\x30\xff??', - (0, 0, 0, 0), (32, 64, 96, 127), (16, 32, 48, 255)) + "RGBA", + "RGBa;16L", + 8, + (63, 127, 191, 8), + (159, 191, 223, 16), + (191, 212, 233, 24), + ) self.assert_unpack( - "RGBA", "RGBa;16L", 8, - (63, 127, 191, 8), (159, 191, 223, 16), (191, 212, 233, 24)) + "RGBA", + "RGBa;16L", + b"\x88\x01\x88\x02\x88\x03\x88\x00" b"\x88\x10\x88\x20\x88\x30\x88\xff", + (0, 0, 0, 0), + (16, 32, 48, 255), + ) self.assert_unpack( - "RGBA", "RGBa;16L", - b'\x88\x01\x88\x02\x88\x03\x88\x00' - b'\x88\x10\x88\x20\x88\x30\x88\xff', - (0, 0, 0, 0), (16, 32, 48, 255)) + "RGBA", + "RGBa;16B", + 8, + (36, 109, 182, 7), + (153, 187, 221, 15), + (188, 210, 232, 23), + ) self.assert_unpack( - "RGBA", "RGBa;16B", 8, - (36, 109, 182, 7), (153, 187, 221, 15), (188, 210, 232, 23)) + "RGBA", + "RGBa;16B", + b"\x01\x88\x02\x88\x03\x88\x00\x88" b"\x10\x88\x20\x88\x30\x88\xff\x88", + (0, 0, 0, 0), + (16, 32, 48, 255), + ) self.assert_unpack( - "RGBA", "RGBa;16B", - b'\x01\x88\x02\x88\x03\x88\x00\x88' - b'\x10\x88\x20\x88\x30\x88\xff\x88', - (0, 0, 0, 0), (16, 32, 48, 255)) + "RGBA", + "BGRa", + 4, + (191, 127, 63, 4), + (223, 191, 159, 8), + (233, 212, 191, 12), + ) self.assert_unpack( - "RGBA", "BGRa", 4, - (191, 127, 63, 4), (223, 191, 159, 8), (233, 212, 191, 12)) + "RGBA", + "BGRa", + b"\x01\x02\x03\x00\x10\x20\x30\xff", + (0, 0, 0, 0), + (48, 32, 16, 255), + ) self.assert_unpack( - "RGBA", "BGRa", - b'\x01\x02\x03\x00\x10\x20\x30\xff', - (0, 0, 0, 0), (48, 32, 16, 255)) + "RGBA", + "RGBA;I", + 4, + (254, 253, 252, 4), + (250, 249, 248, 8), + (246, 245, 244, 12), + ) self.assert_unpack( - "RGBA", "RGBA;I", 4, - (254, 253, 252, 4), (250, 249, 248, 8), (246, 245, 244, 12)) - self.assert_unpack( - "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) + "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) self.assert_unpack("RGBA", "RGBA;15", 2, (8, 131, 0, 0), (24, 0, 8, 0)) self.assert_unpack("RGBA", "BGRA;15", 2, (0, 131, 8, 0), (8, 0, 24, 0)) + self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0)) + self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) self.assert_unpack( - "RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0)) + "RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12) + ) self.assert_unpack( - "RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + "RGBA", "ARGB", 4, (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9) + ) self.assert_unpack( - "RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) + "RGBA", "ABGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9) + ) self.assert_unpack( - "RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) - self.assert_unpack( - "RGBA", "ARGB", 4, (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9)) - self.assert_unpack( - "RGBA", "ABGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) - self.assert_unpack( - "RGBA", "YCCA;P", - b']bE\x04\xdd\xbej\xed57T\xce\xac\xce:\x11', # random data - (0, 161, 0, 4), (255, 255, 255, 237), - (27, 158, 0, 206), (0, 118, 0, 17)) - self.assert_unpack( - "RGBA", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) - self.assert_unpack( - "RGBA", "G", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) - self.assert_unpack( - "RGBA", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) - self.assert_unpack( - "RGBA", "A", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) + "RGBA", + "YCCA;P", + b"]bE\x04\xdd\xbej\xed57T\xce\xac\xce:\x11", # random data + (0, 161, 0, 4), + (255, 255, 255, 237), + (27, 158, 0, 206), + (0, 118, 0, 17), + ) + self.assert_unpack("RGBA", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) + self.assert_unpack("RGBA", "G", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) + self.assert_unpack("RGBA", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) + self.assert_unpack("RGBA", "A", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) def test_RGBa(self): self.assert_unpack( - "RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + "RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) + ) self.assert_unpack( - "RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) + "RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12) + ) self.assert_unpack( - "RGBa", "aRGB", 4, (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9)) + "RGBa", "aRGB", 4, (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9) + ) self.assert_unpack( - "RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) + "RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9) + ) def test_RGBX(self): - self.assert_unpack("RGBX", "RGB", 3, - (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) - self.assert_unpack("RGBX", "RGB;L", 3, - (1, 4, 7, X), (2, 5, 8, X), (3, 6, 9, X)) + self.assert_unpack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) + self.assert_unpack("RGBX", "RGB;L", 3, (1, 4, 7, X), (2, 5, 8, X), (3, 6, 9, X)) self.assert_unpack("RGBX", "RGB;16B", 6, (1, 3, 5, X), (7, 9, 11, X)) - self.assert_unpack("RGBX", "BGR", 3, - (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) + self.assert_unpack("RGBX", "BGR", 3, (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) self.assert_unpack("RGBX", "RGB;15", 2, (8, 131, 0, X), (24, 0, 8, X)) self.assert_unpack("RGBX", "BGR;15", 2, (0, 131, 8, X), (8, 0, 24, X)) self.assert_unpack("RGBX", "RGB;4B", 2, (17, 0, 34, X), (51, 0, 68, X)) self.assert_unpack( - "RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + "RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) + ) self.assert_unpack( - "RGBX", "RGBXX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14)) + "RGBX", "RGBXX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14) + ) self.assert_unpack( - "RGBX", "RGBXXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16)) + "RGBX", "RGBXXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16) + ) self.assert_unpack( - "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) - self.assert_unpack("RGBX", "RGBX;16L", 8, - (2, 4, 6, 8), (10, 12, 14, 16)) - self.assert_unpack("RGBX", "RGBX;16B", 8, - (1, 3, 5, 7), (9, 11, 13, 15)) - self.assert_unpack("RGBX", "BGRX", 4, - (3, 2, 1, X), (7, 6, 5, X), (11, 10, 9, X)) - self.assert_unpack("RGBX", "XRGB", 4, - (2, 3, 4, X), (6, 7, 8, X), (10, 11, 12, X)) - self.assert_unpack("RGBX", "XBGR", 4, - (4, 3, 2, X), (8, 7, 6, X), (12, 11, 10, X)) + "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_unpack("RGBX", "RGBX;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + self.assert_unpack("RGBX", "RGBX;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) self.assert_unpack( - "RGBX", "YCC;P", - b'D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12', # random data - (127, 102, 0, X), (192, 227, 0, X), - (213, 255, 170, X), (98, 255, 133, X)) - self.assert_unpack("RGBX", "R", 1, - (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) - self.assert_unpack("RGBX", "G", 1, - (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) - self.assert_unpack("RGBX", "B", 1, - (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) - self.assert_unpack("RGBX", "X", 1, - (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) + "RGBX", "BGRX", 4, (3, 2, 1, X), (7, 6, 5, X), (11, 10, 9, X) + ) + self.assert_unpack( + "RGBX", "XRGB", 4, (2, 3, 4, X), (6, 7, 8, X), (10, 11, 12, X) + ) + self.assert_unpack( + "RGBX", "XBGR", 4, (4, 3, 2, X), (8, 7, 6, X), (12, 11, 10, X) + ) + self.assert_unpack( + "RGBX", + "YCC;P", + b"D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12", # random data + (127, 102, 0, X), + (192, 227, 0, X), + (213, 255, 170, X), + (98, 255, 133, X), + ) + self.assert_unpack("RGBX", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) + self.assert_unpack("RGBX", "G", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) + self.assert_unpack("RGBX", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) + self.assert_unpack("RGBX", "X", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) def test_CMYK(self): self.assert_unpack( - "CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + "CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) + ) self.assert_unpack( - "CMYK", "CMYKX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14)) + "CMYK", "CMYKX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14) + ) self.assert_unpack( - "CMYK", "CMYKXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16)) + "CMYK", "CMYKXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16) + ) self.assert_unpack( - "CMYK", "CMYK;I", 4, - (254, 253, 252, 251), (250, 249, 248, 247), (246, 245, 244, 243)) + "CMYK", + "CMYK;I", + 4, + (254, 253, 252, 251), + (250, 249, 248, 247), + (246, 245, 244, 243), + ) self.assert_unpack( - "CMYK", "CMYK;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) - self.assert_unpack("CMYK", "C", 1, - (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) - self.assert_unpack("CMYK", "M", 1, - (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) - self.assert_unpack("CMYK", "Y", 1, - (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) - self.assert_unpack("CMYK", "K", 1, - (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) + "CMYK", "CMYK;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_unpack("CMYK", "C", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) + self.assert_unpack("CMYK", "M", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) + self.assert_unpack("CMYK", "Y", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) + self.assert_unpack("CMYK", "K", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) self.assert_unpack( - "CMYK", "C;I", 1, (254, 0, 0, 0), (253, 0, 0, 0), (252, 0, 0, 0)) + "CMYK", "C;I", 1, (254, 0, 0, 0), (253, 0, 0, 0), (252, 0, 0, 0) + ) self.assert_unpack( - "CMYK", "M;I", 1, (0, 254, 0, 0), (0, 253, 0, 0), (0, 252, 0, 0)) + "CMYK", "M;I", 1, (0, 254, 0, 0), (0, 253, 0, 0), (0, 252, 0, 0) + ) self.assert_unpack( - "CMYK", "Y;I", 1, (0, 0, 254, 0), (0, 0, 253, 0), (0, 0, 252, 0)) + "CMYK", "Y;I", 1, (0, 0, 254, 0), (0, 0, 253, 0), (0, 0, 252, 0) + ) self.assert_unpack( - "CMYK", "K;I", 1, (0, 0, 0, 254), (0, 0, 0, 253), (0, 0, 0, 252)) + "CMYK", "K;I", 1, (0, 0, 0, 254), (0, 0, 0, 253), (0, 0, 0, 252) + ) def test_YCbCr(self): - self.assert_unpack( - "YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - self.assert_unpack( - "YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) - self.assert_unpack( - "YCbCr", "YCbCrK", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) - self.assert_unpack( - "YCbCr", "YCbCrX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) + self.assert_unpack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_unpack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_unpack("YCbCr", "YCbCrK", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) + self.assert_unpack("YCbCr", "YCbCrX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) def test_LAB(self): - self.assert_unpack( - "LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) + self.assert_unpack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) self.assert_unpack("LAB", "L", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) self.assert_unpack("LAB", "A", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) self.assert_unpack("LAB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) @@ -494,120 +561,141 @@ class TestLibUnpack(PillowTestCase): def test_I(self): self.assert_unpack("I", "I;8", 1, 0x01, 0x02, 0x03, 0x04) - self.assert_unpack("I", "I;8S", b'\x01\x83', 1, -125) + self.assert_unpack("I", "I;8S", b"\x01\x83", 1, -125) self.assert_unpack("I", "I;16", 2, 0x0201, 0x0403) - self.assert_unpack("I", "I;16S", b'\x83\x01\x01\x83', 0x0183, -31999) + self.assert_unpack("I", "I;16S", b"\x83\x01\x01\x83", 0x0183, -31999) self.assert_unpack("I", "I;16B", 2, 0x0102, 0x0304) - self.assert_unpack("I", "I;16BS", b'\x83\x01\x01\x83', -31999, 0x0183) + self.assert_unpack("I", "I;16BS", b"\x83\x01\x01\x83", -31999, 0x0183) self.assert_unpack("I", "I;32", 4, 0x04030201, 0x08070605) self.assert_unpack( - "I", "I;32S", - b'\x83\x00\x00\x01\x01\x00\x00\x83', 0x01000083, -2097151999) + "I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999 + ) self.assert_unpack("I", "I;32B", 4, 0x01020304, 0x05060708) self.assert_unpack( - "I", "I;32BS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', -2097151999, 0x01000083) + "I", "I;32BS", b"\x83\x00\x00\x01\x01\x00\x00\x83", -2097151999, 0x01000083 + ) - if sys.byteorder == 'little': + if sys.byteorder == "little": self.assert_unpack("I", "I", 4, 0x04030201, 0x08070605) self.assert_unpack("I", "I;16N", 2, 0x0201, 0x0403) - self.assert_unpack("I", "I;16NS", - b'\x83\x01\x01\x83', 0x0183, -31999) + self.assert_unpack("I", "I;16NS", b"\x83\x01\x01\x83", 0x0183, -31999) self.assert_unpack("I", "I;32N", 4, 0x04030201, 0x08070605) self.assert_unpack( - "I", "I;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', 0x01000083, -2097151999) + "I", + "I;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + 0x01000083, + -2097151999, + ) else: self.assert_unpack("I", "I", 4, 0x01020304, 0x05060708) self.assert_unpack("I", "I;16N", 2, 0x0102, 0x0304) - self.assert_unpack("I", "I;16NS", - b'\x83\x01\x01\x83', -31999, 0x0183) + self.assert_unpack("I", "I;16NS", b"\x83\x01\x01\x83", -31999, 0x0183) self.assert_unpack("I", "I;32N", 4, 0x01020304, 0x05060708) self.assert_unpack( - "I", "I;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', -2097151999, 0x01000083) + "I", + "I;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + -2097151999, + 0x01000083, + ) def test_F_int(self): self.assert_unpack("F", "F;8", 1, 0x01, 0x02, 0x03, 0x04) - self.assert_unpack("F", "F;8S", b'\x01\x83', 1, -125) + self.assert_unpack("F", "F;8S", b"\x01\x83", 1, -125) self.assert_unpack("F", "F;16", 2, 0x0201, 0x0403) - self.assert_unpack("F", "F;16S", b'\x83\x01\x01\x83', 0x0183, -31999) + self.assert_unpack("F", "F;16S", b"\x83\x01\x01\x83", 0x0183, -31999) self.assert_unpack("F", "F;16B", 2, 0x0102, 0x0304) - self.assert_unpack("F", "F;16BS", b'\x83\x01\x01\x83', -31999, 0x0183) + self.assert_unpack("F", "F;16BS", b"\x83\x01\x01\x83", -31999, 0x0183) self.assert_unpack("F", "F;32", 4, 67305984, 134678016) self.assert_unpack( - "F", "F;32S", - b'\x83\x00\x00\x01\x01\x00\x00\x83', 16777348, -2097152000) + "F", "F;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 16777348, -2097152000 + ) self.assert_unpack("F", "F;32B", 4, 0x01020304, 0x05060708) self.assert_unpack( - "F", "F;32BS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', -2097152000, 16777348) + "F", "F;32BS", b"\x83\x00\x00\x01\x01\x00\x00\x83", -2097152000, 16777348 + ) - if sys.byteorder == 'little': + if sys.byteorder == "little": self.assert_unpack("F", "F;16N", 2, 0x0201, 0x0403) - self.assert_unpack( - "F", "F;16NS", - b'\x83\x01\x01\x83', 0x0183, -31999) + self.assert_unpack("F", "F;16NS", b"\x83\x01\x01\x83", 0x0183, -31999) self.assert_unpack("F", "F;32N", 4, 67305984, 134678016) self.assert_unpack( - "F", "F;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', 16777348, -2097152000) + "F", + "F;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + 16777348, + -2097152000, + ) else: self.assert_unpack("F", "F;16N", 2, 0x0102, 0x0304) - self.assert_unpack( - "F", "F;16NS", - b'\x83\x01\x01\x83', -31999, 0x0183) + self.assert_unpack("F", "F;16NS", b"\x83\x01\x01\x83", -31999, 0x0183) self.assert_unpack("F", "F;32N", 4, 0x01020304, 0x05060708) self.assert_unpack( - "F", "F;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - -2097152000, 16777348) + "F", + "F;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + -2097152000, + 16777348, + ) def test_F_float(self): self.assert_unpack( - "F", "F;32F", 4, - 1.539989614439558e-36, 4.063216068939723e-34) + "F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34 + ) self.assert_unpack( - "F", "F;32BF", 4, - 2.387939260590663e-38, 6.301941157072183e-36) + "F", "F;32BF", 4, 2.387939260590663e-38, 6.301941157072183e-36 + ) self.assert_unpack( - "F", "F;64F", - b'333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0', # by struct.pack - 0.15000000596046448, -1234.5) + "F", + "F;64F", + b"333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0", # by struct.pack + 0.15000000596046448, + -1234.5, + ) self.assert_unpack( - "F", "F;64BF", - b'?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00', # by struct.pack - 0.15000000596046448, -1234.5) + "F", + "F;64BF", + b"?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00", # by struct.pack + 0.15000000596046448, + -1234.5, + ) - if sys.byteorder == 'little': + if sys.byteorder == "little": self.assert_unpack( - "F", "F", 4, - 1.539989614439558e-36, 4.063216068939723e-34) + "F", "F", 4, 1.539989614439558e-36, 4.063216068939723e-34 + ) self.assert_unpack( - "F", "F;32NF", 4, - 1.539989614439558e-36, 4.063216068939723e-34) + "F", "F;32NF", 4, 1.539989614439558e-36, 4.063216068939723e-34 + ) self.assert_unpack( - "F", "F;64NF", - b'333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0', - 0.15000000596046448, -1234.5) + "F", + "F;64NF", + b"333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0", + 0.15000000596046448, + -1234.5, + ) else: self.assert_unpack( - "F", "F", 4, - 2.387939260590663e-38, 6.301941157072183e-36) + "F", "F", 4, 2.387939260590663e-38, 6.301941157072183e-36 + ) self.assert_unpack( - "F", "F;32NF", 4, - 2.387939260590663e-38, 6.301941157072183e-36) + "F", "F;32NF", 4, 2.387939260590663e-38, 6.301941157072183e-36 + ) self.assert_unpack( - "F", "F;64NF", - b'?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00', - 0.15000000596046448, -1234.5) + "F", + "F;64NF", + b"?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00", + 0.15000000596046448, + -1234.5, + ) def test_I16(self): self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040) - if sys.byteorder == 'little': + if sys.byteorder == "little": self.assert_unpack("I;16", "I;16N", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16B", "I;16N", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16L", "I;16N", 2, 0x0201, 0x0403, 0x0605) @@ -616,6 +704,14 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack("I;16B", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506) + def test_CMYK16(self): + self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + self.assert_unpack("CMYK", "CMYK;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) + if sys.byteorder == "little": + self.assert_unpack("CMYK", "CMYK;16N", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + else: + self.assert_unpack("CMYK", "CMYK;16N", 8, (1, 3, 5, 7), (9, 11, 13, 15)) + def test_value_error(self): self.assertRaises(ValueError, self.assert_unpack, "L", "L", 0, 0) self.assertRaises(ValueError, self.assert_unpack, "RGB", "RGB", 2, 0) diff --git a/Tests/test_locale.py b/Tests/test_locale.py index d40019e59..0b1f330ac 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,9 +1,9 @@ -from __future__ import print_function -from .helper import unittest, PillowTestCase +import locale +import unittest from PIL import Image -import locale +from .helper import PillowTestCase # ref https://github.com/python-pillow/Pillow/issues/272 # on windows, in polish locale: @@ -24,11 +24,16 @@ path = "Tests/images/hopper.jpg" class TestLocale(PillowTestCase): - def test_sanity(self): - Image.open(path) + with Image.open(path): + pass try: locale.setlocale(locale.LC_ALL, "polish") except locale.Error: - unittest.skip('Polish locale not available') - Image.open(path) + unittest.skip("Polish locale not available") + + try: + with Image.open(path): + pass + finally: + locale.setlocale(locale.LC_ALL, (None, None)) diff --git a/Tests/test_main.py b/Tests/test_main.py new file mode 100644 index 000000000..4f52149b6 --- /dev/null +++ b/Tests/test_main.py @@ -0,0 +1,41 @@ +import os +import subprocess +import sys +import unittest +from unittest import TestCase + +from .helper import is_pypy, is_win32, on_github_actions + + +class TestMain(TestCase): + @unittest.skipIf( + is_win32() and is_pypy() and on_github_actions(), + "Failing on Windows on GitHub Actions running PyPy", + ) + def test_main(self): + out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8") + lines = out.splitlines() + self.assertEqual(lines[0], "-" * 68) + self.assertTrue(lines[1].startswith("Pillow ")) + self.assertTrue(lines[2].startswith("Python ")) + lines = lines[3:] + while lines[0].startswith(" "): + lines = lines[1:] + self.assertEqual(lines[0], "-" * 68) + self.assertTrue(lines[1].startswith("Python modules loaded from ")) + self.assertTrue(lines[2].startswith("Binary modules loaded from ")) + self.assertEqual(lines[3], "-" * 68) + jpeg = ( + os.linesep + + "-" * 68 + + os.linesep + + "JPEG image/jpeg" + + os.linesep + + "Extensions: .jfif, .jpe, .jpeg, .jpg" + + os.linesep + + "Features: open, save" + + os.linesep + + "-" * 68 + + os.linesep + ) + self.assertIn(jpeg, out) diff --git a/Tests/test_map.py b/Tests/test_map.py index 2eeb1fc5f..bdc3a7e2c 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,11 +1,17 @@ -from .helper import PillowTestCase, unittest import sys +import unittest from PIL import Image +from .helper import PillowTestCase, is_win32 -@unittest.skipIf(sys.platform.startswith('win32'), - "Win32 does not call map_buffer") +try: + import numpy +except ImportError: + numpy = None + + +@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 @@ -18,8 +24,15 @@ 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 + + @unittest.skipIf(sys.maxsize <= 2 ** 32, "requires 64-bit system") + @unittest.skipIf(numpy is None, "Numpy is not installed") + def test_ysize(self): + # Should not raise 'Integer overflow in ysize' + arr = numpy.zeros((46341, 46341), dtype=numpy.uint8) + Image.fromarray(arr) diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 80730a312..fcd45b3c1 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,11 +1,11 @@ -from .helper import PillowTestCase, hopper - from PIL import Image +from .helper import PillowTestCase, hopper + class TestModeI16(PillowTestCase): - original = hopper().resize((32, 32)).convert('I') + original = hopper().resize((32, 32)).convert("I") def verify(self, im1): im2 = self.original.copy() @@ -18,9 +18,14 @@ class TestModeI16(PillowTestCase): p1 = pix1[xy] p2 = pix2[xy] self.assertEqual( - p1, p2, - ("got %r from mode %s at %s, expected %r" % - (p1, im1.mode, xy, p2))) + p1, + p2, + ( + "got {!r} from mode {} at {}, expected {!r}".format( + p1, im1.mode, xy, p2 + ) + ), + ) def test_basic(self): # PIL 1.1 has limited support for 16-bit image data. Check that @@ -42,17 +47,17 @@ 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) imOut = Image.new(mode, (w, h), None) - imOut.paste(imIn.crop((0, 0, w//2, h)), (0, 0)) - imOut.paste(imIn.crop((w//2, 0, w, h)), (w//2, 0)) + imOut.paste(imIn.crop((0, 0, w // 2, h)), (0, 0)) + imOut.paste(imIn.crop((w // 2, 0, w, h)), (w // 2, 0)) self.verify(imIn) self.verify(imOut) @@ -83,11 +88,10 @@ class TestModeI16(PillowTestCase): basic("I") def test_tobytes(self): - def tobytes(mode): return Image.new(mode, (1, 1), 1).tobytes() - order = 1 if Image._ENDIAN == '<' else -1 + order = 1 if Image._ENDIAN == "<" else -1 self.assertEqual(tobytes("L"), b"\x01") self.assertEqual(tobytes("I;16"), b"\x01\x00") diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index c9c3b0f1e..7de5c3bbd 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,8 +1,9 @@ -from __future__ import print_function +import unittest -from .helper import PillowTestCase, hopper, unittest from PIL import Image +from .helper import PillowTestCase, hopper + try: import numpy except ImportError: @@ -15,7 +16,6 @@ TEST_IMAGE_SIZE = (10, 10) @unittest.skipIf(numpy is None, "Numpy is not installed") class TestNumpy(PillowTestCase): def test_numpy_to_image(self): - def to_image(dtype, bands=1, boolean=0): if bands == 1: if boolean: @@ -29,7 +29,7 @@ class TestNumpy(PillowTestCase): print("data mismatch for", dtype) else: data = list(range(100)) - a = numpy.array([[x]*bands for x in data], dtype=dtype) + a = numpy.array([[x] * bands for x in data], dtype=dtype) a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands i = Image.fromarray(a) if list(i.getchannel(0).getdata()) != list(range(100)): @@ -37,8 +37,8 @@ class TestNumpy(PillowTestCase): return i # Check supported 1-bit integer formats - self.assert_image(to_image(numpy.bool, 1, 1), '1', TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.bool8, 1, 1), '1', TEST_IMAGE_SIZE) + self.assert_image(to_image(numpy.bool, 1, 1), "1", TEST_IMAGE_SIZE) + self.assert_image(to_image(numpy.bool8, 1, 1), "1", TEST_IMAGE_SIZE) # Check supported 8-bit integer formats self.assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE) @@ -53,7 +53,7 @@ class TestNumpy(PillowTestCase): # self.assert_image(to_image(numpy.int), "I", TEST_IMAGE_SIZE) # Check 16-bit integer formats - if Image._ENDIAN == '<': + if Image._ENDIAN == "<": self.assert_image(to_image(numpy.uint16), "I;16", TEST_IMAGE_SIZE) else: self.assert_image(to_image(numpy.uint16), "I;16B", TEST_IMAGE_SIZE) @@ -96,36 +96,31 @@ class TestNumpy(PillowTestCase): np_size = np.shape[1], np.shape[0] self.assertEqual(img.size, np_size) px = img.load() - for x in range(0, img.size[0], int(img.size[0]/10)): - for y in range(0, img.size[1], int(img.size[1]/10)): + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): self.assert_deep_equal(px[x, y], np[y, x]) def test_16bit(self): - img = Image.open('Tests/images/16bit.cropped.tif') + img = Image.open("Tests/images/16bit.cropped.tif") np_img = numpy.array(img) self._test_img_equals_nparray(img, np_img) - self.assertEqual(np_img.dtype, numpy.dtype('u2'), - ("I;16L", 'u2"), + ("I;16L", "", 0), - (b"\x90\x1F\xA3", 8)) - self.assertEqual(PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3), - (b"\x90\x1F\xA0", 17)) + self.assertEqual(PdfParser.get_value(b"%cmt\n %cmt\n 123\n", 0), (123, 15)) + self.assertEqual(PdfParser.get_value(b"<901FA3>", 0), (b"\x90\x1F\xA3", 8)) + self.assertEqual( + PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3), (b"\x90\x1F\xA0", 17) + ) self.assertEqual(PdfParser.get_value(b"(asd)", 0), (b"asd", 5)) - self.assertEqual(PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0), - (b"asd(qwe)zxc", 13)) - self.assertEqual(PdfParser.get_value(b"(Two \\\nwords.)", 0), - (b"Two words.", 14)) - self.assertEqual(PdfParser.get_value(b"(Two\nlines.)", 0), - (b"Two\nlines.", 12)) - self.assertEqual(PdfParser.get_value(b"(Two\r\nlines.)", 0), - (b"Two\nlines.", 13)) - self.assertEqual(PdfParser.get_value(b"(Two\\nlines.)", 0), - (b"Two\nlines.", 13)) - self.assertEqual(PdfParser.get_value(b"(One\\(paren).", 0), - (b"One(paren", 12)) - self.assertEqual(PdfParser.get_value(b"(One\\)paren).", 0), - (b"One)paren", 12)) + self.assertEqual( + PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0), (b"asd(qwe)zxc", 13) + ) + self.assertEqual( + PdfParser.get_value(b"(Two \\\nwords.)", 0), (b"Two words.", 14) + ) + self.assertEqual(PdfParser.get_value(b"(Two\nlines.)", 0), (b"Two\nlines.", 12)) + self.assertEqual( + PdfParser.get_value(b"(Two\r\nlines.)", 0), (b"Two\nlines.", 13) + ) + self.assertEqual( + PdfParser.get_value(b"(Two\\nlines.)", 0), (b"Two\nlines.", 13) + ) + self.assertEqual(PdfParser.get_value(b"(One\\(paren).", 0), (b"One(paren", 12)) + self.assertEqual(PdfParser.get_value(b"(One\\)paren).", 0), (b"One)paren", 12)) self.assertEqual(PdfParser.get_value(b"(\\0053)", 0), (b"\x053", 7)) self.assertEqual(PdfParser.get_value(b"(\\053)", 0), (b"\x2B", 6)) self.assertEqual(PdfParser.get_value(b"(\\53)", 0), (b"\x2B", 5)) @@ -88,37 +98,43 @@ class TestPdfParser(PillowTestCase): b"D:2018072921": "20180729210000", b"D:20180729214124Z": "20180729214124", b"D:20180729214124+08'00'": "20180729134124", - b"D:20180729214124-05'00'": "20180730024124" + b"D:20180729214124-05'00'": "20180730024124", }.items(): d = PdfParser.get_value( - b"<>", 0)[0] - self.assertEqual( - time.strftime("%Y%m%d%H%M%S", getattr(d, name)), value) + b"<>", 0 + )[0] + self.assertEqual(time.strftime("%Y%m%d%H%M%S", getattr(d, name)), value) def test_pdf_repr(self): self.assertEqual(bytes(IndirectReference(1, 2)), b"1 2 R") - self.assertEqual(bytes(IndirectObjectDef(*IndirectReference(1, 2))), - b"1 2 obj") + self.assertEqual(bytes(IndirectObjectDef(*IndirectReference(1, 2))), b"1 2 obj") self.assertEqual(bytes(PdfName(b"Name#Hash")), b"/Name#23Hash") self.assertEqual(bytes(PdfName("Name#Hash")), b"/Name#23Hash") - self.assertEqual(bytes(PdfDict({b"Name": IndirectReference(1, 2)})), - b"<<\n/Name 1 2 R\n>>") - self.assertEqual(bytes(PdfDict({"Name": IndirectReference(1, 2)})), - b"<<\n/Name 1 2 R\n>>") + self.assertEqual( + bytes(PdfDict({b"Name": IndirectReference(1, 2)})), b"<<\n/Name 1 2 R\n>>" + ) + self.assertEqual( + bytes(PdfDict({"Name": IndirectReference(1, 2)})), b"<<\n/Name 1 2 R\n>>" + ) self.assertEqual(pdf_repr(IndirectReference(1, 2)), b"1 2 R") - self.assertEqual(pdf_repr(IndirectObjectDef(*IndirectReference(1, 2))), - b"1 2 obj") + self.assertEqual( + pdf_repr(IndirectObjectDef(*IndirectReference(1, 2))), b"1 2 obj" + ) self.assertEqual(pdf_repr(PdfName(b"Name#Hash")), b"/Name#23Hash") self.assertEqual(pdf_repr(PdfName("Name#Hash")), b"/Name#23Hash") - self.assertEqual(pdf_repr(PdfDict({b"Name": IndirectReference(1, 2)})), - b"<<\n/Name 1 2 R\n>>") - self.assertEqual(pdf_repr(PdfDict({"Name": IndirectReference(1, 2)})), - b"<<\n/Name 1 2 R\n>>") + self.assertEqual( + pdf_repr(PdfDict({b"Name": IndirectReference(1, 2)})), + b"<<\n/Name 1 2 R\n>>", + ) + self.assertEqual( + pdf_repr(PdfDict({"Name": IndirectReference(1, 2)})), b"<<\n/Name 1 2 R\n>>" + ) self.assertEqual(pdf_repr(123), b"123") self.assertEqual(pdf_repr(True), b"true") self.assertEqual(pdf_repr(False), b"false") self.assertEqual(pdf_repr(None), b"null") self.assertEqual(pdf_repr(b"a)/b\\(c"), br"(a\)/b\\\(c)") - self.assertEqual(pdf_repr([123, True, {"a": PdfName(b"b")}]), - b"[ 123 true <<\n/a /b\n>> ]") + self.assertEqual( + pdf_repr([123, True, {"a": PdfName(b"b")}]), b"[ 123 true <<\n/a /b\n>> ]" + ) self.assertEqual(pdf_repr(PdfBinary(b"\x90\x1F\xA0")), b"<901FA0>") diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 46958c085..42f47f169 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,28 +1,28 @@ -from .helper import PillowTestCase - from PIL import Image +from .helper import PillowTestCase + class TestPickle(PillowTestCase): - def helper_pickle_file(self, pickle, protocol=0, mode=None): # Arrange - im = Image.open('Tests/images/hopper.jpg') - filename = self.tempfile('temp.pkl') + im = Image.open("Tests/images/hopper.jpg") + filename = self.tempfile("temp.pkl") if mode: im = im.convert(mode) # Act - with open(filename, 'wb') as f: + with open(filename, "wb") as f: pickle.dump(im, f, protocol) - with open(filename, 'rb') as f: + with open(filename, "rb") as f: loaded_im = pickle.load(f) # Assert self.assertEqual(im, loaded_im) - def helper_pickle_string(self, pickle, protocol=0, - test_file='Tests/images/hopper.jpg', mode=None): + def helper_pickle_string( + self, pickle, protocol=0, test_file="Tests/images/hopper.jpg", mode=None + ): im = Image.open(test_file) if mode: im = im.convert(mode) @@ -61,15 +61,28 @@ class TestPickle(PillowTestCase): # Act / Assert for test_file in [ - "Tests/images/test-card.png", - "Tests/images/zero_bb.png", - "Tests/images/zero_bb_scale2.png", - "Tests/images/non_zero_bb.png", - "Tests/images/non_zero_bb_scale2.png", - "Tests/images/p_trns_single.png", - "Tests/images/pil123p.png" + "Tests/images/test-card.png", + "Tests/images/zero_bb.png", + "Tests/images/zero_bb_scale2.png", + "Tests/images/non_zero_bb.png", + "Tests/images/non_zero_bb_scale2.png", + "Tests/images/p_trns_single.png", + "Tests/images/pil123p.png", + "Tests/images/itxt_chunks.png", ]: - self.helper_pickle_string(pickle, test_file=test_file) + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + self.helper_pickle_string( + pickle, protocol=protocol, test_file=test_file + ) + + def test_pickle_pa_mode(self): + # Arrange + import pickle + + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + self.helper_pickle_string(pickle, protocol, mode="PA") + self.helper_pickle_file(pickle, protocol, mode="PA") def test_pickle_l_mode(self): # Arrange @@ -80,6 +93,25 @@ class TestPickle(PillowTestCase): self.helper_pickle_string(pickle, protocol, mode="L") self.helper_pickle_file(pickle, protocol, mode="L") + def test_pickle_la_mode_with_palette(self): + # Arrange + import pickle + + im = Image.open("Tests/images/hopper.jpg") + filename = self.tempfile("temp.pkl") + im = im.convert("PA") + + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + im.mode = "LA" + with open(filename, "wb") as f: + pickle.dump(im, f, protocol) + with open(filename, "rb") as f: + loaded_im = pickle.load(f) + + im.mode = "PA" + self.assertEqual(im, loaded_im) + def test_cpickle_l_mode(self): # Arrange try: diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 73ef5e2b2..0ef9acb06 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -1,22 +1,23 @@ -from .helper import PillowTestCase - -from PIL import Image, PSDraw import os import sys +from io import StringIO + +from PIL import Image, PSDraw + +from .helper import PillowTestCase class TestPsDraw(PillowTestCase): - def _create_document(self, ps): im = Image.open("Tests/images/hopper.ppm") title = "hopper" - box = (1*72, 2*72, 7*72, 10*72) # in points + box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points ps.begin_document(title) # draw diagonal lines in a cross - ps.line((1*72, 2*72), (7*72, 10*72)) - ps.line((7*72, 2*72), (1*72, 10*72)) + ps.line((1 * 72, 2 * 72), (7 * 72, 10 * 72)) + ps.line((7 * 72, 2 * 72), (1 * 72, 10 * 72)) # draw the image (75 dpi) ps.image(box, im, 75) @@ -24,7 +25,7 @@ class TestPsDraw(PillowTestCase): # draw title ps.setfont("Courier", 36) - ps.text((3*72, 4*72), title) + ps.text((3 * 72, 4 * 72), title) ps.end_document() @@ -34,7 +35,7 @@ class TestPsDraw(PillowTestCase): # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript # Arrange - tempfile = self.tempfile('temp.ps') + tempfile = self.tempfile("temp.ps") with open(tempfile, "wb") as fp: # Act ps = PSDraw.PSDraw(fp) @@ -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/Tests/test_pyroma.py b/Tests/test_pyroma.py index 3c4e3d9a2..accad5451 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,7 +1,9 @@ -from .helper import unittest, PillowTestCase +import unittest from PIL import __version__ +from .helper import PillowTestCase + try: import pyroma except ImportError: @@ -10,7 +12,6 @@ except ImportError: @unittest.skipIf(pyroma is None, "Pyroma is not installed") class TestPyroma(PillowTestCase): - def test_pyroma(self): # Arrange data = pyroma.projectdata.get_data(".") @@ -19,11 +20,12 @@ class TestPyroma(PillowTestCase): rating = pyroma.ratings.rate(data) # Assert - if 'rc' in __version__: - # Pyroma needs to chill about RC versions - # and not kill all our tests. - self.assertEqual(rating, (9, [ - "The package's version number does not comply with PEP-386."])) + if "rc" in __version__: + # Pyroma needs to chill about RC versions and not kill all our tests. + self.assertEqual( + rating, + (9, ["The package's version number does not comply with PEP-386."]), + ) else: # Should have a perfect score diff --git a/Tests/test_qt_image_fromqpixmap.py b/Tests/test_qt_image_fromqpixmap.py index 358f1573d..1cff26d88 100644 --- a/Tests/test_qt_image_fromqpixmap.py +++ b/Tests/test_qt_image_fromqpixmap.py @@ -1,27 +1,15 @@ +from PIL import ImageQt + from .helper import PillowTestCase, hopper from .test_imageqt import PillowQPixmapTestCase -from PIL import ImageQt - class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase): - def roundtrip(self, expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - self.assert_image_equal(result, expected.convert('RGB')) + self.assert_image_equal(result, expected.convert("RGB")) - def test_sanity_1(self): - self.roundtrip(hopper('1')) - - def test_sanity_rgb(self): - self.roundtrip(hopper('RGB')) - - def test_sanity_rgba(self): - self.roundtrip(hopper('RGBA')) - - def test_sanity_l(self): - self.roundtrip(hopper('L')) - - def test_sanity_p(self): - self.roundtrip(hopper('P')) + def test_sanity(self): + for mode in ("1", "RGB", "RGBA", "L", "P"): + self.roundtrip(hopper(mode)) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index c1aa64b57..39d60d197 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,39 +1,22 @@ +from PIL import Image, ImageQt + from .helper import PillowTestCase, hopper from .test_imageqt import PillowQtTestCase -from PIL import ImageQt, Image - - if ImageQt.qt_is_installed: from PIL.ImageQt import QImage 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): - def test_sanity(self): - for mode in ('RGB', 'RGBA', 'L', 'P', '1'): + for mode in ("RGB", "RGBA", "L", "P", "1"): src = hopper(mode) data = ImageQt.toqimage(src) @@ -42,12 +25,12 @@ class TestToQImage(PillowQtTestCase, PillowTestCase): # reload directly from the qimage rt = ImageQt.fromqimage(data) - if mode in ('L', 'P', '1'): - self.assert_image_equal(rt, src.convert('RGB')) + if mode in ("L", "P", "1"): + self.assert_image_equal(rt, src.convert("RGB")) else: self.assert_image_equal(rt, src) - if mode == '1': + if mode == "1": # BW appears to not save correctly on QT4 and QT5 # kicks out errors on console: # libpng warning: Invalid color type/bit depth combination @@ -56,15 +39,11 @@ class TestToQImage(PillowQtTestCase, PillowTestCase): continue # Test saving the file - tempfile = self.tempfile('temp_{}.png'.format(mode)) + tempfile = self.tempfile("temp_{}.png".format(mode)) data.save(tempfile) # Check that it actually worked. reloaded = Image.open(tempfile) - # Gray images appear to come back in palette mode. - # They're roughly equivalent - if QT_VERSION == 4 and mode == 'L': - src = src.convert('P') self.assert_image_equal(reloaded, src) def test_segfault(self): @@ -75,10 +54,10 @@ class TestToQImage(PillowQtTestCase, PillowTestCase): if ImageQt.qt_is_installed: - class Example(QWidget): + class Example(QWidget): def __init__(self): - super(Example, self).__init__() + super().__init__() img = hopper().resize((1000, 1000)) diff --git a/Tests/test_qt_image_toqpixmap.py b/Tests/test_qt_image_toqpixmap.py index 9bb7183b7..2c07f1bf5 100644 --- a/Tests/test_qt_image_toqpixmap.py +++ b/Tests/test_qt_image_toqpixmap.py @@ -1,21 +1,20 @@ +from PIL import ImageQt + from .helper import PillowTestCase, hopper from .test_imageqt import PillowQPixmapTestCase -from PIL import ImageQt - if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase): - def test_sanity(self): - for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): + for mode in ("1", "RGB", "RGBA", "L", "P"): data = ImageQt.toqpixmap(hopper(mode)) self.assertIsInstance(data, QPixmap) self.assertFalse(data.isNull()) # Test saving the file - tempfile = self.tempfile('temp_{}.png'.format(mode)) + tempfile = self.tempfile("temp_{}.png".format(mode)) data.save(tempfile) diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 77fd67f01..3e74647eb 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,32 +1,31 @@ -from .helper import unittest, PillowTestCase -from .helper import djpeg_available, cjpeg_available, netpbm_available - -import sys import shutil -from PIL import Image, JpegImagePlugin, GifImagePlugin +from PIL import GifImagePlugin, Image, JpegImagePlugin + +from .helper import ( + PillowTestCase, + cjpeg_available, + djpeg_available, + is_win32, + netpbm_available, + unittest, +) TEST_JPG = "Tests/images/hopper.jpg" TEST_GIF = "Tests/images/hopper.gif" -test_filenames = ( - "temp_';", - "temp_\";", - "temp_'\"|", - "temp_'\"||", - "temp_'\"&&", -) +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: 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): @@ -34,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): @@ -44,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 fae4d7ed6..dedbbfe6d 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,13 +1,12 @@ -from .helper import PillowTestCase, hopper +from fractions import Fraction -from PIL import TiffImagePlugin, Image +from PIL import Image, TiffImagePlugin from PIL.TiffImagePlugin import IFDRational -from fractions import Fraction +from .helper import PillowTestCase, hopper class Test_IFDRational(PillowTestCase): - def _test_equal(self, num, denom, target): t = IFDRational(num, denom) @@ -44,17 +43,18 @@ class Test_IFDRational(PillowTestCase): def test_ifd_rational_save(self): methods = (True, False) - if 'libtiff_encoder' not in dir(Image.core): + if "libtiff_encoder" not in dir(Image.core): methods = (False,) for libtiff in methods: TiffImagePlugin.WRITE_LIBTIFF = libtiff im = hopper() - out = self.tempfile('temp.tiff') + out = self.tempfile("temp.tiff") res = IFDRational(301, 1) - im.save(out, dpi=(res, res), compression='raw') + 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/Tests/test_uploader.py b/Tests/test_uploader.py index e40e7fb86..46dbd824a 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -3,11 +3,11 @@ from .helper import PillowTestCase, hopper class TestUploader(PillowTestCase): def check_upload_equal(self): - result = hopper('P').convert('RGB') - target = hopper('RGB') + result = hopper("P").convert("RGB") + target = hopper("RGB") self.assert_image_equal(result, target) def check_upload_similar(self): - result = hopper('P').convert('RGB') - target = hopper('RGB') + result = hopper("P").convert("RGB") + target = hopper("RGB") self.assert_image_similar(result, target, 0) diff --git a/Tests/test_util.py b/Tests/test_util.py index 4471b75bd..6a111b5b9 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,30 +1,11 @@ -from .helper import PillowTestCase +import unittest from PIL import _util +from .helper import PillowTestCase + 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" @@ -35,10 +16,23 @@ class TestUtil(PillowTestCase): # Assert self.assertTrue(it_is) + @unittest.skipIf(not _util.py36, "os.path support for Paths added in 3.6") + def test_path_obj_is_path(self): + # Arrange + from pathlib import Path + + test_path = Path("filename.ext") + + # Act + it_is = _util.isPath(test_path) + + # Assert + self.assertTrue(it_is) + def test_is_not_path(self): # Arrange filename = self.tempfile("temp.ext") - fp = open(filename, 'w').close() + fp = open(filename, "w").close() # Act it_is_not = _util.isPath(fp) diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 03befd507..713fc161e 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -1,18 +1,21 @@ -from .helper import unittest, PillowLeakTestCase -from PIL import Image, features +import unittest from io import BytesIO +from PIL import Image, features + +from .helper import PillowLeakTestCase + test_file = "Tests/images/hopper.webp" -@unittest.skipUnless(features.check('webp'), "WebP is not installed") +@unittest.skipUnless(features.check("webp"), "WebP is not installed") class TestWebPLeaks(PillowLeakTestCase): mem_limit = 3 * 1024 # kb iterations = 100 def test_leak_load(self): - with open(test_file, 'rb') as f: + with open(test_file, "rb") as f: im_data = f.read() def core(): diff --git a/Tests/threaded_save.py b/Tests/threaded_save.py index d8faffa0c..d47c2cf99 100644 --- a/Tests/threaded_save.py +++ b/Tests/threaded_save.py @@ -1,12 +1,11 @@ -from __future__ import print_function -from PIL import Image - 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") diff --git a/Tests/versions.py b/Tests/versions.py index 835865b37..d6433b063 100644 --- a/Tests/versions.py +++ b/Tests/versions.py @@ -1,4 +1,3 @@ -from __future__ import print_function from PIL import Image diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 000000000..dd77f3278 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,72 @@ +# Python package +# Create and test a Python package on multiple Python versions. +# Add steps that analyze code, save the dist with the build record, +# publish to a PyPI-compatible index, and more: +# https://docs.microsoft.com/azure/devops/pipelines/ecosystems/python + +jobs: + +- template: .azure-pipelines/jobs/lint.yml + parameters: + name: Lint + vmImage: 'Ubuntu-16.04' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'alpine' + name: 'alpine' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'arch' + name: 'arch' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'ubuntu-16.04-xenial-amd64' + name: 'ubuntu_16_04_xenial_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'ubuntu-18.04-bionic-amd64' + name: 'ubuntu_18_04_bionic_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'debian-9-stretch-x86' + name: 'debian_9_stretch_x86' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'debian-10-buster-x86' + name: 'debian_10_buster_x86' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'centos-6-amd64' + name: 'centos_6_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'centos-7-amd64' + name: 'centos_7_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'amazon-1-amd64' + name: 'amazon_1_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'amazon-2-amd64' + name: 'amazon_2_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'fedora-30-amd64' + name: 'fedora_30_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'fedora-31-amd64' + name: 'fedora_31_amd64' diff --git a/depends/alpine_Dockerfile b/depends/alpine_Dockerfile deleted file mode 100644 index 69bdf84f6..000000000 --- a/depends/alpine_Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -# This is a sample Dockerfile to build Pillow on Alpine Linux -# with all/most of the dependencies working. -# -# Tcl/Tk isn't detecting -# Freetype has different metrics so tests are failing. -# sudo and bash are required for the webp build script. - -FROM alpine -USER root - -RUN apk --no-cache add python \ - build-base \ - python-dev \ - py-pip \ - # Pillow dependencies - jpeg-dev \ - zlib-dev \ - freetype-dev \ - lcms2-dev \ - openjpeg-dev \ - tiff-dev \ - tk-dev \ - tcl-dev - -# install from pip, without webp -#RUN LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "pip install Pillow" - -# install from git, run tests, including webp -RUN apk --no-cache add git \ - bash \ - sudo - -RUN git clone https://github.com/python-pillow/Pillow.git /Pillow -RUN pip install virtualenv && virtualenv /vpy && source /vpy/bin/activate && pip install nose - -RUN echo "#!/bin/bash" >> /test && \ - echo "source /vpy/bin/activate && cd /Pillow " >> test && \ - echo "pushd depends && ./install_webp.sh && ./install_imagequant.sh && popd" >> test && \ - echo "LIBRARY_PATH=/lib:/usr/lib make install && make test" >> test - -RUN chmod +x /test - -CMD ["/test"] diff --git a/depends/debian_8.2.sh b/depends/debian_8.2.sh deleted file mode 100755 index c4f72bf8e..000000000 --- a/depends/debian_8.2.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Debian 8.2 -# for both system Pythons 2.7 and 3.4 -# -# Also works for Raspbian Jessie -# - -sudo apt-get -y install python-dev python-setuptools \ - python3-dev python-virtualenv cmake -sudo apt-get -y install libtiff5-dev libjpeg62-turbo-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ - python-tk python3-tk libharfbuzz-dev libfribidi-dev - -./install_openjpeg.sh -./install_imagequant.sh -./install_raqm.sh diff --git a/depends/fedora_23.sh b/depends/fedora_23.sh deleted file mode 100755 index 5bdcf7f17..000000000 --- a/depends/fedora_23.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Fedora 23 -# for both system Pythons 2.7 and 3.4 -# -# note that Fedora does ship packages for Pillow as python-pillow - -# this is a workaround for -# "gcc: error: /usr/lib/rpm/redhat/redhat-hardened-cc1: No such file or directory" -# errors when compiling. -sudo dnf install redhat-rpm-config - -sudo dnf install python-devel python3-devel python-virtualenv make gcc - -sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ - lcms2-devel libwebp-devel openjpeg2-devel tkinter python3-tkinter \ - tcl-devel tk-devel harfbuzz-devel fribidi-devel libraqm-devel \ No newline at end of file diff --git a/depends/freebsd_10.sh b/depends/freebsd_10.sh deleted file mode 100755 index 36d9c1069..000000000 --- a/depends/freebsd_10.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Freebsd 10.x -# for both system Pythons 2.7 and 3.4 -# -sudo pkg install python2 python3 py27-pip py27-virtualenv wget cmake - -# Openjpeg fails badly using the openjpeg package. -# I can't find a python3.4 version of tkinter -sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 harfbuzz fribidi py27-tkinter - -./install_raqm_cmake.sh diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index e3c091754..0e1364b2a 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,21 +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 - -rm -r test_images +svn_export || svn_export retry || svn_export retry || svn_export retry \ No newline at end of file diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index e284bb0e4..0120dbc0b 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-2.12.2 +archive=libimagequant-2.12.5 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index ed7f0d6b5..a93498282 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -1,7 +1,7 @@ #!/bin/bash # install openjpeg -archive=openjpeg-2.3.0 +archive=openjpeg-2.3.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 655e841d9..38a5bfd52 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,7 +2,7 @@ # install raqm -archive=raqm-0.5.0 +archive=raqm-0.7.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz diff --git a/depends/install_raqm_cmake.sh b/depends/install_raqm_cmake.sh index 0c5ed8b69..c0dcd93b7 100755 --- a/depends/install_raqm_cmake.sh +++ b/depends/install_raqm_cmake.sh @@ -2,7 +2,7 @@ # install raqm -archive=raqm-cmake-b517ba80 +archive=raqm-cmake-99300ff3 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz diff --git a/depends/install_webp.sh b/depends/install_webp.sh index ead5637ee..7ccd09301 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.0.2 +archive=libwebp-1.0.3 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz 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/Guardfile b/docs/Guardfile index f8f3051ed..16f731611 100755 --- a/docs/Guardfile +++ b/docs/Guardfile @@ -1,10 +1,8 @@ #!/usr/bin/env python -from livereload.task import Task from livereload.compiler import shell +from livereload.task import Task Task.add('*.rst', shell('make html')) Task.add('*/*.rst', shell('make html')) -Task.add('_static/*.css', shell('make clean html')) -Task.add('_templates/*', shell('make clean html')) Task.add('Makefile', shell('make html')) Task.add('conf.py', shell('make html')) diff --git a/docs/_static/.gitignore b/docs/_static/.gitignore deleted file mode 100644 index b1f9a2ade..000000000 --- a/docs/_static/.gitignore +++ /dev/null @@ -1 +0,0 @@ -# Empty file, to make the directory available in the repository diff --git a/docs/_templates/.gitignore b/docs/_templates/.gitignore deleted file mode 100644 index b1f9a2ade..000000000 --- a/docs/_templates/.gitignore +++ /dev/null @@ -1 +0,0 @@ -# Empty file, to make the directory available in the repository diff --git a/docs/_templates/sidebarhelp.html b/docs/_templates/sidebarhelp.html deleted file mode 100644 index 8a33f18dc..000000000 --- a/docs/_templates/sidebarhelp.html +++ /dev/null @@ -1,4 +0,0 @@ -

Need help?

-

- You can get help via IRC at irc://irc.freenode.net#pil, Gitter or Stack Overflow. Please report issues on GitHub. -

diff --git a/docs/about.rst b/docs/about.rst index 323593a36..ce6537e14 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -6,12 +6,13 @@ Goals The fork author's goal is to foster and support active development of PIL through: -- Continuous integration testing via `Travis CI`_ and `AppVeyor`_ +- Continuous integration testing via `Travis CI`_, `AppVeyor`_ and `GitHub Actions`_ - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ .. _Travis CI: https://travis-ci.org/python-pillow/Pillow .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow +.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions .. _GitHub: https://github.com/python-pillow/Pillow .. _Python Package Index: https://pypi.org/project/Pillow/ diff --git a/docs/conf.py b/docs/conf.py index 2c25588a0..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. @@ -17,9 +16,8 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) -import sphinx_rtd_theme - import PIL +import sphinx_rtd_theme # -- General configuration ------------------------------------------------ @@ -29,27 +27,23 @@ import PIL # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.intersphinx"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +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 @@ -75,7 +69,7 @@ language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -93,7 +87,7 @@ exclude_patterns = ['_build'] # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -140,7 +134,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static', 'resources'] +html_static_path = ["resources"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied @@ -203,20 +197,17 @@ html_static_path = ['_static', 'resources'] # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'PillowPILForkdoc' +htmlhelp_basename = "PillowPILForkdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # 'preamble': '', - # Latex figure (float) alignment # 'figure_align': 'htbp', } @@ -225,8 +216,13 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'PillowPILFork.tex', u'Pillow (PIL Fork) Documentation', - u'Alex Clark', 'manual'), + ( + master_doc, + "PillowPILFork.tex", + "Pillow (PIL Fork) Documentation", + "Alex Clark", + "manual", + ) ] # The name of an image file (relative to this directory) to place at the top of @@ -255,8 +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. @@ -269,10 +264,15 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'PillowPILFork', u'Pillow (PIL Fork) Documentation', - author, 'PillowPILFork', - 'Pillow is the friendly PIL fork by Alex Clark and Contributors.', - 'Miscellaneous'), + ( + master_doc, + "PillowPILFork", + "Pillow (PIL Fork) Documentation", + author, + "PillowPILFork", + "Pillow is the friendly PIL fork by Alex Clark and Contributors.", + "Miscellaneous", + ) ] # Documents to append as an appendix to all manuals. @@ -289,4 +289,4 @@ texinfo_documents = [ def setup(app): - app.add_javascript('js/script.js') + app.add_javascript("js/script.js") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index b8131ac05..a9b534dca 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,16 +12,82 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -PIL.*ImagePlugin.__version__ attributes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Python 2.7 +~~~~~~~~~~ .. 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. +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. + +ImageCms.CmsProfile attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 3.2.0 + +Some attributes in ``ImageCms.CmsProfile`` are deprecated. From 6.0.0, they issue a +``DeprecationWarning``: + +======================== =============================== +Deprecated Use instead +======================== =============================== +``color_space`` Padded ``xcolor_space`` +``pcs`` Padded ``connection_space`` +``product_copyright`` Unicode ``copyright`` +``product_desc`` Unicode ``profile_description`` +``product_description`` Unicode ``profile_description`` +``product_manufacturer`` Unicode ``manufacturer`` +``product_model`` Unicode ``model`` +======================== =============================== + +Removed features +---------------- + +Deprecated features are only removed in major releases after an appropriate +period of deprecation has passed. + +Image.__del__ +~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. +Use a context manager or call ``Image.close()`` instead to close the file in a +deterministic way. + +Previous method: + +.. code-block:: python + + im = Image.open("hopper.png") + im.save("out.jpg") + +Use instead: + +.. code-block:: python + + with Image.open("hopper.png") as im: + im.save("out.jpg") + +PILLOW_VERSION constant +~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +PIL.*ImagePlugin.__version__ attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +The version constants of individual plugins have been removed. Use ``PIL.__version__`` +instead. =============================== ================================= ================================== -Deprecated Deprecated Deprecated +Removed Removed Removed =============================== ================================= ================================== ``BmpImagePlugin.__version__`` ``Jpeg2KImagePlugin.__version__`` ``PngImagePlugin.__version__`` ``CurImagePlugin.__version__`` ``JpegImagePlugin.__version__`` ``PpmImagePlugin.__version__`` @@ -37,33 +103,32 @@ Deprecated Deprecated Deprecated ``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` =============================== ================================= ================================== +PyQt4 and PySide +~~~~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since +2018-08-31 and PySide since 2015-10-14. + +Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 +or PySide2. + Setting the size of TIFF images ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 5.3.0 +*Removed in version 7.0.0.* -Setting the image size of a TIFF image (eg. ``im.size = (256, 256)``) issues -a ``DeprecationWarning``: +Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws +an error. Use ``Image.resize`` instead. -.. code-block:: none +VERSION constant +~~~~~~~~~~~~~~~~ - Setting the size of a TIFF image directly is deprecated, and will - be removed in a future version. Use the resize method instead. +*Removed in version 6.0.0.* -PILLOW_VERSION and VERSION constants -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.2.0 - -Two version constants – ``VERSION`` (the old PIL version, always 1.1.7) and -``PILLOW_VERSION`` – have been deprecated and will be removed in the next -major release. Use ``__version__`` instead. - -Removed features ----------------- - -Deprecated features are only removed in major releases after an appropriate -period of deprecation has passed. +``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use +``__version__`` instead. Undocumented ImageOps functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 631dc2d61..45f63f164 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -12,8 +12,8 @@ Full text of the CC0 license: import struct from io import BytesIO -from PIL import Image, ImageFile +from PIL import Image, ImageFile # Magic ("DDS ") DDS_MAGIC = 0x20534444 @@ -61,8 +61,7 @@ DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS DDS_ALPHA = DDPF_ALPHA DDS_PAL8 = DDPF_PALETTEINDEXED8 -DDS_HEADER_FLAGS_TEXTURE = (DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | - DDSD_PIXELFORMAT) +DDS_HEADER_FLAGS_TEXTURE = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH DDS_HEADER_FLAGS_PITCH = DDSD_PITCH @@ -94,9 +93,9 @@ DXT5_FOURCC = 0x35545844 def _decode565(bits): - a = ((bits >> 11) & 0x1f) << 3 - b = ((bits >> 5) & 0x3f) << 2 - c = (bits & 0x1f) << 3 + a = ((bits >> 11) & 0x1F) << 3 + b = ((bits >> 5) & 0x3F) << 2 + c = (bits & 0x1F) << 3 return a, b, c @@ -145,7 +144,7 @@ def _dxt1(data, width, height): r, g, b = 0, 0, 0 idx = 4 * ((y + j) * width + x + i) - ret[idx:idx+4] = struct.pack('4B', r, g, b, 255) + ret[idx : idx + 4] = struct.pack("4B", r, g, b, 255) return bytes(ret) @@ -167,7 +166,7 @@ def _dxtc_alpha(a0, a1, ac0, ac1, ai): elif ac == 6: alpha = 0 elif ac == 7: - alpha = 0xff + alpha = 0xFF else: alpha = ((6 - ac) * a0 + (ac - 1) * a1) // 5 @@ -180,8 +179,7 @@ def _dxt5(data, width, height): for y in range(0, height, 4): for x in range(0, width, 4): - a0, a1, ac0, ac1, c0, c1, code = struct.unpack("<2BHI2HI", - data.read(16)) + a0, a1, ac0, ac1, c0, c1, code = struct.unpack("<2BHI2HI", data.read(16)) r0, g0, b0 = _decode565(c0) r1, g1, b1 = _decode565(c1) @@ -202,7 +200,7 @@ def _dxt5(data, width, height): r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1) idx = 4 * ((y + j) * width + x + i) - ret[idx:idx+4] = struct.pack('4B', r, g, b, alpha) + ret[idx : idx + 4] = struct.pack("4B", r, g, b, alpha) return bytes(ret) @@ -214,10 +212,10 @@ class DdsImageFile(ImageFile.ImageFile): def _open(self): magic, header_size = struct.unpack("` that can be used with PythonWin and other Windows-based toolkits. Many other GUI toolkits come with some kind of PIL support. -For debugging, there’s also a :py:meth:`show` method which saves an image to +For debugging, there’s also a :py:meth:`~PIL.Image.Image.show` method which saves an image to disk, and calls an external display utility. Image Processing diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 13a7deba6..63030de21 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 @@ -99,9 +96,9 @@ Create JPEG thumbnails outfile = os.path.splitext(infile)[0] + ".thumbnail" if infile != outfile: try: - im = Image.open(infile) - im.thumbnail(size) - im.save(outfile, "JPEG") + with Image.open(infile) as im: + im.thumbnail(size) + im.save(outfile, "JPEG") except IOError: print("cannot create thumbnail for", infile) @@ -120,7 +117,6 @@ Identify Image Files :: - from __future__ import print_function import sys from PIL import Image @@ -247,7 +243,7 @@ Transposing an image out = im.transpose(Image.ROTATE_270) ``transpose(ROTATE)`` operations can also be performed identically with -:py:meth:`~PIL.Image.Image.rotate` operations, provided the `expand` flag is +:py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is true, to provide for the same changes to the image's size. A more general form of image transformations can be carried out via the @@ -267,7 +263,8 @@ Converting between modes :: from PIL import Image - im = Image.open("hopper.ppm").convert("L") + with Image.open("hopper.ppm") as im: + im = im.convert("L") The library supports transformations between each supported mode and the “L” and “RGB” modes. To convert between other modes, you may have to use an @@ -383,23 +380,19 @@ 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. -Note that most drivers in the current version of the library only allow you to -seek to the next frame (as in the above example). To rewind the file, you may -have to reopen it. - The following class lets you use the for-statement to loop over the sequence: Using the ImageSequence Iterator class @@ -426,32 +419,34 @@ Drawing Postscript from PIL import Image from PIL import PSDraw - im = Image.open("hopper.ppm") - title = "hopper" - box = (1*72, 2*72, 7*72, 10*72) # in points + with Image.open("hopper.ppm") as im: + title = "hopper" + box = (1*72, 2*72, 7*72, 10*72) # in points - ps = PSDraw.PSDraw() # default is sys.stdout - ps.begin_document(title) + ps = PSDraw.PSDraw() # default is sys.stdout + ps.begin_document(title) - # draw the image (75 dpi) - ps.image(box, im, 75) - ps.rectangle(box) + # draw the image (75 dpi) + ps.image(box, im, 75) + ps.rectangle(box) - # draw title - ps.setfont("HelveticaNarrow-Bold", 36) - ps.text((3*72, 4*72), title) + # draw title + ps.setfont("HelveticaNarrow-Bold", 36) + ps.text((3*72, 4*72), title) - ps.end_document() + ps.end_document() More on reading images ---------------------- As described earlier, the :py:func:`~PIL.Image.open` function of the :py:mod:`~PIL.Image` module is used to open an image file. In most cases, you -simply pass it the filename as an argument:: +simply pass it the filename as an argument. ``Image.open()`` can be used a +context manager:: from PIL import Image - im = Image.open("hopper.ppm") + with Image.open("hopper.ppm") as im: + ... If everything goes well, the result is an :py:class:`PIL.Image.Image` object. Otherwise, an :exc:`IOError` exception is raised. @@ -477,8 +472,8 @@ Reading from a string :: + from PIL import Image import StringIO - im = Image.open(StringIO.StringIO(buffer)) Note that the library rewinds the file (using ``seek(0)``) before reading the @@ -517,12 +512,12 @@ This is only available for JPEG and MPO files. :: from PIL import Image - from __future__ import print_function - im = Image.open(file) - print("original =", im.mode, im.size) - im.draft("L", (100, 100)) - print("draft =", im.mode, im.size) + with Image.open(file) as im: + print("original =", im.mode, im.size) + + im.draft("L", (100, 100)) + print("draft =", im.mode, im.size) This prints something like:: diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst index 2e68656ca..0763109ab 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-file-decoder.rst @@ -201,7 +201,8 @@ table describes some commonly used **raw modes**: +-----------+-----------------------------------------------------------------+ | ``BGR`` | 24-bit true colour, stored as (blue, green, red). | +-----------+-----------------------------------------------------------------+ -| ``RGBX`` | 24-bit true colour, stored as (red, green, blue, pad). | +| ``RGBX`` | 24-bit true colour, stored as (red, green, blue, pad). The pad | +| | pixels may vary. | +-----------+-----------------------------------------------------------------+ | ``RGB;L`` | 24-bit true colour, line interleaved (first all red pixels, then| | | all green pixels, finally all blue pixels). | @@ -255,9 +256,10 @@ If the raw decoder cannot handle your format, PIL also provides a special “bit decoder that can be used to read various packed formats into a floating point image memory. -To use the bit decoder with the frombytes function, use the following syntax:: +To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use +the following syntax:: - image = frombytes( + image = Image.frombytes( mode, size, data, "bit", bits, pad, fill, sign, orientation ) diff --git a/docs/index.rst b/docs/index.rst index b300dc16d..034da6eed 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,6 +3,8 @@ Pillow Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. + .. image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow @@ -30,8 +32,8 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors = 6.0.0 | | | | Yes | | | | 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 ------------------ @@ -121,10 +125,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: @@ -149,7 +152,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7**. + above uses liblcms2. Tested with **1.19** and **2.7-2.9**. * **libwebp** provides the WebP format. @@ -161,17 +164,17 @@ Many of Pillow's features require external libraries: * **openjpeg** provides JPEG 2000 functionality. - * Pillow has been tested with openjpeg **2.0.0** and **2.1.0**. + * Pillow has been tested with openjpeg **2.0.0**, **2.1.0** and **2.3.1**. * Pillow does **not** support the earlier **1.5** series which ships - with Ubuntu <= 14.04 and Debian Jessie. + with Debian Jessie. * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-2.12.2** + * Pillow has been tested with libimagequant **2.6-2.12.5** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. - * Windows support: Libimagequant requires VS2013/MSVC 18 to compile, + * Windows support: Libimagequant requires VS2015/MSVC 19 to compile, so it is unlikely to work with Python 2.7 on Windows. * **libraqm** provides complex text layout support. @@ -211,10 +214,11 @@ build with newly installed external libraries. Build Options ^^^^^^^^^^^^^ -* Environment variable: ``MAX_CONCURRENCY=n``. By default, Pillow will - use multiprocessing to build the extension on all available CPUs, - but not more than 4. Setting ``MAX_CONCURRENCY`` to 1 will disable - parallel building. +* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use + multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` + sets the number of CPUs to use, or can disable parallel building by + using a setting of 1. By default, it uses 4 CPUs, or if 4 are not + available, as many as are present. * Build flags: ``--disable-zlib``, ``--disable-jpeg``, ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, @@ -330,19 +334,19 @@ Or for Python 3:: .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. -Prerequisites are installed on **Ubuntu 14.04 LTS** with:: +Prerequisites are installed on **Ubuntu 16.04 LTS** with:: - $ sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev libharfbuzz-dev libfribidi-dev \ - tcl8.6-dev tk8.6-dev python-tk + $ sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk \ + libharfbuzz-dev libfribidi-dev Then see ``depends/install_raqm.sh`` to install libraqm. Prerequisites are installed on recent **RedHat** **Centos** or **Fedora** with:: - $ sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ - lcms2-devel libwebp-devel tcl-devel tk-devel libraqm-devel \ - libimagequant-devel + $ sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ + freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ + harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel Note that the package manager may be yum or dnf, depending on the exact distribution. @@ -380,37 +384,37 @@ These platforms are built and tested for every change. +----------------------------------+-------------------------------+-----------------------+ |**Operating system** |**Tested Python versions** |**Tested Architecture**| +----------------------------------+-------------------------------+-----------------------+ -| Alpine | 2.7 |x86-64 | +| Alpine | 2.7, 3.7 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| Arch | 2.7 |x86-64 | +| Arch | 2.7, 3.7 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| Amazon | 2.7 |x86-64 | +| Amazon Linux 1 | 2.7, 3.6 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| Centos 6 | 2.7 |x86-64 | +| Amazon Linux 2 | 2.7, 3.6 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| Centos 7 | 2.7 |x86-64 | +| CentOS 6 | 2.7, 3.6 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| Debian Stretch | 2.7 |x86 | +| CentOS 7 | 2.7, 3.6 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| Fedora 28 | 2.7 |x86-64 | +| Debian 9 Stretch | 2.7, 3.5 |x86 | +----------------------------------+-------------------------------+-----------------------+ -| Fedora 29 | 2.7 |x86-64 | +| Debian 10 Buster | 2.7, 3.7 |x86 | +----------------------------------+-------------------------------+-----------------------+ -| Mac OS X 10.10 Yosemite* | 2.7, 3.5, 3.6, 3.7 |x86-64 | +| Fedora 30 | 2.7, 3.7 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| Ubuntu Linux 16.04 LTS | 2.7, 3.5, 3.6, 3.7, |x86-64 | +| Fedora 31 | 3.7 |x86-64 | ++----------------------------------+-------------------------------+-----------------------+ +| macOS 10.13 High Sierra* | 2.7, 3.5, 3.6, 3.7, 3.8 |x86-64 | ++----------------------------------+-------------------------------+-----------------------+ +| Ubuntu Linux 16.04 LTS | 2.7, 3.5, 3.6, 3.7, 3.8, |x86-64 | | | PyPy, PyPy3 | | +----------------------------------+-------------------------------+-----------------------+ -| Ubuntu Linux 14.04 LTS | 2.7, 3.5, 3.6 |x86-64 | -| +-------------------------------+-----------------------+ -| | 2.7 |x86 | -+----------------------------------+-------------------------------+-----------------------+ -| 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 | +----------------------------------+-------------------------------+-----------------------+ -\* Mac OS X CI is not run for every commit, but is run for every release. +\* macOS CI is not run for every commit, but is run for every release. Other Platforms ^^^^^^^^^^^^^^^ @@ -425,7 +429,9 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+------------------------------+--------------------------------+-----------------------+ |**Operating system** |**Tested Python versions** |**Latest tested Pillow version**|**Tested processors** | +----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 10.14 Mojave | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | +| macOS 10.14 Mojave | 2.7, 3.5, 3.6, 3.7 | 6.0.0 |x86-64 | +| +------------------------------+--------------------------------+ + +| | 3.4 | 5.4.1 | | +----------------------------------+------------------------------+--------------------------------+-----------------------+ | macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 388116a10..8af56f6c1 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -16,7 +16,7 @@ Open, rotate, and display an image (using the default viewer) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The following script loads an image, rotates it 45 degrees, and displays it -using an external viewer (usually xv on Unix, and the paint program on +using an external viewer (usually xv on Unix, and the Paint program on Windows). .. code-block:: python @@ -52,7 +52,7 @@ Functions .. warning:: To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files which decompress into a huge amount of data and are designed to crash or cause disruption by using up - a lot of memory), Pillow will issue a `DecompressionBombWarning` if the image is over a certain + a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the image is over a certain limit. If desired, the warning can be turned into an error with ``warnings.simplefilter('error', Image.DecompressionBombWarning)`` or suppressed entirely with ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also `the logging @@ -116,15 +116,66 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: rgb2xyz = ( 0.412453, 0.357580, 0.180423, 0, 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0 ) + 0.019334, 0.119193, 0.950227, 0) out = im.convert("RGB", rgb2xyz) .. automethod:: PIL.Image.Image.copy .. automethod:: PIL.Image.Image.crop + +This crops the input image with the provided coordinates: + +.. code-block:: python + + from PIL import Image + + im = Image.open("hopper.jpg") + + # The crop method from the Image module takes four coordinates as input. + # The right can also be represented as (left+width) + # and lower can be represented as (upper+height). + (left, upper, right, lower) = (20, 20, 100, 100) + + # Here the image "im" is cropped and assigned to new variable im_crop + im_crop = im.crop((left, upper, right, lower)) + + .. automethod:: PIL.Image.Image.draft .. automethod:: PIL.Image.Image.filter + +This blurs the input image using a filter from the ``ImageFilter`` module: + +.. code-block:: python + + from PIL import Image, ImageFilter + + im = Image.open("hopper.jpg") + + # Blur the input image using the filter ImageFilter.BLUR + im_blurred = im.filter(filter=ImageFilter.BLUR) + .. automethod:: PIL.Image.Image.getbands + +This helps to get the bands of the input image: + +.. code-block:: python + + from PIL import Image + + im = Image.open("hopper.jpg") + print(im.getbands()) # Returns ('R', 'G', 'B') + .. automethod:: PIL.Image.Image.getbbox + +This helps to get the bounding box coordinates of the input image: + +.. code-block:: python + + from PIL import Image + + im = Image.open("hopper.jpg") + print(im.getbbox()) + # Returns four coordinates in the format (left, upper, right, lower) + .. automethod:: PIL.Image.Image.getcolors .. automethod:: PIL.Image.Image.getdata .. automethod:: PIL.Image.Image.getextrema @@ -140,8 +191,35 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.putpixel .. automethod:: PIL.Image.Image.quantize .. automethod:: PIL.Image.Image.resize + +This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``: + +.. code-block:: python + + from PIL import Image + + im = Image.open("hopper.jpg") + + # Provide the target width and height of the image + (width, height) = (im.width // 2, im.height // 2) + im_resized = im.resize((width, height)) + .. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.rotate + +This rotates the input image by ``theta`` degrees counter clockwise: + +.. code-block:: python + + from PIL import Image + + im = Image.open("hopper.jpg") + + # Rotate the image by 60 degrees counter clockwise + theta = 60 + # Angle is in degrees counter clockwise + im_rotated = im.rotate(angle=theta) + .. automethod:: PIL.Image.Image.save .. automethod:: PIL.Image.Image.seek .. automethod:: PIL.Image.Image.show @@ -154,6 +232,21 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.tostring .. automethod:: PIL.Image.Image.transform .. automethod:: PIL.Image.Image.transpose + +This flips the input image by using the ``Image.FLIP_LEFT_RIGHT`` method. + +.. code-block:: python + + from PIL import Image + + im = Image.open("hopper.jpg") + + # Flip the image from left to right + im_flipped = im.transpose(method=Image.FLIP_LEFT_RIGHT) + # To flip the image from top to bottom, + # use the method "Image.FLIP_TOP_BOTTOM" + + .. automethod:: PIL.Image.Image.verify .. automethod:: PIL.Image.Image.fromstring @@ -169,10 +262,10 @@ Instances of the :py:class:`Image` class have the following attributes: .. py:attribute:: filename The filename or path of the source file. Only images created with the - factory function `open` have a filename attribute. If the input is a + factory function ``open`` have a filename attribute. If the input is a file like object, the filename attribute is set to an empty string. - :type: :py:class: `string` + :type: :py:class:`string` .. py:attribute:: format @@ -210,9 +303,9 @@ Instances of the :py:class:`Image` class have the following attributes: .. py:attribute:: palette - Colour palette table, if any. If mode is “P”, this should be an instance of - the :py:class:`~PIL.ImagePalette.ImagePalette` class. Otherwise, it should - be set to ``None``. + Colour palette table, if any. If mode is "P" or "PA", this should be an + instance of the :py:class:`~PIL.ImagePalette.ImagePalette` class. + Otherwise, it should be set to ``None``. :type: :py:class:`~PIL.ImagePalette.ImagePalette` or ``None`` diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 35f4acee6..922e1685a 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -33,13 +33,13 @@ can be easily displayed in a chromaticity diagram, for example). .. py:attribute:: version The version number of the ICC standard that this profile follows - (e.g. `2.0`). + (e.g. ``2.0``). :type: :py:class:`float` .. py:attribute:: icc_version - Same as `version`, but in encoded format (see 7.2.4 of ICC.1:2010). + Same as ``version``, but in encoded format (see 7.2.4 of ICC.1:2010). .. py:attribute:: device_class @@ -132,21 +132,21 @@ can be easily displayed in a chromaticity diagram, for example). .. py:attribute:: manufacturer - The (english) display string for the device manufacturer (see + The (English) display string for the device manufacturer (see 9.2.22 of ICC.1:2010). :type: :py:class:`unicode` or ``None`` .. py:attribute:: model - The (english) display string for the device model of the device + The (English) display string for the device model of the device for which this profile is created (see 9.2.23 of ICC.1:2010). :type: :py:class:`unicode` or ``None`` .. py:attribute:: profile_description - The (english) display string for the profile description (see + The (English) display string for the profile description (see 9.2.41 of ICC.1:2010). :type: :py:class:`unicode` or ``None`` @@ -269,14 +269,14 @@ can be easily displayed in a chromaticity diagram, for example). .. py:attribute:: viewing_condition - The (english) display string for the viewing conditions (see + The (English) display string for the viewing conditions (see 9.2.48 of ICC.1:2010). :type: :py:class:`unicode` or ``None`` .. py:attribute:: screening_description - The (english) display string for the screening conditions. + The (English) display string for the screening conditions. This tag was available in ICC 3.2, but it is removed from version 4. diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 7c24bae93..51eaf925e 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -132,11 +132,11 @@ Methods Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. - :param xy: Two points to define the bounding box. Sequence of - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, - where ``x1 >= x0`` and ``y1 >= y0``. - :param start: Starting angle, in degrees. Angles are measured from - 3 o'clock, increasing clockwise. + :param xy: Two points to define the bounding box. Sequence of ``[(x0, y0), + (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= + y0``. + :param start: Starting angle, in degrees. Angles are measured from 3 + o'clock, increasing clockwise. :param end: Ending angle, in degrees. :param fill: Color to use for the arc. :param width: The line width, in pixels. @@ -159,9 +159,9 @@ Methods Same as :py:meth:`~PIL.ImageDraw.ImageDraw.arc`, but connects the end points with a straight line. - :param xy: Two points to define the bounding box. Sequence of - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, - where ``x1 >= x0`` and ``y1 >= y0``. + :param xy: Two points to define the bounding box. Sequence of ``[(x0, y0), + (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= + y0``. :param outline: Color to use for the outline. :param fill: Color to use for the fill. :param width: The line width, in pixels. @@ -173,8 +173,8 @@ Methods Draws an ellipse inside the given bounding box. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, - where ``x1 >= x0`` and ``y1 >= y0``. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` + and ``y1 >= y0``. :param outline: Color to use for the outline. :param fill: Color to use for the fill. :param width: The line width, in pixels. @@ -203,11 +203,11 @@ Methods Same as arc, but also draws straight lines between the end points and the center of the bounding box. - :param xy: Two points to define the bounding box. Sequence of - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, - where ``x1 >= x0`` and ``y1 >= y0``. - :param start: Starting angle, in degrees. Angles are measured from - 3 o'clock, increasing clockwise. + :param xy: Two points to define the bounding box. Sequence of ``[(x0, y0), + (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= + y0``. + :param start: Starting angle, in degrees. Angles are measured from 3 + o'clock, increasing clockwise. :param end: Ending angle, in degrees. :param fill: Color to use for the fill. :param outline: Color to use for the outline. @@ -255,7 +255,7 @@ Methods Draw a shape. -.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None) +.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None) Draws the string at the given position. @@ -287,7 +287,26 @@ Methods .. versionadded:: 4.2.0 -.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None) + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param stroke_fill: Color to use for the text stroke. If not given, will default to + the ``fill`` parameter. + + .. versionadded:: 6.2.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None) Draws the string at the given position. @@ -316,7 +335,17 @@ Methods .. versionadded:: 4.2.0 -.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None) + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) Return the size of the given string, in pixels. @@ -330,7 +359,6 @@ Methods Requires libraqm. .. versionadded:: 4.2.0 - :param features: A list of OpenType font features to be used during text layout. This is usually used to turn on optional font features that are not enabled by default, @@ -343,8 +371,21 @@ Methods Requires libraqm. .. versionadded:: 4.2.0 + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + ` + Requires libraqm. -.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None) + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) Return the size of the given string, in pixels. @@ -370,6 +411,20 @@ Methods .. versionadded:: 4.2.0 + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + .. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None) .. warning:: This method is experimental. diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index 3368f799f..52a7d7500 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -38,12 +38,31 @@ image enhancement filters: * **SMOOTH_MORE** .. autoclass:: PIL.ImageFilter.Color3DLUT + :members: + .. autoclass:: PIL.ImageFilter.BoxBlur + :members: + .. autoclass:: PIL.ImageFilter.GaussianBlur + :members: + .. autoclass:: PIL.ImageFilter.UnsharpMask + :members: + .. autoclass:: PIL.ImageFilter.Kernel + :members: + .. autoclass:: PIL.ImageFilter.RankFilter + :members: + .. autoclass:: PIL.ImageFilter.MedianFilter + :members: + .. autoclass:: PIL.ImageFilter.MinFilter + :members: + .. autoclass:: PIL.ImageFilter.MaxFilter + :members: + .. autoclass:: PIL.ImageFilter.ModeFilter + :members: diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 55ce3d382..bb7538096 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -47,43 +47,11 @@ Functions Methods ------- -.. py:method:: PIL.ImageFont.ImageFont.getsize(text) +.. autoclass:: PIL.ImageFont.ImageFont + :members: - :return: (width, height) +.. autoclass:: PIL.ImageFont.FreeTypeFont + :members: -.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[]) - - Create a bitmap for the text. - - If the font uses antialiasing, the bitmap should have mode “L” and use a - maximum value of 255. Otherwise, it should have mode “1”. - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - .. versionadded:: 1.1.5 - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - .. versionadded:: 4.2.0 - - :return: An internal PIL storage memory instance as defined by the - :py:mod:`PIL.Image.core` interface module. +.. autoclass:: PIL.ImageFont.TransposedFont + :members: diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 39aaef6bc..e94e21cb9 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -11,7 +11,7 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 -.. py:function:: PIL.ImageGrab.grab(bbox=None) +.. py:function:: PIL.ImageGrab.grab(bbox=None, include_layered_windows=False, all_screens=False) Take a snapshot of the screen. The pixels inside the bounding box are returned as an "RGB" image on Windows or "RGBA" on macOS. @@ -20,6 +20,13 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS) :param bbox: What region to copy. Default is the entire screen. + Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used. + :param include_layered_windows: Includes layered windows. Windows OS only. + + .. versionadded:: 6.1.0 + :param all_screens: Capture all monitors. Windows OS only. + + .. versionadded:: 6.2.0 :return: An image .. py:function:: PIL.ImageGrab.grabclipboard() diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 445a7e277..ca30244d1 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -5,8 +5,8 @@ ========================== The :py:mod:`ImageMath` module can be used to evaluate “image expressions”. The -module provides a single eval function, which takes an expression string and -one or more images. +module provides a single :py:meth:`~PIL.ImageMath.eval` function, which takes +an expression string and one or more images. Example: Using the :py:mod:`~PIL.ImageMath` module -------------------------------------------------- diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index 50cea90ca..1c86d168f 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -8,13 +8,13 @@ The :py:mod:`ImageOps` module contains a number of ‘ready-made’ image processing operations. This module is somewhat experimental, and most operators only work on L and RGB images. -Only bug fixes have been added since the Pillow fork. - .. versionadded:: 1.1.3 .. autofunction:: autocontrast .. autofunction:: colorize +.. autofunction:: pad .. autofunction:: crop +.. autofunction:: scale .. autofunction:: deform .. autofunction:: equalize .. autofunction:: expand @@ -25,3 +25,4 @@ Only bug fixes have been added since the Pillow fork. .. autofunction:: mirror .. autofunction:: posterize .. autofunction:: solarize +.. autofunction:: exif_transpose diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 978db4caf..5ab350ef3 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -53,7 +53,7 @@ vector data. Path objects can be passed to the methods on the Converts the path to a Python list [(x, y), …]. :param flat: By default, this function returns a list of 2-tuples - [(x, y), ...]. If this argument is `True`, it + [(x, y), ...]. If this argument is ``True``, it returns a flat list [x, y, ...] instead. :return: A list of coordinates. See **flat**. diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 386401075..7dd7084db 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -4,8 +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. +The :py:mod:`ImageQt` module contains support for creating PyQt5 or PySide2 QImage +objects from PIL images. .. versionadded:: 1.1.6 @@ -14,7 +14,7 @@ PySide2 QImage objects from PIL images. Creates an :py:class:`~PIL.ImageQt.ImageQt` object from a PIL :py:class:`~PIL.Image.Image` object. This class is a subclass of QtGui.QImage, which means that you can pass the resulting objects directly - to PyQt4/PyQt5/PySide API functions and methods. + to PyQt5/PySide2 API functions and methods. This operation is currently supported for mode 1, L, P, RGB, and RGBA images. To handle other modes, you need to convert the image first. diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index f8ea9ee92..251ea3a93 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -25,3 +25,4 @@ The :py:class:`~PIL.ImageSequence.Iterator` class ------------------------------------------------- .. autoclass:: PIL.ImageSequence.Iterator + :members: diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index b8925bf8c..32f5917c1 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -20,6 +20,16 @@ for a region of an image. Min/max values for each band in the image. + .. note:: + + This relies on the :py:meth:`~PIL.Image.histogram` method, and + simply returns the low and high bins used. This is correct for + images with 8 bits per channel, but fails for other modes such as + ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.getextrema` to + return per-band extrema for the image. This is more correct and + efficient because, for non-8-bit modes, the histogram method uses + :py:meth:`~PIL.Image.getextrema` to determine the bins used. + .. py:attribute:: count Total number of pixels for each band in the image. diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 2696e7e99..ff3d6a7fc 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -24,6 +24,8 @@ Tkinter makes the window handle available via the winfo_id method: .. autoclass:: PIL.ImageWin.Dib :members: - .. autoclass:: PIL.ImageWin.HDC + :members: + .. autoclass:: PIL.ImageWin.HWND + :members: diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 511eb97bf..ed0ab1a0c 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -3,32 +3,30 @@ File Handling in Pillow ======================= -When opening a file as an image, Pillow requires a filename, -pathlib.Path 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. +When opening a file as an image, Pillow requires a filename, ``pathlib.Path`` +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,42 +36,34 @@ 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 --------------- -* ``Image.open()`` Path-like objects are opened as a file. Metadata is read - from the open file. The file is left open for further usage. +* ``Image.open()`` Filenames and ``Path`` objects are opened as a file. + Metadata is read from the open file. The file is left open for further usage. * ``Image.Image.load()`` When the pixel data from the image is required, ``load()`` is called. The current frame is read into memory. The image can now be used independently of the underlying image file. - If a filename or a path-like object was passed to ``Image.open()``, then - the file object was opened by Pillow and is considered to be used exclusively - by Pillow. So if the image is a single-frame image, the file will - be closed in this method after the frame is read. If the image is a - multi-frame image, (e.g. multipage TIFF and animated GIF) the image file is - left open so that ``Image.Image.seek()`` can load the appropriate frame. + If a filename or a ``Path`` object was passed to ``Image.open()``, then the + file object was opened by Pillow and is considered to be used exclusively by + Pillow. So if the image is a single-frame image, the file will be closed in + this method after the frame is read. If the image is a multi-frame image, + (e.g. multipage TIFF and animated GIF) the image file is left open so that + ``Image.Image.seek()`` can load the appropriate frame. -* ``Image.Image.close()`` Closes the file pointer and destroys the - core image object. This is used in the Pillow context manager - support. e.g.:: +* ``Image.Image.close()`` Closes the file and destroys the core image object. + This is used in the Pillow context manager support. e.g.:: with Image.open('test.jpg') as img: ... # 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, @@ -84,26 +74,20 @@ data until the caller has explicitly closed the image. Complications ------------- -* TiffImagePlugin has some code to pass the underlying file descriptor - into libtiff (if working on an actual file). Since libtiff closes - the file descriptor internally, it is duplicated prior to passing it - into libtiff. +* ``TiffImagePlugin`` has some code to pass the underlying file descriptor into + libtiff (if working on an actual file). Since libtiff closes the file + descriptor internally, it is duplicated prior to passing it into libtiff. -* ``decoder.handles_eof`` This slightly misnamed flag indicates that - the decoder wants to be called with a 0 length buffer when reads are - done. Despite the comments in ``ImageFile.load()``, the only decoder - that actually uses this flag is the Jpeg2K decoder. The use of this - flag in Jpeg2K predated the change to the decoder that added the - pulls_fd flag, and is therefore not used. +* After a file has been closed, operations that require file access will fail:: -* I don't think that there's any way to make this safe without - changing the lazy loading:: - - # 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 ---------------------- @@ -113,5 +97,6 @@ Proposed File Handling * ``Image.Image.seek()`` should never close the image file. -* Users of the library should call ``Image.Image.close()`` on any - multi-frame image to ensure that the underlying file is closed. +* Users of the library should use a context manager or call + ``Image.Image.close()`` on any image opened with a filename or ``Path`` + object to ensure that the underlying file is closed. diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index 4bb25e371..931f9fd1e 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -27,7 +27,7 @@ Image resizing filters ---------------------- Image resizing methods :py:meth:`~PIL.Image.Image.resize` and -:py:meth:`~PIL.Image.Image.thumbnail` take a `resample` argument, which tells +:py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells which filter should be used for resampling. Possible values are: :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`, :py:attr:`PIL.Image.BICUBIC` and :py:attr:`PIL.Image.ANTIALIAS`. diff --git a/docs/releasenotes/2.8.0.rst b/docs/releasenotes/2.8.0.rst index 85235d72a..c522fe8b0 100644 --- a/docs/releasenotes/2.8.0.rst +++ b/docs/releasenotes/2.8.0.rst @@ -4,18 +4,28 @@ Open HTTP response objects with Image.open ------------------------------------------ -HTTP response objects returned from `urllib2.urlopen(url)` or `requests.get(url, stream=True).raw` are 'file-like' but do not support `.seek()` operations. As a result PIL was unable to open them as images, requiring a wrap in `cStringIO` or `BytesIO`. +HTTP response objects returned from ``urllib2.urlopen(url)`` or +``requests.get(url, stream=True).raw`` are 'file-like' but do not support ``.seek()`` +operations. As a result PIL was unable to open them as images, requiring a wrap in +``cStringIO`` or ``BytesIO``. -Now new functionality has been added to `Image.open()` by way of an `.seek(0)` check and catch on exception `AttributeError` or `io.UnsupportedOperation`. If this is caught we attempt to wrap the object using `io.BytesIO` (which will only work on buffer-file-like objects). +Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and +catch on exception ``AttributeError`` or ``io.UnsupportedOperation``. If this is caught we +attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like +objects). -This allows opening of files using both `urllib2` and `requests`, e.g.:: +This allows opening of files using both ``urllib2`` and ``requests``, e.g.:: Image.open(urllib2.urlopen(url)) Image.open(requests.get(url, stream=True).raw) -If the response uses content-encoding (compression, either gzip or deflate) then this will fail as both the urllib2 and requests raw file object will produce compressed data in that case. Using Content-Encoding on images is rather non-sensical as most images are already compressed, but it can still happen. +If the response uses content-encoding (compression, either gzip or deflate) then this +will fail as both the urllib2 and requests raw file object will produce compressed data +in that case. Using Content-Encoding on images is rather non-sensical as most images are +already compressed, but it can still happen. -For requests the work-around is to set the decode_content attribute on the raw object to True:: +For requests the work-around is to set the decode_content attribute on the raw object to +True:: response = requests.get(url, stream=True) response.raw.decode_content = True diff --git a/docs/releasenotes/3.0.0.rst b/docs/releasenotes/3.0.0.rst index 9cc1de98c..67569d337 100644 --- a/docs/releasenotes/3.0.0.rst +++ b/docs/releasenotes/3.0.0.rst @@ -5,8 +5,8 @@ Saving Multipage Images ----------------------- -There is now support for saving multipage images in the `GIF` and -`PDF` formats. To enable this functionality, pass in `save_all=True` +There is now support for saving multipage images in the ``GIF`` and +``PDF`` formats. To enable this functionality, pass in ``save_all=True`` as a keyword argument to the save:: im.save('test.pdf', save_all=True) @@ -37,7 +37,7 @@ have been removed in this release:: ImageDraw.setink() ImageDraw.setfill() The ImageFileIO module - The ImageFont.FreeTypeFont and ImageFont.truetype `file` keyword arg + The ImageFont.FreeTypeFont and ImageFont.truetype ``file`` keyword arg The ImagePalette private _make functions ImageWin.fromstring() ImageWin.tostring() diff --git a/docs/releasenotes/3.1.0.rst b/docs/releasenotes/3.1.0.rst index 388af03ac..3cdb6939d 100644 --- a/docs/releasenotes/3.1.0.rst +++ b/docs/releasenotes/3.1.0.rst @@ -5,8 +5,8 @@ ImageDraw arc, chord and pieslice can now use floats ---------------------------------------------------- -There is no longer a need to ensure that the start and end arguments for `arc`, -`chord` and `pieslice` are integers. +There is no longer a need to ensure that the start and end arguments for ``arc``, +``chord`` and ``pieslice`` are integers. Note that these numbers are not simply rounded internally, but are actually utilised in the drawing process. diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst index e1fa83cce..0145347f2 100644 --- a/docs/releasenotes/6.0.0.rst +++ b/docs/releasenotes/6.0.0.rst @@ -26,12 +26,38 @@ Several undocumented functions in ``ImageOps`` were deprecated in Pillow 4.3.0 ( and have now been removed: ``gaussian_blur``, ``gblur``, ``unsharp_mask``, ``usm`` and ``box_blur``. Use the equivalent operations in ``ImageFilter`` instead. +Removed deprecated VERSION +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use ``__version__`` +instead. + 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.x the last series to support Python 2. + +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 deprecated from ``ImageQt`` and will be removed in +a future version. Please upgrade to PyQt5 or PySide2. + +PIL.*ImagePlugin.__version__ attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + These version constants have been deprecated and will be removed in a future version. @@ -73,18 +99,114 @@ version. Use ``PIL.__version__`` instead. +ImageCms.CmsProfile attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From +6.0.0, they issue a ``DeprecationWarning``: + +======================== =============================== +Deprecated Use instead +======================== =============================== +``color_space`` Padded ``xcolor_space`` +``pcs`` Padded ``connection_space`` +``product_copyright`` Unicode ``copyright`` +``product_desc`` Unicode ``profile_description`` +``product_description`` Unicode ``profile_description`` +``product_manufacturer`` Unicode ``manufacturer`` +``product_model`` Unicode ``model`` +======================== =============================== + +MIME type improvements +^^^^^^^^^^^^^^^^^^^^^^ + +Previously, all JPEG2000 images had the MIME type "image/jpx". This has now been +corrected. After the file format drivers have been loaded, ``Image.MIME["JPEG2000"]`` +will return "image/jp2". ``ImageFile.get_format_mimetype`` will return "image/jpx" if +a JPX profile is present, or "image/jp2" otherwise. + +Previously, all SGI images had the MIME type "image/rgb". This has now been +corrected. After the file format drivers have been loaded, ``Image.MIME["SGI"]`` +will return "image/sgi". ``ImageFile.get_format_mimetype`` will return "image/rgb" if +RGB image data is present, or "image/sgi" otherwise. + +MIME types have been added to the PPM format. After the file format drivers have been +loaded, ``Image.MIME["PPM"]`` will now return the generic "image/x-portable-anymap". +``ImageFile.get_format_mimetype`` will return a MIME type specific to the color type. + +The TGA, PCX and ICO formats also now have MIME types: "image/x-tga", "image/x-pcx" and +"image/x-icon" respectively. + API Additions ============= -TODO -^^^^ +DIB file format +^^^^^^^^^^^^^^^ -TODO +Pillow now supports reading and writing the Device Independent Bitmap file format. + +Image.quantize +^^^^^^^^^^^^^^ + +The ``dither`` option is now a customisable parameter (was previously hardcoded to ``1``). +This parameter takes the same values used in :py:meth:`~PIL.Image.Image.convert`. + +New language parameter +^^^^^^^^^^^^^^^^^^^^^^ + +These text-rendering functions now accept a ``language`` parameter to request +language-specific glyphs and ligatures from the font: + +* ``ImageDraw.ImageDraw.multiline_text()`` +* ``ImageDraw.ImageDraw.multiline_textsize()`` +* ``ImageDraw.ImageDraw.text()`` +* ``ImageDraw.ImageDraw.textsize()`` +* ``ImageFont.ImageFont.getmask()`` +* ``ImageFont.ImageFont.getsize_multiline()`` +* ``ImageFont.ImageFont.getsize()`` + +Added EXIF class +^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.getexif` has been added, which returns an +:py:class:`~PIL.Image.Exif` instance. Values can be retrieved and set like a +dictionary. When saving JPEG, PNG or WEBP, the instance can be passed as an +``exif`` argument to include any changes in the output image. + +Added ImageOps.exif_transpose +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.ImageOps.exif_transpose` returns a copy of an image, transposed +according to its EXIF Orientation tag. + +PNG EXIF data +^^^^^^^^^^^^^ + +EXIF data can now be read from and saved to PNG images. However, unlike other image +formats, EXIF data is not guaranteed to be present in :py:attr:`~PIL.Image.Image.info` +until :py:meth:`~PIL.Image.Image.load` has been called. Other Changes ============= -TODO -^^^^ +Reading new DDS image format +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Pillow can now read uncompressed RGB data from DDS images. + +Reading TIFF with old-style JPEG compression +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Added support reading TIFF files with old-style JPEG compression through LibTIFF. All +YCbCr TIFF images are now always read as RGB. + +TIFF compression codecs +^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for the LZMA, Zstd and WebP TIFF compression codecs. + +Improved support for transposing I;16 images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +I;16, I;16L and I;16B are now supported image modes for all +:py:meth:`~PIL.Image.Image.transpose` operations. diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst new file mode 100644 index 000000000..eb4304843 --- /dev/null +++ b/docs/releasenotes/6.1.0.rst @@ -0,0 +1,111 @@ +6.1.0 +----- + +Deprecations +============ + +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") + +API Additions +============= + +Image.entropy +^^^^^^^^^^^^^ +Calculates and returns the entropy for the image. A bilevel image (mode "1") is treated +as a greyscale ("L") image by this method. If a mask is provided, the method employs +the histogram for those parts of the image where the mask image is non-zero. The mask +image must have the same size as the image, and be either a bi-level image (mode "1") or +a greyscale image ("L"). + +ImageGrab.grab +^^^^^^^^^^^^^^ + +An optional ``include_layered_windows`` parameter has been added to ``ImageGrab.grab``, +defaulting to ``False``. If true, layered windows will be included in the resulting +image on Windows. + +ImageSequence.all_frames +^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method to facilitate applying a given function to all frames in an image, or to +all frames in a list of images. The frames are returned as a list of separate images. +For example, ``ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90))`` +could be used to return all frames from an image, each rotated 90 degrees. + +Variation fonts +^^^^^^^^^^^^^^^ + +Variation fonts are now supported, allowing for different styles from the same font +file. ``ImageFont.FreeTypeFont`` has four new methods, +:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_names` and +:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_name` for using named styles, and +:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_axes` and +:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_axes` for using font axes +instead. An ``IOError`` will be raised if the font is not a variation font. FreeType +2.9.1 or greater is required. + +Other Changes +============= + +ImageTk.getimage +^^^^^^^^^^^^^^^^ + +This function is now supported. It returns the contents of an ``ImageTk.PhotoImage`` as +an RGBA ``Image.Image`` instance. + +Image quality for JPEG compressed TIFF +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The TIFF encoder accepts a ``quality`` parameter for ``jpeg`` compressed TIFF files. A +value from 0 (worst) to 100 (best) controls the image quality, similar to the JPEG +encoder. The default is 75. For example: + +.. code-block:: python + + im.save("out.tif", compression="jpeg", quality=85) + +Improve encoding of TIFF tags +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The TIFF encoder supports more types, especially arrays. This is required for the +GeoTIFF format which encodes geospatial information. + +* Pass ``tagtype`` from v2 directory to libtiff encoder, instead of autodetecting type. +* Use explicit types eg. ``uint32_t`` for ``TIFF_LONG`` to fix issues on platforms with + 64-bit longs. +* Add support for multiple values (arrays). Requires type in v2 directory and values + must be passed as a tuple. +* Add support for signed types eg. ``TIFFTypes.TIFF_SIGNED_SHORT``. + +Respect PKG_CONFIG environment variable when building +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This variable is commonly used by other build systems and using it can help with +cross-compiling. Falls back to ``pkg-config`` as before. + +Top-to-bottom complex text rendering +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Drawing text in the 'ttb' direction with ``ImageFont`` has been significantly improved +and requires Raqm 0.7 or greater. diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst new file mode 100644 index 000000000..c31ade197 --- /dev/null +++ b/docs/releasenotes/6.2.0.rst @@ -0,0 +1,88 @@ +6.2.0 +----- + +API Additions +============= + +Text stroking +^^^^^^^^^^^^^ + +``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing +operations. They allow text to be outlined, setting the width of the stroke and +and the color respectively. If not provided, ``stroke_fill`` will default to +the ``fill`` parameter. + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 40) + font.getsize_multiline("A", stroke_width=2) + font.getsize("ABC\nAaaa", stroke_width=2) + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.textsize("A", font, stroke_width=2) + draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0") + draw.multiline_text((10, 10), "A\nB", "#f00", font, + stroke_width=2, stroke_fill="#0f0") + +For example, + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0") + + +creates the following image: + +.. image:: ../../Tests/images/imagedraw_stroke_different.png + +ImageGrab on multi-monitor Windows +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An ``all_screens`` argument has been added to ``ImageGrab.grab``. If ``True``, +all monitors will be included in the created image. + +API Changes +=========== + +Image.getexif +^^^^^^^^^^^^^ + +To allow for lazy loading of Exif data, ``Image.getexif()`` now returns a +shared instance of ``Image.Exif``. + +Deprecations +^^^^^^^^^^^^ + +Image.frombuffer +~~~~~~~~~~~~~~~~ + +There has been a longstanding warning that the defaults of ``Image.frombuffer`` +may change in the future for the "raw" decoder. The change will now take place +in Pillow 7.0. + +Other Changes +============= + +Removed bdist_wininst .exe installers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.exe installers fell out of favour with PEP 527, and will be deprecated in +Python 3.8. Pillow will no longer be distributing them. Wheels should be used +instead. + +Flags for libwebp in wheels +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When building libwebp for inclusion in wheels, Pillow now adds the -O3 and +-DNDEBUG CFLAGS. These flags would be used by default if building libwebp +without debugging, and using them fixes a significant decrease in speed when +a wheel-installed copy of Pillow performs libwebp operations. diff --git a/docs/releasenotes/6.2.1.rst b/docs/releasenotes/6.2.1.rst new file mode 100644 index 000000000..ca298fa70 --- /dev/null +++ b/docs/releasenotes/6.2.1.rst @@ -0,0 +1,26 @@ +6.2.1 +----- + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +Python 2.7 +~~~~~~~~~~ + +Python 2.7 reaches end-of-life on 2020-01-01. + +Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python +2.7, making Pillow 6.2.x the last release series to support Python 2. + +Other Changes +============= + + + +Support added for Python 3.8 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 6.2.1 supports Python 3.8. diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst new file mode 100644 index 000000000..386a26757 --- /dev/null +++ b/docs/releasenotes/7.0.0.rst @@ -0,0 +1,94 @@ +7.0.0 +----- + +Backwards Incompatible Changes +============================== + +PILLOW_VERSION constant +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +PIL.*ImagePlugin.__version__ attributes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The version constants of individual plugins have been removed. Use ``PIL.__version__`` +instead. + +=============================== ================================= ================================== +Removed Removed Removed +=============================== ================================= ================================== +``BmpImagePlugin.__version__`` ``Jpeg2KImagePlugin.__version__`` ``PngImagePlugin.__version__`` +``CurImagePlugin.__version__`` ``JpegImagePlugin.__version__`` ``PpmImagePlugin.__version__`` +``DcxImagePlugin.__version__`` ``McIdasImagePlugin.__version__`` ``PsdImagePlugin.__version__`` +``EpsImagePlugin.__version__`` ``MicImagePlugin.__version__`` ``SgiImagePlugin.__version__`` +``FliImagePlugin.__version__`` ``MpegImagePlugin.__version__`` ``SunImagePlugin.__version__`` +``FpxImagePlugin.__version__`` ``MpoImagePlugin.__version__`` ``TgaImagePlugin.__version__`` +``GdImageFile.__version__`` ``MspImagePlugin.__version__`` ``TiffImagePlugin.__version__`` +``GifImagePlugin.__version__`` ``PalmImagePlugin.__version__`` ``WmfImagePlugin.__version__`` +``IcoImagePlugin.__version__`` ``PcdImagePlugin.__version__`` ``XbmImagePlugin.__version__`` +``ImImagePlugin.__version__`` ``PcxImagePlugin.__version__`` ``XpmImagePlugin.__version__`` +``ImtImagePlugin.__version__`` ``PdfImagePlugin.__version__`` ``XVThumbImagePlugin.__version__`` +``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` +=============================== ================================= ================================== + +PyQt4 and PySide +^^^^^^^^^^^^^^^^ + +Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since +2018-08-31 and PySide since 2015-10-14. + +Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 +or PySide2. + +Setting the size of TIFF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws +an error. Use ``Image.resize`` instead. + + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +TODO +~~~~ + +TODO + + +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``. + +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. + +Previous method: + +.. code-block:: python + + im = Image.open("hopper.png") + im.save("out.jpg") + +Use instead: + +.. code-block:: python + + with Image.open("hopper.png") as im: + im.save("out.jpg") diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 9a088375b..6f8ecb95c 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,6 +6,10 @@ Release Notes .. toctree:: :maxdepth: 2 + 7.0.0 + 6.2.1 + 6.2.0 + 6.1.0 6.0.0 5.4.1 5.4.0 diff --git a/mp_compile.py b/mp_compile.py deleted file mode 100644 index f50210ba5..000000000 --- a/mp_compile.py +++ /dev/null @@ -1,84 +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 -from multiprocessing import Pool, cpu_count -from distutils.ccompiler import CCompiler -import os -import sys - -try: - MAX_PROCS = int(os.environ.get('MAX_CONCURRENCY', min(4, cpu_count()))) -except NotImplementedError: - MAX_PROCS = None - - -# hideous monkeypatching. but. but. but. -def _mp_compile_one(tp): - (self, obj, build, cc_args, extra_postargs, pp_opts) = tp - try: - src, ext = build[obj] - except KeyError: - return - self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) - return - - -def _mp_compile(self, sources, output_dir=None, macros=None, - include_dirs=None, debug=0, extra_preargs=None, - extra_postargs=None, depends=None): - """Compile one or more source files. - - see distutils.ccompiler.CCompiler.compile for comments. - """ - # A concrete compiler class can either override this method - # entirely or implement _compile(). - - macros, objects, extra_postargs, pp_opts, build = self._setup_compile( - output_dir, macros, include_dirs, sources, depends, extra_postargs) - cc_args = self._get_cc_args(pp_opts, debug, extra_preargs) - - pool = Pool(MAX_PROCS) - try: - print("Building using %d processes" % pool._processes) - except Exception: - pass - arr = [(self, obj, build, cc_args, extra_postargs, pp_opts) - for obj in objects] - pool.map_async(_mp_compile_one, arr) - pool.close() - pool.join() - # Return *all* object filenames, not just the ones we just built. - return objects - - -def install(): - - fl_win = sys.platform.startswith('win') - fl_cygwin = sys.platform.startswith('cygwin') - - if fl_win or fl_cygwin: - # Windows barfs on multiprocessing installs - print("Single threaded build for Windows") - return - - if MAX_PROCS != 1: - # explicitly don't enable if environment says 1 processor - try: - # bug, only enable if we can make a Pool. see issue #790 and - # https://stackoverflow.com/questions/6033599/oserror-38-errno-38-with-multiprocessing - Pool(2) - CCompiler.compile = _mp_compile - except Exception as msg: - print("Exception installing mp_compile, proceeding without:" - "%s" % msg) - else: - print("Single threaded build, not installing mp_compile:" - "%s processes" % MAX_PROCS) - - -# We monkeypatch Python 2.7 -if sys.version_info.major < 3: - install() diff --git a/requirements.txt b/requirements.txt index cd3de4e1f..082d8e516 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,13 @@ # Development, documentation & testing requirements. --e . -alabaster -Babel +black; python_version >= '3.6' check-manifest -cov-core coverage coveralls -docopt -docutils jarn.viewdoc -Jinja2 -MarkupSafe olefile pycodestyle pyflakes -Pygments pyroma pytest pytest-cov -pytz -requests -six -snowballstemmer -Sphinx sphinx-rtd-theme diff --git a/selftest.py b/selftest.py index f4383b120..ea52256f7 100755 --- a/selftest.py +++ b/selftest.py @@ -1,12 +1,9 @@ #!/usr/bin/env python # minimal sanity check -from __future__ import print_function import sys -import os -from PIL import Image -from PIL import features +from PIL import Image, features try: Image.core.ping @@ -43,8 +40,8 @@ def testimage(): Or open existing files: - >>> im = Image.open("Tests/images/hopper.gif") - >>> _info(im) + >>> with Image.open("Tests/images/hopper.gif") as im: + ... _info(im) ('GIF', 'P', (128, 128)) >>> _info(Image.open("Tests/images/hopper.ppm")) ('PPM', 'RGB', (128, 128)) @@ -90,6 +87,8 @@ def testimage(): 2 >>> len(im.histogram()) 768 + >>> '%.7f' % im.entropy() + '8.8212866' >>> _info(im.point(list(range(256))*3)) (None, 'RGB', (128, 128)) >>> _info(im.resize((64, 64))) @@ -161,35 +160,11 @@ if __name__ == "__main__": exit_status = 0 - print("-"*68) - print("Pillow", Image.__version__, "TEST SUMMARY ") - print("-"*68) - print("Python modules loaded from", os.path.dirname(Image.__file__)) - print("Binary modules loaded from", os.path.dirname(Image.core.__file__)) - print("-"*68) - for name, feature in [ - ("pil", "PIL CORE"), - ("tkinter", "TKINTER"), - ("freetype2", "FREETYPE2"), - ("littlecms2", "LITTLECMS2"), - ("webp", "WEBP"), - ("transp_webp", "WEBP Transparency"), - ("webp_mux", "WEBPMUX"), - ("webp_anim", "WEBP Animation"), - ("jpg", "JPEG"), - ("jpg_2000", "OPENJPEG (JPEG2000)"), - ("zlib", "ZLIB (PNG/ZIP)"), - ("libtiff", "LIBTIFF"), - ("raqm", "RAQM (Bidirectional Text)") - ]: - if features.check(name): - print("---", feature, "support ok") - else: - print("***", feature, "support not installed") - print("-"*68) + features.pilinfo(sys.stdout, False) # use doctest to make sure the test program behaves as documented! import doctest + print("Running selftest:") status = doctest.testmod(sys.modules[__name__]) if status[0]: diff --git a/setup.cfg b/setup.cfg index bcaf82f27..1c6ebc84c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,15 @@ [aliases] test=pytest -[metadata] -license_file = LICENSE - [flake8] +extend-ignore = E203, W503 max-line-length = 88 + +[isort] +combine_as_imports = True +include_trailing_comma = True +line_length = 88 +multi_line_output = 3 + +[tool:pytest] +addopts = -rs diff --git a/setup.py b/setup.py index 5d8159a12..f5024990f 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,6 @@ # Final rating: 10/10 # Your cheese is so fresh most people think it's a cream: Mascarpone # ------------------------------ -from __future__ import print_function import os import re @@ -20,35 +19,105 @@ 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 + +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__"] -if sys.platform == "win32" and sys.version_info >= (3, 8): +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), - RuntimeWarning) + "Pillow {} does not support Python {}.{} and does not provide prebuilt " + "Windows binaries. We do not recommend building from source on Windows.".format( + PILLOW_VERSION, sys.version_info.major, sys.version_info.minor + ), + RuntimeWarning, + ) _IMAGING = ("decode", "encode", "map", "display", "outline", "path") _LIB_IMAGING = ( - "Access", "AlphaComposite", "Resample", "Bands", "BcnDecode", "BitDecode", - "Blend", "Chops", "ColorLUT", "Convert", "ConvertYCbCr", "Copy", "Crop", - "Dib", "Draw", "Effects", "EpsEncode", "File", "Fill", "Filter", - "FliDecode", "Geometry", "GetBBox", "GifDecode", "GifEncode", "HexDecode", - "Histo", "JpegDecode", "JpegEncode", "Matrix", "ModeFilter", - "Negative", "Offset", "Pack", "PackDecode", "Palette", "Paste", "Quant", - "QuantOctree", "QuantHash", "QuantHeap", "PcdDecode", "PcxDecode", - "PcxEncode", "Point", "RankFilter", "RawDecode", "RawEncode", "Storage", - "SgiRleDecode", "SunRleDecode", "TgaRleDecode", "TgaRleEncode", "Unpack", - "UnpackYCC", "UnsharpMask", "XbmDecode", "XbmEncode", "ZipDecode", - "ZipEncode", "TiffDecode", "Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur", - "QuantPngQuant", "codec_fd") + "Access", + "AlphaComposite", + "Resample", + "Bands", + "BcnDecode", + "BitDecode", + "Blend", + "Chops", + "ColorLUT", + "Convert", + "ConvertYCbCr", + "Copy", + "Crop", + "Dib", + "Draw", + "Effects", + "EpsEncode", + "File", + "Fill", + "Filter", + "FliDecode", + "Geometry", + "GetBBox", + "GifDecode", + "GifEncode", + "HexDecode", + "Histo", + "JpegDecode", + "JpegEncode", + "Matrix", + "ModeFilter", + "Negative", + "Offset", + "Pack", + "PackDecode", + "Palette", + "Paste", + "Quant", + "QuantOctree", + "QuantHash", + "QuantHeap", + "PcdDecode", + "PcxDecode", + "PcxEncode", + "Point", + "RankFilter", + "RawDecode", + "RawEncode", + "Storage", + "SgiRleDecode", + "SunRleDecode", + "TgaRleDecode", + "TgaRleEncode", + "Unpack", + "UnpackYCC", + "UnsharpMask", + "XbmDecode", + "XbmEncode", + "ZipDecode", + "ZipEncode", + "TiffDecode", + "Jpeg2KDecode", + "Jpeg2KEncode", + "BoxBlur", + "QuantPngQuant", + "codec_fd", +) DEBUG = False @@ -61,8 +130,20 @@ class RequiredDependencyException(Exception): pass -PLATFORM_MINGW = 'mingw' in ccompiler.get_default_compiler() -PLATFORM_PYPY = hasattr(sys, 'pypy_version_info') +PLATFORM_MINGW = "mingw" in ccompiler.get_default_compiler() +PLATFORM_PYPY = hasattr(sys, "pypy_version_info") + +if sys.platform == "win32" and PLATFORM_MINGW: + from distutils import cygwinccompiler + + cygwin_versions = cygwinccompiler.get_versions() + if cygwin_versions[1] is None: + # ld version is None + # distutils cygwinccompiler might fetch the ld path from gcc + # Try the normal path instead + cygwin_versions = list(cygwin_versions) + cygwin_versions[1] = cygwinccompiler._find_exe_version("ld -v") + cygwinccompiler.get_versions = lambda: tuple(cygwin_versions) def _dbg(s, tp=None): @@ -77,39 +158,36 @@ def _find_library_dirs_ldconfig(): # Based on ctypes.util from Python 2 if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): - if struct.calcsize('l') == 4: - machine = os.uname()[4] + '-32' + if struct.calcsize("l") == 4: + machine = os.uname()[4] + "-32" else: - machine = os.uname()[4] + '-64' + machine = os.uname()[4] + "-64" mach_map = { - 'x86_64-64': 'libc6,x86-64', - 'ppc64-64': 'libc6,64bit', - 'sparc64-64': 'libc6,64bit', - 's390x-64': 'libc6,64bit', - 'ia64-64': 'libc6,IA-64', - } - abi_type = mach_map.get(machine, 'libc6') + "x86_64-64": "libc6,x86-64", + "ppc64-64": "libc6,64bit", + "sparc64-64": "libc6,64bit", + "s390x-64": "libc6,64bit", + "ia64-64": "libc6,IA-64", + } + abi_type = mach_map.get(machine, "libc6") # Assuming GLIBC's ldconfig (with option -p) # Alpine Linux uses musl that can't print cache - args = ['/sbin/ldconfig', '-p'] - expr = r'.*\(%s.*\) => (.*)' % abi_type + args = ["/sbin/ldconfig", "-p"] + expr = r".*\(%s.*\) => (.*)" % abi_type env = dict(os.environ) - env['LC_ALL'] = 'C' - env['LANG'] = 'C' + env["LC_ALL"] = "C" + env["LANG"] = "C" elif sys.platform.startswith("freebsd"): - args = ['/sbin/ldconfig', '-r'] - expr = r'.* => (.*)' + args = ["/sbin/ldconfig", "-r"] + 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() @@ -130,10 +208,10 @@ def _add_directory(path, subdir, where=None): subdir = os.path.realpath(subdir) if os.path.isdir(subdir) and subdir not in path: if where is None: - _dbg('Appending path %s', subdir) + _dbg("Appending path %s", subdir) path.append(subdir) else: - _dbg('Inserting path %s', subdir) + _dbg("Inserting path %s", subdir) path.insert(where, subdir) elif subdir in path and where is not None: path.remove(subdir) @@ -142,9 +220,9 @@ def _add_directory(path, subdir, where=None): def _find_include_file(self, include): for directory in self.compiler.include_dirs: - _dbg('Checking for include file %s in %s', (include, directory)) + _dbg("Checking for include file %s in %s", (include, directory)) if os.path.isfile(os.path.join(directory, include)): - _dbg('Found %s', include) + _dbg("Found %s", include) return 1 return 0 @@ -152,10 +230,9 @@ def _find_include_file(self, include): def _find_library_file(self, library): ret = self.compiler.find_library_file(self.compiler.library_dirs, library) if ret: - _dbg('Found library %s at %s', (library, ret)) + _dbg("Found library %s at %s", (library, ret)) else: - _dbg("Couldn't find library %s in %s", - (library, self.compiler.library_dirs)) + _dbg("Couldn't find library %s in %s", (library, self.compiler.library_dirs)) return ret @@ -167,41 +244,18 @@ def _cmd_exists(cmd): def _read(file): - with open(file, 'rb') as fp: + with open(file, "rb") as fp: 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_libs = [ - 'pkg-config', - '--libs-only-L', name, - ] - command_cflags = [ - 'pkg-config', - '--cflags-only-I', name, - ] + command = os.environ.get("PKG_CONFIG", "pkg-config") + command_libs = [command, "--libs-only-L", name] + command_cflags = [command, "--cflags-only-I", name] if not DEBUG: - command_libs.append('--silence-errors') - command_cflags.append('--silence-errors') + command_libs.append("--silence-errors") + command_cflags.append("--silence-errors") libs = ( subprocess.check_output(command_libs) .decode("utf8") @@ -221,10 +275,19 @@ def _pkg_config(name): class pil_build_ext(build_ext): class feature: - features = ['zlib', 'jpeg', 'tiff', 'freetype', 'lcms', 'webp', - 'webpmux', 'jpeg2000', 'imagequant'] + features = [ + "zlib", + "jpeg", + "tiff", + "freetype", + "lcms", + "webp", + "webpmux", + "jpeg2000", + "imagequant", + ] - required = {'jpeg', 'zlib'} + required = {"jpeg", "zlib"} def __init__(self): for f in self.features: @@ -237,51 +300,54 @@ class pil_build_ext(build_ext): return getattr(self, feat) is None def __iter__(self): - for x in self.features: - yield x + yield from self.features feature = feature() - user_options = build_ext.user_options + [ - ('disable-%s' % x, None, 'Disable support for %s' % x) for x in feature - ] + [ - ('enable-%s' % x, None, 'Enable support for %s' % x) for x in feature - ] + [ - ('disable-platform-guessing', None, - 'Disable platform guessing on Linux'), - ('debug', None, 'Debug logging') - ] + [('add-imaging-libs=', None, 'Add libs to _imaging build')] + user_options = ( + build_ext.user_options + + [("disable-%s" % x, None, "Disable support for %s" % x) for x in feature] + + [("enable-%s" % x, None, "Enable support for %s" % x) for x in feature] + + [ + ("disable-platform-guessing", None, "Disable platform guessing on Linux"), + ("debug", None, "Debug logging"), + ] + + [("add-imaging-libs=", None, "Add libs to _imaging build")] + ) def initialize_options(self): self.disable_platform_guessing = None self.add_imaging_libs = "" build_ext.initialize_options(self) for x in self.feature: - setattr(self, 'disable_%s' % x, None) - setattr(self, 'enable_%s' % x, None) + setattr(self, "disable_%s" % x, None) + setattr(self, "enable_%s" % x, None) def finalize_options(self): build_ext.finalize_options(self) if self.debug: global DEBUG DEBUG = True - if sys.version_info.major >= 3 and not self.parallel: - # For Python 2.7, we monkeypatch distutils to have parallel - # builds. If --parallel (or -j) wasn't specified, we want to - # reproduce the same behavior as before, that is, auto-detect the - # number of jobs. - self.parallel = mp_compile.MAX_PROCS + if not self.parallel: + # If --parallel (or -j) wasn't specified, we want to reproduce the same + # behavior as before, that is, auto-detect the number of jobs. + try: + self.parallel = int( + os.environ.get("MAX_CONCURRENCY", min(4, os.cpu_count())) + ) + except TypeError: + self.parallel = None for x in self.feature: - if getattr(self, 'disable_%s' % x): + if getattr(self, "disable_%s" % x): setattr(self.feature, x, False) self.feature.required.discard(x) - _dbg('Disabling %s', x) - if getattr(self, 'enable_%s' % x): + _dbg("Disabling %s", x) + if getattr(self, "enable_%s" % x): raise ValueError( - 'Conflicting options: --enable-%s and --disable-%s' % - (x, x)) - if getattr(self, 'enable_%s' % x): - _dbg('Requiring %s', x) + "Conflicting options: --enable-{} and --disable-{}".format(x, x) + ) + if getattr(self, "enable_%s" % x): + _dbg("Requiring %s", x) self.feature.required.add(x) def build_extensions(self): @@ -292,34 +358,35 @@ class pil_build_ext(build_ext): _add_directory(include_dirs, "src/libImaging") pkg_config = None - if _cmd_exists('pkg-config'): + if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): pkg_config = _pkg_config # # add configured kits - for root_name, lib_name in dict(JPEG_ROOT="libjpeg", - JPEG2K_ROOT="libopenjp2", - TIFF_ROOT=("libtiff-5", "libtiff-4"), - ZLIB_ROOT="zlib", - FREETYPE_ROOT="freetype2", - LCMS_ROOT="lcms2", - IMAGEQUANT_ROOT="libimagequant" - ).items(): + for root_name, lib_name in dict( + JPEG_ROOT="libjpeg", + JPEG2K_ROOT="libopenjp2", + TIFF_ROOT=("libtiff-5", "libtiff-4"), + ZLIB_ROOT="zlib", + FREETYPE_ROOT="freetype2", + LCMS_ROOT="lcms2", + IMAGEQUANT_ROOT="libimagequant", + ).items(): root = globals()[root_name] if root is None and root_name in os.environ: prefix = os.environ[root_name] - root = (os.path.join(prefix, 'lib'), os.path.join(prefix, 'include')) + root = (os.path.join(prefix, "lib"), os.path.join(prefix, "include")) if root is None and pkg_config: if isinstance(lib_name, tuple): for lib_name2 in lib_name: - _dbg('Looking for `%s` using pkg-config.' % lib_name2) + _dbg("Looking for `%s` using pkg-config." % lib_name2) root = pkg_config(lib_name2) if root: break else: - _dbg('Looking for `%s` using pkg-config.' % lib_name) + _dbg("Looking for `%s` using pkg-config." % lib_name) root = pkg_config(lib_name) if isinstance(root, tuple): @@ -330,21 +397,21 @@ class pil_build_ext(build_ext): _add_directory(library_dirs, lib_root) _add_directory(include_dirs, include_root) - # respect CFLAGS/LDFLAGS - for k in ('CFLAGS', 'LDFLAGS'): + # respect CFLAGS/CPPFLAGS/LDFLAGS + for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): if k in os.environ: - for match in re.finditer(r'-I([^\s]+)', os.environ[k]): + for match in re.finditer(r"-I([^\s]+)", os.environ[k]): _add_directory(include_dirs, match.group(1)) - for match in re.finditer(r'-L([^\s]+)', os.environ[k]): + for match in re.finditer(r"-L([^\s]+)", os.environ[k]): _add_directory(library_dirs, match.group(1)) # include, rpath, if set as environment variables: - for k in ('C_INCLUDE_PATH', 'CPATH', 'INCLUDE'): + for k in ("C_INCLUDE_PATH", "CPATH", "INCLUDE"): if k in os.environ: for d in os.environ[k].split(os.path.pathsep): _add_directory(include_dirs, d) - for k in ('LD_RUN_PATH', 'LIBRARY_PATH', 'LIB'): + for k in ("LD_RUN_PATH", "LIBRARY_PATH", "LIB"): if k in os.environ: for d in os.environ[k].split(os.path.pathsep): _add_directory(library_dirs, d) @@ -362,9 +429,12 @@ class pil_build_ext(build_ext): elif sys.platform == "cygwin": # pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory - _add_directory(library_dirs, - os.path.join("/usr/lib", "python%s" % - sys.version[:3], "config")) + _add_directory( + library_dirs, + os.path.join( + "/usr/lib", "python{}.{}".format(*sys.version_info), "config" + ), + ) elif sys.platform == "darwin": # attempt to make sure we pick freetype2 over other versions @@ -379,8 +449,11 @@ class pil_build_ext(build_ext): # if Homebrew is installed, use its lib and include directories try: - prefix = subprocess.check_output(['brew', '--prefix']).strip( - ).decode('latin1') + prefix = ( + subprocess.check_output(["brew", "--prefix"]) + .strip() + .decode("latin1") + ) except Exception: # Homebrew not installed prefix = None @@ -389,28 +462,30 @@ class pil_build_ext(build_ext): if prefix: # add Homebrew's include and lib directories - _add_directory(library_dirs, os.path.join(prefix, 'lib')) - _add_directory(include_dirs, os.path.join(prefix, 'include')) - ft_prefix = os.path.join(prefix, 'opt', 'freetype') + _add_directory(library_dirs, os.path.join(prefix, "lib")) + _add_directory(include_dirs, os.path.join(prefix, "include")) + ft_prefix = os.path.join(prefix, "opt", "freetype") if ft_prefix and os.path.isdir(ft_prefix): # freetype might not be linked into Homebrew's prefix - _add_directory(library_dirs, os.path.join(ft_prefix, 'lib')) - _add_directory(include_dirs, - os.path.join(ft_prefix, 'include')) + _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) + _add_directory(include_dirs, os.path.join(ft_prefix, "include")) else: # fall back to freetype from XQuartz if # Homebrew's freetype is missing _add_directory(library_dirs, "/usr/X11/lib") _add_directory(include_dirs, "/usr/X11/include") - elif sys.platform.startswith("linux") or \ - sys.platform.startswith("gnu") or \ - sys.platform.startswith("freebsd"): + elif ( + sys.platform.startswith("linux") + or sys.platform.startswith("gnu") + or sys.platform.startswith("freebsd") + ): for dirname in _find_library_dirs_ldconfig(): _add_directory(library_dirs, dirname) - if sys.platform.startswith("linux") and \ - os.environ.get('ANDROID_ROOT', None): + if sys.platform.startswith("linux") and os.environ.get( + "ANDROID_ROOT", None + ): # termux support for android. # system libraries (zlib) are installed in /system/lib # headers are at $PREFIX/include @@ -439,25 +514,28 @@ class pil_build_ext(build_ext): # alpine, at least _add_directory(library_dirs, "/lib") - # on Windows, look for the OpenJPEG libraries in the location that - # the official installer puts them if sys.platform == "win32": - program_files = os.environ.get('ProgramFiles', '') + if PLATFORM_MINGW: + _add_directory( + include_dirs, "C:\\msys64\\mingw32\\include\\libimagequant" + ) + + # on Windows, look for the OpenJPEG libraries in the location that + # the official installer puts them + program_files = os.environ.get("ProgramFiles", "") best_version = (0, 0) best_path = None for name in os.listdir(program_files): - if name.startswith('OpenJPEG '): - version = tuple(int(x) for x in - name[9:].strip().split('.')) + if name.startswith("OpenJPEG "): + version = tuple(int(x) for x in name[9:].strip().split(".")) if version > best_version: best_version = version best_path = os.path.join(program_files, name) if best_path: - _dbg('Adding %s to search list', best_path) - _add_directory(library_dirs, os.path.join(best_path, 'lib')) - _add_directory(include_dirs, - os.path.join(best_path, 'include')) + _dbg("Adding %s to search list", best_path) + _add_directory(library_dirs, os.path.join(best_path, "lib")) + _add_directory(include_dirs, os.path.join(best_path, "include")) # # insert new dirs *before* default libs, to avoid conflicts @@ -471,51 +549,51 @@ class pil_build_ext(build_ext): feature = self.feature - if feature.want('zlib'): - _dbg('Looking for zlib') + if feature.want("zlib"): + _dbg("Looking for zlib") if _find_include_file(self, "zlib.h"): if _find_library_file(self, "z"): feature.zlib = "z" - elif (sys.platform == "win32" and - _find_library_file(self, "zlib")): + elif sys.platform == "win32" and _find_library_file(self, "zlib"): feature.zlib = "zlib" # alternative name - if feature.want('jpeg'): - _dbg('Looking for jpeg') + if feature.want("jpeg"): + _dbg("Looking for jpeg") if _find_include_file(self, "jpeglib.h"): if _find_library_file(self, "jpeg"): feature.jpeg = "jpeg" - elif (sys.platform == "win32" and - _find_library_file(self, "libjpeg")): + elif sys.platform == "win32" and _find_library_file(self, "libjpeg"): feature.jpeg = "libjpeg" # alternative name feature.openjpeg_version = None - if feature.want('jpeg2000'): - _dbg('Looking for jpeg2000') + if feature.want("jpeg2000"): + _dbg("Looking for jpeg2000") best_version = None best_path = None # Find the best version for directory in self.compiler.include_dirs: - _dbg('Checking for openjpeg-#.# in %s', directory) + _dbg("Checking for openjpeg-#.# in %s", directory) try: listdir = os.listdir(directory) except Exception: # WindowsError, FileNotFoundError continue for name in listdir: - if name.startswith('openjpeg-') and \ - os.path.isfile(os.path.join(directory, name, - 'openjpeg.h')): - _dbg('Found openjpeg.h in %s/%s', (directory, name)) - version = tuple(int(x) for x in name[9:].split('.')) + if name.startswith("openjpeg-") and os.path.isfile( + os.path.join(directory, name, "openjpeg.h") + ): + _dbg("Found openjpeg.h in %s/%s", (directory, name)) + version = tuple(int(x) for x in name[9:].split(".")) if best_version is None or version > best_version: best_version = version best_path = os.path.join(directory, name) - _dbg('Best openjpeg version %s so far in %s', - (best_version, best_path)) + _dbg( + "Best openjpeg version %s so far in %s", + (best_version, best_path), + ) - if best_version and _find_library_file(self, 'openjp2'): + if best_version and _find_library_file(self, "openjp2"): # Add the directory to the include path so we can include # rather than having to cope with the versioned # include path @@ -524,45 +602,43 @@ class pil_build_ext(build_ext): # self.compiler.include_dirs. Should investigate how that is # possible. _add_directory(self.compiler.include_dirs, best_path, 0) - feature.jpeg2000 = 'openjp2' - feature.openjpeg_version = '.'.join(str(x) for x in - best_version) + feature.jpeg2000 = "openjp2" + feature.openjpeg_version = ".".join(str(x) for x in best_version) - if feature.want('imagequant'): - _dbg('Looking for imagequant') - if _find_include_file(self, 'libimagequant.h'): + if feature.want("imagequant"): + _dbg("Looking for imagequant") + if _find_include_file(self, "libimagequant.h"): if _find_library_file(self, "imagequant"): feature.imagequant = "imagequant" elif _find_library_file(self, "libimagequant"): feature.imagequant = "libimagequant" - if feature.want('tiff'): - _dbg('Looking for tiff') - if _find_include_file(self, 'tiff.h'): + if feature.want("tiff"): + _dbg("Looking for tiff") + if _find_include_file(self, "tiff.h"): if _find_library_file(self, "tiff"): feature.tiff = "tiff" - if (sys.platform in ["win32", "darwin"] and - _find_library_file(self, "libtiff")): + if sys.platform in ["win32", "darwin"] and _find_library_file( + self, "libtiff" + ): feature.tiff = "libtiff" - if feature.want('freetype'): - _dbg('Looking for freetype') + if feature.want("freetype"): + _dbg("Looking for freetype") if _find_library_file(self, "freetype"): # look for freetype2 include files freetype_version = 0 for subdir in self.compiler.include_dirs: - _dbg('Checking for include file %s in %s', - ("ft2build.h", subdir)) + _dbg("Checking for include file %s in %s", ("ft2build.h", subdir)) if os.path.isfile(os.path.join(subdir, "ft2build.h")): - _dbg('Found %s in %s', ("ft2build.h", subdir)) + _dbg("Found %s in %s", ("ft2build.h", subdir)) freetype_version = 21 subdir = os.path.join(subdir, "freetype2") break subdir = os.path.join(subdir, "freetype2") - _dbg('Checking for include file %s in %s', - ("ft2build.h", subdir)) + _dbg("Checking for include file %s in %s", ("ft2build.h", subdir)) if os.path.isfile(os.path.join(subdir, "ft2build.h")): - _dbg('Found %s in %s', ("ft2build.h", subdir)) + _dbg("Found %s in %s", ("ft2build.h", subdir)) freetype_version = 21 break if freetype_version: @@ -570,8 +646,8 @@ class pil_build_ext(build_ext): if subdir: _add_directory(self.compiler.include_dirs, subdir, 0) - if feature.want('lcms'): - _dbg('Looking for lcms') + if feature.want("lcms"): + _dbg("Looking for lcms") if _find_include_file(self, "lcms2.h"): if _find_library_file(self, "lcms2"): feature.lcms = "lcms2" @@ -579,30 +655,34 @@ class pil_build_ext(build_ext): # alternate Windows name. feature.lcms = "lcms2_static" - if feature.want('webp'): - _dbg('Looking for webp') - if (_find_include_file(self, "webp/encode.h") and - _find_include_file(self, "webp/decode.h")): + if feature.want("webp"): + _dbg("Looking for webp") + if _find_include_file(self, "webp/encode.h") and _find_include_file( + self, "webp/decode.h" + ): # In Google's precompiled zip it is call "libwebp": if _find_library_file(self, "webp"): feature.webp = "webp" elif _find_library_file(self, "libwebp"): feature.webp = "libwebp" - if feature.want('webpmux'): - _dbg('Looking for webpmux') - if (_find_include_file(self, "webp/mux.h") and - _find_include_file(self, "webp/demux.h")): - if (_find_library_file(self, "webpmux") and - _find_library_file(self, "webpdemux")): + if feature.want("webpmux"): + _dbg("Looking for webpmux") + if _find_include_file(self, "webp/mux.h") and _find_include_file( + self, "webp/demux.h" + ): + if _find_library_file(self, "webpmux") and _find_library_file( + self, "webpdemux" + ): feature.webpmux = "webpmux" - if (_find_library_file(self, "libwebpmux") and - _find_library_file(self, "libwebpdemux")): + if _find_library_file(self, "libwebpmux") and _find_library_file( + self, "libwebpdemux" + ): feature.webpmux = "libwebpmux" for f in feature: if not getattr(feature, f) and feature.require(f): - if f in ('jpeg', 'zlib'): + if f in ("jpeg", "zlib"): raise RequiredDependencyException(f) raise DependencyException(f) @@ -636,7 +716,7 @@ class pil_build_ext(build_ext): defs.append(("HAVE_LIBTIFF", None)) if sys.platform == "win32": libs.extend(["kernel32", "user32", "gdi32"]) - if struct.unpack("h", "\0\1".encode('ascii'))[0] == 1: + if struct.unpack("h", b"\0\1")[0] == 1: defs.append(("WORDS_BIGENDIAN", None)) if sys.platform == "win32" and not (PLATFORM_PYPY or PLATFORM_MINGW): @@ -644,10 +724,7 @@ class pil_build_ext(build_ext): else: defs.append(("PILLOW_VERSION", '"%s"' % PILLOW_VERSION)) - exts = [(Extension("PIL._imaging", - files, - libraries=libs, - define_macros=defs))] + exts = [(Extension("PIL._imaging", files, libraries=libs, define_macros=defs))] # # additional libraries @@ -655,17 +732,26 @@ class pil_build_ext(build_ext): if feature.freetype: libs = ["freetype"] defs = [] - exts.append(Extension( - "PIL._imagingft", ["src/_imagingft.c"], libraries=libs, - define_macros=defs)) + exts.append( + Extension( + "PIL._imagingft", + ["src/_imagingft.c"], + libraries=libs, + define_macros=defs, + ) + ) if feature.lcms: extra = [] if sys.platform == "win32": extra.extend(["user32", "gdi32"]) - exts.append(Extension("PIL._imagingcms", - ["src/_imagingcms.c"], - libraries=[feature.lcms] + extra)) + exts.append( + Extension( + "PIL._imagingcms", + ["src/_imagingcms.c"], + libraries=[feature.lcms] + extra, + ) + ) if feature.webp: libs = [feature.webp] @@ -674,18 +760,23 @@ class pil_build_ext(build_ext): if feature.webpmux: defs.append(("HAVE_WEBPMUX", None)) libs.append(feature.webpmux) - libs.append(feature.webpmux.replace('pmux', 'pdemux')) + libs.append(feature.webpmux.replace("pmux", "pdemux")) - exts.append(Extension("PIL._webp", - ["src/_webp.c"], - libraries=libs, - define_macros=defs)) + exts.append( + Extension( + "PIL._webp", ["src/_webp.c"], libraries=libs, define_macros=defs + ) + ) - tk_libs = ['psapi'] if sys.platform == 'win32' else [] - exts.append(Extension("PIL._imagingtk", - ["src/_imagingtk.c", "src/Tk/tkImaging.c"], - include_dirs=['src/Tk'], - libraries=tk_libs)) + tk_libs = ["psapi"] if sys.platform == "win32" else [] + exts.append( + Extension( + "PIL._imagingtk", + ["src/_imagingtk.c", "src/Tk/tkImaging.c"], + include_dirs=["src/Tk"], + libraries=tk_libs, + ) + ) exts.append(Extension("PIL._imagingmath", ["src/_imagingmath.c"])) exts.append(Extension("PIL._imagingmorph", ["src/_imagingmorph.c"])) @@ -706,15 +797,14 @@ class pil_build_ext(build_ext): print("-" * 68) print("version Pillow %s" % PILLOW_VERSION) v = sys.version.split("[") - print("platform %s %s" % (sys.platform, v[0].strip())) + print("platform {} {}".format(sys.platform, v[0].strip())) for v in v[1:]: print(" [%s" % v.strip()) print("-" * 68) options = [ (feature.jpeg, "JPEG"), - (feature.jpeg2000, "OPENJPEG (JPEG2000)", - feature.openjpeg_version), + (feature.jpeg2000, "OPENJPEG (JPEG2000)", feature.openjpeg_version), (feature.zlib, "ZLIB (PNG/ZIP)"), (feature.imagequant, "LIBIMAGEQUANT"), (feature.tiff, "LIBTIFF"), @@ -727,10 +817,10 @@ class pil_build_ext(build_ext): all = 1 for option in options: if option[0]: - version = '' + version = "" if len(option) >= 3 and option[2]: - version = ' (%s)' % option[2] - print("--- %s support available%s" % (option[1], version)) + version = " (%s)" % option[2] + print("--- {} support available{}".format(option[1], version)) else: print("*** %s support not available" % option[1]) all = 0 @@ -740,8 +830,10 @@ class pil_build_ext(build_ext): if not all: print("To add a missing option, make sure you have the required") print("library and headers.") - print("See https://pillow.readthedocs.io/en/latest/installation." - "html#building-from-source") + print( + "See https://pillow.readthedocs.io/en/latest/installation." + "html#building-from-source" + ) print("") print("To check the build, run the selftest.py script.") @@ -749,48 +841,55 @@ class pil_build_ext(build_ext): def debug_build(): - return hasattr(sys, 'gettotalrefcount') + return hasattr(sys, "gettotalrefcount") -needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv) -pytest_runner = ['pytest-runner'] if needs_pytest else [] +needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv) +pytest_runner = ["pytest-runner"] if needs_pytest else [] try: - setup(name=NAME, - version=PILLOW_VERSION, - description='Python Imaging Library (Fork)', - long_description=_read('README.rst').decode('utf-8'), - author='Alex Clark (Fork Author)', - author_email='aclark@aclark.net', - url='http://python-pillow.org', - classifiers=[ - "Development Status :: 6 - Mature", - "Topic :: Multimedia :: Graphics", - "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", - "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", - "Topic :: Multimedia :: Graphics :: Graphics Conversion", - "Topic :: Multimedia :: Graphics :: Viewers", - "License :: Other/Proprietary License", - "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 :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - ], - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", - cmdclass={"build_ext": pil_build_ext}, - ext_modules=[Extension("PIL._imaging", ["_imaging.c"])], - include_package_data=True, - setup_requires=pytest_runner, - tests_require=['pytest'], - packages=["PIL"], - package_dir={'': 'src'}, - keywords=["Imaging", ], - license='Standard PIL License', - zip_safe=not (debug_build() or PLATFORM_MINGW), ) + setup( + name=NAME, + version=PILLOW_VERSION, + description="Python Imaging Library (Fork)", + long_description=_read("README.rst").decode("utf-8"), + license="HPND", + author="Alex Clark (PIL Fork Author)", + author_email="aclark@python-pillow.org", + url="https://python-pillow.org", + project_urls={ + "Documentation": "https://pillow.readthedocs.io", + "Source": "https://github.com/python-pillow/Pillow", + "Funding": "https://tidelift.com/subscription/pkg/pypi-pillow", + }, + classifiers=[ + "Development Status :: 6 - Mature", + "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", # noqa: E501 + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Multimedia :: Graphics", + "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", + "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", + "Topic :: Multimedia :: Graphics :: Graphics Conversion", + "Topic :: Multimedia :: Graphics :: Viewers", + ], + python_requires=">=3.5", + cmdclass={"build_ext": pil_build_ext}, + ext_modules=[Extension("PIL._imaging", ["_imaging.c"])], + include_package_data=True, + setup_requires=pytest_runner, + tests_require=["pytest"], + packages=["PIL"], + package_dir={"": "src"}, + keywords=["Imaging"], + zip_safe=not (debug_build() or PLATFORM_MINGW), + ) except RequiredDependencyException as err: msg = """ @@ -800,7 +899,9 @@ a required dependency when compiling Pillow from source. Please see the install instructions at: https://pillow.readthedocs.io/en/latest/installation.html -""" % (str(err)) +""" % ( + str(err) + ) sys.stderr.write(msg) raise RequiredDependencyException(msg) except DependencyException as err: @@ -809,6 +910,9 @@ except DependencyException as err: The headers or library files could not be found for %s, which was requested by the option flag --enable-%s -""" % (str(err), str(err)) +""" % ( + str(err), + str(err), + ) sys.stderr.write(msg) raise DependencyException(msg) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index eac19bde1..7a485cf80 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -17,10 +17,8 @@ # See the README file for information on usage and redistribution. # -from __future__ import print_function - -from . import Image, FontFile +from . import FontFile, Image # -------------------------------------------------------------------- # parse X Bitmap Distribution Format (BDF) @@ -32,14 +30,10 @@ bdf_slant = { "O": "Oblique", "RI": "Reverse Italic", "RO": "Reverse Oblique", - "OT": "Other" + "OT": "Other", } -bdf_spacing = { - "P": "Proportional", - "M": "Monospaced", - "C": "Cell" -} +bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"} def bdf_char(f): @@ -50,7 +44,7 @@ def bdf_char(f): return None if s[:9] == b"STARTCHAR": break - id = s[9:].strip().decode('ascii') + id = s[9:].strip().decode("ascii") # load symbol properties props = {} @@ -59,7 +53,7 @@ def bdf_char(f): if not s or s[:6] == b"BITMAP": break i = s.find(b" ") - props[s[:i].decode('ascii')] = s[i+1:-1].decode('ascii') + props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") # load bitmap bitmap = [] @@ -73,7 +67,7 @@ def bdf_char(f): [x, y, l, d] = [int(p) for p in props["BBX"].split()] [dx, dy] = [int(p) for p in props["DWIDTH"].split()] - bbox = (dx, dy), (l, -d-y, x+l, -d), (0, 0, x, y) + bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y) try: im = Image.frombytes("1", (x, y), bitmap, "hex", "1") @@ -87,11 +81,10 @@ def bdf_char(f): ## # Font file plugin for the X11 BDF format. + class BdfFontFile(FontFile.FontFile): - def __init__(self, fp): - - FontFile.FontFile.__init__(self) + super().__init__() s = fp.readline() if s[:13] != b"STARTFONT 2.1": @@ -105,10 +98,10 @@ class BdfFontFile(FontFile.FontFile): if not s or s[:13] == b"ENDPROPERTIES": break i = s.find(b" ") - props[s[:i].decode('ascii')] = s[i+1:-1].decode('ascii') + props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") if s[:i] in [b"COMMENT", b"COPYRIGHT"]: if s.find(b"LogicalFontDescription") < 0: - comments.append(s[i+1:-1].decode('ascii')) + comments.append(s[i + 1 : -1].decode("ascii")) while True: c = bdf_char(fp) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 398e0fa0c..5ccba37db 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -34,7 +34,6 @@ from io import BytesIO from . import Image, ImageFile - BLP_FORMAT_JPEG = 0 BLP_ENCODING_UNCOMPRESSED = 1 @@ -47,11 +46,7 @@ BLP_ALPHA_ENCODING_DXT5 = 7 def unpack_565(i): - return ( - ((i >> 11) & 0x1f) << 3, - ((i >> 5) & 0x3f) << 2, - (i & 0x1f) << 3 - ) + return (((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3) def decode_dxt1(data, alpha=False): @@ -119,12 +114,12 @@ def decode_dxt3(data): for block in range(blocks): idx = block * 16 - block = data[idx:idx + 16] + block = data[idx : idx + 16] # Decode next 16-byte block. bits = struct.unpack_from("<8B", block) color0, color1 = struct.unpack_from(">= 4 else: high = True - a &= 0xf + a &= 0xF a *= 17 # We get a value between 0 and 15 color_code = (code >> 2 * (4 * j + i)) & 0x03 @@ -172,19 +167,17 @@ def decode_dxt5(data): for block in range(blocks): idx = block * 16 - block = data[idx:idx + 16] + block = data[idx : idx + 16] # Decode next 16-byte block. a0, a1 = struct.unpack_from("= 40: # v3 and OS/2 - file_info['y_flip'] = i8(header_data[7]) == 0xff - file_info['direction'] = 1 if file_info['y_flip'] else -1 - file_info['width'] = i32(header_data[0:4]) - file_info['height'] = (i32(header_data[4:8]) - if not file_info['y_flip'] - else 2**32 - i32(header_data[4:8])) - file_info['planes'] = i16(header_data[8:10]) - file_info['bits'] = i16(header_data[10:12]) - file_info['compression'] = i32(header_data[12:16]) - # byte size of pixel data - file_info['data_size'] = i32(header_data[16:20]) - file_info['pixels_per_meter'] = (i32(header_data[20:24]), - i32(header_data[24:28])) - file_info['colors'] = i32(header_data[28:32]) - file_info['palette_padding'] = 4 - self.info["dpi"] = tuple( - map(lambda x: int(math.ceil(x / 39.3701)), - file_info['pixels_per_meter'])) - if file_info['compression'] == self.BITFIELDS: - if len(header_data) >= 52: - for idx, mask in enumerate(['r_mask', - 'g_mask', - 'b_mask', - 'a_mask']): - file_info[mask] = i32( - header_data[36 + idx * 4:40 + idx * 4] - ) - else: - # 40 byte headers only have the three components in the - # bitfields masks, ref: - # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx - # See also - # https://github.com/python-pillow/Pillow/issues/1293 - # There is a 4th component in the RGBQuad, in the alpha - # location, but it is listed as a reserved component, - # and it is not generally an alpha channel - file_info['a_mask'] = 0x0 - for mask in ['r_mask', 'g_mask', 'b_mask']: - file_info[mask] = i32(read(4)) - file_info['rgb_mask'] = (file_info['r_mask'], - file_info['g_mask'], - file_info['b_mask']) - file_info['rgba_mask'] = (file_info['r_mask'], - file_info['g_mask'], - file_info['b_mask'], - file_info['a_mask']) + elif file_info["header_size"] in (40, 64, 108, 124): + file_info["y_flip"] = i8(header_data[7]) == 0xFF + file_info["direction"] = 1 if file_info["y_flip"] else -1 + file_info["width"] = i32(header_data[0:4]) + file_info["height"] = ( + i32(header_data[4:8]) + if not file_info["y_flip"] + else 2 ** 32 - i32(header_data[4:8]) + ) + file_info["planes"] = i16(header_data[8:10]) + file_info["bits"] = i16(header_data[10:12]) + file_info["compression"] = i32(header_data[12:16]) + # byte size of pixel data + file_info["data_size"] = i32(header_data[16:20]) + file_info["pixels_per_meter"] = ( + i32(header_data[20:24]), + i32(header_data[24:28]), + ) + file_info["colors"] = i32(header_data[28:32]) + file_info["palette_padding"] = 4 + self.info["dpi"] = tuple( + int(x / 39.3701 + 0.5) for x in file_info["pixels_per_meter"] + ) + if file_info["compression"] == self.BITFIELDS: + if len(header_data) >= 52: + for idx, mask in enumerate( + ["r_mask", "g_mask", "b_mask", "a_mask"] + ): + file_info[mask] = i32(header_data[36 + idx * 4 : 40 + idx * 4]) + else: + # 40 byte headers only have the three components in the + # bitfields masks, ref: + # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx + # See also + # https://github.com/python-pillow/Pillow/issues/1293 + # There is a 4th component in the RGBQuad, in the alpha + # location, but it is listed as a reserved component, + # and it is not generally an alpha channel + file_info["a_mask"] = 0x0 + for mask in ["r_mask", "g_mask", "b_mask"]: + file_info[mask] = i32(read(4)) + file_info["rgb_mask"] = ( + file_info["r_mask"], + file_info["g_mask"], + file_info["b_mask"], + ) + file_info["rgba_mask"] = ( + file_info["r_mask"], + file_info["g_mask"], + file_info["b_mask"], + 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 - self._size = file_info['width'], file_info['height'] + self._size = file_info["width"], file_info["height"] # ------- If color count was not found in the header, compute from bits - file_info["colors"] = (file_info["colors"] - if file_info.get("colors", 0) - else (1 << file_info["bits"])) + file_info["colors"] = ( + file_info["colors"] + if file_info.get("colors", 0) + else (1 << file_info["bits"]) + ) # ------------------------------- Check abnormal values for DOS attacks - if file_info['width'] * file_info['height'] > 2**31: - raise IOError("Unsupported BMP Size: (%dx%d)" % self.size) + if file_info["width"] * file_info["height"] > 2 ** 31: + raise OSError("Unsupported BMP Size: (%dx%d)" % self.size) # ---------------------- Check bit depth for unusual unsupported values - self.mode, raw_mode = BIT2MODE.get(file_info['bits'], (None, None)) + 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: + if file_info["compression"] == self.BITFIELDS: SUPPORTED = { - 32: [(0xff0000, 0xff00, 0xff, 0x0), - (0xff0000, 0xff00, 0xff, 0xff000000), - (0x0, 0x0, 0x0, 0x0), - (0xff000000, 0xff0000, 0xff00, 0x0)], - 24: [(0xff0000, 0xff00, 0xff)], - 16: [(0xf800, 0x7e0, 0x1f), (0x7c00, 0x3e0, 0x1f)] + 32: [ + (0xFF0000, 0xFF00, 0xFF, 0x0), + (0xFF0000, 0xFF00, 0xFF, 0xFF000000), + (0xFF, 0xFF00, 0xFF0000, 0xFF000000), + (0x0, 0x0, 0x0, 0x0), + (0xFF000000, 0xFF0000, 0xFF00, 0x0), + ], + 24: [(0xFF0000, 0xFF00, 0xFF)], + 16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)], } MASK_MODES = { - (32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX", - (32, (0xff000000, 0xff0000, 0xff00, 0x0)): "XBGR", - (32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA", + (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", + (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", + (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", + (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", - (24, (0xff0000, 0xff00, 0xff)): "BGR", - (16, (0xf800, 0x7e0, 0x1f)): "BGR;16", - (16, (0x7c00, 0x3e0, 0x1f)): "BGR;15" + (24, (0xFF0000, 0xFF00, 0xFF)): "BGR", + (16, (0xF800, 0x7E0, 0x1F)): "BGR;16", + (16, (0x7C00, 0x3E0, 0x1F)): "BGR;15", } - if file_info['bits'] in SUPPORTED: - if file_info['bits'] == 32 and \ - file_info['rgba_mask'] in SUPPORTED[file_info['bits']]: - raw_mode = MASK_MODES[ - (file_info["bits"], file_info["rgba_mask"]) - ] - self.mode = "RGBA" if raw_mode in ("BGRA",) else self.mode - elif (file_info['bits'] in (24, 16) and - file_info['rgb_mask'] in SUPPORTED[file_info['bits']]): - raw_mode = MASK_MODES[ - (file_info['bits'], file_info['rgb_mask']) - ] + if file_info["bits"] in SUPPORTED: + if ( + file_info["bits"] == 32 + and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] + ): + raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])] + self.mode = "RGBA" if "A" in raw_mode else self.mode + elif ( + file_info["bits"] in (24, 16) + and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]] + ): + 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") - elif file_info['compression'] == self.RAW: - if file_info['bits'] == 32 and header == 22: # 32-bit .cur offset + 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']) + if not (0 < file_info["colors"] <= 65536): + raise OSError("Unsupported BMP Palette size (%d)" % file_info["colors"]) else: - padding = file_info['palette_padding'] - palette = read(padding * file_info['colors']) + padding = file_info["palette_padding"] + palette = read(padding * file_info["colors"]) greyscale = True - indices = (0, 255) if file_info['colors'] == 2 else \ - list(range(file_info['colors'])) + indices = ( + (0, 255) + if file_info["colors"] == 2 + else list(range(file_info["colors"])) + ) # ----------------- Check if greyscale and ignore palette if so for ind, val in enumerate(indices): - rgb = palette[ind*padding:ind*padding + 3] + rgb = palette[ind * padding : ind * padding + 3] if rgb != o8(val) * 3: greyscale = False # ------- If all colors are grey, white or black, ditch palette if greyscale: - self.mode = "1" if file_info['colors'] == 2 else "L" + self.mode = "1" if file_info["colors"] == 2 else "L" raw_mode = self.mode else: self.mode = "P" self.palette = ImagePalette.raw( - "BGRX" if padding == 4 else "BGR", palette) + "BGRX" if padding == 4 else "BGR", palette + ) # ---------------------------- Finally set the tile data for the plugin - self.info['compression'] = file_info['compression'] + self.info["compression"] = file_info["compression"] self.tile = [ - ('raw', - (0, 0, file_info['width'], file_info['height']), - offset or self.fp.tell(), - (raw_mode, - ((file_info['width'] * file_info['bits'] + 31) >> 3) & (~3), - file_info['direction'])) + ( + "raw", + (0, 0, file_info["width"], file_info["height"]), + offset or self.fp.tell(), + ( + raw_mode, + ((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3), + file_info["direction"], + ), + ) ] def _open(self): @@ -277,6 +282,7 @@ class DibImageFile(BmpImageFile): def _open(self): self._bitmap() + # # -------------------------------------------------------------------- # Write BMP file @@ -291,43 +297,53 @@ SAVE = { } -def _save(im, fp, filename): +def _dib_save(im, fp, filename): + _save(im, fp, filename, False) + + +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 dpi = info.get("dpi", (96, 96)) # 1 meter == 39.3701 inches - ppm = tuple(map(lambda x: int(x * 39.3701), dpi)) + ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi)) - stride = ((im.size[0]*bits+7)//8+3) & (~3) + stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3) header = 40 # or 64 for OS/2 version 2 - offset = 14 + header + colors * 4 image = stride * im.size[1] # bitmap header - fp.write(b"BM" + # file type (magic) - o32(offset+image) + # file size - o32(0) + # reserved - o32(offset)) # image data offset + if bitmap_header: + offset = 14 + header + colors * 4 + fp.write( + b"BM" + + o32(offset + image) # file type (magic) + + o32(0) # file size + + o32(offset) # reserved + ) # image data offset # bitmap info header - fp.write(o32(header) + # info header size - o32(im.size[0]) + # width - o32(im.size[1]) + # height - o16(1) + # planes - o16(bits) + # depth - o32(0) + # compression (0=uncompressed) - o32(image) + # size of bitmap - o32(ppm[0]) + o32(ppm[1]) + # resolution - o32(colors) + # colors used - o32(colors)) # colors important + fp.write( + o32(header) # info header size + + o32(im.size[0]) # width + + o32(im.size[1]) # height + + o16(1) # planes + + o16(bits) # depth + + o32(0) # compression (0=uncompressed) + + o32(image) # size of bitmap + + o32(ppm[0]) # resolution + + o32(ppm[1]) # resolution + + o32(colors) # colors used + + o32(colors) # colors important + ) - fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) + fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) if im.mode == "1": for i in (0, 255): @@ -338,8 +354,8 @@ def _save(im, fp, filename): elif im.mode == "P": fp.write(im.im.getpalette("RGB", "BGRX")) - ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, - (rawmode, stride, -1))]) + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) + # # -------------------------------------------------------------------- @@ -352,3 +368,10 @@ Image.register_save(BmpImageFile.format, _save) Image.register_extension(BmpImageFile.format, ".bmp") Image.register_mime(BmpImageFile.format, "image/bmp") + +Image.register_open(DibImageFile.format, DibImageFile, _dib_accept) +Image.register_save(DibImageFile.format, _dib_save) + +Image.register_extension(DibImageFile.format, ".dib") + +Image.register_mime(DibImageFile.format, "image/bmp") diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index a1957b32a..48f21e1b3 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -27,6 +27,7 @@ def register_handler(handler): # -------------------------------------------------------------------- # Image adapter + def _accept(prefix): return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" @@ -59,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 682ad9031..9727601ab 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -18,9 +18,10 @@ # A file object that provides read access to a part of an existing # file (for example a TAR file). +import io -class ContainerIO(object): +class ContainerIO: def __init__(self, file, offset, length): """ Create file object. @@ -39,9 +40,9 @@ class ContainerIO(object): # Always false. def isatty(self): - return 0 + return False - def seek(self, offset, mode=0): + def seek(self, offset, mode=io.SEEK_SET): """ Move file pointer. diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index e0a5fae62..3a1b6d2e5 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -15,16 +15,9 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - -from . import Image, BmpImagePlugin +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" - # # -------------------------------------------------------------------- @@ -36,6 +29,7 @@ def _accept(prefix): ## # Image plugin for Windows Cursor files. + class CurImageFile(BmpImagePlugin.BmpImageFile): format = "CUR" @@ -65,9 +59,9 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): self._bitmap(i32(m[12:]) + offset) # patch up the bitmap height - self._size = self.size[0], self.size[1]//2 + self._size = self.size[0], self.size[1] // 2 d, e, o, a = self.tile[0] - self.tile[0] = d, (0, 0)+self.size, o, a + self.tile[0] = d, (0, 0) + self.size, o, a return diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index 3c8c2bc8a..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? @@ -39,6 +35,7 @@ def _accept(prefix): ## # Image plugin for the Intel DCX format. + class DcxImageFile(PcxImageFile): format = "DCX" diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index edc1b7aa3..9ba6e0ff8 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -12,8 +12,8 @@ Full text of the CC0 license: import struct from io import BytesIO -from . import Image, ImageFile +from . import Image, ImageFile # Magic ("DDS ") DDS_MAGIC = 0x20534444 @@ -61,8 +61,7 @@ DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS DDS_ALPHA = DDPF_ALPHA DDS_PAL8 = DDPF_PALETTEINDEXED8 -DDS_HEADER_FLAGS_TEXTURE = (DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | - DDSD_PIXELFORMAT) +DDS_HEADER_FLAGS_TEXTURE = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH DDS_HEADER_FLAGS_PITCH = DDSD_PITCH @@ -107,10 +106,10 @@ class DdsImageFile(ImageFile.ImageFile): def _open(self): magic, header_size = struct.unpack(" 0: - s = fp.read(min(lengthfile, 100*1024)) + s = fp.read(min(lengthfile, 100 * 1024)) if not s: break lengthfile -= len(s) f.write(s) # Build Ghostscript command - command = ["gs", - "-q", # quiet mode - "-g%dx%d" % size, # set output geometry (pixels) - "-r%fx%f" % res, # set input DPI (dots per inch) - "-dBATCH", # exit after processing - "-dNOPAUSE", # don't pause between pages - "-dSAFER", # safe mode - "-sDEVICE=ppmraw", # ppm driver - "-sOutputFile=%s" % outfile, # output file - # adjust for image origin - "-c", "%d %d translate" % (-bbox[0], -bbox[1]), - "-f", infile, # input file - # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272) - "-c", "showpage", - ] + command = [ + "gs", + "-q", # quiet mode + "-g%dx%d" % size, # set output geometry (pixels) + "-r%fx%f" % res, # set input DPI (dots per inch) + "-dBATCH", # exit after processing + "-dNOPAUSE", # don't pause between pages + "-dSAFER", # safe mode + "-sDEVICE=ppmraw", # ppm driver + "-sOutputFile=%s" % outfile, # output file + # adjust for image origin + "-c", + "%d %d translate" % (-bbox[0], -bbox[1]), + "-f", + infile, # input file + # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272) + "-c", + "showpage", + ] if gs_windows_binary is not None: if not gs_windows_binary: - raise WindowsError('Unable to locate Ghostscript on paths') + raise OSError("Unable to locate Ghostscript on paths") command[0] = gs_windows_binary # push data through Ghostscript try: - with open(os.devnull, 'w+b') as devnull: - startupinfo = None - if sys.platform.startswith('win'): - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - subprocess.check_call(command, stdout=devnull, - startupinfo=startupinfo) + startupinfo = None + if sys.platform.startswith("win"): + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + subprocess.check_call(command, startupinfo=startupinfo) im = Image.open(outfile) im.load() finally: @@ -161,15 +154,16 @@ def Ghostscript(tile, size, fp, scale=1): return im.im.copy() -class PSFile(object): +class PSFile: """ Wrapper for bytesio object that treats either CR or LF as end of line. """ + def __init__(self, fp): self.fp = fp self.char = None - def seek(self, offset, whence=0): + def seek(self, offset, whence=io.SEEK_SET): self.char = None self.fp.seek(offset, whence) @@ -187,12 +181,12 @@ class PSFile(object): if self.char in b"\r\n": self.char = None - return s.decode('latin-1') + return s.decode("latin-1") def _accept(prefix): - return prefix[:4] == b"%!PS" or \ - (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) + return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) + ## # Image plugin for Encapsulated Postscript. This plugin supports only @@ -226,7 +220,7 @@ class EpsImageFile(ImageFile.ImageFile): # Load EPS header s_raw = fp.readline() - s = s_raw.strip('\r\n') + s = s_raw.strip("\r\n") while s_raw: if s: @@ -248,8 +242,9 @@ class EpsImageFile(ImageFile.ImageFile): # put floating point values there anyway. box = [int(float(i)) for i in v.split()] self._size = box[2] - box[0], box[3] - box[1] - self.tile = [("eps", (0, 0) + self.size, offset, - (length, box))] + self.tile = [ + ("eps", (0, 0) + self.size, offset, (length, box)) + ] except Exception: pass @@ -264,15 +259,15 @@ class EpsImageFile(ImageFile.ImageFile): self.info[k[:8]] = k[9:] else: self.info[k] = "" - elif s[0] == '%': + elif s[0] == "%": # handle non-DSC Postscript comments that some # tools mistakenly put in the Comments section pass else: - raise IOError("bad EPS header") + raise OSError("bad EPS header") s_raw = fp.readline() - s = s_raw.strip('\r\n') + s = s_raw.strip("\r\n") if s and s[:1] != "%": break @@ -299,12 +294,12 @@ class EpsImageFile(ImageFile.ImageFile): self._size = int(x), int(y) return - s = fp.readline().strip('\r\n') + s = fp.readline().strip("\r\n") if not s: break if not box: - raise IOError("cannot determine EPS bounding box") + raise OSError("cannot determine EPS bounding box") def _find_offset(self, fp): @@ -312,7 +307,7 @@ class EpsImageFile(ImageFile.ImageFile): if s[:4] == b"%!PS": # for HEAD without binary preview - fp.seek(0, 2) + fp.seek(0, io.SEEK_END) length = fp.tell() offset = 0 elif i32(s[0:4]) == 0xC6D3D0C5: @@ -346,6 +341,7 @@ class EpsImageFile(ImageFile.ImageFile): # # -------------------------------------------------------------------- + def _save(im, fp, filename, eps=1): """EPS Writer for the Python Imaging Library.""" @@ -367,9 +363,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: @@ -383,7 +378,7 @@ def _save(im, fp, filename, eps=1): fp.write("%%EndComments\n") fp.write("%%Page: 1 1\n") fp.write("%%ImageData: %d %d " % im.size) - fp.write("%d %d 0 1 1 \"%s\"\n" % operator) + fp.write('%d %d 0 1 1 "%s"\n' % operator) # # image header @@ -398,7 +393,7 @@ def _save(im, fp, filename, eps=1): if hasattr(fp, "flush"): fp.flush() - ImageFile._save(im, base_fp, [("eps", (0, 0)+im.size, 0, None)]) + ImageFile._save(im, base_fp, [("eps", (0, 0) + im.size, 0, None)]) fp.write("\n%%%%EndBinary\n") fp.write("grestore end\n") @@ -408,6 +403,7 @@ def _save(im, fp, filename, eps=1): if wrapped_fp: fp.detach() + # # -------------------------------------------------------------------- diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index a8ad26bcc..47a981e0f 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -18,11 +18,10 @@ # Maps EXIF tags to tag names. TAGS = { - # possibly incomplete - 0x000b: "ProcessingSoftware", - 0x00fe: "NewSubfileType", - 0x00ff: "SubfileType", + 0x000B: "ProcessingSoftware", + 0x00FE: "NewSubfileType", + 0x00FF: "SubfileType", 0x0100: "ImageWidth", 0x0101: "ImageLength", 0x0102: "BitsPerSample", @@ -31,10 +30,10 @@ TAGS = { 0x0107: "Thresholding", 0x0108: "CellWidth", 0x0109: "CellLength", - 0x010a: "FillOrder", - 0x010d: "DocumentName", - 0x010e: "ImageDescription", - 0x010f: "Make", + 0x010A: "FillOrder", + 0x010D: "DocumentName", + 0x010E: "ImageDescription", + 0x010F: "Make", 0x0110: "Model", 0x0111: "StripOffsets", 0x0112: "Orientation", @@ -43,10 +42,10 @@ TAGS = { 0x0117: "StripByteCounts", 0x0118: "MinSampleValue", 0x0119: "MaxSampleValue", - 0x011a: "XResolution", - 0x011b: "YResolution", - 0x011c: "PlanarConfiguration", - 0x011d: "PageName", + 0x011A: "XResolution", + 0x011B: "YResolution", + 0x011C: "PlanarConfiguration", + 0x011D: "PageName", 0x0120: "FreeOffsets", 0x0121: "FreeByteCounts", 0x0122: "GrayResponseUnit", @@ -55,24 +54,24 @@ TAGS = { 0x0125: "T6Options", 0x0128: "ResolutionUnit", 0x0129: "PageNumber", - 0x012d: "TransferFunction", + 0x012D: "TransferFunction", 0x0131: "Software", 0x0132: "DateTime", - 0x013b: "Artist", - 0x013c: "HostComputer", - 0x013d: "Predictor", - 0x013e: "WhitePoint", - 0x013f: "PrimaryChromaticities", + 0x013B: "Artist", + 0x013C: "HostComputer", + 0x013D: "Predictor", + 0x013E: "WhitePoint", + 0x013F: "PrimaryChromaticities", 0x0140: "ColorMap", 0x0141: "HalftoneHints", 0x0142: "TileWidth", 0x0143: "TileLength", 0x0144: "TileOffsets", 0x0145: "TileByteCounts", - 0x014a: "SubIFDs", - 0x014c: "InkSet", - 0x014d: "InkNames", - 0x014e: "NumberOfInks", + 0x014A: "SubIFDs", + 0x014C: "InkSet", + 0x014D: "InkNames", + 0x014E: "NumberOfInks", 0x0150: "DotRange", 0x0151: "TargetPrinter", 0x0152: "ExtraSamples", @@ -83,9 +82,9 @@ TAGS = { 0x0157: "ClipPath", 0x0158: "XClipPathUnits", 0x0159: "YClipPathUnits", - 0x015a: "Indexed", - 0x015b: "JPEGTables", - 0x015f: "OPIProxy", + 0x015A: "Indexed", + 0x015B: "JPEGTables", + 0x015F: "OPIProxy", 0x0200: "JPEGProc", 0x0201: "JpegIFOffset", 0x0202: "JpegIFByteCount", @@ -99,20 +98,20 @@ TAGS = { 0x0212: "YCbCrSubSampling", 0x0213: "YCbCrPositioning", 0x0214: "ReferenceBlackWhite", - 0x02bc: "XMLPacket", + 0x02BC: "XMLPacket", 0x1000: "RelatedImageFileFormat", 0x1001: "RelatedImageWidth", 0x1002: "RelatedImageLength", 0x4746: "Rating", 0x4749: "RatingPercent", - 0x800d: "ImageID", - 0x828d: "CFARepeatPatternDim", - 0x828e: "CFAPattern", - 0x828f: "BatteryLevel", + 0x800D: "ImageID", + 0x828D: "CFARepeatPatternDim", + 0x828E: "CFAPattern", + 0x828F: "BatteryLevel", 0x8298: "Copyright", - 0x829a: "ExposureTime", - 0x829d: "FNumber", - 0x83bb: "IPTCNAA", + 0x829A: "ExposureTime", + 0x829D: "FNumber", + 0x83BB: "IPTCNAA", 0x8649: "ImageResources", 0x8769: "ExifOffset", 0x8773: "InterColorProfile", @@ -122,8 +121,8 @@ TAGS = { 0x8827: "ISOSpeedRatings", 0x8828: "OECF", 0x8829: "Interlace", - 0x882a: "TimeZoneOffset", - 0x882b: "SelfTimerMode", + 0x882A: "TimeZoneOffset", + 0x882B: "SelfTimerMode", 0x9000: "ExifVersion", 0x9003: "DateTimeOriginal", 0x9004: "DateTimeDigitized", @@ -138,142 +137,142 @@ TAGS = { 0x9207: "MeteringMode", 0x9208: "LightSource", 0x9209: "Flash", - 0x920a: "FocalLength", - 0x920b: "FlashEnergy", - 0x920c: "SpatialFrequencyResponse", - 0x920d: "Noise", + 0x920A: "FocalLength", + 0x920B: "FlashEnergy", + 0x920C: "SpatialFrequencyResponse", + 0x920D: "Noise", 0x9211: "ImageNumber", 0x9212: "SecurityClassification", 0x9213: "ImageHistory", 0x9214: "SubjectLocation", 0x9215: "ExposureIndex", 0x9216: "TIFF/EPStandardID", - 0x927c: "MakerNote", + 0x927C: "MakerNote", 0x9286: "UserComment", 0x9290: "SubsecTime", 0x9291: "SubsecTimeOriginal", 0x9292: "SubsecTimeDigitized", - 0x9c9b: "XPTitle", - 0x9c9c: "XPComment", - 0x9c9d: "XPAuthor", - 0x9c9e: "XPKeywords", - 0x9c9f: "XPSubject", - 0xa000: "FlashPixVersion", - 0xa001: "ColorSpace", - 0xa002: "ExifImageWidth", - 0xa003: "ExifImageHeight", - 0xa004: "RelatedSoundFile", - 0xa005: "ExifInteroperabilityOffset", - 0xa20b: "FlashEnergy", - 0xa20c: "SpatialFrequencyResponse", - 0xa20e: "FocalPlaneXResolution", - 0xa20f: "FocalPlaneYResolution", - 0xa210: "FocalPlaneResolutionUnit", - 0xa214: "SubjectLocation", - 0xa215: "ExposureIndex", - 0xa217: "SensingMethod", - 0xa300: "FileSource", - 0xa301: "SceneType", - 0xa302: "CFAPattern", - 0xa401: "CustomRendered", - 0xa402: "ExposureMode", - 0xa403: "WhiteBalance", - 0xa404: "DigitalZoomRatio", - 0xa405: "FocalLengthIn35mmFilm", - 0xa406: "SceneCaptureType", - 0xa407: "GainControl", - 0xa408: "Contrast", - 0xa409: "Saturation", - 0xa40a: "Sharpness", - 0xa40b: "DeviceSettingDescription", - 0xa40c: "SubjectDistanceRange", - 0xa420: "ImageUniqueID", - 0xa430: "CameraOwnerName", - 0xa431: "BodySerialNumber", - 0xa432: "LensSpecification", - 0xa433: "LensMake", - 0xa434: "LensModel", - 0xa435: "LensSerialNumber", - 0xa500: "Gamma", - 0xc4a5: "PrintImageMatching", - 0xc612: "DNGVersion", - 0xc613: "DNGBackwardVersion", - 0xc614: "UniqueCameraModel", - 0xc615: "LocalizedCameraModel", - 0xc616: "CFAPlaneColor", - 0xc617: "CFALayout", - 0xc618: "LinearizationTable", - 0xc619: "BlackLevelRepeatDim", - 0xc61a: "BlackLevel", - 0xc61b: "BlackLevelDeltaH", - 0xc61c: "BlackLevelDeltaV", - 0xc61d: "WhiteLevel", - 0xc61e: "DefaultScale", - 0xc61f: "DefaultCropOrigin", - 0xc620: "DefaultCropSize", - 0xc621: "ColorMatrix1", - 0xc622: "ColorMatrix2", - 0xc623: "CameraCalibration1", - 0xc624: "CameraCalibration2", - 0xc625: "ReductionMatrix1", - 0xc626: "ReductionMatrix2", - 0xc627: "AnalogBalance", - 0xc628: "AsShotNeutral", - 0xc629: "AsShotWhiteXY", - 0xc62a: "BaselineExposure", - 0xc62b: "BaselineNoise", - 0xc62c: "BaselineSharpness", - 0xc62d: "BayerGreenSplit", - 0xc62e: "LinearResponseLimit", - 0xc62f: "CameraSerialNumber", - 0xc630: "LensInfo", - 0xc631: "ChromaBlurRadius", - 0xc632: "AntiAliasStrength", - 0xc633: "ShadowScale", - 0xc634: "DNGPrivateData", - 0xc635: "MakerNoteSafety", - 0xc65a: "CalibrationIlluminant1", - 0xc65b: "CalibrationIlluminant2", - 0xc65c: "BestQualityScale", - 0xc65d: "RawDataUniqueID", - 0xc68b: "OriginalRawFileName", - 0xc68c: "OriginalRawFileData", - 0xc68d: "ActiveArea", - 0xc68e: "MaskedAreas", - 0xc68f: "AsShotICCProfile", - 0xc690: "AsShotPreProfileMatrix", - 0xc691: "CurrentICCProfile", - 0xc692: "CurrentPreProfileMatrix", - 0xc6bf: "ColorimetricReference", - 0xc6f3: "CameraCalibrationSignature", - 0xc6f4: "ProfileCalibrationSignature", - 0xc6f6: "AsShotProfileName", - 0xc6f7: "NoiseReductionApplied", - 0xc6f8: "ProfileName", - 0xc6f9: "ProfileHueSatMapDims", - 0xc6fa: "ProfileHueSatMapData1", - 0xc6fb: "ProfileHueSatMapData2", - 0xc6fc: "ProfileToneCurve", - 0xc6fd: "ProfileEmbedPolicy", - 0xc6fe: "ProfileCopyright", - 0xc714: "ForwardMatrix1", - 0xc715: "ForwardMatrix2", - 0xc716: "PreviewApplicationName", - 0xc717: "PreviewApplicationVersion", - 0xc718: "PreviewSettingsName", - 0xc719: "PreviewSettingsDigest", - 0xc71a: "PreviewColorSpace", - 0xc71b: "PreviewDateTime", - 0xc71c: "RawImageDigest", - 0xc71d: "OriginalRawFileDigest", - 0xc71e: "SubTileBlockSize", - 0xc71f: "RowInterleaveFactor", - 0xc725: "ProfileLookTableDims", - 0xc726: "ProfileLookTableData", - 0xc740: "OpcodeList1", - 0xc741: "OpcodeList2", - 0xc74e: "OpcodeList3", - 0xc761: "NoiseProfile" + 0x9C9B: "XPTitle", + 0x9C9C: "XPComment", + 0x9C9D: "XPAuthor", + 0x9C9E: "XPKeywords", + 0x9C9F: "XPSubject", + 0xA000: "FlashPixVersion", + 0xA001: "ColorSpace", + 0xA002: "ExifImageWidth", + 0xA003: "ExifImageHeight", + 0xA004: "RelatedSoundFile", + 0xA005: "ExifInteroperabilityOffset", + 0xA20B: "FlashEnergy", + 0xA20C: "SpatialFrequencyResponse", + 0xA20E: "FocalPlaneXResolution", + 0xA20F: "FocalPlaneYResolution", + 0xA210: "FocalPlaneResolutionUnit", + 0xA214: "SubjectLocation", + 0xA215: "ExposureIndex", + 0xA217: "SensingMethod", + 0xA300: "FileSource", + 0xA301: "SceneType", + 0xA302: "CFAPattern", + 0xA401: "CustomRendered", + 0xA402: "ExposureMode", + 0xA403: "WhiteBalance", + 0xA404: "DigitalZoomRatio", + 0xA405: "FocalLengthIn35mmFilm", + 0xA406: "SceneCaptureType", + 0xA407: "GainControl", + 0xA408: "Contrast", + 0xA409: "Saturation", + 0xA40A: "Sharpness", + 0xA40B: "DeviceSettingDescription", + 0xA40C: "SubjectDistanceRange", + 0xA420: "ImageUniqueID", + 0xA430: "CameraOwnerName", + 0xA431: "BodySerialNumber", + 0xA432: "LensSpecification", + 0xA433: "LensMake", + 0xA434: "LensModel", + 0xA435: "LensSerialNumber", + 0xA500: "Gamma", + 0xC4A5: "PrintImageMatching", + 0xC612: "DNGVersion", + 0xC613: "DNGBackwardVersion", + 0xC614: "UniqueCameraModel", + 0xC615: "LocalizedCameraModel", + 0xC616: "CFAPlaneColor", + 0xC617: "CFALayout", + 0xC618: "LinearizationTable", + 0xC619: "BlackLevelRepeatDim", + 0xC61A: "BlackLevel", + 0xC61B: "BlackLevelDeltaH", + 0xC61C: "BlackLevelDeltaV", + 0xC61D: "WhiteLevel", + 0xC61E: "DefaultScale", + 0xC61F: "DefaultCropOrigin", + 0xC620: "DefaultCropSize", + 0xC621: "ColorMatrix1", + 0xC622: "ColorMatrix2", + 0xC623: "CameraCalibration1", + 0xC624: "CameraCalibration2", + 0xC625: "ReductionMatrix1", + 0xC626: "ReductionMatrix2", + 0xC627: "AnalogBalance", + 0xC628: "AsShotNeutral", + 0xC629: "AsShotWhiteXY", + 0xC62A: "BaselineExposure", + 0xC62B: "BaselineNoise", + 0xC62C: "BaselineSharpness", + 0xC62D: "BayerGreenSplit", + 0xC62E: "LinearResponseLimit", + 0xC62F: "CameraSerialNumber", + 0xC630: "LensInfo", + 0xC631: "ChromaBlurRadius", + 0xC632: "AntiAliasStrength", + 0xC633: "ShadowScale", + 0xC634: "DNGPrivateData", + 0xC635: "MakerNoteSafety", + 0xC65A: "CalibrationIlluminant1", + 0xC65B: "CalibrationIlluminant2", + 0xC65C: "BestQualityScale", + 0xC65D: "RawDataUniqueID", + 0xC68B: "OriginalRawFileName", + 0xC68C: "OriginalRawFileData", + 0xC68D: "ActiveArea", + 0xC68E: "MaskedAreas", + 0xC68F: "AsShotICCProfile", + 0xC690: "AsShotPreProfileMatrix", + 0xC691: "CurrentICCProfile", + 0xC692: "CurrentPreProfileMatrix", + 0xC6BF: "ColorimetricReference", + 0xC6F3: "CameraCalibrationSignature", + 0xC6F4: "ProfileCalibrationSignature", + 0xC6F6: "AsShotProfileName", + 0xC6F7: "NoiseReductionApplied", + 0xC6F8: "ProfileName", + 0xC6F9: "ProfileHueSatMapDims", + 0xC6FA: "ProfileHueSatMapData1", + 0xC6FB: "ProfileHueSatMapData2", + 0xC6FC: "ProfileToneCurve", + 0xC6FD: "ProfileEmbedPolicy", + 0xC6FE: "ProfileCopyright", + 0xC714: "ForwardMatrix1", + 0xC715: "ForwardMatrix2", + 0xC716: "PreviewApplicationName", + 0xC717: "PreviewApplicationVersion", + 0xC718: "PreviewSettingsName", + 0xC719: "PreviewSettingsDigest", + 0xC71A: "PreviewColorSpace", + 0xC71B: "PreviewDateTime", + 0xC71C: "RawImageDigest", + 0xC71D: "OriginalRawFileDigest", + 0xC71E: "SubTileBlockSize", + 0xC71F: "RowInterleaveFactor", + 0xC725: "ProfileLookTableDims", + 0xC726: "ProfileLookTableData", + 0xC740: "OpcodeList1", + 0xC741: "OpcodeList2", + 0xC74E: "OpcodeList3", + 0xC761: "NoiseProfile", } ## diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index 63c195c43..c2ce8651c 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -23,6 +23,7 @@ def register_handler(handler): global _handler _handler = handler + # -------------------------------------------------------------------- # Image adapter @@ -62,7 +63,7 @@ class FITSStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr("_handler", "save"): - raise IOError("FITS save handler not installed") + raise OSError("FITS save handler not installed") _handler.save(im, fp, filename) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index bbc1a1340..9bf7d74d6 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -19,14 +19,10 @@ 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 + def _accept(prefix): return len(prefix) >= 6 and i16(prefix[4:6]) in [0xAF11, 0xAF12] @@ -35,6 +31,7 @@ def _accept(prefix): # Image plugin for the FLI/FLC animation format. Use the seek # method to load individual frames. + class FliImageFile(ImageFile.ImageFile): format = "FLI" @@ -46,9 +43,11 @@ class FliImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(128) magic = i16(s[4:6]) - if not (magic in [0xAF11, 0xAF12] and - i16(s[14:16]) in [0, 3] and # flags - s[20:22] == b"\x00\x00"): # reserved + if not ( + magic in [0xAF11, 0xAF12] + and i16(s[14:16]) in [0, 3] # flags + and s[20:22] == b"\x00\x00" # reserved + ): raise SyntaxError("not an FLI/FLC file") # frames @@ -84,7 +83,7 @@ class FliImageFile(ImageFile.ImageFile): elif i16(s[4:6]) == 4: self._palette(palette, 0) - palette = [o8(r)+o8(g)+o8(b) for (r, g, b) in palette] + palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette] self.palette = ImagePalette.raw("RGB", b"".join(palette)) # set things up to decode first frame @@ -106,8 +105,8 @@ class FliImageFile(ImageFile.ImageFile): s = self.fp.read(n * 3) for n in range(0, len(s), 3): r = i8(s[n]) << shift - g = i8(s[n+1]) << shift - b = i8(s[n+2]) << shift + g = i8(s[n + 1]) << shift + b = i8(s[n + 2]) << shift palette[i] = (r, g, b) i += 1 @@ -152,7 +151,7 @@ class FliImageFile(ImageFile.ImageFile): framesize = i32(s) self.decodermaxblock = framesize - self.tile = [("fli", (0, 0)+self.size, self.__offset, None)] + self.tile = [("fli", (0, 0) + self.size, self.__offset, None)] self.__offset += framesize diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index b43f44762..979a1e33c 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -14,9 +14,9 @@ # See the README file for information on usage and redistribution. # -from __future__ import print_function import os + from . import Image, _binary WIDTH = 800 @@ -33,7 +33,8 @@ def puti16(fp, values): ## # Base class for raster font file handlers. -class FontFile(object): + +class FontFile: bitmap = None @@ -61,7 +62,7 @@ class FontFile(object): w = w + (src[2] - src[0]) if w > WIDTH: lines += 1 - w = (src[2] - src[0]) + w = src[2] - src[0] maxwidth = max(maxwidth, w) xsize = maxwidth @@ -103,7 +104,7 @@ class FontFile(object): # font metrics with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: fp.write(b"PILfont\n") - fp.write((";;;;;;%d;\n" % self.ysize).encode('ascii')) # HACK!!! + fp.write((";;;;;;%d;\n" % self.ysize).encode("ascii")) # HACK!!! fp.write(b"DATA\n") for id in range(256): m = self.metrics[id] diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 5e8a814f2..3938f0f09 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -14,37 +14,31 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - -from . import Image, ImageFile -from ._binary import i32le as i32, i8 - import olefile -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" +from . import Image, ImageFile +from ._binary import i8, i32le as i32 # we map from colour field tuples to (mode, rawmode) descriptors MODES = { # opacity - (0x00007ffe): ("A", "L"), + (0x00007FFE): ("A", "L"), # monochrome (0x00010000,): ("L", "L"), - (0x00018000, 0x00017ffe): ("RGBA", "LA"), + (0x00018000, 0x00017FFE): ("RGBA", "LA"), # photo YCC (0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"), - (0x00028000, 0x00028001, 0x00028002, 0x00027ffe): ("RGBA", "YCCA;P"), + (0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"), # standard RGB (NIFRGB) (0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"), - (0x00038000, 0x00038001, 0x00038002, 0x00037ffe): ("RGBA", "RGBA"), + (0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"), } # # -------------------------------------------------------------------- + def _accept(prefix): return prefix[:8] == olefile.MAGIC @@ -52,6 +46,7 @@ def _accept(prefix): ## # Image plugin for the FlashPix images. + class FpxImageFile(ImageFile.ImageFile): format = "FPX" @@ -64,7 +59,7 @@ class FpxImageFile(ImageFile.ImageFile): try: self.ole = olefile.OleFileIO(self.fp) - except IOError: + except OSError: raise SyntaxError("not an FPX file; invalid OLE file") if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": @@ -76,10 +71,9 @@ class FpxImageFile(ImageFile.ImageFile): # # get the Image Contents Property Set - prop = self.ole.getproperties([ - "Data Object Store %06d" % index, - "\005Image Contents" - ]) + prop = self.ole.getproperties( + ["Data Object Store %06d" % index, "\005Image Contents"] + ) # size (highest resolution) @@ -105,7 +99,7 @@ class FpxImageFile(ImageFile.ImageFile): colors = [] for i in range(i32(s, 4)): # note: for now, we ignore the "uncalibrated" flag - colors.append(i32(s, 8+i*4) & 0x7fffffff) + colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF) self.mode, self.rawmode = MODES[tuple(colors)] @@ -125,7 +119,7 @@ class FpxImageFile(ImageFile.ImageFile): stream = [ "Data Object Store %06d" % index, "Resolution %04d" % subimage, - "Subimage 0000 Header" + "Subimage 0000 Header", ] fp = self.ole.openstream(stream) @@ -144,7 +138,7 @@ class FpxImageFile(ImageFile.ImageFile): length = i32(s, 32) if size != self.size: - raise IOError("subimage mismatch") + raise OSError("subimage mismatch") # get tile descriptors fp.seek(28 + offset) @@ -157,17 +151,29 @@ class FpxImageFile(ImageFile.ImageFile): for i in range(0, len(s), length): - compression = i32(s, i+8) + compression = i32(s, i + 8) if compression == 0: - self.tile.append(("raw", (x, y, x+xtile, y+ytile), - i32(s, i) + 28, (self.rawmode))) + self.tile.append( + ( + "raw", + (x, y, x + xtile, y + ytile), + i32(s, i) + 28, + (self.rawmode), + ) + ) elif compression == 1: # FIXME: the fill decoder is not implemented - self.tile.append(("fill", (x, y, x+xtile, y+ytile), - i32(s, i) + 28, (self.rawmode, s[12:16]))) + self.tile.append( + ( + "fill", + (x, y, x + xtile, y + ytile), + i32(s, i) + 28, + (self.rawmode, s[12:16]), + ) + ) elif compression == 2: @@ -189,8 +195,14 @@ class FpxImageFile(ImageFile.ImageFile): # The image is stored as defined by rawmode jpegmode = rawmode - self.tile.append(("jpeg", (x, y, x+xtile, y+ytile), - i32(s, i) + 28, (rawmode, jpegmode))) + self.tile.append( + ( + "jpeg", + (x, y, x + xtile, y + ytile), + i32(s, i) + 28, + (rawmode, jpegmode), + ) + ) # FIXME: jpeg tables are tile dependent; the prefix # data must be placed in the tile descriptor itself! @@ -199,7 +211,7 @@ class FpxImageFile(ImageFile.ImageFile): self.tile_prefix = self.jpeg[jpeg_tables] else: - raise IOError("unknown/invalid compression") + raise OSError("unknown/invalid compression") x = x + xtile if x >= xsize: @@ -213,11 +225,11 @@ class FpxImageFile(ImageFile.ImageFile): def load(self): if not self.fp: - self.fp = self.ole.openstream(self.stream[:2] + - ["Subimage 0000 Data"]) + self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) return ImageFile.ImageFile.load(self) + # # -------------------------------------------------------------------- diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index f1b9acdc1..096ccacac 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -53,8 +53,8 @@ Note: All data is stored in little-Endian (Intel) byte order. import struct from io import BytesIO -from . import Image, ImageFile +from . import Image, ImageFile MAGIC = b"FTEX" FORMAT_DXT1 = 0 @@ -79,7 +79,7 @@ class FtexImageFile(ImageFile.ImageFile): format, where = struct.unpack("<2i", self.fp.read(8)) self.fp.seek(where) - mipmap_size, = struct.unpack("= 8 and \ - i32(prefix[:4]) >= 20 and i32(prefix[4:8]) in (1, 2) + return len(prefix) >= 8 and i32(prefix[:4]) >= 20 and i32(prefix[4:8]) in (1, 2) ## # Image plugin for the GIMP brush format. + class GbrImageFile(ImageFile.ImageFile): format = "GBR" @@ -55,24 +55,23 @@ class GbrImageFile(ImageFile.ImageFile): if width <= 0 or height <= 0: raise SyntaxError("not a GIMP brush") if color_depth not in (1, 4): - raise SyntaxError( - "Unsupported GIMP brush color depth: %s" % color_depth) + raise SyntaxError("Unsupported GIMP brush color depth: %s" % color_depth) if version == 1: - comment_length = header_size-20 + comment_length = header_size - 20 else: - comment_length = header_size-28 + comment_length = header_size - 28 magic_number = self.fp.read(4) - if magic_number != b'GIMP': + if magic_number != b"GIMP": raise SyntaxError("not a GIMP brush, bad magic number") - self.info['spacing'] = i32(self.fp.read(4)) + self.info["spacing"] = i32(self.fp.read(4)) comment = self.fp.read(comment_length)[:-1] if color_depth == 1: self.mode = "L" else: - self.mode = 'RGBA' + self.mode = "RGBA" self._size = width, height @@ -88,6 +87,7 @@ class GbrImageFile(ImageFile.ImageFile): self.im = Image.core.new(self.mode, self.size) self.frombytes(self.fp.read(self._data_size)) + # # registry diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 9f00b2fef..54c88712c 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -23,20 +23,16 @@ # purposes only. -from . import ImageFile, ImagePalette +from . import ImageFile, ImagePalette, UnidentifiedImageError 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 # this plugin, you have to import the GdImageFile module and # use the GdImageFile.open function. + class GdImageFile(ImageFile.ImageFile): format = "GD" @@ -57,15 +53,17 @@ class GdImageFile(ImageFile.ImageFile): trueColorOffset = 2 if trueColor else 0 # transparency index - tindex = i32(s[7+trueColorOffset:7+trueColorOffset+4]) + tindex = i32(s[7 + trueColorOffset : 7 + trueColorOffset + 4]) if tindex < 256: self.info["transparency"] = tindex self.palette = ImagePalette.raw( - "XBGR", s[7+trueColorOffset+4:7+trueColorOffset+4+256*4]) + "XBGR", s[7 + trueColorOffset + 4 : 7 + trueColorOffset + 4 + 256 * 4] + ) - self.tile = [("raw", (0, 0)+self.size, 7+trueColorOffset+4+256*4, - ("L", 0, 1))] + self.tile = [ + ("raw", (0, 0) + self.size, 7 + trueColorOffset + 4 + 256 * 4, ("L", 0, 1)) + ] def open(fp, mode="r"): @@ -84,4 +82,4 @@ def open(fp, mode="r"): try: return GdImageFile(fp) except SyntaxError: - raise IOError("cannot identify this image file") + raise UnidentifiedImageError("cannot identify this image file") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 2d13de022..7717fc707 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -24,19 +24,17 @@ # See the README file for information on usage and redistribution. # -from . import Image, ImageFile, ImagePalette, ImageChops, ImageSequence -from ._binary import i8, i16le as i16, o8, o16le as o16 - import itertools +import os +import subprocess -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.9" - +from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence +from ._binary import i8, i16le as i16, o8, o16le as o16 # -------------------------------------------------------------------- # Identify/read GIF files + def _accept(prefix): return prefix[:6] in [b"GIF87a", b"GIF89a"] @@ -45,6 +43,7 @@ def _accept(prefix): # Image plugin for GIF images. This plugin supports both GIF87 and # GIF89 images. + class GifImageFile(ImageFile.ImageFile): format = "GIF" @@ -78,7 +77,7 @@ class GifImageFile(ImageFile.ImageFile): # check if palette contains colour indices p = self.fp.read(3 << bits) for i in range(0, len(p), 3): - if not (i//3 == i8(p[i]) == i8(p[i+1]) == i8(p[i+2])): + if not (i // 3 == i8(p[i]) == i8(p[i + 1]) == i8(p[i + 2])): p = ImagePalette.raw("RGB", p) self.global_palette = self.palette = p break @@ -122,6 +121,8 @@ class GifImageFile(ImageFile.ImageFile): if not self._seek_check(frame): return if frame < self.__frame: + if frame != 0: + self.im = None self._seek(0) last_frame = self.__frame @@ -166,6 +167,7 @@ class GifImageFile(ImageFile.ImageFile): self.im.paste(self.dispose, self.dispose_extent) from copy import copy + self.palette = copy(self.global_palette) info = {} @@ -231,6 +233,8 @@ class GifImageFile(ImageFile.ImageFile): # extent x0, y0 = i16(s[0:]), i16(s[2:]) x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:]) + if x1 > self.size[0] or y1 > self.size[1]: + self._size = max(x1, self.size[0]), max(y1, self.size[1]) self.dispose_extent = x0, y0, x1, y1 flags = i8(s[8]) @@ -238,16 +242,14 @@ class GifImageFile(ImageFile.ImageFile): if flags & 128: bits = (flags & 7) + 1 - self.palette =\ - ImagePalette.raw("RGB", self.fp.read(3 << bits)) + self.palette = ImagePalette.raw("RGB", self.fp.read(3 << bits)) # image data bits = i8(self.fp.read(1)) self.__offset = self.fp.tell() - self.tile = [("gif", - (x0, y0, x1, y1), - self.__offset, - (bits, interlace))] + self.tile = [ + ("gif", (x0, y0, x1, y1), self.__offset, (bits, interlace)) + ] break else: @@ -260,8 +262,8 @@ class GifImageFile(ImageFile.ImageFile): self.dispose = None elif self.disposal_method == 2: # replace with background colour - self.dispose = Image.core.fill("P", self.size, - self.info["background"]) + Image._decompression_bomb_check(self.size) + self.dispose = Image.core.fill("P", self.size, self.info["background"]) else: # replace with previous contents if self.im: @@ -299,8 +301,7 @@ class GifImageFile(ImageFile.ImageFile): # we do this by pasting the updated area onto the previous # frame which we then use as the current image content updated = self._crop(self.im, self.dispose_extent) - self._prev_im.paste(updated, self.dispose_extent, - updated.convert('RGBA')) + self._prev_im.paste(updated, self.dispose_extent, updated.convert("RGBA")) self.im = self._prev_im self._prev_im = self.im.copy() @@ -313,15 +314,12 @@ class GifImageFile(ImageFile.ImageFile): finally: self.__fp = None + # -------------------------------------------------------------------- # Write GIF files -RAWMODE = { - "1": "L", - "L": "L", - "P": "P" -} +RAWMODE = {"1": "L", "L": "L", "P": "P"} def _normalize_mode(im, initial_call=False): @@ -372,19 +370,23 @@ def _normalize_palette(im, palette, info): if isinstance(palette, (bytes, bytearray, list)): source_palette = bytearray(palette[:768]) if isinstance(palette, ImagePalette.ImagePalette): - source_palette = bytearray(itertools.chain.from_iterable( - zip(palette.palette[:256], - palette.palette[256:512], - palette.palette[512:768]))) + source_palette = bytearray( + itertools.chain.from_iterable( + zip( + palette.palette[:256], + palette.palette[256:512], + palette.palette[512:768], + ) + ) + ) if im.mode == "P": if not source_palette: source_palette = im.im.getpalette("RGB")[:768] else: # L-mode if not source_palette: - source_palette = bytearray(i//3 for i in range(768)) - im.palette = ImagePalette.ImagePalette("RGB", - palette=source_palette) + source_palette = bytearray(i // 3 for i in range(768)) + im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) used_palette_colors = _get_optimize(im, info) if used_palette_colors is not None: @@ -410,8 +412,7 @@ def _write_single_frame(im, fp, palette): _write_local_header(fp, im, (0, 0), flags) im_out.encoderconfig = (8, get_interlace(im)) - ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0, - RAWMODE[im_out.mode])]) + ImageFile._save(im_out, fp, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]) fp.write(b"\0") # end of image data @@ -423,8 +424,8 @@ def _write_multiple_frames(im, fp, palette): im_frames = [] frame_count = 0 - for imSequence in itertools.chain([im], - im.encoderinfo.get("append_images", [])): + background_im = None + for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): for im_frame in ImageSequence.Iterator(imSequence): # a copy is required here since seek can still mutate the image im_frame = _normalize_mode(im_frame.copy()) @@ -435,7 +436,7 @@ def _write_multiple_frames(im, fp, palette): encoderinfo = im.encoderinfo.copy() if isinstance(duration, (list, tuple)): - encoderinfo['duration'] = duration[frame_count] + encoderinfo["duration"] = duration[frame_count] if isinstance(disposal, (list, tuple)): encoderinfo["disposal"] = disposal[frame_count] frame_count += 1 @@ -443,45 +444,54 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if _get_palette_bytes(im_frame) == \ - _get_palette_bytes(previous['im']): - delta = ImageChops.subtract_modulo(im_frame, - previous['im']) + if encoderinfo.get("disposal") == 2: + if background_im is None: + background = _get_background( + im, + im.encoderinfo.get("background", im.info.get("background")), + ) + background_im = Image.new("P", im_frame.size, background) + background_im.putpalette(im_frames[0]["im"].palette) + base_im = background_im + else: + base_im = previous["im"] + if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im): + delta = ImageChops.subtract_modulo(im_frame, base_im) else: delta = ImageChops.subtract_modulo( - im_frame.convert('RGB'), previous['im'].convert('RGB')) + im_frame.convert("RGB"), base_im.convert("RGB") + ) bbox = delta.getbbox() if not bbox: # This frame is identical to the previous frame if duration: - previous['encoderinfo']['duration'] += \ - encoderinfo['duration'] + previous["encoderinfo"]["duration"] += encoderinfo["duration"] continue else: bbox = None - im_frames.append({ - 'im': im_frame, - 'bbox': bbox, - 'encoderinfo': encoderinfo - }) + im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) if len(im_frames) > 1: for frame_data in im_frames: - im_frame = frame_data['im'] - if not frame_data['bbox']: + im_frame = frame_data["im"] + if not frame_data["bbox"]: # global header - for s in _get_global_header(im_frame, - frame_data['encoderinfo']): + for s in _get_global_header(im_frame, frame_data["encoderinfo"]): fp.write(s) offset = (0, 0) else: # compress difference - frame_data['encoderinfo']['include_color_table'] = True + frame_data["encoderinfo"]["include_color_table"] = True - im_frame = im_frame.crop(frame_data['bbox']) - offset = frame_data['bbox'][:2] - _write_frame_data(fp, im_frame, offset, frame_data['encoderinfo']) + im_frame = im_frame.crop(frame_data["bbox"]) + offset = frame_data["bbox"][:2] + _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"]) return True + elif "duration" in im.encoderinfo and isinstance( + im.encoderinfo["duration"], (list, tuple) + ): + # Since multiple frames will not be written, add together the frame durations + im.encoderinfo["duration"] = sum(im.encoderinfo["duration"]) def _save_all(im, fp, filename): @@ -539,7 +549,7 @@ def _write_local_header(fp, im, offset, flags): else: duration = 0 - disposal = int(im.encoderinfo.get('disposal', 0)) + disposal = int(im.encoderinfo.get("disposal", 0)) if transparent_color_exists or duration != 0 or disposal: packed_flag = 1 if transparent_color_exists else 0 @@ -547,50 +557,53 @@ def _write_local_header(fp, im, offset, flags): if not transparent_color_exists: transparency = 0 - fp.write(b"!" + - o8(249) + # extension intro - o8(4) + # length - o8(packed_flag) + # packed fields - o16(duration) + # duration - o8(transparency) + # transparency index - o8(0)) + fp.write( + b"!" + + o8(249) # extension intro + + o8(4) # length + + o8(packed_flag) # packed fields + + o16(duration) # duration + + o8(transparency) # transparency index + + o8(0) + ) - if "comment" in im.encoderinfo and \ - 1 <= len(im.encoderinfo["comment"]): - fp.write(b"!" + - o8(254)) # extension intro + if "comment" in im.encoderinfo and 1 <= len(im.encoderinfo["comment"]): + fp.write(b"!" + o8(254)) # extension intro for i in range(0, len(im.encoderinfo["comment"]), 255): - subblock = im.encoderinfo["comment"][i:i+255] - fp.write(o8(len(subblock)) + - subblock) + subblock = im.encoderinfo["comment"][i : i + 255] + fp.write(o8(len(subblock)) + subblock) fp.write(o8(0)) if "loop" in im.encoderinfo: number_of_loops = im.encoderinfo["loop"] - fp.write(b"!" + - o8(255) + # extension intro - o8(11) + - b"NETSCAPE2.0" + - o8(3) + - o8(1) + - o16(number_of_loops) + # number of loops - o8(0)) - include_color_table = im.encoderinfo.get('include_color_table') + fp.write( + b"!" + + o8(255) # extension intro + + o8(11) + + b"NETSCAPE2.0" + + o8(3) + + o8(1) + + o16(number_of_loops) # number of loops + + o8(0) + ) + include_color_table = im.encoderinfo.get("include_color_table") if include_color_table: palette_bytes = _get_palette_bytes(im) color_table_size = _get_color_table_size(palette_bytes) if color_table_size: - flags = flags | 128 # local color table flag + flags = flags | 128 # local color table flag flags = flags | color_table_size - fp.write(b"," + - o16(offset[0]) + # offset - o16(offset[1]) + - o16(im.size[0]) + # size - o16(im.size[1]) + - o8(flags)) # flags + fp.write( + b"," + + o16(offset[0]) # offset + + o16(offset[1]) + + o16(im.size[0]) # size + + o16(im.size[1]) + + o8(flags) # flags + ) if include_color_table and color_table_size: fp.write(_get_header_palette(palette_bytes)) - fp.write(o8(8)) # bits + fp.write(o8(8)) # bits def _save_netpbm(im, fp, filename): @@ -601,38 +614,38 @@ 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. + tempfile = im._dump() - import os - from subprocess import Popen, check_call, PIPE, CalledProcessError - file = im._dump() - - with open(filename, 'wb') as f: + with open(filename, "wb") as f: if im.mode != "RGB": - with open(os.devnull, 'wb') as devnull: - check_call(["ppmtogif", file], 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" % (file, filename) - quant_cmd = ["ppmquant", "256", file] + # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) + quant_cmd = ["ppmquant", "256", tempfile] togif_cmd = ["ppmtogif"] - with open(os.devnull, 'wb') as devnull: - quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=devnull) - togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout, - stdout=f, stderr=devnull) + quant_proc = subprocess.Popen( + quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) + togif_proc = subprocess.Popen( + togif_cmd, stdin=quant_proc.stdout, stdout=f, stderr=subprocess.DEVNULL + ) # Allow ppmquant to receive SIGPIPE if ppmtogif exits quant_proc.stdout.close() retcode = quant_proc.wait() if retcode: - raise CalledProcessError(retcode, quant_cmd) + 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(file) + os.unlink(tempfile) except OSError: pass @@ -664,7 +677,7 @@ def _get_optimize(im, info): # * If we have a 'large' image, the palette is in the noise. # create the new palette if not every color is used - optimise = _FORCE_OPTIMIZE or im.mode == 'L' + optimise = _FORCE_OPTIMIZE or im.mode == "L" if optimise or im.width * im.height < 512 * 512: # check which colors are used used_palette_colors = [] @@ -672,18 +685,23 @@ def _get_optimize(im, info): if count: used_palette_colors.append(i) - if optimise or (len(used_palette_colors) <= 128 and - max(used_palette_colors) > len(used_palette_colors)): + if optimise or ( + len(used_palette_colors) <= 128 + and max(used_palette_colors) > len(used_palette_colors) + ): return used_palette_colors def _get_color_table_size(palette_bytes): # calculate the palette size for the header import math - color_table_size = int(math.ceil(math.log(len(palette_bytes)//3, 2)))-1 - if color_table_size < 0: - color_table_size = 0 - return color_table_size + + if not palette_bytes: + return 0 + elif len(palette_bytes) < 9: + return 1 + else: + return int(math.ceil(math.log(len(palette_bytes) // 3, 2))) - 1 def _get_header_palette(palette_bytes): @@ -698,7 +716,7 @@ def _get_header_palette(palette_bytes): # add the missing amount of bytes # the palette has to be 2< 0: palette_bytes += o8(0) * 3 * actual_target_size_diff return palette_bytes @@ -714,6 +732,18 @@ def _get_palette_bytes(im): return im.palette.palette +def _get_background(im, infoBackground): + background = 0 + if infoBackground: + background = infoBackground + if isinstance(background, tuple): + # WebPImagePlugin stores an RGBA value in info["background"] + # So it must be converted to the same format as GifImagePlugin's + # info["background"] - a global color table index + background = im.palette.getcolor(background) + return background + + def _get_global_header(im, info): """Return a list of strings representing a GIF header""" @@ -723,9 +753,9 @@ def _get_global_header(im, info): version = b"87a" for extensionKey in ["transparency", "duration", "loop", "comment"]: if info and extensionKey in info: - if ((extensionKey == "duration" and info[extensionKey] == 0) or - (extensionKey == "comment" and - not (1 <= len(info[extensionKey]) <= 255))): + if (extensionKey == "duration" and info[extensionKey] == 0) or ( + extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255) + ): continue version = b"89a" break @@ -733,31 +763,23 @@ def _get_global_header(im, info): if im.info.get("version") == b"89a": version = b"89a" - background = 0 - if "background" in info: - background = info["background"] - if isinstance(background, tuple): - # WebPImagePlugin stores an RGBA value in info["background"] - # So it must be converted to the same format as GifImagePlugin's - # info["background"] - a global color table index - background = im.palette.getcolor(background) + background = _get_background(im, info.get("background")) palette_bytes = _get_palette_bytes(im) color_table_size = _get_color_table_size(palette_bytes) return [ - b"GIF"+version + # signature + version - o16(im.size[0]) + # canvas width - o16(im.size[1]), # canvas height - + b"GIF" # signature + + version # version + + o16(im.size[0]) # canvas width + + o16(im.size[1]), # canvas height # Logical Screen Descriptor # size of global color table + global color table flag - o8(color_table_size + 128), # packed fields + o8(color_table_size + 128), # packed fields # background + reserved/aspect o8(background) + o8(0), - # Global Color Table - _get_header_palette(palette_bytes) + _get_header_palette(palette_bytes), ] @@ -768,13 +790,15 @@ def _write_frame_data(fp, im_frame, offset, params): # local image header _write_local_header(fp, im_frame, offset, 0) - ImageFile._save(im_frame, fp, [("gif", (0, 0)+im_frame.size, 0, - RAWMODE[im_frame.mode])]) + ImageFile._save( + im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])] + ) fp.write(b"\0") # end of image data finally: del im_frame.encoderinfo + # -------------------------------------------------------------------- # Legacy GIF utilities @@ -823,7 +847,8 @@ def getdata(im, offset=(0, 0), **params): :returns: List of Bytes containing gif encoded frame data """ - class Collector(object): + + class Collector: data = [] def write(self, data): diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index 10593da24..851e24d62 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -13,7 +13,8 @@ # See the README file for information on usage and redistribution. # -from math import pi, log, sin, sqrt +from math import log, pi, sin, sqrt + from ._binary import o8 # -------------------------------------------------------------------- @@ -59,7 +60,7 @@ def sphere_decreasing(middle, pos): SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] -class GradientFile(object): +class GradientFile: gradient = None @@ -72,7 +73,7 @@ class GradientFile(object): for i in range(entries): - x = i / float(entries-1) + x = i / float(entries - 1) while x1 < x: ix += 1 @@ -100,8 +101,8 @@ class GradientFile(object): ## # File handler for GIMP's gradient format. -class GimpGradientFile(GradientFile): +class GimpGradientFile(GradientFile): def __init__(self, fp): if fp.readline()[:13] != b"GIMP Gradient": @@ -131,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 6eef6a2dd..e3060ab8a 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -15,31 +15,30 @@ # import re -from ._binary import o8 +from ._binary import o8 ## # File handler for GIMP's palette format. -class GimpPaletteFile(object): + +class GimpPaletteFile: rawmode = "RGB" def __init__(self, fp): - self.palette = [o8(i)*3 for i in range(256)] + self.palette = [o8(i) * 3 for i in range(256)] if fp.readline()[:12] != b"GIMP Palette": raise SyntaxError("not a GIMP palette file") - i = 0 - - while i <= 255: + for i in range(256): s = fp.readline() - if not s: break + # skip fields and comment lines if re.match(br"\w+:|#", s): continue @@ -50,10 +49,7 @@ class GimpPaletteFile(object): if len(v) != 3: raise ValueError("bad palette entry") - if 0 <= i <= 255: - self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) - - i += 1 + self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) self.palette = b"".join(self.palette) diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 243ea2a53..515c272f7 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -28,6 +28,7 @@ def register_handler(handler): # -------------------------------------------------------------------- # Image adapter + def _accept(prefix): return prefix[0:4] == b"GRIB" and i8(prefix[7]) == 1 @@ -60,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 8783f804d..362f2d399 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -27,6 +27,7 @@ def register_handler(handler): # -------------------------------------------------------------------- # Image adapter + def _accept(prefix): return prefix[:8] == b"\x89HDF\r\n\x1a\n" @@ -59,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 2ea66675f..18e8403d1 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -15,16 +15,18 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile, PngImagePlugin -from PIL._binary import i8 import io import os import shutil import struct +import subprocess import sys import tempfile -enable_jpeg2k = hasattr(Image.core, 'jp2klib_version') +from PIL import Image, ImageFile, PngImagePlugin +from PIL._binary import i8 + +enable_jpeg2k = hasattr(Image.core, "jp2klib_version") if enable_jpeg2k: from PIL import Jpeg2KImagePlugin @@ -32,7 +34,7 @@ HEADERSIZE = 8 def nextheader(fobj): - return struct.unpack('>4sI', fobj.read(HEADERSIZE)) + return struct.unpack(">4sI", fobj.read(HEADERSIZE)) def read_32t(fobj, start_length, size): @@ -40,8 +42,8 @@ def read_32t(fobj, start_length, size): (start, length) = start_length fobj.seek(start) sig = fobj.read(4) - if sig != b'\x00\x00\x00\x00': - raise SyntaxError('Unknown signature, expecting 0x00000000') + if sig != b"\x00\x00\x00\x00": + raise SyntaxError("Unknown signature, expecting 0x00000000") return read_32(fobj, (start + 4, length - 4), size) @@ -81,12 +83,8 @@ def read_32(fobj, start_length, size): if bytesleft <= 0: break if bytesleft != 0: - raise SyntaxError( - "Error reading channel [%r left]" % bytesleft - ) - band = Image.frombuffer( - "L", pixel_size, b"".join(data), "raw", "L", 0, 1 - ) + raise SyntaxError("Error reading channel [%r left]" % bytesleft) + band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) im.im.putband(band.im, band_ix) return {"RGB": im} @@ -97,9 +95,7 @@ def read_mk(fobj, start_length, size): fobj.seek(start) pixel_size = (size[0] * size[2], size[1] * size[2]) sizesq = pixel_size[0] * pixel_size[1] - band = Image.frombuffer( - "L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1 - ) + band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1) return {"A": band} @@ -107,73 +103,58 @@ def read_png_or_jpeg2000(fobj, start_length, size): (start, length) = start_length fobj.seek(start) sig = fobj.read(12) - if sig[:8] == b'\x89PNG\x0d\x0a\x1a\x0a': + if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a": fobj.seek(start) im = PngImagePlugin.PngImageFile(fobj) return {"RGBA": im} - elif sig[:4] == b'\xff\x4f\xff\x51' \ - or sig[:4] == b'\x0d\x0a\x87\x0a' \ - or sig == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a': + elif ( + sig[:4] == b"\xff\x4f\xff\x51" + or sig[:4] == b"\x0d\x0a\x87\x0a" + or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" + ): if not enable_jpeg2k: - raise ValueError('Unsupported icon subimage format (rebuild PIL ' - 'with JPEG 2000 support to fix this)') + raise ValueError( + "Unsupported icon subimage format (rebuild PIL " + "with JPEG 2000 support to fix this)" + ) # j2k, jpc or j2c fobj.seek(start) jp2kstream = fobj.read(length) f = io.BytesIO(jp2kstream) im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) - if im.mode != 'RGBA': - im = im.convert('RGBA') + if im.mode != "RGBA": + im = im.convert("RGBA") return {"RGBA": im} else: - raise ValueError('Unsupported icon subimage format') + raise ValueError("Unsupported icon subimage format") -class IcnsFile(object): +class IcnsFile: SIZES = { - (512, 512, 2): [ - (b'ic10', read_png_or_jpeg2000), - ], - (512, 512, 1): [ - (b'ic09', read_png_or_jpeg2000), - ], - (256, 256, 2): [ - (b'ic14', read_png_or_jpeg2000), - ], - (256, 256, 1): [ - (b'ic08', read_png_or_jpeg2000), - ], - (128, 128, 2): [ - (b'ic13', read_png_or_jpeg2000), - ], + (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], + (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], + (256, 256, 2): [(b"ic14", read_png_or_jpeg2000)], + (256, 256, 1): [(b"ic08", read_png_or_jpeg2000)], + (128, 128, 2): [(b"ic13", read_png_or_jpeg2000)], (128, 128, 1): [ - (b'ic07', read_png_or_jpeg2000), - (b'it32', read_32t), - (b't8mk', read_mk), - ], - (64, 64, 1): [ - (b'icp6', read_png_or_jpeg2000), - ], - (32, 32, 2): [ - (b'ic12', read_png_or_jpeg2000), - ], - (48, 48, 1): [ - (b'ih32', read_32), - (b'h8mk', read_mk), + (b"ic07", read_png_or_jpeg2000), + (b"it32", read_32t), + (b"t8mk", read_mk), ], + (64, 64, 1): [(b"icp6", read_png_or_jpeg2000)], + (32, 32, 2): [(b"ic12", read_png_or_jpeg2000)], + (48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)], (32, 32, 1): [ - (b'icp5', read_png_or_jpeg2000), - (b'il32', read_32), - (b'l8mk', read_mk), - ], - (16, 16, 2): [ - (b'ic11', read_png_or_jpeg2000), + (b"icp5", read_png_or_jpeg2000), + (b"il32", read_32), + (b"l8mk", read_mk), ], + (16, 16, 2): [(b"ic11", read_png_or_jpeg2000)], (16, 16, 1): [ - (b'icp4', read_png_or_jpeg2000), - (b'is32', read_32), - (b's8mk', read_mk), + (b"icp4", read_png_or_jpeg2000), + (b"is32", read_32), + (b"s8mk", read_mk), ], } @@ -185,17 +166,17 @@ class IcnsFile(object): self.dct = dct = {} self.fobj = fobj sig, filesize = nextheader(fobj) - if sig != b'icns': - raise SyntaxError('not an icns file') + if sig != b"icns": + raise SyntaxError("not an icns file") i = HEADERSIZE while i < filesize: sig, blocksize = nextheader(fobj) if blocksize <= 0: - raise SyntaxError('invalid block header') + raise SyntaxError("invalid block header") i += HEADERSIZE blocksize -= HEADERSIZE dct[sig] = (i, blocksize) - fobj.seek(blocksize, 1) + fobj.seek(blocksize, io.SEEK_CUR) i += blocksize def itersizes(self): @@ -233,7 +214,7 @@ class IcnsFile(object): size = (size[0], size[1], 1) channels = self.dataforsize(size) - im = channels.get('RGBA', None) + im = channels.get("RGBA", None) if im: return im @@ -248,6 +229,7 @@ class IcnsFile(object): ## # Image plugin for Mac OS icons. + class IcnsImageFile(ImageFile.ImageFile): """ PIL image support for Mac OS .icns files. @@ -264,13 +246,13 @@ class IcnsImageFile(ImageFile.ImageFile): def _open(self): self.icns = IcnsFile(self.fp) - self.mode = 'RGBA' - self.info['sizes'] = self.icns.itersizes() + self.mode = "RGBA" + self.info["sizes"] = self.icns.itersizes() self.best_size = self.icns.bestsize() - self.size = (self.best_size[0] * self.best_size[2], - self.best_size[1] * self.best_size[2]) - # Just use this to see if it's loaded or not yet. - self.tile = ('',) + self.size = ( + self.best_size[0] * self.best_size[2], + self.best_size[1] * self.best_size[2], + ) @property def size(self): @@ -279,27 +261,33 @@ class IcnsImageFile(ImageFile.ImageFile): @size.setter def size(self, value): info_size = value - if info_size not in self.info['sizes'] and len(info_size) == 2: + if info_size not in self.info["sizes"] and len(info_size) == 2: info_size = (info_size[0], info_size[1], 1) - if info_size not in self.info['sizes'] and len(info_size) == 3 and \ - info_size[2] == 1: - simple_sizes = [(size[0] * size[2], size[1] * size[2]) - for size in self.info['sizes']] + if ( + info_size not in self.info["sizes"] + and len(info_size) == 3 + and info_size[2] == 1 + ): + simple_sizes = [ + (size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"] + ] if value in simple_sizes: - info_size = self.info['sizes'][simple_sizes.index(value)] - if info_size not in self.info['sizes']: - raise ValueError( - "This is not one of the allowed sizes of this image") + info_size = self.info["sizes"][simple_sizes.index(value)] + if info_size not in self.info["sizes"]: + raise ValueError("This is not one of the allowed sizes of this image") self._size = value def load(self): if len(self.size) == 3: self.best_size = self.size - self.size = (self.best_size[0] * self.best_size[2], - self.best_size[1] * self.best_size[2]) + self.size = ( + self.best_size[0] * self.best_size[2], + self.best_size[1] * self.best_size[2], + ) Image.Image.load(self) - if not self.tile: + if self.im and self.im.size == self.size: + # Already loaded return self.load_prepare() # This is likely NOT the best way to do it, but whatever. @@ -311,11 +299,6 @@ class IcnsImageFile(ImageFile.ImageFile): self.im = im.im self.mode = im.mode self.size = im.size - if self._exclusive_fp: - self.fp.close() - self.fp = None - self.icns = None - self.tile = () self.load_end() @@ -331,67 +314,64 @@ def _save(im, fp, filename): fp.flush() # create the temporary set of pngs - iconset = tempfile.mkdtemp('.iconset') - provided_images = {im.width: im - for im in im.encoderinfo.get("append_images", [])} - last_w = None - second_path = None - for w in [16, 32, 128, 256, 512]: - prefix = 'icon_{}x{}'.format(w, w) + with tempfile.TemporaryDirectory(".iconset") as iconset: + provided_images = { + im.width: im for im in im.encoderinfo.get("append_images", []) + } + last_w = None + second_path = None + for w in [16, 32, 128, 256, 512]: + prefix = "icon_{}x{}".format(w, w) - first_path = os.path.join(iconset, prefix+'.png') - if last_w == w: - shutil.copyfile(second_path, first_path) - else: - im_w = provided_images.get(w, im.resize((w, w), Image.LANCZOS)) - im_w.save(first_path) + first_path = os.path.join(iconset, prefix + ".png") + if last_w == w: + shutil.copyfile(second_path, first_path) + else: + im_w = provided_images.get(w, im.resize((w, w), Image.LANCZOS)) + im_w.save(first_path) - second_path = os.path.join(iconset, prefix+'@2x.png') - im_w2 = provided_images.get(w*2, im.resize((w*2, w*2), Image.LANCZOS)) - im_w2.save(second_path) - last_w = w*2 + second_path = os.path.join(iconset, prefix + "@2x.png") + im_w2 = provided_images.get(w * 2, im.resize((w * 2, w * 2), Image.LANCZOS)) + im_w2.save(second_path) + last_w = w * 2 - # iconutil -c icns -o {} {} - from subprocess import Popen, PIPE, CalledProcessError + # iconutil -c icns -o {} {} - convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] - with open(os.devnull, 'wb') as devnull: - convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=devnull) + convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] + convert_proc = subprocess.Popen( + convert_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) - convert_proc.stdout.close() + convert_proc.stdout.close() - retcode = convert_proc.wait() + retcode = convert_proc.wait() - # remove the temporary files - shutil.rmtree(iconset) - - if retcode: - raise CalledProcessError(retcode, convert_cmd) + if retcode: + raise subprocess.CalledProcessError(retcode, convert_cmd) -Image.register_open(IcnsImageFile.format, IcnsImageFile, - lambda x: x[:4] == b'icns') -Image.register_extension(IcnsImageFile.format, '.icns') +Image.register_open(IcnsImageFile.format, IcnsImageFile, lambda x: x[:4] == b"icns") +Image.register_extension(IcnsImageFile.format, ".icns") -if sys.platform == 'darwin': +if sys.platform == "darwin": Image.register_save(IcnsImageFile.format, _save) Image.register_mime(IcnsImageFile.format, "image/icns") -if __name__ == '__main__': +if __name__ == "__main__": if len(sys.argv) < 2: print("Syntax: python IcnsImagePlugin.py [file]") sys.exit() - imf = IcnsImageFile(open(sys.argv[1], 'rb')) - for size in imf.info['sizes']: + imf = IcnsImageFile(open(sys.argv[1], "rb")) + for size in imf.info["sizes"]: imf.size = size imf.load() im = imf.im - im.save('out-%s-%s-%s.png' % size) + im.save("out-%s-%s-%s.png" % size) im = Image.open(sys.argv[1]) im.save("out.png") - if sys.platform == 'windows': + if sys.platform == "windows": os.startfile("out.png") diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 16683fb8f..19cb548a6 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -23,15 +23,12 @@ import struct +import warnings from io import BytesIO +from math import ceil, log -from . import Image, ImageFile, BmpImagePlugin, PngImagePlugin +from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from ._binary import i8, i16le as i16, i32le as i32 -from math import log, ceil - -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" # # -------------------------------------------------------------------- @@ -41,16 +38,20 @@ _MAGIC = b"\0\0\1\0" def _save(im, fp, filename): fp.write(_MAGIC) # (2+2) - sizes = im.encoderinfo.get("sizes", - [(16, 16), (24, 24), (32, 32), (48, 48), - (64, 64), (128, 128), (256, 256)]) + sizes = im.encoderinfo.get( + "sizes", + [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], + ) width, height = im.size - sizes = filter(lambda x: False if (x[0] > width or x[1] > height or - x[0] > 256 or x[1] > 256) else True, - sizes) + sizes = filter( + lambda x: False + if (x[0] > width or x[1] > height or x[0] > 256 or x[1] > 256) + else True, + sizes, + ) sizes = list(sizes) fp.write(struct.pack("=8bpp) - 'reserved': i8(s[3]), - 'planes': i16(s[4:]), - 'bpp': i16(s[6:]), - 'size': i32(s[8:]), - 'offset': i32(s[12:]) + "width": i8(s[0]), + "height": i8(s[1]), + "nb_color": i8(s[2]), # No. of colors in image (0 if >=8bpp) + "reserved": i8(s[3]), + "planes": i16(s[4:]), + "bpp": i16(s[6:]), + "size": i32(s[8:]), + "offset": i32(s[12:]), } # See Wikipedia - for j in ('width', 'height'): + for j in ("width", "height"): if not icon_header[j]: icon_header[j] = 256 # See Wikipedia notes about color depth. # We need this just to differ images with equal sizes - icon_header['color_depth'] = (icon_header['bpp'] or - (icon_header['nb_color'] != 0 and - ceil(log(icon_header['nb_color'], - 2))) or 256) + icon_header["color_depth"] = ( + icon_header["bpp"] + or ( + icon_header["nb_color"] != 0 + and ceil(log(icon_header["nb_color"], 2)) + ) + or 256 + ) - icon_header['dim'] = (icon_header['width'], icon_header['height']) - icon_header['square'] = (icon_header['width'] * - icon_header['height']) + icon_header["dim"] = (icon_header["width"], icon_header["height"]) + icon_header["square"] = icon_header["width"] * icon_header["height"] self.entry.append(icon_header) - self.entry = sorted(self.entry, key=lambda x: x['color_depth']) + self.entry = sorted(self.entry, key=lambda x: x["color_depth"]) # ICO images are usually squares # self.entry = sorted(self.entry, key=lambda x: x['width']) - self.entry = sorted(self.entry, key=lambda x: x['square']) + self.entry = sorted(self.entry, key=lambda x: x["square"]) self.entry.reverse() def sizes(self): """ Get a list of all available icon sizes and color depths. """ - return {(h['width'], h['height']) for h in self.entry} + return {(h["width"], h["height"]) for h in self.entry} + + def getentryindex(self, size, bpp=False): + for (i, h) in enumerate(self.entry): + if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): + return i + return 0 def getimage(self, size, bpp=False): """ Get an image from the icon """ - for (i, h) in enumerate(self.entry): - if size == h['dim'] and (bpp is False or bpp == h['color_depth']): - return self.frame(i) - return self.frame(0) + return self.frame(self.getentryindex(size, bpp)) def frame(self, idx): """ @@ -159,9 +166,9 @@ class IcoFile(object): header = self.entry[idx] - self.buf.seek(header['offset']) + self.buf.seek(header["offset"]) data = self.buf.read(8) - self.buf.seek(header['offset']) + self.buf.seek(header["offset"]) if data[:8] == PngImagePlugin._MAGIC: # png frame @@ -169,6 +176,7 @@ class IcoFile(object): else: # XOR + AND mask bmp frame im = BmpImagePlugin.DibImageFile(self.buf) + Image._decompression_bomb_check(im.size) # change tile dimension to only encompass XOR image im._size = (im.size[0], int(im.size[1] / 2)) @@ -196,11 +204,11 @@ class IcoFile(object): # convert to an 8bpp grayscale image mask = Image.frombuffer( - 'L', # 8bpp - im.size, # (w, h) - alpha_bytes, # source chars - 'raw', # raw decoder - ('L', 0, -1) # 8bpp inverted, unpadded, reversed + "L", # 8bpp + im.size, # (w, h) + alpha_bytes, # source chars + "raw", # raw decoder + ("L", 0, -1), # 8bpp inverted, unpadded, reversed ) else: # get AND image from end of bitmap @@ -212,8 +220,7 @@ class IcoFile(object): # the total mask data is # padded row size * height / bits per char - and_mask_offset = o + int(im.size[0] * im.size[1] * - (bpp / 8.0)) + and_mask_offset = o + int(im.size[0] * im.size[1] * (bpp / 8.0)) total_bytes = int((w * im.size[1]) / 8) self.buf.seek(and_mask_offset) @@ -221,17 +228,17 @@ class IcoFile(object): # convert raw data to image mask = Image.frombuffer( - '1', # 1 bpp - im.size, # (w, h) - mask_data, # source chars - 'raw', # raw decoder - ('1;I', int(w/8), -1) # 1bpp inverted, padded, reversed + "1", # 1 bpp + im.size, # (w, h) + mask_data, # source chars + "raw", # raw decoder + ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed ) # now we have two images, im is XOR image and mask is AND image # apply mask image as alpha channel - im = im.convert('RGBA') + im = im.convert("RGBA") im.putalpha(mask) return im @@ -240,6 +247,7 @@ class IcoFile(object): ## # Image plugin for Windows Icon files. + class IcoImageFile(ImageFile.ImageFile): """ PIL read-only image support for Microsoft Windows .ico files. @@ -252,17 +260,21 @@ class IcoImageFile(ImageFile.ImageFile): Handles classic, XP and Vista icon formats. + When saving, PNG compression is used. Support for this was only added in + Windows Vista. + This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis . https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki """ + format = "ICO" format_description = "Windows Icon" def _open(self): self.ico = IcoFile(self.fp) - self.info['sizes'] = self.ico.sizes() - self.size = self.ico.entry[0]['dim'] + self.info["sizes"] = self.ico.sizes() + self.size = self.ico.entry[0]["dim"] self.load() @property @@ -271,23 +283,35 @@ class IcoImageFile(ImageFile.ImageFile): @size.setter def size(self, value): - if value not in self.info['sizes']: - raise ValueError( - "This is not one of the allowed sizes of this image") + if value not in self.info["sizes"]: + raise ValueError("This is not one of the allowed sizes of this image") self._size = value def load(self): + if self.im and self.im.size == self.size: + # Already loaded + return im = self.ico.getimage(self.size) # if tile is PNG, it won't really be loaded yet im.load() self.im = im.im self.mode = im.mode - self.size = im.size + if im.size != self.size: + warnings.warn("Image was not the expected size") + + index = self.ico.getentryindex(self.size) + sizes = list(self.info["sizes"]) + sizes[index] = im.size + self.info["sizes"] = set(sizes) + + self.size = im.size def load_seek(self): # Flag the ImageFile.Parser so that it # just does all the decode at the end. pass + + # # -------------------------------------------------------------------- @@ -295,3 +319,5 @@ class IcoImageFile(ImageFile.ImageFile): Image.register_open(IcoImageFile.format, IcoImageFile, _accept) Image.register_save(IcoImageFile.format, _save) Image.register_extension(IcoImageFile.format, ".ico") + +Image.register_mime(IcoImageFile.format, "image/x-icon") diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 08250e959..12c9237f0 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -27,14 +27,10 @@ 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 @@ -48,8 +44,17 @@ SCALE = "Scale (x,y)" SIZE = "Image size (x*y)" MODE = "Image type" -TAGS = {COMMENT: 0, DATE: 0, EQUIPMENT: 0, FRAMES: 0, LUT: 0, NAME: 0, - SCALE: 0, SIZE: 0, MODE: 0} +TAGS = { + COMMENT: 0, + DATE: 0, + EQUIPMENT: 0, + FRAMES: 0, + LUT: 0, + NAME: 0, + SCALE: 0, + SIZE: 0, + MODE: 0, +} OPEN = { # ifunc93/p3cfunc formats @@ -71,6 +76,7 @@ OPEN = { "RYB3 image": ("RGB", "RYB;T"), # extensions "LA image": ("LA", "LA;L"), + "PA image": ("LA", "PA;L"), "RGBA image": ("RGBA", "RGBA;L"), "RGBX image": ("RGBX", "RGBX;L"), "CMYK image": ("CMYK", "CMYK;L"), @@ -107,6 +113,7 @@ def number(s): ## # Image plugin for the IFUNC IM file format. + class ImImageFile(ImageFile.ImageFile): format = "IM" @@ -139,7 +146,7 @@ class ImImageFile(ImageFile.ImageFile): if s == b"\r": continue - if not s or s == b'\0' or s == b'\x1A': + if not s or s == b"\0" or s == b"\x1A": break # FIXME: this may read whole file if not a text file @@ -148,9 +155,9 @@ class ImImageFile(ImageFile.ImageFile): if len(s) > 100: raise SyntaxError("not an IM file") - if s[-2:] == b'\r\n': + if s[-2:] == b"\r\n": s = s[:-2] - elif s[-1:] == b'\n': + elif s[-1:] == b"\n": s = s[:-1] try: @@ -164,8 +171,8 @@ class ImImageFile(ImageFile.ImageFile): # Don't know if this is the correct encoding, # but a decent guess (I guess) - k = k.decode('latin-1', 'replace') - v = v.decode('latin-1', 'replace') + k = k.decode("latin-1", "replace") + v = v.decode("latin-1", "replace") # Convert value as appropriate if k in [FRAMES, SCALE, SIZE]: @@ -191,8 +198,9 @@ class ImImageFile(ImageFile.ImageFile): else: - raise SyntaxError("Syntax error in IM header: " + - s.decode('ascii', 'replace')) + raise SyntaxError( + "Syntax error in IM header: " + s.decode("ascii", "replace") + ) if not n: raise SyntaxError("Not an IM file") @@ -202,7 +210,7 @@ class ImImageFile(ImageFile.ImageFile): self.mode = self.info[MODE] # Skip forward to start of image data - while s and s[0:1] != b'\x1A': + while s and s[0:1] != b"\x1A": s = self.fp.read(1) if not s: raise SyntaxError("File truncated") @@ -213,20 +221,21 @@ class ImImageFile(ImageFile.ImageFile): greyscale = 1 # greyscale palette linear = 1 # linear greyscale palette for i in range(256): - if palette[i] == palette[i+256] == palette[i+512]: + if palette[i] == palette[i + 256] == palette[i + 512]: if i8(palette[i]) != i: linear = 0 else: greyscale = 0 - if self.mode == "L" or self.mode == "LA": + if self.mode in ["L", "LA", "P", "PA"]: if greyscale: if not linear: self.lut = [i8(c) for c in palette[:256]] else: - if self.mode == "L": + if self.mode in ["L", "P"]: self.mode = self.rawmode = "P" - elif self.mode == "LA": - self.mode = self.rawmode = "PA" + elif self.mode in ["LA", "PA"]: + self.mode = "PA" + self.rawmode = "PA;L" self.palette = ImagePalette.raw("RGB;L", palette) elif self.mode == "RGB": if not greyscale or not linear: @@ -245,8 +254,7 @@ class ImImageFile(ImageFile.ImageFile): # use bit decoder (if necessary) bits = int(self.rawmode[2:]) if bits not in [8, 16, 32]: - self.tile = [("bit", (0, 0)+self.size, offs, - (bits, 8, 3, 0, -1))] + self.tile = [("bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1))] return except ValueError: pass @@ -255,13 +263,14 @@ class ImImageFile(ImageFile.ImageFile): # Old LabEye/3PC files. Would be very surprised if anyone # ever stumbled upon such a file ;-) size = self.size[0] * self.size[1] - self.tile = [("raw", (0, 0)+self.size, offs, ("G", 0, -1)), - ("raw", (0, 0)+self.size, offs+size, ("R", 0, -1)), - ("raw", (0, 0)+self.size, offs+2*size, ("B", 0, -1))] + self.tile = [ + ("raw", (0, 0) + self.size, offs, ("G", 0, -1)), + ("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)), + ("raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)), + ] else: # LabEye/IFUNC files - self.tile = [("raw", (0, 0)+self.size, offs, - (self.rawmode, 0, -1))] + self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] @property def n_frames(self): @@ -287,7 +296,7 @@ class ImImageFile(ImageFile.ImageFile): self.fp = self.__fp - self.tile = [("raw", (0, 0)+self.size, offs, (self.rawmode, 0, -1))] + self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] def tell(self): return self.frame @@ -301,6 +310,7 @@ class ImImageFile(ImageFile.ImageFile): finally: self.__fp = None + # # -------------------------------------------------------------------- # Save IM files @@ -322,7 +332,7 @@ SAVE = { "RGBA": ("RGBA", "RGBA;L"), "RGBX": ("RGBX", "RGBX;L"), "CMYK": ("CMYK", "CMYK;L"), - "YCbCr": ("YCC", "YCbCr;L") + "YCbCr": ("YCC", "YCbCr;L"), } @@ -335,17 +345,18 @@ def _save(im, fp, filename): frames = im.encoderinfo.get("frames", 1) - fp.write(("Image type: %s image\r\n" % image_type).encode('ascii')) + fp.write(("Image type: %s image\r\n" % image_type).encode("ascii")) if filename: - fp.write(("Name: %s\r\n" % filename).encode('ascii')) - fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode('ascii')) - fp.write(("File size (no of images): %d\r\n" % frames).encode('ascii')) - if im.mode == "P": + fp.write(("Name: %s\r\n" % filename).encode("ascii")) + fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii")) + fp.write(("File size (no of images): %d\r\n" % frames).encode("ascii")) + if im.mode in ["P", "PA"]: fp.write(b"Lut: 1\r\n") - fp.write(b"\000" * (511-fp.tell()) + b"\032") - if im.mode == "P": + fp.write(b"\000" * (511 - fp.tell()) + b"\032") + if im.mode in ["P", "PA"]: fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes - ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, 0, -1))]) + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) + # # -------------------------------------------------------------------- diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ef4a7a88a..30631ef60 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -24,45 +24,26 @@ # See the README file for information on usage and redistribution. # -# VERSION is deprecated and will be removed in Pillow 6.0.0. -# PILLOW_VERSION is deprecated and will be removed after that. -# Use __version__ instead. -from . import VERSION, PILLOW_VERSION, __version__, _plugins -from ._util import py3 - -import logging -import warnings -import math - -try: - import builtins -except ImportError: - import __builtin__ - builtins = __builtin__ - -from . import ImageMode -from ._binary import i8 -from ._util import isPath, isStringType, deferred_error - -import os -import sys -import io -import struct import atexit - -# type stuff +import builtins +import io +import logging +import math import numbers -try: - # Python 3 - from collections.abc import Callable -except ImportError: - # Python 2.7 - from collections import Callable +import os +import struct +import sys +import tempfile +import warnings +from collections.abc import Callable, MutableMapping +from pathlib import Path - -# Silence warnings -assert VERSION -assert PILLOW_VERSION +# VERSION was removed in Pillow 6.0.0. +# PILLOW_VERSION was removed in Pillow 7.0.0. +# Use __version__ instead. +from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins +from ._binary import i8, i32le +from ._util import deferred_error, isPath logger = logging.getLogger(__name__) @@ -75,12 +56,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) @@ -92,66 +67,39 @@ try: # Also note that Image.core is not a publicly documented interface, # and should be considered private and subject to change. from . import _imaging as core - if __version__ != getattr(core, 'PILLOW_VERSION', None): - raise ImportError("The _imaging extension was built for another " - "version of Pillow or PIL:\n" - "Core version: %s\n" - "Pillow version: %s" % - (getattr(core, 'PILLOW_VERSION', None), - __version__)) + + if __version__ != getattr(core, "PILLOW_VERSION", None): + raise ImportError( + "The _imaging extension was built for another version of Pillow or PIL:\n" + "Core version: %s\n" + "Pillow version: %s" % (getattr(core, "PILLOW_VERSION", None), __version__) + ) 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 # the right version (windows only). Print a warning, if # possible. warnings.warn( - "The _imaging extension was built for another version " - "of Python.", - RuntimeWarning - ) + "The _imaging extension was built for another version of Python.", + RuntimeWarning, + ) 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 # works everywhere, win for pypy, not cpython -USE_CFFI_ACCESS = hasattr(sys, 'pypy_version_info') +USE_CFFI_ACCESS = hasattr(sys, "pypy_version_info") try: import cffi 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): """ @@ -216,7 +164,7 @@ NORMAL = 0 SEQUENCE = 1 CONTAINER = 2 -if hasattr(core, 'DEFAULT_STRATEGY'): +if hasattr(core, "DEFAULT_STRATEGY"): DEFAULT_STRATEGY = core.DEFAULT_STRATEGY FILTERED = core.FILTERED HUFFMAN_ONLY = core.HUFFMAN_ONLY @@ -242,13 +190,12 @@ ENCODERS = {} _MODEINFO = { # NOTE: this table will be removed in future versions. use # getmode* functions or ImageMode descriptors instead. - # official modes "1": ("L", "L", ("1",)), "L": ("L", "L", ("L",)), "I": ("L", "I", ("I",)), "F": ("L", "F", ("F",)), - "P": ("RGB", "L", ("P",)), + "P": ("P", "L", ("P",)), "RGB": ("RGB", "L", ("R", "G", "B")), "RGBX": ("RGB", "L", ("R", "G", "B", "X")), "RGBA": ("RGB", "L", ("R", "G", "B", "A")), @@ -256,46 +203,44 @@ _MODEINFO = { "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), "LAB": ("RGB", "L", ("L", "A", "B")), "HSV": ("RGB", "L", ("H", "S", "V")), - # Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and # BGR;24. Use these modes only if you know exactly what you're # doing... - } -if sys.byteorder == 'little': - _ENDIAN = '<' +if sys.byteorder == "little": + _ENDIAN = "<" else: - _ENDIAN = '>' + _ENDIAN = ">" _MODE_CONV = { # official modes - "1": ('|b1', None), # Bits need to be extended to bytes - "L": ('|u1', None), - "LA": ('|u1', 2), - "I": (_ENDIAN + 'i4', None), - "F": (_ENDIAN + 'f4', None), - "P": ('|u1', None), - "RGB": ('|u1', 3), - "RGBX": ('|u1', 4), - "RGBA": ('|u1', 4), - "CMYK": ('|u1', 4), - "YCbCr": ('|u1', 3), - "LAB": ('|u1', 3), # UNDONE - unsigned |u1i1i1 - "HSV": ('|u1', 3), + "1": ("|b1", None), # Bits need to be extended to bytes + "L": ("|u1", None), + "LA": ("|u1", 2), + "I": (_ENDIAN + "i4", None), + "F": (_ENDIAN + "f4", None), + "P": ("|u1", None), + "RGB": ("|u1", 3), + "RGBX": ("|u1", 4), + "RGBA": ("|u1", 4), + "CMYK": ("|u1", 4), + "YCbCr": ("|u1", 3), + "LAB": ("|u1", 3), # UNDONE - unsigned |u1i1i1 + "HSV": ("|u1", 3), # I;16 == I;16L, and I;32 == I;32L - "I;16": ('u2', None), - "I;16L": ('i2', None), - "I;16LS": ('u4', None), - "I;32L": ('i4', None), - "I;32LS": ('u2", None), + "I;16L": ("i2", None), + "I;16LS": ("u4", None), + "I;32L": ("i4", None), + "I;32LS": ("= 3: - def __del__(self): - if hasattr(self, "_close__fp"): - self._close__fp() - if (hasattr(self, 'fp') and hasattr(self, '_exclusive_fp') - and self.fp and self._exclusive_fp): - self.fp.close() - self.fp = None - def _copy(self): self.load() self.im = self.im.copy() @@ -631,11 +588,9 @@ class Image(object): self.load() def _dump(self, file=None, format=None, **options): - import tempfile - - suffix = '' + suffix = "" if format: - suffix = '.'+format + suffix = "." + format if not file: f, filename = tempfile.mkstemp(suffix) @@ -655,35 +610,34 @@ class Image(object): return filename def __eq__(self, other): - return (isinstance(other, Image) and - self.__class__.__name__ == other.__class__.__name__ and - self.mode == other.mode and - self.size == other.size and - self.info == other.info and - self.category == other.category and - self.readonly == other.readonly and - self.getpalette() == other.getpalette() and - self.tobytes() == other.tobytes()) - - def __ne__(self, other): - eq = (self == other) - return not eq + return ( + self.__class__ is other.__class__ + and self.mode == other.mode + and self.size == other.size + and self.info == other.info + and self.category == other.category + and self.readonly == other.readonly + and self.getpalette() == other.getpalette() + and self.tobytes() == other.tobytes() + ) def __repr__(self): return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( - self.__class__.__module__, self.__class__.__name__, - self.mode, self.size[0], self.size[1], - id(self) - ) + self.__class__.__module__, + self.__class__.__name__, + self.mode, + self.size[0], + self.size[1], + id(self), + ) def _repr_png_(self): """ iPython display hook support :returns: png version of the image as bytes """ - from io import BytesIO - b = BytesIO() - self.save(b, 'PNG') + b = io.BytesIO() + self.save(b, "PNG") return b.getvalue() @property @@ -691,24 +645,19 @@ class Image(object): # numpy array interface support new = {} shape, typestr = _conv_type_shape(self) - new['shape'] = shape - new['typestr'] = typestr - new['version'] = 3 - if self.mode == '1': + new["shape"] = shape + new["typestr"] = typestr + new["version"] = 3 + if self.mode == "1": # Binary images need to be extended from bits to bytes # See: https://github.com/python-pillow/Pillow/issues/350 - new['data'] = self.tobytes('raw', 'L') + new["data"] = self.tobytes("raw", "L") else: - new['data'] = self.tobytes() + new["data"] = self.tobytes() return new def __getstate__(self): - return [ - self.info, - self.mode, - self.size, - self.getpalette(), - self.tobytes()] + return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()] def __setstate__(self, state): Image.__init__(self) @@ -718,7 +667,7 @@ class Image(object): self.mode = mode self._size = size self.im = core.new(mode, size) - if mode in ("L", "P") and palette: + if mode in ("L", "LA", "P", "PA") and palette: self.putpalette(palette) self.frombytes(data) @@ -766,8 +715,9 @@ class Image(object): return b"".join(data) def tostring(self, *args, **kw): - raise NotImplementedError("tostring() has been removed. " - "Please call tobytes() instead.") + raise NotImplementedError( + "tostring() has been removed. Please call tobytes() instead." + ) def tobitmap(self, name="image"): """ @@ -784,11 +734,15 @@ class Image(object): if self.mode != "1": raise ValueError("not a bitmap") data = self.tobytes("xbm") - return b"".join([ - ("#define %s_width %d\n" % (name, self.size[0])).encode('ascii'), - ("#define %s_height %d\n" % (name, self.size[1])).encode('ascii'), - ("static char %s_bits[] = {\n" % name).encode('ascii'), data, b"};" - ]) + return b"".join( + [ + ("#define %s_width %d\n" % (name, self.size[0])).encode("ascii"), + ("#define %s_height %d\n" % (name, self.size[1])).encode("ascii"), + ("static char %s_bits[] = {\n" % name).encode("ascii"), + data, + b"};", + ] + ) def frombytes(self, data, decoder_name="raw", *args): """ @@ -817,8 +771,9 @@ class Image(object): raise ValueError("cannot decode image data") def fromstring(self, *args, **kw): - raise NotImplementedError("fromstring() has been removed. " - "Please call frombytes() instead.") + raise NotImplementedError( + "fromstring() has been removed. Please call frombytes() instead." + ) def load(self): """ @@ -853,6 +808,7 @@ class Image(object): if self.pyaccess: return self.pyaccess from . import PyAccess + self.pyaccess = PyAccess.new(self, self.readonly) if self.pyaccess: return self.pyaccess @@ -869,8 +825,7 @@ class Image(object): """ pass - def convert(self, mode=None, matrix=None, dither=None, - palette=WEB, colors=256): + def convert(self, mode=None, matrix=None, dither=None, palette=WEB, colors=256): """ Returns a converted copy of this image. For the "P" mode, this method translates pixels through the palette. If mode is @@ -923,7 +878,7 @@ class Image(object): if not mode or (mode == self.mode and not matrix): return self.copy() - has_transparency = self.info.get('transparency') is not None + has_transparency = self.info.get("transparency") is not None if matrix: # matrix conversion if mode not in ("L", "RGB"): @@ -931,19 +886,24 @@ class Image(object): im = self.im.convert_matrix(mode, matrix) new = self._new(im) if has_transparency and self.im.bands == 3: - transparency = new.info['transparency'] + transparency = new.info["transparency"] def convert_transparency(m, v): - v = m[0]*v[0] + m[1]*v[1] + m[2]*v[2] + m[3]*0.5 + v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 return max(0, min(255, int(v))) + if mode == "L": transparency = convert_transparency(matrix, transparency) elif len(mode) == 3: - transparency = tuple([ - convert_transparency(matrix[i*4:i*4+4], transparency) - for i in range(0, len(transparency)) - ]) - new.info['transparency'] = transparency + transparency = tuple( + [ + convert_transparency( + matrix[i * 4 : i * 4 + 4], transparency + ) + for i in range(0, len(transparency)) + ] + ) + new.info["transparency"] = transparency return new if mode == "P" and self.mode == "RGBA": @@ -953,45 +913,48 @@ class Image(object): delete_trns = False # transparency handling if has_transparency: - if self.mode in ('L', 'RGB') and mode == 'RGBA': + if self.mode in ("1", "L", "I", "RGB") and mode == "RGBA": # Use transparent conversion to promote from transparent # color to an alpha channel. - new_im = self._new(self.im.convert_transparent( - mode, self.info['transparency'])) - del new_im.info['transparency'] + new_im = self._new( + self.im.convert_transparent(mode, self.info["transparency"]) + ) + del new_im.info["transparency"] return new_im - elif self.mode in ('L', 'RGB', 'P') and mode in ('L', 'RGB', 'P'): - t = self.info['transparency'] + elif self.mode in ("L", "RGB", "P") and mode in ("L", "RGB", "P"): + t = self.info["transparency"] if isinstance(t, bytes): # Dragons. This can't be represented by a single color - warnings.warn('Palette images with Transparency ' + - ' expressed in bytes should be converted ' + - 'to RGBA images') + warnings.warn( + "Palette images with Transparency expressed in bytes should be " + "converted to RGBA images" + ) delete_trns = True else: # get the new transparency color. # use existing conversions trns_im = Image()._new(core.new(self.mode, (1, 1))) - if self.mode == 'P': + if self.mode == "P": trns_im.putpalette(self.palette) if isinstance(t, tuple): try: t = trns_im.palette.getcolor(t) except Exception: - raise ValueError("Couldn't allocate a palette " - "color for transparency") + raise ValueError( + "Couldn't allocate a palette color for transparency" + ) trns_im.putpixel((0, 0), t) - if mode in ('L', 'RGB'): + if mode in ("L", "RGB"): trns_im = trns_im.convert(mode) else: # can't just retrieve the palette number, got to do it # after quantization. - trns_im = trns_im.convert('RGB') + trns_im = trns_im.convert("RGB") trns = trns_im.getpixel((0, 0)) - elif self.mode == 'P' and mode == 'RGBA': - t = self.info['transparency'] + elif self.mode == "P" and mode == "RGBA": + t = self.info["transparency"] delete_trns = True if isinstance(t, bytes): @@ -999,27 +962,26 @@ class Image(object): elif isinstance(t, int): self.im.putpalettealpha(t, 0) else: - raise ValueError("Transparency for P mode should" + - " be bytes or int") + raise ValueError("Transparency for P mode should be bytes or int") if mode == "P" and palette == ADAPTIVE: im = self.im.quantize(colors) new = self._new(im) from . import ImagePalette + new.palette = ImagePalette.raw("RGB", new.im.getpalette("RGB")) if delete_trns: # This could possibly happen if we requantize to fewer colors. # The transparency would be totally off in that case. - del new.info['transparency'] + del new.info["transparency"] if trns is not None: try: - new.info['transparency'] = new.palette.getcolor(trns) + new.info["transparency"] = new.palette.getcolor(trns) except Exception: # if we can't make a transparent color, don't leave the old # transparency hanging around to mess us up. - del new.info['transparency'] - warnings.warn("Couldn't allocate palette entry " + - "for transparency") + del new.info["transparency"] + warnings.warn("Couldn't allocate palette entry for transparency") return new # colorspace conversion @@ -1039,20 +1001,19 @@ class Image(object): new_im = self._new(im) if delete_trns: # crash fail if we leave a bytes transparency in an rgb/l mode. - del new_im.info['transparency'] + del new_im.info["transparency"] if trns is not None: - if new_im.mode == 'P': + if new_im.mode == "P": try: - new_im.info['transparency'] = new_im.palette.getcolor(trns) + new_im.info["transparency"] = new_im.palette.getcolor(trns) except Exception: - del new_im.info['transparency'] - warnings.warn("Couldn't allocate palette entry " + - "for transparency") + del new_im.info["transparency"] + warnings.warn("Couldn't allocate palette entry for transparency") else: - new_im.info['transparency'] = trns + new_im.info["transparency"] = trns return new_im - def quantize(self, colors=256, method=None, kmeans=0, palette=None): + def quantize(self, colors=256, method=None, kmeans=0, palette=None, dither=1): """ Convert the image to 'P' mode with the specified number of colors. @@ -1065,6 +1026,10 @@ class Image(object): :param kmeans: Integer :param palette: Quantize to the palette of given :py:class:`PIL.Image.Image`. + :param dither: Dithering method, used when converting from + mode "RGB" to "P" or from "RGB" or "L" to "1". + Available methods are NONE or FLOYDSTEINBERG (default). + Default: 1 (legacy setting) :returns: A new image """ @@ -1074,14 +1039,15 @@ class Image(object): if method is None: # defaults: method = 0 - if self.mode == 'RGBA': + if self.mode == "RGBA": method = 2 - if self.mode == 'RGBA' and method not in (2, 3): + if self.mode == "RGBA" and method not in (2, 3): # Caller specified an invalid mode. raise ValueError( - 'Fast Octree (method == 2) and libimagequant (method == 3) ' + - 'are the only valid methods for quantizing RGBA images') + "Fast Octree (method == 2) and libimagequant (method == 3) " + "are the only valid methods for quantizing RGBA images" + ) if palette: # use palette from reference image @@ -1091,11 +1057,18 @@ class Image(object): if self.mode != "RGB" and self.mode != "L": raise ValueError( "only RGB or L mode images can be quantized to a palette" - ) - im = self.im.convert("P", 1, palette.im) + ) + im = self.im.convert("P", dither, palette.im) return self._new(im) - return self._new(self.im.quantize(colors, method, kmeans)) + im = self._new(self.im.quantize(colors, method, kmeans)) + + from . import ImagePalette + + mode = im.im.getpalettemode() + im.palette = ImagePalette.ImagePalette(mode, im.im.getpalette(mode, mode)) + + return im def copy(self): """ @@ -1190,8 +1163,9 @@ class Image(object): if isinstance(filter, Callable): filter = filter() if not hasattr(filter, "filter"): - raise TypeError("filter argument should be ImageFilter.Filter " + - "instance or class") + raise TypeError( + "filter argument should be ImageFilter.Filter instance or class" + ) multiband = isinstance(filter, ImageFilter.MultibandFilter) if self.im.bands == 1 or multiband: @@ -1290,6 +1264,12 @@ class Image(object): return tuple(extrema) return self.im.getextrema() + def getexif(self): + if self._exif is None: + self._exif = Exif() + self._exif.load(self.info.get("exif")) + return self._exif + def getim(self): """ Returns a capsule that points to the internal image memory. @@ -1310,10 +1290,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 @@ -1361,6 +1338,7 @@ class Image(object): bi-level image (mode "1") or a greyscale image ("L"). :param mask: An optional mask. + :param extrema: An optional tuple of manually-specified extrema. :returns: A list containing pixel counts. """ self.load() @@ -1373,9 +1351,36 @@ class Image(object): return self.im.histogram(extrema) return self.im.histogram() + def entropy(self, mask=None, extrema=None): + """ + Calculates and returns the entropy for the image. + + A bilevel image (mode "1") is treated as a greyscale ("L") + image by this method. + + If a mask is provided, the method employs the histogram for + those parts of the image where the mask image is non-zero. + The mask image must have the same size as the image, and be + either a bi-level image (mode "1") or a greyscale image ("L"). + + :param mask: An optional mask. + :param extrema: An optional tuple of manually-specified extrema. + :returns: A float value representing the image entropy + """ + self.load() + if mask: + mask.load() + return self.im.entropy((0, 0), mask.im) + if self.mode in ("I", "F"): + if extrema is None: + extrema = self.getextrema() + return self.im.entropy(extrema) + return self.im.entropy() + def offset(self, xoffset, yoffset=None): - raise NotImplementedError("offset() has been removed. " - "Please call ImageChops.offset() instead.") + raise NotImplementedError( + "offset() has been removed. Please call ImageChops.offset() instead." + ) def paste(self, im, box=None, mask=None): """ @@ -1433,13 +1438,12 @@ class Image(object): size = mask.size else: # FIXME: use self.size here? - raise ValueError( - "cannot determine region size; use 4-item box" - ) - box += (box[0]+size[0], box[1]+size[1]) + 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) elif isImageType(im): @@ -1558,7 +1562,7 @@ class Image(object): self._ensure_mutable() - if self.mode not in ("LA", "RGBA"): + if self.mode not in ("LA", "PA", "RGBA"): # attempt to promote self to a matching alpha mode try: mode = getmodebase(self.mode) + "A" @@ -1567,7 +1571,7 @@ class Image(object): except (AttributeError, ValueError): # do things the hard way im = self.im.convert(mode) - if im.mode not in ("LA", "RGBA"): + if im.mode not in ("LA", "PA", "RGBA"): raise ValueError # sanity check self.im = im self.pyaccess = None @@ -1575,7 +1579,7 @@ class Image(object): except (KeyError, ValueError): raise ValueError("illegal image mode") - if self.mode == "LA": + if self.mode in ("LA", "PA"): band = 1 else: band = 3 @@ -1618,10 +1622,10 @@ class Image(object): def putpalette(self, data, rawmode="RGB"): """ - Attaches a palette to this image. The image must be a "P" or - "L" image, and the palette sequence must contain 768 integer - values, where each group of three values represent the red, - green, and blue values for the corresponding pixel + Attaches a palette to this image. The image must be a "P", + "PA", "L" or "LA" image, and the palette sequence must contain + 768 integer values, where each group of three values represent + the red, green, and blue values for the corresponding pixel index. Instead of an integer sequence, you can use an 8-bit string. @@ -1630,19 +1634,16 @@ class Image(object): """ from . import ImagePalette - if self.mode not in ("L", "P"): + if self.mode not in ("L", "LA", "P", "PA"): raise ValueError("illegal image mode") self.load() if isinstance(data, ImagePalette.ImagePalette): 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 = "P" + self.mode = "PA" if "A" in self.mode else "P" self.palette = palette self.palette.mode = "RGB" self.load() # install new palette @@ -1676,8 +1677,11 @@ class Image(object): if self.pyaccess: return self.pyaccess.putpixel(xy, value) - if self.mode == "P" and \ - isinstance(value, (list, tuple)) and len(value) in [3, 4]: + if ( + self.mode == "P" + and isinstance(value, (list, tuple)) + and len(value) in [3, 4] + ): # RGB or RGBA value for a P image value = self.palette.getcolor(value) return self.im.putpixel(xy, value) @@ -1687,7 +1691,7 @@ class Image(object): Rewrites the image to reorder the palette. :param dest_map: A list of indexes into the original palette. - e.g. [1,0] would swap a two item palette, and list(range(255)) + e.g. [1,0] would swap a two item palette, and list(range(256)) is the identity transform. :param source_palette: Bytes or None. :returns: An :py:class:`~PIL.Image.Image` object. @@ -1702,16 +1706,16 @@ class Image(object): if self.mode == "P": real_source_palette = self.im.getpalette("RGB")[:768] else: # L-mode - real_source_palette = bytearray(i//3 for i in range(768)) + real_source_palette = bytearray(i // 3 for i in range(768)) else: real_source_palette = source_palette palette_bytes = b"" - new_positions = [0]*256 + new_positions = [0] * 256 # pick only the used colors from the palette for i, oldPosition in enumerate(dest_map): - palette_bytes += real_source_palette[oldPosition*3:oldPosition*3+3] + palette_bytes += real_source_palette[oldPosition * 3 : oldPosition * 3 + 3] new_positions[oldPosition] = i # replace the palette color id of all pixel with the new id @@ -1735,26 +1739,25 @@ class Image(object): mapping_palette = bytearray(new_positions) m_im = self.copy() - m_im.mode = 'P' + m_im.mode = "P" - m_im.palette = ImagePalette.ImagePalette("RGB", - palette=mapping_palette*3, - size=768) + m_im.palette = ImagePalette.ImagePalette( + "RGB", palette=mapping_palette * 3, size=768 + ) # possibly set palette dirty, then # m_im.putpalette(mapping_palette, 'L') # converts to 'P' # or just force it. # UNDONE -- this is part of the general issue with palettes m_im.im.putpalette(*m_im.palette.getdata()) - m_im = m_im.convert('L') + m_im = m_im.convert("L") # Internally, we require 768 bytes for a palette. - new_palette_bytes = (palette_bytes + - (768 - len(palette_bytes)) * b'\x00') + new_palette_bytes = palette_bytes + (768 - len(palette_bytes)) * b"\x00" m_im.putpalette(new_palette_bytes) - m_im.palette = ImagePalette.ImagePalette("RGB", - palette=palette_bytes, - size=len(palette_bytes)) + m_im.palette = ImagePalette.ImagePalette( + "RGB", palette=palette_bytes, size=len(palette_bytes) + ) return m_im @@ -1778,10 +1781,23 @@ class Image(object): :returns: An :py:class:`~PIL.Image.Image` object. """ - if resample not in ( - NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING, - ): - raise ValueError("unknown resampling filter") + if resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING): + message = "Unknown resampling filter ({}).".format(resample) + + filters = [ + "{} ({})".format(filter[1], filter[0]) + for filter in ( + (NEAREST, "Image.NEAREST"), + (LANCZOS, "Image.LANCZOS"), + (BILINEAR, "Image.BILINEAR"), + (BICUBIC, "Image.BICUBIC"), + (BOX, "Image.BOX"), + (HAMMING, "Image.HAMMING"), + ) + ] + raise ValueError( + message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + ) size = tuple(size) @@ -1796,8 +1812,8 @@ class Image(object): if self.mode in ("1", "P"): resample = NEAREST - if self.mode in ['LA', 'RGBA']: - im = self.convert(self.mode[:-1]+'a') + if self.mode in ["LA", "RGBA"]: + im = self.convert(self.mode[:-1] + "a") im = im.resize(size, resample, box) return im.convert(self.mode) @@ -1805,8 +1821,15 @@ class Image(object): return self._new(self.im.resize(size, resample, box)) - def rotate(self, angle, resample=NEAREST, expand=0, center=None, - translate=None, fillcolor=None): + def rotate( + self, + angle, + resample=NEAREST, + expand=0, + center=None, + translate=None, + fillcolor=None, + ): """ Returns a rotated copy of this image. This method returns a copy of this image, rotated the given number of degrees counter @@ -1876,19 +1899,23 @@ class Image(object): else: rotn_center = center - angle = - math.radians(angle) + angle = -math.radians(angle) matrix = [ - round(math.cos(angle), 15), round(math.sin(angle), 15), 0.0, - round(-math.sin(angle), 15), round(math.cos(angle), 15), 0.0 + round(math.cos(angle), 15), + round(math.sin(angle), 15), + 0.0, + round(-math.sin(angle), 15), + round(math.cos(angle), 15), + 0.0, ] def transform(x, y, matrix): (a, b, c, d, e, f) = matrix - return a*x + b*y + c, d*x + e*y + f + return a * x + b * y + c, d * x + e * y + f - matrix[2], matrix[5] = transform(-rotn_center[0] - post_trans[0], - -rotn_center[1] - post_trans[1], - matrix) + matrix[2], matrix[5] = transform( + -rotn_center[0] - post_trans[0], -rotn_center[1] - post_trans[1], matrix + ) matrix[2] += rotn_center[0] matrix[5] += rotn_center[1] @@ -1906,13 +1933,10 @@ class Image(object): # We multiply a translation matrix from the right. Because of its # special form, this is the same as taking the image of the # translation vector as new translation vector. - matrix[2], matrix[5] = transform(-(nw - w) / 2.0, - -(nh - h) / 2.0, - matrix) + matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix) w, h = nw, nh - return self.transform((w, h), AFFINE, matrix, resample, - fillcolor=fillcolor) + return self.transform((w, h), AFFINE, matrix, resample, fillcolor=fillcolor) def save(self, fp, format=None, **params): """ @@ -1949,7 +1973,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): @@ -1957,9 +1981,9 @@ class Image(object): filename = fp.name # may mutate self! - self.load() + self._ensure_mutable() - save_all = params.pop('save_all', False) + save_all = params.pop("save_all", False) self.encoderinfo = params self.encoderconfig = () @@ -1973,7 +1997,7 @@ class Image(object): try: format = EXTENSION[ext] except KeyError: - raise ValueError('unknown file extension: {}'.format(ext)) + raise ValueError("unknown file extension: {}".format(ext)) if format.upper() not in SAVE: init() @@ -1983,11 +2007,11 @@ class Image(object): save_handler = SAVE[format.upper()] if open_fp: - if params.get('append', False): - fp = builtins.open(filename, "r+b") - else: + if params.get("append", False): # Open also for reading ("+"), because TIFF save_all # writer needs to go back and edit the written data. + fp = builtins.open(filename, "r+b") + else: fp = builtins.open(filename, "w+b") try: @@ -2004,9 +2028,6 @@ class Image(object): **EOFError** exception. When a sequence file is opened, the library automatically seeks to frame 0. - Note that in the current version of the library, most sequence - formats only allows you to seek to the next frame. - See :py:meth:`~PIL.Image.Image.tell`. :param frame: Frame number, starting at 0. @@ -2023,15 +2044,15 @@ class Image(object): Displays this image. This method is mainly intended for debugging purposes. - On Unix platforms, this method saves the image to a temporary - PPM file, and calls either the **xv** utility or the **display** - utility, depending on which one can be found. + The image is first saved to a temporary file. By default, it will be in + PNG format. - On macOS, this method saves the image to a temporary BMP file, and - opens it with the native Preview application. + On Unix, the image is then opened using the **display**, **eog** or + **xv** utility, depending on which one can be found. - On Windows, it saves the image to a temporary BMP file, and uses - the standard BMP display utility to show it (usually Paint). + On macOS, the image is opened with the native Preview application. + + On Windows, the image is opened with the standard PNG display utility. :param title: Optional title to use for the image window, where possible. @@ -2074,12 +2095,11 @@ class Image(object): """ self.load() - if isStringType(channel): + if isinstance(channel, str): try: channel = self.getbands().index(channel) except ValueError: - raise ValueError( - 'The image has no channel "{}"'.format(channel)) + raise ValueError('The image has no channel "{}"'.format(channel)) return self._new(self.im.getband(channel)) @@ -2129,19 +2149,21 @@ class Image(object): self.draft(None, size) - im = self.resize(size, resample) + if self.size != size: + im = self.resize(size, resample) - self.im = im.im - self.mode = im.mode - self._size = size + self.im = im.im + self._size = size + self.mode = self.im.mode self.readonly = 0 self.pyaccess = None # FIXME: the different transform methods need further explanation # instead of bloating the method docs, add a separate chapter. - def transform(self, size, method, data=None, resample=NEAREST, - fill=1, fillcolor=None): + def transform( + self, size, method, data=None, resample=NEAREST, fill=1, fillcolor=None + ): """ Transforms this image. This method creates a new image with the given size, and the same mode as the original, and copies data @@ -2184,13 +2206,19 @@ class Image(object): :returns: An :py:class:`~PIL.Image.Image` object. """ - if self.mode == 'LA': - return self.convert('La').transform( - size, method, data, resample, fill, fillcolor).convert('LA') + if self.mode == "LA": + return ( + self.convert("La") + .transform(size, method, data, resample, fill, fillcolor) + .convert("LA") + ) - if self.mode == 'RGBA': - return self.convert('RGBa').transform( - size, method, data, resample, fill, fillcolor).convert('RGBA') + if self.mode == "RGBA": + return ( + self.convert("RGBa") + .transform(size, method, data, resample, fill, fillcolor) + .convert("RGBA") + ) if isinstance(method, ImageTransformHandler): return method.transform(size, self, resample=resample, fill=fill) @@ -2203,19 +2231,19 @@ 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: - im.__transformer(box, self, QUAD, quad, resample, - fillcolor is None) + im.__transformer(box, self, QUAD, quad, resample, fillcolor is None) else: - im.__transformer((0, 0)+size, self, method, data, - resample, fillcolor is None) + im.__transformer( + (0, 0) + size, self, method, data, resample, fillcolor is None + ) return im - def __transformer(self, box, image, method, data, - resample=NEAREST, fill=1): + def __transformer(self, box, image, method, data, resample=NEAREST, fill=1): w = box[2] - box[0] h = box[3] - box[1] @@ -2243,16 +2271,41 @@ class Image(object): x0, y0 = nw As = 1.0 / w At = 1.0 / h - data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At, - (se[0]-sw[0]-ne[0]+x0)*As*At, - y0, (ne[1]-y0)*As, (sw[1]-y0)*At, - (se[1]-sw[1]-ne[1]+y0)*As*At) + data = ( + x0, + (ne[0] - x0) * As, + (sw[0] - x0) * At, + (se[0] - sw[0] - ne[0] + x0) * As * At, + y0, + (ne[1] - y0) * As, + (sw[1] - y0) * At, + (se[1] - sw[1] - ne[1] + y0) * As * At, + ) else: raise ValueError("unknown transformation method") if resample not in (NEAREST, BILINEAR, BICUBIC): - raise ValueError("unknown resampling filter") + if resample in (BOX, HAMMING, LANCZOS): + message = { + BOX: "Image.BOX", + HAMMING: "Image.HAMMING", + LANCZOS: "Image.LANCZOS/Image.ANTIALIAS", + }[resample] + " ({}) cannot be used.".format(resample) + else: + message = "Unknown resampling filter ({}).".format(resample) + + filters = [ + "{} ({})".format(filter[1], filter[0]) + for filter in ( + (NEAREST, "Image.NEAREST"), + (BILINEAR, "Image.BILINEAR"), + (BICUBIC, "Image.BICUBIC"), + ) + ] + raise ValueError( + message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + ) image.load() @@ -2289,6 +2342,7 @@ class Image(object): def toqimage(self): """Returns a QImage copy of this image""" from . import ImageQt + if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.toqimage(self) @@ -2296,6 +2350,7 @@ class Image(object): def toqpixmap(self): """Returns a QPixmap copy of this image""" from . import ImageQt + if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.toqpixmap(self) @@ -2304,12 +2359,13 @@ 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 @@ -2320,6 +2376,7 @@ class ImageTransformHandler(object): # # Debugging + def _wedge(): """Create greyscale wedge (for debugging only)""" @@ -2366,13 +2423,21 @@ 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 + color = ImageColor.getcolor(color, mode) - return Image()._new(core.fill(mode, size, color)) + im = Image() + if mode == "P" and isinstance(color, (list, tuple)) and len(color) in [3, 4]: + # RGB or RGBA value for a P image + from . import ImagePalette + + im.palette = ImagePalette.ImagePalette() + color = im.palette.getcolor(color) + return im._new(core.fill(mode, size, color)) def frombytes(mode, size, data, decoder_name="raw", *args): @@ -2414,8 +2479,9 @@ def frombytes(mode, size, data, decoder_name="raw", *args): def fromstring(*args, **kw): - raise NotImplementedError("fromstring() has been removed. " + - "Please call frombytes() instead.") + raise NotImplementedError( + "fromstring() has been removed. Please call frombytes() instead." + ) def frombuffer(mode, size, data, decoder_name="raw", *args): @@ -2461,18 +2527,10 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): if decoder_name == "raw": if args == (): - warnings.warn( - "the frombuffer defaults may change in a future release; " - "for portability, change the call to read:\n" - " frombuffer(mode, size, data, 'raw', mode, 0, 1)", - RuntimeWarning, stacklevel=2 - ) - args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 + args = mode, 0, 1 if args[0] in _MAPMODES: im = new(mode, (1, 1)) - im = im._new( - core.map_buffer(data, size, decoder_name, None, 0, args) - ) + im = im._new(core.map_buffer(data, size, decoder_name, None, 0, args)) im.readonly = 1 return im @@ -2506,12 +2564,12 @@ def fromarray(obj, mode=None): .. versionadded:: 1.1.6 """ arr = obj.__array_interface__ - shape = arr['shape'] + shape = arr["shape"] ndim = len(shape) - strides = arr.get('strides', None) + strides = arr.get("strides", None) if mode is None: try: - typekey = (1, 1) + shape[2:], arr['typestr'] + typekey = (1, 1) + shape[2:], arr["typestr"] mode, rawmode = _fromarray_typemap[typekey] except KeyError: raise TypeError("Cannot handle this data type") @@ -2528,7 +2586,7 @@ def fromarray(obj, mode=None): size = shape[1], shape[0] if strides is not None: - if hasattr(obj, 'tobytes'): + if hasattr(obj, "tobytes"): obj = obj.tobytes() else: obj = obj.tostring() @@ -2539,6 +2597,7 @@ def fromarray(obj, mode=None): def fromqimage(im): """Creates an image instance from a QImage image""" from . import ImageQt + if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.fromqimage(im) @@ -2547,6 +2606,7 @@ def fromqimage(im): def fromqpixmap(im): """Creates an image instance from a QPixmap image""" from . import ImageQt + if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.fromqpixmap(im) @@ -2573,7 +2633,7 @@ _fromarray_typemap = { ((1, 1, 2), "|u1"): ("LA", "LA"), ((1, 1, 3), "|u1"): ("RGB", "RGB"), ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), - } +} # shortcuts _fromarray_typemap[((1, 1), _ENDIAN + "i4")] = ("I", "I") @@ -2589,15 +2649,15 @@ def _decompression_bomb_check(size): if pixels > 2 * MAX_IMAGE_PIXELS: raise DecompressionBombError( "Image size (%d pixels) exceeds limit of %d pixels, " - "could be decompression bomb DOS attack." % - (pixels, 2 * MAX_IMAGE_PIXELS)) + "could be decompression bomb DOS attack." % (pixels, 2 * MAX_IMAGE_PIXELS) + ) if pixels > MAX_IMAGE_PIXELS: warnings.warn( "Image size (%d pixels) exceeds limit of %d pixels, " - "could be decompression bomb DOS attack." % - (pixels, MAX_IMAGE_PIXELS), - DecompressionBombWarning) + "could be decompression bomb DOS attack." % (pixels, MAX_IMAGE_PIXELS), + DecompressionBombWarning, + ) def open(fp, mode="r"): @@ -2625,10 +2685,10 @@ def open(fp, mode="r"): exclusive_fp = False filename = "" - if isPath(fp): - filename = fp - elif HAS_PATHLIB and isinstance(fp, Path): + if isinstance(fp, Path): filename = str(fp.resolve()) + elif isPath(fp): + filename = fp if filename: fp = builtins.open(filename, "rb") @@ -2683,8 +2743,10 @@ 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) + ) + # # Image processing. @@ -2788,6 +2850,7 @@ def merge(mode, bands): # -------------------------------------------------------------------- # Plugin registry + def register_open(id, factory, accept=None): """ Register an image file plugin. This function should not be used @@ -2901,6 +2964,7 @@ def register_encoder(name, encoder): # -------------------------------------------------------------------- # Simple display support. User code may override this. + def _show(image, **options): # override me, as necessary _showxv(image, **options) @@ -2908,12 +2972,14 @@ def _show(image, **options): def _showxv(image, title=None, **options): from . import ImageShow + ImageShow.show(image, title, **options) # -------------------------------------------------------------------- # Effects + def effect_mandelbrot(size, extent, quality): """ Generate a Mandelbrot set covering the given extent. @@ -2959,14 +3025,15 @@ def radial_gradient(mode): # -------------------------------------------------------------------- # Resources + def _apply_env_variables(env=None): if env is None: env = os.environ for var_name, setter in [ - ('PILLOW_ALIGNMENT', core.set_alignment), - ('PILLOW_BLOCK_SIZE', core.set_block_size), - ('PILLOW_BLOCKS_MAX', core.set_blocks_max), + ("PILLOW_ALIGNMENT", core.set_alignment), + ("PILLOW_BLOCK_SIZE", core.set_block_size), + ("PILLOW_BLOCKS_MAX", core.set_blocks_max), ]: if var_name not in env: continue @@ -2974,22 +3041,228 @@ def _apply_env_variables(env=None): var = env[var_name].lower() units = 1 - for postfix, mul in [('k', 1024), ('m', 1024*1024)]: + for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]: if var.endswith(postfix): units = mul - var = var[:-len(postfix)] + var = var[: -len(postfix)] try: var = int(var) * units except ValueError: - warnings.warn("{0} is not int".format(var_name)) + warnings.warn("{} is not int".format(var_name)) continue try: setter(var) except ValueError as e: - warnings.warn("{0}: {1}".format(var_name, e)) + warnings.warn("{}: {}".format(var_name, e)) _apply_env_variables() atexit.register(core.clear_cache) + + +class Exif(MutableMapping): + endian = "<" + + def __init__(self): + self._data = {} + self._ifds = {} + self._info = None + self._loaded_exif = None + + def _fixup(self, value): + try: + if len(value) == 1 and not isinstance(value, dict): + return value[0] + except Exception: + pass + return value + + def _fixup_dict(self, src_dict): + # Helper function for _getexif() + # returns a dict with any single item tuples/lists as individual values + return {k: self._fixup(v) for k, v in src_dict.items()} + + def _get_ifd_dict(self, tag): + try: + # an offset pointer to the location of the nested embedded IFD. + # It should be a long, but may be corrupted. + self.fp.seek(self[tag]) + except (KeyError, TypeError): + pass + else: + from . import TiffImagePlugin + + info = TiffImagePlugin.ImageFileDirectory_v1(self.head) + info.load(self.fp) + return self._fixup_dict(info) + + def load(self, data): + # Extract EXIF information. This is highly experimental, + # and is likely to be replaced with something better in a future + # version. + + # The EXIF record consists of a TIFF file embedded in a JPEG + # application marker (!). + if data == self._loaded_exif: + return + self._loaded_exif = data + self._data.clear() + self._ifds.clear() + self._info = None + if not data: + return + + self.fp = io.BytesIO(data[6:]) + self.head = self.fp.read(8) + # process dictionary + from . import TiffImagePlugin + + self._info = TiffImagePlugin.ImageFileDirectory_v1(self.head) + self.endian = self._info._endian + self.fp.seek(self._info.next) + self._info.load(self.fp) + + # get EXIF extension + ifd = self._get_ifd_dict(0x8769) + if ifd: + self._data.update(ifd) + self._ifds[0x8769] = ifd + + def tobytes(self, offset=0): + from . import TiffImagePlugin + + if self.endian == "<": + head = b"II\x2A\x00\x08\x00\x00\x00" + else: + head = b"MM\x00\x2A\x00\x00\x00\x08" + ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) + for tag, value in self.items(): + ifd[tag] = value + return b"Exif\x00\x00" + head + ifd.tobytes(offset) + + def get_ifd(self, tag): + if tag not in self._ifds and tag in self: + if tag in [0x8825, 0xA005]: + # gpsinfo, interop + self._ifds[tag] = self._get_ifd_dict(tag) + elif tag == 0x927C: # makernote + from .TiffImagePlugin import ImageFileDirectory_v2 + + if self[0x927C][:8] == b"FUJIFILM": + exif_data = self[0x927C] + ifd_offset = i32le(exif_data[8:12]) + ifd_data = exif_data[ifd_offset:] + + makernote = {} + for i in range(0, struct.unpack(" 4: + (offset,) = struct.unpack("H", ifd_data[:2])[0]): + ifd_tag, typ, count, data = struct.unpack( + ">HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2] + ) + if ifd_tag == 0x1101: + # CameraInfo + (offset,) = struct.unpack(">L", data) + self.fp.seek(offset) + + camerainfo = {"ModelID": self.fp.read(4)} + + self.fp.read(4) + # Seconds since 2000 + camerainfo["TimeStamp"] = i32le(self.fp.read(12)) + + self.fp.read(4) + camerainfo["InternalSerialNumber"] = self.fp.read(4) + + self.fp.read(12) + parallax = self.fp.read(4) + handler = ImageFileDirectory_v2._load_dispatch[ + TiffTags.FLOAT + ][1] + camerainfo["Parallax"] = handler( + ImageFileDirectory_v2(), parallax, False + ) + + self.fp.read(4) + camerainfo["Category"] = self.fp.read(2) + + makernote = {0x1101: dict(self._fixup_dict(camerainfo))} + self._ifds[0x927C] = makernote + return self._ifds.get(tag, {}) + + def __str__(self): + if self._info is not None: + # Load all keys into self._data + for tag in self._info.keys(): + self[tag] + + return str(self._data) + + def __len__(self): + keys = set(self._data) + if self._info is not None: + keys.update(self._info) + return len(keys) + + def __getitem__(self, tag): + if self._info is not None and tag not in self._data and tag in self._info: + self._data[tag] = self._fixup(self._info[tag]) + if tag == 0x8825: + self._data[tag] = self.get_ifd(tag) + del self._info[tag] + return self._data[tag] + + def __contains__(self, tag): + return tag in self._data or (self._info is not None and tag in self._info) + + def __setitem__(self, tag, value): + if self._info is not None and tag in self._info: + del self._info[tag] + self._data[tag] = value + + def __delitem__(self, tag): + if self._info is not None and tag in self._info: + del self._info[tag] + del self._data[tag] + + def __iter__(self): + keys = set(self._data) + if self._info is not None: + keys.update(self._info) + return iter(keys) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 29959a83a..d05b3102c 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -15,18 +15,18 @@ # 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 + try: from PIL import _imagingcms except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. - from _util import deferred_error + from ._util import deferred_error + _imagingcms = deferred_error(ex) -from PIL._util import isStringType DESCRIPTION = """ pyCMS @@ -132,7 +132,7 @@ FLAGS = { "SOFTPROOFING": 16384, # Do softproofing "PRESERVEBLACK": 32768, # Black preservation "NODEFAULTRESOURCEDEF": 16777216, # CRD special - "GRIDPOINTS": lambda n: ((n) & 0xFF) << 16 # Gridpoints + "GRIDPOINTS": lambda n: ((n) & 0xFF) << 16, # Gridpoints } _MAX_FLAG = 0 @@ -148,8 +148,8 @@ for flag in FLAGS.values(): ## # Profile. -class ImageCmsProfile(object): +class ImageCmsProfile: def __init__(self, profile): """ :param profile: Either a string representing a filename, @@ -158,7 +158,7 @@ class ImageCmsProfile(object): """ - if isStringType(profile): + if isinstance(profile, str): self._set(core.profile_open(profile), profile) elif hasattr(profile, "read"): self._set(core.profile_frombytes(profile.read())) @@ -197,22 +197,31 @@ class ImageCmsTransform(Image.ImagePointHandler): Will return the output profile in the output.info['icc_profile']. """ - def __init__(self, input, output, input_mode, output_mode, - intent=INTENT_PERCEPTUAL, proof=None, - proof_intent=INTENT_ABSOLUTE_COLORIMETRIC, flags=0): + def __init__( + self, + input, + output, + input_mode, + output_mode, + intent=INTENT_PERCEPTUAL, + proof=None, + proof_intent=INTENT_ABSOLUTE_COLORIMETRIC, + flags=0, + ): if proof is None: self.transform = core.buildTransform( - input.profile, output.profile, - input_mode, output_mode, - intent, - flags + input.profile, output.profile, input_mode, output_mode, intent, flags ) else: self.transform = core.buildProofTransform( - input.profile, output.profile, proof.profile, - input_mode, output_mode, - intent, proof_intent, - flags + input.profile, + output.profile, + proof.profile, + input_mode, + output_mode, + intent, + proof_intent, + flags, ) # Note: inputMode and outputMode are for pyCMS compatibility only self.input_mode = self.inputMode = input_mode @@ -228,7 +237,7 @@ class ImageCmsTransform(Image.ImagePointHandler): if imOut is None: imOut = Image.new(self.output_mode, im.size, None) self.transform.apply(im.im.id, imOut.im.id) - imOut.info['icc_profile'] = self.output_profile.tobytes() + imOut.info["icc_profile"] = self.output_profile.tobytes() return imOut def apply_in_place(self, im): @@ -236,7 +245,7 @@ class ImageCmsTransform(Image.ImagePointHandler): if im.mode != self.output_mode: raise ValueError("mode mismatch") # wrong output mode self.transform.apply(im.im.id, im.im.id) - im.info['icc_profile'] = self.output_profile.tobytes() + im.info["icc_profile"] = self.output_profile.tobytes() return im @@ -247,6 +256,7 @@ def get_display_profile(handle=None): if sys.platform == "win32": from PIL import ImageWin + if isinstance(handle, ImageWin.HDC): profile = core.get_display_profile_win32(handle, 1) else: @@ -265,16 +275,24 @@ def get_display_profile(handle=None): # pyCMS compatible layer # --------------------------------------------------------------------. + class PyCMSError(Exception): """ (pyCMS) Exception class. This is used for all errors in the pyCMS API. """ + pass def profileToProfile( - im, inputProfile, outputProfile, renderingIntent=INTENT_PERCEPTUAL, - outputMode=None, inPlace=False, flags=0): + im, + inputProfile, + outputProfile, + renderingIntent=INTENT_PERCEPTUAL, + outputMode=None, + inPlace=False, + flags=0, +): """ (pyCMS) Applies an ICC transformation to a given image, mapping from inputProfile to outputProfile. @@ -333,8 +351,7 @@ def profileToProfile( raise PyCMSError("renderingIntent must be an integer between 0 and 3") if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError( - "flags must be an integer between 0 and %s" + _MAX_FLAG) + raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -342,15 +359,19 @@ def profileToProfile( if not isinstance(outputProfile, ImageCmsProfile): outputProfile = ImageCmsProfile(outputProfile) transform = ImageCmsTransform( - inputProfile, outputProfile, im.mode, outputMode, - renderingIntent, flags=flags + inputProfile, + outputProfile, + im.mode, + outputMode, + renderingIntent, + flags=flags, ) if inPlace: transform.apply_in_place(im) imOut = None else: imOut = transform.apply(im) - except (IOError, TypeError, ValueError) as v: + except (OSError, TypeError, ValueError) as v: raise PyCMSError(v) return imOut @@ -374,13 +395,18 @@ def getOpenProfile(profileFilename): try: return ImageCmsProfile(profileFilename) - except (IOError, TypeError, ValueError) as v: + except (OSError, TypeError, ValueError) as v: raise PyCMSError(v) def buildTransform( - inputProfile, outputProfile, inMode, outMode, - renderingIntent=INTENT_PERCEPTUAL, flags=0): + inputProfile, + outputProfile, + inMode, + outMode, + renderingIntent=INTENT_PERCEPTUAL, + flags=0, +): """ (pyCMS) Builds an ICC transform mapping from the inputProfile to the outputProfile. Use applyTransform to apply the transform to a given @@ -440,8 +466,7 @@ def buildTransform( raise PyCMSError("renderingIntent must be an integer between 0 and 3") if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError( - "flags must be an integer between 0 and %s" + _MAX_FLAG) + raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -449,17 +474,22 @@ def buildTransform( if not isinstance(outputProfile, ImageCmsProfile): outputProfile = ImageCmsProfile(outputProfile) return ImageCmsTransform( - inputProfile, outputProfile, inMode, outMode, - renderingIntent, flags=flags) - except (IOError, TypeError, ValueError) as v: + inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags + ) + except (OSError, TypeError, ValueError) as v: raise PyCMSError(v) def buildProofTransform( - inputProfile, outputProfile, proofProfile, inMode, outMode, - renderingIntent=INTENT_PERCEPTUAL, - proofRenderingIntent=INTENT_ABSOLUTE_COLORIMETRIC, - flags=FLAGS["SOFTPROOFING"]): + inputProfile, + outputProfile, + proofProfile, + inMode, + outMode, + renderingIntent=INTENT_PERCEPTUAL, + proofRenderingIntent=INTENT_ABSOLUTE_COLORIMETRIC, + flags=FLAGS["SOFTPROOFING"], +): """ (pyCMS) Builds an ICC transform mapping from the inputProfile to the outputProfile, but tries to simulate the result that would be @@ -538,8 +568,7 @@ def buildProofTransform( raise PyCMSError("renderingIntent must be an integer between 0 and 3") if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError( - "flags must be an integer between 0 and %s" + _MAX_FLAG) + raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -549,9 +578,16 @@ def buildProofTransform( if not isinstance(proofProfile, ImageCmsProfile): proofProfile = ImageCmsProfile(proofProfile) return ImageCmsTransform( - inputProfile, outputProfile, inMode, outMode, renderingIntent, - proofProfile, proofRenderingIntent, flags) - except (IOError, TypeError, ValueError) as v: + inputProfile, + outputProfile, + inMode, + outMode, + renderingIntent, + proofProfile, + proofRenderingIntent, + flags, + ) + except (OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -568,7 +604,7 @@ def applyTransform(im, transform, inPlace=False): If inPlace is True and transform.inMode != transform.outMode, a PyCMSError is raised. - If im.mode, transfer.inMode, or transfer.outMode is not supported by + If im.mode, transform.inMode, or transform.outMode is not supported by pyCMSdll or the profiles you used for the transform, a PyCMSError is raised. @@ -641,15 +677,16 @@ def createProfile(colorSpace, colorTemp=-1): if colorSpace not in ["LAB", "XYZ", "sRGB"]: raise PyCMSError( "Color space not supported for on-the-fly profile creation (%s)" - % colorSpace) + % colorSpace + ) if colorSpace == "LAB": try: colorTemp = float(colorTemp) except (TypeError, ValueError): raise PyCMSError( - "Color temperature must be numeric, \"%s\" not valid" - % colorTemp) + 'Color temperature must be numeric, "%s" not valid' % colorTemp + ) try: return core.createProfile(colorSpace, colorTemp) @@ -686,16 +723,16 @@ def getProfileName(profile): # // name was "%s - %s" (model, manufacturer) || Description , # // but if the Model and Manufacturer were the same or the model # // was long, Just the model, in 1.x - model = profile.profile.product_model - manufacturer = profile.profile.product_manufacturer + model = profile.profile.model + manufacturer = profile.profile.manufacturer if not (model or manufacturer): - return profile.profile.product_description + "\n" + 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) @@ -727,15 +764,15 @@ def getProfileInfo(profile): # Python, not C. the white point bits weren't working well, # so skipping. # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint - description = profile.profile.product_description - cpright = profile.profile.product_copyright + description = profile.profile.profile_description + cpright = profile.profile.copyright arr = [] for elt in (description, cpright): if elt: 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) @@ -762,8 +799,8 @@ def getProfileCopyright(profile): # add an extra newline to preserve pyCMS compatibility if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) - return profile.profile.product_copyright + "\n" - except (AttributeError, IOError, TypeError, ValueError) as v: + return (profile.profile.copyright or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -790,8 +827,8 @@ def getProfileManufacturer(profile): # add an extra newline to preserve pyCMS compatibility if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) - return profile.profile.product_manufacturer + "\n" - except (AttributeError, IOError, TypeError, ValueError) as v: + return (profile.profile.manufacturer or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -819,8 +856,8 @@ def getProfileModel(profile): # add an extra newline to preserve pyCMS compatibility if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) - return profile.profile.product_model + "\n" - except (AttributeError, IOError, TypeError, ValueError) as v: + return (profile.profile.model or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -848,8 +885,8 @@ def getProfileDescription(profile): # add an extra newline to preserve pyCMS compatibility if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) - return profile.profile.product_description + "\n" - except (AttributeError, IOError, TypeError, ValueError) as v: + return (profile.profile.profile_description or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -888,7 +925,7 @@ def getDefaultIntent(profile): if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) return profile.profile.rendering_intent - except (AttributeError, IOError, TypeError, ValueError) as v: + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -939,7 +976,7 @@ def isIntentSupported(profile, intent, direction): return 1 else: return -1 - except (AttributeError, IOError, TypeError, ValueError) as v: + except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -948,7 +985,4 @@ def versions(): (pyCMS) Fetches versions. """ - return ( - VERSION, core.littlecms_version, - sys.version.split()[0], Image.VERSION - ) + return (VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__) diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index d3b3b00ac..692d7d2c3 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -17,9 +17,10 @@ # See the README file for information on usage and redistribution. # -from . import Image import re +from . import Image + def getrgb(color): """ @@ -41,95 +42,77 @@ def getrgb(color): return rgb # check for known string formats - if re.match('#[a-f0-9]{3}$', color): - return ( - int(color[1]*2, 16), - int(color[2]*2, 16), - int(color[3]*2, 16), - ) + if re.match("#[a-f0-9]{3}$", color): + return (int(color[1] * 2, 16), int(color[2] * 2, 16), int(color[3] * 2, 16)) - if re.match('#[a-f0-9]{4}$', color): + if re.match("#[a-f0-9]{4}$", color): return ( - int(color[1]*2, 16), - int(color[2]*2, 16), - int(color[3]*2, 16), - int(color[4]*2, 16), - ) + int(color[1] * 2, 16), + int(color[2] * 2, 16), + int(color[3] * 2, 16), + int(color[4] * 2, 16), + ) - if re.match('#[a-f0-9]{6}$', color): - return ( - int(color[1:3], 16), - int(color[3:5], 16), - int(color[5:7], 16), - ) + if re.match("#[a-f0-9]{6}$", color): + return (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)) - if re.match('#[a-f0-9]{8}$', color): + if re.match("#[a-f0-9]{8}$", color): return ( int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16), int(color[7:9], 16), - ) + ) m = re.match(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) if m: - return ( - int(m.group(1)), - int(m.group(2)), - int(m.group(3)) - ) + return (int(m.group(1)), int(m.group(2)), int(m.group(3))) m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) if m: return ( int((int(m.group(1)) * 255) / 100.0 + 0.5), int((int(m.group(2)) * 255) / 100.0 + 0.5), - int((int(m.group(3)) * 255) / 100.0 + 0.5) - ) + int((int(m.group(3)) * 255) / 100.0 + 0.5), + ) m = re.match( - r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", - color, + r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color ) if m: from colorsys import hls_to_rgb + rgb = hls_to_rgb( float(m.group(1)) / 360.0, float(m.group(3)) / 100.0, float(m.group(2)) / 100.0, - ) + ) return ( int(rgb[0] * 255 + 0.5), int(rgb[1] * 255 + 0.5), - int(rgb[2] * 255 + 0.5) - ) + int(rgb[2] * 255 + 0.5), + ) m = re.match( - r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", - color, + r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color ) if m: from colorsys import hsv_to_rgb + rgb = hsv_to_rgb( float(m.group(1)) / 360.0, float(m.group(2)) / 100.0, float(m.group(3)) / 100.0, - ) + ) return ( int(rgb[0] * 255 + 0.5), int(rgb[1] * 255 + 0.5), - int(rgb[2] * 255 + 0.5) - ) + int(rgb[2] * 255 + 0.5), + ) - m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", - color) + m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) if m: - return ( - int(m.group(1)), - int(m.group(2)), - int(m.group(3)), - int(m.group(4)) - ) + return (int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))) raise ValueError("unknown color specifier: %r" % color) @@ -151,11 +134,11 @@ def getcolor(color, mode): if Image.getmodebase(mode) == "L": r, g, b = color - color = (r*299 + g*587 + b*114)//1000 - if mode[-1] == 'A': + color = (r * 299 + g * 587 + b * 114) // 1000 + if mode[-1] == "A": return (color, alpha) else: - if mode[-1] == 'A': + if mode[-1] == "A": return color + (alpha,) return color diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index ac549790a..c6e12150e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,7 +34,7 @@ import math import numbers from . import Image, ImageColor -from ._util import isStringType + """ A simple 2D drawing interface for PIL images. @@ -44,8 +44,7 @@ directly. """ -class ImageDraw(object): - +class ImageDraw: def __init__(self, im, mode=None): """ Create a drawing instance. @@ -76,9 +75,9 @@ class ImageDraw(object): self.draw = Image.core.draw(self.im, blend) self.mode = mode if mode in ("I", "F"): - self.ink = self.draw.draw_ink(1, mode) + self.ink = self.draw.draw_ink(1) else: - self.ink = self.draw.draw_ink(-1, mode) + self.ink = self.draw.draw_ink(-1) if mode in ("1", "P", "I", "F"): # FIXME: fix Fill2 to properly support matte for I+F images self.fontmode = "1" @@ -95,6 +94,7 @@ class ImageDraw(object): if not self.font: # FIXME: should add a font repository from . import ImageFont + self.font = ImageFont.load_default() return self.font @@ -106,17 +106,17 @@ class ImageDraw(object): ink = self.ink else: if ink is not None: - if isStringType(ink): + if isinstance(ink, str): ink = ImageColor.getcolor(ink, self.mode) if self.palette and not isinstance(ink, numbers.Number): ink = self.palette.getcolor(ink) - ink = self.draw.draw_ink(ink, self.mode) + 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) - fill = self.draw.draw_ink(fill, self.mode) + fill = self.draw.draw_ink(fill) return ink, fill def arc(self, xy, start, end, fill=None, width=0): @@ -156,13 +156,12 @@ class ImageDraw(object): if ink is not None: self.draw.draw_lines(xy, ink, width) if joint == "curve" and width > 4: - for i in range(1, len(xy)-1): + for i in range(1, len(xy) - 1): point = xy[i] angles = [ - math.degrees(math.atan2( - end[0] - start[0], start[1] - end[1] - )) % 360 - for start, end in ((xy[i-1], point), (point, xy[i+1])) + math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) + % 360 + for start, end in ((xy[i - 1], point), (point, xy[i + 1])) ] if angles[0] == angles[1]: # This is a straight line, so no joint is required @@ -171,21 +170,23 @@ class ImageDraw(object): def coord_at_angle(coord, angle): x, y = coord angle -= 90 - distance = width/2 - 1 - return tuple([ - p + - (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) - for p, p_d in - ((x, distance * math.cos(math.radians(angle))), - (y, distance * math.sin(math.radians(angle)))) - ]) - flipped = ((angles[1] > angles[0] and - angles[1] - 180 > angles[0]) or - (angles[1] < angles[0] and - angles[1] + 180 > angles[0])) + distance = width / 2 - 1 + return tuple( + [ + p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) + for p, p_d in ( + (x, distance * math.cos(math.radians(angle))), + (y, distance * math.sin(math.radians(angle))), + ) + ] + ) + + flipped = ( + angles[1] > angles[0] and angles[1] - 180 > angles[0] + ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0]) coords = [ - (point[0] - width/2 + 1, point[1] - width/2 + 1), - (point[0] + width/2 - 1, point[1] + width/2 - 1) + (point[0] - width / 2 + 1, point[1] - width / 2 + 1), + (point[0] + width / 2 - 1, point[1] + width / 2 - 1), ] if flipped: start, end = (angles[1] + 90, angles[0] + 90) @@ -197,15 +198,15 @@ class ImageDraw(object): # Cover potential gaps between the line and the joint if flipped: gapCoords = [ - coord_at_angle(point, angles[0]+90), + coord_at_angle(point, angles[0] + 90), point, - coord_at_angle(point, angles[1]+90) + coord_at_angle(point, angles[1] + 90), ] else: gapCoords = [ - coord_at_angle(point, angles[0]-90), + coord_at_angle(point, angles[0] - 90), point, - coord_at_angle(point, angles[1]-90) + coord_at_angle(point, angles[1] - 90), ] self.line(gapCoords, fill, width=3) @@ -259,36 +260,126 @@ class ImageDraw(object): return text.split(split_character) - def text(self, xy, text, fill=None, font=None, anchor=None, - *args, **kwargs): + def text( + self, + xy, + text, + fill=None, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + stroke_fill=None, + *args, + **kwargs + ): if self._multiline_check(text): - return self.multiline_text(xy, text, fill, font, anchor, - *args, **kwargs) - ink, fill = self._getink(fill) + return self.multiline_text( + xy, + text, + fill, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + stroke_fill, + ) + if font is None: font = self.getfont() - if ink is None: - ink = fill - if ink is not None: + + def getink(fill): + ink, fill = self._getink(fill) + if ink is None: + return fill + return ink + + def draw_text(ink, stroke_width=0, stroke_offset=None): + coord = xy try: - mask, offset = font.getmask2(text, self.fontmode, - *args, **kwargs) - xy = xy[0] + offset[0], xy[1] + offset[1] + mask, offset = font.getmask2( + text, + self.fontmode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + *args, + **kwargs, + ) + coord = coord[0] + offset[0], coord[1] + offset[1] except AttributeError: try: - mask = font.getmask(text, self.fontmode, *args, **kwargs) + mask = font.getmask( + text, + self.fontmode, + direction, + features, + language, + stroke_width, + *args, + **kwargs, + ) except TypeError: mask = font.getmask(text) - self.draw.draw_bitmap(xy, mask, ink) + if stroke_offset: + coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] + self.draw.draw_bitmap(coord, mask, ink) - def multiline_text(self, xy, text, fill=None, font=None, anchor=None, - spacing=4, align="left", direction=None, features=None): + ink = getink(fill) + if ink is not None: + stroke_ink = None + if stroke_width: + stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink + + if stroke_ink is not None: + # Draw stroked text + draw_text(stroke_ink, stroke_width) + + # Draw normal text + draw_text(ink, 0, (stroke_width, stroke_width)) + else: + # Only draw normal text + draw_text(ink) + + def multiline_text( + self, + xy, + text, + fill=None, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + stroke_fill=None, + ): widths = [] max_width = 0 lines = self._multiline_split(text) - line_spacing = self.textsize('A', font=font)[1] + spacing + line_spacing = ( + self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing + ) for line in lines: - line_width, line_height = self.textsize(line, font) + line_width, line_height = self.textsize( + line, + font, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + ) widths.append(line_width) max_width = max(max_width, line_width) left, top = xy @@ -298,35 +389,65 @@ class ImageDraw(object): elif align == "center": left += (max_width - widths[idx]) / 2.0 elif align == "right": - left += (max_width - widths[idx]) + left += max_width - widths[idx] else: raise ValueError('align must be "left", "center" or "right"') - self.text((left, top), line, fill, font, anchor, - direction=direction, features=features) + self.text( + (left, top), + line, + fill, + font, + anchor, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + stroke_fill=stroke_fill, + ) top += line_spacing left = xy[0] - def textsize(self, text, font=None, spacing=4, direction=None, - features=None): + def textsize( + self, + text, + font=None, + spacing=4, + direction=None, + features=None, + language=None, + stroke_width=0, + ): """Get the size of a given string, in pixels.""" if self._multiline_check(text): - return self.multiline_textsize(text, font, spacing, - direction, features) + return self.multiline_textsize( + text, font, spacing, direction, features, language, stroke_width + ) if font is None: font = self.getfont() - return font.getsize(text, direction, features) + return font.getsize(text, direction, features, language, stroke_width) - def multiline_textsize(self, text, font=None, spacing=4, direction=None, - features=None): + def multiline_textsize( + self, + text, + font=None, + spacing=4, + direction=None, + features=None, + language=None, + stroke_width=0, + ): max_width = 0 lines = self._multiline_split(text) - line_spacing = self.textsize('A', font=font)[1] + spacing + line_spacing = ( + self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing + ) for line in lines: - line_width, line_height = self.textsize(line, font, spacing, - direction, features) + line_width, line_height = self.textsize( + line, font, spacing, direction, features, language, stroke_width + ) max_width = max(max_width, line_width) - return max_width, len(lines)*line_spacing - spacing + return max_width, len(lines) * line_spacing - spacing def Draw(im, mode=None): @@ -412,9 +533,10 @@ def floodfill(image, xy, value, border=None, thresh=0): while edge: new_edge = set() for (x, y) in edge: # 4 adjacent method - for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)): - if (s, t) in full_edge: - continue # if already processed, skip + for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + # If already processed, or if a coordinate is negative, skip + if (s, t) in full_edge or s < 0 or t < 0: + continue try: p = pixel[s, t] except (ValueError, IndexError): @@ -437,6 +559,6 @@ def _color_diff(color1, color2): Uses 1-norm distance to calculate difference between two values. """ if isinstance(color2, tuple): - return sum([abs(color1[i]-color2[i]) for i in range(0, len(color2))]) + return sum([abs(color1[i] - color2[i]) for i in range(0, len(color2))]) else: - return abs(color1-color2) + return abs(color1 - color2) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index f7902b031..20b5fe4c4 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -19,26 +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 1b78bfd9b..3b79d5c46 100644 --- a/src/PIL/ImageEnhance.py +++ b/src/PIL/ImageEnhance.py @@ -21,8 +21,7 @@ from . import Image, ImageFilter, ImageStat -class _Enhance(object): - +class _Enhance: def enhance(self, factor): """ Returns an enhanced image. @@ -45,14 +44,14 @@ class Color(_Enhance): factor of 0.0 gives a black and white image. A factor of 1.0 gives the original image. """ + def __init__(self, image): self.image = image - self.intermediate_mode = 'L' - if 'A' in image.getbands(): - self.intermediate_mode = 'LA' + self.intermediate_mode = "L" + if "A" in image.getbands(): + self.intermediate_mode = "LA" - self.degenerate = image.convert( - self.intermediate_mode).convert(image.mode) + self.degenerate = image.convert(self.intermediate_mode).convert(image.mode) class Contrast(_Enhance): @@ -62,13 +61,14 @@ class Contrast(_Enhance): to the contrast control on a TV set. An enhancement factor of 0.0 gives a solid grey image. A factor of 1.0 gives the original image. """ + def __init__(self, image): self.image = image mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5) self.degenerate = Image.new("L", image.size, mean).convert(image.mode) - if 'A' in image.getbands(): - self.degenerate.putalpha(image.getchannel('A')) + if "A" in image.getbands(): + self.degenerate.putalpha(image.getchannel("A")) class Brightness(_Enhance): @@ -78,12 +78,13 @@ class Brightness(_Enhance): enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the original image. """ + def __init__(self, image): self.image = image self.degenerate = Image.new(image.mode, image.size, 0) - if 'A' in image.getbands(): - self.degenerate.putalpha(image.getchannel('A')) + if "A" in image.getbands(): + self.degenerate.putalpha(image.getchannel("A")) class Sharpness(_Enhance): @@ -93,9 +94,10 @@ class Sharpness(_Enhance): enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the original image, and a factor of 2.0 gives a sharpened image. """ + def __init__(self, image): self.image = image self.degenerate = image.filter(ImageFilter.SMOOTH) - if 'A' in image.getbands(): - self.degenerate.putalpha(image.getchannel('A')) + if "A" in image.getbands(): + self.degenerate.putalpha(image.getchannel("A")) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index cdf746932..a343bd43c 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -27,15 +27,16 @@ # See the README file for information on usage and redistribution. # +import io +import struct +import sys + from . import Image from ._util import isPath -import io -import sys -import struct MAXBLOCK = 65536 -SAFEBLOCK = 1024*1024 +SAFEBLOCK = 1024 * 1024 LOAD_TRUNCATED_IMAGES = False @@ -44,7 +45,7 @@ ERRORS = { -2: "decoding error", -3: "unknown error", -8: "bad configuration", - -9: "out of memory error" + -9: "out of memory error", } @@ -55,13 +56,14 @@ 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") # # -------------------------------------------------------------------- # Helpers + def _tilesort(t): # sort on offset return t[2] @@ -71,11 +73,12 @@ def _tilesort(t): # -------------------------------------------------------------------- # ImageFile base class + 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 @@ -100,19 +103,24 @@ class ImageFile(Image.Image): self._exclusive_fp = None try: - self._open() - except (IndexError, # end of data + 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: + 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""" @@ -120,9 +128,10 @@ class ImageFile(Image.Image): pass def get_format_mimetype(self): - if self.format is None: - return - return self.custom_mimetype or Image.MIME.get(self.format.upper()) + if self.custom_mimetype: + return self.custom_mimetype + if self.format is not None: + return Image.MIME.get(self.format.upper()) def verify(self): """Check file integrity""" @@ -139,14 +148,14 @@ 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 self.map = None use_mmap = self.filename and len(self.tile) == 1 # As of pypy 2.1.0, memory mapping was failing here. - use_mmap = use_mmap and not hasattr(sys, 'pypy_version_info') + use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") readonly = 0 @@ -167,9 +176,12 @@ class ImageFile(Image.Image): if use_mmap: # try memory mapping decoder_name, extents, offset, args = self.tile[0] - if decoder_name == "raw" and len(args) >= 3 and \ - args[0] == self.mode and \ - args[0] in Image._MAPMODES: + if ( + decoder_name == "raw" + and len(args) >= 3 + and args[0] == self.mode + and args[0] in Image._MAPMODES + ): try: if hasattr(Image.core, "map"): # use built-in mapper WIN32 only @@ -177,22 +189,24 @@ class ImageFile(Image.Image): self.map.seek(offset) self.im = self.map.readimage( self.mode, self.size, args[1], args[2] - ) + ) else: # use mmap, if possible import mmap + with open(self.filename, "r") as fp: - self.map = mmap.mmap(fp.fileno(), 0, - access=mmap.ACCESS_READ) + self.map = mmap.mmap( + fp.fileno(), 0, access=mmap.ACCESS_READ + ) self.im = Image.core.map_buffer( - self.map, self.size, decoder_name, extents, - offset, args) + self.map, self.size, decoder_name, extents, offset, args + ) readonly = 1 # After trashing self.im, # we might need to reload the palette data. if self.palette: self.palette.dirty = 1 - except (AttributeError, EnvironmentError, ImportError): + except (AttributeError, OSError, ImportError): self.map = None self.load_prepare() @@ -208,8 +222,9 @@ class ImageFile(Image.Image): prefix = b"" for decoder_name, extents, offset, args in self.tile: - decoder = Image._getdecoder(self.mode, decoder_name, - args, self.decoderconfig) + decoder = Image._getdecoder( + self.mode, decoder_name, args, self.decoderconfig + ) try: seek(offset) decoder.setimage(self.im, extents) @@ -226,16 +241,16 @@ class ImageFile(Image.Image): if LOAD_TRUNCATED_IMAGES: break else: - raise IOError("image file is truncated") + raise OSError("image file is truncated") if not s: # truncated jpeg if LOAD_TRUNCATED_IMAGES: break else: - self.tile = [] - raise IOError("image file is truncated " - "(%d bytes not processed)" % - len(b)) + raise OSError( + "image file is truncated " + "(%d bytes not processed)" % len(b) + ) b = b + s n, err_code = decoder.decode(b) @@ -263,8 +278,7 @@ class ImageFile(Image.Image): def load_prepare(self): # create image memory if necessary - if not self.im or\ - self.im.mode != self.mode or self.im.size != self.size: + if not self.im or self.im.mode != self.mode or self.im.size != self.size: self.im = Image.core.new(self.mode, self.size) # create palette (optional) if self.mode == "P": @@ -283,11 +297,15 @@ class ImageFile(Image.Image): # pass def _seek_check(self, frame): - if (frame < self._min_frame or + if ( + frame < self._min_frame # Only check upper limit on frames if additional seek operations # are not required to do so - (not (hasattr(self, "_n_frames") and self._n_frames is None) and - frame >= self.n_frames+self._min_frame)): + or ( + not (hasattr(self, "_n_frames") and self._n_frames is None) + and frame >= self.n_frames + self._min_frame + ) + ): raise EOFError("attempt to seek outside sequence") return self.tell() != frame @@ -302,14 +320,12 @@ class StubImageFile(ImageFile): """ def _open(self): - raise NotImplementedError( - "StubImageFile subclass must implement _open" - ) + raise NotImplementedError("StubImageFile subclass must implement _open") 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 (!) @@ -318,16 +334,15 @@ class StubImageFile(ImageFile): def _load(self): """(Hook) Find actual image loader.""" - raise NotImplementedError( - "StubImageFile subclass must implement _load" - ) + raise NotImplementedError("StubImageFile subclass must implement _load") -class Parser(object): +class Parser: """ Incremental image parser. This class implements the standard feed/close consumer interface. """ + incremental = None image = None data = None @@ -399,7 +414,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: @@ -412,15 +427,13 @@ class Parser(object): im.load_prepare() d, e, o, a = im.tile[0] im.tile = [] - self.decoder = Image._getdecoder( - im.mode, d, a, im.decoderconfig - ) + self.decoder = Image._getdecoder(im.mode, d, a, im.decoderconfig) self.decoder.setimage(im.im, e) # calculate decoder offset self.offset = o if self.offset <= len(self.data): - self.data = self.data[self.offset:] + self.data = self.data[self.offset :] self.offset = 0 self.image = im @@ -446,9 +459,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 @@ -462,6 +475,7 @@ class Parser(object): # -------------------------------------------------------------------- + def _save(im, fp, tile, bufsize=0): """Helper to save image based on tile list @@ -491,7 +505,7 @@ def _save(im, fp, tile, bufsize=0): for e, b, o, a in tile: e = Image._getencoder(im.mode, e, a, im.encoderconfig) if o > 0: - fp.seek(o, 0) + fp.seek(o) e.setimage(im.im, b) if e.pushes_fd: e.setfd(fp) @@ -503,14 +517,14 @@ 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 for e, b, o, a in tile: e = Image._getencoder(im.mode, e, a, im.encoderconfig) if o > 0: - fp.seek(o, 0) + fp.seek(o) e.setimage(im.im, b) if e.pushes_fd: e.setfd(fp) @@ -518,7 +532,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() @@ -548,7 +562,7 @@ def _safe_read(fp, size): return b"".join(data) -class PyCodecState(object): +class PyCodecState: def __init__(self): self.xsize = 0 self.ysize = 0 @@ -556,11 +570,10 @@ class PyCodecState(object): self.yoff = 0 def extents(self): - return (self.xoff, self.yoff, - self.xoff+self.xsize, self.yoff+self.ysize) + 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. @@ -595,8 +608,6 @@ class PyDecoder(object): Override to perform the decoding process. :param buffer: A bytes object with the data to be decoded. - If `handles_eof` is set, then `buffer` will be empty and `self.fd` - will be set. :returns: A tuple of (bytes consumed, errcode). If finished with decoding return <0 for the bytes consumed. Err codes are from `ERRORS` @@ -649,8 +660,10 @@ class PyDecoder(object): if self.state.xsize <= 0 or self.state.ysize <= 0: raise ValueError("Size cannot be negative") - if (self.state.xsize + self.state.xoff > self.im.size[0] or - self.state.ysize + self.state.yoff > self.im.size[1]): + if ( + self.state.xsize + self.state.xoff > self.im.size[0] + or self.state.ysize + self.state.yoff > self.im.size[1] + ): raise ValueError("Tile cannot extend outside image") def set_as_raw(self, data, rawmode=None): @@ -665,7 +678,7 @@ class PyDecoder(object): if not rawmode: rawmode = self.mode - d = Image._getdecoder(self.mode, 'raw', (rawmode)) + d = Image._getdecoder(self.mode, "raw", (rawmode)) d.setimage(self.im, self.state.extents()) s = d.decode(data) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 271f93b0a..6b0f5eb37 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -14,9 +14,6 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import division - import functools try: @@ -25,7 +22,7 @@ except ImportError: # pragma: no cover numpy = None -class Filter(object): +class Filter: pass @@ -57,12 +54,13 @@ class Kernel(BuiltinFilter): :param offset: Offset. If given, this value is added to the result, after it has been divided by the scale factor. """ + name = "Kernel" def __init__(self, size, kernel, scale=None, offset=0): if scale is None: # default scale is sum of kernel - scale = functools.reduce(lambda a, b: a+b, kernel) + scale = functools.reduce(lambda a, b: a + b, kernel) if size[0] * size[1] != len(kernel): raise ValueError("not enough coefficients in kernel") self.filterargs = size, scale, offset, kernel @@ -78,6 +76,7 @@ class RankFilter(Filter): ``size * size / 2`` for a median filter, ``size * size - 1`` for a max filter, etc. """ + name = "Rank" def __init__(self, size, rank): @@ -87,7 +86,7 @@ class RankFilter(Filter): def filter(self, image): if image.mode == "P": raise ValueError("cannot filter palette images") - image = image.expand(self.size//2, self.size//2) + image = image.expand(self.size // 2, self.size // 2) return image.rankfilter(self.size, self.rank) @@ -98,11 +97,12 @@ class MedianFilter(RankFilter): :param size: The kernel size, in pixels. """ + name = "Median" def __init__(self, size=3): self.size = size - self.rank = size*size//2 + self.rank = size * size // 2 class MinFilter(RankFilter): @@ -112,6 +112,7 @@ class MinFilter(RankFilter): :param size: The kernel size, in pixels. """ + name = "Min" def __init__(self, size=3): @@ -126,11 +127,12 @@ class MaxFilter(RankFilter): :param size: The kernel size, in pixels. """ + name = "Max" def __init__(self, size=3): self.size = size - self.rank = size*size-1 + self.rank = size * size - 1 class ModeFilter(Filter): @@ -141,6 +143,7 @@ class ModeFilter(Filter): :param size: The kernel size, in pixels. """ + name = "Mode" def __init__(self, size=3): @@ -155,6 +158,7 @@ class GaussianBlur(MultibandFilter): :param radius: Blur radius. """ + name = "GaussianBlur" def __init__(self, radius=2): @@ -175,6 +179,7 @@ class BoxBlur(MultibandFilter): returns an identical image. Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total. """ + name = "BoxBlur" def __init__(self, radius): @@ -198,6 +203,7 @@ class UnsharpMask(MultibandFilter): .. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking """ # noqa: E501 + name = "UnsharpMask" def __init__(self, radius=2, percent=150, threshold=3): @@ -211,96 +217,116 @@ class UnsharpMask(MultibandFilter): class BLUR(BuiltinFilter): name = "Blur" + # fmt: off filterargs = (5, 5), 16, 0, ( - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1 - ) + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + ) + # fmt: on class CONTOUR(BuiltinFilter): name = "Contour" + # fmt: off filterargs = (3, 3), 1, 255, ( -1, -1, -1, -1, 8, -1, - -1, -1, -1 - ) + -1, -1, -1, + ) + # fmt: on class DETAIL(BuiltinFilter): name = "Detail" + # fmt: off filterargs = (3, 3), 6, 0, ( - 0, -1, 0, + 0, -1, 0, -1, 10, -1, - 0, -1, 0 - ) + 0, -1, 0, + ) + # fmt: on class EDGE_ENHANCE(BuiltinFilter): name = "Edge-enhance" + # fmt: off filterargs = (3, 3), 2, 0, ( -1, -1, -1, -1, 10, -1, - -1, -1, -1 - ) + -1, -1, -1, + ) + # fmt: on class EDGE_ENHANCE_MORE(BuiltinFilter): name = "Edge-enhance More" + # fmt: off filterargs = (3, 3), 1, 0, ( -1, -1, -1, -1, 9, -1, - -1, -1, -1 - ) + -1, -1, -1, + ) + # fmt: on class EMBOSS(BuiltinFilter): name = "Emboss" + # fmt: off filterargs = (3, 3), 1, 128, ( - -1, 0, 0, - 0, 1, 0, - 0, 0, 0 - ) + -1, 0, 0, + 0, 1, 0, + 0, 0, 0, + ) + # fmt: on class FIND_EDGES(BuiltinFilter): name = "Find Edges" + # fmt: off filterargs = (3, 3), 1, 0, ( -1, -1, -1, -1, 8, -1, - -1, -1, -1 - ) + -1, -1, -1, + ) + # fmt: on class SHARPEN(BuiltinFilter): name = "Sharpen" + # fmt: off filterargs = (3, 3), 16, 0, ( -2, -2, -2, -2, 32, -2, - -2, -2, -2 - ) + -2, -2, -2, + ) + # fmt: on class SMOOTH(BuiltinFilter): name = "Smooth" + # fmt: off filterargs = (3, 3), 13, 0, ( - 1, 1, 1, - 1, 5, 1, - 1, 1, 1 - ) + 1, 1, 1, + 1, 5, 1, + 1, 1, 1, + ) + # fmt: on class SMOOTH_MORE(BuiltinFilter): name = "Smooth More" + # fmt: off filterargs = (5, 5), 100, 0, ( - 1, 1, 1, 1, 1, - 1, 5, 5, 5, 1, - 1, 5, 44, 5, 1, - 1, 5, 5, 5, 1, - 1, 1, 1, 1, 1 - ) + 1, 1, 1, 1, 1, + 1, 5, 5, 5, 1, + 1, 5, 44, 5, 1, + 1, 5, 5, 5, 1, + 1, 1, 1, 1, 1, + ) + # fmt: on class Color3DLUT(MultibandFilter): @@ -327,6 +353,7 @@ class Color3DLUT(MultibandFilter): than ``channels`` channels. Default is ``None``, which means that mode wouldn't be changed. """ + name = "Color 3D LUT" def __init__(self, size, table, channels=3, target_mode=None, **kwargs): @@ -338,7 +365,7 @@ class Color3DLUT(MultibandFilter): # Hidden flag `_copy_table=False` could be used to avoid extra copying # of the table if the table is specially made for the constructor. - copy_table = kwargs.get('_copy_table', True) + copy_table = kwargs.get("_copy_table", True) items = size[0] * size[1] * size[2] wrong_size = False @@ -346,8 +373,11 @@ class Color3DLUT(MultibandFilter): if copy_table: table = table.copy() - if table.shape in [(items * channels,), (items, channels), - (size[2], size[1], size[0], channels)]: + if table.shape in [ + (items * channels,), + (items, channels), + (size[2], size[1], size[0], channels), + ]: table = table.reshape(items * channels) else: wrong_size = True @@ -363,7 +393,8 @@ class Color3DLUT(MultibandFilter): if len(pixel) != channels: raise ValueError( "The elements of the table should " - "have a length of {}.".format(channels)) + "have a length of {}.".format(channels) + ) table.extend(pixel) if wrong_size or len(table) != items * channels: @@ -371,7 +402,9 @@ class Color3DLUT(MultibandFilter): "The table should have either channels * size**3 float items " "or size**3 items of channels-sized tuples with floats. " "Table should be: {}x{}x{}x{}. Actual length: {}".format( - channels, size[0], size[1], size[2], len(table))) + channels, size[0], size[1], size[2], len(table) + ) + ) self.table = table @staticmethod @@ -379,8 +412,9 @@ class Color3DLUT(MultibandFilter): try: _, _, _ = size except ValueError: - raise ValueError("Size should be either an integer or " - "a tuple of three integers.") + raise ValueError( + "Size should be either an integer or a tuple of three integers." + ) except TypeError: size = (size, size, size) size = [int(x) for x in size] @@ -411,15 +445,20 @@ class Color3DLUT(MultibandFilter): for b in range(size3D): for g in range(size2D): for r in range(size1D): - table[idx_out:idx_out + channels] = callback( - r / (size1D-1), g / (size2D-1), b / (size3D-1)) + table[idx_out : idx_out + channels] = callback( + r / (size1D - 1), g / (size2D - 1), b / (size3D - 1) + ) idx_out += channels - return cls((size1D, size2D, size3D), table, channels=channels, - target_mode=target_mode, _copy_table=False) + return cls( + (size1D, size2D, size3D), + table, + channels=channels, + target_mode=target_mode, + _copy_table=False, + ) - def transform(self, callback, with_normals=False, channels=None, - target_mode=None): + def transform(self, callback, with_normals=False, channels=None, target_mode=None): """Transforms the table values using provided callback and returns a new LUT with altered values. @@ -450,24 +489,31 @@ class Color3DLUT(MultibandFilter): for b in range(size3D): for g in range(size2D): for r in range(size1D): - values = self.table[idx_in:idx_in + ch_in] + values = self.table[idx_in : idx_in + ch_in] if with_normals: - values = callback(r / (size1D-1), g / (size2D-1), - b / (size3D-1), *values) + values = callback( + r / (size1D - 1), + g / (size2D - 1), + b / (size3D - 1), + *values, + ) else: values = callback(*values) - table[idx_out:idx_out + ch_out] = values + table[idx_out : idx_out + ch_out] = values idx_in += ch_in idx_out += ch_out - return type(self)(self.size, table, channels=ch_out, - target_mode=target_mode or self.mode, - _copy_table=False) + return type(self)( + self.size, + table, + channels=ch_out, + target_mode=target_mode or self.mode, + _copy_table=False, + ) def __repr__(self): r = [ - "{} from {}".format(self.__class__.__name__, - self.table.__class__.__name__), + "{} from {}".format(self.__class__.__name__, self.table.__class__.__name__), "size={:d}x{:d}x{:d}".format(*self.size), "channels={:d}".format(self.channels), ] @@ -479,5 +525,11 @@ class Color3DLUT(MultibandFilter): from . import Image return image.color_lut_3d( - self.mode or image.mode, Image.LINEAR, self.channels, - self.size[0], self.size[1], self.size[2], self.table) + self.mode or image.mode, + Image.LINEAR, + self.channels, + self.size[0], + self.size[1], + self.size[2], + self.table, + ) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 7454b4413..619800829 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -25,16 +25,19 @@ # See the README file for information on usage and redistribution. # -from . import Image -from ._util import isDirectory, isPath, py3 +import base64 import os import sys +from io import BytesIO + +from . import Image +from ._util import isDirectory, isPath LAYOUT_BASIC = 0 LAYOUT_RAQM = 1 -class _imagingft_not_installed(object): +class _imagingft_not_installed: # module placeholder def __getattr__(self, id): raise ImportError("The _imagingft C module is not installed") @@ -62,7 +65,7 @@ except ImportError: # -------------------------------------------------------------------- -class ImageFont(object): +class ImageFont: "PIL font wrapper" def _load_pilfont(self, filename): @@ -78,7 +81,7 @@ class ImageFont(object): if image and image.mode in ("1", "L"): break else: - raise IOError("cannot find glyph data file") + raise OSError("cannot find glyph data file") self.file = fullname @@ -98,7 +101,7 @@ class ImageFont(object): self.info.append(s) # read PILfont metrics - data = file.read(256*20) + data = file.read(256 * 20) # check image if image.mode not in ("1", "L"): @@ -109,9 +112,33 @@ class ImageFont(object): self.font = Image.core.font(image.im, data) def getsize(self, text, *args, **kwargs): + """ + Returns width and height (in pixels) of given text. + + :param text: Text to measure. + + :return: (width, height) + """ return self.font.getsize(text) def getmask(self, text, mode="", *args, **kwargs): + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + maximum value of 255. Otherwise, it should have mode ``1``. + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :return: An internal PIL storage memory instance as defined by the + :py:mod:`PIL.Image.core` interface module. + """ return self.font.getmask(text, mode) @@ -119,11 +146,11 @@ class ImageFont(object): # Wrapper for FreeType fonts. Application code should use the # 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): + def __init__(self, font=None, size=10, index=0, encoding="", layout_engine=None): # FIXME: use service provider instead self.path = font @@ -135,60 +162,317 @@ class FreeTypeFont(object): layout_engine = LAYOUT_BASIC if core.HAVE_RAQM: layout_engine = LAYOUT_RAQM - if layout_engine == LAYOUT_RAQM and not core.HAVE_RAQM: + elif layout_engine == LAYOUT_RAQM and not core.HAVE_RAQM: layout_engine = LAYOUT_BASIC self.layout_engine = layout_engine - if isPath(font): - self.font = core.getfont(font, size, index, encoding, - layout_engine=layout_engine) - else: - self.font_bytes = font.read() + def load_from_bytes(f): + self.font_bytes = f.read() self.font = core.getfont( - "", size, index, encoding, self.font_bytes, layout_engine) + "", size, index, encoding, self.font_bytes, layout_engine + ) + + if isPath(font): + if sys.platform == "win32": + font_bytes_path = font if isinstance(font, bytes) else font.encode() + try: + font_bytes_path.decode("ascii") + except UnicodeDecodeError: + # FreeType cannot load fonts with non-ASCII characters on Windows + # So load it into memory first + with open(font, "rb") as f: + load_from_bytes(f) + return + self.font = core.getfont( + font, size, index, encoding, layout_engine=layout_engine + ) + else: + load_from_bytes(font) def _multiline_split(self, text): split_character = "\n" if isinstance(text, str) else b"\n" return text.split(split_character) def getname(self): + """ + :return: A tuple of the font family (e.g. Helvetica) and the font style + (e.g. Bold) + """ return self.font.family, self.font.style def getmetrics(self): + """ + :return: A tuple of the font ascent (the distance from the baseline to + the highest outline point) and descent (the distance from the + baseline to the lowest outline point, a negative value) + """ return self.font.ascent, self.font.descent - def getsize(self, text, direction=None, features=None): - size, offset = self.font.getsize(text, direction, features) - return (size[0] + offset[0], size[1] + offset[1]) + def getsize( + self, text, direction=None, features=None, language=None, stroke_width=0 + ): + """ + Returns width and height (in pixels) of given text if rendered in font with + provided direction, features, and language. - def getsize_multiline(self, text, direction=None, - spacing=4, features=None): + :param text: Text to measure. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :return: (width, height) + """ + size, offset = self.font.getsize(text, direction, features, language) + return ( + size[0] + stroke_width * 2 + offset[0], + size[1] + stroke_width * 2 + offset[1], + ) + + def getsize_multiline( + self, + text, + direction=None, + spacing=4, + features=None, + language=None, + stroke_width=0, + ): + """ + Returns width and height (in pixels) of given text if rendered in font + with provided direction, features, and language, while respecting + newline characters. + + :param text: Text to measure. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param spacing: The vertical gap between lines, defaulting to 4 pixels. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :return: (width, height) + """ max_width = 0 lines = self._multiline_split(text) - line_spacing = self.getsize('A')[1] + spacing + line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing for line in lines: - line_width, line_height = self.getsize(line, direction, features) + line_width, line_height = self.getsize( + line, direction, features, language, stroke_width + ) max_width = max(max_width, line_width) - return max_width, len(lines)*line_spacing - spacing + return max_width, len(lines) * line_spacing - spacing def getoffset(self, text): + """ + Returns the offset of given text. This is the gap between the + starting coordinate and the first marking. Note that this gap is + included in the result of :py:func:`~PIL.ImageFont.FreeTypeFont.getsize`. + + :param text: Text to measure. + + :return: A tuple of the x and y offset + """ return self.font.getsize(text)[1] - def getmask(self, text, mode="", direction=None, features=None): - return self.getmask2(text, mode, direction=direction, - features=features)[0] + def getmask( + self, + text, + mode="", + direction=None, + features=None, + language=None, + stroke_width=0, + ): + """ + Create a bitmap for the text. - def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, - features=None, *args, **kwargs): - size, offset = self.font.getsize(text, direction, features) + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + maximum value of 255. Otherwise, it should have mode ``1``. + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :return: An internal PIL storage memory instance as defined by the + :py:mod:`PIL.Image.core` interface module. + """ + return self.getmask2( + text, + mode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + )[0] + + def getmask2( + self, + text, + mode="", + fill=Image.core.fill, + direction=None, + features=None, + language=None, + stroke_width=0, + *args, + **kwargs + ): + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + maximum value of 255. Otherwise, it should have mode ``1``. + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :return: A tuple of an internal PIL storage memory instance as defined by the + :py:mod:`PIL.Image.core` interface module, and the text offset, the + gap between the starting coordinate and the first marking + """ + size, offset = self.font.getsize(text, direction, features, language) + size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 im = fill("L", size, 0) - self.font.render(text, im.id, mode == "1", direction, features) + self.font.render( + text, im.id, mode == "1", direction, features, language, stroke_width + ) return im, offset - def font_variant(self, font=None, size=None, index=None, encoding=None, - layout_engine=None): + def font_variant( + self, font=None, size=None, index=None, encoding=None, layout_engine=None + ): """ Create a copy of this FreeTypeFont object, using any specified arguments to override the settings. @@ -203,11 +487,64 @@ class FreeTypeFont(object): size=self.size if size is None else size, index=self.index if index is None else index, encoding=self.encoding if encoding is None else encoding, - layout_engine=layout_engine or self.layout_engine + layout_engine=layout_engine or self.layout_engine, ) + def get_variation_names(self): + """ + :returns: A list of the named styles in a variation font. + :exception IOError: If the font is not a variation font. + """ + try: + names = self.font.getvarnames() + except AttributeError: + raise NotImplementedError("FreeType 2.9.1 or greater is required") + return [name.replace(b"\x00", b"") for name in names] -class TransposedFont(object): + def set_variation_by_name(self, name): + """ + :param name: The name of the style. + :exception IOError: If the font is not a variation font. + """ + names = self.get_variation_names() + if not isinstance(name, bytes): + name = name.encode() + index = names.index(name) + + if index == getattr(self, "_last_variation_index", None): + # When the same name is set twice in a row, + # there is an 'unknown freetype error' + # https://savannah.nongnu.org/bugs/?56186 + return + self._last_variation_index = index + + self.font.setvarname(index) + + def get_variation_axes(self): + """ + :returns: A list of the axes in a variation font. + :exception IOError: If the font is not a variation font. + """ + try: + axes = self.font.getvaraxes() + except AttributeError: + raise NotImplementedError("FreeType 2.9.1 or greater is required") + for axis in axes: + axis["name"] = axis["name"].replace(b"\x00", b"") + return axes + + def set_variation_by_axes(self, axes): + """ + :param axes: A list of values for each axis. + :exception IOError: If the font is not a variation font. + """ + try: + self.font.setvaraxes(axes) + except AttributeError: + raise NotImplementedError("FreeType 2.9.1 or greater is required") + + +class TransposedFont: "Wrapper for writing rotated or mirrored text" def __init__(self, font, orientation=None): @@ -250,35 +587,62 @@ def load(filename): return f -def truetype(font=None, size=10, index=0, encoding="", - layout_engine=None): +def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): """ Load a TrueType or OpenType font from a file or file-like object, and create a font object. This function loads a font object from the given file or file-like object, and creates a font object for a font of the given size. + Pillow uses FreeType to open font files. If you are opening many fonts + simultaneously on Windows, be aware that Windows limits the number of files + that can be open in C at once to 512. If you approach that limit, an + ``OSError`` may be thrown, reporting that FreeType "cannot open resource". + This function requires the _imagingft service. :param font: A filename or file-like object containing a TrueType font. - Under Windows, if the file is not found in this filename, - the loader also looks in Windows :file:`fonts/` directory. + If the file is not found in this filename, the loader may also + search in other directories, such as the :file:`fonts/` + directory on Windows or :file:`/Library/Fonts/`, + :file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on + macOS. + :param size: The requested size, in points. :param index: Which font face to load (default is first available face). - :param encoding: Which font encoding to use (default is Unicode). Common - encodings are "unic" (Unicode), "symb" (Microsoft - Symbol), "ADOB" (Adobe Standard), "ADBE" (Adobe Expert), - and "armn" (Apple Roman). See the FreeType documentation - for more information. + :param encoding: Which font encoding to use (default is Unicode). Possible + encodings include (see the FreeType documentation for more + information): + + * "unic" (Unicode) + * "symb" (Microsoft Symbol) + * "ADOB" (Adobe Standard) + * "ADBE" (Adobe Expert) + * "ADBC" (Adobe Custom) + * "armn" (Apple Roman) + * "sjis" (Shift JIS) + * "gb " (PRC) + * "big5" + * "wans" (Extended Wansung) + * "joha" (Johab) + * "lat1" (Latin-1) + + This specifies the character set to use. It does not alter the + encoding of any text provided in subsequent operations. :param layout_engine: Which layout engine to use, if available: `ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`. :return: A font object. :exception IOError: If the file could not be read. """ - try: + def freetype(font): return FreeTypeFont(font, size, index, encoding, layout_engine) - except IOError: + + try: + return freetype(font) + except OSError: + if not isPath(font): + raise ttf_filename = os.path.basename(font) dirs = [] @@ -289,17 +653,19 @@ def truetype(font=None, size=10, index=0, encoding="", windir = os.environ.get("WINDIR") if windir: dirs.append(os.path.join(windir, "fonts")) - elif sys.platform in ('linux', 'linux2'): + elif sys.platform in ("linux", "linux2"): lindirs = os.environ.get("XDG_DATA_DIRS", "") if not lindirs: # According to the freedesktop spec, XDG_DATA_DIRS should # default to /usr/share - lindirs = '/usr/share' - dirs += [os.path.join(lindir, "fonts") - for lindir in lindirs.split(":")] - elif sys.platform == 'darwin': - dirs += ['/Library/Fonts', '/System/Library/Fonts', - os.path.expanduser('~/Library/Fonts')] + lindirs = "/usr/share" + dirs += [os.path.join(lindir, "fonts") for lindir in lindirs.split(":")] + elif sys.platform == "darwin": + dirs += [ + "/Library/Fonts", + "/System/Library/Fonts", + os.path.expanduser("~/Library/Fonts"), + ] ext = os.path.splitext(ttf_filename)[1] first_font_with_a_different_extension = None @@ -307,21 +673,15 @@ def truetype(font=None, size=10, index=0, encoding="", for walkroot, walkdir, walkfilenames in os.walk(directory): for walkfilename in walkfilenames: if ext and walkfilename == ttf_filename: + return freetype(os.path.join(walkroot, walkfilename)) + elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename: fontpath = os.path.join(walkroot, walkfilename) - return FreeTypeFont(fontpath, size, index, - encoding, layout_engine) - elif (not ext and - os.path.splitext(walkfilename)[0] == ttf_filename): - fontpath = os.path.join(walkroot, walkfilename) - if os.path.splitext(fontpath)[1] == '.ttf': - return FreeTypeFont(fontpath, size, index, - encoding, layout_engine) - if not ext \ - and first_font_with_a_different_extension is None: + if os.path.splitext(fontpath)[1] == ".ttf": + return freetype(fontpath) + if not ext and first_font_with_a_different_extension is None: first_font_with_a_different_extension = fontpath if first_font_with_a_different_extension: - return FreeTypeFont(first_font_with_a_different_extension, size, - index, encoding, layout_engine) + return freetype(first_font_with_a_different_extension) raise @@ -337,15 +697,12 @@ def load_path(filename): for directory in sys.path: if isDirectory(directory): if not isinstance(filename, str): - if py3: - filename = filename.decode("utf-8") - else: - filename = filename.encode("utf-8") + filename = filename.decode("utf-8") try: return load(os.path.join(directory, filename)) - except IOError: + except OSError: pass - raise IOError("cannot find font file") + raise OSError("cannot find font file") def load_default(): @@ -355,12 +712,12 @@ def load_default(): :return: A font object. """ - from io import BytesIO - import base64 f = ImageFont() f._load_pilfont_data( # courB08 - BytesIO(base64.b64decode(b''' + BytesIO( + base64.b64decode( + b""" UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA @@ -452,7 +809,13 @@ AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA//// +QAGAAIAzgAKANUAEw== -''')), Image.open(BytesIO(base64.b64decode(b''' +""" + ) + ), + Image.open( + BytesIO( + base64.b64decode( + b""" iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9 M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g @@ -476,5 +839,9 @@ evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v// Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR w7IkEbzhVQAAAABJRU5ErkJggg== -''')))) +""" + ) + ) + ), + ) return f diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index d0fe76ef4..e587d942d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -15,51 +15,58 @@ # See the README file for information on usage and redistribution. # +import os +import subprocess +import sys +import tempfile + from . import Image -import sys if sys.platform not in ["win32", "darwin"]: raise ImportError("ImageGrab is macOS and Windows only") -if sys.platform == "win32": - grabber = Image.core.grabscreen -elif sys.platform == "darwin": - import os - import tempfile - import subprocess - -def grab(bbox=None): +def grab(bbox=None, include_layered_windows=False, all_screens=False): if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp('.png') + fh, filepath = tempfile.mkstemp(".png") os.close(fh) - subprocess.call(['screencapture', '-x', filepath]) + subprocess.call(["screencapture", "-x", filepath]) im = Image.open(filepath) im.load() os.unlink(filepath) + if bbox: + im = im.crop(bbox) else: - size, data = grabber() + offset, size, data = Image.core.grabscreen(include_layered_windows, all_screens) im = Image.frombytes( - "RGB", size, data, + "RGB", + size, + data, # RGB, 32-bit line padding, origin lower left corner - "raw", "BGR", (size[0]*3 + 3) & -4, -1 - ) - if bbox: - im = im.crop(bbox) + "raw", + "BGR", + (size[0] * 3 + 3) & -4, + -1, + ) + if bbox: + x0, y0 = offset + left, top, right, bottom = bbox + im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) return im def grabclipboard(): if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp('.jpg') + fh, filepath = tempfile.mkstemp(".jpg") os.close(fh) commands = [ - "set theFile to (open for access POSIX file \"" - + filepath + "\" with write permission)", + 'set theFile to (open for access POSIX file "' + + filepath + + '" with write permission)', "try", " write (the clipboard as JPEG picture) to theFile", "end try", - "close access theFile" + "close access theFile", ] script = ["osascript"] for command in commands: @@ -77,5 +84,6 @@ def grabclipboard(): if isinstance(data, bytes): from . import BmpImagePlugin import io + return BmpImagePlugin.DibImageFile(io.BytesIO(data)) return data diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index d985877a6..adbb94000 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -15,23 +15,18 @@ # See the README file for information on usage and redistribution. # -from . import Image, _imagingmath -from ._util import py3 +import builtins -try: - import builtins -except ImportError: - import __builtin__ - builtins = __builtin__ +from . import Image, _imagingmath VERBOSE = 0 def _isconstant(v): - return isinstance(v, int) or isinstance(v, float) + return isinstance(v, (int, float)) -class _Operand(object): +class _Operand: """Wraps an image operand, providing standard operators""" def __init__(self, im): @@ -61,7 +56,7 @@ class _Operand(object): out = Image.new(mode or im1.mode, im1.size, None) im1.load() try: - op = getattr(_imagingmath, op+"_"+im1.mode) + op = getattr(_imagingmath, op + "_" + im1.mode) except AttributeError: raise TypeError("bad operand type for '%s'" % op) _imagingmath.unop(op, out.im.id, im1.im.id) @@ -78,8 +73,7 @@ class _Operand(object): raise ValueError("mode mismatch") if im1.size != im2.size: # crop both arguments to a common size - size = (min(im1.size[0], im2.size[0]), - min(im1.size[1], im2.size[1])) + size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1])) if im1.size != size: im1 = im1.crop((0, 0) + size) if im2.size != size: @@ -90,7 +84,7 @@ class _Operand(object): im1.load() im2.load() try: - op = getattr(_imagingmath, op+"_"+im1.mode) + op = getattr(_imagingmath, op + "_" + im1.mode) except AttributeError: raise TypeError("bad operand type for '%s'" % op) _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id) @@ -101,11 +95,6 @@ class _Operand(object): # an image is "true" if it contains at least one non-zero pixel return self.im.getbbox() is not None - if not py3: - # Provide __nonzero__ for pre-Py3k - __nonzero__ = __bool__ - del __bool__ - def __abs__(self): return self.apply("abs", self) @@ -152,13 +141,6 @@ class _Operand(object): def __rpow__(self, other): return self.apply("pow", other, self) - if not py3: - # Provide __div__ and __rdiv__ for pre-Py3k - __div__ = __truediv__ - __rdiv__ = __rtruediv__ - del __truediv__ - del __rtruediv__ - # bitwise def __invert__(self): return self.apply("invert", self) diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 2b3377a14..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): @@ -37,20 +37,28 @@ def getmode(mode): # initialize mode cache from . import Image + modes = {} # core modes for m, (basemode, basetype, bands) in Image._MODEINFO.items(): modes[m] = ModeDescriptor(m, bands, basemode, basetype) # extra experimental modes - modes["RGBa"] = ModeDescriptor("RGBa", - ("R", "G", "B", "a"), "RGB", "L") + modes["RGBa"] = ModeDescriptor("RGBa", ("R", "G", "B", "a"), "RGB", "L") modes["LA"] = ModeDescriptor("LA", ("L", "A"), "L", "L") modes["La"] = ModeDescriptor("La", ("L", "a"), "L", "L") modes["PA"] = ModeDescriptor("PA", ("P", "A"), "RGB", "L") # mapping modes - modes["I;16"] = ModeDescriptor("I;16", "I", "L", "L") - modes["I;16L"] = ModeDescriptor("I;16L", "I", "L", "L") - modes["I;16B"] = ModeDescriptor("I;16B", "I", "L", "L") + for i16mode in ( + "I;16", + "I;16S", + "I;16L", + "I;16LS", + "I;16B", + "I;16BS", + "I;16N", + "I;16NS", + ): + modes[i16mode] = ModeDescriptor(i16mode, ("I",), "L", "L") # set global mode cache atomically _modes = modes return _modes[mode] diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 54ceb7905..d1ec09eac 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -5,15 +5,27 @@ # # Copyright (c) 2014 Dov Grobgeld -from __future__ import print_function +import re from . import Image, _imagingmorph -import re LUT_SIZE = 1 << 9 +# fmt: off +ROTATION_MATRIX = [ + 6, 3, 0, + 7, 4, 1, + 8, 5, 2, +] +MIRROR_MATRIX = [ + 2, 1, 0, + 5, 4, 3, + 8, 7, 6, +] +# 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:: @@ -48,6 +60,7 @@ class LutBuilder(object): lut = lb.build_lut() """ + def __init__(self, patterns=None, op_name=None): if patterns is not None: self.patterns = patterns @@ -56,20 +69,19 @@ class LutBuilder(object): self.lut = None if op_name is not None: known_patterns = { - 'corner': ['1:(... ... ...)->0', - '4:(00. 01. ...)->1'], - 'dilation4': ['4:(... .0. .1.)->1'], - 'dilation8': ['4:(... .0. .1.)->1', - '4:(... .0. ..1)->1'], - 'erosion4': ['4:(... .1. .0.)->0'], - 'erosion8': ['4:(... .1. .0.)->0', - '4:(... .1. ..0)->0'], - 'edge': ['1:(... ... ...)->0', - '4:(.0. .1. ...)->1', - '4:(01. .1. ...)->1'] + "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"], + "dilation4": ["4:(... .0. .1.)->1"], + "dilation8": ["4:(... .0. .1.)->1", "4:(... .0. ..1)->1"], + "erosion4": ["4:(... .1. .0.)->0"], + "erosion8": ["4:(... .1. .0.)->0", "4:(... .1. ..0)->0"], + "edge": [ + "1:(... ... ...)->0", + "4:(.0. .1. ...)->1", + "4:(01. .1. ...)->1", + ], } if op_name not in known_patterns: - raise Exception('Unknown pattern '+op_name+'!') + raise Exception("Unknown pattern " + op_name + "!") self.patterns = known_patterns[op_name] @@ -88,8 +100,8 @@ class LutBuilder(object): """string_permute takes a pattern and a permutation and returns the string permuted according to the permutation list. """ - assert(len(permutation) == 9) - return ''.join(pattern[p] for p in permutation) + assert len(permutation) == 9 + return "".join(pattern[p] for p in permutation) def _pattern_permute(self, basic_pattern, options, basic_result): """pattern_permute takes a basic pattern and its result and clones @@ -98,32 +110,25 @@ class LutBuilder(object): patterns = [(basic_pattern, basic_result)] # rotations - if '4' in options: + if "4" in options: res = patterns[-1][1] for i in range(4): patterns.append( - (self._string_permute(patterns[-1][0], [6, 3, 0, - 7, 4, 1, - 8, 5, 2]), res)) + (self._string_permute(patterns[-1][0], ROTATION_MATRIX), res) + ) # mirror - if 'M' in options: + if "M" in options: n = len(patterns) for pattern, res in patterns[0:n]: - patterns.append( - (self._string_permute(pattern, [2, 1, 0, - 5, 4, 3, - 8, 7, 6]), res)) + patterns.append((self._string_permute(pattern, MIRROR_MATRIX), res)) # negate - if 'N' in options: + if "N" in options: n = len(patterns) for pattern, res in patterns[0:n]: # Swap 0 and 1 - pattern = (pattern - .replace('0', 'Z') - .replace('1', '0') - .replace('Z', '1')) - res = 1-int(res) + pattern = pattern.replace("0", "Z").replace("1", "0").replace("Z", "1") + res = 1 - int(res) patterns.append((pattern, res)) return patterns @@ -138,22 +143,21 @@ class LutBuilder(object): # Parse and create symmetries of the patterns strings for p in self.patterns: - m = re.search( - r'(\w*):?\s*\((.+?)\)\s*->\s*(\d)', p.replace('\n', '')) + m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) if not m: - raise Exception('Syntax error in pattern "'+p+'"') + raise Exception('Syntax error in pattern "' + p + '"') options = m.group(1) pattern = m.group(2) result = int(m.group(3)) # Get rid of spaces - pattern = pattern.replace(' ', '').replace('\n', '') + pattern = pattern.replace(" ", "").replace("\n", "") patterns += self._pattern_permute(pattern, options, result) # compile the patterns into regular expressions for speed for i, pattern in enumerate(patterns): - p = pattern[0].replace('.', 'X').replace('X', '[01]') + p = pattern[0].replace(".", "X").replace("X", "[01]") p = re.compile(p) patterns[i] = (p, pattern[1]) @@ -163,7 +167,7 @@ class LutBuilder(object): for i in range(LUT_SIZE): # Build the bit pattern bitpattern = bin(i)[2:] - bitpattern = ('0'*(9-len(bitpattern)) + bitpattern)[::-1] + bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1] for p, r in patterns: if p.match(bitpattern): @@ -172,13 +176,10 @@ 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): + def __init__(self, lut=None, op_name=None, patterns=None): """Create a binary morphological operator""" self.lut = lut if op_name is not None: @@ -192,13 +193,12 @@ class MorphOp(object): Returns a tuple of the number of changed pixels and the morphed image""" if self.lut is None: - raise Exception('No operator loaded') + raise Exception("No operator loaded") - if image.mode != 'L': - raise Exception('Image must be binary, meaning it must use mode L') + if image.mode != "L": + raise Exception("Image must be binary, meaning it must use mode L") outimage = Image.new(image.mode, image.size, None) - count = _imagingmorph.apply( - bytes(self.lut), image.im.id, outimage.im.id) + count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage def match(self, image): @@ -208,10 +208,10 @@ class MorphOp(object): Returns a list of tuples of (x,y) coordinates of all matching pixels. See :ref:`coordinate-system`.""" if self.lut is None: - raise Exception('No operator loaded') + raise Exception("No operator loaded") - if image.mode != 'L': - raise Exception('Image must be binary, meaning it must use mode L') + if image.mode != "L": + raise Exception("Image must be binary, meaning it must use mode L") return _imagingmorph.match(bytes(self.lut), image.im.id) def get_on_pixels(self, image): @@ -220,24 +220,24 @@ class MorphOp(object): Returns a list of tuples of (x,y) coordinates of all matching pixels. See :ref:`coordinate-system`.""" - if image.mode != 'L': - raise Exception('Image must be binary, meaning it must use mode L') + if image.mode != "L": + raise Exception("Image must be binary, meaning it must use mode L") return _imagingmorph.get_on_pixels(image.im.id) def load_lut(self, filename): """Load an operator from an mrl file""" - with open(filename, 'rb') as f: + with open(filename, "rb") as f: self.lut = bytearray(f.read()) if len(self.lut) != LUT_SIZE: self.lut = None - raise Exception('Wrong size operator file!') + raise Exception("Wrong size operator file!") def save_lut(self, filename): """Save an operator to an mrl file""" if self.lut is None: - raise Exception('No operator loaded') - with open(filename, 'wb') as f: + raise Exception("No operator loaded") + with open(filename, "wb") as f: f.write(self.lut) def set_lut(self, lut): diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index b93f6f35d..3ffe50806 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -17,15 +17,15 @@ # See the README file for information on usage and redistribution. # -from . import Image -from ._util import isStringType -import operator import functools +import operator +from . import Image # # helpers + def _border(border): if isinstance(border, tuple): if len(border) == 2: @@ -38,8 +38,9 @@ def _border(border): def _color(color, mode): - if isStringType(color): + if isinstance(color, str): from . import ImageColor + color = ImageColor.getcolor(color, mode) return color @@ -53,7 +54,8 @@ 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") + # # actions @@ -75,7 +77,7 @@ def autocontrast(image, cutoff=0, ignore=None): histogram = image.histogram() lut = [] for layer in range(0, len(histogram), 256): - h = histogram[layer:layer+256] + h = histogram[layer : layer + 256] if ignore is not None: # get rid of outliers try: @@ -135,8 +137,7 @@ def autocontrast(image, cutoff=0, ignore=None): return _lut(image, lut) -def colorize(image, black, white, mid=None, blackpoint=0, - whitepoint=255, midpoint=127): +def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127): """ Colorize grayscale image. This function calculates a color wedge which maps all black pixels in @@ -276,9 +277,7 @@ def crop(image, border=0): :return: An image. """ left, top, right, bottom = _border(border) - return image.crop( - (left, top, image.size[0]-right, image.size[1]-bottom) - ) + return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) def scale(image, factor, resample=Image.NEAREST): @@ -298,8 +297,7 @@ def scale(image, factor, resample=Image.NEAREST): elif factor <= 0: raise ValueError("the factor must be greater than 0") else: - size = (int(round(factor * image.width)), - int(round(factor * image.height))) + size = (int(round(factor * image.width)), int(round(factor * image.height))) return image.resize(size, resample) @@ -314,9 +312,7 @@ def deform(image, deformer, resample=Image.BILINEAR): in the PIL.Image.transform function. :return: An image. """ - return image.transform( - image.size, Image.MESH, deformer.getmesh(image), resample - ) + return image.transform(image.size, Image.MESH, deformer.getmesh(image), resample) def equalize(image, mask=None): @@ -335,7 +331,7 @@ def equalize(image, mask=None): h = image.histogram(mask) lut = [] for b in range(0, len(h), 256): - histo = [_f for _f in h[b:b+256] if _f] + histo = [_f for _f in h[b : b + 256] if _f] if len(histo) <= 1: lut.extend(list(range(256))) else: @@ -346,7 +342,7 @@ def equalize(image, mask=None): n = step // 2 for i in range(256): lut.append(n // step) - n = n + h[i+b] + n = n + h[i + b] return _lut(image, lut) @@ -417,8 +413,10 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): # number of pixels to trim off on Top and Bottom, Left and Right bleed_pixels = (bleed * image.size[0], bleed * image.size[1]) - live_size = (image.size[0] - bleed_pixels[0] * 2, - image.size[1] - bleed_pixels[1] * 2) + live_size = ( + image.size[0] - bleed_pixels[0] * 2, + image.size[1] - bleed_pixels[1] * 2, + ) # calculate the aspect ratio of the live_size live_size_ratio = float(live_size[0]) / live_size[1] @@ -427,7 +425,11 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): output_ratio = float(size[0]) / size[1] # figure out if the sides or top/bottom will be cropped off - if live_size_ratio >= output_ratio: + if live_size_ratio == output_ratio: + # live_size is already the needed ratio + crop_width = live_size[0] + crop_height = live_size[1] + elif live_size_ratio >= output_ratio: # live_size is wider than what's needed, crop the sides crop_width = output_ratio * live_size[1] crop_height = live_size[1] @@ -437,13 +439,10 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): crop_height = live_size[0] / output_ratio # make the crop - crop_left = bleed_pixels[0] + (live_size[0]-crop_width) * centering[0] - crop_top = bleed_pixels[1] + (live_size[1]-crop_height) * centering[1] + crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0] + crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1] - crop = ( - crop_left, crop_top, - crop_left + crop_width, crop_top + crop_height - ) + crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) # resize the image and return it return image.resize(size, method, box=crop) @@ -478,7 +477,7 @@ def invert(image): """ lut = [] for i in range(256): - lut.append(255-i) + lut.append(255 - i) return _lut(image, lut) @@ -501,7 +500,7 @@ def posterize(image, bits): :return: An image. """ lut = [] - mask = ~(2**(8-bits)-1) + mask = ~(2 ** (8 - bits) - 1) for i in range(256): lut.append(i & mask) return _lut(image, lut) @@ -520,5 +519,32 @@ def solarize(image, threshold=128): if i < threshold: lut.append(i) else: - lut.append(255-i) + lut.append(255 - i) return _lut(image, lut) + + +def exif_transpose(image): + """ + If an image has an EXIF Orientation tag, return a new image that is + transposed accordingly. Otherwise, return a copy of the image. + + :param image: The image to transpose. + :return: An image. + """ + exif = image.getexif() + orientation = exif.get(0x0112) + method = { + 2: Image.FLIP_LEFT_RIGHT, + 3: Image.ROTATE_180, + 4: Image.FLIP_TOP_BOTTOM, + 5: Image.TRANSPOSE, + 6: Image.ROTATE_270, + 7: Image.TRANSVERSE, + 8: Image.ROTATE_90, + }.get(orientation) + if method is not None: + transposed_image = image.transpose(method) + del exif[0x0112] + transposed_image.info["exif"] = exif.tobytes() + return transposed_image + return image.copy() diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 81e99abbf..e0d439c98 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -17,10 +17,11 @@ # import array -from . import ImageColor, GimpPaletteFile, GimpGradientFile, PaletteFile + +from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile -class ImagePalette(object): +class ImagePalette: """ Color palette for palette mapped images @@ -38,11 +39,12 @@ class ImagePalette(object): def __init__(self, mode="RGB", palette=None, size=0): self.mode = mode self.rawmode = None # if set, palette contains raw data - self.palette = palette or bytearray(range(256))*len(self.mode) + self.palette = palette or bytearray(range(256)) * len(self.mode) self.colors = {} self.dirty = None - if ((size == 0 and len(self.mode)*256 != len(self.palette)) or - (size != 0 and size != len(self.palette))): + if (size == 0 and len(self.mode) * 256 != len(self.palette)) or ( + size != 0 and size != len(self.palette) + ): raise ValueError("wrong palette size") def copy(self): @@ -78,7 +80,7 @@ class ImagePalette(object): if isinstance(self.palette, bytes): return self.palette arr = array.array("B", self.palette) - if hasattr(arr, 'tobytes'): + if hasattr(arr, "tobytes"): return arr.tobytes() return arr.tostring() @@ -104,8 +106,8 @@ class ImagePalette(object): raise ValueError("cannot allocate more than 256 colors") self.colors[color] = index self.palette[index] = color[0] - self.palette[index+256] = color[1] - self.palette[index+512] = color[2] + self.palette[index + 256] = color[1] + self.palette[index + 512] = color[2] self.dirty = 1 return index else: @@ -124,7 +126,7 @@ class ImagePalette(object): fp.write("# Mode: %s\n" % self.mode) for i in range(256): fp.write("%d" % i) - for j in range(i*len(self.mode), (i+1)*len(self.mode)): + for j in range(i * len(self.mode), (i + 1) * len(self.mode)): try: fp.write(" %d" % self.palette[j]) except IndexError: @@ -136,6 +138,7 @@ class ImagePalette(object): # -------------------------------------------------------------------- # Internal + def raw(rawmode, data): palette = ImagePalette() palette.rawmode = rawmode @@ -147,11 +150,12 @@ def raw(rawmode, data): # -------------------------------------------------------------------- # Factories + def make_linear_lut(black, white): lut = [] if black == 0: for i in range(256): - lut.append(white*i//255) + lut.append(white * i // 255) else: raise NotImplementedError # FIXME return lut @@ -172,8 +176,9 @@ def negative(mode="RGB"): def random(mode="RGB"): from random import randint + palette = [] - for i in range(256*len(mode)): + for i in range(256 * len(mode)): palette.append(randint(0, 255)) return ImagePalette(mode, palette) @@ -199,7 +204,7 @@ def load(filename): for paletteHandler in [ GimpPaletteFile.GimpPaletteFile, GimpGradientFile.GimpGradientFile, - PaletteFile.PaletteFile + PaletteFile.PaletteFile, ]: try: fp.seek(0) @@ -211,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/ImagePath.py b/src/PIL/ImagePath.py index 8cbfec0d3..3d3538c97 100644 --- a/src/PIL/ImagePath.py +++ b/src/PIL/ImagePath.py @@ -16,5 +16,4 @@ from . import Image - Path = Image.core.path diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index b747781c5..dfe2f80bd 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -16,34 +16,24 @@ # See the README file for information on usage and redistribution. # -from . import Image -from ._util import isPath, py3 -from io import BytesIO import sys +from io import BytesIO + +from . import Image +from ._util import isPath + +qt_versions = [["5", "PyQt5"], ["side2", "PySide2"]] -qt_versions = [ - ['5', 'PyQt5'], - ['side2', 'PySide2'], - ['4', 'PyQt4'], - ['side', 'PySide'] -] # 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) +qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True) for qt_version, qt_module in qt_versions: try: - if qt_module == 'PyQt5': + if qt_module == "PyQt5": from PyQt5.QtGui import QImage, qRgba, QPixmap from PyQt5.QtCore import QBuffer, QIODevice - elif qt_module == 'PySide2': + 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 - elif qt_module == 'PySide': - from PySide.QtGui import QImage, qRgba, QPixmap - from PySide.QtCore import QBuffer, QIODevice except (ImportError, RuntimeError): continue qt_is_installed = True @@ -57,7 +47,7 @@ def rgb(r, g, b, a=255): """(Internal) Turns an RGB color into a Qt compatible color integer.""" # use qRgb to pack the colors, and then turn the resulting long # into a negative integer with the same bitpattern. - return (qRgba(r, g, b, a) & 0xffffffff) + return qRgba(r, g, b, a) & 0xFFFFFFFF def fromqimage(im): @@ -67,19 +57,15 @@ def fromqimage(im): """ buffer = QBuffer() buffer.open(QIODevice.ReadWrite) - # preserve alha channel with png + # preserve alpha channel with png # otherwise ppm is more friendly with Image.open if im.hasAlphaChannel(): - im.save(buffer, 'png') + im.save(buffer, "png") else: - im.save(buffer, 'ppm') + 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) @@ -105,11 +91,7 @@ def align8to32(bytes, width, mode): converts each scanline of data from 8 bit to 32 bit aligned """ - bits_per_pixel = { - '1': 1, - 'L': 8, - 'P': 8, - }[mode] + bits_per_pixel = {"1": 1, "L": 8, "P": 8}[mode] # calculate bytes per line and the extra padding if needed bits_per_line = bits_per_pixel * width @@ -124,10 +106,12 @@ def align8to32(bytes, width, mode): new_data = [] for i in range(len(bytes) // bytes_per_line): - new_data.append(bytes[i*bytes_per_line:(i+1)*bytes_per_line] - + b'\x00' * extra_padding) + new_data.append( + bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + + b"\x00" * extra_padding + ) - return b''.join(new_data) + return b"".join(new_data) def _toqclass_helper(im): @@ -137,10 +121,7 @@ def _toqclass_helper(im): # handle filename, if given instead of image name if hasattr(im, "toUtf8"): # FIXME - is this really the best way to do this? - if py3: - im = str(im.toUtf8(), "utf-8") - else: - im = unicode(im.toUtf8(), "utf-8") # noqa: F821 + im = str(im.toUtf8(), "utf-8") if isPath(im): im = Image.open(im) @@ -156,7 +137,7 @@ def _toqclass_helper(im): colortable = [] palette = im.getpalette() for i in range(0, len(palette), 3): - colortable.append(rgb(*palette[i:i+3])) + colortable.append(rgb(*palette[i : i + 3])) elif im.mode == "RGB": data = im.tobytes("raw", "BGRX") format = QImage.Format_RGB32 @@ -172,14 +153,12 @@ def _toqclass_helper(im): raise ValueError("unsupported image mode %r" % im.mode) __data = data or align8to32(im.tobytes(), im.size[0], im.mode) - return { - 'data': __data, 'im': im, 'format': format, 'colortable': colortable - } + return {"data": __data, "im": im, "format": format, "colortable": colortable} if qt_is_installed: - class ImageQt(QImage): + class ImageQt(QImage): def __init__(self, im): """ An PIL image wrapper for Qt. This is a subclass of PyQt's QImage @@ -193,12 +172,15 @@ if qt_is_installed: # All QImage constructors that take data operate on an existing # 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, - self.__data, im_data['im'].size[0], - im_data['im'].size[1], im_data['format']) - if im_data['colortable']: - self.setColorTable(im_data['colortable']) + self.__data = im_data["data"] + super().__init__( + self.__data, + im_data["im"].size[0], + im_data["im"].size[1], + im_data["format"], + ) + if im_data["colortable"]: + self.setColorTable(im_data["colortable"]) def toqimage(im): @@ -211,8 +193,8 @@ def toqpixmap(im): # result = QPixmap(im_data['im'].size[0], im_data['im'].size[1]) # result.loadFromData(im_data['data']) # Fix some strange bug that causes - if im.mode == 'RGB': - im = im.convert('RGBA') + if im.mode == "RGB": + im = im.convert("RGBA") qimage = toqimage(im) return QPixmap.fromImage(qimage) diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 1fc6e5de1..4e9f5c210 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -16,7 +16,7 @@ ## -class Iterator(object): +class Iterator: """ This class implements an iterator object that can be used to loop over an image sequence. @@ -32,7 +32,7 @@ class Iterator(object): if not hasattr(im, "seek"): raise AttributeError("im must have seek method") self.im = im - self.position = 0 + self.position = getattr(self.im, "_min_frame", 0) def __getitem__(self, ix): try: @@ -52,5 +52,24 @@ class Iterator(object): except EOFError: raise StopIteration - def next(self): - return self.__next__() + +def all_frames(im, func=None): + """ + Applies a given function to all frames in an image or a list of images. + The frames are returned as a list of separate images. + + :param im: An image, or a list of images. + :param func: The function to apply to all of the image frames. + :returns: A list of images. + """ + if not isinstance(im, list): + im = [im] + + ims = [] + for imSequence in im: + current = imSequence.tell() + + ims += [im_frame.copy() for im_frame in Iterator(imSequence)] + + imSequence.seek(current) + return [func(im) for im in ims] if func else ims diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index bbd841db7..f7e809279 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -11,19 +11,14 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function +import os +import shutil +import subprocess +import sys +import tempfile +from shlex import quote from PIL import Image -import os -import sys -import subprocess -import tempfile - -if sys.version_info.major >= 3: - from shlex import quote -else: - from pipes import quote _viewers = [] @@ -55,7 +50,7 @@ def show(image, title=None, **options): return 0 -class Viewer(object): +class Viewer: """Base class for viewers.""" # main api @@ -63,16 +58,12 @@ class Viewer(object): def show(self, image, **options): # save temporary image to disk - if image.mode[:4] == "I;16": - # @PIL88 @PIL101 - # "I;16" isn't an 'official' mode, but we still want to - # provide a simple way to show 16-bit images. - base = "L" - # FIXME: auto-contrast if max() > 255? - else: + if not ( + image.mode in ("1", "RGBA") or (self.format == "PNG" and image.mode == "LA") + ): base = Image.getmodebase(image.mode) - if base != image.mode and image.mode != "1" and image.mode != "RGBA": - image = image.convert(base) + if image.mode != base: + image = image.convert(base) return self.show_image(image, **options) @@ -101,18 +92,22 @@ class Viewer(object): os.system(self.get_command(file, **options)) return 1 + # -------------------------------------------------------------------- if sys.platform == "win32": class WindowsViewer(Viewer): - format = "BMP" + format = "PNG" + options = {"compress_level": 1} def get_command(self, file, **options): - return ('start "Pillow" /WAIT "%s" ' - '&& ping -n 2 127.0.0.1 >NUL ' - '&& del /f "%s"' % (file, file)) + return ( + 'start "Pillow" /WAIT "%s" ' + "&& ping -n 2 127.0.0.1 >NUL " + '&& del /f "%s"' % (file, file) + ) register(WindowsViewer) @@ -120,28 +115,28 @@ elif sys.platform == "darwin": class MacViewer(Viewer): format = "PNG" - options = {'compress_level': 1} + options = {"compress_level": 1} def get_command(self, file, **options): # on darwin open returns immediately resulting in the temp # file removal while app is opening - command = "open -a /Applications/Preview.app" - command = "(%s %s; sleep 20; rm -f %s)&" % (command, quote(file), - quote(file)) + command = "open -a Preview.app" + command = "({} {}; sleep 20; rm -f {})&".format( + command, quote(file), quote(file) + ) return command def show_file(self, file, **options): """Display given file""" fd, path = tempfile.mkstemp() - with os.fdopen(fd, 'w') as f: + with os.fdopen(fd, "w") as f: f.write(file) with open(path, "r") as f: - subprocess.Popen([ - 'im=$(cat);' - 'open -a /Applications/Preview.app $im;' - 'sleep 20;' - 'rm -f $im' - ], shell=True, stdin=f) + subprocess.Popen( + ["im=$(cat); open -a Preview.app $im; sleep 20; rm -f $im"], + shell=True, + stdin=f, + ) os.remove(path) return 1 @@ -151,36 +146,24 @@ 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} + options = {"compress_level": 1} def get_command(self, file, **options): command = self.get_command_ex(file, **options)[0] - return "(%s %s; rm -f %s)&" % (command, quote(file), quote(file)) + return "({} {}; rm -f {})&".format(command, quote(file), quote(file)) def show_file(self, file, **options): """Display given file""" fd, path = tempfile.mkstemp() - with os.fdopen(fd, 'w') as f: + with os.fdopen(fd, "w") as f: f.write(file) with open(path, "r") as f: command = self.get_command_ex(file, **options)[0] - subprocess.Popen([ - 'im=$(cat);' + - command+' $im;' - 'rm -f $im' - ], shell=True, stdin=f) + subprocess.Popen( + ["im=$(cat);" + command + " $im; rm -f $im"], shell=True, stdin=f + ) os.remove(path) return 1 @@ -191,7 +174,7 @@ else: command = executable = "display" return command, executable - if which("display"): + if shutil.which("display"): register(DisplayViewer) class EogViewer(UnixViewer): @@ -199,7 +182,7 @@ else: command = executable = "eog" return command, executable - if which("eog"): + if shutil.which("eog"): register(EogViewer) class XVViewer(UnixViewer): @@ -211,7 +194,7 @@ else: command += " -name %s" % quote(title) return command, executable - if which("xv"): + if shutil.which("xv"): register(XVViewer) if __name__ == "__main__": diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index c926a7416..50bafc972 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -21,13 +21,12 @@ # See the README file for information on usage and redistribution. # +import functools import math import operator -import functools -class Stat(object): - +class Stat: def __init__(self, image_or_list, mask=None): try: if mask: @@ -71,7 +70,7 @@ class Stat(object): v = [] for i in range(0, len(self.h), 256): - v.append(functools.reduce(operator.add, self.h[i:i+256])) + v.append(functools.reduce(operator.add, self.h[i : i + 256])) return v def _getsum(self): @@ -110,10 +109,10 @@ class Stat(object): v = [] for i in self.bands: s = 0 - half = self.count[i]//2 + half = self.count[i] // 2 b = i * 256 for j in range(256): - s = s + self.h[b+j] + s = s + self.h[b + j] if s > half: break v.append(j) @@ -133,7 +132,7 @@ class Stat(object): v = [] for i in self.bands: n = self.count[i] - v.append((self.sum2[i]-(self.sum[i]**2.0)/n)/n) + v.append((self.sum2[i] - (self.sum[i] ** 2.0) / n) / n) return v def _getstddev(self): diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index cc0c5294c..ee707cffb 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -25,17 +25,11 @@ # See the README file for information on usage and redistribution. # -import sys +import tkinter from io import BytesIO from . import Image -if sys.version_info.major > 2: - import tkinter -else: - import Tkinter as tkinter - - # -------------------------------------------------------------------- # Check for Tkinter interface hooks @@ -67,7 +61,8 @@ 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 @@ -183,17 +178,18 @@ class PhotoImage(object): # activate Tkinter hook try: from . import _imagingtk + try: - if hasattr(tk, 'interp'): + if hasattr(tk, "interp"): # Required for PyPy, which always has CFFI installed from cffi import FFI + ffi = FFI() # PyPy is using an FFI CDATA element # (Pdb) self.tk.interp # - _imagingtk.tkinit( - int(ffi.cast("uintptr_t", tk.interp)), 1) + _imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1) else: _imagingtk.tkinit(tk.interpaddr(), 1) except AttributeError: @@ -202,11 +198,12 @@ class PhotoImage(object): except (ImportError, AttributeError, tkinter.TclError): raise # configuration problem; cannot attach to Tkinter + # -------------------------------------------------------------------- # BitmapImage -class BitmapImage(object): +class BitmapImage: """ A Tkinter-compatible bitmap image. This can be used everywhere Tkinter expects an image object. @@ -275,10 +272,13 @@ class BitmapImage(object): def getimage(photo): - """ This function is unimplemented """ - """Copies the contents of a PhotoImage to a PIL image memory.""" - photo.tk.call("PyImagingPhotoGet", photo) + im = Image.new("RGBA", (photo.width(), photo.height())) + block = im.im + + photo.tk.call("PyImagingPhotoGet", photo, block.id) + + return im def _show(image, title): @@ -290,11 +290,10 @@ def _show(image, title): self.image = BitmapImage(im, foreground="white", master=master) else: self.image = PhotoImage(im, master=master) - tkinter.Label.__init__(self, master, image=self.image, - bg="black", bd=0) + super().__init__(master, image=self.image, bg="black", bd=0) if not tkinter._default_root: - raise IOError("tkinter not initialized") + raise OSError("tkinter not initialized") top = tkinter.Toplevel() if title: top.title(title) diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index c3f6af8b5..77791ab72 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -46,6 +46,7 @@ class AffineTransform(Transform): :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows from an affine transform matrix. """ + method = Image.AFFINE @@ -67,6 +68,7 @@ class ExtentTransform(Transform): :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the input image's coordinate system. See :ref:`coordinate-system`. """ + method = Image.EXTENT @@ -83,6 +85,7 @@ class QuadTransform(Transform): upper left, lower left, lower right, and upper right corner of the source quadrilateral. """ + method = Image.QUAD @@ -95,4 +98,5 @@ class MeshTransform(Transform): :param data: A list of (bbox, quad) tuples. """ + method = Image.MESH diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 9b86270bc..927b1694b 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -20,12 +20,13 @@ 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` methods. """ + def __init__(self, dc): self.dc = dc @@ -33,12 +34,13 @@ 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` methods, instead of a DC. """ + def __init__(self, wnd): self.wnd = wnd @@ -46,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". @@ -184,13 +186,13 @@ 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): self.hwnd = Image.core.createwindow( title, self.__dispatcher, width or 0, height or 0 - ) + ) def __dispatcher(self, action, *args): return getattr(self, "ui_handle_" + action)(*args) @@ -222,7 +224,7 @@ class ImageWindow(Window): image = Dib(image) self.image = image width, height = image.size - Window.__init__(self, title, width=width, height=height) + super().__init__(title, width=width, height=height) def ui_handle_repair(self, dc, x0, y0, x1, y1): self.image.draw(dc, (x0, y0, x1, y1)) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 18b7dd839..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" - - # # -------------------------------------------------------------------- @@ -33,6 +28,7 @@ field = re.compile(br"([a-z]*) ([^ \r\n]*)") ## # Image plugin for IM Tools images. + class ImtImageFile(ImageFile.ImageFile): format = "IMT" @@ -55,12 +51,12 @@ class ImtImageFile(ImageFile.ImageFile): if not s: break - if s == b'\x0C': + if s == b"\x0C": # image data begins - self.tile = [("raw", (0, 0)+self.size, - self.fp.tell(), - (self.mode, 0, 1))] + self.tile = [ + ("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1)) + ] break diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 5055d2a00..86a7ee8cb 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -14,22 +14,13 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - -from . import Image, ImageFile -from ._binary import i8, i16be as i16, i32be as i32, o8 import os import tempfile -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.3" +from . import Image, ImageFile +from ._binary import i8, i16be as i16, i32be as i32, o8 -COMPRESSION = { - 1: "raw", - 5: "jpeg" -} +COMPRESSION = {1: "raw", 5: "jpeg"} PAD = o8(0) * 4 @@ -37,13 +28,14 @@ PAD = o8(0) * 4 # # Helpers + def i(c): return i32((PAD + c)[-4:]) def dump(c): for i in c: - print("%02x" % i8(i), end=' ') + print("%02x" % i8(i), end=" ") print() @@ -51,6 +43,7 @@ def dump(c): # Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields # from TIFF and JPEG files, use the getiptcinfo function. + class IptcImageFile(ImageFile.ImageFile): format = "IPTC" @@ -75,11 +68,11 @@ 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: - size = i(self.fp.read(size-128)) + size = i(self.fp.read(size - 128)) else: size = i16(s[3:]) @@ -109,7 +102,7 @@ class IptcImageFile(ImageFile.ImageFile): layers = i8(self.info[(3, 60)][0]) component = i8(self.info[(3, 60)][1]) if (3, 65) in self.info: - id = i8(self.info[(3, 65)][0])-1 + id = i8(self.info[(3, 65)][0]) - 1 else: id = 0 if layers == 1 and not component: @@ -126,12 +119,13 @@ 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): - self.tile = [("iptc", (compression, offset), - (0, 0, self.size[0], self.size[1]))] + self.tile = [ + ("iptc", (compression, offset), (0, 0, self.size[0], self.size[1])) + ] def load(self): @@ -198,35 +192,9 @@ def getiptcinfo(im): elif isinstance(im, JpegImagePlugin.JpegImageFile): # extract the IPTC/NAA resource - try: - app = im.app["APP13"] - if app[:14] == b"Photoshop 3.0\x00": - app = app[14:] - # parse the image resource block - offset = 0 - while app[offset:offset+4] == b"8BIM": - offset += 4 - # resource code - code = i16(app, offset) - offset += 2 - # resource name (usually empty) - name_len = i8(app[offset]) - # name = app[offset+1:offset+1+name_len] - offset = 1 + offset + name_len - if offset & 1: - offset += 1 - # resource data block - size = i32(app, offset) - offset += 4 - if code == 0x0404: - # 0x0404 contains IPTC/NAA data - data = app[offset:offset+size] - break - offset = offset + size - if offset & 1: - offset += 1 - except (AttributeError, KeyError): - pass + photoshop = im.info.get("photoshop") + if photoshop: + data = photoshop.get(0x0404) elif isinstance(im, TiffImagePlugin.TiffImageFile): # get raw data from the IPTC/NAA tag (PhotoShop tags the data @@ -240,8 +208,9 @@ def getiptcinfo(im): return None # no properties # create an IptcImagePlugin object without initializing it - class FakeImage(object): + class FakeImage: pass + im = FakeImage() im.__class__ = IptcImageFile diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index ab01845c7..2c51d3678 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -12,14 +12,11 @@ # # See the README file for information on usage and redistribution. # -from . import Image, ImageFile -import struct -import os import io +import os +import struct -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" +from . import Image, ImageFile def _parse_codestream(fp): @@ -27,30 +24,29 @@ def _parse_codestream(fp): count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" hdr = fp.read(2) - lsiz = struct.unpack('>H', hdr)[0] + lsiz = struct.unpack(">H", hdr)[0] siz = hdr + fp.read(lsiz - 2) - lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, xtsiz, ytsiz, \ - xtosiz, ytosiz, csiz \ - = struct.unpack_from('>HHIIIIIIIIH', siz) - ssiz = [None]*csiz - xrsiz = [None]*csiz - yrsiz = [None]*csiz + lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( + ">HHIIIIIIIIH", siz + ) + ssiz = [None] * csiz + xrsiz = [None] * csiz + yrsiz = [None] * csiz for i in range(csiz): - ssiz[i], xrsiz[i], yrsiz[i] \ - = struct.unpack_from('>BBB', siz, 36 + 3 * i) + ssiz[i], xrsiz[i], yrsiz[i] = struct.unpack_from(">BBB", siz, 36 + 3 * i) size = (xsiz - xosiz, ysiz - yosiz) if csiz == 1: - if (yrsiz[0] & 0x7f) > 8: - mode = 'I;16' + if (yrsiz[0] & 0x7F) > 8: + mode = "I;16" else: - mode = 'L' + mode = "L" elif csiz == 2: - mode = 'LA' + mode = "LA" elif csiz == 3: - mode = 'RGB' + mode = "RGB" elif csiz == 4: - mode = 'RGBA' + mode = "RGBA" else: mode = None @@ -65,28 +61,28 @@ def _parse_jp2_header(fp): header = None mimetype = None while True: - lbox, tbox = struct.unpack('>I4s', fp.read(8)) + lbox, tbox = struct.unpack(">I4s", fp.read(8)) if lbox == 1: - lbox = struct.unpack('>Q', fp.read(8))[0] + lbox = struct.unpack(">Q", fp.read(8))[0] hlen = 16 else: hlen = 8 if lbox < hlen: - raise SyntaxError('Invalid JP2 header length') + raise SyntaxError("Invalid JP2 header length") - if tbox == b'jp2h': + if tbox == b"jp2h": header = fp.read(lbox - hlen) break - elif tbox == b'ftyp': - if fp.read(4) == b'jpx ': - mimetype = 'image/jpx' + elif tbox == b"ftyp": + if fp.read(4) == b"jpx ": + mimetype = "image/jpx" fp.seek(lbox - hlen - 4, os.SEEK_CUR) else: fp.seek(lbox - hlen, os.SEEK_CUR) if header is None: - raise SyntaxError('could not find JP2 header') + raise SyntaxError("could not find JP2 header") size = None mode = None @@ -95,58 +91,57 @@ def _parse_jp2_header(fp): hio = io.BytesIO(header) while True: - lbox, tbox = struct.unpack('>I4s', hio.read(8)) + lbox, tbox = struct.unpack(">I4s", hio.read(8)) if lbox == 1: - lbox = struct.unpack('>Q', hio.read(8))[0] + lbox = struct.unpack(">Q", hio.read(8))[0] hlen = 16 else: hlen = 8 content = hio.read(lbox - hlen) - if tbox == b'ihdr': - height, width, nc, bpc, c, unkc, ipr \ - = struct.unpack('>IIHBBBB', content) + if tbox == b"ihdr": + height, width, nc, bpc, c, unkc, ipr = struct.unpack(">IIHBBBB", content) size = (width, height) if unkc: - if nc == 1 and (bpc & 0x7f) > 8: - mode = 'I;16' + if nc == 1 and (bpc & 0x7F) > 8: + mode = "I;16" elif nc == 1: - mode = 'L' + mode = "L" elif nc == 2: - mode = 'LA' + mode = "LA" elif nc == 3: - mode = 'RGB' + mode = "RGB" elif nc == 4: - mode = 'RGBA' + mode = "RGBA" break - elif tbox == b'colr': - meth, prec, approx = struct.unpack_from('>BBB', content) + elif tbox == b"colr": + meth, prec, approx = struct.unpack_from(">BBB", content) if meth == 1: - cs = struct.unpack_from('>I', content, 3)[0] - if cs == 16: # sRGB - if nc == 1 and (bpc & 0x7f) > 8: - mode = 'I;16' + cs = struct.unpack_from(">I", content, 3)[0] + if cs == 16: # sRGB + if nc == 1 and (bpc & 0x7F) > 8: + mode = "I;16" elif nc == 1: - mode = 'L' + mode = "L" elif nc == 3: - mode = 'RGB' + mode = "RGB" elif nc == 4: - mode = 'RGBA' + mode = "RGBA" break elif cs == 17: # grayscale - if nc == 1 and (bpc & 0x7f) > 8: - mode = 'I;16' + if nc == 1 and (bpc & 0x7F) > 8: + mode = "I;16" elif nc == 1: - mode = 'L' + mode = "L" elif nc == 2: - mode = 'LA' + mode = "LA" break elif cs == 18: # sYCC if nc == 3: - mode = 'RGB' + mode = "RGB" elif nc == 4: - mode = 'RGBA' + mode = "RGBA" break if size is None or mode is None: @@ -154,6 +149,7 @@ def _parse_jp2_header(fp): return (size, mode, mimetype) + ## # Image plugin for JPEG2000 images. @@ -164,21 +160,21 @@ class Jpeg2KImageFile(ImageFile.ImageFile): def _open(self): sig = self.fp.read(4) - if sig == b'\xff\x4f\xff\x51': + if sig == b"\xff\x4f\xff\x51": self.codec = "j2k" self._size, self.mode = _parse_codestream(self.fp) else: sig = sig + self.fp.read(8) - if sig == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a': + if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a": self.codec = "jp2" header = _parse_jp2_header(self.fp) self._size, self.mode, self.custom_mimetype = header else: - raise SyntaxError('not a JPEG 2000 file') + raise SyntaxError("not a JPEG 2000 file") if self.size is None or self.mode is None: - raise SyntaxError('unable to determine size/mode') + raise SyntaxError("unable to determine size/mode") self.reduce = 0 self.layers = 0 @@ -193,21 +189,29 @@ class Jpeg2KImageFile(ImageFile.ImageFile): fd = -1 try: pos = self.fp.tell() - self.fp.seek(0, 2) + self.fp.seek(0, io.SEEK_END) length = self.fp.tell() - self.fp.seek(pos, 0) + self.fp.seek(pos) except Exception: length = -1 - self.tile = [('jpeg2k', (0, 0) + self.size, 0, - (self.codec, self.reduce, self.layers, fd, length))] + self.tile = [ + ( + "jpeg2k", + (0, 0) + self.size, + 0, + (self.codec, self.reduce, self.layers, fd, length), + ) + ] def load(self): if self.reduce: power = 1 << self.reduce adjust = power >> 1 - self._size = (int((self.size[0] + adjust) / power), - int((self.size[1] + adjust) / power)) + self._size = ( + int((self.size[0] + adjust) / power), + int((self.size[1] + adjust) / power), + ) if self.tile: # Update the reduce and layers settings @@ -219,40 +223,47 @@ class Jpeg2KImageFile(ImageFile.ImageFile): def _accept(prefix): - return (prefix[:4] == b'\xff\x4f\xff\x51' or - prefix[:12] == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a') + return ( + prefix[:4] == b"\xff\x4f\xff\x51" + or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" + ) # ------------------------------------------------------------ # Save support + def _save(im, fp, filename): - if filename.endswith('.j2k'): - kind = 'j2k' + if filename.endswith(".j2k"): + kind = "j2k" else: - kind = 'jp2' + kind = "jp2" # Get the keyword arguments info = im.encoderinfo - offset = info.get('offset', None) - tile_offset = info.get('tile_offset', None) - tile_size = info.get('tile_size', None) - quality_mode = info.get('quality_mode', 'rates') - quality_layers = info.get('quality_layers', None) + offset = info.get("offset", None) + tile_offset = info.get("tile_offset", None) + tile_size = info.get("tile_size", None) + quality_mode = info.get("quality_mode", "rates") + quality_layers = info.get("quality_layers", None) if quality_layers is not None and not ( - isinstance(quality_layers, (list, tuple)) and - all([isinstance(quality_layer, (int, float)) - for quality_layer in quality_layers]) + isinstance(quality_layers, (list, tuple)) + and all( + [ + isinstance(quality_layer, (int, float)) + for quality_layer in quality_layers + ] + ) ): - raise ValueError('quality_layers must be a sequence of numbers') + raise ValueError("quality_layers must be a sequence of numbers") - num_resolutions = info.get('num_resolutions', 0) - cblk_size = info.get('codeblock_size', None) - precinct_size = info.get('precinct_size', None) - irreversible = info.get('irreversible', False) - progression = info.get('progression', 'LRCP') - cinema_mode = info.get('cinema_mode', 'no') + num_resolutions = info.get("num_resolutions", 0) + cblk_size = info.get("codeblock_size", None) + precinct_size = info.get("precinct_size", None) + irreversible = info.get("irreversible", False) + progression = info.get("progression", "LRCP") + cinema_mode = info.get("cinema_mode", "no") fd = -1 if hasattr(fp, "fileno"): @@ -273,10 +284,11 @@ def _save(im, fp, filename): irreversible, progression, cinema_mode, - fd + fd, ) - ImageFile._save(im, fp, [('jpeg2k', (0, 0)+im.size, 0, kind)]) + ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) + # ------------------------------------------------------------ # Registry stuff @@ -285,7 +297,8 @@ def _save(im, fp, filename): Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept) Image.register_save(Jpeg2KImageFile.format, _save) -Image.register_extensions(Jpeg2KImageFile.format, - [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"]) +Image.register_extensions( + Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"] +) -Image.register_mime(Jpeg2KImageFile.format, 'image/jp2') +Image.register_mime(Jpeg2KImageFile.format, "image/jp2") diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 2f76e9675..ea2f795a7 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -31,28 +31,24 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - import array -import struct import io +import os +import struct +import subprocess +import tempfile import warnings + from . import Image, ImageFile, TiffImagePlugin -from ._binary import i8, o8, i16be as i16 +from ._binary import i8, i16be as i16, i32be as i32, o8 from .JpegPresets import presets -from ._util import isStringType - -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.6" - # # Parser + def Skip(self, marker): - n = i16(self.fp.read(2))-2 + n = i16(self.fp.read(2)) - 2 ImageFile._safe_read(self.fp, n) @@ -61,7 +57,7 @@ def APP(self, marker): # Application marker. Store these in the APP dictionary. # Also look for well-known application markers. - n = i16(self.fp.read(2))-2 + n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) app = "APP%d" % (marker & 15) @@ -86,7 +82,7 @@ def APP(self, marker): self.info["jfif_density"] = jfif_density elif marker == 0xFFE1 and s[:5] == b"Exif\0": if "exif" not in self.info: - # extract Exif information (incomplete) + # extract EXIF information (incomplete) self.info["exif"] = s # FIXME: value will change elif marker == 0xFFE2 and s[:5] == b"FPXR\0": # extract FlashPix information (incomplete) @@ -104,6 +100,39 @@ def APP(self, marker): # reassemble the profile, rather than assuming that the APP2 # markers appear in the correct sequence. self.icclist.append(s) + elif marker == 0xFFED: + if s[:14] == b"Photoshop 3.0\x00": + blocks = s[14:] + # parse the image resource block + offset = 0 + photoshop = {} + while blocks[offset : offset + 4] == b"8BIM": + offset += 4 + # resource code + code = i16(blocks, offset) + offset += 2 + # resource name (usually empty) + name_len = i8(blocks[offset]) + # name = blocks[offset+1:offset+1+name_len] + offset = 1 + offset + name_len + if offset & 1: + offset += 1 + # resource data block + size = i32(blocks, offset) + offset += 4 + data = blocks[offset : offset + size] + if code == 0x03ED: # ResolutionInfo + data = { + "XResolution": i32(data[:4]) / 65536, + "DisplayedUnitsX": i16(data[4:8]), + "YResolution": i32(data[8:12]) / 65536, + "DisplayedUnitsY": i16(data[12:]), + } + photoshop[code] = data + offset = offset + size + if offset & 1: + offset += 1 + self.info["photoshop"] = photoshop elif marker == 0xFFEE and s[:5] == b"Adobe": self.info["adobe"] = i16(s, 5) # extract Adobe custom properties @@ -123,19 +152,19 @@ def APP(self, marker): # If DPI isn't in JPEG header, fetch from EXIF if "dpi" not in self.info and "exif" in self.info: try: - exif = self._getexif() + exif = self.getexif() resolution_unit = exif[0x0128] x_resolution = exif[0x011A] try: - dpi = x_resolution[0] / x_resolution[1] + dpi = float(x_resolution[0]) / x_resolution[1] except TypeError: dpi = x_resolution if resolution_unit == 3: # cm # 1 dpcm = 2.54 dpi dpi *= 2.54 - self.info["dpi"] = dpi, dpi + self.info["dpi"] = int(dpi + 0.5), int(dpi + 0.5) except (KeyError, SyntaxError, ZeroDivisionError): - # SyntaxError for invalid/unreadable exif + # SyntaxError for invalid/unreadable EXIF # KeyError for dpi not included # ZeroDivisionError for invalid dpi rational value self.info["dpi"] = 72, 72 @@ -144,7 +173,7 @@ def APP(self, marker): def COM(self, marker): # # Comment marker. Store these in the APP dictionary. - n = i16(self.fp.read(2))-2 + n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) self.app["COM"] = s # compatibility @@ -159,7 +188,7 @@ def SOF(self, marker): # mode. Note that this could be made a bit brighter, by # looking for JFIF and Adobe APP markers. - n = i16(self.fp.read(2))-2 + n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) self._size = i16(s[3:]), i16(s[1:]) @@ -194,9 +223,9 @@ def SOF(self, marker): self.icclist = None for i in range(6, len(s), 3): - t = s[i:i+3] + t = s[i : i + 3] # 4-tuples: id, vsamp, hsamp, qtable - self.layer.append((t[0], i8(t[1])//16, i8(t[1]) & 15, i8(t[2]))) + self.layer.append((t[0], i8(t[1]) // 16, i8(t[1]) & 15, i8(t[2]))) def DQT(self, marker): @@ -208,13 +237,13 @@ def DQT(self, marker): # FIXME: The quantization tables can be used to estimate the # compression quality. - n = i16(self.fp.read(2))-2 + n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) while len(s): if len(s) < 65: raise SyntaxError("bad quantization table marker") v = i8(s[0]) - if v//16 == 0: + if v // 16 == 0: self.quantization[v & 15] = array.array("B", s[1:65]) s = s[65:] else: @@ -288,7 +317,7 @@ MARKER = { 0xFFFB: ("JPG11", "Extension 11", None), 0xFFFC: ("JPG12", "Extension 12", None), 0xFFFD: ("JPG13", "Extension 13", None), - 0xFFFE: ("COM", "Comment", COM) + 0xFFFE: ("COM", "Comment", COM), } @@ -299,6 +328,7 @@ def _accept(prefix): ## # Image plugin for JPEG and JFIF images. + class JpegImageFile(ImageFile.ImageFile): format = "JPEG" @@ -342,8 +372,7 @@ class JpegImageFile(ImageFile.ImageFile): rawmode = self.mode if self.mode == "CMYK": rawmode = "CMYK;I" # assume adobe conventions - self.tile = [("jpeg", (0, 0) + self.size, 0, - (rawmode, ""))] + self.tile = [("jpeg", (0, 0) + self.size, 0, (rawmode, ""))] # self.__offset = self.fp.tell() break s = self.fp.read(1) @@ -391,8 +420,13 @@ class JpegImageFile(ImageFile.ImageFile): for s in [8, 4, 2, 1]: if scale >= s: break - e = e[0], e[1], (e[2]-e[0]+s-1)//s+e[0], (e[3]-e[1]+s-1)//s+e[1] - self._size = ((self.size[0]+s-1)//s, (self.size[1]+s-1)//s) + e = ( + e[0], + e[1], + (e[2] - e[0] + s - 1) // s + e[0], + (e[3] - e[1] + s - 1) // s + e[1], + ) + self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s) scale = s self.tile = [(d, e, o, a)] @@ -404,9 +438,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): @@ -439,61 +470,14 @@ class JpegImageFile(ImageFile.ImageFile): def _fixup_dict(src_dict): # Helper function for _getexif() # returns a dict with any single item tuples/lists as individual values - def _fixup(value): - try: - if len(value) == 1 and not isinstance(value, dict): - return value[0] - except Exception: - pass - return value - - return {k: _fixup(v) for k, v in src_dict.items()} + exif = Image.Exif() + return exif._fixup_dict(src_dict) def _getexif(self): - # Extract EXIF information. This method is highly experimental, - # and is likely to be replaced with something better in a future - # version. - - # The EXIF record consists of a TIFF file embedded in a JPEG - # application marker (!). - try: - data = self.info["exif"] - except KeyError: + if "exif" not in self.info: return None - fp = io.BytesIO(data[6:]) - head = fp.read(8) - # process dictionary - info = TiffImagePlugin.ImageFileDirectory_v1(head) - fp.seek(info.next) - info.load(fp) - exif = dict(_fixup_dict(info)) - # get exif extension - try: - # exif field 0x8769 is an offset pointer to the location - # of the nested embedded exif ifd. - # It should be a long, but may be corrupted. - fp.seek(exif[0x8769]) - except (KeyError, TypeError): - pass - else: - info = TiffImagePlugin.ImageFileDirectory_v1(head) - info.load(fp) - exif.update(_fixup_dict(info)) - # get gpsinfo extension - try: - # exif field 0x8825 is an offset pointer to the location - # of the nested embedded gps exif ifd. - # It should be a long, but may be corrupted. - fp.seek(exif[0x8825]) - except (KeyError, TypeError): - pass - else: - info = TiffImagePlugin.ImageFileDirectory_v1(head) - info.load(fp) - exif[0x8825] = _fixup_dict(info) - - return exif + return dict(self.getexif()) def _getmp(self): @@ -509,7 +493,7 @@ def _getmp(self): return None file_contents = io.BytesIO(data) head = file_contents.read(8) - endianness = '>' if head[:4] == b'\x4d\x4d\x00\x2a' else '<' + endianness = ">" if head[:4] == b"\x4d\x4d\x00\x2a" else "<" # process dictionary try: info = TiffImagePlugin.ImageFileDirectory_v2(head) @@ -529,37 +513,33 @@ def _getmp(self): rawmpentries = mp[0xB002] for entrynum in range(0, quant): unpackedentry = struct.unpack_from( - '{}LLLHH'.format(endianness), rawmpentries, entrynum * 16) - labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1', - 'EntryNo2') + "{}LLLHH".format(endianness), rawmpentries, entrynum * 16 + ) + labels = ("Attribute", "Size", "DataOffset", "EntryNo1", "EntryNo2") mpentry = dict(zip(labels, unpackedentry)) mpentryattr = { - 'DependentParentImageFlag': bool(mpentry['Attribute'] & - (1 << 31)), - 'DependentChildImageFlag': bool(mpentry['Attribute'] & - (1 << 30)), - 'RepresentativeImageFlag': bool(mpentry['Attribute'] & - (1 << 29)), - 'Reserved': (mpentry['Attribute'] & (3 << 27)) >> 27, - 'ImageDataFormat': (mpentry['Attribute'] & (7 << 24)) >> 24, - 'MPType': mpentry['Attribute'] & 0x00FFFFFF + "DependentParentImageFlag": bool(mpentry["Attribute"] & (1 << 31)), + "DependentChildImageFlag": bool(mpentry["Attribute"] & (1 << 30)), + "RepresentativeImageFlag": bool(mpentry["Attribute"] & (1 << 29)), + "Reserved": (mpentry["Attribute"] & (3 << 27)) >> 27, + "ImageDataFormat": (mpentry["Attribute"] & (7 << 24)) >> 24, + "MPType": mpentry["Attribute"] & 0x00FFFFFF, } - if mpentryattr['ImageDataFormat'] == 0: - mpentryattr['ImageDataFormat'] = 'JPEG' + if mpentryattr["ImageDataFormat"] == 0: + mpentryattr["ImageDataFormat"] = "JPEG" else: raise SyntaxError("unsupported picture format in MPO") mptypemap = { - 0x000000: 'Undefined', - 0x010001: 'Large Thumbnail (VGA Equivalent)', - 0x010002: 'Large Thumbnail (Full HD Equivalent)', - 0x020001: 'Multi-Frame Image (Panorama)', - 0x020002: 'Multi-Frame Image: (Disparity)', - 0x020003: 'Multi-Frame Image: (Multi-Angle)', - 0x030000: 'Baseline MP Primary Image' + 0x000000: "Undefined", + 0x010001: "Large Thumbnail (VGA Equivalent)", + 0x010002: "Large Thumbnail (Full HD Equivalent)", + 0x020001: "Multi-Frame Image (Panorama)", + 0x020002: "Multi-Frame Image: (Disparity)", + 0x020003: "Multi-Frame Image: (Multi-Angle)", + 0x030000: "Baseline MP Primary Image", } - mpentryattr['MPType'] = mptypemap.get(mpentryattr['MPType'], - 'Unknown') - mpentry['Attribute'] = mpentryattr + mpentryattr["MPType"] = mptypemap.get(mpentryattr["MPType"], "Unknown") + mpentry["Attribute"] = mpentryattr mpentries.append(mpentry) mp[0xB002] = mpentries except KeyError: @@ -582,19 +562,24 @@ RAWMODE = { "YCbCr": "YCbCr", } -zigzag_index = (0, 1, 5, 6, 14, 15, 27, 28, # noqa: E128 - 2, 4, 7, 13, 16, 26, 29, 42, - 3, 8, 12, 17, 25, 30, 41, 43, - 9, 11, 18, 24, 31, 40, 44, 53, - 10, 19, 23, 32, 39, 45, 52, 54, - 20, 22, 33, 38, 46, 51, 55, 60, - 21, 34, 37, 47, 50, 56, 59, 61, - 35, 36, 48, 49, 57, 58, 62, 63) +# fmt: off +zigzag_index = ( + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63, +) -samplings = {(1, 1, 1, 1, 1, 1): 0, - (2, 1, 1, 1, 1, 1): 1, - (2, 2, 1, 1, 1, 1): 2, - } +samplings = { + (1, 1, 1, 1, 1, 1): 0, + (2, 1, 1, 1, 1, 1): 1, + (2, 2, 1, 1, 1, 1): 2, +} +# fmt: on def convert_dict_qtables(qtables): @@ -612,7 +597,7 @@ def get_sampling(im): # NOTE: currently Pillow can't encode JPEG to YCCK format. # If YCCK support is added in the future, subsampling code will have # to be updated (here and in JpegEncode.c) to deal with 4 layers. - if not hasattr(im, 'layers') or im.layers in (1, 4): + if not hasattr(im, "layers") or im.layers in (1, 4): return -1 sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3] return samplings.get(sampling, -1) @@ -623,7 +608,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 @@ -640,15 +625,15 @@ def _save(im, fp, filename): elif quality in presets: preset = presets[quality] quality = 0 - subsampling = preset.get('subsampling', -1) - qtables = preset.get('quantization') + subsampling = preset.get("subsampling", -1) + qtables = preset.get("quantization") elif not isinstance(quality, int): raise ValueError("Invalid quality setting") else: if subsampling in presets: - subsampling = presets[subsampling].get('subsampling', -1) - if isStringType(qtables) and qtables in presets: - qtables = presets[qtables].get('quantization') + subsampling = presets[subsampling].get("subsampling", -1) + if isinstance(qtables, str) and qtables in presets: + qtables = presets[qtables].get("quantization") if subsampling == "4:4:4": subsampling = 0 @@ -662,21 +647,23 @@ def _save(im, fp, filename): subsampling = 2 elif subsampling == "keep": if im.format != "JPEG": - raise ValueError( - "Cannot use 'keep' when original image is not a JPEG") + raise ValueError("Cannot use 'keep' when original image is not a JPEG") subsampling = get_sampling(im) def validate_qtables(qtables): if qtables is None: return qtables - if isStringType(qtables): + if isinstance(qtables, str): try: - lines = [int(num) for line in qtables.splitlines() - for num in line.split('#', 1)[0].split()] + lines = [ + int(num) + for line in qtables.splitlines() + for num in line.split("#", 1)[0].split() + ] except ValueError: raise ValueError("Invalid quantization table") else: - qtables = [lines[s:s+64] for s in range(0, len(lines), 64)] + qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] if isinstance(qtables, (tuple, list, dict)): if isinstance(qtables, dict): qtables = convert_dict_qtables(qtables) @@ -688,7 +675,7 @@ def _save(im, fp, filename): try: if len(table) != 64: raise TypeError - table = array.array('B', table) + table = array.array("B", table) except TypeError: raise ValueError("Invalid quantization table") else: @@ -697,8 +684,7 @@ def _save(im, fp, filename): if qtables == "keep": if im.format != "JPEG": - raise ValueError( - "Cannot use 'keep' when original image is not a JPEG") + raise ValueError("Cannot use 'keep' when original image is not a JPEG") qtables = getattr(im, "quantization", None) qtables = validate_qtables(qtables) @@ -716,18 +702,27 @@ def _save(im, fp, filename): i = 1 for marker in markers: size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker)) - extra += (b"\xFF\xE2" + size + b"ICC_PROFILE\0" + o8(i) + - o8(len(markers)) + marker) + extra += ( + b"\xFF\xE2" + + size + + b"ICC_PROFILE\0" + + o8(i) + + o8(len(markers)) + + marker + ) i += 1 # "progressive" is the official name, but older documentation # says "progression" # FIXME: issue a warning if the wrong form is used (post-1.1.7) - progressive = (info.get("progressive", False) or - info.get("progression", False)) + progressive = info.get("progressive", False) or info.get("progression", False) optimize = info.get("optimize", False) + exif = info.get("exif", b"") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + # get keyword arguments im.encoderconfig = ( quality, @@ -735,12 +730,13 @@ def _save(im, fp, filename): info.get("smooth", 0), optimize, info.get("streamtype", 0), - dpi[0], dpi[1], + dpi[0], + dpi[1], subsampling, qtables, extra, - info.get("exif", b"") - ) + exif, + ) # if we optimize, libjpeg needs a buffer big enough to hold the whole image # in a shot. Guessing on the size, at im.size bytes. (raw pixel size is @@ -749,7 +745,7 @@ def _save(im, fp, filename): bufsize = 0 if optimize or progressive: # CMYK can be bigger - if im.mode == 'CMYK': + if im.mode == "CMYK": bufsize = 4 * im.size[0] * im.size[1] # keep sets quality to 0, but the actual value may be high. elif quality >= 95 or quality == 0: @@ -757,18 +753,15 @@ def _save(im, fp, filename): else: bufsize = im.size[0] * im.size[1] - # The exif info needs to be written as one block, + APP1, + one spare byte. + # The EXIF info needs to be written as one block, + APP1, + one spare byte. # Ensure that our buffer is big enough. Same with the icc_profile block. - bufsize = max(ImageFile.MAXBLOCK, bufsize, len(info.get("exif", b"")) + 5, - len(extra) + 1) + bufsize = max(ImageFile.MAXBLOCK, bufsize, len(exif) + 5, len(extra) + 1) - ImageFile._save(im, fp, [("jpeg", (0, 0)+im.size, 0, rawmode)], bufsize) + ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) 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: @@ -786,13 +779,17 @@ def jpeg_factory(fp=None, filename=None): if mpheader[45057] > 1: # It's actually an MPO from .MpoImagePlugin import MpoImageFile - im = MpoImageFile(fp, filename) + + # Don't reload everything, just convert it. + im = MpoImageFile.adopt(im, mpheader) except (TypeError, IndexError): # It is really a JPEG pass except SyntaxError: - warnings.warn("Image appears to be a malformed MPO file, it will be " - "interpreted as a base JPEG file") + warnings.warn( + "Image appears to be a malformed MPO file, it will be " + "interpreted as a base JPEG file" + ) return im @@ -802,7 +799,6 @@ def jpeg_factory(fp=None, filename=None): Image.register_open(JpegImageFile.format, jpeg_factory, _accept) Image.register_save(JpegImageFile.format, _save) -Image.register_extensions(JpegImageFile.format, - [".jfif", ".jpe", ".jpg", ".jpeg"]) +Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"]) Image.register_mime(JpegImageFile.format, "image/jpeg") diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index f7a533c18..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 @@ -67,6 +70,7 @@ https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/li """ +# fmt: off presets = { # noqa: E128 'web_low': {'subsampling': 2, # "4:2:0" 'quantization': [ @@ -240,3 +244,4 @@ presets = { # noqa: E128 15, 12, 12, 12, 12, 12, 12, 12] ]}, } +# fmt: on diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 2cdb6f828..cd047fe9d 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -17,11 +17,8 @@ # import struct -from . import Image, ImageFile -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.2" +from . import Image, ImageFile def _accept(s): @@ -31,6 +28,7 @@ def _accept(s): ## # Image plugin for McIdas area images. + class McIdasImageFile(ImageFile.ImageFile): format = "MCIDAS" @@ -64,7 +62,7 @@ class McIdasImageFile(ImageFile.ImageFile): self._size = w[10], w[9] offset = w[34] + w[15] - stride = w[15] + w[10]*w[11]*w[14] + stride = w[15] + w[10] * w[11] * w[14] self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))] diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 8c7707dae..8610988fc 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -17,14 +17,9 @@ # -from . import Image, TiffImagePlugin - import olefile -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - +from . import Image, TiffImagePlugin # # -------------------------------------------------------------------- @@ -37,6 +32,7 @@ def _accept(prefix): ## # Image plugin for Microsoft's Image Composer file format. + class MicImageFile(TiffImagePlugin.TiffImageFile): format = "MIC" @@ -50,7 +46,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): try: self.ole = olefile.OleFileIO(self.fp) - except IOError: + except OSError: raise SyntaxError("not an MIC file; invalid OLE file") # find ACI subfiles with Image members (maybe not the diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 7f419c5dc..a358dfdce 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -17,16 +17,11 @@ from . import Image, ImageFile from ._binary import i8 -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - - # # Bitstream parser -class BitStream(object): +class BitStream: def __init__(self, fp): self.fp = fp self.bits = 0 @@ -61,6 +56,7 @@ class BitStream(object): # Image plugin for MPEG streams. This plugin can identify a stream, # but it cannot read it. + class MpegImageFile(ImageFile.ImageFile): format = "MPEG" diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 64c0f57a4..a63e76fe7 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -18,11 +18,8 @@ # See the README file for information on usage and redistribution. # -from . import Image, JpegImagePlugin - -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" +from . import Image, ImageFile, JpegImagePlugin +from ._binary import i16be as i16 def _accept(prefix): @@ -37,6 +34,7 @@ def _save(im, fp, filename): ## # Image plugin for MPO images. + class MpoImageFile(JpegImagePlugin.JpegImageFile): format = "MPO" @@ -46,15 +44,19 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): def _open(self): self.fp.seek(0) # prep the fp in order to pass the JPEG test JpegImagePlugin.JpegImageFile._open(self) - self.mpinfo = self._getmp() + self._after_jpeg_open() + + def _after_jpeg_open(self, mpheader=None): + self.mpinfo = mpheader if mpheader is not None else self._getmp() self.__framecount = self.mpinfo[0xB001] - self.__mpoffsets = [mpent['DataOffset'] + self.info['mpoffset'] - for mpent in self.mpinfo[0xB002]] + self.__mpoffsets = [ + mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002] + ] self.__mpoffsets[0] = 0 # Note that the following assertion will only be invalid if something # gets broken within JpegImagePlugin. assert self.__framecount == len(self.__mpoffsets) - del self.info['mpoffset'] # no longer needed + del self.info["mpoffset"] # no longer needed self.__fp = self.fp # FIXME: hack self.__fp.seek(self.__mpoffsets[0]) # get ready to read first frame self.__frame = 0 @@ -78,9 +80,19 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): return self.fp = self.__fp self.offset = self.__mpoffsets[frame] - self.tile = [ - ("jpeg", (0, 0) + self.size, self.offset, (self.mode, "")) - ] + + self.fp.seek(self.offset + 2) # skip SOI marker + if i16(self.fp.read(2)) == 0xFFE1: # APP1 + n = i16(self.fp.read(2)) - 2 + self.info["exif"] = ImageFile._safe_read(self.fp, n) + + exif = self.getexif() + if 40962 in exif and 40963 in exif: + self._size = (exif[40962], exif[40963]) + elif "exif" in self.info: + del self.info["exif"] + + self.tile = [("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))] self.__frame = frame def tell(self): @@ -95,6 +107,22 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): finally: self.__fp = None + @staticmethod + def adopt(jpeg_instance, mpheader=None): + """ + Transform the instance of JpegImageFile into + an instance of MpoImageFile. + After the call, the JpegImageFile is extended + to be an MpoImageFile. + + This is essentially useful when opening a JPEG + file that reveals itself as an MPO, to avoid + double call to _open. + """ + jpeg_instance.__class__ = MpoImageFile + jpeg_instance._after_jpeg_open(mpheader) + return jpeg_instance + # --------------------------------------------------------------------- # Registry stuff diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 711f8f09a..2b2937ecf 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -23,15 +23,11 @@ # # See also: http://www.fileformat.info/format/mspaint/egff.htm -from . import Image, ImageFile -from ._binary import i16le as i16, o16le as o16, i8 -import struct import io +import struct -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.1" - +from . import Image, ImageFile +from ._binary import i8, i16le as i16, o16le as o16 # # read MSP files @@ -45,6 +41,7 @@ def _accept(prefix): # Image plugin for Windows MSP images. This plugin supports both # uncompressed (Windows 1.0). + class MspImageFile(ImageFile.ImageFile): format = "MSP" @@ -60,7 +57,7 @@ class MspImageFile(ImageFile.ImageFile): # Header checksum checksum = 0 for i in range(0, 32, 2): - checksum = checksum ^ i16(s[i:i+2]) + checksum = checksum ^ i16(s[i : i + 2]) if checksum != 0: raise SyntaxError("bad MSP checksum") @@ -68,9 +65,9 @@ class MspImageFile(ImageFile.ImageFile): self._size = i16(s[4:]), i16(s[6:]) if s[:4] == b"DanM": - self.tile = [("raw", (0, 0)+self.size, 32, ("1", 0, 1))] + self.tile = [("raw", (0, 0) + self.size, 32, ("1", 0, 1))] else: - self.tile = [("MSP", (0, 0)+self.size, 32, None)] + self.tile = [("MSP", (0, 0) + self.size, 32, None)] class MspDecoder(ImageFile.PyDecoder): @@ -113,13 +110,14 @@ class MspDecoder(ImageFile.PyDecoder): def decode(self, buffer): img = io.BytesIO() - blank_line = bytearray((0xff,)*((self.state.xsize+7)//8)) + blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) try: self.fd.seek(32) - rowmap = struct.unpack_from("<%dH" % (self.state.ysize), - self.fd.read(self.state.ysize*2)) + rowmap = struct.unpack_from( + "<%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: @@ -128,9 +126,9 @@ class MspDecoder(ImageFile.PyDecoder): continue row = self.fd.read(rowlen) if len(row) != rowlen: - raise IOError( - "Truncated MSP file, expected %d bytes on row %s", - (rowlen, x)) + raise OSError( + "Truncated MSP file, expected %d bytes on row %s", (rowlen, x) + ) idx = 0 while idx < rowlen: runtype = i8(row[idx]) @@ -141,18 +139,18 @@ class MspDecoder(ImageFile.PyDecoder): idx += 2 else: runcount = runtype - img.write(row[idx:idx+runcount]) + img.write(row[idx : idx + runcount]) 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)) return 0, 0 -Image.register_decoder('MSP', MspDecoder) +Image.register_decoder("MSP", MspDecoder) # @@ -162,7 +160,7 @@ Image.register_decoder('MSP', MspDecoder) def _save(im, fp, filename): if im.mode != "1": - raise IOError("cannot write mode %s as MSP" % im.mode) + raise OSError("cannot write mode %s as MSP" % im.mode) # create MSP header header = [0] * 16 @@ -183,7 +181,7 @@ def _save(im, fp, filename): fp.write(o16(h)) # image body - ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 32, ("1", 0, 1))]) + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) # diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index d2ded6fea..90bcad036 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -15,15 +15,15 @@ # See the README file for information on usage and redistribution. # -from . import EpsImagePlugin -from ._util import py3 import sys +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. @@ -35,19 +35,21 @@ 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')) + self.fp.write(bytes(to_write, "UTF-8")) def begin_document(self, id=None): """Set up printing of a document. (Write Postscript DSC header.)""" # FIXME: incomplete - self._fp_write("%!PS-Adobe-3.0\n" - "save\n" - "/showpage { } def\n" - "%%EndComments\n" - "%%BeginDocument\n") + self._fp_write( + "%!PS-Adobe-3.0\n" + "save\n" + "/showpage { } def\n" + "%%EndComments\n" + "%%BeginDocument\n" + ) # self._fp_write(ERROR_PS) # debugging! self._fp_write(EDROFF_PS) self._fp_write(VDI_PS) @@ -56,9 +58,7 @@ class PSDraw(object): def end_document(self): """Ends printing. (Write Postscript DSC footer.)""" - self._fp_write("%%EndDocument\n" - "restore showpage\n" - "%%End\n") + self._fp_write("%%EndDocument\nrestore showpage\n%%End\n") if hasattr(self.fp, "flush"): self.fp.flush() @@ -71,8 +71,7 @@ class PSDraw(object): """ if font not in self.isofont: # reencode font - self._fp_write("/PSDraw-%s ISOLatin1Encoding /%s E\n" % - (font, font)) + self._fp_write("/PSDraw-{} ISOLatin1Encoding /{} E\n".format(font, font)) self.isofont[font] = 1 # rough self._fp_write("/F0 %d /PSDraw-%s F\n" % (size, font)) @@ -133,15 +132,16 @@ 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") + # -------------------------------------------------------------------- # Postscript driver diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 9ed69d687..73f1b4b27 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -15,11 +15,11 @@ 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 096867207..804ece34a 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -10,10 +10,7 @@ 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), (255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204), @@ -79,6 +76,7 @@ _Palm8BitColormapValues = ( # noqa: E131 (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)) +# fmt: on # so build a prototype image to be used for palette resampling @@ -88,7 +86,7 @@ def build_prototype_image(): palettedata = () for colormapValue in _Palm8BitColormapValues: palettedata += colormapValue - palettedata += (0, 0, 0)*(256 - len(_Palm8BitColormapValues)) + palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues)) image.putpalette(palettedata) return image @@ -100,17 +98,9 @@ Palm8BitColormapImage = build_prototype_image() # # -------------------------------------------------------------------- -_FLAGS = { - "custom-colormap": 0x4000, - "is-compressed": 0x8000, - "has-transparent": 0x2000, - } +_FLAGS = {"custom-colormap": 0x4000, "is-compressed": 0x8000, "has-transparent": 0x2000} -_COMPRESSION_TYPES = { - "none": 0xFF, - "rle": 0x01, - "scanline": 0x00, - } +_COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} # @@ -119,6 +109,7 @@ _COMPRESSION_TYPES = { ## # (Internal) Image save plugin for the Palm format. + def _save(im, fp, filename): if im.mode == "P": @@ -130,28 +121,24 @@ def _save(im, fp, filename): bpp = 8 version = 1 - elif (im.mode == "L" and - "bpp" in im.encoderinfo and - im.encoderinfo["bpp"] in (1, 2, 4)): + elif im.mode == "L": + if im.encoderinfo.get("bpp") in (1, 2, 4): + # this is 8-bit grayscale, so we shift it to get the high-order bits, + # and invert it because + # Palm does greyscale from white (0) to black (1) + bpp = im.encoderinfo["bpp"] + im = im.point( + lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift) + ) + elif im.info.get("bpp") in (1, 2, 4): + # here we assume that even though the inherent mode is 8-bit grayscale, + # only the lower bpp bits are significant. + # We invert them to match the Palm. + bpp = im.info["bpp"] + im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval)) + else: + raise OSError("cannot write mode %s as Palm" % im.mode) - # this is 8-bit grayscale, so we shift it to get the high-order bits, - # and invert it because - # Palm does greyscale from white (0) to black (1) - bpp = im.encoderinfo["bpp"] - im = im.point( - lambda x, shift=8-bpp, maxval=(1 << bpp)-1: maxval - (x >> shift)) - # we ignore the palette here - im.mode = "P" - rawmode = "P;" + str(bpp) - version = 1 - - elif im.mode == "L" and "bpp" in im.info and im.info["bpp"] in (1, 2, 4): - - # here we assume that even though the inherent mode is 8-bit grayscale, - # only the lower bpp bits are significant. - # We invert them to match the Palm. - bpp = im.info["bpp"] - im = im.point(lambda x, maxval=(1 << bpp)-1: maxval - (x & maxval)) # we ignore the palette here im.mode = "P" rawmode = "P;" + str(bpp) @@ -166,7 +153,7 @@ def _save(im, fp, filename): else: - raise IOError("cannot write mode %s as Palm" % im.mode) + raise OSError("cannot write mode %s as Palm" % im.mode) # # make sure image data is available @@ -177,7 +164,7 @@ def _save(im, fp, filename): cols = im.size[0] rows = im.size[1] - rowbytes = int((cols + (16//bpp - 1)) / (16 // bpp)) * 2 + rowbytes = int((cols + (16 // bpp - 1)) / (16 // bpp)) * 2 transparent_index = 0 compression_type = _COMPRESSION_TYPES["none"] @@ -201,7 +188,7 @@ def _save(im, fp, filename): fp.write(o16b(offset)) fp.write(o8(transparent_index)) fp.write(o8(compression_type)) - fp.write(o16b(0)) # reserved by Palm + fp.write(o16b(0)) # reserved by Palm # now write colormap if necessary @@ -209,20 +196,21 @@ def _save(im, fp, filename): fp.write(o16b(256)) for i in range(256): fp.write(o8(i)) - if colormapmode == 'RGB': + if colormapmode == "RGB": fp.write( - o8(colormap[3 * i]) + - o8(colormap[3 * i + 1]) + - o8(colormap[3 * i + 2])) - elif colormapmode == 'RGBA': + o8(colormap[3 * i]) + + o8(colormap[3 * i + 1]) + + o8(colormap[3 * i + 2]) + ) + elif colormapmode == "RGBA": fp.write( - o8(colormap[4 * i]) + - o8(colormap[4 * i + 1]) + - o8(colormap[4 * i + 2])) + o8(colormap[4 * i]) + + o8(colormap[4 * i + 1]) + + o8(colormap[4 * i + 2]) + ) # now convert data to raw form - ImageFile._save( - im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, rowbytes, 1))]) + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))]) if hasattr(fp, "flush"): fp.flush() diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 7e8fa3128..625f55646 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -18,16 +18,12 @@ 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 # encoding. + class PcdImageFile(ImageFile.ImageFile): format = "PCD" @@ -51,7 +47,7 @@ class PcdImageFile(ImageFile.ImageFile): self.mode = "RGB" self._size = 768, 512 # FIXME: not correct for rotated images! - self.tile = [("pcd", (0, 0)+self.size, 96*2048, None)] + self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] def load_end(self): if self.tile_post_rotate: diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 471d6647a..605cbbdf3 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -16,39 +16,42 @@ # See the README file for information on usage and redistribution. # -from . import Image, FontFile -from ._binary import i8, i16le as l16, i32le as l32, i16be as b16, i32be as b32 +import io + +from . import FontFile, Image +from ._binary import i8, i16be as b16, i16le as l16, i32be as b32, i32le as l32 # -------------------------------------------------------------------- # declarations PCF_MAGIC = 0x70636601 # "\x01fcp" -PCF_PROPERTIES = (1 << 0) -PCF_ACCELERATORS = (1 << 1) -PCF_METRICS = (1 << 2) -PCF_BITMAPS = (1 << 3) -PCF_INK_METRICS = (1 << 4) -PCF_BDF_ENCODINGS = (1 << 5) -PCF_SWIDTHS = (1 << 6) -PCF_GLYPH_NAMES = (1 << 7) -PCF_BDF_ACCELERATORS = (1 << 8) +PCF_PROPERTIES = 1 << 0 +PCF_ACCELERATORS = 1 << 1 +PCF_METRICS = 1 << 2 +PCF_BITMAPS = 1 << 3 +PCF_INK_METRICS = 1 << 4 +PCF_BDF_ENCODINGS = 1 << 5 +PCF_SWIDTHS = 1 << 6 +PCF_GLYPH_NAMES = 1 << 7 +PCF_BDF_ACCELERATORS = 1 << 8 BYTES_PER_ROW = [ - lambda bits: ((bits+7) >> 3), - lambda bits: ((bits+15) >> 3) & ~1, - lambda bits: ((bits+31) >> 3) & ~3, - lambda bits: ((bits+63) >> 3) & ~7, + lambda bits: ((bits + 7) >> 3), + lambda bits: ((bits + 15) >> 3) & ~1, + lambda bits: ((bits + 31) >> 3) & ~3, + lambda bits: ((bits + 63) >> 3) & ~7, ] def sz(s, o): - return s[o:s.index(b"\0", o)] + return s[o : s.index(b"\0", o)] ## # Font file plugin for the X11 PCF format. + class PcfFontFile(FontFile.FontFile): name = "name" @@ -59,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 = {} @@ -82,7 +85,7 @@ class PcfFontFile(FontFile.FontFile): ix = encoding[ch] if ix is not None: x, y, l, r, w, a, d, f = metrics[ix] - glyph = (w, 0), (l, d-y, x+l, d), (0, 0, x, y), bitmaps[ix] + glyph = (w, 0), (l, d - y, x + l, d), (0, 0, x, y), bitmaps[ix] self.glyph[ch] = glyph def _getformat(self, tag): @@ -117,7 +120,7 @@ class PcfFontFile(FontFile.FontFile): for i in range(nprops): p.append((i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4)))) if nprops & 3: - fp.seek(4 - (nprops & 3), 1) # pad + fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad data = fp.read(i32(fp.read(4))) @@ -140,7 +143,7 @@ class PcfFontFile(FontFile.FontFile): append = metrics.append - if (format & 0xff00) == 0x100: + if (format & 0xFF00) == 0x100: # "compressed" metrics for i in range(i16(fp.read(2))): @@ -151,10 +154,7 @@ class PcfFontFile(FontFile.FontFile): descent = i8(fp.read(1)) - 128 xsize = right - left ysize = ascent + descent - append( - (xsize, ysize, left, right, width, - ascent, descent, 0) - ) + append((xsize, ysize, left, right, width, ascent, descent, 0)) else: @@ -168,10 +168,7 @@ class PcfFontFile(FontFile.FontFile): attributes = i16(fp.read(2)) xsize = right - left ysize = ascent + descent - append( - (xsize, ysize, left, right, width, - ascent, descent, attributes) - ) + append((xsize, ysize, left, right, width, ascent, descent, attributes)) return metrics @@ -187,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): @@ -198,7 +195,7 @@ class PcfFontFile(FontFile.FontFile): bitmapSizes.append(i32(fp.read(4))) # byteorder = format & 4 # non-zero => MSB - bitorder = format & 8 # non-zero => MSB + bitorder = format & 8 # non-zero => MSB padindex = format & 3 bitmapsize = bitmapSizes[padindex] @@ -213,10 +210,8 @@ class PcfFontFile(FontFile.FontFile): for i in range(nbitmaps): x, y, l, r, w, a, d, f = metrics[i] - b, e = offsets[i], offsets[i+1] - bitmaps.append( - Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x)) - ) + b, e = offsets[i], offsets[i + 1] + bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) return bitmaps @@ -238,7 +233,7 @@ class PcfFontFile(FontFile.FontFile): encodingOffset = i16(fp.read(2)) if encodingOffset != 0xFFFF: try: - encoding[i+firstCol] = encodingOffset + encoding[i + firstCol] = encodingOffset except IndexError: break # only load ISO-8859-1 glyphs diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 57d8189dd..6cf10deb3 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -25,16 +25,14 @@ # See the README file for information on usage and redistribution. # +import io import logging + from . import Image, ImageFile, ImagePalette 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] @@ -43,6 +41,7 @@ def _accept(prefix): ## # Image plugin for Paintbrush images. + class PcxImageFile(ImageFile.ImageFile): format = "PCX" @@ -56,7 +55,7 @@ class PcxImageFile(ImageFile.ImageFile): raise SyntaxError("not a PCX file") # image - bbox = i16(s, 4), i16(s, 6), i16(s, 8)+1, i16(s, 10)+1 + bbox = i16(s, 4), i16(s, 6), i16(s, 8) + 1, i16(s, 10) + 1 if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: raise SyntaxError("bad PCX image size") logger.debug("BBox: %s %s %s %s", *bbox) @@ -66,8 +65,13 @@ class PcxImageFile(ImageFile.ImageFile): bits = i8(s[3]) planes = i8(s[65]) stride = i16(s, 66) - logger.debug("PCX version %s, bits %s, planes %s, stride %s", - version, bits, planes, stride) + logger.debug( + "PCX version %s, bits %s, planes %s, stride %s", + version, + bits, + planes, + stride, + ) self.info["dpi"] = i16(s, 12), i16(s, 14) @@ -82,12 +86,12 @@ class PcxImageFile(ImageFile.ImageFile): elif version == 5 and bits == 8 and planes == 1: mode = rawmode = "L" # FIXME: hey, this doesn't work with the incremental loader !!! - self.fp.seek(-769, 2) + self.fp.seek(-769, io.SEEK_END) s = self.fp.read(769) if len(s) == 769 and i8(s[0]) == 12: # check if the palette is linear greyscale for i in range(256): - if s[i*3+1:i*3+4] != o8(i)*3: + if s[i * 3 + 1 : i * 3 + 4] != o8(i) * 3: mode = rawmode = "P" break if mode == "P": @@ -99,16 +103,17 @@ 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] + self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] bbox = (0, 0) + self.size logger.debug("size: %sx%s", *self.size) self.tile = [("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))] + # -------------------------------------------------------------------- # save PCX files @@ -137,8 +142,12 @@ def _save(im, fp, filename): # Ideally it should be passed in in the state, but the bytes value # gets overwritten. - logger.debug("PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d", - im.size[0], bits, stride) + logger.debug( + "PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d", + im.size[0], + bits, + stride, + ) # under windows, we could determine the current screen size with # "Image.core.display_mode()[1]", but I think that's overkill... @@ -149,17 +158,30 @@ def _save(im, fp, filename): # PCX header fp.write( - o8(10) + o8(version) + o8(1) + o8(bits) + o16(0) + - o16(0) + o16(im.size[0]-1) + o16(im.size[1]-1) + o16(dpi[0]) + - o16(dpi[1]) + b"\0"*24 + b"\xFF"*24 + b"\0" + o8(planes) + - o16(stride) + o16(1) + o16(screen[0]) + o16(screen[1]) + - b"\0"*54 - ) + o8(10) + + o8(version) + + o8(1) + + o8(bits) + + o16(0) + + o16(0) + + o16(im.size[0] - 1) + + o16(im.size[1] - 1) + + o16(dpi[0]) + + o16(dpi[1]) + + b"\0" * 24 + + b"\xFF" * 24 + + b"\0" + + o8(planes) + + o16(stride) + + o16(1) + + o16(screen[0]) + + o16(screen[1]) + + b"\0" * 54 + ) assert fp.tell() == 128 - ImageFile._save(im, fp, [("pcx", (0, 0)+im.size, 0, - (rawmode, bits*planes))]) + ImageFile._save(im, fp, [("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))]) if im.mode == "P": # colour palette @@ -169,7 +191,8 @@ def _save(im, fp, filename): # greyscale palette fp.write(o8(12)) for i in range(256): - fp.write(o8(i)*3) + fp.write(o8(i) * 3) + # -------------------------------------------------------------------- # registry @@ -179,3 +202,5 @@ Image.register_open(PcxImageFile.format, PcxImageFile, _accept) Image.register_save(PcxImageFile.format, _save) Image.register_extension(PcxImageFile.format, ".pcx") + +Image.register_mime(PcxImageFile.format, "image/x-pcx") diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 702aaa392..d9bbf6fab 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -20,15 +20,11 @@ # Image plugin for PDF images (output only). ## -from . import Image, ImageFile, ImageSequence, PdfParser import io import os import time -# __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__ # # -------------------------------------------------------------------- @@ -48,6 +44,7 @@ def _save_all(im, fp, filename): ## # (Internal) Image save plugin for the PDF format. + def _save(im, fp, filename, save_all=False): is_appending = im.encoderinfo.get("append", False) if is_appending: @@ -58,16 +55,16 @@ def _save(im, fp, filename, save_all=False): resolution = im.encoderinfo.get("resolution", 72.0) info = { - "title": None if is_appending else os.path.splitext( - os.path.basename(filename) - )[0], + "title": None + if is_appending + else os.path.splitext(os.path.basename(filename))[0], "author": None, "subject": None, "keywords": None, "creator": None, "producer": None, "creationDate": None if is_appending else time.gmtime(), - "modDate": None if is_appending else time.gmtime() + "modDate": None if is_appending else time.gmtime(), } for k, default in info.items(): v = im.encoderinfo.get(k) if k in im.encoderinfo else default @@ -80,7 +77,7 @@ def _save(im, fp, filename, save_all=False): existing_pdf.start_writing() existing_pdf.write_header() - existing_pdf.write_comment("created by PIL PDF driver " + __version__) + existing_pdf.write_comment("created by Pillow {} PDF driver".format(__version__)) # # pages @@ -142,7 +139,7 @@ def _save(im, fp, filename, save_all=False): PdfParser.PdfName("Indexed"), PdfParser.PdfName("DeviceRGB"), 255, - PdfParser.PdfBinary(palette) + PdfParser.PdfBinary(palette), ] procset = "ImageI" # indexed color elif im.mode == "RGB": @@ -166,16 +163,15 @@ def _save(im, fp, filename, save_all=False): # FIXME: the hex encoder doesn't support packed 1-bit # images; do things the hard way... data = im.tobytes("raw", "1") - im = Image.new("L", (len(data), 1), None) + im = Image.new("L", im.size) im.putdata(data) - ImageFile._save(im, op, [("hex", (0, 0)+im.size, 0, im.mode)]) + ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) elif filter == "FlateDecode": - ImageFile._save(im, op, [("zip", (0, 0)+im.size, 0, im.mode)]) + ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)]) elif filter == "RunLengthDecode": - ImageFile._save(im, op, - [("packbits", (0, 0)+im.size, 0, im.mode)]) + ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)]) else: raise ValueError("unsupported PDF filter (%s)" % filter) @@ -184,48 +180,46 @@ def _save(im, fp, filename, save_all=False): width, height = im.size - existing_pdf.write_obj(image_refs[pageNumber], - stream=op.getvalue(), - Type=PdfParser.PdfName("XObject"), - Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / resolution, - Height=height, # * 72.0 / resolution, - Filter=PdfParser.PdfName(filter), - BitsPerComponent=bits, - DecodeParams=params, - ColorSpace=colorspace) + existing_pdf.write_obj( + image_refs[pageNumber], + stream=op.getvalue(), + Type=PdfParser.PdfName("XObject"), + Subtype=PdfParser.PdfName("Image"), + Width=width, # * 72.0 / resolution, + Height=height, # * 72.0 / resolution, + Filter=PdfParser.PdfName(filter), + BitsPerComponent=bits, + DecodeParams=params, + ColorSpace=colorspace, + ) # # page - existing_pdf.write_page(page_refs[pageNumber], - Resources=PdfParser.PdfDict( - ProcSet=[ - PdfParser.PdfName("PDF"), - PdfParser.PdfName(procset) - ], - XObject=PdfParser.PdfDict( - image=image_refs[pageNumber] - ) - ), - MediaBox=[ - 0, - 0, - int(width * 72.0 / resolution), - int(height * 72.0 / resolution) - ], - Contents=contents_refs[pageNumber]) + existing_pdf.write_page( + page_refs[pageNumber], + Resources=PdfParser.PdfDict( + ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)], + XObject=PdfParser.PdfDict(image=image_refs[pageNumber]), + ), + MediaBox=[ + 0, + 0, + int(width * 72.0 / resolution), + int(height * 72.0 / resolution), + ], + Contents=contents_refs[pageNumber], + ) # # page contents page_contents = PdfParser.make_bytes( - "q %d 0 0 %d 0 0 cm /image Do Q\n" % ( - int(width * 72.0 / resolution), - int(height * 72.0 / resolution))) + "q %d 0 0 %d 0 0 cm /image Do Q\n" + % (int(width * 72.0 / resolution), int(height * 72.0 / resolution)) + ) - existing_pdf.write_obj(contents_refs[pageNumber], - stream=page_contents) + existing_pdf.write_obj(contents_refs[pageNumber], stream=page_contents) pageNumber += 1 @@ -236,6 +230,7 @@ def _save(im, fp, filename, save_all=False): fp.flush() existing_pdf.close() + # # -------------------------------------------------------------------- diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index a7773ef1e..3267ee491 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -6,20 +6,10 @@ import os 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 @@ -29,62 +19,61 @@ def encode_text(s): PDFDocEncoding = { - 0x16: u"\u0017", - 0x18: u"\u02D8", - 0x19: u"\u02C7", - 0x1A: u"\u02C6", - 0x1B: u"\u02D9", - 0x1C: u"\u02DD", - 0x1D: u"\u02DB", - 0x1E: u"\u02DA", - 0x1F: u"\u02DC", - 0x80: u"\u2022", - 0x81: u"\u2020", - 0x82: u"\u2021", - 0x83: u"\u2026", - 0x84: u"\u2014", - 0x85: u"\u2013", - 0x86: u"\u0192", - 0x87: u"\u2044", - 0x88: u"\u2039", - 0x89: u"\u203A", - 0x8A: u"\u2212", - 0x8B: u"\u2030", - 0x8C: u"\u201E", - 0x8D: u"\u201C", - 0x8E: u"\u201D", - 0x8F: u"\u2018", - 0x90: u"\u2019", - 0x91: u"\u201A", - 0x92: u"\u2122", - 0x93: u"\uFB01", - 0x94: u"\uFB02", - 0x95: u"\u0141", - 0x96: u"\u0152", - 0x97: u"\u0160", - 0x98: u"\u0178", - 0x99: u"\u017D", - 0x9A: u"\u0131", - 0x9B: u"\u0142", - 0x9C: u"\u0153", - 0x9D: u"\u0161", - 0x9E: u"\u017E", - 0xA0: u"\u20AC", + 0x16: "\u0017", + 0x18: "\u02D8", + 0x19: "\u02C7", + 0x1A: "\u02C6", + 0x1B: "\u02D9", + 0x1C: "\u02DD", + 0x1D: "\u02DB", + 0x1E: "\u02DA", + 0x1F: "\u02DC", + 0x80: "\u2022", + 0x81: "\u2020", + 0x82: "\u2021", + 0x83: "\u2026", + 0x84: "\u2014", + 0x85: "\u2013", + 0x86: "\u0192", + 0x87: "\u2044", + 0x88: "\u2039", + 0x89: "\u203A", + 0x8A: "\u2212", + 0x8B: "\u2030", + 0x8C: "\u201E", + 0x8D: "\u201C", + 0x8E: "\u201D", + 0x8F: "\u2018", + 0x90: "\u2019", + 0x91: "\u201A", + 0x92: "\u2122", + 0x93: "\uFB01", + 0x94: "\uFB02", + 0x95: "\u0141", + 0x96: "\u0152", + 0x97: "\u0160", + 0x98: "\u0178", + 0x99: "\u017D", + 0x9A: "\u0131", + 0x9B: "\u0142", + 0x9C: "\u0153", + 0x9D: "\u0161", + 0x9E: "\u017E", + 0xA0: "\u20AC", } def decode_text(b): - if b[:len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE: - return b[len(codecs.BOM_UTF16_BE):].decode("utf_16_be") - elif py3: # Python 3.x + if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE: + return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be") + 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): """An error that probably indicates a syntactic or semantic error in the PDF file structure""" + pass @@ -93,8 +82,9 @@ def check_format_condition(condition, error_message): raise PdfFormatError(error_message) -class IndirectReference(collections.namedtuple("IndirectReferenceTuple", - ["object_id", "generation"])): +class IndirectReference( + collections.namedtuple("IndirectReferenceTuple", ["object_id", "generation"]) +): def __str__(self): return "%s %s R" % self @@ -102,9 +92,11 @@ class IndirectReference(collections.namedtuple("IndirectReferenceTuple", return self.__str__().encode("us-ascii") def __eq__(self, other): - return other.__class__ is self.__class__ and \ - other.object_id == self.object_id and \ - other.generation == self.generation + return ( + other.__class__ is self.__class__ + and other.object_id == self.object_id + and other.generation == self.generation + ) def __ne__(self, other): return not (self == other) @@ -120,9 +112,9 @@ class IndirectObjectDef(IndirectReference): class XrefTable: def __init__(self): - self.existing_entries = {} # object ID => (offset, generation) - self.new_entries = {} # object ID => (offset, generation) - self.deleted_entries = {0: 65536} # object ID => generation + self.existing_entries = {} # object ID => (offset, generation) + self.new_entries = {} # object ID => (offset, generation) + self.deleted_entries = {0: 65536} # object ID => generation self.reading_finished = False def __setitem__(self, key, value): @@ -150,26 +142,27 @@ class XrefTable: elif key in self.deleted_entries: generation = self.deleted_entries[key] else: - raise IndexError("object ID " + str(key) + - " cannot be deleted because it doesn't exist") + raise IndexError( + "object ID " + str(key) + " cannot be deleted because it doesn't exist" + ) def __contains__(self, key): return key in self.existing_entries or key in self.new_entries def __len__(self): - return len(set(self.existing_entries.keys()) | - set(self.new_entries.keys()) | - set(self.deleted_entries.keys())) + return len( + set(self.existing_entries.keys()) + | set(self.new_entries.keys()) + | set(self.deleted_entries.keys()) + ) def keys(self): return ( - set(self.existing_entries.keys()) - - set(self.deleted_entries.keys()) + set(self.existing_entries.keys()) - set(self.deleted_entries.keys()) ) | set(self.new_entries.keys()) def write(self, f): - keys = sorted(set(self.new_entries.keys()) | - set(self.deleted_entries.keys())) + keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys())) deleted_keys = sorted(set(self.deleted_entries.keys())) startxref = f.tell() f.write(b"xref\n") @@ -177,7 +170,7 @@ class XrefTable: # find a contiguous sequence of object IDs prev = None for index, key in enumerate(keys): - if prev is None or prev+1 == key: + if prev is None or prev + 1 == key: prev = key else: contiguous_keys = keys[:index] @@ -186,25 +179,27 @@ class XrefTable: else: contiguous_keys = keys keys = None - f.write(make_bytes("%d %d\n" % - (contiguous_keys[0], len(contiguous_keys)))) + f.write(make_bytes("%d %d\n" % (contiguous_keys[0], len(contiguous_keys)))) for object_id in contiguous_keys: if object_id in self.new_entries: - f.write(make_bytes("%010d %05d n \n" % - self.new_entries[object_id])) + f.write(make_bytes("%010d %05d n \n" % self.new_entries[object_id])) else: this_deleted_object_id = deleted_keys.pop(0) - check_format_condition(object_id == this_deleted_object_id, - "expected the next deleted object " - "ID to be %s, instead found %s" % - (object_id, this_deleted_object_id)) + check_format_condition( + object_id == this_deleted_object_id, + "expected the next deleted object ID to be %s, instead found %s" + % (object_id, this_deleted_object_id), + ) try: next_in_linked_list = deleted_keys[0] except IndexError: next_in_linked_list = 0 - f.write(make_bytes("%010d %05d f \n" % - (next_in_linked_list, - self.deleted_entries[object_id]))) + f.write( + make_bytes( + "%010d %05d f \n" + % (next_in_linked_list, self.deleted_entries[object_id]) + ) + ) return startxref @@ -221,8 +216,9 @@ class PdfName: return self.name.decode("us-ascii") def __eq__(self, other): - return (isinstance(other, PdfName) and other.name == self.name) or \ - other == self.name + return ( + isinstance(other, PdfName) and other.name == self.name + ) or other == self.name def __hash__(self): return hash(self.name) @@ -234,21 +230,15 @@ class PdfName: def from_pdf_stream(cls, data): return cls(PdfParser.interpret_name(data)) - allowed_chars = set(range(33, 127)) - set(ord(c) for c in "#%/()<>[]{}") + allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} def __bytes__(self): result = bytearray(b"/") for b in self.name: - if py3: # Python 3.x - if b in self.allowed_chars: - result.append(b) - else: - result.extend(make_bytes("#%02X" % b)) - else: # Python 2.x - if ord(b) in self.allowed_chars: - result.append(b) - else: - result.extend(b"#%02X" % ord(b)) + if b in self.allowed_chars: + result.append(b) + else: + result.extend(make_bytes("#%02X" % b)) return bytes(result) __str__ = __bytes__ @@ -261,44 +251,36 @@ 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: - if isinstance(key, str): - key = key.encode("us-ascii") - self[key] = value + self[key.encode("us-ascii")] = value def __getattr__(self, key): try: - value = self[key] + value = self[key.encode("us-ascii")] except KeyError: - try: - value = self[key.encode("us-ascii")] - except KeyError: - raise AttributeError(key) + raise AttributeError(key) if isinstance(value, bytes): value = decode_text(value) if key.endswith("Date"): if value.startswith("D:"): value = value[2:] - relationship = 'Z' + relationship = "Z" if len(value) > 17: relationship = value[14] offset = int(value[15:17]) * 60 if len(value) > 20: offset += int(value[18:20]) - format = '%Y%m%d%H%M%S'[:len(value) - 2] - value = time.strptime(value[:len(format)+2], format) - if relationship in ['+', '-']: + format = "%Y%m%d%H%M%S"[: len(value) - 2] + value = time.strptime(value[: len(format) + 2], format) + if relationship in ["+", "-"]: offset *= 60 - if relationship == '+': + if relationship == "+": offset *= -1 value = time.gmtime(calendar.timegm(value) + offset) return value @@ -316,20 +298,13 @@ class PdfDict(UserDict): out.extend(b"\n>>") return bytes(out) - if not py3: - __str__ = __bytes__ - class PdfBinary: def __init__(self, data): self.data = data - if py3: # Python 3.x - def __bytes__(self): - return make_bytes("<%s>" % "".join("%02X" % b for b in self.data)) - else: # Python 2.x - def __str__(self): - return "<%s>" % "".join("%02X" % ord(b) for b in self.data) + def __bytes__(self): + return make_bytes("<%s>" % "".join("%02X" % b for b in self.data)) class PdfStream: @@ -350,8 +325,8 @@ class PdfStream: return zlib.decompress(self.buf, bufsize=int(expected_length)) else: raise NotImplementedError( - "stream filter %s unknown/unsupported" % - repr(self.dictionary.Filter)) + "stream filter %s unknown/unsupported" % repr(self.dictionary.Filter) + ) def pdf_repr(x): @@ -361,19 +336,17 @@ def pdf_repr(x): return b"false" elif x is None: return b"null" - elif (isinstance(x, PdfName) or isinstance(x, PdfDict) or - isinstance(x, PdfArray) or isinstance(x, PdfBinary)): + elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)): return bytes(x) elif isinstance(x, int): return str(x).encode("us-ascii") elif isinstance(x, time.struct_time): - return b'(D:'+time.strftime('%Y%m%d%H%M%SZ', x).encode("us-ascii")+b')' + return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")" elif isinstance(x, dict): 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 @@ -391,11 +364,9 @@ class PdfParser: Supports PDF up to 1.4 """ - def __init__(self, filename=None, f=None, - buf=None, start_offset=0, mode="rb"): + def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"): if buf and f: - raise RuntimeError( - "specify buf or f or filename, but not both buf and f") + raise RuntimeError("specify buf or f or filename, but not both buf and f") self.filename = filename self.buf = buf self.f = f @@ -462,20 +433,20 @@ 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() self.root_ref = self.next_object_id(self.f.tell()) self.pages_ref = self.next_object_id(0) self.rewrite_pages() - self.write_obj(self.root_ref, - Type=PdfName(b"Catalog"), - Pages=self.pages_ref) - self.write_obj(self.pages_ref, - Type=PdfName(b"Pages"), - Count=len(self.pages), - Kids=self.pages) + self.write_obj(self.root_ref, Type=PdfName(b"Catalog"), Pages=self.pages_ref) + self.write_obj( + self.pages_ref, + Type=PdfName(b"Pages"), + Count=len(self.pages), + Kids=self.pages, + ) return self.root_ref def rewrite_pages(self): @@ -521,8 +492,11 @@ class PdfParser: if self.info: trailer_dict[b"Info"] = self.info_ref self.last_xref_section_offset = start_xref - self.f.write(b"trailer\n" + bytes(PdfDict(trailer_dict)) + - make_bytes("\nstartxref\n%d\n%%%%EOF" % start_xref)) + self.f.write( + b"trailer\n" + + bytes(PdfDict(trailer_dict)) + + make_bytes("\nstartxref\n%d\n%%%%EOF" % start_xref) + ) def write_page(self, ref, *objs, **dict_obj): if isinstance(ref, int): @@ -584,12 +558,14 @@ class PdfParser: else: self.info = PdfDict(self.read_indirect(self.info_ref)) check_format_condition(b"Type" in self.root, "/Type missing in Root") - check_format_condition(self.root[b"Type"] == b"Catalog", - "/Type in Root is not /Catalog") + check_format_condition( + self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog" + ) check_format_condition(b"Pages" in self.root, "/Pages missing in Root") - check_format_condition(isinstance(self.root[b"Pages"], - IndirectReference), - "/Pages in Root is not an indirect reference") + check_format_condition( + isinstance(self.root[b"Pages"], IndirectReference), + "/Pages in Root is not an indirect reference", + ) self.pages_ref = self.root[b"Pages"] self.page_tree_root = self.read_indirect(self.pages_ref) self.pages = self.linearize_page_tree(self.page_tree_root) @@ -617,13 +593,34 @@ class PdfParser: newline_only = br"[\r\n]+" newline = whitespace_optional + newline_only + whitespace_optional re_trailer_end = re.compile( - whitespace_mandatory + br"trailer" + whitespace_optional + - br"\<\<(.*\>\>)" + newline + br"startxref" + newline + br"([0-9]+)" + - newline + br"%%EOF" + whitespace_optional + br"$", re.DOTALL) + whitespace_mandatory + + br"trailer" + + whitespace_optional + + br"\<\<(.*\>\>)" + + newline + + br"startxref" + + newline + + br"([0-9]+)" + + newline + + br"%%EOF" + + whitespace_optional + + br"$", + re.DOTALL, + ) re_trailer_prev = re.compile( - whitespace_optional + br"trailer" + whitespace_optional + - br"\<\<(.*?\>\>)" + newline + br"startxref" + newline + br"([0-9]+)" + - newline + br"%%EOF" + whitespace_optional, re.DOTALL) + whitespace_optional + + br"trailer" + + whitespace_optional + + br"\<\<(.*?\>\>)" + + newline + + br"startxref" + + newline + + br"([0-9]+)" + + newline + + br"%%EOF" + + whitespace_optional, + re.DOTALL, + ) def read_trailer(self): search_start_offset = len(self.buf) - 16384 @@ -635,7 +632,7 @@ class PdfParser: last_match = m while m: last_match = m - m = self.re_trailer_end.search(self.buf, m.start()+16) + m = self.re_trailer_end.search(self.buf, m.start() + 16) if not m: m = last_match trailer_data = m.group(1) @@ -647,26 +644,29 @@ class PdfParser: self.read_prev_trailer(self.trailer_dict[b"Prev"]) def read_prev_trailer(self, xref_section_offset): - trailer_offset = self.read_xref_table( - xref_section_offset=xref_section_offset) + trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset) m = self.re_trailer_prev.search( - self.buf[trailer_offset:trailer_offset+16384]) + self.buf[trailer_offset : trailer_offset + 16384] + ) check_format_condition(m, "previous trailer not found") trailer_data = m.group(1) - check_format_condition(int(m.group(2)) == xref_section_offset, - "xref section offset in previous trailer " - "doesn't match what was expected") + check_format_condition( + int(m.group(2)) == xref_section_offset, + "xref section offset in previous trailer doesn't match what was expected", + ) trailer_dict = self.interpret_trailer(trailer_data) if b"Prev" in trailer_dict: self.read_prev_trailer(trailer_dict[b"Prev"]) re_whitespace_optional = re.compile(whitespace_optional) re_name = re.compile( - whitespace_optional + br"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" + - delimiter_or_ws + br")") + whitespace_optional + + br"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" + + delimiter_or_ws + + br")" + ) re_dict_start = re.compile(whitespace_optional + br"\<\<") - re_dict_end = re.compile( - whitespace_optional + br"\>\>" + whitespace_optional) + re_dict_end = re.compile(whitespace_optional + br"\>\>" + whitespace_optional) @classmethod def interpret_trailer(cls, trailer_data): @@ -678,19 +678,21 @@ class PdfParser: m = cls.re_dict_end.match(trailer_data, offset) check_format_condition( m and m.end() == len(trailer_data), - "name not found in trailer, remaining data: " + - repr(trailer_data[offset:])) + "name not found in trailer, remaining data: " + + repr(trailer_data[offset:]), + ) break key = cls.interpret_name(m.group(1)) value, offset = cls.get_value(trailer_data, m.end()) trailer[key] = value check_format_condition( b"Size" in trailer and isinstance(trailer[b"Size"], int), - "/Size not in trailer or not an integer") + "/Size not in trailer or not an integer", + ) check_format_condition( - b"Root" in trailer and - isinstance(trailer[b"Root"], IndirectReference), - "/Root not in trailer or not an indirect reference") + b"Root" in trailer and isinstance(trailer[b"Root"], IndirectReference), + "/Root not in trailer or not an indirect reference", + ) return trailer re_hashes_in_name = re.compile(br"([^#]*)(#([0-9a-fA-F]{2}))?") @@ -700,8 +702,7 @@ class PdfParser: name = b"" for m in cls.re_hashes_in_name.finditer(raw): if m.group(3): - name += m.group(1) + \ - bytearray.fromhex(m.group(3).decode("us-ascii")) + name += m.group(1) + bytearray.fromhex(m.group(3).decode("us-ascii")) else: name += m.group(1) if as_text: @@ -709,37 +710,54 @@ class PdfParser: else: return bytes(name) - re_null = re.compile( - whitespace_optional + br"null(?=" + delimiter_or_ws + br")") - re_true = re.compile( - whitespace_optional + br"true(?=" + delimiter_or_ws + br")") - re_false = re.compile( - whitespace_optional + br"false(?=" + delimiter_or_ws + br")") + re_null = re.compile(whitespace_optional + br"null(?=" + delimiter_or_ws + br")") + re_true = re.compile(whitespace_optional + br"true(?=" + delimiter_or_ws + br")") + re_false = re.compile(whitespace_optional + br"false(?=" + delimiter_or_ws + br")") re_int = re.compile( - whitespace_optional + br"([-+]?[0-9]+)(?=" + delimiter_or_ws + br")") + whitespace_optional + br"([-+]?[0-9]+)(?=" + delimiter_or_ws + br")" + ) re_real = re.compile( - whitespace_optional + br"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" + - delimiter_or_ws + br")") + whitespace_optional + + br"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" + + delimiter_or_ws + + br")" + ) re_array_start = re.compile(whitespace_optional + br"\[") re_array_end = re.compile(whitespace_optional + br"]") re_string_hex = re.compile( - whitespace_optional + br"\<(" + whitespace_or_hex + br"*)\>") + whitespace_optional + br"\<(" + whitespace_or_hex + br"*)\>" + ) re_string_lit = re.compile(whitespace_optional + br"\(") re_indirect_reference = re.compile( - whitespace_optional + br"([-+]?[0-9]+)" + whitespace_mandatory + - br"([-+]?[0-9]+)" + whitespace_mandatory + br"R(?=" + delimiter_or_ws + - br")") + whitespace_optional + + br"([-+]?[0-9]+)" + + whitespace_mandatory + + br"([-+]?[0-9]+)" + + whitespace_mandatory + + br"R(?=" + + delimiter_or_ws + + br")" + ) re_indirect_def_start = re.compile( - whitespace_optional + br"([-+]?[0-9]+)" + whitespace_mandatory + - br"([-+]?[0-9]+)" + whitespace_mandatory + br"obj(?=" + - delimiter_or_ws + br")") + whitespace_optional + + br"([-+]?[0-9]+)" + + whitespace_mandatory + + br"([-+]?[0-9]+)" + + whitespace_mandatory + + br"obj(?=" + + delimiter_or_ws + + br")" + ) re_indirect_def_end = re.compile( - whitespace_optional + br"endobj(?=" + delimiter_or_ws + br")") + whitespace_optional + br"endobj(?=" + delimiter_or_ws + br")" + ) re_comment = re.compile( - br"(" + whitespace_optional + br"%[^\r\n]*" + newline + br")*") + br"(" + whitespace_optional + br"%[^\r\n]*" + newline + br")*" + ) re_stream_start = re.compile(whitespace_optional + br"stream\r?\n") re_stream_end = re.compile( - whitespace_optional + br"endstream(?=" + delimiter_or_ws + br")") + whitespace_optional + br"endstream(?=" + delimiter_or_ws + br")" + ) @classmethod def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): @@ -752,32 +770,37 @@ class PdfParser: if m: check_format_condition( int(m.group(1)) > 0, - "indirect object definition: object ID must be greater than 0") + "indirect object definition: object ID must be greater than 0", + ) check_format_condition( int(m.group(2)) >= 0, - "indirect object definition: generation must be non-negative") + "indirect object definition: generation must be non-negative", + ) check_format_condition( - expect_indirect is None or expect_indirect == - IndirectReference(int(m.group(1)), int(m.group(2))), - "indirect object definition different than expected") - object, offset = cls.get_value( - data, m.end(), max_nesting=max_nesting-1) + expect_indirect is None + or expect_indirect + == IndirectReference(int(m.group(1)), int(m.group(2))), + "indirect object definition different than expected", + ) + object, offset = cls.get_value(data, m.end(), max_nesting=max_nesting - 1) if offset is None: return object, None m = cls.re_indirect_def_end.match(data, offset) - check_format_condition( - m, "indirect object definition end not found") + check_format_condition(m, "indirect object definition end not found") return object, m.end() check_format_condition( - not expect_indirect, "indirect object definition not found") + not expect_indirect, "indirect object definition not found" + ) m = cls.re_indirect_reference.match(data, offset) if m: check_format_condition( int(m.group(1)) > 0, - "indirect object reference: object ID must be greater than 0") + "indirect object reference: object ID must be greater than 0", + ) check_format_condition( int(m.group(2)) >= 0, - "indirect object reference: generation must be non-negative") + "indirect object reference: generation must be non-negative", + ) return IndirectReference(int(m.group(1)), int(m.group(2))), m.end() m = cls.re_dict_start.match(data, offset) if m: @@ -785,12 +808,10 @@ class PdfParser: result = {} m = cls.re_dict_end.match(data, offset) while not m: - key, offset = cls.get_value( - data, offset, max_nesting=max_nesting-1) + key, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) if offset is None: return result, None - value, offset = cls.get_value( - data, offset, max_nesting=max_nesting-1) + value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) result[key] = value if offset is None: return result, None @@ -802,9 +823,10 @@ class PdfParser: stream_len = int(result[b"Length"]) except (TypeError, KeyError, ValueError): raise PdfFormatError( - "bad or missing Length in stream dict (%r)" % - result.get(b"Length", None)) - stream_data = data[m.end():m.end() + stream_len] + "bad or missing Length in stream dict (%r)" + % result.get(b"Length", None) + ) + stream_data = data[m.end() : m.end() + stream_len] m = cls.re_stream_end.match(data, m.end() + stream_len) check_format_condition(m, "stream end not found") offset = m.end() @@ -818,8 +840,7 @@ class PdfParser: result = [] m = cls.re_array_end.match(data, offset) while not m: - value, offset = cls.get_value( - data, offset, max_nesting=max_nesting-1) + value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) result.append(value) if offset is None: return result, None @@ -847,10 +868,9 @@ class PdfParser: m = cls.re_string_hex.match(data, offset) if m: # filter out whitespace - hex_string = bytearray([ - b for b in m.group(1) - if b in b"0123456789abcdefABCDEF" - ]) + hex_string = bytearray( + [b for b in m.group(1) if b in b"0123456789abcdefABCDEF"] + ) if len(hex_string) % 2 == 1: # append a 0 if the length is not even - yes, at the end hex_string.append(ord(b"0")) @@ -859,11 +879,11 @@ class PdfParser: if m: return cls.get_literal_string(data, m.end()) # return None, offset # fallback (only for debugging) - raise PdfFormatError( - "unrecognized object: " + repr(data[offset:offset+32])) + raise PdfFormatError("unrecognized object: " + repr(data[offset : offset + 32])) re_lit_str_token = re.compile( - br"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))") + br"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" + ) escaped_chars = { b"n": b"\n", b"r": b"\r", @@ -881,14 +901,14 @@ class PdfParser: ord(b"("): b"(", ord(b")"): b")", ord(b"\\"): b"\\", - } + } @classmethod def get_literal_string(cls, data, offset): nesting_depth = 0 result = bytearray() for m in cls.re_lit_str_token.finditer(data, offset): - result.extend(data[offset:m.start()]) + result.extend(data[offset : m.start()]) if m.group(1): result.extend(cls.escaped_chars[m.group(1)[1]]) elif m.group(2): @@ -908,30 +928,36 @@ class PdfParser: offset = m.end() raise PdfFormatError("unfinished literal string") - re_xref_section_start = re.compile( - whitespace_optional + br"xref" + newline) + re_xref_section_start = re.compile(whitespace_optional + br"xref" + newline) re_xref_subsection_start = re.compile( - whitespace_optional + br"([0-9]+)" + whitespace_mandatory + - br"([0-9]+)" + whitespace_optional + newline_only) + whitespace_optional + + br"([0-9]+)" + + whitespace_mandatory + + br"([0-9]+)" + + whitespace_optional + + newline_only + ) re_xref_entry = re.compile(br"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") def read_xref_table(self, xref_section_offset): subsection_found = False m = self.re_xref_section_start.match( - self.buf, xref_section_offset + self.start_offset) + self.buf, xref_section_offset + self.start_offset + ) check_format_condition(m, "xref section start not found") offset = m.end() while True: m = self.re_xref_subsection_start.match(self.buf, offset) if not m: check_format_condition( - subsection_found, "xref subsection start not found") + subsection_found, "xref subsection start not found" + ) break subsection_found = True offset = m.end() first_object = int(m.group(1)) num_objects = int(m.group(2)) - for i in range(first_object, first_object+num_objects): + for i in range(first_object, first_object + num_objects): m = self.re_xref_entry.match(self.buf, offset) check_format_condition(m, "xref entry not found") offset = m.end() @@ -940,9 +966,9 @@ class PdfParser: if not is_free: new_entry = (int(m.group(1)), generation) check_format_condition( - i not in self.xref_table or - self.xref_table[i] == new_entry, - "xref entry duplicated (and not identical)") + i not in self.xref_table or self.xref_table[i] == new_entry, + "xref entry duplicated (and not identical)", + ) self.xref_table[i] = new_entry return offset @@ -952,10 +978,14 @@ class PdfParser: generation == ref[1], "expected to find generation %s for object ID %s in xref table, " "instead found generation %s at offset %s" - % (ref[1], ref[0], generation, offset)) - value = self.get_value(self.buf, offset + self.start_offset, - expect_indirect=IndirectReference(*ref), - max_nesting=max_nesting)[0] + % (ref[1], ref[0], generation, offset), + ) + value = self.get_value( + self.buf, + offset + self.start_offset, + expect_indirect=IndirectReference(*ref), + max_nesting=max_nesting, + )[0] self.cached_objects[ref] = value return value @@ -963,7 +993,8 @@ class PdfParser: if node is None: node = self.page_tree_root check_format_condition( - node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages") + node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages" + ) pages = [] for kid in node[b"Kids"]: kid_object = self.read_indirect(kid) diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index b4f19a96c..5ea32ba89 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -22,14 +22,10 @@ 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 + def _accept(prefix): return prefix[:4] == b"\200\350\000\000" @@ -37,6 +33,7 @@ def _accept(prefix): ## # Image plugin for PIXAR raster images. + class PixarImageFile(ImageFile.ImageFile): format = "PIXAR" @@ -62,7 +59,7 @@ class PixarImageFile(ImageFile.ImageFile): # FIXME: to be continued... # create tile descriptor (assuming "dumped") - self.tile = [("raw", (0, 0)+self.size, 1024, (self.mode, 0, 1))] + self.tile = [("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))] # diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 4b8b10a51..9f98cd3d7 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -33,16 +33,11 @@ import logging import re -import zlib import struct +import zlib from . import Image, ImageFile, ImagePalette from ._binary import i8, i16be as i16, i32be as i32, o16be as o16, o32be as o32 -from ._util import py3 - -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.9" logger = logging.getLogger(__name__) @@ -54,25 +49,30 @@ _MAGIC = b"\211PNG\r\n\032\n" _MODES = { # supported bits/color combinations, and corresponding modes/rawmodes - (1, 0): ("1", "1"), - (2, 0): ("L", "L;2"), - (4, 0): ("L", "L;4"), - (8, 0): ("L", "L"), + # Greyscale + (1, 0): ("1", "1"), + (2, 0): ("L", "L;2"), + (4, 0): ("L", "L;4"), + (8, 0): ("L", "L"), (16, 0): ("I", "I;16B"), - (8, 2): ("RGB", "RGB"), + # Truecolour + (8, 2): ("RGB", "RGB"), (16, 2): ("RGB", "RGB;16B"), - (1, 3): ("P", "P;1"), - (2, 3): ("P", "P;2"), - (4, 3): ("P", "P;4"), - (8, 3): ("P", "P"), - (8, 4): ("LA", "LA"), + # Indexed-colour + (1, 3): ("P", "P;1"), + (2, 3): ("P", "P;2"), + (4, 3): ("P", "P;4"), + (8, 3): ("P", "P"), + # Greyscale with alpha + (8, 4): ("LA", "LA"), (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available - (8, 6): ("RGBA", "RGBA"), + # Truecolour with alpha + (8, 6): ("RGBA", "RGBA"), (16, 6): ("RGBA", "RGBA;16B"), } -_simple_palette = re.compile(b'^\xff*\x00\xff*$') +_simple_palette = re.compile(b"^\xff*\x00\xff*$") # Maximum decompressed size for a iTXt or zTXt chunk. # Eliminates decompression bombs where compressed chunks can expand 1000x @@ -90,14 +90,14 @@ def _safe_zlib_decompress(s): def _crc32(data, seed=0): - return zlib.crc32(data, seed) & 0xffffffff + return zlib.crc32(data, seed) & 0xFFFFFFFF # -------------------------------------------------------------------- # Support classes. Suitable for PNG and related formats like MNG etc. -class ChunkStream(object): +class ChunkStream: def __init__(self, fp): self.fp = fp @@ -139,7 +139,7 @@ class ChunkStream(object): """Call the appropriate chunk handler""" logger.debug("STREAM %r %s %s", cid, pos, length) - return getattr(self, "chunk_" + cid.decode('ascii'))(pos, length) + return getattr(self, "chunk_" + cid.decode("ascii"))(pos, length) def crc(self, cid, data): """Read and verify checksum""" @@ -155,11 +155,9 @@ class ChunkStream(object): crc1 = _crc32(data, _crc32(cid)) crc2 = i32(self.fp.read(4)) if crc1 != crc2: - raise SyntaxError("broken PNG file (bad header checksum in %r)" - % cid) + raise SyntaxError("broken PNG file (bad header checksum in %r)" % cid) except struct.error: - raise SyntaxError("broken PNG file (incomplete checksum in %r)" - % cid) + raise SyntaxError("broken PNG file (incomplete checksum in %r)" % cid) def crc_skip(self, cid, data): """Read checksum. Used if the C module is not present""" @@ -177,7 +175,7 @@ class ChunkStream(object): try: cid, pos, length = self.read() except struct.error: - raise IOError("truncated PNG file") + raise OSError("truncated PNG file") if cid == endchunk: break @@ -193,8 +191,9 @@ class iTXt(str): keeping their extra information """ + @staticmethod - def __new__(cls, text, lang, tkey): + def __new__(cls, text, lang=None, tkey=None): """ :param cls: the class to use when creating the instance :param text: value for this key @@ -208,7 +207,7 @@ class iTXt(str): return self -class PngInfo(object): +class PngInfo: """ PNG chunk container (for use with save(pnginfo=)) @@ -248,11 +247,12 @@ class PngInfo(object): tkey = tkey.encode("utf-8", "strict") if zip: - self.add(b"iTXt", key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + - zlib.compress(value)) + self.add( + b"iTXt", + key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value), + ) else: - self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + - value) + self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) def add_text(self, key, value, zip=False): """Appends a text chunk. @@ -269,12 +269,12 @@ class PngInfo(object): # The tEXt chunk stores latin-1 text if not isinstance(value, bytes): try: - value = value.encode('latin-1', 'strict') + value = value.encode("latin-1", "strict") except UnicodeError: return self.add_itxt(key, value, zip=zip) if not isinstance(key, bytes): - key = key.encode('latin-1', 'strict') + key = key.encode("latin-1", "strict") if zip: self.add(b"zTXt", key + b"\0\0" + zlib.compress(value)) @@ -285,11 +285,10 @@ class PngInfo(object): # -------------------------------------------------------------------- # PNG image stream (IHDR/IEND) + class PngStream(ChunkStream): - def __init__(self, fp): - - ChunkStream.__init__(self, fp) + super().__init__(fp) # local copies of Image attributes self.im_info = {} @@ -305,8 +304,10 @@ class PngStream(ChunkStream): def check_text_memory(self, chunklen): self.text_memory += chunklen if self.text_memory > MAX_TEXT_MEMORY: - raise ValueError("Too much memory used in text chunks: " - "%s>MAX_TEXT_MEMORY" % self.text_memory) + raise ValueError( + "Too much memory used in text chunks: %s>MAX_TEXT_MEMORY" + % self.text_memory + ) def chunk_iCCP(self, pos, length): @@ -322,10 +323,11 @@ class PngStream(ChunkStream): logger.debug("Compression method %s", i8(s[i])) comp_method = i8(s[i]) if comp_method != 0: - raise SyntaxError("Unknown compression method %s in iCCP chunk" % - comp_method) + raise SyntaxError( + "Unknown compression method %s in iCCP chunk" % comp_method + ) try: - icc_profile = _safe_zlib_decompress(s[i+2:]) + icc_profile = _safe_zlib_decompress(s[i + 2 :]) except ValueError: if ImageFile.LOAD_TRUNCATED_IMAGES: icc_profile = None @@ -354,7 +356,7 @@ class PngStream(ChunkStream): def chunk_IDAT(self, pos, length): # image data - self.im_tile = [("zip", (0, 0)+self.im_size, pos, self.im_rawmode)] + self.im_tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] self.im_idat = length raise EOFError @@ -386,7 +388,7 @@ class PngStream(ChunkStream): # otherwise, we have a byte string with one alpha value # for each palette entry self.im_info["transparency"] = s - elif self.im_mode == "L": + elif self.im_mode in ("1", "L", "I"): self.im_info["transparency"] = i16(s) elif self.im_mode == "RGB": self.im_info["transparency"] = i16(s), i16(s[2:]), i16(s[4:]) @@ -403,8 +405,8 @@ class PngStream(ChunkStream): # WP x,y, Red x,y, Green x,y Blue x,y s = ImageFile._safe_read(self.fp, length) - raw_vals = struct.unpack('>%dI' % (len(s) // 4), s) - self.im_info['chromaticity'] = tuple(elt/100000.0 for elt in raw_vals) + raw_vals = struct.unpack(">%dI" % (len(s) // 4), s) + self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) return s def chunk_sRGB(self, pos, length): @@ -415,7 +417,7 @@ class PngStream(ChunkStream): # 3 absolute colorimetric s = ImageFile._safe_read(self.fp, length) - self.im_info['srgb'] = i8(s) + self.im_info["srgb"] = i8(s) return s def chunk_pHYs(self, pos, length): @@ -442,9 +444,8 @@ class PngStream(ChunkStream): k = s v = b"" if k: - if py3: - k = k.decode('latin-1', 'strict') - v = v.decode('latin-1', 'replace') + k = k.decode("latin-1", "strict") + v = v.decode("latin-1", "replace") self.im_info[k] = self.im_text[k] = v self.check_text_memory(len(v)) @@ -465,8 +466,9 @@ class PngStream(ChunkStream): else: comp_method = 0 if comp_method != 0: - raise SyntaxError("Unknown compression method %s in zTXt chunk" % - comp_method) + raise SyntaxError( + "Unknown compression method %s in zTXt chunk" % comp_method + ) try: v = _safe_zlib_decompress(v[1:]) except ValueError: @@ -478,9 +480,8 @@ class PngStream(ChunkStream): v = b"" if k: - if py3: - k = k.decode('latin-1', 'strict') - v = v.decode('latin-1', 'replace') + k = k.decode("latin-1", "strict") + v = v.decode("latin-1", "replace") self.im_info[k] = self.im_text[k] = v self.check_text_memory(len(v)) @@ -515,30 +516,35 @@ 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)) return s + def chunk_eXIf(self, pos, length): + s = ImageFile._safe_read(self.fp, length) + self.im_info["exif"] = b"Exif\x00\x00" + s + return s + # APNG chunks def chunk_acTL(self, pos, length): s = ImageFile._safe_read(self.fp, length) - self.im_custom_mimetype = 'image/apng' + self.im_custom_mimetype = "image/apng" return s # -------------------------------------------------------------------- # PNG reader + def _accept(prefix): return prefix[:8] == _MAGIC @@ -546,6 +552,7 @@ def _accept(prefix): ## # Image plugin for PNG images. + class PngImageFile(ImageFile.ImageFile): format = "PNG" @@ -596,7 +603,7 @@ class PngImageFile(ImageFile.ImageFile): rawmode, data = self.png.im_palette self.palette = ImagePalette.raw(rawmode, data) - self.__idat = length # used by load_read() + self.__prepare_idat = length # used by load_prepare() @property def text(self): @@ -629,6 +636,7 @@ class PngImageFile(ImageFile.ImageFile): if self.info.get("interlace"): self.decoderconfig = self.decoderconfig + (1,) + self.__idat = self.__prepare_idat # used by load_read() ImageFile.ImageFile.load_prepare(self) def load_read(self, read_bytes): @@ -683,25 +691,38 @@ class PngImageFile(ImageFile.ImageFile): self.png.close() self.png = None + def _getexif(self): + if "exif" not in self.info: + self.load() + if "exif" not in self.info: + return None + return dict(self.getexif()) + + def getexif(self): + if "exif" not in self.info: + self.load() + return ImageFile.ImageFile.getexif(self) + # -------------------------------------------------------------------- # PNG writer _OUTMODES = { # supported PIL modes, and corresponding rawmodes/bits/color combinations - "1": ("1", b'\x01\x00'), - "L;1": ("L;1", b'\x01\x00'), - "L;2": ("L;2", b'\x02\x00'), - "L;4": ("L;4", b'\x04\x00'), - "L": ("L", b'\x08\x00'), - "LA": ("LA", b'\x08\x04'), - "I": ("I;16B", b'\x10\x00'), - "P;1": ("P;1", b'\x01\x03'), - "P;2": ("P;2", b'\x02\x03'), - "P;4": ("P;4", b'\x04\x03'), - "P": ("P", b'\x08\x03'), - "RGB": ("RGB", b'\x08\x02'), - "RGBA": ("RGBA", b'\x08\x06'), + "1": ("1", b"\x01\x00"), + "L;1": ("L;1", b"\x01\x00"), + "L;2": ("L;2", b"\x02\x00"), + "L;4": ("L;4", b"\x04\x00"), + "L": ("L", b"\x08\x00"), + "LA": ("LA", b"\x08\x04"), + "I": ("I;16B", b"\x10\x00"), + "I;16": ("I;16B", b"\x10\x00"), + "P;1": ("P;1", b"\x01\x03"), + "P;2": ("P;2", b"\x02\x03"), + "P;4": ("P;4", b"\x04\x03"), + "P": ("P", b"\x08\x03"), + "RGB": ("RGB", b"\x08\x02"), + "RGBA": ("RGBA", b"\x08\x06"), } @@ -716,7 +737,7 @@ def putchunk(fp, cid, *data): fp.write(o32(crc)) -class _idat(object): +class _idat: # wrap output from the encoder in IDAT chunks def __init__(self, fp, chunk): @@ -742,7 +763,7 @@ def _save(im, fp, filename, chunk=putchunk): else: # check palette contents if im.palette: - colors = max(min(len(im.palette.getdata()[1])//3, 256), 2) + colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 2) else: colors = 256 @@ -758,28 +779,34 @@ def _save(im, fp, filename, chunk=putchunk): mode = "%s;%d" % (mode, bits) # encoder options - im.encoderconfig = (im.encoderinfo.get("optimize", False), - im.encoderinfo.get("compress_level", -1), - im.encoderinfo.get("compress_type", -1), - im.encoderinfo.get("dictionary", b"")) + im.encoderconfig = ( + im.encoderinfo.get("optimize", False), + im.encoderinfo.get("compress_level", -1), + im.encoderinfo.get("compress_type", -1), + im.encoderinfo.get("dictionary", b""), + ) # get the corresponding PNG mode 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 fp.write(_MAGIC) - chunk(fp, b"IHDR", - o32(im.size[0]), o32(im.size[1]), # 0: size - mode, # 8: depth/type - b'\0', # 10: compression - b'\0', # 11: filter category - b'\0') # 12: interlace flag + chunk( + fp, + b"IHDR", + o32(im.size[0]), # 0: size + o32(im.size[1]), + mode, # 8: depth/type + b"\0", # 10: compression + b"\0", # 11: filter category + b"\0", # 12: interlace flag + ) chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"] @@ -813,23 +840,22 @@ def _save(im, fp, filename, chunk=putchunk): palette_byte_number = (2 ** bits) * 3 palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] while len(palette_bytes) < palette_byte_number: - palette_bytes += b'\0' + palette_bytes += b"\0" chunk(fp, b"PLTE", palette_bytes) - transparency = im.encoderinfo.get('transparency', - im.info.get('transparency', None)) + transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None)) if transparency or transparency == 0: if im.mode == "P": # limit to actual palette size - alpha_bytes = 2**bits + alpha_bytes = 2 ** bits if isinstance(transparency, bytes): chunk(fp, b"tRNS", transparency[:alpha_bytes]) else: transparency = max(0, min(255, transparency)) - alpha = b'\xFF' * transparency + b'\0' + alpha = b"\xFF" * transparency + b"\0" chunk(fp, b"tRNS", alpha[:alpha_bytes]) - elif im.mode == "L": + elif im.mode in ("1", "L", "I"): transparency = max(0, min(65535, transparency)) chunk(fp, b"tRNS", o16(transparency)) elif im.mode == "RGB": @@ -839,19 +865,22 @@ 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") - alpha_bytes = 2**bits + alpha_bytes = 2 ** bits chunk(fp, b"tRNS", alpha[:alpha_bytes]) dpi = im.encoderinfo.get("dpi") if dpi: - chunk(fp, b"pHYs", - o32(int(dpi[0] / 0.0254 + 0.5)), - o32(int(dpi[1] / 0.0254 + 0.5)), - b'\x01') + chunk( + fp, + b"pHYs", + o32(int(dpi[0] / 0.0254 + 0.5)), + o32(int(dpi[1] / 0.0254 + 0.5)), + b"\x01", + ) info = im.encoderinfo.get("pnginfo") if info: @@ -861,8 +890,15 @@ def _save(im, fp, filename, chunk=putchunk): chunks.remove(cid) chunk(fp, cid, data) - ImageFile._save(im, _idat(fp, chunk), - [("zip", (0, 0)+im.size, 0, rawmode)]) + exif = im.encoderinfo.get("exif", im.info.get("exif")) + if exif: + if isinstance(exif, Image.Exif): + exif = exif.tobytes(8) + if exif.startswith(b"Exif\x00\x00"): + exif = exif[6:] + chunk(fp, b"eXIf", exif) + + ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) chunk(fp, b"IEND", b"") @@ -873,10 +909,11 @@ def _save(im, fp, filename, chunk=putchunk): # -------------------------------------------------------------------- # PNG chunk converter + 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 750454dc5..35a77bafb 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -17,14 +17,10 @@ from . import Image, ImageFile -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.2" - # # -------------------------------------------------------------------- -b_whitespace = b'\x20\x09\x0a\x0b\x0c\x0d' +b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d" MODES = { # standard @@ -36,7 +32,7 @@ MODES = { # PIL extensions (for test purposes only) b"PyP": "P", b"PyRGBA": "RGBA", - b"PyCMYK": "CMYK" + b"PyCMYK": "CMYK", } @@ -47,6 +43,7 @@ def _accept(prefix): ## # Image plugin for PBM, PGM, and PPM images. + class PpmImageFile(ImageFile.ImageFile): format = "PPM" @@ -57,10 +54,10 @@ class PpmImageFile(ImageFile.ImageFile): c = self.fp.read(1) if not c or c in b_whitespace: break - if c > b'\x79': + if c > b"\x79": raise ValueError("Expected ASCII value, found binary") s = s + c - if (len(s) > 9): + if len(s) > 9: raise ValueError("Expected int, got > 9 digits") return s @@ -70,7 +67,14 @@ class PpmImageFile(ImageFile.ImageFile): s = self.fp.read(1) if s != b"P": raise SyntaxError("not a PPM file") - mode = MODES[self._token(s)] + magic_number = self._token(s) + mode = MODES[magic_number] + + self.custom_mimetype = { + b"P4": "image/x-portable-bitmap", + b"P5": "image/x-portable-graymap", + b"P6": "image/x-portable-pixmap", + }.get(magic_number) if mode == "1": self.mode = "1" @@ -85,8 +89,7 @@ class PpmImageFile(ImageFile.ImageFile): if s not in b_whitespace: break if s == b"": - raise ValueError( - "File does not extend beyond magic number") + raise ValueError("File does not extend beyond magic number") if s != b"#": break s = self.fp.readline() @@ -100,32 +103,30 @@ class PpmImageFile(ImageFile.ImageFile): elif ix == 2: # maxgrey if s > 255: - if not mode == 'L': + if not mode == "L": raise ValueError("Too many colors for band: %s" % s) - if s < 2**16: - self.mode = 'I' - rawmode = 'I;16B' + if s < 2 ** 16: + self.mode = "I" + rawmode = "I;16B" else: - self.mode = 'I' - rawmode = 'I;32B' + self.mode = "I" + rawmode = "I;32B" self._size = xsize, ysize - self.tile = [("raw", - (0, 0, xsize, ysize), - self.fp.tell(), - (rawmode, 0, 1))] + self.tile = [("raw", (0, 0, xsize, ysize), self.fp.tell(), (rawmode, 0, 1))] # # -------------------------------------------------------------------- + def _save(im, fp, filename): if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": rawmode, head = "L", b"P5" elif im.mode == "I": - if im.getextrema()[1] < 2**16: + if im.getextrema()[1] < 2 ** 16: rawmode, head = "I;16B", b"P5" else: rawmode, head = "I;32B", b"P5" @@ -134,8 +135,8 @@ 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) - fp.write(head + ("\n%d %d\n" % im.size).encode('ascii')) + 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") if head == b"P5": @@ -145,11 +146,12 @@ def _save(im, fp, filename): fp.write(b"65535\n") elif rawmode == "I;32B": fp.write(b"2147483648\n") - ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, 0, 1))]) + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) # ALTERNATIVE: save via builtin debug function # im._dump(filename) + # # -------------------------------------------------------------------- @@ -157,4 +159,6 @@ def _save(im, fp, filename): Image.register_open(PpmImageFile.format, PpmImageFile, _accept) Image.register_save(PpmImageFile.format, _save) -Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm"]) +Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"]) + +Image.register_mime(PpmImageFile.format, "image/x-portable-anymap") diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index c631f41cb..cceb85c5b 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -16,9 +16,7 @@ # 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 from ._binary import i8, i16be as i16, i32be as i32 @@ -33,13 +31,14 @@ MODES = { (4, 8): ("CMYK", 4), (7, 8): ("L", 1), # FIXME: multilayer (8, 8): ("L", 1), # duotone - (9, 8): ("LAB", 3) + (9, 8): ("LAB", 3), } # --------------------------------------------------------------------. # read PSD images + def _accept(prefix): return prefix[:4] == b"8BPS" @@ -47,10 +46,12 @@ def _accept(prefix): ## # Image plugin for Photoshop images. + class PsdImageFile(ImageFile.ImageFile): format = "PSD" format_description = "Adobe Photoshop" + _close_exclusive_fp_after_loading = False def _open(self): @@ -70,7 +71,7 @@ class PsdImageFile(ImageFile.ImageFile): mode, channels = MODES[(psd_mode, psd_bits)] if channels > psd_channels: - raise IOError("not enough channels") + raise OSError("not enough channels") self.mode = mode self._size = i32(s[18:]), i32(s[14:]) @@ -100,7 +101,7 @@ class PsdImageFile(ImageFile.ImageFile): if not (len(name) & 1): read(1) # padding data = read(i32(read(4))) - if (len(data) & 1): + if len(data) & 1: read(1) # padding self.resources.append((id, name, data)) if id == 1039: # ICC profile @@ -125,7 +126,7 @@ class PsdImageFile(ImageFile.ImageFile): self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels) # keep the file open - self._fp = self.fp + self.__fp = self.fp self.frame = 1 self._min_frame = 1 @@ -143,11 +144,11 @@ class PsdImageFile(ImageFile.ImageFile): # seek to given layer (1..max) try: - name, mode, bbox, tile = self.layers[layer-1] + name, mode, bbox, tile = self.layers[layer - 1] self.mode = mode self.tile = tile self.frame = layer - self.fp = self._fp + self.fp = self.__fp return name, bbox except IndexError: raise EOFError("no such layer") @@ -158,13 +159,21 @@ class PsdImageFile(ImageFile.ImageFile): def load_prepare(self): # create image memory if necessary - if not self.im or\ - self.im.mode != self.mode or self.im.size != self.size: + if not self.im or self.im.mode != self.mode or self.im.size != self.size: self.im = Image.core.fill(self.mode, self.size, 0) # create palette (optional) if self.mode == "P": Image.Image.load(self) + def _close__fp(self): + try: + if self.__fp != self.fp: + self.__fp.close() + except AttributeError: + pass + finally: + self.__fp = None + def _layerinfo(file): # read layerinfo block @@ -211,27 +220,29 @@ def _layerinfo(file): # skip over blend flags and extra information read(12) # filler name = "" - size = i32(read(4)) + size = i32(read(4)) # length of the extra data field combined = 0 if size: + data_end = file.tell() + size + length = i32(read(4)) if length: - file.seek(length - 16, 1) + file.seek(length - 16, io.SEEK_CUR) combined += length + 4 length = i32(read(4)) if length: - file.seek(length, 1) + file.seek(length, io.SEEK_CUR) combined += length + 4 length = i8(read(1)) if length: # Don't know the proper encoding, # Latin-1 should be a good guess - name = read(length).decode('latin-1', 'replace') + name = read(length).decode("latin-1", "replace") combined += length + 1 - file.seek(size - combined, 1) + file.seek(data_end) layers.append((name, mode, (x0, y0, x1, y1))) # get tiles @@ -269,7 +280,7 @@ def _maketile(file, mode, bbox, channels): if mode == "CMYK": layer += ";I" tile.append(("raw", bbox, offset, layer)) - offset = offset + xsize*ysize + offset = offset + xsize * ysize elif compression == 1: # @@ -282,11 +293,9 @@ def _maketile(file, mode, bbox, channels): layer = mode[channel] if mode == "CMYK": layer += ";I" - tile.append( - ("packbits", bbox, offset, layer) - ) + tile.append(("packbits", bbox, offset, layer)) for y in range(ysize): - offset = offset + i16(bytecount[i:i+2]) + offset = offset + i16(bytecount[i : i + 2]) i += 2 file.seek(offset) @@ -296,6 +305,7 @@ def _maketile(file, mode, bbox, channels): return tile + # -------------------------------------------------------------------- # registry diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 5df1d400c..359a94919 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -25,7 +25,6 @@ import sys from cffi import FFI - logger = logging.getLogger(__name__) @@ -41,14 +40,13 @@ 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 - self.image8 = ffi.cast('unsigned char **', vals['image8']) - self.image32 = ffi.cast('int **', vals['image32']) - self.image = ffi.cast('unsigned char **', vals['image']) + self.image8 = ffi.cast("unsigned char **", vals["image8"]) + self.image32 = ffi.cast("int **", vals["image32"]) + self.image = ffi.cast("unsigned char **", vals["image"]) self.xsize, self.ysize = img.im.size # Keep pointer to im object to prevent dereferencing. @@ -75,7 +73,7 @@ class PyAccess(object): :param color: The pixel value. """ if self.readonly: - raise ValueError('Attempt to putpixel a read only image') + raise ValueError("Attempt to putpixel a read only image") (x, y) = xy if x < 0: x = self.xsize + x @@ -83,8 +81,11 @@ class PyAccess(object): y = self.ysize + y (x, y) = self.check_xy((x, y)) - if self._im.mode == "P" and \ - isinstance(color, (list, tuple)) and len(color) in [3, 4]: + if ( + self._im.mode == "P" + and isinstance(color, (list, tuple)) + and len(color) in [3, 4] + ): # RGB or RGBA value for a P image color = self._palette.getcolor(color) @@ -115,12 +116,13 @@ class PyAccess(object): def check_xy(self, xy): (x, y) = xy if not (0 <= x < self.xsize and 0 <= y < self.ysize): - raise ValueError('pixel location out of range') + raise ValueError("pixel location out of range") return xy class _PyAccess32_2(PyAccess): """ PA, LA, stored in first and last bytes of a 32 bit word """ + def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) @@ -156,6 +158,7 @@ class _PyAccess32_3(PyAccess): class _PyAccess32_4(PyAccess): """ RGBA etc, all 4 bytes of a 32 bit word """ + def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) @@ -174,6 +177,7 @@ class _PyAccess32_4(PyAccess): class _PyAccess8(PyAccess): """ 1, L, P, 8 bit images stored as uint8 """ + def _post_init(self, *args, **kwargs): self.pixels = self.image8 @@ -191,8 +195,9 @@ class _PyAccess8(PyAccess): class _PyAccessI16_N(PyAccess): """ I;16 access, native bitendian without conversion """ + def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast('unsigned short **', self.image) + self.pixels = ffi.cast("unsigned short **", self.image) def get_pixel(self, x, y): return self.pixels[y][x] @@ -208,8 +213,9 @@ class _PyAccessI16_N(PyAccess): class _PyAccessI16_L(PyAccess): """ I;16L access, with conversion """ + def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast('struct Pixel_I16 **', self.image) + self.pixels = ffi.cast("struct Pixel_I16 **", self.image) def get_pixel(self, x, y): pixel = self.pixels[y][x] @@ -228,8 +234,9 @@ class _PyAccessI16_L(PyAccess): class _PyAccessI16_B(PyAccess): """ I;16B access, with conversion """ + def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast('struct Pixel_I16 **', self.image) + self.pixels = ffi.cast("struct Pixel_I16 **", self.image) def get_pixel(self, x, y): pixel = self.pixels[y][x] @@ -248,6 +255,7 @@ class _PyAccessI16_B(PyAccess): class _PyAccessI32_N(PyAccess): """ Signed Int32 access, native endian """ + def _post_init(self, *args, **kwargs): self.pixels = self.image32 @@ -260,15 +268,15 @@ class _PyAccessI32_N(PyAccess): class _PyAccessI32_Swap(PyAccess): """ I;32L/B access, with byteswapping conversion """ + def _post_init(self, *args, **kwargs): self.pixels = self.image32 def reverse(self, i): - orig = ffi.new('int *', i) - chars = ffi.cast('unsigned char *', orig) - chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], \ - chars[1], chars[0] - return ffi.cast('int *', chars)[0] + orig = ffi.new("int *", i) + chars = ffi.cast("unsigned char *", orig) + chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0] + return ffi.cast("int *", chars)[0] def get_pixel(self, x, y): return self.reverse(self.pixels[y][x]) @@ -279,8 +287,9 @@ class _PyAccessI32_Swap(PyAccess): class _PyAccessF(PyAccess): """ 32 bit float access """ + def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast('float **', self.image32) + self.pixels = ffi.cast("float **", self.image32) def get_pixel(self, x, y): return self.pixels[y][x] @@ -294,38 +303,39 @@ class _PyAccessF(PyAccess): self.pixels[y][x] = color[0] -mode_map = {'1': _PyAccess8, - 'L': _PyAccess8, - 'P': _PyAccess8, - 'LA': _PyAccess32_2, - 'La': _PyAccess32_2, - 'PA': _PyAccess32_2, - 'RGB': _PyAccess32_3, - 'LAB': _PyAccess32_3, - 'HSV': _PyAccess32_3, - 'YCbCr': _PyAccess32_3, - 'RGBA': _PyAccess32_4, - 'RGBa': _PyAccess32_4, - 'RGBX': _PyAccess32_4, - 'CMYK': _PyAccess32_4, - 'F': _PyAccessF, - 'I': _PyAccessI32_N, - } +mode_map = { + "1": _PyAccess8, + "L": _PyAccess8, + "P": _PyAccess8, + "LA": _PyAccess32_2, + "La": _PyAccess32_2, + "PA": _PyAccess32_2, + "RGB": _PyAccess32_3, + "LAB": _PyAccess32_3, + "HSV": _PyAccess32_3, + "YCbCr": _PyAccess32_3, + "RGBA": _PyAccess32_4, + "RGBa": _PyAccess32_4, + "RGBX": _PyAccess32_4, + "CMYK": _PyAccess32_4, + "F": _PyAccessF, + "I": _PyAccessI32_N, +} -if sys.byteorder == 'little': - mode_map['I;16'] = _PyAccessI16_N - mode_map['I;16L'] = _PyAccessI16_N - mode_map['I;16B'] = _PyAccessI16_B +if sys.byteorder == "little": + mode_map["I;16"] = _PyAccessI16_N + mode_map["I;16L"] = _PyAccessI16_N + mode_map["I;16B"] = _PyAccessI16_B - mode_map['I;32L'] = _PyAccessI32_N - mode_map['I;32B'] = _PyAccessI32_Swap + mode_map["I;32L"] = _PyAccessI32_N + mode_map["I;32B"] = _PyAccessI32_Swap else: - mode_map['I;16'] = _PyAccessI16_L - mode_map['I;16L'] = _PyAccessI16_L - mode_map['I;16B'] = _PyAccessI16_N + mode_map["I;16"] = _PyAccessI16_L + mode_map["I;16L"] = _PyAccessI16_L + mode_map["I;16B"] = _PyAccessI16_N - mode_map['I;32L'] = _PyAccessI32_Swap - mode_map['I;32B'] = _PyAccessI32_N + mode_map["I;32L"] = _PyAccessI32_Swap + mode_map["I;32B"] = _PyAccessI32_N def new(img, readonly=False): diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 37867bdb7..ddd3de379 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -22,16 +22,11 @@ # -from . import Image, ImageFile -from ._binary import i8, o8, i16be as i16 -from ._util import py3 -import struct import os +import struct - -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.3" +from . import Image, ImageFile +from ._binary import i8, i16be as i16, o8 def _accept(prefix): @@ -46,7 +41,7 @@ MODES = { (1, 3, 3): "RGB", (2, 3, 3): "RGB;16B", (1, 3, 4): "RGBA", - (2, 3, 4): "RGBA;16B" + (2, 3, 4): "RGBA;16B", } @@ -100,8 +95,8 @@ class SgiImageFile(ImageFile.ImageFile): self._size = xsize, ysize self.mode = rawmode.split(";")[0] - if self.mode == 'RGB': - self.custom_mimetype = 'image/rgb' + if self.mode == "RGB": + self.custom_mimetype = "image/rgb" # orientation -1 : scanlines begins at the bottom-left corner orientation = -1 @@ -110,19 +105,21 @@ class SgiImageFile(ImageFile.ImageFile): if compression == 0: pagesize = xsize * ysize * bpc if bpc == 2: - self.tile = [("SGI16", (0, 0) + self.size, - headlen, (self.mode, 0, orientation))] + self.tile = [ + ("SGI16", (0, 0) + self.size, headlen, (self.mode, 0, orientation)) + ] else: self.tile = [] offset = headlen for layer in self.mode: self.tile.append( - ("raw", (0, 0) + self.size, - offset, (layer, 0, orientation))) + ("raw", (0, 0) + self.size, offset, (layer, 0, orientation)) + ) offset += pagesize elif compression == 1: - self.tile = [("sgi_rle", (0, 0) + self.size, - headlen, (rawmode, orientation, bpc))] + self.tile = [ + ("sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc)) + ] def _save(im, fp, filename): @@ -161,8 +158,11 @@ 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()))) + raise ValueError( + "incorrect number of bands in SGI write: {} vs {}".format( + z, len(im.getbands()) + ) + ) # Minimum Byte value pinmin = 0 @@ -170,31 +170,30 @@ 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)) + fp.write(struct.pack(">h", magicNumber)) fp.write(o8(rle)) fp.write(o8(bpc)) - fp.write(struct.pack('>H', dim)) - fp.write(struct.pack('>H', x)) - fp.write(struct.pack('>H', y)) - fp.write(struct.pack('>H', z)) - fp.write(struct.pack('>l', pinmin)) - fp.write(struct.pack('>l', pinmax)) - fp.write(struct.pack('4s', b'')) # dummy - fp.write(struct.pack('79s', imgName)) # truncates to 79 chars - fp.write(struct.pack('s', b'')) # force null byte after imgname - fp.write(struct.pack('>l', colormap)) - fp.write(struct.pack('404s', b'')) # dummy + fp.write(struct.pack(">H", dim)) + fp.write(struct.pack(">H", x)) + fp.write(struct.pack(">H", y)) + fp.write(struct.pack(">H", z)) + fp.write(struct.pack(">l", pinmin)) + fp.write(struct.pack(">l", pinmax)) + fp.write(struct.pack("4s", b"")) # dummy + fp.write(struct.pack("79s", imgName)) # truncates to 79 chars + fp.write(struct.pack("s", b"")) # force null byte after imgname + fp.write(struct.pack(">l", colormap)) + fp.write(struct.pack("404s", b"")) # dummy - rawmode = 'L' + rawmode = "L" if bpc == 2: - rawmode = 'L;16B' + rawmode = "L;16B" for channel in im.split(): - fp.write(channel.tobytes('raw', rawmode, 0, orientation)) + fp.write(channel.tobytes("raw", rawmode, 0, orientation)) fp.close() @@ -209,13 +208,15 @@ class SGI16Decoder(ImageFile.PyDecoder): self.fd.seek(512) for band in range(zsize): - channel = Image.new('L', (self.state.xsize, self.state.ysize)) - channel.frombytes(self.fd.read(2 * pagesize), 'raw', - 'L;16B', stride, orientation) + channel = Image.new("L", (self.state.xsize, self.state.ysize)) + channel.frombytes( + self.fd.read(2 * pagesize), "raw", "L;16B", stride, orientation + ) self.im.putband(channel.im, band) return -1, 0 + # # registry @@ -225,7 +226,6 @@ Image.register_open(SgiImageFile.format, SgiImageFile, _accept) Image.register_save(SgiImageFile.format, _save) Image.register_mime(SgiImageFile.format, "image/sgi") -Image.register_extensions(SgiImageFile.format, - [".bw", ".rgb", ".rgba", ".sgi"]) +Image.register_extensions(SgiImageFile.format, [".bw", ".rgb", ".rgba", ".sgi"]) # End of file diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 4d5aa3f0e..dd0620c14 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -32,19 +32,17 @@ # Details about the Spider image format: # https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html # - -from __future__ import print_function - -from PIL import Image, ImageFile import os import struct import sys +from PIL import Image, ImageFile + def isInt(f): try: i = int(f) - if f-i == 0: + if f - i == 0: return 1 else: return 0 @@ -60,8 +58,9 @@ iforms = [1, 3, -11, -12, -21, -22] # Returns no. of bytes in the header, if it is a valid Spider header, # otherwise returns 0 + def isSpiderHeader(t): - h = (99,) + t # add 1 value so can use spider header index start=1 + h = (99,) + t # add 1 value so can use spider header index start=1 # header values 1,2,5,12,13,22,23 should be integers for i in [1, 2, 5, 12, 13, 22, 23]: if not isInt(h[i]): @@ -71,9 +70,9 @@ def isSpiderHeader(t): if iform not in iforms: return 0 # check other header values - labrec = int(h[13]) # no. records in file header - labbyt = int(h[22]) # total no. of bytes in header - lenbyt = int(h[23]) # record length in bytes + labrec = int(h[13]) # no. records in file header + labbyt = int(h[22]) # total no. of bytes in header + lenbyt = int(h[23]) # record length in bytes if labbyt != (labrec * lenbyt): return 0 # looks like a valid header @@ -81,12 +80,12 @@ def isSpiderHeader(t): def isSpiderImage(filename): - with open(filename, 'rb') as fp: - f = fp.read(92) # read 23 * 4 bytes - t = struct.unpack('>23f', f) # try big-endian first + with open(filename, "rb") as fp: + f = fp.read(92) # read 23 * 4 bytes + t = struct.unpack(">23f", f) # try big-endian first hdrlen = isSpiderHeader(t) if hdrlen == 0: - t = struct.unpack('<23f', f) # little-endian + t = struct.unpack("<23f", f) # little-endian hdrlen = isSpiderHeader(t) return hdrlen @@ -104,18 +103,18 @@ class SpiderImageFile(ImageFile.ImageFile): try: self.bigendian = 1 - t = struct.unpack('>27f', f) # try big-endian first + t = struct.unpack(">27f", f) # try big-endian first hdrlen = isSpiderHeader(t) if hdrlen == 0: self.bigendian = 0 - t = struct.unpack('<27f', f) # little-endian + t = struct.unpack("<27f", f) # little-endian hdrlen = isSpiderHeader(t) if hdrlen == 0: raise SyntaxError("not a valid Spider file") except struct.error: raise SyntaxError("not a valid Spider file") - h = (99,) + t # add 1 value : spider header index starts at 1 + h = (99,) + t # add 1 value : spider header index starts at 1 iform = int(h[5]) if iform != 1: raise SyntaxError("not a Spider 2D image") @@ -149,9 +148,7 @@ class SpiderImageFile(ImageFile.ImageFile): self.rawmode = "F;32F" self.mode = "F" - self.tile = [ - ("raw", (0, 0) + self.size, offset, - (self.rawmode, 0, 1))] + self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))] self.__fp = self.fp # FIXME: hack @property @@ -184,13 +181,14 @@ class SpiderImageFile(ImageFile.ImageFile): (minimum, maximum) = self.getextrema() m = 1 if maximum != minimum: - m = depth / (maximum-minimum) + m = depth / (maximum - minimum) b = -m * minimum return self.point(lambda i, m=m, b=b: i * m + b).convert("L") # returns a ImageTk.PhotoImage object, after rescaling to 0..255 def tkPhotoImage(self): from PIL import ImageTk + return ImageTk.PhotoImage(self.convert2byte(), palette=256) def _close__fp(self): @@ -208,7 +206,7 @@ class SpiderImageFile(ImageFile.ImageFile): # given a list of filenames, return a list of images def loadImageSeries(filelist=None): - """create a list of Image.images for use in montage""" + """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" if filelist is None or len(filelist) < 1: return @@ -218,12 +216,13 @@ 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") continue - im.info['filename'] = img + im.info["filename"] = img imglist.append(im) return imglist @@ -231,10 +230,11 @@ def loadImageSeries(filelist=None): # -------------------------------------------------------------------- # For saving images in Spider format + def makeSpiderHeader(im): nsam, nrow = im.size lenbyt = nsam * 4 # There are labrec records in the header - labrec = 1024 / lenbyt + labrec = int(1024 / lenbyt) if 1024 % lenbyt != 0: labrec += 1 labbyt = labrec * lenbyt @@ -247,10 +247,10 @@ def makeSpiderHeader(im): return [] # NB these are Fortran indices - hdr[1] = 1.0 # nslice (=1 for an image) - hdr[2] = float(nrow) # number of rows per slice - hdr[5] = 1.0 # iform for 2D image - hdr[12] = float(nsam) # number of pixels per line + hdr[1] = 1.0 # nslice (=1 for an image) + hdr[2] = float(nrow) # number of rows per slice + hdr[5] = 1.0 # iform for 2D image + hdr[12] = float(nsam) # number of pixels per line hdr[13] = float(labrec) # number of records in file header hdr[22] = float(labbyt) # total number of bytes in header hdr[23] = float(lenbyt) # record length in bytes @@ -261,23 +261,23 @@ def makeSpiderHeader(im): # pack binary data into a string hdrstr = [] for v in hdr: - hdrstr.append(struct.pack('f', v)) + hdrstr.append(struct.pack("f", v)) return hdrstr def _save(im, fp, filename): if im.mode[0] != "F": - im = im.convert('F') + im = im.convert("F") 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) rawmode = "F;32NF" # 32-bit native floating point - ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, 0, 1))]) + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) def _save_spider(im, fp, filename): @@ -286,6 +286,7 @@ def _save_spider(im, fp, filename): Image.register_extension(SpiderImageFile.format, ext) _save(im, fp, filename) + # -------------------------------------------------------------------- @@ -308,7 +309,7 @@ if __name__ == "__main__": print("format: " + str(im.format)) print("size: " + str(im.size)) print("mode: " + str(im.mode)) - print("max, min: ", end=' ') + print("max, min: ", end=" ") print(im.getextrema()) if len(sys.argv) > 2: @@ -317,6 +318,7 @@ if __name__ == "__main__": # perform some image operation im = im.transpose(Image.FLIP_LEFT_RIGHT) print( - "saving a flipped version of %s as %s " % - (os.path.basename(filename), outfile)) + "saving a flipped version of %s as %s " + % (os.path.basename(filename), outfile) + ) im.save(outfile, SpiderImageFile.format) diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index 485099fd4..fd7ca8a40 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -20,18 +20,15 @@ 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 + return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 ## # Image plugin for Sun raster files. + class SunImageFile(ImageFile.ImageFile): format = "SUN" @@ -56,7 +53,7 @@ class SunImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(32) - if i32(s) != 0x59a66a95: + if i32(s) != 0x59A66A95: raise SyntaxError("not an SUN raster file") offset = 32 @@ -82,9 +79,9 @@ class SunImageFile(ImageFile.ImageFile): self.mode, rawmode = "RGB", "BGR" elif depth == 32: if file_type == 3: - self.mode, rawmode = 'RGB', 'RGBX' + self.mode, rawmode = "RGB", "RGBX" else: - self.mode, rawmode = 'RGB', 'BGRX' + self.mode, rawmode = "RGB", "BGRX" else: raise SyntaxError("Unsupported Mode/Bit Depth") @@ -96,11 +93,10 @@ class SunImageFile(ImageFile.ImageFile): raise SyntaxError("Unsupported Palette Type") offset = offset + palette_length - self.palette = ImagePalette.raw("RGB;L", - self.fp.read(palette_length)) + self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) if self.mode == "L": self.mode = "P" - rawmode = rawmode.replace('L', 'P') + rawmode = rawmode.replace("L", "P") # 16 bit boundaries on stride stride = ((self.size[0] * depth + 15) // 16) * 2 @@ -124,11 +120,12 @@ class SunImageFile(ImageFile.ImageFile): # (https://www.fileformat.info/format/sunraster/egff.htm) if file_type in (0, 1, 3, 4, 5): - self.tile = [("raw", (0, 0)+self.size, offset, (rawmode, stride))] + self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride))] elif file_type == 2: - self.tile = [("sun_rle", (0, 0)+self.size, offset, rawmode)] + self.tile = [("sun_rle", (0, 0) + self.size, offset, rawmode)] else: - raise SyntaxError('Unsupported Sun Raster file type') + raise SyntaxError("Unsupported Sun Raster file type") + # # registry diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 89957fba4..ede646453 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -14,16 +14,16 @@ # See the README file for information on usage and redistribution. # -import sys -from . import ContainerIO +import io +from . import ContainerIO ## # A file object that provides read access to a given member of a TAR # file. -class TarIO(ContainerIO.ContainerIO): +class TarIO(ContainerIO.ContainerIO): def __init__(self, tarfile, file): """ Create file object. @@ -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') + 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] @@ -51,10 +51,10 @@ class TarIO(ContainerIO.ContainerIO): if file == name: break - self.fh.seek((size + 511) & (~511), 1) + 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): @@ -63,9 +63,5 @@ class TarIO(ContainerIO.ContainerIO): def __exit__(self, *args): self.close() - if sys.version_info.major >= 3: - def __del__(self): - self.close() - def close(self): self.fh.close() diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 26b1e9c60..fd71e545d 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -17,15 +17,10 @@ # -from . import Image, ImageFile, ImagePalette -from ._binary import i8, i16le as i16, o8, o16le as o16 - import warnings -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.3" - +from . import Image, ImageFile, ImagePalette +from ._binary import i8, i16le as i16, o8, o16le as o16 # # -------------------------------------------------------------------- @@ -34,9 +29,9 @@ __version__ = "0.3" MODES = { # map imagetype/depth to rawmode - (1, 8): "P", - (3, 1): "1", - (3, 8): "L", + (1, 8): "P", + (3, 1): "1", + (3, 8): "L", (3, 16): "LA", (2, 16): "BGR;5", (2, 24): "BGR", @@ -47,6 +42,7 @@ MODES = { ## # Image plugin for Targa files. + class TgaImageFile(ImageFile.ImageFile): format = "TGA" @@ -69,9 +65,12 @@ class TgaImageFile(ImageFile.ImageFile): self._size = i16(s[12:]), i16(s[14:]) # validate header fields - if colormaptype not in (0, 1) or\ - self.size[0] <= 0 or self.size[1] <= 0 or\ - depth not in (1, 8, 16, 24, 32): + if ( + colormaptype not in (0, 1) + or self.size[0] <= 0 + or self.size[1] <= 0 + or depth not in (1, 8, 16, 24, 32) + ): raise SyntaxError("not a TGA file") # image mode @@ -112,27 +111,43 @@ class TgaImageFile(ImageFile.ImageFile): start, size, mapdepth = i16(s[3:]), i16(s[5:]), i16(s[7:]) if mapdepth == 16: self.palette = ImagePalette.raw( - "BGR;16", b"\0"*2*start + self.fp.read(2*size)) + "BGR;16", b"\0" * 2 * start + self.fp.read(2 * size) + ) elif mapdepth == 24: self.palette = ImagePalette.raw( - "BGR", b"\0"*3*start + self.fp.read(3*size)) + "BGR", b"\0" * 3 * start + self.fp.read(3 * size) + ) elif mapdepth == 32: self.palette = ImagePalette.raw( - "BGRA", b"\0"*4*start + self.fp.read(4*size)) + "BGRA", b"\0" * 4 * start + self.fp.read(4 * size) + ) # setup tile descriptor try: rawmode = MODES[(imagetype & 7, depth)] if imagetype & 8: # compressed - self.tile = [("tga_rle", (0, 0)+self.size, - self.fp.tell(), (rawmode, orientation, depth))] + self.tile = [ + ( + "tga_rle", + (0, 0) + self.size, + self.fp.tell(), + (rawmode, orientation, depth), + ) + ] else: - self.tile = [("raw", (0, 0)+self.size, - self.fp.tell(), (rawmode, 0, orientation))] + self.tile = [ + ( + "raw", + (0, 0) + self.size, + self.fp.tell(), + (rawmode, 0, orientation), + ) + ] except KeyError: pass # cannot decode + # # -------------------------------------------------------------------- # Write TGA file @@ -153,19 +168,17 @@ 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"] else: - compression = im.encoderinfo.get("compression", - im.info.get("compression")) + compression = im.encoderinfo.get("compression", im.info.get("compression")) rle = compression == "tga_rle" if rle: imagetype += 8 - id_section = im.encoderinfo.get("id_section", - im.info.get("id_section", "")) + id_section = im.encoderinfo.get("id_section", im.info.get("id_section", "")) id_len = len(id_section) if id_len > 255: id_len = 255 @@ -182,23 +195,24 @@ def _save(im, fp, filename): else: flags = 0 - orientation = im.encoderinfo.get("orientation", - im.info.get("orientation", -1)) + orientation = im.encoderinfo.get("orientation", im.info.get("orientation", -1)) if orientation > 0: flags = flags | 0x20 - fp.write(o8(id_len) + - o8(colormaptype) + - o8(imagetype) + - o16(colormapfirst) + - o16(colormaplength) + - o8(colormapentry) + - o16(0) + - o16(0) + - o16(im.size[0]) + - o16(im.size[1]) + - o8(bits) + - o8(flags)) + fp.write( + o8(id_len) + + o8(colormaptype) + + o8(imagetype) + + o16(colormapfirst) + + o16(colormaplength) + + o8(colormapentry) + + o16(0) + + o16(0) + + o16(im.size[0]) + + o16(im.size[1]) + + o8(bits) + + o8(flags) + ) if id_section: fp.write(id_section) @@ -208,16 +222,17 @@ def _save(im, fp, filename): if rle: ImageFile._save( - im, - fp, - [("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))]) + im, fp, [("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))] + ) else: ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))]) + im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))] + ) # write targa version 2 footer fp.write(b"\000" * 8 + b"TRUEVISION-XFILE." + b"\000") + # # -------------------------------------------------------------------- # Registry @@ -226,4 +241,6 @@ def _save(im, fp, filename): Image.register_open(TgaImageFile.format, TgaImageFile) Image.register_save(TgaImageFile.format, _save) -Image.register_extension(TgaImageFile.format, ".tga") +Image.register_extensions(TgaImageFile.format, [".tga", ".icb", ".vda", ".vst"]) + +Image.register_mime(TgaImageFile.format, "image/x-tga") diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index a0609bb4f..47b69a003 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -38,37 +38,20 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import division, print_function - -from . import Image, ImageFile, ImagePalette, TiffTags -from ._binary import i8, o8 -from ._util import py3 - -from fractions import Fraction -from numbers import Number, Rational - +import distutils.version import io import itertools import os import struct -import sys import warnings -import distutils.version +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 .TiffTags import TYPES -try: - # Python 3 - from collections.abc import MutableMapping -except ImportError: - # Python 2.7 - from collections import MutableMapping - - -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "1.3.5" DEBUG = False # Needs to be merged with the new logging approach. # Set these to true to force use of libtiff for reading or writing. @@ -99,6 +82,7 @@ X_RESOLUTION = 282 Y_RESOLUTION = 283 PLANAR_CONFIGURATION = 284 RESOLUTION_UNIT = 296 +TRANSFERFUNCTION = 301 SOFTWARE = 305 DATE_TIME = 306 ARTIST = 315 @@ -108,12 +92,14 @@ TILEOFFSETS = 324 EXTRASAMPLES = 338 SAMPLEFORMAT = 339 JPEGTABLES = 347 +REFERENCEBLACKWHITE = 532 COPYRIGHT = 33432 IPTC_NAA_CHUNK = 33723 # newsphoto properties PHOTOSHOP_CHUNK = 34377 # photoshop properties ICCPROFILE = 34675 EXIFIFD = 34665 XMP = 700 +JPEGQUALITY = 65537 # pseudo-tag by libtiff # https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java IMAGEJ_META_DATA_BYTE_COUNTS = 50838 @@ -135,6 +121,9 @@ COMPRESSION_INFO = { 32946: "tiff_deflate", 34676: "tiff_sgilog", 34677: "tiff_sgilog24", + 34925: "lzma", + 50000: "zstd", + 50001: "webp", } COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} @@ -150,7 +139,6 @@ OPEN_INFO = { (MM, 1, (1,), 1, (1,), ()): ("1", "1"), (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), - (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), @@ -159,7 +147,6 @@ OPEN_INFO = { (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), - (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), @@ -168,7 +155,6 @@ OPEN_INFO = { (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), - (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), @@ -177,14 +163,11 @@ OPEN_INFO = { (MM, 1, (1,), 1, (8,), ()): ("L", "L"), (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), - (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), - (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"), - (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), @@ -192,10 +175,8 @@ OPEN_INFO = { (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"), (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), - (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), - (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), @@ -222,7 +203,6 @@ OPEN_INFO = { (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 - (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), @@ -233,7 +213,6 @@ OPEN_INFO = { (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"), - (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), @@ -252,19 +231,17 @@ OPEN_INFO = { (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), - (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), - - # JPEG compressed images handled by LibTiff and auto-converted to RGB + (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), + # JPEG compressed images handled by LibTiff and auto-converted to RGBX # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel - (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), - (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), - + (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), + (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), } @@ -310,7 +287,7 @@ class IFDRational(Rational): """ - __slots__ = ('_numerator', '_denominator', '_val') + __slots__ = ("_numerator", "_denominator", "_val") def __init__(self, value, denominator=1): """ @@ -318,25 +295,21 @@ class IFDRational(Rational): float/rational/other number, or an IFDRational :param denominator: Optional integer denominator """ - self._denominator = denominator - self._numerator = value - self._val = float(1) + if isinstance(value, IFDRational): + self._numerator = value.numerator + self._denominator = value.denominator + self._val = value._val + return if isinstance(value, Fraction): self._numerator = value.numerator self._denominator = value.denominator - self._val = value - - if isinstance(value, IFDRational): - self._denominator = value.denominator - self._numerator = value.numerator - self._val = value._val - return + else: + self._numerator = value + self._denominator = denominator if denominator == 0: - self._val = float('nan') - return - + self._val = float("nan") elif denominator == 1: self._val = Fraction(value) else: @@ -375,44 +348,43 @@ class IFDRational(Rational): def _delegate(op): def delegate(self, *args): return getattr(self._val, op)(*args) + return delegate - """ a = ['add','radd', 'sub', 'rsub','div', 'rdiv', 'mul', 'rmul', - 'truediv', 'rtruediv', 'floordiv', - 'rfloordiv','mod','rmod', 'pow','rpow', 'pos', 'neg', - 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'nonzero', + """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', + 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', + 'mod','rmod', 'pow','rpow', 'pos', 'neg', + 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool', 'ceil', 'floor', 'round'] print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) """ - __add__ = _delegate('__add__') - __radd__ = _delegate('__radd__') - __sub__ = _delegate('__sub__') - __rsub__ = _delegate('__rsub__') - __div__ = _delegate('__div__') - __rdiv__ = _delegate('__rdiv__') - __mul__ = _delegate('__mul__') - __rmul__ = _delegate('__rmul__') - __truediv__ = _delegate('__truediv__') - __rtruediv__ = _delegate('__rtruediv__') - __floordiv__ = _delegate('__floordiv__') - __rfloordiv__ = _delegate('__rfloordiv__') - __mod__ = _delegate('__mod__') - __rmod__ = _delegate('__rmod__') - __pow__ = _delegate('__pow__') - __rpow__ = _delegate('__rpow__') - __pos__ = _delegate('__pos__') - __neg__ = _delegate('__neg__') - __abs__ = _delegate('__abs__') - __trunc__ = _delegate('__trunc__') - __lt__ = _delegate('__lt__') - __gt__ = _delegate('__gt__') - __le__ = _delegate('__le__') - __ge__ = _delegate('__ge__') - __nonzero__ = _delegate('__nonzero__') - __ceil__ = _delegate('__ceil__') - __floor__ = _delegate('__floor__') - __round__ = _delegate('__round__') + __add__ = _delegate("__add__") + __radd__ = _delegate("__radd__") + __sub__ = _delegate("__sub__") + __rsub__ = _delegate("__rsub__") + __mul__ = _delegate("__mul__") + __rmul__ = _delegate("__rmul__") + __truediv__ = _delegate("__truediv__") + __rtruediv__ = _delegate("__rtruediv__") + __floordiv__ = _delegate("__floordiv__") + __rfloordiv__ = _delegate("__rfloordiv__") + __mod__ = _delegate("__mod__") + __rmod__ = _delegate("__rmod__") + __pow__ = _delegate("__pow__") + __rpow__ = _delegate("__rpow__") + __pos__ = _delegate("__pos__") + __neg__ = _delegate("__neg__") + __abs__ = _delegate("__abs__") + __trunc__ = _delegate("__trunc__") + __lt__ = _delegate("__lt__") + __gt__ = _delegate("__gt__") + __le__ = _delegate("__le__") + __ge__ = _delegate("__ge__") + __bool__ = _delegate("__bool__") + __ceil__ = _delegate("__ceil__") + __floor__ = _delegate("__floor__") + __round__ = _delegate("__round__") class ImageFileDirectory_v2(MutableMapping): @@ -423,7 +395,7 @@ class ImageFileDirectory_v2(MutableMapping): ifd = ImageFileDirectory_v2() ifd[key] = 'Some Data' - ifd.tagtype[key] = 2 + ifd.tagtype[key] = TiffTags.ASCII print(ifd[key]) 'Some Data' @@ -446,6 +418,7 @@ class ImageFileDirectory_v2(MutableMapping): .. versionadded:: 3.0.0 """ + """ Documentation: @@ -490,7 +463,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) @@ -505,7 +478,7 @@ class ImageFileDirectory_v2(MutableMapping): self._tags_v1 = {} # will remain empty if legacy_api is false self._tags_v2 = {} # main tag storage self._tagdata = {} - self.tagtype = {} # added 2008-06-05 by Florian Hoech + self.tagtype = {} # added 2008-06-05 by Florian Hoech self._next = None self._offset = None @@ -518,8 +491,7 @@ class ImageFileDirectory_v2(MutableMapping): Returns the complete tag dictionary, with named tags where possible. """ - return dict((TiffTags.lookup(code).name, value) - for code, value in self.items()) + return {TiffTags.lookup(code).name: value for code, value in self.items()} def __len__(self): return len(set(self._tagdata) | set(self._tags_v2)) @@ -532,23 +504,17 @@ class ImageFileDirectory_v2(MutableMapping): self[tag] = handler(self, data, self.legacy_api) # check type val = self._tags_v2[tag] if self.legacy_api and not isinstance(val, (tuple, bytes)): - val = val, + val = (val,) return val 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 @@ -557,7 +523,7 @@ class ImageFileDirectory_v2(MutableMapping): if info.type: self.tagtype[tag] = info.type else: - self.tagtype[tag] = 7 + self.tagtype[tag] = TiffTags.UNDEFINED if all(isinstance(v, IFDRational) for v in values): self.tagtype[tag] = TiffTags.RATIONAL elif all(isinstance(v, int) for v in values): @@ -568,19 +534,15 @@ 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: - values = [value.encode("ascii", 'replace') if isinstance( - value, str) else value] + if self.tagtype[tag] == TiffTags.UNDEFINED: + values = [ + value.encode("ascii", "replace") if isinstance(value, str) else value + ] elif self.tagtype[tag] == TiffTags.RATIONAL: - values = [float(v) if isinstance(v, int) else v - for v in values] + values = [float(v) if isinstance(v, int) else v for v in values] values = tuple(info.cvt_enum(value) for value in values) @@ -591,22 +553,23 @@ class ImageFileDirectory_v2(MutableMapping): # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. # Don't mess with the legacy api, since it's frozen. - if (info.length == 1) or \ - (info.length is None and len(values) == 1 and not legacy_api): + if (info.length == 1) or ( + info.length is None and len(values) == 1 and not legacy_api + ): # Don't mess with the legacy api, since it's frozen. if legacy_api and self.tagtype[tag] in [ TiffTags.RATIONAL, - TiffTags.SIGNED_RATIONAL + TiffTags.SIGNED_RATIONAL, ]: # rationals - values = values, + values = (values,) try: - dest[tag], = values + (dest[tag],) = values except ValueError: # We've got a builtin tag with 1 expected entry warnings.warn( - "Metadata Warning, tag %s had too many entries: " - "%s, expected 1" % ( - tag, len(values))) + "Metadata Warning, tag %s had too many entries: %s, expected 1" + % (tag, len(values)) + ) dest[tag] = values[0] else: @@ -631,36 +594,51 @@ class ImageFileDirectory_v2(MutableMapping): def _register_loader(idx, size): def decorator(func): from .TiffTags import TYPES + if func.__name__.startswith("load_"): TYPES[idx] = func.__name__[5:].replace("_", " ") _load_dispatch[idx] = size, func # noqa: F821 return func + return decorator def _register_writer(idx): def decorator(func): _write_dispatch[idx] = func # noqa: F821 return func + return decorator def _register_basic(idx_fmt_name): from .TiffTags import TYPES + idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize("=" + fmt) - _load_dispatch[idx] = size, lambda self, data, legacy_api=True: ( # noqa: F821 - self._unpack("{}{}".format(len(data) // size, fmt), data)) + _load_dispatch[idx] = ( # noqa: F821 + size, + lambda self, data, legacy_api=True: ( + self._unpack("{}{}".format(len(data) // size, fmt), data) + ), + ) _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 - b"".join(self._pack(fmt, value) for value in values)) + b"".join(self._pack(fmt, value) for value in values) + ) - list(map(_register_basic, - [(TiffTags.SHORT, "H", "short"), - (TiffTags.LONG, "L", "long"), - (TiffTags.SIGNED_BYTE, "b", "signed byte"), - (TiffTags.SIGNED_SHORT, "h", "signed short"), - (TiffTags.SIGNED_LONG, "l", "signed long"), - (TiffTags.FLOAT, "f", "float"), - (TiffTags.DOUBLE, "d", "double")])) + list( + map( + _register_basic, + [ + (TiffTags.SHORT, "H", "short"), + (TiffTags.LONG, "L", "long"), + (TiffTags.SIGNED_BYTE, "b", "signed byte"), + (TiffTags.SIGNED_SHORT, "h", "signed short"), + (TiffTags.SIGNED_LONG, "l", "signed long"), + (TiffTags.FLOAT, "f", "float"), + (TiffTags.DOUBLE, "d", "double"), + ], + ) + ) @_register_loader(1, 1) # Basic type, except for the legacy API. def load_byte(self, data, legacy_api=True): @@ -679,22 +657,22 @@ 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" + return b"" + value.encode("ascii", "replace") + b"\0" @_register_loader(5, 8) def load_rational(self, data, legacy_api=True): vals = self._unpack("{}L".format(len(data) // 4), data) - def combine(a, b): return (a, b) if legacy_api else IFDRational(a, b) - return tuple(combine(num, denom) - for num, denom in zip(vals[::2], vals[1::2])) + def combine(a, b): + return (a, b) if legacy_api else IFDRational(a, b) + + return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(5) def write_rational(self, *values): - return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 31)) - for frac in values) + return b"".join( + self._pack("2L", *_limit_rational(frac, 2 ** 31)) for frac in values + ) @_register_loader(7, 1) def load_undefined(self, data, legacy_api=True): @@ -708,21 +686,24 @@ class ImageFileDirectory_v2(MutableMapping): def load_signed_rational(self, data, legacy_api=True): vals = self._unpack("{}l".format(len(data) // 4), data) - def combine(a, b): return (a, b) if legacy_api else IFDRational(a, b) - return tuple(combine(num, denom) - for num, denom in zip(vals[::2], vals[1::2])) + def combine(a, b): + return (a, b) if legacy_api else IFDRational(a, b) + + return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(10) def write_signed_rational(self, *values): - return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 30)) - for frac in values) + return b"".join( + self._pack("2L", *_limit_rational(frac, 2 ** 30)) for frac in values + ) def _ensure_read(self, fp, size): ret = fp.read(size) if len(ret) != size: - raise IOError("Corrupt EXIF data. " + - "Expecting to read %d bytes but only got %d. " % - (size, len(ret))) + raise OSError( + "Corrupt EXIF data. " + + "Expecting to read %d bytes but only got %d. " % (size, len(ret)) + ) return ret def load(self, fp): @@ -732,13 +713,14 @@ class ImageFileDirectory_v2(MutableMapping): try: for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]): - tag, typ, count, data = self._unpack("HHL4s", - self._ensure_read(fp, 12)) + tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12)) if DEBUG: tagname = TiffTags.lookup(tag).name typname = TYPES.get(typ, "unknown") - print("tag: %s (%d) - type: %s (%d)" % - (tagname, tag, typname, typ), end=" ") + print( + "tag: %s (%d) - type: %s (%d)" % (tagname, tag, typname, typ), + end=" ", + ) try: unit_size, handler = self._load_dispatch[typ] @@ -749,10 +731,12 @@ 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), end=" ") + print( + "Tag Location: {} - Data Location: {}".format(here, offset), + end=" ", + ) fp.seek(offset) data = ImageFile._safe_read(fp, size) fp.seek(here) @@ -760,9 +744,11 @@ class ImageFileDirectory_v2(MutableMapping): data = data[:size] if len(data) != size: - warnings.warn("Possibly corrupt EXIF data. " - "Expecting to read %d bytes but only got %d." - " Skipping tag %s" % (size, len(data), tag)) + warnings.warn( + "Possibly corrupt EXIF data. " + "Expecting to read %d bytes but only got %d." + " Skipping tag %s" % (size, len(data), tag) + ) continue if not data: @@ -777,22 +763,17 @@ class ImageFileDirectory_v2(MutableMapping): else: print("- value:", self[tag]) - self.next, = self._unpack("L", self._ensure_read(fp, 4)) - except IOError as msg: + (self.next,) = self._unpack("L", self._ensure_read(fp, 4)) + except OSError as msg: warnings.warn(str(msg)) return - def save(self, fp): - - if fp.tell() == 0: # skip TIFF header on subsequent pages - # tiff header -- PIL always starts the first IFD at offset 8 - fp.write(self._prefix + self._pack("HL", 42, 8)) - + def tobytes(self, offset=0): # FIXME What about tagdata? - fp.write(self._pack("H", len(self._tags_v2))) + result = self._pack("H", len(self._tags_v2)) entries = [] - offset = fp.tell() + len(self._tags_v2) * 12 + 4 + offset = offset + len(result) + len(self._tags_v2) * 12 + 4 stripoffsets = None # pass 1: convert tags to binary format @@ -802,21 +783,23 @@ 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: tagname = TiffTags.lookup(tag).name typname = TYPES.get(typ, "unknown") - print("save: %s (%d) - type: %s (%d)" % - (tagname, tag, typname, typ), end=" ") + print( + "save: %s (%d) - type: %s (%d)" % (tagname, tag, typname, typ), + end=" ", + ) if len(data) >= 16: print("- value: " % len(data)) else: print("- value:", values) # count is sum of lengths for string and arbitrary data - if typ in [TiffTags.ASCII, TiffTags.UNDEFINED]: + if typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: count = len(data) else: count = len(values) @@ -824,35 +807,44 @@ class ImageFileDirectory_v2(MutableMapping): if len(data) <= 4: entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) else: - entries.append((tag, typ, count, self._pack("L", offset), - data)) + entries.append((tag, typ, count, self._pack("L", offset), data)) offset += (len(data) + 1) // 2 * 2 # pad to word # update strip offset data to point beyond auxiliary data if stripoffsets is not None: tag, typ, count, value, data = entries[stripoffsets] if data: - raise NotImplementedError( - "multistrip support not yet implemented") + raise NotImplementedError("multistrip support not yet implemented") value = self._pack("L", self._unpack("L", value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data # 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)) - fp.write(self._pack("HHL4s", tag, typ, count, value)) + result += self._pack("HHL4s", tag, typ, count, value) # -- overwrite here for multi-page -- - fp.write(b"\0\0\0\0") # end of entries + result += b"\0\0\0\0" # end of entries # pass 3: write auxiliary data to file for tag, typ, count, value, data in entries: - fp.write(data) + result += data if len(data) & 1: - fp.write(b"\0") + result += b"\0" - return offset + return result + + def save(self, fp): + + if fp.tell() == 0: # skip TIFF header on subsequent pages + # tiff header -- PIL always starts the first IFD at offset 8 + fp.write(self._prefix + self._pack("HL", 42, 8)) + + offset = fp.tell() + result = self.tobytes(offset) + fp.write(result) + return offset + len(result) ImageFileDirectory_v2._load_dispatch = _load_dispatch @@ -872,7 +864,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): ifd = ImageFileDirectory_v1() ifd[key] = 'Some Data' - ifd.tagtype[key] = 2 + ifd.tagtype[key] = TiffTags.ASCII print(ifd[key]) ('Some Data',) @@ -883,8 +875,9 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): .. deprecated:: 3.0.0 """ + 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) @@ -947,7 +940,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): self._setitem(tag, handler(self, data, legacy), legacy) val = self._tags_v1[tag] if not isinstance(val, (tuple, bytes)): - val = val, + val = (val,) return val @@ -958,6 +951,7 @@ ImageFileDirectory = ImageFileDirectory_v1 ## # Image plugin for TIFF files. + class TiffImageFile(ImageFile.ImageFile): format = "TIFF" @@ -982,7 +976,6 @@ class TiffImageFile(ImageFile.ImageFile): self.__fp = self.fp self._frame_pos = [] self._n_frames = None - self._is_animated = None if DEBUG: print("*** TiffImageFile._open ***") @@ -996,29 +989,14 @@ class TiffImageFile(ImageFile.ImageFile): def n_frames(self): if self._n_frames is None: current = self.tell() - try: - while True: - self._seek(self.tell() + 1) - except EOFError: - self._n_frames = self.tell() + 1 + self._seek(len(self._frame_pos)) + while self._n_frames is None: + self._seek(self.tell() + 1) self.seek(current) return self._n_frames @property def is_animated(self): - if self._is_animated is None: - if self._n_frames is not None: - self._is_animated = self._n_frames != 1 - else: - current = self.tell() - - try: - self.seek(1) - self._is_animated = True - except EOFError: - self._is_animated = False - - self.seek(current) return self._is_animated def seek(self, frame): @@ -1038,10 +1016,11 @@ class TiffImageFile(ImageFile.ImageFile): if not self.__next: raise EOFError("no more images in TIFF file") if DEBUG: - print("Seeking to frame %s, on frame %s, " - "__next %s, location: %s" % - (frame, self.__frame, self.__next, self.fp.tell())) - # reset python3 buffered io handle in case fp + print( + "Seeking to frame %s, on frame %s, __next %s, location: %s" + % (frame, self.__frame, self.__next, self.fp.tell()) + ) + # reset buffered io handle in case fp # was passed to libtiff, invalidating the buffer self.fp.tell() self.fp.seek(self.__next) @@ -1050,10 +1029,13 @@ class TiffImageFile(ImageFile.ImageFile): print("Loading tags, location: %s" % self.fp.tell()) self.tag_v2.load(self.fp) self.__next = self.tag_v2.next + if self.__next == 0: + self._n_frames = frame + 1 + if len(self._frame_pos) == 1: + self._is_animated = self.__next != 0 self.__frame += 1 self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) - self.__next = self.tag_v2.next # fill the legacy tag/ifd entries self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) self.__frame = frame @@ -1063,28 +1045,29 @@ class TiffImageFile(ImageFile.ImageFile): """Return the current frame number""" return self.__frame - @property - def size(self): - return self._size - - @size.setter - def size(self, value): - warnings.warn( - 'Setting the size of a TIFF image directly is deprecated, and will' - ' be removed in a future version. Use the resize method instead.', - DeprecationWarning - ) - self._size = value - def load(self): if self.use_load_libtiff: return self._load_libtiff() - return super(TiffImageFile, self).load() + return super().load() def load_end(self): + if self._tile_orientation: + method = { + 2: Image.FLIP_LEFT_RIGHT, + 3: Image.ROTATE_180, + 4: Image.FLIP_TOP_BOTTOM, + 5: Image.TRANSPOSE, + 6: Image.ROTATE_270, + 7: Image.TRANSVERSE, + 8: Image.ROTATE_90, + }.get(self._tile_orientation) + if method is not None: + self.im = self.im.transpose(method) + self._size = self.im.size + # allow closing if we're on the first frame, there's no next # This is the ImageFile.load path only, libtiff specific below. - if self.__frame == 0 and not self.__next: + if not self._is_animated: self._close_exclusive_fp_after_loading = True def _load_libtiff(self): @@ -1094,19 +1077,19 @@ 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)) extents = self.tile[0][1] - args = list(self.tile[0][3]) + [self.tag_v2.offset] + args = list(self.tile[0][3]) # To be nice on memory footprint, if there's a # file descriptor, use that instead of reading @@ -1115,11 +1098,11 @@ class TiffImageFile(ImageFile.ImageFile): try: fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno()) # flush the file descriptor, prevents error on pypy 2.4+ - # should also eliminate the need for fp.tell for py3 + # should also eliminate the need for fp.tell # in _seek if hasattr(self.fp, "flush"): self.fp.flush() - except IOError: + except OSError: # io.BytesIO have a fileno, but returns an IOError if # it doesn't use a file descriptor. fp = False @@ -1127,13 +1110,15 @@ class TiffImageFile(ImageFile.ImageFile): if fp: args[2] = fp - decoder = Image._getdecoder(self.mode, 'libtiff', tuple(args), - self.decoderconfig) + decoder = Image._getdecoder( + self.mode, "libtiff", tuple(args), self.decoderconfig + ) try: decoder.setimage(self.im, extents) except ValueError: - raise IOError("Couldn't set the image") + raise OSError("Couldn't set the image") + close_self_fp = self._exclusive_fp and not self._is_animated if hasattr(self.fp, "getvalue"): # We've got a stringio like thing passed in. Yay for all in memory. # The decoder needs the entire file in one shot, so there's not @@ -1147,30 +1132,34 @@ class TiffImageFile(ImageFile.ImageFile): if DEBUG: print("have getvalue. just sending in a string from getvalue") n, err = decoder.decode(self.fp.getvalue()) - elif hasattr(self.fp, "fileno"): + elif fp: # we've got a actual file on disk, pass in the fp. if DEBUG: print("have fileno, calling fileno version of the decoder.") - self.fp.seek(0) + if not close_self_fp: + self.fp.seek(0) # 4 bytes, otherwise the trace might error out n, err = decoder.decode(b"fpfp") else: # we have something else. if DEBUG: print("don't have fileno or getvalue. just reading") + self.fp.seek(0) # UNDONE -- so much for that buffer size thing. n, err = decoder.decode(self.fp.read()) self.tile = [] self.readonly = 0 + + self.load_end() + # libtiff closed the fp in a, we need to close self.fp, if possible - if self._exclusive_fp: - if self.__frame == 0 and not self.__next: - self.fp.close() - self.fp = None # might be shared + if close_self_fp: + self.fp.close() + self.fp = None # might be shared if err < 0: - raise IOError(err) + raise OSError(err) return Image.Image.load(self) @@ -1178,7 +1167,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)] @@ -1188,6 +1177,10 @@ class TiffImageFile(ImageFile.ImageFile): # the specification photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) + # old style jpeg compression images most certainly are YCbCr + if self._compression == "tiff_jpeg": + photo = 6 + fillorder = self.tag_v2.get(FILLORDER, 1) if DEBUG: @@ -1199,16 +1192,15 @@ class TiffImageFile(ImageFile.ImageFile): print("- YCbCr subsampling:", self.tag.get(530)) # size - xsize = self.tag_v2.get(IMAGEWIDTH) - ysize = self.tag_v2.get(IMAGELENGTH) + xsize = int(self.tag_v2.get(IMAGEWIDTH)) + ysize = int(self.tag_v2.get(IMAGELENGTH)) self._size = xsize, ysize if DEBUG: print("- size:", self.size) sampleFormat = self.tag_v2.get(SAMPLEFORMAT, (1,)) - if (len(sampleFormat) > 1 - and max(sampleFormat) == min(sampleFormat) == 1): + if len(sampleFormat) > 1 and max(sampleFormat) == min(sampleFormat) == 1: # SAMPLEFORMAT is properly per band, so an RGB image will # be (1,1,1). But, we don't support per band pixel types, # and anything more than one band is a uint8. So, just @@ -1231,8 +1223,14 @@ class TiffImageFile(ImageFile.ImageFile): bps_tuple = bps_tuple * bps_count # mode: check photometric interpretation and bits per pixel - key = (self.tag_v2.prefix, photo, sampleFormat, fillorder, - bps_tuple, extra_tuple) + key = ( + self.tag_v2.prefix, + photo, + sampleFormat, + fillorder, + bps_tuple, + extra_tuple, + ) if DEBUG: print("format key:", key) try: @@ -1254,11 +1252,11 @@ class TiffImageFile(ImageFile.ImageFile): if xres and yres: resunit = self.tag_v2.get(RESOLUTION_UNIT) if resunit == 2: # dots per inch - self.info["dpi"] = xres, yres + self.info["dpi"] = int(xres + 0.5), int(yres + 0.5) elif resunit == 3: # dots per centimeter. convert to dpi - self.info["dpi"] = xres * 2.54, yres * 2.54 + self.info["dpi"] = int(xres * 2.54 + 0.5), int(yres * 2.54 + 0.5) elif resunit is None: # used to default to 1, but now 2) - self.info["dpi"] = xres, yres + self.info["dpi"] = int(xres + 0.5), int(yres + 0.5) # For backward compatibility, # we also preserve the old behavior self.info["resolution"] = xres, yres @@ -1268,7 +1266,7 @@ class TiffImageFile(ImageFile.ImageFile): # build tile descriptors x = y = layer = 0 self.tile = [] - self.use_load_libtiff = READ_LIBTIFF or self._compression != 'raw' + self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw" if self.use_load_libtiff: # Decoder expects entire file as one tile. # There's a buffer size limit in load (64k) @@ -1295,20 +1293,17 @@ class TiffImageFile(ImageFile.ImageFile): # we're expecting image byte order. So, if the rawmode # contains I;16, we need to convert from native to image # byte order. - if rawmode == 'I;16': - rawmode = 'I;16N' - if ';16B' in rawmode: - rawmode = rawmode.replace(';16B', ';16N') - if ';16L' in rawmode: - rawmode = rawmode.replace(';16L', ';16N') + if rawmode == "I;16": + rawmode = "I;16N" + if ";16B" in rawmode: + rawmode = rawmode.replace(";16B", ";16N") + if ";16L" in rawmode: + rawmode = rawmode.replace(";16L", ";16N") # Offset in the tile tuple is 0, we go from 0,0 to # w,h, and we only do this once -- eds - a = (rawmode, self._compression, False) - self.tile.append( - (self._compression, - (0, 0, xsize, ysize), - 0, a)) + a = (rawmode, self._compression, False, self.tag_v2.offset) + self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a)) elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: # striped image @@ -1337,9 +1332,13 @@ class TiffImageFile(ImageFile.ImageFile): a = (tile_rawmode, int(stride), 1) self.tile.append( - (self._compression, - (x, y, min(x+w, xsize), min(y+h, ysize)), - offset, a)) + ( + self._compression, + (x, y, min(x + w, xsize), min(y + h, ysize)), + offset, + a, + ) + ) x = x + w if x >= self.size[0]: x, y = 0, y + h @@ -1353,14 +1352,16 @@ class TiffImageFile(ImageFile.ImageFile): # Fix up info. if ICCPROFILE in self.tag_v2: - self.info['icc_profile'] = self.tag_v2[ICCPROFILE] + self.info["icc_profile"] = self.tag_v2[ICCPROFILE] # fixup palette descriptor - if self.mode == "P": + if self.mode in ["P", "PA"]: palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) + self._tile_orientation = self.tag_v2.get(0x0112) + def _close__fp(self): try: if self.__fp != self.fp: @@ -1396,7 +1397,6 @@ SAVE_INFO = { "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), - "I;32BS": ("I;32BS", MM, 1, 2, (32,), None), "I;16B": ("I;16B", MM, 1, 1, (16,), None), "I;16BS": ("I;16BS", MM, 1, 2, (16,), None), @@ -1409,18 +1409,18 @@ 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) - compression = im.encoderinfo.get('compression', im.info.get('compression')) + compression = im.encoderinfo.get("compression", im.info.get("compression")) if compression is None: - compression = 'raw' + compression = "raw" - libtiff = WRITE_LIBTIFF or compression != 'raw' + libtiff = WRITE_LIBTIFF or compression != "raw" # required for color libtiff images - ifd[PLANAR_CONFIGURATION] = getattr(im, '_planar_configuration', 1) + ifd[PLANAR_CONFIGURATION] = getattr(im, "_planar_configuration", 1) ifd[IMAGEWIDTH] = im.size[0] ifd[IMAGELENGTH] = im.size[1] @@ -1436,14 +1436,20 @@ def _save(im, fp, filename): try: ifd.tagtype[key] = info.tagtype[key] except Exception: - pass # might not be an IFD, Might not have populated type + pass # might not be an IFD. Might not have populated type # additions written by Greg Couch, gregc@cgl.ucsf.edu # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com - if hasattr(im, 'tag_v2'): + if hasattr(im, "tag_v2"): # preserve tags from original TIFF image file - for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION, - IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP): + for key in ( + RESOLUTION_UNIT, + X_RESOLUTION, + Y_RESOLUTION, + IPTC_NAA_CHUNK, + PHOTOSHOP_CHUNK, + XMP, + ): if key in im.tag_v2: ifd[key] = im.tag_v2[key] ifd.tagtype[key] = im.tag_v2.tagtype[key] @@ -1453,24 +1459,26 @@ def _save(im, fp, filename): if "icc_profile" in im.info: ifd[ICCPROFILE] = im.info["icc_profile"] - for key, name in [(IMAGEDESCRIPTION, "description"), - (X_RESOLUTION, "resolution"), - (Y_RESOLUTION, "resolution"), - (X_RESOLUTION, "x_resolution"), - (Y_RESOLUTION, "y_resolution"), - (RESOLUTION_UNIT, "resolution_unit"), - (SOFTWARE, "software"), - (DATE_TIME, "date_time"), - (ARTIST, "artist"), - (COPYRIGHT, "copyright")]: + for key, name in [ + (IMAGEDESCRIPTION, "description"), + (X_RESOLUTION, "resolution"), + (Y_RESOLUTION, "resolution"), + (X_RESOLUTION, "x_resolution"), + (Y_RESOLUTION, "y_resolution"), + (RESOLUTION_UNIT, "resolution_unit"), + (SOFTWARE, "software"), + (DATE_TIME, "date_time"), + (ARTIST, "artist"), + (COPYRIGHT, "copyright"), + ]: if name in im.encoderinfo: ifd[key] = im.encoderinfo[name] dpi = im.encoderinfo.get("dpi") if dpi: ifd[RESOLUTION_UNIT] = 2 - ifd[X_RESOLUTION] = dpi[0] - ifd[Y_RESOLUTION] = dpi[1] + ifd[X_RESOLUTION] = int(dpi[0] + 0.5) + ifd[Y_RESOLUTION] = int(dpi[1] + 0.5) if bits != (1,): ifd[BITSPERSAMPLE] = bits @@ -1483,11 +1491,11 @@ def _save(im, fp, filename): ifd[PHOTOMETRIC_INTERPRETATION] = photo - if im.mode == "P": + if im.mode in ["P", "PA"]: lut = im.im.getpalette("RGB", "RGB;L") ifd[COLORMAP] = tuple(i8(v) * 256 for v in lut) # data orientation - stride = len(bits) * ((im.size[0]*bits[0]+7)//8) + stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) ifd[ROWSPERSTRIP] = im.size[1] ifd[STRIPBYTECOUNTS] = stride * im.size[1] ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer @@ -1495,6 +1503,16 @@ def _save(im, fp, filename): ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) if libtiff: + if "quality" in im.encoderinfo: + quality = im.encoderinfo["quality"] + if not isinstance(quality, int) or quality < 0 or quality > 100: + raise ValueError("Invalid quality setting") + if compression != "jpeg": + raise ValueError( + "quality setting only supported for 'jpeg' compression" + ) + ifd[JPEGQUALITY] = quality + if DEBUG: print("Saving using libtiff encoder") print("Items: %s" % sorted(ifd.items())) @@ -1506,9 +1524,24 @@ def _save(im, fp, filename): except io.UnsupportedOperation: pass + # optional types for non core tags + types = {} + # SAMPLEFORMAT is determined by the image format and should not be copied + # from legacy_ifd. # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library # based on the data in the strip. - blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS] + # The other tags expect arrays with a certain length (fixed or depending on + # BITSPERSAMPLE, etc), passing arrays with a different length will result in + # segfaults. Block these tags until we add extra validation. + blocklist = [ + COLORMAP, + REFERENCEBLACKWHITE, + SAMPLEFORMAT, + STRIPBYTECOUNTS, + STRIPOFFSETS, + TRANSFERFUNCTION, + ] + atts = {} # bits per sample is a single short in the tiff directory, not a list. atts[BITSPERSAMPLE] = bits[0] @@ -1516,26 +1549,30 @@ def _save(im, fp, filename): # the original file, e.g x,y resolution so that we can # save(load('')) == original file. legacy_ifd = {} - if hasattr(im, 'tag'): + if hasattr(im, "tag"): legacy_ifd = im.tag.to_v2() - for tag, value in itertools.chain(ifd.items(), - getattr(im, 'tag_v2', {}).items(), - legacy_ifd.items()): + for tag, value in itertools.chain( + ifd.items(), getattr(im, "tag_v2", {}).items(), legacy_ifd.items() + ): # Libtiff can only process certain core items without adding # them to the custom dictionary. - # Support for custom items has only been been added - # for int, float, unicode, string and byte values + # Custom items are supported for int, float, unicode, string and byte + # values. Other types and tuples require a tagtype. if tag not in TiffTags.LIBTIFF_CORE: if TiffTags.lookup(tag).type == TiffTags.UNDEFINED: continue - if (distutils.version.StrictVersion(_libtiff_version()) < - distutils.version.StrictVersion("4.0")) \ - or not (isinstance(value, (int, float, str, bytes)) or - (not py3 and isinstance(value, unicode))): # noqa: F821 + if distutils.version.StrictVersion( + _libtiff_version() + ) < distutils.version.StrictVersion("4.0"): + continue + + if tag in ifd.tagtype: + types[tag] = ifd.tagtype[tag] + 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 - atts[tag] = value.encode('ascii', 'replace') + b"\0" + if isinstance(value, str): + atts[tag] = value.encode("ascii", "replace") + b"\0" elif isinstance(value, IFDRational): atts[tag] = float(value) else: @@ -1548,28 +1585,33 @@ def _save(im, fp, filename): # we're storing image byte order. So, if the rawmode # contains I;16, we need to convert from native to image # byte order. - if im.mode in ('I;16B', 'I;16'): - rawmode = 'I;16N' + if im.mode in ("I;16B", "I;16"): + rawmode = "I;16N" - a = (rawmode, compression, _fp, filename, atts) - e = Image._getencoder(im.mode, 'libtiff', a, im.encoderconfig) - e.setimage(im.im, (0, 0)+im.size) + # Pass tags as sorted list so that the tags are set in a fixed order. + # This is required by libtiff for some tags. For example, the JPEGQUALITY + # pseudo tag requires that the COMPRESS tag was already set. + tags = list(atts.items()) + tags.sort() + a = (rawmode, compression, _fp, filename, tags, types) + e = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig) + e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - l, s, d = e.encode(16*1024) + l, s, d = e.encode(16 * 1024) if not _fp: fp.write(d) 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) - ImageFile._save(im, fp, [ - ("raw", (0, 0)+im.size, offset, (rawmode, stride, 1)) - ]) + ImageFile._save( + im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] + ) # -- helper for multi-page save -- if "_debug_multipage" in im.encoderinfo: @@ -1603,16 +1645,16 @@ class AppendingTiffWriter: Tags = {273, 288, 324, 519, 520, 521} def __init__(self, fn, new=False): - if hasattr(fn, 'read'): + if hasattr(fn, "read"): self.f = fn self.close_fp = False else: 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() @@ -1654,8 +1696,7 @@ class AppendingTiffWriter: return if IIMM != self.IIMM: - raise RuntimeError("IIMM of new page doesn't match IIMM of " - "first page") + raise RuntimeError("IIMM of new page doesn't match IIMM of first page") IFDoffset = self.readLong() IFDoffset += self.offsetOfNewPage @@ -1680,7 +1721,7 @@ class AppendingTiffWriter: def tell(self): return self.f.tell() - self.offsetOfNewPage - def seek(self, offset, whence): + def seek(self, offset, whence=io.SEEK_SET): if whence == os.SEEK_SET: offset += self.offsetOfNewPage @@ -1694,7 +1735,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): @@ -1718,45 +1759,40 @@ 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): self.f.seek(-2, os.SEEK_CUR) bytesWritten = self.f.write(struct.pack(self.longFmt, value)) if bytesWritten is not None and bytesWritten != 4: - raise RuntimeError("wrote only %u bytes but wanted 4" % - bytesWritten) + raise RuntimeError("wrote only %u bytes but wanted 4" % bytesWritten) def rewriteLastShort(self, value): self.f.seek(-2, os.SEEK_CUR) bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) if bytesWritten is not None and bytesWritten != 2: - raise RuntimeError("wrote only %u bytes but wanted 2" % - bytesWritten) + raise RuntimeError("wrote only %u bytes but wanted 2" % bytesWritten) def rewriteLastLong(self, value): self.f.seek(-4, os.SEEK_CUR) bytesWritten = self.f.write(struct.pack(self.longFmt, value)) if bytesWritten is not None and bytesWritten != 4: - raise RuntimeError("wrote only %u bytes but wanted 4" % - bytesWritten) + raise RuntimeError("wrote only %u bytes but wanted 4" % bytesWritten) def writeShort(self, value): bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) if bytesWritten is not None and bytesWritten != 2: - raise RuntimeError("wrote only %u bytes but wanted 2" % - bytesWritten) + raise RuntimeError("wrote only %u bytes but wanted 2" % bytesWritten) def writeLong(self, value): bytesWritten = self.f.write(struct.pack(self.longFmt, value)) if bytesWritten is not None and bytesWritten != 4: - raise RuntimeError("wrote only %u bytes but wanted 4" % - bytesWritten) + raise RuntimeError("wrote only %u bytes but wanted 4" % bytesWritten) def close(self): self.finalize() @@ -1766,12 +1802,11 @@ class AppendingTiffWriter: numTags = self.readShort() for i in range(numTags): - tag, fieldType, count = struct.unpack(self.tagFormat, - self.f.read(8)) + tag, fieldType, count = struct.unpack(self.tagFormat, self.f.read(8)) fieldSize = self.fieldSizes[fieldType] totalSize = fieldSize * count - isLocal = (totalSize <= 4) + isLocal = totalSize <= 4 if not isLocal: offset = self.readLong() offset += self.offsetOfNewPage @@ -1781,13 +1816,15 @@ class AppendingTiffWriter: curPos = self.f.tell() if isLocal: - self.fixOffsets(count, isShort=(fieldSize == 2), - isLong=(fieldSize == 4)) + self.fixOffsets( + count, isShort=(fieldSize == 2), isLong=(fieldSize == 4) + ) self.f.seek(curPos + 4) else: self.f.seek(offset) - self.fixOffsets(count, isShort=(fieldSize == 2), - isLong=(fieldSize == 4)) + self.fixOffsets( + count, isShort=(fieldSize == 2), isLong=(fieldSize == 4) + ) self.f.seek(curPos) offset = curPos = None @@ -1830,7 +1867,7 @@ def _save_all(im, fp, filename): cur_idx = im.tell() try: with AppendingTiffWriter(fp) as tf: - for ims in [im]+append_images: + for ims in [im] + append_images: ims.encoderinfo = encoderinfo ims.encoderconfig = encoderconfig if not hasattr(ims, "n_frames"): diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 3e0291512..6cc9ff7f3 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -23,10 +23,8 @@ from collections import namedtuple 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 {}) + def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): + 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 @@ -44,7 +42,7 @@ def lookup(tag): """ - return TAGS_V2.get(tag, TagInfo(tag, TAGS.get(tag, 'unknown'))) + return TAGS_V2.get(tag, TagInfo(tag, TAGS.get(tag, "unknown"))) ## @@ -73,27 +71,47 @@ FLOAT = 11 DOUBLE = 12 TAGS_V2 = { - 254: ("NewSubfileType", LONG, 1), 255: ("SubfileType", SHORT, 1), 256: ("ImageWidth", LONG, 1), 257: ("ImageLength", LONG, 1), 258: ("BitsPerSample", SHORT, 0), - 259: ("Compression", SHORT, 1, - {"Uncompressed": 1, "CCITT 1d": 2, "Group 3 Fax": 3, - "Group 4 Fax": 4, "LZW": 5, "JPEG": 6, "PackBits": 32773}), - - 262: ("PhotometricInterpretation", SHORT, 1, - {"WhiteIsZero": 0, "BlackIsZero": 1, "RGB": 2, "RGB Palette": 3, - "Transparency Mask": 4, "CMYK": 5, "YCbCr": 6, "CieLAB": 8, - "CFA": 32803, # TIFF/EP, Adobe DNG - "LinearRaw": 32892}), # Adobe DNG + 259: ( + "Compression", + SHORT, + 1, + { + "Uncompressed": 1, + "CCITT 1d": 2, + "Group 3 Fax": 3, + "Group 4 Fax": 4, + "LZW": 5, + "JPEG": 6, + "PackBits": 32773, + }, + ), + 262: ( + "PhotometricInterpretation", + SHORT, + 1, + { + "WhiteIsZero": 0, + "BlackIsZero": 1, + "RGB": 2, + "RGB Palette": 3, + "Transparency Mask": 4, + "CMYK": 5, + "YCbCr": 6, + "CieLAB": 8, + "CFA": 32803, # TIFF/EP, Adobe DNG + "LinearRaw": 32892, # Adobe DNG + }, + ), 263: ("Threshholding", SHORT, 1), 264: ("CellWidth", SHORT, 1), 265: ("CellLength", SHORT, 1), 266: ("FillOrder", SHORT, 1), 269: ("DocumentName", ASCII, 1), - 270: ("ImageDescription", ASCII, 1), 271: ("Make", ASCII, 1), 272: ("Model", ASCII, 1), @@ -102,8 +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), @@ -113,31 +130,26 @@ TAGS_V2 = { 287: ("YPosition", RATIONAL, 1), 288: ("FreeOffsets", LONG, 1), 289: ("FreeByteCounts", LONG, 1), - 290: ("GrayResponseUnit", SHORT, 1), 291: ("GrayResponseCurve", SHORT, 0), 292: ("T4Options", LONG, 1), 293: ("T6Options", LONG, 1), 296: ("ResolutionUnit", SHORT, 1, {"none": 1, "inch": 2, "cm": 3}), 297: ("PageNumber", SHORT, 2), - 301: ("TransferFunction", SHORT, 0), 305: ("Software", ASCII, 1), 306: ("DateTime", ASCII, 1), - 315: ("Artist", ASCII, 1), 316: ("HostComputer", ASCII, 1), 317: ("Predictor", SHORT, 1, {"none": 1, "Horizontal Differencing": 2}), 318: ("WhitePoint", RATIONAL, 2), 319: ("PrimaryChromaticities", RATIONAL, 6), - 320: ("ColorMap", SHORT, 0), 321: ("HalftoneHints", SHORT, 2), 322: ("TileWidth", LONG, 1), 323: ("TileLength", LONG, 1), 324: ("TileOffsets", LONG, 0), 325: ("TileByteCounts", LONG, 0), - 332: ("InkSet", SHORT, 1), 333: ("InkNames", ASCII, 1), 334: ("NumberOfInks", SHORT, 1), @@ -145,13 +157,10 @@ TAGS_V2 = { 337: ("TargetPrinter", ASCII, 1), 338: ("ExtraSamples", SHORT, 0), 339: ("SampleFormat", SHORT, 0), - 340: ("SMinSampleValue", DOUBLE, 0), 341: ("SMaxSampleValue", DOUBLE, 0), 342: ("TransferRange", SHORT, 6), - 347: ("JPEGTables", UNDEFINED, 1), - # obsolete JPEG tags 512: ("JPEGProc", SHORT, 1), 513: ("JPEGInterchangeFormat", LONG, 1), @@ -162,22 +171,18 @@ TAGS_V2 = { 519: ("JPEGQTables", LONG, 0), 520: ("JPEGDCTables", LONG, 0), 521: ("JPEGACTables", LONG, 0), - 529: ("YCbCrCoefficients", RATIONAL, 3), 530: ("YCbCrSubSampling", SHORT, 2), 531: ("YCbCrPositioning", SHORT, 1), 532: ("ReferenceBlackWhite", RATIONAL, 6), - - 700: ('XMP', BYTE, 1), - + 700: ("XMP", BYTE, 0), 33432: ("Copyright", ASCII, 1), - 34377: ('PhotoshopInfo', BYTE, 1), - + 33723: ("IptcNaaInfo", UNDEFINED, 0), + 34377: ("PhotoshopInfo", BYTE, 0), # FIXME add more tags here - 34665: ("ExifIFD", SHORT, 1), - 34675: ('ICCProfile', UNDEFINED, 1), - 34853: ('GPSInfoIFD', BYTE, 1), - + 34665: ("ExifIFD", LONG, 1), + 34675: ("ICCProfile", UNDEFINED, 1), + 34853: ("GPSInfoIFD", LONG, 1), # MPInfo 45056: ("MPFVersion", UNDEFINED, 1), 45057: ("NumberOfImages", LONG, 1), @@ -198,159 +203,157 @@ TAGS_V2 = { 45579: ("YawAngle", SIGNED_RATIONAL, 1), 45580: ("PitchAngle", SIGNED_RATIONAL, 1), 45581: ("RollAngle", SIGNED_RATIONAL, 1), - 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), 50780: ("BestQualityScale", RATIONAL, 1), 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one - 50839: ("ImageJMetaData", UNDEFINED, 1) # see Issue #2006 + 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006 } # Legacy Tags structure # these tags aren't included above, but were in the previous versions -TAGS = {347: 'JPEGTables', - 700: 'XMP', - - # Additional Exif Info - 32932: 'Wang Annotation', - 33434: 'ExposureTime', - 33437: 'FNumber', - 33445: 'MD FileTag', - 33446: 'MD ScalePixel', - 33447: 'MD ColorTable', - 33448: 'MD LabName', - 33449: 'MD SampleInfo', - 33450: 'MD PrepDate', - 33451: 'MD PrepTime', - 33452: 'MD FileUnits', - 33550: 'ModelPixelScaleTag', - 33723: 'IptcNaaInfo', - 33918: 'INGR Packet Data Tag', - 33919: 'INGR Flag Registers', - 33920: 'IrasB Transformation Matrix', - 33922: 'ModelTiepointTag', - 34264: 'ModelTransformationTag', - 34377: 'PhotoshopInfo', - 34735: 'GeoKeyDirectoryTag', - 34736: 'GeoDoubleParamsTag', - 34737: 'GeoAsciiParamsTag', - 34850: 'ExposureProgram', - 34852: 'SpectralSensitivity', - 34855: 'ISOSpeedRatings', - 34856: 'OECF', - 34864: 'SensitivityType', - 34865: 'StandardOutputSensitivity', - 34866: 'RecommendedExposureIndex', - 34867: 'ISOSpeed', - 34868: 'ISOSpeedLatitudeyyy', - 34869: 'ISOSpeedLatitudezzz', - 34908: 'HylaFAX FaxRecvParams', - 34909: 'HylaFAX FaxSubAddress', - 34910: 'HylaFAX FaxRecvTime', - 36864: 'ExifVersion', - 36867: 'DateTimeOriginal', - 36868: 'DateTImeDigitized', - 37121: 'ComponentsConfiguration', - 37122: 'CompressedBitsPerPixel', - 37724: 'ImageSourceData', - 37377: 'ShutterSpeedValue', - 37378: 'ApertureValue', - 37379: 'BrightnessValue', - 37380: 'ExposureBiasValue', - 37381: 'MaxApertureValue', - 37382: 'SubjectDistance', - 37383: 'MeteringMode', - 37384: 'LightSource', - 37385: 'Flash', - 37386: 'FocalLength', - 37396: 'SubjectArea', - 37500: 'MakerNote', - 37510: 'UserComment', - 37520: 'SubSec', - 37521: 'SubSecTimeOriginal', - 37522: 'SubsecTimeDigitized', - 40960: 'FlashPixVersion', - 40961: 'ColorSpace', - 40962: 'PixelXDimension', - 40963: 'PixelYDimension', - 40964: 'RelatedSoundFile', - 40965: 'InteroperabilityIFD', - 41483: 'FlashEnergy', - 41484: 'SpatialFrequencyResponse', - 41486: 'FocalPlaneXResolution', - 41487: 'FocalPlaneYResolution', - 41488: 'FocalPlaneResolutionUnit', - 41492: 'SubjectLocation', - 41493: 'ExposureIndex', - 41495: 'SensingMethod', - 41728: 'FileSource', - 41729: 'SceneType', - 41730: 'CFAPattern', - 41985: 'CustomRendered', - 41986: 'ExposureMode', - 41987: 'WhiteBalance', - 41988: 'DigitalZoomRatio', - 41989: 'FocalLengthIn35mmFilm', - 41990: 'SceneCaptureType', - 41991: 'GainControl', - 41992: 'Contrast', - 41993: 'Saturation', - 41994: 'Sharpness', - 41995: 'DeviceSettingDescription', - 41996: 'SubjectDistanceRange', - 42016: 'ImageUniqueID', - 42032: 'CameraOwnerName', - 42033: 'BodySerialNumber', - 42034: 'LensSpecification', - 42035: 'LensMake', - 42036: 'LensModel', - 42037: 'LensSerialNumber', - 42112: 'GDAL_METADATA', - 42113: 'GDAL_NODATA', - 42240: 'Gamma', - 50215: 'Oce Scanjob Description', - 50216: 'Oce Application Selector', - 50217: 'Oce Identification Number', - 50218: 'Oce ImageLogic Characteristics', - - # Adobe DNG - 50706: 'DNGVersion', - 50707: 'DNGBackwardVersion', - 50708: 'UniqueCameraModel', - 50709: 'LocalizedCameraModel', - 50710: 'CFAPlaneColor', - 50711: 'CFALayout', - 50712: 'LinearizationTable', - 50713: 'BlackLevelRepeatDim', - 50714: 'BlackLevel', - 50715: 'BlackLevelDeltaH', - 50716: 'BlackLevelDeltaV', - 50717: 'WhiteLevel', - 50718: 'DefaultScale', - 50719: 'DefaultCropOrigin', - 50720: 'DefaultCropSize', - 50721: 'ColorMatrix1', - 50722: 'ColorMatrix2', - 50723: 'CameraCalibration1', - 50724: 'CameraCalibration2', - 50725: 'ReductionMatrix1', - 50726: 'ReductionMatrix2', - 50727: 'AnalogBalance', - 50728: 'AsShotNeutral', - 50729: 'AsShotWhiteXY', - 50730: 'BaselineExposure', - 50731: 'BaselineNoise', - 50732: 'BaselineSharpness', - 50733: 'BayerGreenSplit', - 50734: 'LinearResponseLimit', - 50735: 'CameraSerialNumber', - 50736: 'LensInfo', - 50737: 'ChromaBlurRadius', - 50738: 'AntiAliasStrength', - 50740: 'DNGPrivateData', - 50778: 'CalibrationIlluminant1', - 50779: 'CalibrationIlluminant2', - 50784: 'Alias Layer Metadata' - } +TAGS = { + 347: "JPEGTables", + 700: "XMP", + # Additional Exif Info + 32932: "Wang Annotation", + 33434: "ExposureTime", + 33437: "FNumber", + 33445: "MD FileTag", + 33446: "MD ScalePixel", + 33447: "MD ColorTable", + 33448: "MD LabName", + 33449: "MD SampleInfo", + 33450: "MD PrepDate", + 33451: "MD PrepTime", + 33452: "MD FileUnits", + 33550: "ModelPixelScaleTag", + 33723: "IptcNaaInfo", + 33918: "INGR Packet Data Tag", + 33919: "INGR Flag Registers", + 33920: "IrasB Transformation Matrix", + 33922: "ModelTiepointTag", + 34264: "ModelTransformationTag", + 34377: "PhotoshopInfo", + 34735: "GeoKeyDirectoryTag", + 34736: "GeoDoubleParamsTag", + 34737: "GeoAsciiParamsTag", + 34850: "ExposureProgram", + 34852: "SpectralSensitivity", + 34855: "ISOSpeedRatings", + 34856: "OECF", + 34864: "SensitivityType", + 34865: "StandardOutputSensitivity", + 34866: "RecommendedExposureIndex", + 34867: "ISOSpeed", + 34868: "ISOSpeedLatitudeyyy", + 34869: "ISOSpeedLatitudezzz", + 34908: "HylaFAX FaxRecvParams", + 34909: "HylaFAX FaxSubAddress", + 34910: "HylaFAX FaxRecvTime", + 36864: "ExifVersion", + 36867: "DateTimeOriginal", + 36868: "DateTImeDigitized", + 37121: "ComponentsConfiguration", + 37122: "CompressedBitsPerPixel", + 37724: "ImageSourceData", + 37377: "ShutterSpeedValue", + 37378: "ApertureValue", + 37379: "BrightnessValue", + 37380: "ExposureBiasValue", + 37381: "MaxApertureValue", + 37382: "SubjectDistance", + 37383: "MeteringMode", + 37384: "LightSource", + 37385: "Flash", + 37386: "FocalLength", + 37396: "SubjectArea", + 37500: "MakerNote", + 37510: "UserComment", + 37520: "SubSec", + 37521: "SubSecTimeOriginal", + 37522: "SubsecTimeDigitized", + 40960: "FlashPixVersion", + 40961: "ColorSpace", + 40962: "PixelXDimension", + 40963: "PixelYDimension", + 40964: "RelatedSoundFile", + 40965: "InteroperabilityIFD", + 41483: "FlashEnergy", + 41484: "SpatialFrequencyResponse", + 41486: "FocalPlaneXResolution", + 41487: "FocalPlaneYResolution", + 41488: "FocalPlaneResolutionUnit", + 41492: "SubjectLocation", + 41493: "ExposureIndex", + 41495: "SensingMethod", + 41728: "FileSource", + 41729: "SceneType", + 41730: "CFAPattern", + 41985: "CustomRendered", + 41986: "ExposureMode", + 41987: "WhiteBalance", + 41988: "DigitalZoomRatio", + 41989: "FocalLengthIn35mmFilm", + 41990: "SceneCaptureType", + 41991: "GainControl", + 41992: "Contrast", + 41993: "Saturation", + 41994: "Sharpness", + 41995: "DeviceSettingDescription", + 41996: "SubjectDistanceRange", + 42016: "ImageUniqueID", + 42032: "CameraOwnerName", + 42033: "BodySerialNumber", + 42034: "LensSpecification", + 42035: "LensMake", + 42036: "LensModel", + 42037: "LensSerialNumber", + 42112: "GDAL_METADATA", + 42113: "GDAL_NODATA", + 42240: "Gamma", + 50215: "Oce Scanjob Description", + 50216: "Oce Application Selector", + 50217: "Oce Identification Number", + 50218: "Oce ImageLogic Characteristics", + # Adobe DNG + 50706: "DNGVersion", + 50707: "DNGBackwardVersion", + 50708: "UniqueCameraModel", + 50709: "LocalizedCameraModel", + 50710: "CFAPlaneColor", + 50711: "CFALayout", + 50712: "LinearizationTable", + 50713: "BlackLevelRepeatDim", + 50714: "BlackLevel", + 50715: "BlackLevelDeltaH", + 50716: "BlackLevelDeltaV", + 50717: "WhiteLevel", + 50718: "DefaultScale", + 50719: "DefaultCropOrigin", + 50720: "DefaultCropSize", + 50721: "ColorMatrix1", + 50722: "ColorMatrix2", + 50723: "CameraCalibration1", + 50724: "CameraCalibration2", + 50725: "ReductionMatrix1", + 50726: "ReductionMatrix2", + 50727: "AnalogBalance", + 50728: "AsShotNeutral", + 50729: "AsShotWhiteXY", + 50730: "BaselineExposure", + 50731: "BaselineNoise", + 50732: "BaselineSharpness", + 50733: "BayerGreenSplit", + 50734: "LinearResponseLimit", + 50735: "CameraSerialNumber", + 50736: "LensInfo", + 50737: "ChromaBlurRadius", + 50738: "AntiAliasStrength", + 50740: "DNGPrivateData", + 50778: "CalibrationIlluminant1", + 50779: "CalibrationIlluminant2", + 50784: "Alias Layer Metadata", +} def _populate(): @@ -430,16 +433,55 @@ TYPES = {} # 389: case TIFFTAG_REFERENCEBLACKWHITE: # 393: case TIFFTAG_INKNAMES: +# Following pseudo-tags are also handled by default in libtiff: +# TIFFTAG_JPEGQUALITY 65537 + # some of these are not in our TAGS_V2 dict and were included from tiff.h # This list also exists in encode.c -LIBTIFF_CORE = {255, 256, 257, 258, 259, 262, 263, 266, 274, 277, - 278, 280, 281, 340, 341, 282, 283, 284, 286, 287, - 296, 297, 321, 320, 338, 32995, 322, 323, 32998, - 32996, 339, 32997, 330, 531, 530, 301, 532, 333, - # as above - 269 # this has been in our tests forever, and works - } +LIBTIFF_CORE = { + 255, + 256, + 257, + 258, + 259, + 262, + 263, + 266, + 274, + 277, + 278, + 280, + 281, + 340, + 341, + 282, + 283, + 284, + 286, + 287, + 296, + 297, + 321, + 320, + 338, + 32995, + 322, + 323, + 32998, + 32996, + 339, + 32997, + 330, + 531, + 530, + 301, + 532, + 333, + # as above + 269, # this has been in our tests forever, and works + 65537, +} LIBTIFF_CORE.remove(320) # Array of short, crashes LIBTIFF_CORE.remove(301) # Array of short, crashes diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index 6602cc86b..d5a5c8e67 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -1,4 +1,3 @@ -# encoding: utf-8 # # The Python Imaging Library. # $Id$ @@ -21,15 +20,11 @@ # https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml # and has been tested with a few sample files found using google. +import builtins + from . import Image from ._binary import i32le as i32 -try: - import builtins -except ImportError: - import __builtin__ - builtins = __builtin__ - def open(filename): """ @@ -46,7 +41,7 @@ def open(filename): def imopen(fp): # read header fields - header = fp.read(32+24+32+12) + header = fp.read(32 + 24 + 32 + 12) size = i32(header, 32), i32(header, 36) offset = i32(header, 40) @@ -62,7 +57,7 @@ def open(filename): # strings are null-terminated im.info["name"] = header[:32].split(b"\0", 1)[0] - next_name = header[56:56+32].split(b"\0", 1)[0] + next_name = header[56 : 56 + 32].split(b"\0", 1)[0] if next_name: im.info["next_name"] = next_name diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 212e6b4c7..eda685508 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -1,28 +1,24 @@ +from io import BytesIO + from . import Image, ImageFile + try: from . import _webp + SUPPORTED = True except ImportError: SUPPORTED = False -from io import BytesIO -_VALID_WEBP_MODES = { - "RGBX": True, - "RGBA": True, - "RGB": True, - } +_VALID_WEBP_MODES = {"RGBX": True, "RGBA": True, "RGB": True} -_VALID_WEBP_LEGACY_MODES = { - "RGB": True, - "RGBA": True, - } +_VALID_WEBP_LEGACY_MODES = {"RGB": True, "RGBA": True} _VP8_MODES_BY_IDENTIFIER = { b"VP8 ": "RGB", b"VP8X": "RGBA", b"VP8L": "RGBA", # lossless - } +} def _accept(prefix): @@ -32,8 +28,9 @@ def _accept(prefix): if is_riff_file_format and is_webp_file and is_valid_vp8_mode: if not SUPPORTED: - return "image file could not be identified " \ - "because WEBP support not installed" + return ( + "image file could not be identified because WEBP support not installed" + ) return True @@ -45,8 +42,9 @@ class WebPImageFile(ImageFile.ImageFile): def _open(self): if not _webp.HAVE_WEBPANIM: # Legacy mode - data, width, height, self.mode, icc_profile, exif = \ - _webp.WebPDecode(self.fp.read()) + data, width, height, self.mode, icc_profile, exif = _webp.WebPDecode( + self.fp.read() + ) if icc_profile: self.info["icc_profile"] = icc_profile if exif: @@ -62,18 +60,18 @@ class WebPImageFile(ImageFile.ImageFile): self._decoder = _webp.WebPAnimDecoder(self.fp.read()) # Get info from decoder - width, height, loop_count, bgcolor, frame_count, mode = \ - self._decoder.get_info() + width, height, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() self._size = width, height self.info["loop"] = loop_count - bg_a, bg_r, bg_g, bg_b = \ - (bgcolor >> 24) & 0xFF, \ - (bgcolor >> 16) & 0xFF, \ - (bgcolor >> 8) & 0xFF, \ - bgcolor & 0xFF + bg_a, bg_r, bg_g, bg_b = ( + (bgcolor >> 24) & 0xFF, + (bgcolor >> 16) & 0xFF, + (bgcolor >> 8) & 0xFF, + bgcolor & 0xFF, + ) self.info["background"] = (bg_r, bg_g, bg_b, bg_a) self._n_frames = frame_count - self.mode = 'RGB' if mode == 'RGBX' else mode + self.mode = "RGB" if mode == "RGBX" else mode self.rawmode = mode self.tile = [] @@ -93,8 +91,9 @@ class WebPImageFile(ImageFile.ImageFile): self.seek(0) def _getexif(self): - from .JpegImagePlugin import _getexif - return _getexif(self) + if "exif" not in self.info: + return None + return dict(self.getexif()) @property def n_frames(self): @@ -106,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: @@ -131,7 +130,7 @@ class WebPImageFile(ImageFile.ImageFile): # Check if an error occurred if ret is None: - self._reset() # Reset just to be safe + self._reset() # Reset just to be safe self.seek(0) raise EOFError("failed to decode next frame in WebP file") @@ -146,11 +145,11 @@ class WebPImageFile(ImageFile.ImageFile): def _seek(self, frame): if self.__physical_frame == frame: - return # Nothing to do + return # Nothing to do if frame < self.__physical_frame: - self._reset() # Rewind to beginning + self._reset() # Rewind to beginning while self.__physical_frame < frame: - self._get_next() # Advance to the requested frame + self._get_next() # Advance to the requested frame def load(self): if _webp.HAVE_WEBPANIM: @@ -169,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 @@ -185,8 +184,8 @@ def _save_all(im, fp, filename): # If total frame count is 1, then save using the legacy API, which # will preserve non-alpha modes total = 0 - for ims in [im]+append_images: - total += 1 if not hasattr(ims, "n_frames") else ims.n_frames + for ims in [im] + append_images: + total += getattr(ims, "n_frames", 1) if total == 1: _save(im, fp, filename) return @@ -201,7 +200,7 @@ def _save_all(im, fp, filename): # info["background"]. So it must be converted to an RGBA value palette = im.getpalette() if palette: - r, g, b = palette[background*3:(background+1)*3] + r, g, b = palette[background * 3 : (background + 1) * 3] background = (r, g, b, 0) duration = im.encoderinfo.get("duration", 0) @@ -216,6 +215,8 @@ def _save_all(im, fp, filename): method = im.encoderinfo.get("method", 0) icc_profile = im.encoderinfo.get("icc_profile", "") exif = im.encoderinfo.get("exif", "") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() xmp = im.encoderinfo.get("xmp", "") if allow_mixed: lossless = False @@ -227,10 +228,15 @@ def _save_all(im, fp, filename): kmax = 17 if lossless else 5 # Validate background color - if (not isinstance(background, (list, tuple)) or len(background) != 4 or - not all(v >= 0 and v < 256 for v in background)): - raise IOError("Background color is not an RGBA tuple clamped " - "to (0-255): %s" % str(background)) + if ( + not isinstance(background, (list, tuple)) + or len(background) != 4 + or not all(v >= 0 and v < 256 for v in background) + ): + raise OSError( + "Background color is not an RGBA tuple clamped to (0-255): %s" + % str(background) + ) # Convert to packed uint bg_r, bg_g, bg_b, bg_a = background @@ -238,13 +244,15 @@ def _save_all(im, fp, filename): # Setup the WebP animation encoder enc = _webp.WebPAnimEncoder( - im.size[0], im.size[1], + im.size[0], + im.size[1], background, loop, minimize_size, - kmin, kmax, + kmin, + kmax, allow_mixed, - verbose + verbose, ) # Add each frame @@ -252,12 +260,9 @@ def _save_all(im, fp, filename): timestamp = 0 cur_idx = im.tell() try: - for ims in [im]+append_images: + for ims in [im] + append_images: # Get # of frames in this image - if not hasattr(ims, "n_frames"): - nfr = 1 - else: - nfr = ims.n_frames + nfr = getattr(ims, "n_frames", 1) for idx in range(nfr): ims.seek(idx) @@ -267,25 +272,28 @@ def _save_all(im, fp, filename): frame = ims rawmode = ims.mode if ims.mode not in _VALID_WEBP_MODES: - alpha = 'A' in ims.mode or 'a' in ims.mode \ - or (ims.mode == 'P' and - 'A' in ims.im.getpalettemode()) - rawmode = 'RGBA' if alpha else 'RGB' + alpha = ( + "A" in ims.mode + or "a" in ims.mode + or (ims.mode == "P" and "A" in ims.im.getpalettemode()) + ) + rawmode = "RGBA" if alpha else "RGB" frame = ims.convert(rawmode) - if rawmode == 'RGB': + if rawmode == "RGB": # For faster conversion, use RGBX - rawmode = 'RGBX' + rawmode = "RGBX" # Append the frame to the animation encoder enc.add( - frame.tobytes('raw', rawmode), + frame.tobytes("raw", rawmode), timestamp, - frame.size[0], frame.size[1], + frame.size[0], + frame.size[1], rawmode, lossless, quality, - method + method, ) # Update timestamp and frame index @@ -299,16 +307,12 @@ def _save_all(im, fp, filename): im.seek(cur_idx) # Force encoder to flush frames - enc.add( - None, - timestamp, - 0, 0, "", lossless, quality, 0 - ) + enc.add(None, timestamp, 0, 0, "", lossless, quality, 0) # 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) @@ -318,12 +322,17 @@ def _save(im, fp, filename): quality = im.encoderinfo.get("quality", 80) icc_profile = im.encoderinfo.get("icc_profile", "") exif = im.encoderinfo.get("exif", "") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() xmp = im.encoderinfo.get("xmp", "") if im.mode not in _VALID_WEBP_LEGACY_MODES: - alpha = 'A' in im.mode or 'a' in im.mode \ - or (im.mode == 'P' and 'A' in im.im.getpalettemode()) - im = im.convert('RGBA' if alpha else 'RGB') + alpha = ( + "A" in im.mode + or "a" in im.mode + or (im.mode == "P" and "A" in im.im.getpalettemode()) + ) + im = im.convert("RGBA" if alpha else "RGB") data = _webp.WebPEncode( im.tobytes(), @@ -334,10 +343,10 @@ def _save(im, fp, filename): im.mode, icc_profile, exif, - xmp + 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 06eb851c9..7695a68e7 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -19,23 +19,11 @@ # http://wvware.sourceforge.net/caolan/index.html # http://wvware.sourceforge.net/caolan/ora-wmf.html -from __future__ import print_function - from . import Image, ImageFile -from ._binary import i16le as word, si16le as short, \ - i32le as dword, 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" +from ._binary import i16le as word, i32le as dword, si16le as short, si32le as _long _handler = None -if py3: - long = int - def register_handler(handler): """ @@ -50,8 +38,7 @@ def register_handler(handler): if hasattr(Image.core, "drawwmf"): # install default handler (windows only) - class WmfHandler(object): - + class WmfHandler: def open(self, im): im.mode = "RGB" self.bbox = im.info["wmf_bbox"] @@ -59,10 +46,14 @@ if hasattr(Image.core, "drawwmf"): def load(self, im): im.fp.seek(0) # rewind return Image.frombytes( - "RGB", im.size, + "RGB", + im.size, Image.core.drawwmf(im.fp.read(), im.size, self.bbox), - "raw", "BGR", (im.size[0]*3 + 3) & -4, -1 - ) + "raw", + "BGR", + (im.size[0] * 3 + 3) & -4, + -1, + ) register_handler(WmfHandler()) @@ -73,14 +64,14 @@ if hasattr(Image.core, "drawwmf"): def _accept(prefix): return ( - prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or - prefix[:4] == b"\x01\x00\x00\x00" - ) + prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or prefix[:4] == b"\x01\x00\x00\x00" + ) ## # Image plugin for Windows metafiles. + class WmfStubImageFile(ImageFile.StubImageFile): format = "WMF" @@ -131,8 +122,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): size = x1 - x0, y1 - y0 # calculate dots per inch from bbox and frame - xdpi = 2540 * (x1 - y0) // (frame[2] - frame[0]) - ydpi = 2540 * (y1 - y0) // (frame[3] - frame[1]) + xdpi = int(2540.0 * (x1 - y0) / (frame[2] - frame[0]) + 0.5) + ydpi = int(2540.0 * (y1 - y0) / (frame[3] - frame[1]) + 0.5) self.info["wmf_bbox"] = x0, y0, x1, y1 @@ -157,9 +148,10 @@ 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) + # # -------------------------------------------------------------------- # Registry stuff diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index ad913b2a8..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) @@ -31,7 +27,9 @@ PALETTE = b"" for r in range(8): for g in range(8): for b in range(4): - PALETTE = PALETTE + (o8((r*255)//7)+o8((g*255)//7)+o8((b*255)//3)) + PALETTE = PALETTE + ( + o8((r * 255) // 7) + o8((g * 255) // 7) + o8((b * 255) // 3) + ) def _accept(prefix): @@ -41,6 +39,7 @@ def _accept(prefix): ## # Image plugin for XV thumbnail images. + class XVThumbImageFile(ImageFile.ImageFile): format = "XVThumb" @@ -71,10 +70,7 @@ class XVThumbImageFile(ImageFile.ImageFile): self.palette = ImagePalette.raw("RGB", PALETTE) - self.tile = [ - ("raw", (0, 0)+self.size, - self.fp.tell(), (self.mode, 0, 1) - )] + self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1))] # -------------------------------------------------------------------- diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index af5adccd2..ead9722c8 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -20,11 +20,8 @@ # import re -from . import Image, ImageFile -# __version__ is deprecated and will be removed in a future version. Use -# PIL.__version__ instead. -__version__ = "0.6" +from . import Image, ImageFile # XBM header xbm_head = re.compile( @@ -45,6 +42,7 @@ def _accept(prefix): ## # Image plugin for X11 bitmaps. + class XbmImageFile(ImageFile.ImageFile): format = "XBM" @@ -60,32 +58,30 @@ class XbmImageFile(ImageFile.ImageFile): ysize = int(m.group("height")) if m.group("hotspot"): - self.info["hotspot"] = ( - int(m.group("xhot")), int(m.group("yhot")) - ) + self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) self.mode = "1" self._size = xsize, ysize - self.tile = [("xbm", (0, 0)+self.size, m.end(), None)] + self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] 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')) + fp.write(("#define im_width %d\n" % im.size[0]).encode("ascii")) + fp.write(("#define im_height %d\n" % im.size[1]).encode("ascii")) hotspot = im.encoderinfo.get("hotspot") if hotspot: - fp.write(("#define im_x_hot %d\n" % hotspot[0]).encode('ascii')) - fp.write(("#define im_y_hot %d\n" % hotspot[1]).encode('ascii')) + fp.write(("#define im_x_hot %d\n" % hotspot[0]).encode("ascii")) + fp.write(("#define im_y_hot %d\n" % hotspot[1]).encode("ascii")) fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [("xbm", (0, 0)+im.size, 0, None)]) + ImageFile._save(im, fp, [("xbm", (0, 0) + im.size, 0, None)]) fp.write(b"};\n") diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 9cecdbca2..d8bd00a1b 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -16,15 +16,12 @@ 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]*)") +xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') def _accept(prefix): @@ -34,6 +31,7 @@ def _accept(prefix): ## # Image plugin for X11 pixel maps. + class XpmImageFile(ImageFile.ImageFile): format = "XPM" @@ -69,9 +67,9 @@ class XpmImageFile(ImageFile.ImageFile): for i in range(pal): s = self.fp.readline() - if s[-2:] == b'\r\n': + if s[-2:] == b"\r\n": s = s[:-2] - elif s[-1:] in b'\r\n': + elif s[-1:] in b"\r\n": s = s[:-1] c = i8(s[1]) @@ -82,15 +80,15 @@ class XpmImageFile(ImageFile.ImageFile): if s[i] == b"c": # process colour key - rgb = s[i+1] + rgb = s[i + 1] if rgb == b"None": self.info["transparency"] = c elif rgb[0:1] == b"#": # FIXME: handle colour names (see ImagePalette.py) rgb = int(rgb[1:], 16) - palette[c] = (o8((rgb >> 16) & 255) + - o8((rgb >> 8) & 255) + - o8(rgb & 255)) + palette[c] = ( + o8((rgb >> 16) & 255) + o8((rgb >> 8) & 255) + o8(rgb & 255) + ) else: # unknown colour raise ValueError("cannot read this XPM file") @@ -104,7 +102,7 @@ class XpmImageFile(ImageFile.ImageFile): self.mode = "P" self.palette = ImagePalette.raw("RGB", b"".join(palette)) - self.tile = [("raw", (0, 0)+self.size, self.fp.tell(), ("P", 0, 1))] + self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] def load_read(self, bytes): @@ -116,10 +114,11 @@ class XpmImageFile(ImageFile.ImageFile): s = [None] * ysize for i in range(ysize): - s[i] = self.fp.readline()[1:xsize+1].ljust(xsize) + s[i] = self.fp.readline()[1 : xsize + 1].ljust(xsize) return b"".join(s) + # # Registry diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index bc8cfed8c..e7f26488d 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -9,64 +9,68 @@ 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 . import _version -# VERSION is deprecated and will be removed in Pillow 6.0.0. -# PILLOW_VERSION is deprecated and will be removed after that. +# VERSION was removed in Pillow 6.0.0. +# PILLOW_VERSION was removed in Pillow 7.0.0. # Use __version__ instead. -VERSION = '1.1.7' # PIL Version -PILLOW_VERSION = __version__ = _version.__version__ +__version__ = _version.__version__ del _version -_plugins = ['BlpImagePlugin', - 'BmpImagePlugin', - 'BufrStubImagePlugin', - 'CurImagePlugin', - 'DcxImagePlugin', - 'DdsImagePlugin', - 'EpsImagePlugin', - 'FitsStubImagePlugin', - 'FliImagePlugin', - 'FpxImagePlugin', - 'FtexImagePlugin', - 'GbrImagePlugin', - 'GifImagePlugin', - 'GribStubImagePlugin', - 'Hdf5StubImagePlugin', - 'IcnsImagePlugin', - 'IcoImagePlugin', - 'ImImagePlugin', - 'ImtImagePlugin', - 'IptcImagePlugin', - 'JpegImagePlugin', - 'Jpeg2KImagePlugin', - 'McIdasImagePlugin', - 'MicImagePlugin', - 'MpegImagePlugin', - 'MpoImagePlugin', - 'MspImagePlugin', - 'PalmImagePlugin', - 'PcdImagePlugin', - 'PcxImagePlugin', - 'PdfImagePlugin', - 'PixarImagePlugin', - 'PngImagePlugin', - 'PpmImagePlugin', - 'PsdImagePlugin', - 'SgiImagePlugin', - 'SpiderImagePlugin', - 'SunImagePlugin', - 'TgaImagePlugin', - 'TiffImagePlugin', - 'WebPImagePlugin', - 'WmfImagePlugin', - 'XbmImagePlugin', - 'XpmImagePlugin', - 'XVThumbImagePlugin'] +_plugins = [ + "BlpImagePlugin", + "BmpImagePlugin", + "BufrStubImagePlugin", + "CurImagePlugin", + "DcxImagePlugin", + "DdsImagePlugin", + "EpsImagePlugin", + "FitsStubImagePlugin", + "FliImagePlugin", + "FpxImagePlugin", + "FtexImagePlugin", + "GbrImagePlugin", + "GifImagePlugin", + "GribStubImagePlugin", + "Hdf5StubImagePlugin", + "IcnsImagePlugin", + "IcoImagePlugin", + "ImImagePlugin", + "ImtImagePlugin", + "IptcImagePlugin", + "JpegImagePlugin", + "Jpeg2KImagePlugin", + "McIdasImagePlugin", + "MicImagePlugin", + "MpegImagePlugin", + "MpoImagePlugin", + "MspImagePlugin", + "PalmImagePlugin", + "PcdImagePlugin", + "PcxImagePlugin", + "PdfImagePlugin", + "PixarImagePlugin", + "PngImagePlugin", + "PpmImagePlugin", + "PsdImagePlugin", + "SgiImagePlugin", + "SpiderImagePlugin", + "SunImagePlugin", + "TgaImagePlugin", + "TiffImagePlugin", + "WebPImagePlugin", + "WmfImagePlugin", + "XbmImagePlugin", + "XpmImagePlugin", + "XVThumbImagePlugin", +] + + +class UnidentifiedImageError(IOError): + pass diff --git a/src/PIL/__main__.py b/src/PIL/__main__.py new file mode 100644 index 000000000..a05323f93 --- /dev/null +++ b/src/PIL/__main__.py @@ -0,0 +1,3 @@ +from .features import pilinfo + +pilinfo() diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 767c13b9d..529b8c94b 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -11,21 +11,15 @@ # See the README file for information on usage and redistribution. # -from struct import unpack_from, pack -from ._util import py3 +from struct import pack, unpack_from -if py3: - def i8(c): - return c if c.__class__ is int else c[0] - def o8(i): - return bytes((i & 255,)) -else: - def i8(c): - return ord(c) +def i8(c): + return c if c.__class__ is int else c[0] - def o8(i): - return chr(i & 255) + +def o8(i): + return bytes((i & 255,)) # Input, le = little endian, be = big endian @@ -33,8 +27,8 @@ def i16le(c, o=0): """ Converts a 2-bytes (16 bits) string to an unsigned integer. - c: string containing bytes to convert - o: offset of bytes to convert in string + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string """ return unpack_from(" 2: - from tkinter import _tkinter as tk -else: - from Tkinter import tkinter as tk - -if hasattr(sys, 'pypy_find_executable'): +if hasattr(sys, "pypy_find_executable"): # Tested with packages at https://bitbucket.org/pypy/pypy/downloads. # PyPies 1.6, 2.0 do not have tkinter built in. PyPy3-2.3.1 gives an # OSError trying to import tkinter. Otherwise: diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 5828c2c24..755b4b272 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,20 +1,20 @@ import os import sys -py3 = sys.version_info.major >= 3 +py36 = sys.version_info[0:2] >= (3, 6) -if py3: - def isStringType(t): - return isinstance(t, str) + +if py36: + from pathlib import Path + + def isPath(f): + return isinstance(f, (bytes, str, Path)) + + +else: def isPath(f): return isinstance(f, (bytes, str)) -else: - def isStringType(t): - return isinstance(t, basestring) # noqa: F821 - - def isPath(f): - return isinstance(f, basestring) # noqa: F821 # Checks if an object is a string, and that it points to a directory. @@ -22,7 +22,7 @@ def isDirectory(f): return isPath(f) and os.path.isdir(f) -class deferred_error(object): +class deferred_error: def __init__(self, ex): self.ex = ex diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 0b0d90e80..eddf15683 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = '6.0.0.dev0' +__version__ = "7.0.0.dev0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 6530038b7..5822febab 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -1,3 +1,9 @@ +import collections +import os +import sys + +import PIL + from . import Image modules = { @@ -26,12 +32,7 @@ def get_supported_modules(): return [f for f in modules if check_module(f)] -codecs = { - "jpg": "jpeg", - "jpg_2000": "jpeg2k", - "zlib": "zip", - "libtiff": "libtiff" -} +codecs = {"jpg": "jpeg", "jpg_2000": "jpeg2k", "zlib": "zip", "libtiff": "libtiff"} def check_codec(feature): @@ -48,11 +49,12 @@ def get_supported_codecs(): features = { - "webp_anim": ("PIL._webp", 'HAVE_WEBPANIM'), - "webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'), + "webp_anim": ("PIL._webp", "HAVE_WEBPANIM"), + "webp_mux": ("PIL._webp", "HAVE_WEBPMUX"), "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), "raqm": ("PIL._imagingft", "HAVE_RAQM"), "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO"), + "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT"), } @@ -63,7 +65,7 @@ def check_feature(feature): module, flag = features[feature] try: - imported_module = __import__(module, fromlist=['PIL']) + imported_module = __import__(module, fromlist=["PIL"]) return getattr(imported_module, flag) except ImportError: return None @@ -74,9 +76,14 @@ def get_supported_features(): def check(feature): - return (feature in modules and check_module(feature) or - feature in codecs and check_codec(feature) or - feature in features and check_feature(feature)) + return ( + feature in modules + and check_module(feature) + or feature in codecs + and check_codec(feature) + or feature in features + and check_feature(feature) + ) def get_supported(): @@ -84,3 +91,80 @@ def get_supported(): ret.extend(get_supported_features()) ret.extend(get_supported_codecs()) return ret + + +def pilinfo(out=None, supported_formats=True): + if out is None: + out = sys.stdout + + Image.init() + + 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__)), + file=out, + ) + print( + "Binary modules loaded from {}".format(os.path.dirname(Image.core.__file__)), + file=out, + ) + print("-" * 68, file=out) + + 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)"), + ("libimagequant", "LIBIMAGEQUANT (Quantization method)"), + ]: + if check(name): + print("---", feature, "support ok", file=out) + else: + print("***", feature, "support not installed", file=out) + print("-" * 68, file=out) + + if supported_formats: + extensions = collections.defaultdict(list) + for ext, i in Image.EXTENSION.items(): + extensions[i].append(ext) + + for i in sorted(Image.ID): + line = "{}".format(i) + if i in Image.MIME: + line = "{} {}".format(line, Image.MIME[i]) + print(line, file=out) + + if i in extensions: + print( + "Extensions: {}".format(", ".join(sorted(extensions[i]))), file=out + ) + + features = [] + if i in Image.OPEN: + features.append("open") + if i in Image.SAVE: + features.append("save") + if i in Image.SAVE_ALL: + features.append("save_all") + if i in Image.DECODERS: + features.append("decode") + if i in Image.ENCODERS: + features.append("encode") + + print("Features: {}".format(", ".join(features)), file=out) + print("-" * 68, file=out) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 991cf1c95..59801f58e 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -149,17 +149,18 @@ PyImagingPhotoPut(ClientData clientdata, Tcl_Interp* interp, return TCL_OK; } -/* Warning -- this does not work at all */ static int PyImagingPhotoGet(ClientData clientdata, Tcl_Interp* interp, int argc, const char **argv) { + Imaging im; Tk_PhotoHandle photo; Tk_PhotoImageBlock block; + int x, y, z; - if (argc != 2) { + if (argc != 3) { TCL_APPEND_RESULT(interp, "usage: ", argv[0], - " srcPhoto", (char *) NULL); + " srcPhoto destImage", (char *) NULL); return TCL_ERROR; } @@ -172,21 +173,26 @@ PyImagingPhotoGet(ClientData clientdata, Tcl_Interp* interp, return TCL_ERROR; } + /* get PIL Image handle */ + im = ImagingFind(argv[2]); + if (!im) { + TCL_APPEND_RESULT(interp, "bad name", (char*) NULL); + return TCL_ERROR; + } + TK_PHOTO_GET_IMAGE(photo, &block); - printf("pixelPtr = %p\n", block.pixelPtr); - printf("width = %d\n", block.width); - printf("height = %d\n", block.height); - printf("pitch = %d\n", block.pitch); - printf("pixelSize = %d\n", block.pixelSize); - printf("offset = %d %d %d %d\n", block.offset[0], block.offset[1], - block.offset[2], block.offset[3]); + for (y = 0; y < block.height; y++) { + UINT8* out = (UINT8*)im->image32[y]; + for (x = 0; x < block.pitch; x += block.pixelSize) { + for (z=0; z < block.pixelSize; z++) { + int offset = block.offset[z]; + out[x + offset] = block.pixelPtr[y * block.pitch + x + offset]; + } + } + } - TCL_APPEND_RESULT( - interp, "this function is not yet supported", (char *) NULL - ); - - return TCL_ERROR; + return TCL_OK; } @@ -219,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) { @@ -348,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; @@ -358,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 7715a7995..b0cbfc665 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -71,6 +71,7 @@ * See the README file for information on usage and redistribution. */ +#define PY_SSIZE_T_CLEAN #include "Python.h" #ifdef HAVE_LIBJPEG @@ -83,7 +84,8 @@ #include "Imaging.h" -#include "py3.h" +#define _USE_MATH_DEFINES +#include /* Configuration stuff. Feel free to undef things you don't need. */ #define WITH_IMAGECHOPS /* ImageChops support */ @@ -233,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 } /* -------------------------------------------------------------------- */ @@ -378,7 +348,8 @@ getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type) Py_ssize_t i, n; int itemp; double dtemp; - void* list; + FLOAT32 ftemp; + UINT8* list; PyObject* seq; PyObject* op; @@ -411,20 +382,20 @@ 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); - ((UINT8*)list)[i] = CLIP8(itemp); + itemp = PyLong_AsLong(op); + list[i] = CLIP8(itemp); break; case TYPE_INT32: - itemp = PyInt_AsLong(op); - ((INT32*)list)[i] = itemp; + itemp = PyLong_AsLong(op); + memcpy(list + i * sizeof(INT32), &itemp, sizeof(itemp)); break; case TYPE_FLOAT32: - dtemp = PyFloat_AsDouble(op); - ((FLOAT32*)list)[i] = (FLOAT32) dtemp; + ftemp = (FLOAT32)PyFloat_AsDouble(op); + memcpy(list + i * sizeof(ftemp), &ftemp, sizeof(ftemp)); break; case TYPE_DOUBLE: dtemp = PyFloat_AsDouble(op); - ((double*)list)[i] = (double) dtemp; + memcpy(list + i * sizeof(dtemp), &dtemp, sizeof(dtemp)); break; } } @@ -494,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: @@ -504,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; } @@ -528,6 +499,8 @@ getink(PyObject* color, Imaging im, char* ink) to return it into a 32 bit C long */ PY_LONG_LONG r = 0; + FLOAT32 ftmp; + INT32 itmp; /* fill ink buffer (four bytes) with something that can be cast to either UINT8 or INT32 */ @@ -536,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()) { @@ -593,14 +558,16 @@ getink(PyObject* color, Imaging im, char* ink) /* signed integer */ if (rIsInt != 1) return NULL; - *(INT32*) ink = r; + itmp = r; + memcpy(ink, &itmp, sizeof(itmp)); return ink; case IMAGING_TYPE_FLOAT32: /* floating point */ f = PyFloat_AsDouble(color); if (f == -1.0 && PyErr_Occurred()) return NULL; - *(FLOAT32*) ink = (FLOAT32) f; + ftmp = f; + memcpy(ink, &ftmp, sizeof(ftmp)); return ink; case IMAGING_TYPE_SPECIAL: if (strncmp(im->mode, "I;16", 4) == 0) { @@ -795,15 +762,19 @@ _prepare_lut_table(PyObject* table, Py_ssize_t table_size) } for (i = 0; i < table_size; i++) { + FLOAT16 htmp; + double dtmp; switch (data_type) { case TYPE_FLOAT16: - item = float16tofloat32(((FLOAT16*) table_data)[i]); + memcpy(&htmp, ((char*) table_data) + i * sizeof(htmp), sizeof(htmp)); + item = float16tofloat32(htmp); break; case TYPE_FLOAT32: - item = ((FLOAT32*) table_data)[i]; + memcpy(&item, ((char*) table_data) + i * sizeof(FLOAT32), sizeof(FLOAT32)); break; case TYPE_DOUBLE: - item = ((double*) table_data)[i]; + memcpy(&dtmp, ((char*) table_data) + i * sizeof(dtmp), sizeof(dtmp)); + item = (FLOAT32) dtmp; break; } /* Max value for INT16 */ @@ -1116,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 @@ -1175,65 +1146,74 @@ _getpixel(ImagingObject* self, PyObject* args) return getpixel(self->image, self->access, x, y); } +union hist_extrema { + UINT8 u[2]; + INT32 i[2]; + FLOAT32 f[2]; +}; + +static union hist_extrema* +parse_histogram_extremap(ImagingObject* self, PyObject* extremap, + union hist_extrema* ep) +{ + int i0, i1; + double f0, f1; + + if (extremap) { + switch (self->image->type) { + case IMAGING_TYPE_UINT8: + if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) + return NULL; + ep->u[0] = CLIP8(i0); + ep->u[1] = CLIP8(i1); + break; + case IMAGING_TYPE_INT32: + if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) + return NULL; + ep->i[0] = i0; + ep->i[1] = i1; + break; + case IMAGING_TYPE_FLOAT32: + if (!PyArg_ParseTuple(extremap, "dd", &f0, &f1)) + return NULL; + ep->f[0] = (FLOAT32) f0; + ep->f[1] = (FLOAT32) f1; + break; + default: + return NULL; + } + } else { + return NULL; + } + return ep; +} + static PyObject* _histogram(ImagingObject* self, PyObject* args) { ImagingHistogram h; PyObject* list; int i; - union { - UINT8 u[2]; - INT32 i[2]; - FLOAT32 f[2]; - } extrema; - void* ep; - int i0, i1; - double f0, f1; + union hist_extrema extrema; + union hist_extrema* ep; PyObject* extremap = NULL; ImagingObject* maskp = NULL; if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) - return NULL; - - if (extremap) { - ep = &extrema; - switch (self->image->type) { - case IMAGING_TYPE_UINT8: - if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) - return NULL; - /* FIXME: clip */ - extrema.u[0] = i0; - extrema.u[1] = i1; - break; - case IMAGING_TYPE_INT32: - if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) - return NULL; - extrema.i[0] = i0; - extrema.i[1] = i1; - break; - case IMAGING_TYPE_FLOAT32: - if (!PyArg_ParseTuple(extremap, "dd", &f0, &f1)) - return NULL; - extrema.f[0] = (FLOAT32) f0; - extrema.f[1] = (FLOAT32) f1; - break; - default: - ep = NULL; - break; - } - } else - ep = NULL; + return NULL; + /* Using a var to avoid allocations. */ + ep = parse_histogram_extremap(self, extremap, &extrema); h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); if (!h) - return NULL; + return NULL; /* Build an integer list containing the histogram */ 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; @@ -1242,11 +1222,59 @@ _histogram(ImagingObject* self, PyObject* args) PyList_SetItem(list, i, item); } + /* Destroy the histogram structure */ ImagingHistogramDelete(h); return list; } +static PyObject* +_entropy(ImagingObject* self, PyObject* args) +{ + ImagingHistogram h; + int idx, length; + long sum; + double entropy, fsum, p; + union hist_extrema extrema; + union hist_extrema* ep; + + PyObject* extremap = NULL; + ImagingObject* maskp = NULL; + if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) + return NULL; + + /* Using a local var to avoid allocations. */ + ep = parse_histogram_extremap(self, extremap, &extrema); + h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); + + if (!h) + return NULL; + + /* Calculate the histogram entropy */ + /* First, sum the histogram data */ + length = h->bands * 256; + sum = 0; + for (idx = 0; idx < length; idx++) { + sum += h->histogram[idx]; + } + + /* Next, normalize the histogram data, */ + /* using the histogram sum value */ + fsum = (double)sum; + entropy = 0.0; + for (idx = 0; idx < length; idx++) { + p = (double)h->histogram[idx] / fsum; + if (p != 0.0) { + entropy += p * log(p) * M_LOG2E; + } + } + + /* Destroy the histogram structure */ + ImagingHistogramDelete(h); + + return PyFloat_FromDouble(-entropy); +} + #ifdef WITH_MODEFILTER static PyObject* _modefilter(ImagingObject* self, PyObject* args) @@ -1454,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++; } @@ -1564,11 +1592,12 @@ _putpalette(ImagingObject* self, PyObject* args) char* rawmode; UINT8* palette; - int palettesize; - if (!PyArg_ParseTuple(args, "s"PY_ARG_BYTES_LENGTH, &rawmode, &palette, &palettesize)) + Py_ssize_t palettesize; + if (!PyArg_ParseTuple(args, "sy#", &rawmode, &palette, &palettesize)) return NULL; - if (strcmp(self->image->mode, "L") != 0 && strcmp(self->image->mode, "P")) { + if (strcmp(self->image->mode, "L") && strcmp(self->image->mode, "LA") && + strcmp(self->image->mode, "P") && strcmp(self->image->mode, "PA")) { PyErr_SetString(PyExc_ValueError, wrong_mode); return NULL; } @@ -1586,7 +1615,7 @@ _putpalette(ImagingObject* self, PyObject* args) ImagingPaletteDelete(self->image->palette); - strcpy(self->image->mode, "P"); + strcpy(self->image->mode, strlen(self->image->mode) == 2 ? "PA" : "P"); self->image->palette = ImagingPaletteNew("RGB"); @@ -1626,8 +1655,8 @@ _putpalettealphas(ImagingObject* self, PyObject* args) { int i; UINT8 *values; - int length; - if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH, &values, &length)) + Py_ssize_t length; + if (!PyArg_ParseTuple(args, "y#", &values, &length)) return NULL; if (!self->image->palette) { @@ -1770,7 +1799,7 @@ im_setmode(ImagingObject* self, PyObject* args) Imaging im; char* mode; - int modelen; + Py_ssize_t modelen; if (!PyArg_ParseTuple(args, "s#:setmode", &mode, &modelen)) return NULL; @@ -2065,9 +2094,9 @@ _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, - xprofile, self->image->xsize, - yprofile, self->image->ysize); + result = Py_BuildValue("y#y#", + xprofile, (Py_ssize_t)self->image->xsize, + yprofile, (Py_ssize_t)self->image->ysize); free(xprofile); free(yprofile); @@ -2342,8 +2371,8 @@ _font_new(PyObject* self_, PyObject* args) ImagingObject* imagep; unsigned char* glyphdata; - int glyphdata_length; - if (!PyArg_ParseTuple(args, "O!"PY_ARG_BYTES_LENGTH, + Py_ssize_t glyphdata_length; + if (!PyArg_ParseTuple(args, "O!y#", &Imaging_Type, &imagep, &glyphdata, &glyphdata_length)) return NULL; @@ -2571,14 +2600,13 @@ _draw_ink(ImagingDrawObject* self, PyObject* args) { INT32 ink = 0; PyObject* color; - char* mode = NULL; /* not used in this release */ - if (!PyArg_ParseTuple(args, "O|s", &color, &mode)) + if (!PyArg_ParseTuple(args, "O", &color)) return NULL; if (!getink(color, self->image->image, (char*) &ink)) return NULL; - return PyInt_FromLong((int) ink); + return PyLong_FromLong((int) ink); } static PyObject* @@ -3191,6 +3219,7 @@ static struct PyMethodDef methods[] = { {"expand", (PyCFunction)_expand_image, 1}, {"filter", (PyCFunction)_filter, 1}, {"histogram", (PyCFunction)_histogram, 1}, + {"entropy", (PyCFunction)_entropy, 1}, #ifdef WITH_MODEFILTER {"modefilter", (PyCFunction)_modefilter, 1}, #endif @@ -3285,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* @@ -3504,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; } @@ -3542,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* @@ -3551,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* @@ -3560,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* @@ -3623,6 +3652,12 @@ _set_blocks_max(PyObject* self, PyObject* args) "blocks_max should be greater than 0"); return NULL; } + else if ( blocks_max > SIZE_MAX/sizeof(ImagingDefaultArena.blocks_pool[0])) { + PyErr_SetString(PyExc_ValueError, + "blocks_max is too large"); + return NULL; + } + if ( ! ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max)) { ImagingError_MemoryError(); @@ -3857,6 +3892,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); @@ -3882,7 +3923,6 @@ setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imaging(void) { PyObject* m; @@ -3902,12 +3942,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 5e4196cb7..0b22ab695 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -25,13 +25,13 @@ kevin@cazabon.com\n\ http://www.cazabon.com\n\ " +#define PY_SSIZE_T_CLEAN #include "Python.h" // Include before wchar.h so _GNU_SOURCE is set #include "wchar.h" #include "datetime.h" #include "lcms2.h" #include "Imaging.h" -#include "py3.h" #define PYCMSVERSION "1.0.0 pil" @@ -120,14 +120,9 @@ cms_profile_fromstring(PyObject* self, PyObject* args) cmsHPROFILE hProfile; char* pProfile; - int nProfile; -#if PY_VERSION_HEX >= 0x03000000 + Py_ssize_t nProfile; 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) { @@ -171,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; @@ -591,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 @@ -690,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; } @@ -735,12 +722,12 @@ _xyz3_py(cmsCIEXYZ* XYZ) cmsXYZ2xyY(&xyY[2], &XYZ[2]); return Py_BuildValue("(((d,d,d),(d,d,d),(d,d,d)),((d,d,d),(d,d,d),(d,d,d)))", - XYZ[0].X, XYZ[0].Y, XYZ[0].Z, - XYZ[1].X, XYZ[1].Y, XYZ[1].Z, - XYZ[2].X, XYZ[2].Y, XYZ[2].Z, - xyY[0].x, xyY[0].y, xyY[0].Y, - xyY[1].x, xyY[1].y, xyY[1].Y, - xyY[2].x, xyY[2].y, xyY[2].Y); + XYZ[0].X, XYZ[0].Y, XYZ[0].Z, + XYZ[1].X, XYZ[1].Y, XYZ[1].Z, + XYZ[2].X, XYZ[2].Y, XYZ[2].Z, + xyY[0].x, xyY[0].y, xyY[0].Y, + xyY[1].x, xyY[1].y, xyY[1].Y, + xyY[2].x, xyY[2].y, xyY[2].Y); } static PyObject* @@ -783,9 +770,9 @@ _profile_read_ciexyy_triple(CmsProfileObject* self, cmsTagSignature info) /* Note: lcms does all the heavy lifting and error checking (nr of channels == 3). */ return Py_BuildValue("((d,d,d),(d,d,d),(d,d,d)),", - triple->Red.x, triple->Red.y, triple->Red.Y, - triple->Green.x, triple->Green.y, triple->Green.Y, - triple->Blue.x, triple->Blue.y, triple->Blue.Y); + triple->Red.x, triple->Red.y, triple->Red.Y, + triple->Green.x, triple->Green.y, triple->Green.Y, + triple->Blue.x, triple->Blue.y, triple->Blue.Y); } static PyObject* @@ -817,12 +804,12 @@ _profile_read_named_color_list(CmsProfileObject* self, cmsTagSignature info) for (i = 0; i < n; i++) { PyObject* str; cmsNamedColorInfo(ncl, i, name, NULL, NULL, NULL, NULL); - str = PyUnicode_FromString(name); - if (str == NULL) { - Py_DECREF(result); - Py_INCREF(Py_None); - return Py_None; - } + str = PyUnicode_FromString(name); + if (str == NULL) { + Py_DECREF(result); + Py_INCREF(Py_None); + return Py_None; + } PyList_SET_ITEM(result, i, str); } @@ -844,9 +831,9 @@ static cmsBool _calculate_rgb_primaries(CmsProfileObject* self, cmsCIEXYZTRIPLE* // transform from our profile to XYZ using doubles for highest precision hTransform = cmsCreateTransform(self->profile, TYPE_RGB_DBL, - hXYZ, TYPE_XYZ_DBL, - INTENT_RELATIVE_COLORIMETRIC, - cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); + hXYZ, TYPE_XYZ_DBL, + INTENT_RELATIVE_COLORIMETRIC, + cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); cmsCloseProfile(hXYZ); if (hTransform == NULL) return 0; @@ -885,31 +872,31 @@ _is_intent_supported(CmsProfileObject* self, int clut) n = cmsGetSupportedIntents(INTENTS, - intent_ids, - intent_descs); + intent_ids, + intent_descs); for (i = 0; i < n; i++) { int intent = (int) intent_ids[i]; PyObject* id; - PyObject* entry; + PyObject* entry; - /* Only valid for ICC Intents (otherwise we read invalid memory in lcms cmsio1.c). */ - if (!(intent == INTENT_PERCEPTUAL || intent == INTENT_RELATIVE_COLORIMETRIC - || intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) - continue; + /* Only valid for ICC Intents (otherwise we read invalid memory in lcms cmsio1.c). */ + if (!(intent == INTENT_PERCEPTUAL || intent == INTENT_RELATIVE_COLORIMETRIC + || intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) + continue; - id = PyInt_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, - _check_intent(clut, self->profile, intent, LCMS_USED_AS_PROOF) ? Py_True : Py_False); - if (id == NULL || entry == NULL) { - Py_XDECREF(id); - Py_XDECREF(entry); - Py_XDECREF(result); - Py_INCREF(Py_None); - return Py_None; - } - PyDict_SetItem(result, id, entry); + 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, + _check_intent(clut, self->profile, intent, LCMS_USED_AS_PROOF) ? Py_True : Py_False); + if (id == NULL || entry == NULL) { + Py_XDECREF(id); + Py_XDECREF(entry); + Py_XDECREF(result); + Py_INCREF(Py_None); + return Py_None; + } + PyDict_SetItem(result, id, entry); } return result; } @@ -966,6 +953,8 @@ _profile_getattr(CmsProfileObject* self, cmsInfoType field) static PyObject* cms_profile_getattr_product_desc(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "product_desc is deprecated. Use Unicode profile_description instead.", 1); // description was Description != 'Copyright' || or "%s - %s" (manufacturer, model) in 1.x return _profile_getattr(self, cmsInfoDescription); } @@ -975,42 +964,54 @@ cms_profile_getattr_product_desc(CmsProfileObject* self, void* closure) static PyObject* cms_profile_getattr_product_description(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "product_description is deprecated. Use Unicode profile_description instead.", 1); return _profile_getattr(self, cmsInfoDescription); } static PyObject* cms_profile_getattr_product_model(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "product_model is deprecated. Use Unicode model instead.", 1); return _profile_getattr(self, cmsInfoModel); } static PyObject* cms_profile_getattr_product_manufacturer(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "product_manufacturer is deprecated. Use Unicode manufacturer instead.", 1); return _profile_getattr(self, cmsInfoManufacturer); } static PyObject* cms_profile_getattr_product_copyright(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "product_copyright is deprecated. Use Unicode copyright instead.", 1); return _profile_getattr(self, cmsInfoCopyright); } static PyObject* cms_profile_getattr_rendering_intent(CmsProfileObject* self, void* closure) { - return PyInt_FromLong(cmsGetHeaderRenderingIntent(self->profile)); + return PyLong_FromLong(cmsGetHeaderRenderingIntent(self->profile)); } static PyObject* cms_profile_getattr_pcs(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "pcs is deprecated. Use padded connection_space instead.", 1); return PyUnicode_DecodeFSDefault(findICmode(cmsGetPCS(self->profile))); } static PyObject* cms_profile_getattr_color_space(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "color_space is deprecated. Use padded xcolor_space instead.", 1); return PyUnicode_DecodeFSDefault(findICmode(cmsGetColorSpace(self->profile))); } @@ -1070,7 +1071,7 @@ cms_profile_getattr_creation_date(CmsProfileObject* self, void* closure) } return PyDateTime_FromDateAndTime(1900 + ct.tm_year, ct.tm_mon, ct.tm_mday, - ct.tm_hour, ct.tm_min, ct.tm_sec, 0); + ct.tm_hour, ct.tm_min, ct.tm_sec, 0); } static PyObject* @@ -1083,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* @@ -1100,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* @@ -1295,7 +1296,7 @@ cms_profile_getattr_green_primary(CmsProfileObject* self, void* closure) result = _calculate_rgb_primaries(self, &primaries); if (! result) { Py_INCREF(Py_None); - return Py_None; + return Py_None; } return _xyz_py(&primaries.Green); @@ -1311,7 +1312,7 @@ cms_profile_getattr_blue_primary(CmsProfileObject* self, void* closure) result = _calculate_rgb_primaries(self, &primaries); if (! result) { Py_INCREF(Py_None); - return Py_None; + return Py_None; } return _xyz_py(&primaries.Blue); @@ -1394,11 +1395,11 @@ cms_profile_getattr_icc_measurement_condition (CmsProfileObject* self, void* clo geo = "unknown"; return Py_BuildValue("{s:i,s:(ddd),s:s,s:d,s:s}", - "observer", mc->Observer, - "backing", mc->Backing.X, mc->Backing.Y, mc->Backing.Z, - "geo", geo, - "flare", mc->Flare, - "illuminant_type", _illu_map(mc->IlluminantType)); + "observer", mc->Observer, + "backing", mc->Backing.X, mc->Backing.Y, mc->Backing.Z, + "geo", geo, + "flare", mc->Flare, + "illuminant_type", _illu_map(mc->IlluminantType)); } static PyObject* @@ -1419,9 +1420,9 @@ cms_profile_getattr_icc_viewing_condition (CmsProfileObject* self, void* closure } return Py_BuildValue("{s:(ddd),s:(ddd),s:s}", - "illuminant", vc->IlluminantXYZ.X, vc->IlluminantXYZ.Y, vc->IlluminantXYZ.Z, - "surround", vc->SurroundXYZ.X, vc->SurroundXYZ.Y, vc->SurroundXYZ.Z, - "illuminant_type", _illu_map(vc->IlluminantType)); + "illuminant", vc->IlluminantXYZ.X, vc->IlluminantXYZ.Y, vc->IlluminantXYZ.Z, + "surround", vc->SurroundXYZ.X, vc->SurroundXYZ.Y, vc->SurroundXYZ.Z, + "illuminant_type", _illu_map(vc->IlluminantType)); } @@ -1596,7 +1597,6 @@ setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingcms(void) { PyObject* m; @@ -1618,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 f94e55803..62a4c283e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -18,15 +18,18 @@ * Copyright (c) 1998-2007 by Secret Labs AB */ +#define PY_SSIZE_T_CLEAN #include "Python.h" #include "Imaging.h" #include #include FT_FREETYPE_H #include FT_GLYPH_H +#include FT_STROKER_H +#include FT_MULTIPLE_MASTERS_H +#include FT_SFNT_NAMES_H #define KEEP_PY_UNICODE -#include "py3.h" #if !defined(_MSC_VER) #include @@ -53,7 +56,7 @@ typedef struct { - int index, x_offset, x_advance, y_offset; + int index, x_offset, x_advance, y_offset, y_advance; unsigned int cluster; } GlyphInfo; @@ -78,6 +81,9 @@ typedef struct { static PyTypeObject Font_Type; +typedef bool (*t_raqm_version_atleast)(unsigned int major, + unsigned int minor, + unsigned int micro); typedef raqm_t* (*t_raqm_create)(void); typedef int (*t_raqm_set_text)(raqm_t *rq, const uint32_t *text, @@ -87,6 +93,10 @@ typedef bool (*t_raqm_set_text_utf8) (raqm_t *rq, size_t len); typedef bool (*t_raqm_set_par_direction) (raqm_t *rq, raqm_direction_t dir); +typedef bool (*t_raqm_set_language) (raqm_t *rq, + const char *lang, + size_t start, + size_t len); typedef bool (*t_raqm_add_font_feature) (raqm_t *rq, const char *feature, int len); @@ -102,10 +112,12 @@ typedef void (*t_raqm_destroy) (raqm_t *rq); typedef struct { void* raqm; int version; + t_raqm_version_atleast version_atleast; t_raqm_create create; t_raqm_set_text set_text; t_raqm_set_text_utf8 set_text_utf8; t_raqm_set_par_direction set_par_direction; + t_raqm_set_language set_language; t_raqm_add_font_feature add_font_feature; t_raqm_set_freetype_face set_freetype_face; t_raqm_layout layout; @@ -156,10 +168,12 @@ setraqm(void) } #if !defined(_MSC_VER) + p_raqm.version_atleast = (t_raqm_version_atleast)dlsym(p_raqm.raqm, "raqm_version_atleast"); p_raqm.create = (t_raqm_create)dlsym(p_raqm.raqm, "raqm_create"); p_raqm.set_text = (t_raqm_set_text)dlsym(p_raqm.raqm, "raqm_set_text"); p_raqm.set_text_utf8 = (t_raqm_set_text_utf8)dlsym(p_raqm.raqm, "raqm_set_text_utf8"); p_raqm.set_par_direction = (t_raqm_set_par_direction)dlsym(p_raqm.raqm, "raqm_set_par_direction"); + p_raqm.set_language = (t_raqm_set_language)dlsym(p_raqm.raqm, "raqm_set_language"); p_raqm.add_font_feature = (t_raqm_add_font_feature)dlsym(p_raqm.raqm, "raqm_add_font_feature"); p_raqm.set_freetype_face = (t_raqm_set_freetype_face)dlsym(p_raqm.raqm, "raqm_set_freetype_face"); p_raqm.layout = (t_raqm_layout)dlsym(p_raqm.raqm, "raqm_layout"); @@ -176,6 +190,7 @@ setraqm(void) p_raqm.set_text && p_raqm.set_text_utf8 && p_raqm.set_par_direction && + p_raqm.set_language && p_raqm.add_font_feature && p_raqm.set_freetype_face && p_raqm.layout && @@ -186,10 +201,12 @@ setraqm(void) return 2; } #else + p_raqm.version_atleast = (t_raqm_version_atleast)GetProcAddress(p_raqm.raqm, "raqm_version_atleast"); p_raqm.create = (t_raqm_create)GetProcAddress(p_raqm.raqm, "raqm_create"); p_raqm.set_text = (t_raqm_set_text)GetProcAddress(p_raqm.raqm, "raqm_set_text"); p_raqm.set_text_utf8 = (t_raqm_set_text_utf8)GetProcAddress(p_raqm.raqm, "raqm_set_text_utf8"); p_raqm.set_par_direction = (t_raqm_set_par_direction)GetProcAddress(p_raqm.raqm, "raqm_set_par_direction"); + p_raqm.set_language = (t_raqm_set_language)GetProcAddress(p_raqm.raqm, "raqm_set_language"); p_raqm.add_font_feature = (t_raqm_add_font_feature)GetProcAddress(p_raqm.raqm, "raqm_add_font_feature"); p_raqm.set_freetype_face = (t_raqm_set_freetype_face)GetProcAddress(p_raqm.raqm, "raqm_set_freetype_face"); p_raqm.layout = (t_raqm_layout)GetProcAddress(p_raqm.raqm, "raqm_layout"); @@ -205,6 +222,7 @@ setraqm(void) p_raqm.set_text && p_raqm.set_text_utf8 && p_raqm.set_par_direction && + p_raqm.set_language && p_raqm.add_font_feature && p_raqm.set_freetype_face && p_raqm.layout && @@ -228,12 +246,12 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) int error = 0; char* filename = NULL; - int size; - int index = 0; - int layout_engine = 0; + Py_ssize_t size; + Py_ssize_t index = 0; + Py_ssize_t layout_engine = 0; unsigned char* encoding; unsigned char* font_bytes; - int font_bytes_size = 0; + Py_ssize_t font_bytes_size = 0; static char* kwlist[] = { "filename", "size", "index", "encoding", "font_bytes", "layout_engine", NULL @@ -247,8 +265,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) return NULL; } - if (!PyArg_ParseTupleAndKeywords(args, kw, "eti|is"PY_ARG_BYTES_LENGTH"i", - kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kw, "etn|nsy#n", kwlist, Py_FileSystemDefaultEncoding, &filename, &size, &index, &encoding, &font_bytes, &font_bytes_size, &layout_engine)) { @@ -297,6 +314,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) if (error) { if (self->font_bytes) { PyMem_Free(self->font_bytes); + self->font_bytes = NULL; } Py_DECREF(self); return geterror(error); @@ -309,35 +327,20 @@ static int font_getchar(PyObject* string, int index, FT_ULong* char_out) { if (PyUnicode_Check(string)) { - Py_UNICODE* p = PyUnicode_AS_UNICODE(string); - int size = PyUnicode_GET_SIZE(string); - if (index >= size) + if (index >= PyUnicode_GET_LENGTH(string)) return 0; - *char_out = p[index]; + *char_out = PyUnicode_READ_CHAR(string, 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 - return 0; } static size_t -text_layout_raqm(PyObject* string, FontObject* self, const char* dir, - PyObject *features ,GlyphInfo **glyph_info, int mask) +text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject *features, + const char* lang, GlyphInfo **glyph_info, int mask) { - int i = 0; + size_t i = 0, count = 0, start = 0; raqm_t *rq; - size_t count = 0; raqm_glyph_t *glyphs = NULL; raqm_glyph_t_01 *glyphs_01 = NULL; raqm_direction_t direction; @@ -348,6 +351,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, goto failed; } +#if (defined(PYPY_VERSION_NUM) && (PYPY_VERSION_NUM < 0x07020000)) if (PyUnicode_Check(string)) { Py_UNICODE *text = PyUnicode_AS_UNICODE(string); Py_ssize_t size = PyUnicode_GET_SIZE(string); @@ -360,18 +364,34 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); goto failed; } + if (lang) { + if (!(*p_raqm.set_language)(rq, lang, start, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); + goto failed; + } + } } -#if PY_VERSION_HEX < 0x03000000 - else if (PyString_Check(string)) { - char *text = PyString_AS_STRING(string); - int size = PyString_GET_SIZE(string); - if (! size) { +#else + if (PyUnicode_Check(string)) { + Py_UCS4 *text = PyUnicode_AsUCS4Copy(string); + Py_ssize_t size = PyUnicode_GET_LENGTH(string); + if (!text || !size) { + /* return 0 and clean up, no glyphs==no size, + and raqm fails with empty strings */ goto failed; } - if (!(*p_raqm.set_text_utf8)(rq, text, size)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_text_utf8() failed"); + int set_text = (*p_raqm.set_text)(rq, text, size); + PyMem_Free(text); + if (!set_text) { + PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); goto failed; } + if (lang) { + if (!(*p_raqm.set_language)(rq, lang, start, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); + goto failed; + } + } } #endif else { @@ -385,9 +405,13 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, direction = RAQM_DIRECTION_RTL; else if (strcmp(dir, "ltr") == 0) direction = RAQM_DIRECTION_LTR; - else if (strcmp(dir, "ttb") == 0) + else if (strcmp(dir, "ttb") == 0) { direction = RAQM_DIRECTION_TTB; - else { + if (p_raqm.version_atleast == NULL || !(*p_raqm.version_atleast)(0, 7, 0)) { + PyErr_SetString(PyExc_ValueError, "libraqm 0.7 or greater required for 'ttb' direction"); + goto failed; + } + } else { PyErr_SetString(PyExc_ValueError, "direction must be either 'rtl', 'ltr' or 'ttb'"); goto failed; } @@ -399,24 +423,20 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, } if (features != Py_None) { - int len; + int j, len; PyObject *seq = PySequence_Fast(features, "expected a sequence"); if (!seq) { goto failed; } len = PySequence_Size(seq); - for (i = 0; i < len; i++) { - PyObject *item = PySequence_Fast_GET_ITEM(seq, i); + for (j = 0; j < len; j++) { + PyObject *item = PySequence_Fast_GET_ITEM(seq, j); char *feature = NULL; 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; } @@ -428,12 +448,6 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, 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; @@ -451,7 +465,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, goto failed; } - if (p_raqm.version == 1){ + if (p_raqm.version == 1) { glyphs_01 = (*p_raqm.get_glyphs_01)(rq, &count); if (glyphs_01 == NULL) { PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); @@ -474,12 +488,13 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, goto failed; } - if (p_raqm.version == 1){ + if (p_raqm.version == 1) { for (i = 0; i < count; i++) { (*glyph_info)[i].index = glyphs_01[i].index; (*glyph_info)[i].x_offset = glyphs_01[i].x_offset; (*glyph_info)[i].x_advance = glyphs_01[i].x_advance; (*glyph_info)[i].y_offset = glyphs_01[i].y_offset; + (*glyph_info)[i].y_advance = glyphs_01[i].y_advance; (*glyph_info)[i].cluster = glyphs_01[i].cluster; } } else { @@ -488,6 +503,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, (*glyph_info)[i].x_offset = glyphs[i].x_offset; (*glyph_info)[i].x_advance = glyphs[i].x_advance; (*glyph_info)[i].y_offset = glyphs[i].y_offset; + (*glyph_info)[i].y_advance = glyphs[i].y_advance; (*glyph_info)[i].cluster = glyphs[i].cluster; } } @@ -498,8 +514,8 @@ failed: } static size_t -text_layout_fallback(PyObject* string, FontObject* self, const char* dir, - PyObject *features ,GlyphInfo **glyph_info, int mask) +text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObject *features, + const char* lang, GlyphInfo **glyph_info, int mask) { int error, load_flags; FT_ULong ch; @@ -509,14 +525,10 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, FT_UInt last_index = 0; int i; - if (features != Py_None || dir != NULL) { - PyErr_SetString(PyExc_KeyError, "setting text direction or font features is not supported without libraqm"); + 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; } @@ -554,9 +566,11 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, if (FT_Get_Kerning(self->face, last_index, (*glyph_info)[i].index, ft_kerning_default,&delta) == 0) (*glyph_info)[i-1].x_advance += PIXEL(delta.x); + (*glyph_info)[i-1].y_advance += PIXEL(delta.y); } (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; + (*glyph_info)[i].y_advance = glyph->metrics.vertAdvance; last_index = (*glyph_info)[i].index; (*glyph_info)[i].cluster = ch; } @@ -564,15 +578,15 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, } static size_t -text_layout(PyObject* string, FontObject* self, const char* dir, - PyObject *features, GlyphInfo **glyph_info, int mask) +text_layout(PyObject* string, FontObject* self, const char* dir, PyObject *features, + const char* lang, GlyphInfo **glyph_info, int mask) { size_t count; if (p_raqm.raqm && self->layout_engine == LAYOUT_RAQM) { - count = text_layout_raqm(string, self, dir, features, glyph_info, mask); + count = text_layout_raqm(string, self, dir, features, lang, glyph_info, mask); } else { - count = text_layout_fallback(string, self, dir, features, glyph_info, mask); + count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask); } return count; } @@ -580,32 +594,34 @@ text_layout(PyObject* string, FontObject* self, const char* dir, static PyObject* font_getsize(FontObject* self, PyObject* args) { - int i, x, y_max, y_min; + int x_position, x_max, x_min, y_max, y_min; FT_Face face; int xoffset, yoffset; + int horizontal_dir; const char *dir = NULL; - size_t count; + const char *lang = NULL; + size_t i, count; GlyphInfo *glyph_info = NULL; PyObject *features = Py_None; /* calculate size and bearing for a given string */ PyObject* string; - if (!PyArg_ParseTuple(args, "O|zO:getsize", &string, &dir, &features)) + if (!PyArg_ParseTuple(args, "O|zOz:getsize", &string, &dir, &features, &lang)) return NULL; - face = NULL; - xoffset = yoffset = 0; - y_max = y_min = 0; - - count = text_layout(string, self, dir, features, &glyph_info, 0); + count = text_layout(string, self, dir, features, lang, &glyph_info, 0); if (PyErr_Occurred()) { return NULL; } + face = NULL; + xoffset = yoffset = 0; + x_position = x_max = x_min = y_max = y_min = 0; - for (x = i = 0; i < count; i++) { - int index, error; + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + for (i = 0; i < count; i++) { + int index, error, offset, x_advanced; FT_BBox bbox; FT_Glyph glyph; face = self->face; @@ -617,35 +633,62 @@ font_getsize(FontObject* self, PyObject* args) if (error) return geterror(error); - if (i == 0 && face->glyph->metrics.horiBearingX < 0) { - xoffset = face->glyph->metrics.horiBearingX; - x -= xoffset; - } - - x += glyph_info[i].x_advance; - - if (i == count - 1) - { - int offset; - offset = glyph_info[i].x_advance - - face->glyph->metrics.width - - face->glyph->metrics.horiBearingX; - if (offset < 0) - x -= offset; + if (i == 0) { + if (horizontal_dir) { + if (face->glyph->metrics.horiBearingX < 0) { + xoffset = face->glyph->metrics.horiBearingX; + x_position -= xoffset; + } + } else { + if (face->glyph->metrics.vertBearingY < 0) { + yoffset = face->glyph->metrics.vertBearingY; + y_max -= yoffset; + } + } } FT_Get_Glyph(face->glyph, &glyph); FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &bbox); - bbox.yMax -= glyph_info[i].y_offset; - bbox.yMin -= glyph_info[i].y_offset; - if (bbox.yMax > y_max) - y_max = bbox.yMax; - if (bbox.yMin < y_min) - y_min = bbox.yMin; + if (horizontal_dir) { + x_position += glyph_info[i].x_advance; - /* find max distance of baseline from top */ - if (face->glyph->metrics.horiBearingY > yoffset) - yoffset = face->glyph->metrics.horiBearingY; + x_advanced = x_position; + offset = glyph_info[i].x_advance - + face->glyph->metrics.width - + face->glyph->metrics.horiBearingX; + if (offset < 0) + x_advanced -= offset; + if (x_advanced > x_max) + x_max = x_advanced; + + bbox.yMax += glyph_info[i].y_offset; + bbox.yMin += glyph_info[i].y_offset; + if (bbox.yMax > y_max) + y_max = bbox.yMax; + if (bbox.yMin < y_min) + y_min = bbox.yMin; + + // find max distance of baseline from top + if (face->glyph->metrics.horiBearingY > yoffset) + yoffset = face->glyph->metrics.horiBearingY; + } else { + y_max -= glyph_info[i].y_advance; + + if (i == count - 1) { + // trim end gap from final glyph + int offset; + offset = -glyph_info[i].y_advance - + face->glyph->metrics.height - + face->glyph->metrics.vertBearingY; + if (offset < 0) + y_max -= offset; + } + + if (bbox.xMax > x_max) + x_max = bbox.xMax; + if (i == 0 || bbox.xMin < x_min) + x_min = bbox.xMin; + } FT_Done_Glyph(glyph); } @@ -656,20 +699,28 @@ font_getsize(FontObject* self, PyObject* args) } if (face) { + if (horizontal_dir) { + // left bearing + if (xoffset < 0) + x_max -= xoffset; + else + xoffset = 0; - /* left bearing */ - if (xoffset < 0) - x -= xoffset; - else - xoffset = 0; - /* difference between the font ascender and the distance of - * the baseline from the top */ - yoffset = PIXEL(self->face->size->metrics.ascender - yoffset); + /* difference between the font ascender and the distance of + * the baseline from the top */ + yoffset = PIXEL(self->face->size->metrics.ascender - yoffset); + } else { + // top bearing + if (yoffset < 0) + y_max -= yoffset; + else + yoffset = 0; + } } return Py_BuildValue( "(ii)(ii)", - PIXEL(x), PIXEL(y_max - y_min), + PIXEL(x_max - x_min), PIXEL(y_max - y_min), PIXEL(xoffset), yoffset ); } @@ -677,12 +728,19 @@ font_getsize(FontObject* self, PyObject* args) static PyObject* font_render(FontObject* self, PyObject* args) { - int i, x, y; + int x; + unsigned int y; Imaging im; - int index, error, ascender; + int index, error, ascender, horizontal_dir; int load_flags; unsigned char *source; - FT_GlyphSlot glyph; + FT_Glyph glyph; + FT_GlyphSlot glyph_slot; + FT_Bitmap bitmap; + FT_BitmapGlyph bitmap_glyph; + int stroke_width = 0; + FT_Stroker stroker = NULL; + FT_Int left; /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ PyObject* string; @@ -690,17 +748,21 @@ font_render(FontObject* self, PyObject* args) int mask = 0; int temp; int xx, x0, x1; + int yy; + unsigned int bitmap_y; const char *dir = NULL; - size_t count; + const char *lang = NULL; + size_t i, count; GlyphInfo *glyph_info; PyObject *features = NULL; - if (!PyArg_ParseTuple(args, "On|izO:render", &string, &id, &mask, &dir, &features)) { + if (!PyArg_ParseTuple(args, "On|izOzi:render", &string, &id, &mask, &dir, &features, &lang, + &stroke_width)) { return NULL; } glyph_info = NULL; - count = text_layout(string, self, dir, features, &glyph_info, mask); + count = text_layout(string, self, dir, features, lang, &glyph_info, mask); if (PyErr_Occurred()) { return NULL; } @@ -708,96 +770,295 @@ font_render(FontObject* self, PyObject* args) Py_RETURN_NONE; } + if (stroke_width) { + error = FT_Stroker_New(library, &stroker); + if (error) { + return geterror(error); + } + + FT_Stroker_Set(stroker, (FT_Fixed)stroke_width*64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); + } + im = (Imaging) id; /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ - load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP; - if (mask) + load_flags = FT_LOAD_NO_BITMAP; + if (stroker == NULL) { + load_flags |= FT_LOAD_RENDER; + } + if (mask) { load_flags |= FT_LOAD_TARGET_MONO; + } ascender = 0; for (i = 0; i < count; i++) { index = glyph_info[i].index; error = FT_Load_Glyph(self->face, index, load_flags); - if (error) + if (error) { return geterror(error); + } - glyph = self->face->glyph; - temp = (glyph->bitmap.rows - glyph->bitmap_top); + glyph_slot = self->face->glyph; + bitmap = glyph_slot->bitmap; + + temp = bitmap.rows - glyph_slot->bitmap_top; temp -= PIXEL(glyph_info[i].y_offset); if (temp > ascender) ascender = temp; } - for (x = i = 0; i < count; i++) { - if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) - x = -self->face->glyph->metrics.horiBearingX; - + x = y = 0; + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + for (i = 0; i < count; i++) { index = glyph_info[i].index; error = FT_Load_Glyph(self->face, index, load_flags); - if (error) + if (error) { return geterror(error); + } - if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) { - x = -self->face->glyph->metrics.horiBearingX; - } + glyph_slot = self->face->glyph; + if (stroker != NULL) { + error = FT_Get_Glyph(glyph_slot, &glyph); + if (!error) { + error = FT_Glyph_Stroke(&glyph, stroker, 1); + } + if (!error) { + FT_Vector origin = {0, 0}; + error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1); + } + if (error) { + return geterror(error); + } - glyph = self->face->glyph; + bitmap_glyph = (FT_BitmapGlyph)glyph; + + bitmap = bitmap_glyph->bitmap; + left = bitmap_glyph->left; + } else { + bitmap = glyph_slot->bitmap; + left = glyph_slot->bitmap_left; + } + + if (horizontal_dir) { + if (i == 0 && glyph_slot->metrics.horiBearingX < 0) { + x = -glyph_slot->metrics.horiBearingX; + } + xx = PIXEL(x) + left; + xx += PIXEL(glyph_info[i].x_offset) + stroke_width; + } else { + if (glyph_slot->metrics.vertBearingX < 0) { + x = -glyph_slot->metrics.vertBearingX; + } + xx = im->xsize / 2 - bitmap.width / 2; + } - source = (unsigned char*) glyph->bitmap.buffer; - xx = PIXEL(x) + glyph->bitmap_left; - xx += PIXEL(glyph_info[i].x_offset); x0 = 0; - x1 = glyph->bitmap.width; + x1 = bitmap.width; if (xx < 0) x0 = -xx; if (xx + x1 > im->xsize) x1 = im->xsize - xx; - if (mask) { - /* use monochrome mask (on palette images, etc) */ - for (y = 0; y < glyph->bitmap.rows; y++) { - int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); - yy -= PIXEL(glyph_info[i].y_offset); - if (yy >= 0 && yy < im->ysize) { - /* blend this glyph into the buffer */ - unsigned char *target = im->image8[yy] + xx; - int i, j, m = 128; - for (i = j = 0; j < x1; j++) { - if (j >= x0 && (source[i] & m)) + source = (unsigned char*) bitmap.buffer; + for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++) { + if (horizontal_dir) { + yy = bitmap_y + im->ysize - (PIXEL(glyph_slot->metrics.horiBearingY) + ascender); + yy -= PIXEL(glyph_info[i].y_offset) + stroke_width * 2; + } else { + yy = bitmap_y + PIXEL(y + glyph_slot->metrics.vertBearingY) + ascender; + yy += PIXEL(glyph_info[i].y_offset); + } + if (yy >= 0 && yy < im->ysize) { + // blend this glyph into the buffer + unsigned char *target = im->image8[yy] + xx; + if (mask) { + // use monochrome mask (on palette images, etc) + int j, k, m = 128; + for (j = k = 0; j < x1; j++) { + if (j >= x0 && (source[k] & m)) target[j] = 255; if (!(m >>= 1)) { m = 128; - i++; + k++; } } - } - source += glyph->bitmap.pitch; - } - } else { - /* use antialiased rendering */ - for (y = 0; y < glyph->bitmap.rows; y++) { - int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); - yy -= PIXEL(glyph_info[i].y_offset); - if (yy >= 0 && yy < im->ysize) { - /* blend this glyph into the buffer */ - - int i; - unsigned char *target = im->image8[yy] + xx; - for (i = x0; i < x1; i++) { - if (target[i] < source[i]) - target[i] = source[i]; + } else { + // use antialiased rendering + int k; + for (k = x0; k < x1; k++) { + if (target[k] < source[k]) + target[k] = source[k]; } } - source += glyph->bitmap.pitch; } + source += bitmap.pitch; } x += glyph_info[i].x_advance; + y -= glyph_info[i].y_advance; + if (stroker != NULL) { + FT_Done_Glyph(glyph); + } } + FT_Stroker_Done(stroker); PyMem_Del(glyph_info); Py_RETURN_NONE; } +#if FREETYPE_MAJOR > 2 ||\ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) ||\ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) + static PyObject* + font_getvarnames(FontObject* self, PyObject* args) + { + int error; + FT_UInt i, j, num_namedstyles, name_count; + FT_MM_Var *master; + FT_SfntName name; + PyObject *list_names, *list_name; + + error = FT_Get_MM_Var(self->face, &master); + if (error) + return geterror(error); + + num_namedstyles = master->num_namedstyles; + list_names = PyList_New(num_namedstyles); + + name_count = FT_Get_Sfnt_Name_Count(self->face); + for (i = 0; i < name_count; i++) { + error = FT_Get_Sfnt_Name(self->face, i, &name); + if (error) + return geterror(error); + + for (j = 0; j < num_namedstyles; j++) { + if (PyList_GetItem(list_names, j) != NULL) + continue; + + if (master->namedstyle[j].strid == name.name_id) { + list_name = Py_BuildValue("y#", name.string, name.string_len); + PyList_SetItem(list_names, j, list_name); + break; + } + } + } + + FT_Done_MM_Var(library, master); + + return list_names; + } + + static PyObject* + font_getvaraxes(FontObject* self, PyObject* args) + { + int error; + FT_UInt i, j, num_axis, name_count; + FT_MM_Var* master; + FT_Var_Axis axis; + FT_SfntName name; + PyObject *list_axes, *list_axis, *axis_name; + error = FT_Get_MM_Var(self->face, &master); + if (error) + return geterror(error); + + num_axis = master->num_axis; + name_count = FT_Get_Sfnt_Name_Count(self->face); + + list_axes = PyList_New(num_axis); + for (i = 0; i < num_axis; i++) { + axis = master->axis[i]; + + list_axis = PyDict_New(); + PyDict_SetItemString(list_axis, "minimum", + PyLong_FromLong(axis.minimum / 65536)); + PyDict_SetItemString(list_axis, "default", + PyLong_FromLong(axis.def / 65536)); + PyDict_SetItemString(list_axis, "maximum", + PyLong_FromLong(axis.maximum / 65536)); + + for (j = 0; j < name_count; j++) { + error = FT_Get_Sfnt_Name(self->face, j, &name); + if (error) + return geterror(error); + + if (name.name_id == axis.strid) { + axis_name = Py_BuildValue("y#", name.string, name.string_len); + PyDict_SetItemString(list_axis, "name", axis_name); + break; + } + } + + PyList_SetItem(list_axes, i, list_axis); + } + + FT_Done_MM_Var(library, master); + + return list_axes; + } + + static PyObject* + font_setvarname(FontObject* self, PyObject* args) + { + int error; + + int instance_index; + if (!PyArg_ParseTuple(args, "i", &instance_index)) + return NULL; + + error = FT_Set_Named_Instance(self->face, instance_index); + if (error) + return geterror(error); + + Py_INCREF(Py_None); + return Py_None; + } + + static PyObject* + font_setvaraxes(FontObject* self, PyObject* args) + { + int error; + + PyObject *axes, *item; + Py_ssize_t i, num_coords; + FT_Fixed *coords; + FT_Fixed coord; + if (!PyArg_ParseTuple(args, "O", &axes)) + return NULL; + + if (!PyList_Check(axes)) { + PyErr_SetString(PyExc_TypeError, "argument must be a list"); + return NULL; + } + + num_coords = PyObject_Length(axes); + coords = malloc(2 * sizeof(coords)); + if (coords == NULL) { + return PyErr_NoMemory(); + } + for (i = 0; i < num_coords; i++) { + item = PyList_GET_ITEM(axes, i); + if (PyFloat_Check(item)) + coord = PyFloat_AS_DOUBLE(item); + else if (PyLong_Check(item)) + coord = (float) PyLong_AS_LONG(item); + else if (PyNumber_Check(item)) + coord = PyFloat_AsDouble(item); + else { + free(coords); + PyErr_SetString(PyExc_TypeError, "list must contain numbers"); + return NULL; + } + coords[i] = coord * 65536; + } + + error = FT_Set_Var_Design_Coordinates(self->face, num_coords, coords); + free(coords); + if (error) + return geterror(error); + + Py_INCREF(Py_None); + return Py_None; + } +#endif + static void font_dealloc(FontObject* self) { @@ -813,70 +1074,68 @@ font_dealloc(FontObject* self) static PyMethodDef font_methods[] = { {"render", (PyCFunction) font_render, METH_VARARGS}, {"getsize", (PyCFunction) font_getsize, METH_VARARGS}, +#if FREETYPE_MAJOR > 2 ||\ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) ||\ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) + {"getvarnames", (PyCFunction) font_getvarnames, METH_VARARGS }, + {"getvaraxes", (PyCFunction) font_getvaraxes, METH_VARARGS }, + {"setvarname", (PyCFunction) font_setvarname, METH_VARARGS}, + {"setvaraxes", (PyCFunction) font_setvaraxes, METH_VARARGS}, +#endif {NULL, NULL} }; 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[] = { @@ -944,11 +1203,7 @@ setup_module(PyObject* m) { FT_Library_Version(library, &major, &minor, &patch); -#if PY_VERSION_HEX >= 0x03000000 v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); -#else - v = PyString_FromFormat("%d.%d.%d", major, minor, patch); -#endif PyDict_SetItemString(d, "freetype2_version", v); @@ -959,7 +1214,6 @@ setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingft(void) { PyObject* m; @@ -979,12 +1233,3 @@ PyInit__imagingft(void) { return m; } -#else -PyMODINIT_FUNC -init_imagingft(void) -{ - PyObject* m = Py_InitModule("_imagingft", _functions); - setup_module(m); -} -#endif - diff --git a/src/_imagingmath.c b/src/_imagingmath.c index ea9f103c6..bc66a581a 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -16,7 +16,6 @@ #include "Python.h" #include "Imaging.h" -#include "py3.h" #include "math.h" #include "float.h" @@ -215,7 +214,7 @@ static PyMethodDef _functions[] = { static void install(PyObject *d, char* name, void* value) { - PyObject *v = PyInt_FromSsize_t((Py_ssize_t) value); + PyObject *v = PyLong_FromSsize_t((Py_ssize_t) value); if (!v || PyDict_SetItemString(d, name, v)) PyErr_Clear(); Py_XDECREF(v); @@ -273,7 +272,6 @@ setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingmath(void) { PyObject* m; @@ -293,12 +291,3 @@ PyInit__imagingmath(void) { return m; } -#else -PyMODINIT_FUNC -init_imagingmath(void) -{ - PyObject* m = Py_InitModule("_imagingmath", _functions); - setup_module(m); -} -#endif - diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index fc8f246cc..050ae9f02 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -13,7 +13,6 @@ #include "Python.h" #include "Imaging.h" -#include "py3.h" #define LUT_SIZE (1<<9) @@ -273,7 +272,6 @@ static PyMethodDef functions[] = { {NULL, NULL, 0, NULL} }; -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingmorph(void) { PyObject* m; @@ -293,12 +291,3 @@ PyInit__imagingmorph(void) { return m; } -#else -PyMODINIT_FUNC -init_imagingmorph(void) -{ - PyObject* m = Py_InitModule("_imagingmorph", functions); - setup_module(m); -} -#endif - diff --git a/src/_imagingtk.c b/src/_imagingtk.c index d0295f317..bdf5e68d1 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -64,7 +64,6 @@ static PyMethodDef functions[] = { {NULL, NULL} /* sentinel */ }; -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingtk(void) { static PyModuleDef module_def = { @@ -78,12 +77,3 @@ PyInit__imagingtk(void) { m = PyModule_Create(&module_def); return (load_tkinter_funcs() == 0) ? m : NULL; } -#else -PyMODINIT_FUNC -init_imagingtk(void) -{ - Py_InitModule("_imagingtk", functions); - load_tkinter_funcs(); -} -#endif - diff --git a/src/_webp.c b/src/_webp.c index 66b6d3268..4581ef89d 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -1,7 +1,6 @@ #define PY_SSIZE_T_CLEAN #include #include "Imaging.h" -#include "py3.h" #include #include #include @@ -557,7 +556,7 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) Py_ssize_t xmp_size; size_t ret_size; - if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH"iiifss#s#s#", + if (!PyArg_ParseTuple(args, "y#iiifss#s#s#", (char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode, &icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) { return NULL; @@ -754,11 +753,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) config.output.u.YUVA.y_size); } -#if PY_VERSION_HEX >= 0x03000000 pymode = PyUnicode_FromString(mode); -#else - pymode = PyString_FromString(mode); -#endif ret = Py_BuildValue("SiiSSS", bytes, config.output.width, config.output.height, pymode, NULL == icc_profile ? Py_None : icc_profile, @@ -848,7 +843,6 @@ static int setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__webp(void) { PyObject* m; @@ -867,11 +861,3 @@ PyInit__webp(void) { return m; } -#else -PyMODINIT_FUNC -init_webp(void) -{ - PyObject* m = Py_InitModule("_webp", webpMethods); - setup_module(m); -} -#endif diff --git a/src/decode.c b/src/decode.c index be1b503e6..5ab6ca9d1 100644 --- a/src/decode.c +++ b/src/decode.c @@ -29,10 +29,10 @@ /* FIXME: make these pluggable! */ +#define PY_SSIZE_T_CLEAN #include "Python.h" #include "Imaging.h" -#include "py3.h" #include "Gif.h" #include "Raw.h" @@ -47,7 +47,7 @@ typedef struct { PyObject_HEAD int (*decode)(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); int (*cleanup)(ImagingCodecState state); struct ImagingCodecStateInstance state; Imaging im; @@ -117,10 +117,11 @@ static PyObject* _decode(ImagingDecoderObject* decoder, PyObject* args) { UINT8* buffer; - int bufsize, status; + Py_ssize_t bufsize; + 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) { @@ -501,9 +502,9 @@ PyImaging_LibTiffDecoderNew(PyObject* self, PyObject* args) char* rawmode; char* compname; int fp; - int ifdoffset; + uint32 ifdoffset; - if (! PyArg_ParseTuple(args, "sssii", &mode, &rawmode, &compname, &fp, &ifdoffset)) + if (! PyArg_ParseTuple(args, "sssiI", &mode, &rawmode, &compname, &fp, &ifdoffset)) return NULL; TRACE(("new tiff decoder %s\n", compname)); diff --git a/src/display.c b/src/display.c index 49143b72e..4c2faf9e0 100644 --- a/src/display.c +++ b/src/display.c @@ -26,7 +26,6 @@ #include "Python.h" #include "Imaging.h" -#include "py3.h" /* -------------------------------------------------------------------- */ /* Windows DIB support */ @@ -187,13 +186,8 @@ _frombytes(ImagingDisplayObject* display, PyObject* args) char* ptr; int bytes; -#if PY_VERSION_HEX >= 0x03000000 if (!PyArg_ParseTuple(args, "y#:frombytes", &ptr, &bytes)) return NULL; -#else - if (!PyArg_ParseTuple(args, "s#:fromstring", &ptr, &bytes)) - return NULL; -#endif if (display->dib->ysize * display->dib->linesize != bytes) { PyErr_SetString(PyExc_ValueError, "wrong size"); @@ -209,13 +203,8 @@ _frombytes(ImagingDisplayObject* display, PyObject* args) static PyObject* _tobytes(ImagingDisplayObject* display, PyObject* args) { -#if PY_VERSION_HEX >= 0x03000000 if (!PyArg_ParseTuple(args, ":tobytes")) return NULL; -#else - if (!PyArg_ParseTuple(args, ":tostring")) - return NULL; -#endif return PyBytes_FromStringAndSize( display->dib->bits, display->dib->ysize * display->dib->linesize @@ -319,14 +308,24 @@ PyImaging_DisplayModeWin32(PyObject* self, PyObject* args) /* -------------------------------------------------------------------- */ /* Windows screen grabber */ +typedef HANDLE(__stdcall* Func_SetThreadDpiAwarenessContext)(HANDLE); + PyObject* PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) { - int width, height; + int x = 0, y = 0, width, height; + int includeLayeredWindows = 0, all_screens = 0; HBITMAP bitmap; BITMAPCOREHEADER core; HDC screen, screen_copy; + DWORD rop; PyObject* buffer; + HANDLE dpiAwareness; + HMODULE user32; + Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; + + if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) + return NULL; /* step 1: create a memory DC large enough to hold the entire screen */ @@ -334,8 +333,32 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) screen = CreateDC("DISPLAY", NULL, NULL, NULL); screen_copy = CreateCompatibleDC(screen); - width = GetDeviceCaps(screen, HORZRES); - height = GetDeviceCaps(screen, VERTRES); + // added in Windows 10 (1607) + // loaded dynamically to avoid link errors + user32 = LoadLibraryA("User32.dll"); + SetThreadDpiAwarenessContext_function = + (Func_SetThreadDpiAwarenessContext) + GetProcAddress(user32, "SetThreadDpiAwarenessContext"); + if (SetThreadDpiAwarenessContext_function != NULL) { + // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) + dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE) -3); + } + + if (all_screens) { + x = GetSystemMetrics(SM_XVIRTUALSCREEN); + y = GetSystemMetrics(SM_YVIRTUALSCREEN); + width = GetSystemMetrics(SM_CXVIRTUALSCREEN); + height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + } else { + width = GetDeviceCaps(screen, HORZRES); + height = GetDeviceCaps(screen, VERTRES); + } + + if (SetThreadDpiAwarenessContext_function != NULL) { + SetThreadDpiAwarenessContext_function(dpiAwareness); + } + + FreeLibrary(user32); bitmap = CreateCompatibleBitmap(screen, width, height); if (!bitmap) @@ -346,7 +369,10 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) /* step 2: copy bits into memory DC bitmap */ - if (!BitBlt(screen_copy, 0, 0, width, height, screen, 0, 0, SRCCOPY)) + rop = SRCCOPY; + if (includeLayeredWindows) + rop |= CAPTUREBLT; + if (!BitBlt(screen_copy, 0, 0, width, height, screen, x, y, rop)) goto error; /* step 3: extract bits from bitmap */ @@ -368,7 +394,7 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) DeleteDC(screen_copy); DeleteDC(screen); - return Py_BuildValue("(ii)N", width, height, buffer); + return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer); error: PyErr_SetString(PyExc_IOError, "screen grab failed"); @@ -704,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 13da1b999..41ba124c4 100644 --- a/src/encode.c +++ b/src/encode.c @@ -22,10 +22,10 @@ /* FIXME: make these pluggable! */ +#define PY_SSIZE_T_CLEAN #include "Python.h" #include "Imaging.h" -#include "py3.h" #include "Gif.h" #ifdef HAVE_UNISTD_H @@ -123,9 +123,9 @@ _encode(ImagingEncoderObject* encoder, PyObject* args) /* Encode to a Python string (allocated by this method) */ - int bufsize = 16384; + Py_ssize_t bufsize = 16384; - if (!PyArg_ParseTuple(args, "|i", &bufsize)) + if (!PyArg_ParseTuple(args, "|n", &bufsize)) return NULL; buf = PyBytes_FromStringAndSize(NULL, bufsize); @@ -176,10 +176,10 @@ _encode_to_file(ImagingEncoderObject* encoder, PyObject* args) /* Encode to a file handle */ - int fh; - int bufsize = 16384; + Py_ssize_t fh; + Py_ssize_t bufsize = 16384; - if (!PyArg_ParseTuple(args, "i|i", &fh, &bufsize)) + if (!PyArg_ParseTuple(args, "n|n", &fh, &bufsize)) return NULL; /* Allocate an encoder buffer */ @@ -221,14 +221,14 @@ _setimage(ImagingEncoderObject* encoder, PyObject* args) PyObject* op; Imaging im; ImagingCodecState state; - int x0, y0, x1, y1; + Py_ssize_t x0, y0, x1, y1; /* Define where image data should be stored */ x0 = y0 = x1 = y1 = 0; /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) + if (!PyArg_ParseTuple(args, "O|(nnnn)", &op, &x0, &y0, &x1, &y1)) return NULL; im = PyImaging_AsImaging(op); if (!im) @@ -406,9 +406,9 @@ PyImaging_GifEncoderNew(PyObject* self, PyObject* args) char *mode; char *rawmode; - int bits = 8; - int interlace = 0; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &bits, &interlace)) + Py_ssize_t bits = 8; + Py_ssize_t interlace = 0; + if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &bits, &interlace)) return NULL; encoder = PyImaging_EncoderNew(sizeof(GIFENCODERSTATE)); @@ -438,9 +438,9 @@ PyImaging_PcxEncoderNew(PyObject* self, PyObject* args) char *mode; char *rawmode; - int bits = 8; + Py_ssize_t bits = 8; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &bits)) { + if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &bits)) { return NULL; } @@ -470,10 +470,10 @@ PyImaging_RawEncoderNew(PyObject* self, PyObject* args) char *mode; char *rawmode; - int stride = 0; - int ystep = 1; + Py_ssize_t stride = 0; + Py_ssize_t ystep = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &stride, &ystep)) + if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &stride, &ystep)) return NULL; encoder = PyImaging_EncoderNew(0); @@ -503,9 +503,9 @@ PyImaging_TgaRleEncoderNew(PyObject* self, PyObject* args) char *mode; char *rawmode; - int ystep = 1; + Py_ssize_t ystep = 1; - if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &ystep)) + if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &ystep)) return NULL; encoder = PyImaging_EncoderNew(0); @@ -561,12 +561,12 @@ PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) char* mode; char* rawmode; - int optimize = 0; - int compress_level = -1; - int compress_type = -1; + Py_ssize_t optimize = 0; + Py_ssize_t compress_level = -1; + Py_ssize_t compress_type = -1; char* dictionary = NULL; - int dictionary_size = 0; - if (!PyArg_ParseTuple(args, "ss|iii"PY_ARG_BYTES_LENGTH, &mode, &rawmode, + Py_ssize_t dictionary_size = 0; + if (!PyArg_ParseTuple(args, "ss|nnny#", &mode, &rawmode, &optimize, &compress_level, &compress_type, &dictionary, &dictionary_size)) @@ -612,6 +612,313 @@ PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) #endif +/* -------------------------------------------------------------------- */ +/* LibTiff */ +/* -------------------------------------------------------------------- */ + +#ifdef HAVE_LIBTIFF + +#include "TiffDecode.h" + +#include + +PyObject* +PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) +{ + ImagingEncoderObject* encoder; + + char* mode; + char* rawmode; + char* compname; + char* filename; + Py_ssize_t fp; + + PyObject *tags, *types; + PyObject *key, *value; + Py_ssize_t pos = 0; + int key_int, status, is_core_tag, is_var_length, num_core_tags, i; + TIFFDataType type = TIFF_NOTYPE; + // This list also exists in TiffTags.py + const int core_tags[] = { + 256, 257, 258, 259, 262, 263, 266, 269, 274, 277, 278, 280, 281, 340, + 341, 282, 283, 284, 286, 287, 296, 297, 321, 338, 32995, 32998, 32996, + 339, 32997, 330, 531, 530, 65537 + }; + + Py_ssize_t tags_size; + PyObject *item; + + if (! PyArg_ParseTuple(args, "sssnsOO", &mode, &rawmode, &compname, &fp, &filename, &tags, &types)) { + return NULL; + } + + if (!PyList_Check(tags)) { + PyErr_SetString(PyExc_ValueError, "Invalid tags list"); + return NULL; + } else { + tags_size = PyList_Size(tags); + TRACE(("tags size: %d\n", (int)tags_size)); + for (pos=0;posstate, filename, fp)) { + Py_DECREF(encoder); + PyErr_SetString(PyExc_RuntimeError, "tiff codec initialization failed"); + return NULL; + } + + num_core_tags = sizeof(core_tags) / sizeof(int); + for (pos = 0; pos < tags_size; pos++) { + item = PyList_GetItem(tags, pos); + // We already checked that tags is a 2-tuple list. + key = PyTuple_GetItem(item, 0); + key_int = (int)PyLong_AsLong(key); + value = PyTuple_GetItem(item, 1); + status = 0; + is_core_tag = 0; + is_var_length = 0; + type = TIFF_NOTYPE; + + for (i=0; i= TIFF_BYTE && type_int <= TIFF_DOUBLE) { + type = (TIFFDataType)type_int; + } + } + } + + + if (type == TIFF_NOTYPE) { + // Autodetect type. Types should not be changed for backwards + // compatibility. + if (PyLong_Check(value)) { + type = TIFF_LONG; + } else if (PyFloat_Check(value)) { + type = TIFF_DOUBLE; + } else if (PyBytes_Check(value)) { + type = TIFF_ASCII; + } + } + + if (PyBytes_Check(value) && + (type == TIFF_BYTE || type == TIFF_UNDEFINED)) { + // For backwards compatibility + type = TIFF_ASCII; + } + + if (PyTuple_Check(value)) { + Py_ssize_t len; + len = PyTuple_Size(value); + + is_var_length = 1; + + if (!len) { + continue; + } + + if (type == TIFF_NOTYPE) { + // Autodetect type based on first item. Types should not be + // changed for backwards compatibility. + if (PyLong_Check(PyTuple_GetItem(value,0))) { + type = TIFF_LONG; + } else if (PyFloat_Check(PyTuple_GetItem(value,0))) { + type = TIFF_FLOAT; + } + } + } + + if (!is_core_tag) { + // Register field for non core tags. + if (ImagingLibTiffMergeFieldInfo(&encoder->state, type, key_int, is_var_length)) { + continue; + } + } + + if (is_var_length) { + Py_ssize_t len,i; + TRACE(("Setting from Tuple: %d \n", key_int)); + len = PyTuple_Size(value); + + if (type == TIFF_BYTE) { + UINT8 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(UINT8)); + if (av) { + for (i=0;istate, (ttag_t) key_int, len, av); + free(av); + } + } else if (type == TIFF_SHORT) { + UINT16 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(UINT16)); + if (av) { + for (i=0;istate, (ttag_t) key_int, len, av); + free(av); + } + } else if (type == TIFF_LONG) { + UINT32 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(UINT32)); + if (av) { + for (i=0;istate, (ttag_t) key_int, len, av); + free(av); + } + } else if (type == TIFF_SBYTE) { + INT8 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(INT8)); + if (av) { + for (i=0;istate, (ttag_t) key_int, len, av); + free(av); + } + } else if (type == TIFF_SSHORT) { + INT16 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(INT16)); + if (av) { + for (i=0;istate, (ttag_t) key_int, len, av); + free(av); + } + } else if (type == TIFF_SLONG) { + INT32 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(INT32)); + if (av) { + for (i=0;istate, (ttag_t) key_int, len, av); + free(av); + } + } else if (type == TIFF_FLOAT) { + FLOAT32 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(FLOAT32)); + if (av) { + for (i=0;istate, (ttag_t) key_int, len, av); + free(av); + } + } else if (type == TIFF_DOUBLE) { + FLOAT64 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(FLOAT64)); + if (av) { + for (i=0;istate, (ttag_t) key_int, len, av); + free(av); + } + } + } else { + if (type == TIFF_SHORT) { + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) key_int, + (UINT16)PyLong_AsLong(value)); + } else if (type == TIFF_LONG) { + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) key_int, + (UINT32)PyLong_AsLong(value)); + } else if (type == TIFF_SSHORT) { + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) key_int, + (INT16)PyLong_AsLong(value)); + } else if (type == TIFF_SLONG) { + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) key_int, + (INT32)PyLong_AsLong(value)); + } else if (type == TIFF_FLOAT) { + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) key_int, + (FLOAT32)PyFloat_AsDouble(value)); + } else if (type == TIFF_DOUBLE) { + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) key_int, + (FLOAT64)PyFloat_AsDouble(value)); + } else if (type == TIFF_BYTE) { + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) key_int, + (UINT8)PyLong_AsLong(value)); + } else if (type == TIFF_SBYTE) { + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) key_int, + (INT8)PyLong_AsLong(value)); + } else if (type == TIFF_ASCII) { + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) key_int, + PyBytes_AsString(value)); + } else if (type == TIFF_RATIONAL) { + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) key_int, + (FLOAT64)PyFloat_AsDouble(value)); + } else { + TRACE(("Unhandled type for key %d : %s \n", + key_int, + PyBytes_AsString(PyObject_Str(value)))); + } + } + if (!status) { + TRACE(("Error setting Field\n")); + Py_DECREF(encoder); + PyErr_SetString(PyExc_RuntimeError, "Error setting from dictionary"); + return NULL; + } + } + + encoder->encode = ImagingLibTiffEncode; + + return (PyObject*) encoder; +} + +#endif + /* -------------------------------------------------------------------- */ /* JPEG */ /* -------------------------------------------------------------------- */ @@ -676,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); } @@ -701,22 +1008,22 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) char *mode; char *rawmode; - int quality = 0; - int progressive = 0; - int smooth = 0; - int optimize = 0; - int streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ - int xdpi = 0, ydpi = 0; - int subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ + Py_ssize_t quality = 0; + Py_ssize_t progressive = 0; + Py_ssize_t smooth = 0; + Py_ssize_t optimize = 0; + Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ + Py_ssize_t xdpi = 0, ydpi = 0; + Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ PyObject* qtables=NULL; unsigned int *qarrays = NULL; int qtablesLen = 0; char* extra = NULL; - int extra_size; + Py_ssize_t extra_size; char* rawExif = NULL; - int rawExifLen = 0; + Py_ssize_t rawExifLen = 0; - if (!PyArg_ParseTuple(args, "ss|iiiiiiiiO"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, @@ -786,168 +1093,6 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) #endif -/* -------------------------------------------------------------------- */ -/* LibTiff */ -/* -------------------------------------------------------------------- */ - -#ifdef HAVE_LIBTIFF - -#include "TiffDecode.h" - -#include - -PyObject* -PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) -{ - ImagingEncoderObject* encoder; - - char* mode; - char* rawmode; - char* compname; - char* filename; - int fp; - - PyObject *dir; - PyObject *key, *value; - Py_ssize_t pos = 0; - int key_int, status, is_core_tag, number_of_tags, i; - // This list also exists in TiffTags.py - const int tags[] = { - 256, 257, 258, 259, 262, 263, 266, 269, 274, 277, 278, 280, 281, 340, - 341, 282, 283, 284, 286, 287, 296, 297, 321, 338, 32995, 32998, 32996, - 339, 32997, 330, 531, 530 - }; - - Py_ssize_t d_size; - PyObject *keys, *values; - - - if (! PyArg_ParseTuple(args, "sssisO", &mode, &rawmode, &compname, &fp, &filename, &dir)) { - return NULL; - } - - if (!PyDict_Check(dir)) { - PyErr_SetString(PyExc_ValueError, "Invalid Dictionary"); - return NULL; - } else { - d_size = PyDict_Size(dir); - TRACE(("dict size: %d\n", (int)d_size)); - keys = PyDict_Keys(dir); - values = PyDict_Values(dir); - for (pos=0;posstate, filename, fp)) { - Py_DECREF(encoder); - PyErr_SetString(PyExc_RuntimeError, "tiff codec initialization failed"); - return NULL; - } - - number_of_tags = sizeof(tags) / sizeof(int); - for (pos = 0; pos < d_size; pos++) { - key = PyList_GetItem(keys, pos); - key_int = (int)PyInt_AsLong(key); - value = PyList_GetItem(values, pos); - status = 0; - is_core_tag = 0; - for (i=0; istate, TIFF_LONG, key_int)) { - status = ImagingLibTiffSetField(&encoder->state, - (ttag_t) PyInt_AsLong(key), - PyInt_AsLong(value)); - } - } else if (PyFloat_Check(value)) { - TRACE(("Setting from Float: %d, %f \n", key_int, PyFloat_AsDouble(value))); - if (is_core_tag || !ImagingLibTiffMergeFieldInfo(&encoder->state, TIFF_DOUBLE, key_int)) { - status = ImagingLibTiffSetField(&encoder->state, - (ttag_t) PyInt_AsLong(key), - (double)PyFloat_AsDouble(value)); - } - } else if (PyBytes_Check(value)) { - TRACE(("Setting from Bytes: %d, %s \n", key_int, PyBytes_AsString(value))); - if (is_core_tag || !ImagingLibTiffMergeFieldInfo(&encoder->state, TIFF_ASCII, key_int)) { - status = ImagingLibTiffSetField(&encoder->state, - (ttag_t) PyInt_AsLong(key), - PyBytes_AsString(value)); - } - } else if (PyTuple_Check(value)) { - Py_ssize_t len,i; - float *floatav; - int *intav; - TRACE(("Setting from Tuple: %d \n", key_int)); - len = PyTuple_Size(value); - if (len) { - if (PyInt_Check(PyTuple_GetItem(value,0))) { - TRACE((" %d elements, setting as ints \n", (int)len)); - /* malloc check ok, calloc checks for overflow */ - intav = calloc(len, sizeof(int)); - if (intav) { - for (i=0;istate, - (ttag_t) PyInt_AsLong(key), - len, intav); - free(intav); - } - } else if (PyFloat_Check(PyTuple_GetItem(value,0))) { - TRACE((" %d elements, setting as floats \n", (int)len)); - /* malloc check ok, calloc checks for overflow */ - floatav = calloc(len, sizeof(float)); - if (floatav) { - for (i=0;istate, - (ttag_t) PyInt_AsLong(key), - len, floatav); - free(floatav); - } - } else { - TRACE(("Unhandled type in tuple for key %d : %s \n", - key_int, - PyBytes_AsString(PyObject_Str(value)))); - } - } - } else { - TRACE(("Unhandled type for key %d : %s \n", - key_int, - PyBytes_AsString(PyObject_Str(value)))); - } - if (!status) { - TRACE(("Error setting Field\n")); - Py_DECREF(encoder); - PyErr_SetString(PyExc_RuntimeError, "Error setting from dictionary"); - return NULL; - } - } - - encoder->encode = ImagingLibTiffEncode; - - return (PyObject*) encoder; -} - -#endif /* -------------------------------------------------------------------- */ /* JPEG 2000 */ @@ -963,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; @@ -985,16 +1130,16 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) PyObject *offset = NULL, *tile_offset = NULL, *tile_size = NULL; char *quality_mode = "rates"; PyObject *quality_layers = NULL; - int num_resolutions = 0; + Py_ssize_t num_resolutions = 0; PyObject *cblk_size = NULL, *precinct_size = NULL; PyObject *irreversible = NULL; char *progression = "LRCP"; OPJ_PROG_ORDER prog_order; char *cinema_mode = "no"; OPJ_CINEMA_MODE cine_mode; - int fd = -1; + Py_ssize_t fd = -1; - if (!PyArg_ParseTuple(args, "ss|OOOsOIOOOssi", &mode, &format, + if (!PyArg_ParseTuple(args, "ss|OOOsOnOOOssn", &mode, &format, &offset, &tile_offset, &tile_size, &quality_mode, &quality_layers, &num_resolutions, &cblk_size, &precinct_size, @@ -1065,6 +1210,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) PyErr_SetString(PyExc_ValueError, "JPEG 2000 tile offset too small; top left tile must " "intersect image area"); + Py_DECREF(encoder); + return NULL; } if (context->tile_offset_x > context->offset_x diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 292968f1c..15ffa11fc 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -94,11 +94,11 @@ static void get_pixel_16L(Imaging im, int x, int y, void* color) { UINT8* in = (UINT8*) &im->image[y][x+x]; - UINT16* out = color; #ifdef WORDS_BIGENDIAN - out[0] = in[0] + (in[1]<<8); + UINT16 out = in[0] + (in[1]<<8); + memcpy(color, &out, sizeof(out)); #else - out[0] = *(UINT16*) in; + memcpy(color, in, sizeof(UINT16)); #endif } @@ -106,30 +106,29 @@ static void get_pixel_16B(Imaging im, int x, int y, void* color) { UINT8* in = (UINT8*) &im->image[y][x+x]; - UINT16* out = color; #ifdef WORDS_BIGENDIAN - out[0] = *(UINT16*) in; + memcpy(color, in, sizeof(UINT16)); #else - out[0] = in[1] + (in[0]<<8); + UINT16 out = in[1] + (in[0]<<8); + memcpy(color, &out, sizeof(out)); #endif } static void get_pixel_32(Imaging im, int x, int y, void* color) { - INT32* out = color; - out[0] = im->image32[y][x]; + memcpy(color, &im->image32[y][x], sizeof(INT32)); } static void get_pixel_32L(Imaging im, int x, int y, void* color) { UINT8* in = (UINT8*) &im->image[y][x*4]; - INT32* out = color; #ifdef WORDS_BIGENDIAN - out[0] = in[0] + (in[1]<<8) + (in[2]<<16) + (in[3]<<24); + INT32 out = in[0] + (in[1]<<8) + (in[2]<<16) + (in[3]<<24); + memcpy(color, &out, sizeof(out)); #else - out[0] = *(INT32*) in; + memcpy(color, in, sizeof(INT32)); #endif } @@ -137,11 +136,11 @@ static void get_pixel_32B(Imaging im, int x, int y, void* color) { UINT8* in = (UINT8*) &im->image[y][x*4]; - INT32* out = color; #ifdef WORDS_BIGENDIAN - out[0] = *(INT32*) in; + memcpy(color, in, sizeof(INT32)); #else - out[0] = in[3] + (in[2]<<8) + (in[1]<<16) + (in[0]<<24); + INT32 out = in[3] + (in[2]<<8) + (in[1]<<16) + (in[0]<<24); + memcpy(color, &out, sizeof(out)); #endif } @@ -153,7 +152,7 @@ put_pixel(Imaging im, int x, int y, const void* color) if (im->image8) im->image8[y][x] = *((UINT8*) color); else - im->image32[y][x] = *((INT32*) color); + memcpy(&im->image32[y][x], color, sizeof(INT32)); } static void @@ -165,10 +164,7 @@ put_pixel_8(Imaging im, int x, int y, const void* color) static void put_pixel_16L(Imaging im, int x, int y, const void* color) { - const char* in = color; - UINT8* out = (UINT8*) &im->image8[y][x+x]; - out[0] = in[0]; - out[1] = in[1]; + memcpy(&im->image8[y][x+x], color, 2); } static void @@ -183,12 +179,7 @@ put_pixel_16B(Imaging im, int x, int y, const void* color) static void put_pixel_32L(Imaging im, int x, int y, const void* color) { - const char* in = color; - UINT8* out = (UINT8*) &im->image8[y][x*4]; - out[0] = in[0]; - out[1] = in[1]; - out[2] = in[2]; - out[3] = in[3]; + memcpy(&im->image8[y][x*4], color, 4); } static void @@ -205,7 +196,7 @@ put_pixel_32B(Imaging im, int x, int y, const void* color) static void put_pixel_32(Imaging im, int x, int y, const void* color) { - im->image32[y][x] = *((INT32*) color); + memcpy(&im->image32[y][x], color, sizeof(INT32)); } void diff --git a/src/libImaging/Bands.c b/src/libImaging/Bands.c index e38e22819..7fff04486 100644 --- a/src/libImaging/Bands.c +++ b/src/libImaging/Bands.c @@ -50,7 +50,8 @@ ImagingGetBand(Imaging imIn, int band) UINT8* out = imOut->image8[y]; x = 0; for (; x < imIn->xsize - 3; x += 4) { - *((UINT32*) (out + x)) = MAKE_UINT32(in[0], in[4], in[8], in[12]); + UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); + memcpy(out + x, &v, sizeof(v)); in += 16; } for (; x < imIn->xsize; x++) { @@ -98,8 +99,10 @@ ImagingSplit(Imaging imIn, Imaging bands[4]) UINT8* out1 = bands[1]->image8[y]; x = 0; for (; x < imIn->xsize - 3; x += 4) { - *((UINT32*) (out0 + x)) = MAKE_UINT32(in[0], in[4], in[8], in[12]); - *((UINT32*) (out1 + x)) = MAKE_UINT32(in[0+3], in[4+3], in[8+3], in[12+3]); + UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); + memcpy(out0 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0+3], in[4+3], in[8+3], in[12+3]); + memcpy(out1 + x, &v, sizeof(v)); in += 16; } for (; x < imIn->xsize; x++) { @@ -116,9 +119,12 @@ ImagingSplit(Imaging imIn, Imaging bands[4]) UINT8* out2 = bands[2]->image8[y]; x = 0; for (; x < imIn->xsize - 3; x += 4) { - *((UINT32*) (out0 + x)) = MAKE_UINT32(in[0], in[4], in[8], in[12]); - *((UINT32*) (out1 + x)) = MAKE_UINT32(in[0+1], in[4+1], in[8+1], in[12+1]); - *((UINT32*) (out2 + x)) = MAKE_UINT32(in[0+2], in[4+2], in[8+2], in[12+2]); + UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); + memcpy(out0 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0+1], in[4+1], in[8+1], in[12+1]); + memcpy(out1 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0+2], in[4+2], in[8+2], in[12+2]); + memcpy(out2 + x, &v, sizeof(v)); in += 16; } for (; x < imIn->xsize; x++) { @@ -137,10 +143,14 @@ ImagingSplit(Imaging imIn, Imaging bands[4]) UINT8* out3 = bands[3]->image8[y]; x = 0; for (; x < imIn->xsize - 3; x += 4) { - *((UINT32*) (out0 + x)) = MAKE_UINT32(in[0], in[4], in[8], in[12]); - *((UINT32*) (out1 + x)) = MAKE_UINT32(in[0+1], in[4+1], in[8+1], in[12+1]); - *((UINT32*) (out2 + x)) = MAKE_UINT32(in[0+2], in[4+2], in[8+2], in[12+2]); - *((UINT32*) (out3 + x)) = MAKE_UINT32(in[0+3], in[4+3], in[8+3], in[12+3]); + UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); + memcpy(out0 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0+1], in[4+1], in[8+1], in[12+1]); + memcpy(out1 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0+2], in[4+2], in[8+2], in[12+2]); + memcpy(out2 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0+3], in[4+3], in[8+3], in[12+3]); + memcpy(out3 + x, &v, sizeof(v)); in += 16; } for (; x < imIn->xsize; x++) { diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index 1472b0296..c2c4f21e7 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -844,7 +844,7 @@ static int decode_bcn(Imaging im, ImagingCodecState state, const UINT8* src, int return (int)(ptr - src); } -int ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) { +int ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { int N = state->state & 0xf; int width = state->xsize; int height = state->ysize; diff --git a/src/libImaging/BitDecode.c b/src/libImaging/BitDecode.c index a78183542..7120b3321 100644 --- a/src/libImaging/BitDecode.c +++ b/src/libImaging/BitDecode.c @@ -20,7 +20,7 @@ int -ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) +ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { BITSTATE* bitstate = state->context; UINT8* ptr; diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 1a415ed16..9537c4f98 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -1,4 +1,3 @@ -#include "Python.h" #include "Imaging.h" diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index 46b00142b..f01d38993 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -105,7 +105,7 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, ImagingSectionEnter(&cookie); for (y = 0; y < imOut->ysize; y++) { UINT8* rowIn = (UINT8 *)imIn->image[y]; - UINT32* rowOut = (UINT32 *)imOut->image[y]; + char* rowOut = (char *)imOut->image[y]; for (x = 0; x < imOut->xsize; x++) { UINT32 index1D = rowIn[x*4 + 0] * scale1D; UINT32 index2D = rowIn[x*4 + 1] * scale2D; @@ -120,6 +120,7 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, INT16 leftleft[4], leftright[4], rightleft[4], rightright[4]; if (table_channels == 3) { + UINT32 v; interpolate3(leftleft, &table[idx + 0], &table[idx + 3], shift1D); interpolate3(leftright, &table[idx + size1D*3], &table[idx + size1D*3 + 3], shift1D); @@ -133,12 +134,14 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, interpolate3(result, left, right, shift3D); - rowOut[x] = MAKE_UINT32( + v = MAKE_UINT32( clip8(result[0]), clip8(result[1]), clip8(result[2]), rowIn[x*4 + 3]); + memcpy(rowOut + x * sizeof(v), &v, sizeof(v)); } if (table_channels == 4) { + UINT32 v; interpolate4(leftleft, &table[idx + 0], &table[idx + 4], shift1D); interpolate4(leftright, &table[idx + size1D*4], &table[idx + size1D*4 + 4], shift1D); @@ -152,9 +155,10 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, interpolate4(result, left, right, shift3D); - rowOut[x] = MAKE_UINT32( + v = MAKE_UINT32( clip8(result[0]), clip8(result[1]), clip8(result[2]), clip8(result[3])); + memcpy(rowOut + x * sizeof(v), &v, sizeof(v)); } } } diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 39ddf8721..60513c66d 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -101,6 +101,19 @@ bit2ycbcr(UINT8* out, const UINT8* in, int xsize) } } +static void +bit2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, out += 4) { + UINT8 v = (*in++ != 0) ? 255 : 0; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = 255; + } +} + /* ----------------- */ /* RGB/L conversions */ /* ----------------- */ @@ -175,6 +188,19 @@ l2rgb(UINT8* out, const UINT8* in, int xsize) } } +static void +l2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, out += 4) { + UINT8 v = *in++; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = 255; + } +} + static void la2l(UINT8* out, const UINT8* in, int xsize) { @@ -196,6 +222,19 @@ la2rgb(UINT8* out, const UINT8* in, int xsize) } } +static void +la2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + UINT8 v = in[0]; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = in[3]; + } +} + static void rgb2bit(UINT8* out, const UINT8* in, int xsize) { @@ -229,42 +268,46 @@ static void rgb2i(UINT8* out_, const UINT8* in, int xsize) { int x; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++, in += 4) - *out++ = L24(in) >> 16; + for (x = 0; x < xsize; x++, in += 4, out_ += 4) { + INT32 v = L24(in) >> 16; + memcpy(out_, &v, sizeof(v)); + } } static void rgb2f(UINT8* out_, const UINT8* in, int xsize) { int x; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++, in += 4) - *out++ = (float) L(in) / 1000.0F; + for (x = 0; x < xsize; x++, in += 4, out_ += 4) { + FLOAT32 v = (float) L(in) / 1000.0F; + memcpy(out_, &v, sizeof(v)); + } } static void rgb2bgr15(UINT8* out_, const UINT8* in, int xsize) { int x; - UINT16* out = (UINT16*) out_; - for (x = 0; x < xsize; x++, in += 4) - *out++ = + for (x = 0; x < xsize; x++, in += 4, out_ += 2) { + UINT16 v = ((((UINT16)in[0])<<7)&0x7c00) + ((((UINT16)in[1])<<2)&0x03e0) + ((((UINT16)in[2])>>3)&0x001f); + memcpy(out_, &v, sizeof(v)); + } } static void rgb2bgr16(UINT8* out_, const UINT8* in, int xsize) { int x; - UINT16* out = (UINT16*) out_; - for (x = 0; x < xsize; x++, in += 4) - *out++ = + for (x = 0; x < xsize; x++, in += 4, out_ += 2) { + UINT16 v = ((((UINT16)in[0])<<8)&0xf800) + ((((UINT16)in[1])<<3)&0x07e0) + ((((UINT16)in[2])>>3)&0x001f); + memcpy(out_, &v, sizeof(v)); + } } static void @@ -279,54 +322,58 @@ rgb2bgr24(UINT8* out, const UINT8* in, int xsize) } static void -rgb2hsv(UINT8* out, const UINT8* in, int xsize) +rgb2hsv_row(UINT8* out, const UINT8* in) { // following colorsys.py float h,s,rc,gc,bc,cr; UINT8 maxc,minc; UINT8 r, g, b; UINT8 uh,us,uv; - int x; - for (x = 0; x < xsize; x++, in += 4) { - r = in[0]; - g = in[1]; - b = in[2]; - - maxc = MAX(r,MAX(g,b)); - minc = MIN(r,MIN(g,b)); - uv = maxc; - if (minc == maxc){ - *out++ = 0; - *out++ = 0; - *out++ = uv; + r = in[0]; + g = in[1]; + b = in[2]; + maxc = MAX(r,MAX(g,b)); + minc = MIN(r,MIN(g,b)); + uv = maxc; + if (minc == maxc){ + uh = 0; + us = 0; + } else { + cr = (float)(maxc-minc); + s = cr/(float)maxc; + rc = ((float)(maxc-r))/cr; + gc = ((float)(maxc-g))/cr; + bc = ((float)(maxc-b))/cr; + if (r == maxc) { + h = bc-gc; + } else if (g == maxc) { + h = 2.0 + rc-bc; } else { - cr = (float)(maxc-minc); - s = cr/(float)maxc; - rc = ((float)(maxc-r))/cr; - gc = ((float)(maxc-g))/cr; - bc = ((float)(maxc-b))/cr; - if (r == maxc) { - h = bc-gc; - } else if (g == maxc) { - h = 2.0 + rc-bc; - } else { - h = 4.0 + gc-rc; - } - // incorrect hue happens if h/6 is negative. - h = fmod((h/6.0 + 1.0), 1.0); - - uh = (UINT8)CLIP8((int)(h*255.0)); - us = (UINT8)CLIP8((int)(s*255.0)); - - *out++ = uh; - *out++ = us; - *out++ = uv; - + h = 4.0 + gc-rc; } - *out++ = in[3]; + // incorrect hue happens if h/6 is negative. + h = fmod((h/6.0 + 1.0), 1.0); + + uh = (UINT8)CLIP8((int)(h*255.0)); + us = (UINT8)CLIP8((int)(s*255.0)); + } + out[0] = uh; + out[1] = us; + out[2] = uv; +} + +static void +rgb2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + rgb2hsv_row(out, in); + out[3] = in[3]; } } + + static void hsv2rgb(UINT8* out, const UINT8* in, int xsize) { // following colorsys.py @@ -490,12 +537,13 @@ rgbT2rgba(UINT8* out, int xsize, int r, int g, int b) UINT32 repl = trns & 0x00ffffff; #endif - UINT32* tmp = (UINT32 *)out; int i; - for (i=0; i < xsize; i++ ,tmp++) { - if (tmp[0]==trns) { - tmp[0]=repl; + for (i=0; i < xsize; i++ ,out += sizeof(trns)) { + UINT32 v; + memcpy(&v, out, sizeof(v)); + if (v==trns) { + memcpy(out, &repl, sizeof(repl)); } } } @@ -517,6 +565,18 @@ l2cmyk(UINT8* out, const UINT8* in, int xsize) } } +static void +la2cmyk(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = ~(in[0]); + } +} + static void rgb2cmyk(UINT8* out, const UINT8* in, int xsize) { @@ -545,6 +605,22 @@ cmyk2rgb(UINT8* out, const UINT8* in, int xsize) } } +static void +cmyk2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x, nk, tmp; + for (x = 0; x < xsize; x++) { + nk = 255 - in[3]; + out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp)); + out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp)); + out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp)); + rgb2hsv_row(out, out); + out[3] = 255; + out += 4; + in += 4; + } +} + /* ------------- */ /* I conversions */ /* ------------- */ @@ -553,43 +629,84 @@ static void bit2i(UINT8* out_, const UINT8* in, int xsize) { int x; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (*in++ != 0) ? 255 : 0; + for (x = 0; x < xsize; x++, out_ += 4) { + INT32 v = (*in++ != 0) ? 255 : 0; + memcpy(out_, &v, sizeof(v)); + } } static void l2i(UINT8* out_, const UINT8* in, int xsize) { int x; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (INT32) *in++; + for (x = 0; x < xsize; x++, out_ += 4) { + INT32 v = *in++; + memcpy(out_, &v, sizeof(v)); + } } static void i2l(UINT8* out, const UINT8* in_, int xsize) { int x; - INT32* in = (INT32*) in_; - for (x = 0; x < xsize; x++, in++, out++) { - if (*in <= 0) + for (x = 0; x < xsize; x++, out++, in_ += 4) { + INT32 v; + memcpy(&v, in_, sizeof(v)); + if (v <= 0) *out = 0; - else if (*in >= 255) + else if (v >= 255) *out = 255; else - *out = (UINT8) *in; + *out = (UINT8) v; } } static void i2f(UINT8* out_, const UINT8* in_, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in_ += 4, out_ += 4) { + INT32 i; + FLOAT32 f; + memcpy(&i, in_, sizeof(i)); + f = i; + memcpy(out_, &f, sizeof(f)); + } +} + +static void +i2rgb(UINT8* out, const UINT8* in_, int xsize) { int x; INT32* in = (INT32*) in_; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (FLOAT32) *in++; + for (x = 0; x < xsize; x++, in++, out+=4) { + if (*in <= 0) + out[0] = out[1] = out[2] = 0; + else if (*in >= 255) + out[0] = out[1] = out[2] = 255; + else + out[0] = out[1] = out[2] = (UINT8) *in; + out[3] = 255; + } +} + +static void +i2hsv(UINT8* out, const UINT8* in_, int xsize) +{ + int x; + INT32* in = (INT32*) in_; + for (x = 0; x < xsize; x++, in++, out+=4) { + out[0] = 0; + out[1] = 0; + if (*in <= 0) { + out[2] = 0; + } else if (*in >= 255) { + out[2] = 255; + } else { + out[2] = (UINT8) *in; + } + out[3] = 255; + } } /* ------------- */ @@ -600,32 +717,35 @@ static void bit2f(UINT8* out_, const UINT8* in, int xsize) { int x; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (*in++ != 0) ? 255.0F : 0.0F; + for (x = 0; x < xsize; x++, out_ += 4) { + FLOAT32 f = (*in++ != 0) ? 255.0F : 0.0F; + memcpy(out_, &f, sizeof(f)); + } } static void l2f(UINT8* out_, const UINT8* in, int xsize) { int x; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (FLOAT32) *in++; + for (x = 0; x < xsize; x++, out_ += 4) { + FLOAT32 f = (FLOAT32) *in++; + memcpy(out_, &f, sizeof(f)); + } } static void f2l(UINT8* out, const UINT8* in_, int xsize) { int x; - FLOAT32* in = (FLOAT32*) in_; - for (x = 0; x < xsize; x++, in++, out++) { - if (*in <= 0.0) + for (x = 0; x < xsize; x++, out++, in_ += 4) { + FLOAT32 v; + memcpy(&v, in_, sizeof(v)); + if (v <= 0.0) *out = 0; - else if (*in >= 255.0) + else if (v >= 255.0) *out = 255; else - *out = (UINT8) *in; + *out = (UINT8) v; } } @@ -633,10 +753,13 @@ static void f2i(UINT8* out_, const UINT8* in_, int xsize) { int x; - FLOAT32* in = (FLOAT32*) in_; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (INT32) *in++; + for (x = 0; x < xsize; x++, in_ += 4, out_ += 4) { + FLOAT32 f; + INT32 i; + memcpy(&f, in_, sizeof(f)); + i = f; + memcpy(out_, &i, sizeof(i)); + } } /* ----------------- */ @@ -657,6 +780,18 @@ l2ycbcr(UINT8* out, const UINT8* in, int xsize) } } +static void +la2ycbcr(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = in[0]; + *out++ = 128; + *out++ = 128; + *out++ = 255; + } +} + static void ycbcr2l(UINT8* out, const UINT8* in, int xsize) { @@ -665,6 +800,16 @@ ycbcr2l(UINT8* out, const UINT8* in, int xsize) *out++ = in[0]; } +static void +ycbcr2la(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + out[0] = out[1] = out[2] = in[0]; + out[3] = 255; + } +} + /* ------------------------- */ /* I;16 (16-bit) conversions */ /* ------------------------- */ @@ -673,9 +818,10 @@ static void I_I16L(UINT8* out, const UINT8* in_, int xsize) { int x, v; - INT32* in = (INT32*) in_; - for (x = 0; x < xsize; x++, in++) { - v = CLIP16(*in); + for (x = 0; x < xsize; x++, in_ += 4) { + INT32 i; + memcpy(&i, in_, sizeof(i)); + v = CLIP16(i); *out++ = (UINT8) v; *out++ = (UINT8) (v >> 8); } @@ -685,9 +831,10 @@ static void I_I16B(UINT8* out, const UINT8* in_, int xsize) { int x, v; - INT32* in = (INT32*) in_; - for (x = 0; x < xsize; x++, in++) { - v = CLIP16(*in); + for (x = 0; x < xsize; x++, in_ += 4) { + INT32 i; + memcpy(&i, in_, sizeof(i)); + v = CLIP16(i); *out++ = (UINT8) (v >> 8); *out++ = (UINT8) v; } @@ -698,9 +845,10 @@ static void I16L_I(UINT8* out_, const UINT8* in, int xsize) { int x; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++, in += 2) - *out++ = in[0] + ((int) in[1] << 8); + for (x = 0; x < xsize; x++, in += 2, out_ += 4) { + INT32 v = in[0] + ((int) in[1] << 8); + memcpy(out_, &v, sizeof(v)); + } } @@ -708,18 +856,20 @@ static void I16B_I(UINT8* out_, const UINT8* in, int xsize) { int x; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++, in += 2) - *out++ = in[1] + ((int) in[0] << 8); + for (x = 0; x < xsize; x++, in += 2, out_ += 4) { + INT32 v = in[1] + ((int) in[0] << 8); + memcpy(out_, &v, sizeof(v)); + } } static void I16L_F(UINT8* out_, const UINT8* in, int xsize) { int x; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++, in += 2) - *out++ = (FLOAT32) (in[0] + ((int) in[1] << 8)); + for (x = 0; x < xsize; x++, in += 2, out_ += 4) { + FLOAT32 v = in[0] + ((int) in[1] << 8); + memcpy(out_, &v, sizeof(v)); + } } @@ -727,9 +877,10 @@ static void I16B_F(UINT8* out_, const UINT8* in, int xsize) { int x; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++, in += 2) - *out++ = (FLOAT32) (in[1] + ((int) in[0] << 8)); + for (x = 0; x < xsize; x++, in += 2, out_ += 4) { + FLOAT32 v = in[1] + ((int) in[0] << 8); + memcpy(out_, &v, sizeof(v)); + } } static void @@ -788,6 +939,7 @@ static struct { { "1", "RGBX", bit2rgb }, { "1", "CMYK", bit2cmyk }, { "1", "YCbCr", bit2ycbcr }, + { "1", "HSV", bit2hsv }, { "L", "1", l2bit }, { "L", "LA", l2la }, @@ -798,20 +950,28 @@ static struct { { "L", "RGBX", l2rgb }, { "L", "CMYK", l2cmyk }, { "L", "YCbCr", l2ycbcr }, + { "L", "HSV", l2hsv }, { "LA", "L", la2l }, { "LA", "La", lA2la }, { "LA", "RGB", la2rgb }, - { "LA", "RGBX", la2rgb }, { "LA", "RGBA", la2rgb }, + { "LA", "RGBX", la2rgb }, + { "LA", "CMYK", la2cmyk }, + { "LA", "YCbCr", la2ycbcr }, + { "LA", "HSV", la2hsv }, { "La", "LA", la2lA }, - { "I", "L", i2l }, - { "I", "F", i2f }, + { "I", "L", i2l }, + { "I", "F", i2f }, + { "I", "RGB", i2rgb }, + { "I", "RGBA", i2rgb }, + { "I", "RGBX", i2rgb }, + { "I", "HSV", i2hsv }, - { "F", "L", f2l }, - { "F", "I", f2i }, + { "F", "L", f2l }, + { "F", "I", f2i }, { "RGB", "1", rgb2bit }, { "RGB", "L", rgb2l }, @@ -837,22 +997,27 @@ static struct { { "RGBA", "RGBX", rgb2rgba }, { "RGBA", "CMYK", rgb2cmyk }, { "RGBA", "YCbCr", ImagingConvertRGB2YCbCr }, + { "RGBA", "HSV", rgb2hsv }, { "RGBa", "RGBA", rgba2rgbA }, { "RGBX", "1", rgb2bit }, { "RGBX", "L", rgb2l }, - { "RGBA", "I", rgb2i }, - { "RGBA", "F", rgb2f }, + { "RGBX", "LA", rgb2la }, + { "RGBX", "I", rgb2i }, + { "RGBX", "F", rgb2f }, { "RGBX", "RGB", rgba2rgb }, { "RGBX", "CMYK", rgb2cmyk }, { "RGBX", "YCbCr", ImagingConvertRGB2YCbCr }, + { "RGBX", "HSV", rgb2hsv }, { "CMYK", "RGB", cmyk2rgb }, { "CMYK", "RGBA", cmyk2rgb }, { "CMYK", "RGBX", cmyk2rgb }, + { "CMYK", "HSV", cmyk2hsv }, { "YCbCr", "L", ycbcr2l }, + { "YCbCr", "LA", ycbcr2la }, { "YCbCr", "RGB", ImagingConvertYCbCr2RGB }, { "HSV", "RGB", hsv2rgb }, @@ -894,6 +1059,15 @@ p2bit(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) *out++ = (L(&palette[in[x]*4]) >= 128000) ? 255 : 0; } +static void +pa2bit(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + /* FIXME: precalculate greyscale palette? */ + for (x = 0; x < xsize; x++, in += 4) + *out++ = (L(&palette[in[0]*4]) >= 128000) ? 255 : 0; +} + static void p2l(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { @@ -903,6 +1077,28 @@ p2l(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) *out++ = L(&palette[in[x]*4]) / 1000; } +static void +pa2l(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + /* FIXME: precalculate greyscale palette? */ + for (x = 0; x < xsize; x++, in += 4) + *out++ = L(&palette[in[0]*4]) / 1000; +} + +static void +p2pa(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + for (x = 0; x < xsize; x++, in++) { + const UINT8* rgba = &palette[in[0]]; + *out++ = in[0]; + *out++ = in[0]; + *out++ = in[0]; + *out++ = rgba[3]; + } +} + static void p2la(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { @@ -920,28 +1116,48 @@ pa2la(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { int x; /* FIXME: precalculate greyscale palette? */ - for (x = 0; x < xsize; x++, in += 2) { - *out++ = L(&palette[in[0]*4]) / 1000; - *out++ = in[1]; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + out[0] = out[1] = out[2] = L(&palette[in[0]*4]) / 1000; + out[3] = in[3]; } } static void p2i(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + for (x = 0; x < xsize; x++, out_ += 4) { + INT32 v = L(&palette[in[x]*4]) / 1000; + memcpy(out_, &v, sizeof(v)); + } +} + +static void +pa2i(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) { int x; INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = L(&palette[in[x]*4]) / 1000; + for (x = 0; x < xsize; x++, in += 4) + *out++ = L(&palette[in[0]*4]) / 1000; } static void p2f(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + for (x = 0; x < xsize; x++, out_ += 4) { + FLOAT32 v = L(&palette[in[x]*4]) / 1000.0F; + memcpy(out_, &v, sizeof(v)); + } +} + +static void +pa2f(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) { int x; FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (float) L(&palette[in[x]*4]) / 1000.0F; + for (x = 0; x < xsize; x++, in += 4) + *out++ = (float) L(&palette[in[0]*4]) / 1000.0F; } static void @@ -957,6 +1173,41 @@ p2rgb(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) } } +static void +pa2rgb(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + for (x = 0; x < xsize; x++, in += 4) { + const UINT8* rgb = &palette[in[0] * 4]; + *out++ = rgb[0]; + *out++ = rgb[1]; + *out++ = rgb[2]; + *out++ = 255; + } +} + +static void +p2hsv(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + for (x = 0; x < xsize; x++, out += 4) { + const UINT8* rgb = &palette[*in++ * 4]; + rgb2hsv_row(out, rgb); + out[3] = 255; + } +} + +static void +pa2hsv(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + const UINT8* rgb = &palette[in[0] * 4]; + rgb2hsv_row(out, rgb); + out[3] = 255; + } +} + static void p2rgba(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { @@ -990,6 +1241,13 @@ p2cmyk(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) rgb2cmyk(out, out, xsize); } +static void +pa2cmyk(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + pa2rgb(out, in, xsize, palette); + rgb2cmyk(out, out, xsize); +} + static void p2ycbcr(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { @@ -997,6 +1255,13 @@ p2ycbcr(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) ImagingConvertRGB2YCbCr(out, out, xsize); } +static void +pa2ycbcr(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + pa2rgb(out, in, xsize, palette); + ImagingConvertRGB2YCbCr(out, out, xsize); +} + static Imaging frompalette(Imaging imOut, Imaging imIn, const char *mode) { @@ -1013,25 +1278,29 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) alpha = !strcmp(imIn->mode, "PA"); if (strcmp(mode, "1") == 0) - convert = p2bit; + convert = alpha ? pa2bit : p2bit; else if (strcmp(mode, "L") == 0) - convert = p2l; + convert = alpha ? pa2l : p2l; else if (strcmp(mode, "LA") == 0) - convert = (alpha) ? pa2la : p2la; + convert = alpha ? pa2la : p2la; + else if (strcmp(mode, "PA") == 0) + convert = p2pa; else if (strcmp(mode, "I") == 0) - convert = p2i; + convert = alpha ? pa2i : p2i; else if (strcmp(mode, "F") == 0) - convert = p2f; + convert = alpha ? pa2f : p2f; else if (strcmp(mode, "RGB") == 0) - convert = p2rgb; + convert = alpha ? pa2rgb : p2rgb; else if (strcmp(mode, "RGBA") == 0) - convert = (alpha) ? pa2rgba : p2rgba; + convert = alpha ? pa2rgba : p2rgba; else if (strcmp(mode, "RGBX") == 0) - convert = p2rgba; + convert = alpha ? pa2rgba : p2rgba; else if (strcmp(mode, "CMYK") == 0) - convert = p2cmyk; + convert = alpha ? pa2cmyk : p2cmyk; else if (strcmp(mode, "YCbCr") == 0) - convert = p2ycbcr; + convert = alpha ? pa2ycbcr : p2ycbcr; + else if (strcmp(mode, "HSV") == 0) + convert = alpha ? pa2hsv : p2hsv; else return (Imaging) ImagingError_ValueError("conversion not supported"); @@ -1052,9 +1321,10 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) #pragma optimize("", off) #endif static Imaging -topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) +topalette(Imaging imOut, Imaging imIn, const char *mode, ImagingPalette inpalette, int dither) { ImagingSectionCookie cookie; + int alpha; int x, y; ImagingPalette palette = inpalette;; @@ -1062,6 +1332,8 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) if (strcmp(imIn->mode, "L") != 0 && strncmp(imIn->mode, "RGB", 3) != 0) return (Imaging) ImagingError_ValueError("conversion not supported"); + alpha = !strcmp(mode, "PA"); + if (palette == NULL) { /* FIXME: make user configurable */ if (imIn->bands == 1) @@ -1073,7 +1345,7 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) if (!palette) return (Imaging) ImagingError_ValueError("no palette"); - imOut = ImagingNew2Dirty("P", imOut, imIn); + imOut = ImagingNew2Dirty(mode, imOut, imIn); if (!imOut) { if (palette != inpalette) ImagingPaletteDelete(palette); @@ -1088,8 +1360,13 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) /* Greyscale palette: copy data as is */ ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) - memcpy(imOut->image[y], imIn->image[y], imIn->linesize); + for (y = 0; y < imIn->ysize; y++) { + if (alpha) { + l2la((UINT8*) imOut->image[y], (UINT8*) imIn->image[y], imIn->xsize); + } else { + memcpy(imOut->image[y], imIn->image[y], imIn->linesize); + } + } ImagingSectionLeave(&cookie); } else { @@ -1120,7 +1397,7 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) int g, g0, g1, g2; int b, b0, b1, b2; UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out = imOut->image8[y]; + UINT8* out = alpha ? (UINT8*) imOut->image32[y] : imOut->image8[y]; int* e = errors; r = r0 = r1 = 0; @@ -1139,7 +1416,12 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) cache = &ImagingPaletteCache(palette, r, g, b); if (cache[0] == 0x100) ImagingPaletteCacheUpdate(palette, r, g, b); - out[x] = (UINT8) cache[0]; + if (alpha) { + out[x*4] = out[x*4+1] = out[x*4+2] = (UINT8) cache[0]; + out[x*4+3] = 255; + } else { + out[x] = (UINT8) cache[0]; + } r -= (int) palette->palette[cache[0]*4]; g -= (int) palette->palette[cache[0]*4+1]; @@ -1172,7 +1454,7 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) for (y = 0; y < imIn->ysize; y++) { int r, g, b; UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out = imOut->image8[y]; + UINT8* out = alpha ? (UINT8*) imOut->image32[y] : imOut->image8[y]; for (x = 0; x < imIn->xsize; x++, in += 4) { INT16* cache; @@ -1183,8 +1465,12 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) cache = &ImagingPaletteCache(palette, r, g, b); if (cache[0] == 0x100) ImagingPaletteCacheUpdate(palette, r, g, b); - out[x] = (UINT8) cache[0]; - + if (alpha) { + out[x*4] = out[x*4+1] = out[x*4+2] = (UINT8) cache[0]; + out[x*4+3] = 255; + } else { + out[x] = (UINT8) cache[0]; + } } } ImagingSectionLeave(&cookie); @@ -1314,8 +1600,8 @@ convert(Imaging imOut, Imaging imIn, const char *mode, if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "PA") == 0) return frompalette(imOut, imIn, mode); - if (strcmp(mode, "P") == 0) - return topalette(imOut, imIn, palette, dither); + if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) + return topalette(imOut, imIn, mode, palette, dither); if (dither && strcmp(mode, "1") == 0) return tobilevel(imOut, imIn, dither); @@ -1385,6 +1671,8 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, } if (!((strcmp(imIn->mode, "RGB") == 0 || + strcmp(imIn->mode, "1") == 0 || + strcmp(imIn->mode, "I") == 0 || strcmp(imIn->mode, "L") == 0) && strcmp(mode, "RGBA") == 0)) #ifdef notdef @@ -1403,7 +1691,13 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, if (strcmp(imIn->mode, "RGB") == 0) { convert = rgb2rgba; } else { - convert = l2rgb; + if (strcmp(imIn->mode, "1") == 0) { + convert = bit2rgb; + } else if (strcmp(imIn->mode, "I") == 0) { + convert = i2rgb; + } else { + convert = l2rgb; + } g = b = r; } diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index d0f374fe1..dee7c524d 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -40,7 +40,6 @@ #define FLOOR(v) ((v) >= 0.0 ? (int) (v) : (int) floor(v)) #define INK8(ink) (*(UINT8*)ink) -#define INK32(ink) (*(INT32*)ink) /* * Rounds around zero (up=away from zero, down=torwards zero) @@ -67,8 +66,14 @@ typedef void (*hline_handler)(Imaging, int, int, int, int); static inline void point8(Imaging im, int x, int y, int ink) { - if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) - im->image8[y][x] = (UINT8) ink; + if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { + if (strncmp(im->mode, "I;16", 4) == 0) { + im->image8[y][x*2] = (UINT8) ink; + im->image8[y][x*2+1] = (UINT8) ink; + } else { + im->image8[y][x] = (UINT8) ink; + } + } } static inline void @@ -95,7 +100,7 @@ point32rgba(Imaging im, int x, int y, int ink) static inline void hline8(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; + int tmp, pixelwidth; if (y0 >= 0 && y0 < im->ysize) { if (x0 > x1) @@ -108,8 +113,11 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) return; else if (x1 >= im->xsize) x1 = im->xsize-1; - if (x0 <= x1) - memset(im->image8[y0] + x0, (UINT8) ink, x1 - x0 + 1); + if (x0 <= x1) { + pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1; + memset(im->image8[y0] + x0 * pixelwidth, (UINT8) ink, + (x1 - x0 + 1) * pixelwidth); + } } } @@ -555,7 +563,7 @@ DRAW draw32rgba = { point32rgba, hline32rgba, line32rgba, polygon32rgba }; ink = INK8(ink_);\ } else {\ draw = (op) ? &draw32rgba : &draw32; \ - ink = INK32(ink_);\ + memcpy(&ink, ink_, sizeof(ink)); \ } int @@ -765,115 +773,150 @@ ellipse(Imaging im, int x0, int y0, int x1, int y1, int width, int mode, int op) { float i; - int j; + int inner; int n; - int cx, cy; + int maxEdgeCount; int w, h; - int x = 0, y = 0; + int x, y; + int cx, cy; int lx = 0, ly = 0; int sx = 0, sy = 0; + int lx_inner = 0, ly_inner = 0; + int sx_inner = 0, sy_inner = 0; DRAW* draw; INT32 ink; + Edge* e; DRAWINIT(); - if (width == 0) { - width = 1; + while (end < start) + end += 360; + + if (end - start > 360) { + // no need to go in loops + end = start + 361; } - for (j = 0; j < width; j++) { + w = x1 - x0; + h = y1 - y0; + if (w <= 0 || h <= 0) + return 0; - w = x1 - x0; - h = y1 - y0; - if (w < 0 || h < 0) + cx = (x0 + x1) / 2; + cy = (y0 + y1) / 2; + + if (!fill && width <= 1) { + for (i = start; i < end+1; i++) { + if (i > end) { + i = end; + } + ellipsePoint(cx, cy, w, h, i, &x, &y); + if (i != start) + draw->line(im, lx, ly, x, y, ink); + else + sx = x, sy = y; + lx = x, ly = y; + } + + if (i != start) { + if (mode == PIESLICE) { + if (x != cx || y != cy) { + draw->line(im, x, y, cx, cy, ink); + draw->line(im, cx, cy, sx, sy, ink); + } + } else if (mode == CHORD) { + if (x != sx || y != sy) + draw->line(im, x, y, sx, sy, ink); + } + } + } else { + inner = (mode == ARC || !fill) ? 1 : 0; + + // Build edge list + // malloc check UNDONE, FLOAT? + maxEdgeCount = ceil(end - start); + if (inner) { + maxEdgeCount *= 2; + } + maxEdgeCount += 3; + e = calloc(maxEdgeCount, sizeof(Edge)); + if (!e) { + ImagingError_MemoryError(); + return -1; + } + + // Outer circle + n = 0; + for (i = start; i < end+1; i++) { + if (i > end) { + i = end; + } + ellipsePoint(cx, cy, w, h, i, &x, &y); + if (i == start) { + sx = x, sy = y; + } else { + add_edge(&e[n++], lx, ly, x, y); + } + lx = x, ly = y; + } + if (n == 0) return 0; - cx = (x0 + x1) / 2; - cy = (y0 + y1) / 2; + if (inner) { + // Inner circle + x0 += width - 1; + y0 += width - 1; + x1 -= width - 1; + y1 -= width - 1; - while (end < start) - end += 360; - - if (end - start > 360) { - /* no need to go in loops */ - end = start + 361; - } - - if (mode != ARC && fill) { - - /* Build edge list */ - /* malloc check UNDONE, FLOAT? */ - Edge* e = calloc((end - start + 3), sizeof(Edge)); - if (!e) { - ImagingError_MemoryError(); - return -1; - } - n = 0; - - for (i = start; i < end+1; i++) { - if (i > end) { - i = end; - } - ellipsePoint(cx, cy, w, h, i, &x, &y); - if (i != start) - add_edge(&e[n++], lx, ly, x, y); - else - sx = x, sy = y; - lx = x, ly = y; - } - - if (n > 0) { - /* close and draw polygon */ - if (mode == PIESLICE) { - if (x != cx || y != cy) { - add_edge(&e[n++], x, y, cx, cy); - add_edge(&e[n++], cx, cy, sx, sy); + w = x1 - x0; + h = y1 - y0; + if (w <= 0 || h <= 0) { + // ARC with no gap in the middle is a PIESLICE + mode = PIESLICE; + inner = 0; + } else { + for (i = start; i < end+1; i++) { + if (i > end) { + i = end; } - } else { - if (x != sx || y != sy) - add_edge(&e[n++], x, y, sx, sy); - } - draw->polygon(im, n, e, ink, 0); - } - - free(e); - - } else { - - for (i = start; i < end+1; i++) { - if (i > end) { - i = end; - } - ellipsePoint(cx, cy, w, h, i, &x, &y); - if (i != start) - draw->line(im, lx, ly, x, y, ink); - else - sx = x, sy = y; - lx = x, ly = y; - } - - if (i != start) { - if (mode == PIESLICE) { - if (j == 0 && (x != cx || y != cy)) { - if (width == 1) { - draw->line(im, x, y, cx, cy, ink); - draw->line(im, cx, cy, sx, sy, ink); - } else { - ImagingDrawWideLine(im, x, y, cx, cy, &ink, width, op); - ImagingDrawWideLine(im, cx, cy, sx, sy, &ink, width, op); - } - } - } else if (mode == CHORD) { - if (x != sx || y != sy) - draw->line(im, x, y, sx, sy, ink); + ellipsePoint(cx, cy, w, h, i, &x, &y); + if (i == start) + sx_inner = x, sy_inner = y; + else + add_edge(&e[n++], lx_inner, ly_inner, x, y); + lx_inner = x, ly_inner = y; } } } - x0++; - y0++; - x1--; - y1--; + + if (end - start < 360) { + // Close polygon + if (mode == PIESLICE) { + if (x != cx || y != cy) { + add_edge(&e[n++], sx, sy, cx, cy); + add_edge(&e[n++], cx, cy, lx, ly); + if (inner) { + ImagingDrawWideLine(im, sx, sy, cx, cy, &ink, width, op); + ImagingDrawWideLine(im, cx, cy, lx, ly, &ink, width, op); + } + } + } else if (mode == CHORD) { + add_edge(&e[n++], sx, sy, lx, ly); + if (inner) { + add_edge(&e[n++], sx_inner, sy_inner, lx_inner, ly_inner); + } + } else if (mode == ARC) { + add_edge(&e[n++], sx, sy, sx_inner, sy_inner); + add_edge(&e[n++], lx, ly, lx_inner, ly_inner); + } + } + + draw->polygon(im, n, e, ink, 0); + + free(e); } + return 0; } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 6e4a00501..b033abf66 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -122,26 +122,29 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float* kernel, UINT8* in_1 = (UINT8*) im->image[y-1]; UINT8* in0 = (UINT8*) im->image[y]; UINT8* in1 = (UINT8*) im->image[y+1]; - UINT32* out = (UINT32*) imOut->image[y]; + UINT8* out = (UINT8*) imOut->image[y]; - out[0] = ((UINT32*) in0)[0]; + memcpy(out, in0, sizeof(UINT32)); if (im->bands == 2) { for (x = 1; x < im->xsize-1; x++) { float ss0 = offset; float ss3 = offset; + UINT32 v; ss0 += KERNEL1x3(in1, x*4+0, &kernel[0], 4); ss3 += KERNEL1x3(in1, x*4+3, &kernel[0], 4); ss0 += KERNEL1x3(in0, x*4+0, &kernel[3], 4); ss3 += KERNEL1x3(in0, x*4+3, &kernel[3], 4); ss0 += KERNEL1x3(in_1, x*4+0, &kernel[6], 4); ss3 += KERNEL1x3(in_1, x*4+3, &kernel[6], 4); - out[x] = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + memcpy(out + x * sizeof(v), &v, sizeof(v)); } } else if (im->bands == 3) { for (x = 1; x < im->xsize-1; x++) { float ss0 = offset; float ss1 = offset; float ss2 = offset; + UINT32 v; ss0 += KERNEL1x3(in1, x*4+0, &kernel[0], 4); ss1 += KERNEL1x3(in1, x*4+1, &kernel[0], 4); ss2 += KERNEL1x3(in1, x*4+2, &kernel[0], 4); @@ -151,8 +154,9 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float* kernel, ss0 += KERNEL1x3(in_1, x*4+0, &kernel[6], 4); ss1 += KERNEL1x3(in_1, x*4+1, &kernel[6], 4); ss2 += KERNEL1x3(in_1, x*4+2, &kernel[6], 4); - out[x] = MAKE_UINT32( + v = MAKE_UINT32( clip8(ss0), clip8(ss1), clip8(ss2), 0); + memcpy(out + x * sizeof(v), &v, sizeof(v)); } } else if (im->bands == 4) { for (x = 1; x < im->xsize-1; x++) { @@ -160,6 +164,7 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float* kernel, float ss1 = offset; float ss2 = offset; float ss3 = offset; + UINT32 v; ss0 += KERNEL1x3(in1, x*4+0, &kernel[0], 4); ss1 += KERNEL1x3(in1, x*4+1, &kernel[0], 4); ss2 += KERNEL1x3(in1, x*4+2, &kernel[0], 4); @@ -172,11 +177,12 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float* kernel, ss1 += KERNEL1x3(in_1, x*4+1, &kernel[6], 4); ss2 += KERNEL1x3(in_1, x*4+2, &kernel[6], 4); ss3 += KERNEL1x3(in_1, x*4+3, &kernel[6], 4); - out[x] = MAKE_UINT32( + v = MAKE_UINT32( clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + memcpy(out + x * sizeof(v), &v, sizeof(v)); } } - out[x] = ((UINT32*) in0)[x]; + memcpy(out + x * sizeof(UINT32), in0 + x * sizeof(UINT32), sizeof(UINT32)); } } memcpy(imOut->image[y], im->image[y], im->linesize); @@ -232,14 +238,14 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float* kernel, UINT8* in0 = (UINT8*) im->image[y]; UINT8* in1 = (UINT8*) im->image[y+1]; UINT8* in2 = (UINT8*) im->image[y+2]; - UINT32* out = (UINT32*) imOut->image[y]; + UINT8* out = (UINT8*) imOut->image[y]; - out[0] = ((UINT32*) in0)[0]; - out[1] = ((UINT32*) in0)[1]; + memcpy(out, in0, sizeof(UINT32) * 2); if (im->bands == 2) { for (x = 2; x < im->xsize-2; x++) { float ss0 = offset; float ss3 = offset; + UINT32 v; ss0 += KERNEL1x5(in2, x*4+0, &kernel[0], 4); ss3 += KERNEL1x5(in2, x*4+3, &kernel[0], 4); ss0 += KERNEL1x5(in1, x*4+0, &kernel[5], 4); @@ -250,13 +256,15 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float* kernel, ss3 += KERNEL1x5(in_1, x*4+3, &kernel[15], 4); ss0 += KERNEL1x5(in_2, x*4+0, &kernel[20], 4); ss3 += KERNEL1x5(in_2, x*4+3, &kernel[20], 4); - out[x] = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + memcpy(out + x * sizeof(v), &v, sizeof(v)); } } else if (im->bands == 3) { for (x = 2; x < im->xsize-2; x++) { float ss0 = offset; float ss1 = offset; float ss2 = offset; + UINT32 v; ss0 += KERNEL1x5(in2, x*4+0, &kernel[0], 4); ss1 += KERNEL1x5(in2, x*4+1, &kernel[0], 4); ss2 += KERNEL1x5(in2, x*4+2, &kernel[0], 4); @@ -272,8 +280,9 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float* kernel, ss0 += KERNEL1x5(in_2, x*4+0, &kernel[20], 4); ss1 += KERNEL1x5(in_2, x*4+1, &kernel[20], 4); ss2 += KERNEL1x5(in_2, x*4+2, &kernel[20], 4); - out[x] = MAKE_UINT32( + v = MAKE_UINT32( clip8(ss0), clip8(ss1), clip8(ss2), 0); + memcpy(out + x * sizeof(v), &v, sizeof(v)); } } else if (im->bands == 4) { for (x = 2; x < im->xsize-2; x++) { @@ -281,6 +290,7 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float* kernel, float ss1 = offset; float ss2 = offset; float ss3 = offset; + UINT32 v; ss0 += KERNEL1x5(in2, x*4+0, &kernel[0], 4); ss1 += KERNEL1x5(in2, x*4+1, &kernel[0], 4); ss2 += KERNEL1x5(in2, x*4+2, &kernel[0], 4); @@ -301,12 +311,12 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float* kernel, ss1 += KERNEL1x5(in_2, x*4+1, &kernel[20], 4); ss2 += KERNEL1x5(in_2, x*4+2, &kernel[20], 4); ss3 += KERNEL1x5(in_2, x*4+3, &kernel[20], 4); - out[x] = MAKE_UINT32( + v = MAKE_UINT32( clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + memcpy(out + x * sizeof(v), &v, sizeof(v)); } } - out[x] = ((UINT32*) in0)[x]; - out[x+1] = ((UINT32*) in0)[x+1]; + memcpy(out + x * sizeof(UINT32), in0 + x * sizeof(UINT32), sizeof(UINT32) * 2); } } memcpy(imOut->image[y], im->image[y], im->linesize); diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index 6d22c6c4e..5f4485f89 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -26,11 +26,11 @@ int -ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) +ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { UINT8* ptr; int framesize; - int c, chunks; + int c, chunks, advance; int l, lines; int i, j, x = 0, y, ymax; @@ -59,10 +59,16 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) chunks = I16(ptr+6); ptr += 16; + bytes -= 16; /* Process subchunks */ for (c = 0; c < chunks; c++) { - UINT8 *data = ptr + 6; + UINT8* data; + if (bytes < 10) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + data = ptr + 6; switch (I16(ptr+4)) { case 4: case 11: /* FLI COLOR chunk */ @@ -198,7 +204,9 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) state->errcode = IMAGING_CODEC_UNKNOWN; return -1; } - ptr += I32(ptr); + advance = I32(ptr); + ptr += advance; + bytes -= advance; } return -1; /* end of frame */ diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 1d08728da..fd5e25958 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -29,8 +29,8 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) #define FLIP_LEFT_RIGHT(INT, image) \ for (y = 0; y < imIn->ysize; y++) { \ - INT* in = imIn->image[y]; \ - INT* out = imOut->image[y]; \ + INT* in = (INT *)imIn->image[y]; \ + INT* out = (INT *)imOut->image[y]; \ xr = imIn->xsize-1; \ for (x = 0; x < imIn->xsize; x++, xr--) \ out[xr] = in[x]; \ @@ -39,7 +39,11 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); if (imIn->image8) { - FLIP_LEFT_RIGHT(UINT8, image8) + if (strncmp(imIn->mode, "I;16", 4) == 0) { + FLIP_LEFT_RIGHT(UINT16, image8) + } else { + FLIP_LEFT_RIGHT(UINT8, image8) + } } else { FLIP_LEFT_RIGHT(INT32, image32) } @@ -101,10 +105,11 @@ ImagingRotate90(Imaging imOut, Imaging imIn) yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize ? yy + ROTATE_SMALL_CHUNK : imIn->ysize; \ xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ for (yyy = yy; yyy < yyysize; yyy++) { \ - INT* in = imIn->image[yyy]; \ + INT* in = (INT *)imIn->image[yyy]; \ xr = imIn->xsize - 1 - xx; \ for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ - imOut->image[xr][yyy] = in[xxx]; \ + INT* out = (INT *)imOut->image[xr]; \ + out[yyy] = in[xxx]; \ } \ } \ } \ @@ -114,10 +119,15 @@ ImagingRotate90(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); - if (imIn->image8) - ROTATE_90(UINT8, image8) - else - ROTATE_90(INT32, image32) + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ROTATE_90(UINT16, image8); + } else { + ROTATE_90(UINT8, image8); + } + } else { + ROTATE_90(INT32, image32); + } ImagingSectionLeave(&cookie); @@ -151,9 +161,10 @@ ImagingTranspose(Imaging imOut, Imaging imIn) yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize ? yy + ROTATE_SMALL_CHUNK : imIn->ysize; \ xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ for (yyy = yy; yyy < yyysize; yyy++) { \ - INT* in = imIn->image[yyy]; \ + INT* in = (INT *)imIn->image[yyy]; \ for (xxx = xx; xxx < xxxsize; xxx++) { \ - imOut->image[xxx][yyy] = in[xxx]; \ + INT* out = (INT *)imOut->image[xxx]; \ + out[yyy] = in[xxx]; \ } \ } \ } \ @@ -163,10 +174,15 @@ ImagingTranspose(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); - if (imIn->image8) - TRANSPOSE(UINT8, image8) - else - TRANSPOSE(INT32, image32) + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + TRANSPOSE(UINT16, image8); + } else { + TRANSPOSE(UINT8, image8); + } + } else { + TRANSPOSE(INT32, image32); + } ImagingSectionLeave(&cookie); @@ -201,10 +217,11 @@ ImagingTransverse(Imaging imOut, Imaging imIn) xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ yr = imIn->ysize - 1 - yy; \ for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ - INT* in = imIn->image[yyy]; \ + INT* in = (INT *)imIn->image[yyy]; \ xr = imIn->xsize - 1 - xx; \ for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ - imOut->image[xr][yr] = in[xxx]; \ + INT* out = (INT *)imOut->image[xr]; \ + out[yr] = in[xxx]; \ } \ } \ } \ @@ -214,10 +231,15 @@ ImagingTransverse(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); - if (imIn->image8) - TRANSVERSE(UINT8, image8) - else - TRANSVERSE(INT32, image32) + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + TRANSVERSE(UINT16, image8); + } else { + TRANSVERSE(UINT8, image8); + } + } else { + TRANSVERSE(INT32, image32); + } ImagingSectionLeave(&cookie); @@ -242,8 +264,8 @@ ImagingRotate180(Imaging imOut, Imaging imIn) #define ROTATE_180(INT, image) \ for (y = 0; y < imIn->ysize; y++, yr--) { \ - INT* in = imIn->image[y]; \ - INT* out = imOut->image[yr]; \ + INT* in = (INT *)imIn->image[y]; \ + INT* out = (INT *)imOut->image[yr]; \ xr = imIn->xsize-1; \ for (x = 0; x < imIn->xsize; x++, xr--) \ out[xr] = in[x]; \ @@ -253,7 +275,11 @@ ImagingRotate180(Imaging imOut, Imaging imIn) yr = imIn->ysize-1; if (imIn->image8) { - ROTATE_180(UINT8, image8) + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ROTATE_180(UINT16, image8) + } else { + ROTATE_180(UINT8, image8) + } } else { ROTATE_180(INT32, image32) } @@ -291,9 +317,10 @@ ImagingRotate270(Imaging imOut, Imaging imIn) xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ yr = imIn->ysize - 1 - yy; \ for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ - INT* in = imIn->image[yyy]; \ + INT* in = (INT *)imIn->image[yyy]; \ for (xxx = xx; xxx < xxxsize; xxx++) { \ - imOut->image[xxx][yr] = in[xxx]; \ + INT* out = (INT *)imOut->image[xxx]; \ + out[yr] = in[xxx]; \ } \ } \ } \ @@ -303,10 +330,15 @@ ImagingRotate270(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); - if (imIn->image8) - ROTATE_270(UINT8, image8) - else - ROTATE_270(INT32, image32) + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ROTATE_270(UINT16, image8); + } else { + ROTATE_270(UINT8, image8); + } + } else { + ROTATE_270(INT32, image32); + } ImagingSectionLeave(&cookie); @@ -396,7 +428,7 @@ nearest_filter16(void* out, Imaging im, double xin, double yin) int y = COORD(yin); if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) return 0; - ((INT16*)out)[0] = ((INT16*)(im->image8[y]))[x]; + memcpy(out, im->image8[y] + x * sizeof(INT16), sizeof(INT16)); return 1; } @@ -407,7 +439,7 @@ nearest_filter32(void* out, Imaging im, double xin, double yin) int y = COORD(yin); if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) return 0; - ((INT32*)out)[0] = im->image32[y][x]; + memcpy(out, &im->image32[y][x], sizeof(INT32)); return 1; } @@ -457,18 +489,22 @@ bilinear_filter8(void* out, Imaging im, double xin, double yin) static int bilinear_filter32I(void* out, Imaging im, double xin, double yin) { + INT32 k; BILINEAR_HEAD(INT32); BILINEAR_BODY(INT32, im->image32, 1, 0); - ((INT32*)out)[0] = (INT32) v1; + k = v1; + memcpy(out, &k, sizeof(k)); return 1; } static int bilinear_filter32F(void* out, Imaging im, double xin, double yin) { + FLOAT32 k; BILINEAR_HEAD(FLOAT32); BILINEAR_BODY(FLOAT32, im->image32, 1, 0); - ((FLOAT32*)out)[0] = (FLOAT32) v1; + k = v1; + memcpy(out, &k, sizeof(k)); return 1; } @@ -569,18 +605,22 @@ bicubic_filter8(void* out, Imaging im, double xin, double yin) static int bicubic_filter32I(void* out, Imaging im, double xin, double yin) { + INT32 k; BICUBIC_HEAD(INT32); BICUBIC_BODY(INT32, im->image32, 1, 0); - ((INT32*)out)[0] = (INT32) v1; + k = v1; + memcpy(out, &k, sizeof(k)); return 1; } static int bicubic_filter32F(void* out, Imaging im, double xin, double yin) { + FLOAT32 k; BICUBIC_HEAD(FLOAT32); BICUBIC_BODY(FLOAT32, im->image32, 1, 0); - ((FLOAT32*)out)[0] = (FLOAT32) v1; + k = v1; + memcpy(out, &k, sizeof(k)); return 1; } diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 3cfa42c48..b63888f87 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -146,8 +146,8 @@ ImagingGetExtrema(Imaging im, void *extrema) imax = in[x]; } } - ((INT32*) extrema)[0] = imin; - ((INT32*) extrema)[1] = imax; + memcpy(extrema, &imin, sizeof(imin)); + memcpy(((char*)extrema) + sizeof(imin), &imax, sizeof(imax)); break; case IMAGING_TYPE_FLOAT32: fmin = fmax = ((FLOAT32*) im->image32[0])[0]; @@ -160,23 +160,27 @@ ImagingGetExtrema(Imaging im, void *extrema) fmax = in[x]; } } - ((FLOAT32*) extrema)[0] = fmin; - ((FLOAT32*) extrema)[1] = fmax; + memcpy(extrema, &fmin, sizeof(fmin)); + memcpy(((char*)extrema) + sizeof(fmin), &fmax, sizeof(fmax)); break; case IMAGING_TYPE_SPECIAL: if (strcmp(im->mode, "I;16") == 0) { - imin = imax = ((UINT16*) im->image8[0])[0]; + UINT16 v; + memcpy(&v, *im->image8, sizeof(v)); + imin = imax = v; for (y = 0; y < im->ysize; y++) { - UINT16* in = (UINT16 *) im->image[y]; for (x = 0; x < im->xsize; x++) { - if (imin > in[x]) - imin = in[x]; - else if (imax < in[x]) - imax = in[x]; + memcpy(&v, im->image[y] + x * sizeof(v), sizeof(v)); + if (imin > v) + imin = v; + else if (imax < v) + imax = v; } } - ((UINT16*) extrema)[0] = (UINT16) imin; - ((UINT16*) extrema)[1] = (UINT16) imax; + v = (UINT16) imin; + memcpy(extrema, &v, sizeof(v)); + v = (UINT16) imax; + memcpy(((char*)extrema) + sizeof(v), &v, sizeof(v)); break; } /* FALL THROUGH */ diff --git a/src/libImaging/GifDecode.c b/src/libImaging/GifDecode.c index 58cf54400..68429ab26 100644 --- a/src/libImaging/GifDecode.c +++ b/src/libImaging/GifDecode.c @@ -58,7 +58,7 @@ int -ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes) +ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ssize_t bytes) { UINT8* p; UINT8* out; diff --git a/src/libImaging/HexDecode.c b/src/libImaging/HexDecode.c index 14f5241dc..8bd9bf67f 100644 --- a/src/libImaging/HexDecode.c +++ b/src/libImaging/HexDecode.c @@ -21,7 +21,7 @@ (v >= 'A' && v <= 'F') ? v - 'A' + 10 : -1) int -ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) +ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { UINT8* ptr; int a, b; diff --git a/src/libImaging/Histo.c b/src/libImaging/Histo.c index b7c1a9834..5c2824ab0 100644 --- a/src/libImaging/Histo.c +++ b/src/libImaging/Histo.c @@ -130,8 +130,8 @@ ImagingGetHistogram(Imaging im, Imaging imMask, void* minmax) } if (!im->xsize || !im->ysize) break; - imin = ((INT32*) minmax)[0]; - imax = ((INT32*) minmax)[1]; + memcpy(&imin, minmax, sizeof(imin)); + memcpy(&imax, ((char*)minmax) + sizeof(imin), sizeof(imax)); if (imin >= imax) break; ImagingSectionEnter(&cookie); @@ -153,8 +153,8 @@ ImagingGetHistogram(Imaging im, Imaging imMask, void* minmax) } if (!im->xsize || !im->ysize) break; - fmin = ((FLOAT32*) minmax)[0]; - fmax = ((FLOAT32*) minmax)[1]; + memcpy(&fmin, minmax, sizeof(fmin)); + memcpy(&fmax, ((char*)minmax) + sizeof(fmin), sizeof(fmax)); if (fmin >= fmax) break; ImagingSectionEnter(&cookie); diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index e705e0a60..25c15e758 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -413,22 +413,22 @@ typedef int (*ImagingCodec)(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); extern int ImagingBcnDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingBitDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); extern int ImagingFliDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingGifDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); extern int ImagingHexDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); #ifdef HAVE_LIBJPEG extern int ImagingJpegDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingJpegDecodeCleanup(ImagingCodecState state); extern int ImagingJpegUseJCSExtensions(void); @@ -437,7 +437,7 @@ extern int ImagingJpegEncode(Imaging im, ImagingCodecState state, #endif #ifdef HAVE_OPENJPEG extern int ImagingJpeg2KDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingJpeg2KDecodeCleanup(ImagingCodecState state); extern int ImagingJpeg2KEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); @@ -445,44 +445,44 @@ extern int ImagingJpeg2KEncodeCleanup(ImagingCodecState state); #endif #ifdef HAVE_LIBTIFF extern int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); #endif #ifdef HAVE_LIBMPEG extern int ImagingMpegDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); #endif extern int ImagingMspDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingPackbitsDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingPcdDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingPcxDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); extern int ImagingRawDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); extern int ImagingSgiRleDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingSgiRleDecodeCleanup(ImagingCodecState state); extern int ImagingSunRleDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingTgaRleDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); extern int ImagingXbmDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingXbmEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); #ifdef HAVE_LIBZ extern int ImagingZipDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + UINT8* buffer, Py_ssize_t bytes); extern int ImagingZipDecodeCleanup(ImagingCodecState state); extern int ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index 9140e0074..f2e437dda 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -769,7 +769,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) } int -ImagingJpeg2KDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) +ImagingJpeg2KDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { if (bytes){ diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 92e55e190..aef1a4b94 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -194,7 +194,7 @@ static void j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { float rate; - unsigned n; + int n; /* These settings have been copied from opj_compress in the OpenJPEG sources. */ @@ -522,7 +522,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) tile_ndx = 0; for (y = 0; y < tiles_y; ++y) { - unsigned ty0 = params.cp_ty0 + y * tile_height; + int ty0 = params.cp_ty0 + y * tile_height; unsigned ty1 = ty0 + tile_height; unsigned pixy, pixh; @@ -535,7 +535,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) pixh = ty1 - ty0; for (x = 0; x < tiles_x; ++x) { - unsigned tx0 = params.cp_tx0 + x * tile_width; + int tx0 = params.cp_tx0 + x * tile_width; unsigned tx1 = tx0 + tile_width; unsigned pixx, pixw; unsigned data_size; diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index 33cc5d095..39d96de53 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -144,7 +144,7 @@ output(j_common_ptr cinfo) /* -------------------------------------------------------------------- */ int -ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) +ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { JPEGSTATE* context = (JPEGSTATE*) state->context; int ok; diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 5c298c6c5..eaa276af4 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -251,8 +251,17 @@ ImagingPackRGB(UINT8* out, const UINT8* in, int pixels) { int i = 0; /* RGB triplets */ +#ifdef __sparc + /* SPARC CPUs cannot read integers from nonaligned addresses. */ + for (; i < pixels; i++) { + out[0] = in[R]; + out[1] = in[G]; + out[2] = in[B]; + out += 3; in += 4; + } +#else for (; i < pixels-1; i++) { - ((UINT32*)out)[0] = ((UINT32*)in)[i]; + memcpy(out, in + i * 4, 4); out += 3; } for (; i < pixels; i++) { @@ -261,6 +270,7 @@ ImagingPackRGB(UINT8* out, const UINT8* in, int pixels) out[2] = in[i*4+B]; out += 3; } +#endif } void @@ -392,18 +402,19 @@ static void packI16B(UINT8* out, const UINT8* in_, int pixels) { int i; - INT32* in = (INT32*) in_; UINT16 tmp_; UINT8* tmp = (UINT8*) &tmp_; for (i = 0; i < pixels; i++) { - if (in[0] <= 0) + INT32 in; + memcpy(&in, in_, sizeof(in)); + if (in <= 0) tmp_ = 0; - else if (in[0] > 65535) + else if (in > 65535) tmp_ = 65535; else - tmp_ = in[0]; + tmp_ = in; C16B; - out += 2; in++; + out += 2; in_ += sizeof(in); } } @@ -639,6 +650,7 @@ static struct { /* storage modes */ {"I;16", "I;16", 16, copy2}, + {"I;16", "I;16B", 16, packI16N_I16B}, {"I;16B", "I;16B", 16, copy2}, {"I;16L", "I;16L", 16, copy2}, {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. diff --git a/src/libImaging/PackDecode.c b/src/libImaging/PackDecode.c index aea8f04e3..ef54f3c9a 100644 --- a/src/libImaging/PackDecode.c +++ b/src/libImaging/PackDecode.c @@ -18,7 +18,7 @@ int ImagingPackbitsDecode(Imaging im, ImagingCodecState state, - UINT8* buf, int bytes) + UINT8* buf, Py_ssize_t bytes) { UINT8 n; UINT8* ptr; diff --git a/src/libImaging/PcdDecode.c b/src/libImaging/PcdDecode.c index f92343890..8ff264edf 100644 --- a/src/libImaging/PcdDecode.c +++ b/src/libImaging/PcdDecode.c @@ -24,7 +24,7 @@ int -ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) +ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { int x; int chunk; diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index e5417f1bd..67dcc1e08 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -17,11 +17,16 @@ #include "Imaging.h" int -ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) +ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { UINT8 n; UINT8* ptr; + if (strcmp(im->mode, "1") == 0 && state->xsize > state->bytes * 8) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + ptr = buf; for (;;) { diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index 426c410c9..9b4bf6b75 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -99,12 +99,12 @@ im_point_8_32(Imaging imOut, Imaging imIn, im_point_context* context) { int x, y; /* 8-bit source, 32-bit destination */ - INT32* table = (INT32*) context->table; + char* table = (char*) context->table; for (y = 0; y < imIn->ysize; y++) { UINT8* in = imIn->image8[y]; INT32* out = imOut->image32[y]; for (x = 0; x < imIn->xsize; x++) - out[x] = table[in[x]]; + memcpy(out + x, table + in[x] * sizeof(INT32), sizeof(INT32)); } } @@ -242,11 +242,15 @@ ImagingPointTransform(Imaging imIn, double scale, double offset) if (strcmp(imIn->mode,"I;16") == 0) { ImagingSectionEnter(&cookie); for (y = 0; y < imIn->ysize; y++) { - UINT16* in = (UINT16 *)imIn->image[y]; - UINT16* out = (UINT16 *)imOut->image[y]; + char* in = (char*)imIn->image[y]; + char* out = (char*)imOut->image[y]; /* FIXME: add clipping? */ - for (x = 0; x < imIn->xsize; x++) - out[x] = in[x] * scale + offset; + for (x = 0; x < imIn->xsize; x++) { + UINT16 v; + memcpy(&v, in + x * sizeof(v), sizeof(v)); + v = v * scale + offset; + memcpy(out + x * sizeof(UINT16), &v, sizeof(v)); + } } ImagingSectionLeave(&cookie); break; diff --git a/src/libImaging/QuantHash.c b/src/libImaging/QuantHash.c index 48b7a973e..3fcbf3c02 100644 --- a/src/libImaging/QuantHash.c +++ b/src/libImaging/QuantHash.c @@ -35,8 +35,6 @@ struct _HashTable { uint32_t count; HashFunc hashFunc; HashCmpFunc cmpFunc; - KeyDestroyFunc keyDestroyFunc; - ValDestroyFunc valDestroyFunc; void *userData; }; @@ -51,8 +49,6 @@ HashTable *hashtable_new(HashFunc hf,HashCmpFunc cf) { if (!h) { return NULL; } h->hashFunc=hf; h->cmpFunc=cf; - h->keyDestroyFunc=NULL; - h->valDestroyFunc=NULL; h->length=MIN_LENGTH; h->count=0; h->userData=NULL; @@ -62,15 +58,6 @@ HashTable *hashtable_new(HashFunc hf,HashCmpFunc cf) { return h; } -static void _hashtable_destroy(const HashTable *h,const HashKey_t key,const HashVal_t val,void *u) { - if (h->keyDestroyFunc) { - h->keyDestroyFunc(h,key); - } - if (h->valDestroyFunc) { - h->valDestroyFunc(h,val); - } -} - static uint32_t _findPrime(uint32_t start,int dir) { static int unit[]={0,1,0,1,0,0,0,1,0,1,0,1,0,1,0,0}; uint32_t t; @@ -144,12 +131,6 @@ static int _hashtable_insert_node(HashTable *h,HashNode *node,int resize,int upd free(node); return 1; } else { - if (h->valDestroyFunc) { - h->valDestroyFunc(h,nv->value); - } - if (h->keyDestroyFunc) { - h->keyDestroyFunc(h,nv->key); - } nv->key=node->key; nv->value=node->value; free(node); @@ -180,7 +161,6 @@ static int _hashtable_insert(HashTable *h,HashKey_t key,HashVal_t val,int resize nv=*n; i=h->cmpFunc(h,nv->key,key); if (!i) { - if (h->valDestroyFunc) { h->valDestroyFunc(h,nv->value); } nv->value=val; return 1; } else if (i>0) { @@ -202,34 +182,6 @@ static int _hashtable_insert(HashTable *h,HashKey_t key,HashVal_t val,int resize } } -static int _hashtable_lookup_or_insert(HashTable *h,HashKey_t key,HashVal_t *retVal,HashVal_t newVal,int resize) { - HashNode **n,*nv; - HashNode *t; - int i; - uint32_t hash=h->hashFunc(h,key)%h->length; - - for (n=&(h->table[hash]);*n;n=&((*n)->next)) { - nv=*n; - i=h->cmpFunc(h,nv->key,key); - if (!i) { - *retVal=nv->value; - return 1; - } else if (i>0) { - break; - } - } - t=malloc(sizeof(HashNode)); - if (!t) return 0; - t->next=*n; - *n=t; - t->key=key; - t->value=newVal; - *retVal=newVal; - h->count++; - if (resize) _hashtable_resize(h); - return 1; -} - int hashtable_insert_or_update_computed(HashTable *h, HashKey_t key, ComputeFunc newFunc, @@ -243,14 +195,8 @@ int hashtable_insert_or_update_computed(HashTable *h, nv=*n; i=h->cmpFunc(h,nv->key,key); if (!i) { - HashVal_t old=nv->value; if (existsFunc) { existsFunc(h,nv->key,&(nv->value)); - if (nv->value!=old) { - if (h->valDestroyFunc) { - h->valDestroyFunc(h,old); - } - } } else { return 0; } @@ -275,10 +221,6 @@ int hashtable_insert_or_update_computed(HashTable *h, return 1; } -int hashtable_update(HashTable *h,HashKey_t key,HashVal_t val) { - return _hashtable_insert(h,key,val,1,0); -} - int hashtable_insert(HashTable *h,HashKey_t key,HashVal_t val) { return _hashtable_insert(h,key,val,1,0); } @@ -314,9 +256,6 @@ void hashtable_free(HashTable *h) { uint32_t i; if (h->table) { - if (h->keyDestroyFunc || h->keyDestroyFunc) { - hashtable_foreach(h,_hashtable_destroy,NULL); - } for (i=0;ilength;i++) { for (n=h->table[i];n;n=nn) { nn=n->next; @@ -328,84 +267,10 @@ void hashtable_free(HashTable *h) { free(h); } -ValDestroyFunc hashtable_set_value_destroy_func(HashTable *h,ValDestroyFunc d) { - ValDestroyFunc r=h->valDestroyFunc; - h->valDestroyFunc=d; - return r; -} - -KeyDestroyFunc hashtable_set_key_destroy_func(HashTable *h,KeyDestroyFunc d) { - KeyDestroyFunc r=h->keyDestroyFunc; - h->keyDestroyFunc=d; - return r; -} - -static int _hashtable_remove(HashTable *h, - const HashKey_t key, - HashKey_t *keyRet, - HashVal_t *valRet, - int resize) { - uint32_t hash=h->hashFunc(h,key)%h->length; - HashNode *n,*p; - int i; - - for (p=NULL,n=h->table[hash];n;p=n,n=n->next) { - i=h->cmpFunc(h,n->key,key); - if (!i) { - if (p) p=n->next; else h->table[hash]=n->next; - *keyRet=n->key; - *valRet=n->value; - free(n); - h->count++; - return 1; - } else if (i>0) { - break; - } - } - return 0; -} - -static int _hashtable_delete(HashTable *h,const HashKey_t key,int resize) { - uint32_t hash=h->hashFunc(h,key)%h->length; - HashNode *n,*p; - int i; - - for (p=NULL,n=h->table[hash];n;p=n,n=n->next) { - i=h->cmpFunc(h,n->key,key); - if (!i) { - if (p) p=n->next; else h->table[hash]=n->next; - if (h->valDestroyFunc) { h->valDestroyFunc(h,n->value); } - if (h->keyDestroyFunc) { h->keyDestroyFunc(h,n->key); } - free(n); - h->count++; - return 1; - } else if (i>0) { - break; - } - } - return 0; -} - -int hashtable_remove(HashTable *h,const HashKey_t key,HashKey_t *keyRet,HashVal_t *valRet) { - return _hashtable_remove(h,key,keyRet,valRet,1); -} - -int hashtable_delete(HashTable *h,const HashKey_t key) { - return _hashtable_delete(h,key,1); -} - void hashtable_rehash_compute(HashTable *h,CollisionFunc cf) { _hashtable_rehash(h,cf,h->length); } -void hashtable_rehash(HashTable *h) { - _hashtable_rehash(h,NULL,h->length); -} - -int hashtable_lookup_or_insert(HashTable *h,HashKey_t key,HashVal_t *valp,HashVal_t val) { - return _hashtable_lookup_or_insert(h,key,valp,val,1); -} - int hashtable_lookup(const HashTable *h,const HashKey_t key,HashVal_t *valp) { uint32_t hash=h->hashFunc(h,key)%h->length; HashNode *n; diff --git a/src/libImaging/QuantHash.h b/src/libImaging/QuantHash.h index 028b4af89..9874114e5 100644 --- a/src/libImaging/QuantHash.h +++ b/src/libImaging/QuantHash.h @@ -22,8 +22,6 @@ typedef uint32_t (*HashFunc)(const HashTable *,const HashKey_t); typedef int (*HashCmpFunc)(const HashTable *,const HashKey_t,const HashKey_t); typedef void (*IteratorFunc)(const HashTable *,const HashKey_t,const HashVal_t,void *); typedef void (*IteratorUpdateFunc)(const HashTable *,const HashKey_t,HashVal_t *,void *); -typedef void (*KeyDestroyFunc)(const HashTable *,HashKey_t); -typedef void (*ValDestroyFunc)(const HashTable *,HashVal_t); typedef void (*ComputeFunc)(const HashTable *,const HashKey_t,HashVal_t *); typedef void (*CollisionFunc)(const HashTable *,HashKey_t *,HashVal_t *,HashKey_t,HashVal_t); @@ -32,18 +30,11 @@ void hashtable_free(HashTable *h); void hashtable_foreach(HashTable *h,IteratorFunc i,void *u); void hashtable_foreach_update(HashTable *h,IteratorUpdateFunc i,void *u); int hashtable_insert(HashTable *h,HashKey_t key,HashVal_t val); -int hashtable_update(HashTable *h,HashKey_t key,HashVal_t val); int hashtable_lookup(const HashTable *h,const HashKey_t key,HashVal_t *valp); -int hashtable_lookup_or_insert(HashTable *h,HashKey_t key,HashVal_t *valp,HashVal_t val); int hashtable_insert_or_update_computed(HashTable *h,HashKey_t key,ComputeFunc newFunc,ComputeFunc existsFunc); -int hashtable_delete(HashTable *h,const HashKey_t key); -int hashtable_remove(HashTable *h,const HashKey_t key,HashKey_t *keyRet,HashVal_t *valRet); void *hashtable_set_user_data(HashTable *h,void *data); void *hashtable_get_user_data(const HashTable *h); -KeyDestroyFunc hashtable_set_key_destroy_func(HashTable *,KeyDestroyFunc d); -ValDestroyFunc hashtable_set_value_destroy_func(HashTable *,ValDestroyFunc d); uint32_t hashtable_get_count(const HashTable *h); -void hashtable_rehash(HashTable *h); void hashtable_rehash_compute(HashTable *h,CollisionFunc cf); #endif // __QUANTHASH_H__ diff --git a/src/libImaging/RawDecode.c b/src/libImaging/RawDecode.c index 40c0cb79a..c069bdb88 100644 --- a/src/libImaging/RawDecode.c +++ b/src/libImaging/RawDecode.c @@ -20,7 +20,7 @@ int -ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) +ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { enum { LINE = 1, SKIP }; RAWSTATE* rawstate = state->context; @@ -33,8 +33,15 @@ ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) /* get size of image data and padding */ state->bytes = (state->xsize * state->bits + 7) / 8; - rawstate->skip = (rawstate->stride) ? - rawstate->stride - state->bytes : 0; + if (rawstate->stride) { + rawstate->skip = rawstate->stride - state->bytes; + if (rawstate->skip < 0) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + } else { + rawstate->skip = 0; + } /* check image orientation */ if (state->ystep < 0) { diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 90e2aa1d0..d1a89e2ce 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -304,6 +304,7 @@ ImagingResampleHorizontal_8bpc(Imaging imOut, Imaging imIn, int offset, if (imIn->bands == 2) { for (yy = 0; yy < imOut->ysize; yy++) { for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; xmin = bounds[xx * 2 + 0]; xmax = bounds[xx * 2 + 1]; k = &kk[xx * ksize]; @@ -312,13 +313,14 @@ ImagingResampleHorizontal_8bpc(Imaging imOut, Imaging imIn, int offset, ss0 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 0]) * k[x]; ss3 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 3]) * k[x]; } - ((UINT32 *) imOut->image[yy])[xx] = MAKE_UINT32( - clip8(ss0), 0, 0, clip8(ss3)); + v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); } } } else if (imIn->bands == 3) { for (yy = 0; yy < imOut->ysize; yy++) { for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; xmin = bounds[xx * 2 + 0]; xmax = bounds[xx * 2 + 1]; k = &kk[xx * ksize]; @@ -328,13 +330,14 @@ ImagingResampleHorizontal_8bpc(Imaging imOut, Imaging imIn, int offset, ss1 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 1]) * k[x]; ss2 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 2]) * k[x]; } - ((UINT32 *) imOut->image[yy])[xx] = MAKE_UINT32( - clip8(ss0), clip8(ss1), clip8(ss2), 0); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); } } } else { for (yy = 0; yy < imOut->ysize; yy++) { for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; xmin = bounds[xx * 2 + 0]; xmax = bounds[xx * 2 + 1]; k = &kk[xx * ksize]; @@ -345,8 +348,8 @@ ImagingResampleHorizontal_8bpc(Imaging imOut, Imaging imIn, int offset, ss2 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 2]) * k[x]; ss3 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 3]) * k[x]; } - ((UINT32 *) imOut->image[yy])[xx] = MAKE_UINT32( - clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); } } } @@ -388,13 +391,14 @@ ImagingResampleVertical_8bpc(Imaging imOut, Imaging imIn, int offset, ymin = bounds[yy * 2 + 0]; ymax = bounds[yy * 2 + 1]; for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; ss0 = ss3 = 1 << (PRECISION_BITS -1); for (y = 0; y < ymax; y++) { ss0 += ((UINT8) imIn->image[y + ymin][xx*4 + 0]) * k[y]; ss3 += ((UINT8) imIn->image[y + ymin][xx*4 + 3]) * k[y]; } - ((UINT32 *) imOut->image[yy])[xx] = MAKE_UINT32( - clip8(ss0), 0, 0, clip8(ss3)); + v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); } } } else if (imIn->bands == 3) { @@ -403,14 +407,15 @@ ImagingResampleVertical_8bpc(Imaging imOut, Imaging imIn, int offset, ymin = bounds[yy * 2 + 0]; ymax = bounds[yy * 2 + 1]; for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; ss0 = ss1 = ss2 = 1 << (PRECISION_BITS -1); for (y = 0; y < ymax; y++) { ss0 += ((UINT8) imIn->image[y + ymin][xx*4 + 0]) * k[y]; ss1 += ((UINT8) imIn->image[y + ymin][xx*4 + 1]) * k[y]; ss2 += ((UINT8) imIn->image[y + ymin][xx*4 + 2]) * k[y]; } - ((UINT32 *) imOut->image[yy])[xx] = MAKE_UINT32( - clip8(ss0), clip8(ss1), clip8(ss2), 0); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); } } } else { @@ -419,6 +424,7 @@ ImagingResampleVertical_8bpc(Imaging imOut, Imaging imIn, int offset, ymin = bounds[yy * 2 + 0]; ymax = bounds[yy * 2 + 1]; for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; ss0 = ss1 = ss2 = ss3 = 1 << (PRECISION_BITS -1); for (y = 0; y < ymax; y++) { ss0 += ((UINT8) imIn->image[y + ymin][xx*4 + 0]) * k[y]; @@ -426,8 +432,8 @@ ImagingResampleVertical_8bpc(Imaging imOut, Imaging imIn, int offset, ss2 += ((UINT8) imIn->image[y + ymin][xx*4 + 2]) * k[y]; ss3 += ((UINT8) imIn->image[y + ymin][xx*4 + 3]) * k[y]; } - ((UINT32 *) imOut->image[yy])[xx] = MAKE_UINT32( - clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); } } } @@ -621,8 +627,6 @@ ImagingResampleInner(Imaging imIn, int xsize, int ysize, if ( ! ksize_vert) { free(bounds_horiz); free(kk_horiz); - free(bounds_vert); - free(kk_vert); return NULL; } diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index 9d8e56376..8a81ba8e6 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -56,15 +56,15 @@ static int expandrow(UINT8* dest, UINT8* src, int n, int z) return 0; } -static int expandrow2(UINT16* dest, UINT16* src, int n, int z) +static int expandrow2(UINT8* dest, const UINT8* src, int n, int z) { UINT8 pixel, count; for (;n > 0; n--) { - pixel = ((UINT8*)src)[1]; - ++src; + pixel = src[1]; + src+=2; if (n == 1 && pixel != 0) return n; count = pixel & RLE_MAX_RUN; @@ -72,16 +72,17 @@ static int expandrow2(UINT16* dest, UINT16* src, int n, int z) return count; if (pixel & RLE_COPY_FLAG) { while(count--) { - *dest = *src++; - dest += z; + memcpy(dest, src, 2); + src += 2; + dest += z * 2; } } else { while (count--) { - *dest = *src; - dest += z; + memcpy(dest, src, 2); + dest += z * 2; } - ++src; + src+=2; } } return 0; @@ -90,7 +91,7 @@ static int expandrow2(UINT16* dest, UINT16* src, int n, int z) int ImagingSgiRleDecode(Imaging im, ImagingCodecState state, - UINT8* buf, int bytes) + UINT8* buf, Py_ssize_t bytes) { UINT8 *ptr; SGISTATE *c; @@ -156,13 +157,18 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, c->rlelength = c->lengthtab[c->rowno + c->channo * im->ysize]; c->rleoffset -= SGI_HEADER_SIZE; + if (c->rleoffset + c->rlelength > c->bufsize) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + /* row decompression */ if (c->bpc ==1) { if(expandrow(&state->buffer[c->channo], &ptr[c->rleoffset], c->rlelength, im->bands)) goto sgi_finish_decode; } else { - if(expandrow2((UINT16*)&state->buffer[c->channo * 2], (UINT16*)&ptr[c->rleoffset], c->rlelength, im->bands)) + if(expandrow2(&state->buffer[c->channo * 2], &ptr[c->rleoffset], c->rlelength, im->bands)) goto sgi_finish_decode; } diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 7e0c14339..ab476939a 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -132,7 +132,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) } else if (strcmp(mode, "BGR;15") == 0) { /* EXPERIMENTAL */ - /* 15-bit true colour */ + /* 15-bit reversed true colour */ im->bands = 1; im->pixelsize = 2; im->linesize = (xsize*2 + 3) & -4; @@ -405,7 +405,7 @@ ImagingAllocateArray(Imaging im, int dirty, int block_size) // printf("NEW size: %dx%d, ls: %d, lpb: %d, blocks: %d\n", // im->xsize, im->ysize, aligned_linesize, lines_per_block, blocks_count); - /* One extra ponter is always NULL */ + /* One extra pointer is always NULL */ im->blocks = calloc(sizeof(*im->blocks), blocks_count + 1); if ( ! im->blocks) { return (Imaging) ImagingError_MemoryError(); diff --git a/src/libImaging/SunRleDecode.c b/src/libImaging/SunRleDecode.c index 50d816e38..e627c2c9a 100644 --- a/src/libImaging/SunRleDecode.c +++ b/src/libImaging/SunRleDecode.c @@ -20,7 +20,7 @@ int -ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) +ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { int n; UINT8* ptr; diff --git a/src/libImaging/TgaRleDecode.c b/src/libImaging/TgaRleDecode.c index cad6bc3bc..d1971e546 100644 --- a/src/libImaging/TgaRleDecode.c +++ b/src/libImaging/TgaRleDecode.c @@ -20,7 +20,7 @@ int ImagingTgaRleDecode(Imaging im, ImagingCodecState state, - UINT8* buf, int bytes) + UINT8* buf, Py_ssize_t bytes) { int n, depth; UINT8* ptr; diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 64ac86e6a..7592f7f39 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -124,6 +124,7 @@ toff_t _tiffSizeProc(thandle_t hdata) { return (toff_t)state->size; } + int _tiffMapProc(thandle_t hdata, tdata_t* pbase, toff_t* psize) { TIFFSTATE *state = (TIFFSTATE *)hdata; @@ -146,7 +147,7 @@ void _tiffUnmapProc(thandle_t hdata, tdata_t base, toff_t size) { (void) hdata; (void) base; (void) size; } -int ImagingLibTiffInit(ImagingCodecState state, int fp, int offset) { +int ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset) { TIFFSTATE *clientstate = (TIFFSTATE *)state->context; TRACE(("initing libtiff\n")); @@ -168,13 +169,120 @@ int ImagingLibTiffInit(ImagingCodecState state, int fp, int offset) { return 1; } -int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes) { + +int ReadTile(TIFF* tiff, UINT32 col, UINT32 row, UINT32* buffer) { + uint16 photometric; + + TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); + + // To avoid dealing with YCbCr subsampling, let libtiff handle it + if (photometric == PHOTOMETRIC_YCBCR) { + UINT32 tile_width, tile_height, swap_line_size, i_row; + UINT32* swap_line; + + TIFFGetField(tiff, TIFFTAG_TILEWIDTH, &tile_width); + TIFFGetField(tiff, TIFFTAG_TILELENGTH, &tile_height); + + swap_line_size = tile_width * sizeof(UINT32); + if (tile_width != swap_line_size / sizeof(UINT32)) { + return -1; + } + + /* Read the tile into an RGBA array */ + if (!TIFFReadRGBATile(tiff, col, row, buffer)) { + return -1; + } + + swap_line = (UINT32*)malloc(swap_line_size); + if (swap_line == NULL) { + return -1; + } + /* + * For some reason the TIFFReadRGBATile() function chooses the + * lower left corner as the origin. Vertically mirror scanlines. + */ + for(i_row = 0; i_row < tile_height / 2; i_row++) { + UINT32 *top_line, *bottom_line; + + top_line = buffer + tile_width * i_row; + bottom_line = buffer + tile_width * (tile_height - i_row - 1); + + memcpy(swap_line, top_line, 4*tile_width); + memcpy(top_line, bottom_line, 4*tile_width); + memcpy(bottom_line, swap_line, 4*tile_width); + } + + free(swap_line); + + return 0; + } + + if (TIFFReadTile(tiff, (tdata_t)buffer, col, row, 0, 0) == -1) { + TRACE(("Decode Error, Tile at %dx%d\n", col, row)); + return -1; + } + + TRACE(("Successfully read tile at %dx%d; \n\n", col, row)); + + return 0; +} + +int ReadStrip(TIFF* tiff, UINT32 row, UINT32* buffer) { + uint16 photometric; + TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); + + // To avoid dealing with YCbCr subsampling, let libtiff handle it + if (photometric == PHOTOMETRIC_YCBCR) { + TIFFRGBAImage img; + char emsg[1024] = ""; + UINT32 rows_per_strip, rows_to_read; + int ok; + + + TIFFGetFieldDefaulted(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); + if ((row % rows_per_strip) != 0) { + TRACE(("Row passed to ReadStrip() must be first in a strip.")); + return -1; + } + + if (TIFFRGBAImageOK(tiff, emsg) && TIFFRGBAImageBegin(&img, tiff, 0, emsg)) { + TRACE(("Initialized RGBAImage\n")); + + img.req_orientation = ORIENTATION_TOPLEFT; + img.row_offset = row; + img.col_offset = 0; + + rows_to_read = min(rows_per_strip, img.height - row); + + TRACE(("rows to read: %d\n", rows_to_read)); + ok = TIFFRGBAImageGet(&img, buffer, img.width, rows_to_read); + + TIFFRGBAImageEnd(&img); + } else { + ok = 0; + } + + if (ok == 0) { + TRACE(("Decode Error, row %d; msg: %s\n", row, emsg)); + return -1; + } + + return 0; + } + + if (TIFFReadEncodedStrip(tiff, TIFFComputeStrip(tiff, row, 0), (tdata_t)buffer, -1) == -1) { + TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, row, 0))); + return -1; + } + + return 0; +} + +int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ssize_t bytes) { TIFFSTATE *clientstate = (TIFFSTATE *)state->context; char *filename = "tempfile.tif"; char *mode = "r"; TIFF *tiff; - uint16 photometric = 0, compression; - /* buffer is the encoded file, bytes is the length of the encoded file */ /* it all ends up in state->buffer, which is a uint8* from Imaging.h */ @@ -235,19 +343,17 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int } } - TIFFGetFieldDefaulted(tiff, TIFFTAG_COMPRESSION, &compression); - TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); - if (compression == COMPRESSION_JPEG && photometric == PHOTOMETRIC_YCBCR) { - /* Set pseudo-tag to force automatic YCbCr->RGB conversion */ - TIFFSetField(tiff, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); - } - if (TIFFIsTiled(tiff)) { - uint32 x, y, tile_y; - uint32 tileWidth, tileLength; + UINT32 x, y, tile_y, row_byte_size; + UINT32 tile_width, tile_length, current_tile_width; UINT8 *new_data; - state->bytes = TIFFTileSize(tiff); + TIFFGetField(tiff, TIFFTAG_TILEWIDTH, &tile_width); + TIFFGetField(tiff, TIFFTAG_TILELENGTH, &tile_length); + + // We could use TIFFTileSize, but for YCbCr data it returns subsampled data size + row_byte_size = (tile_width * state->bits + 7) / 8; + state->bytes = row_byte_size * tile_length; /* overflow check for malloc */ if (state->bytes > INT_MAX - 1) { @@ -268,12 +374,9 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int TRACE(("TIFFTileSize: %d\n", state->bytes)); - TIFFGetField(tiff, TIFFTAG_TILEWIDTH, &tileWidth); - TIFFGetField(tiff, TIFFTAG_TILELENGTH, &tileLength); - - for (y = state->yoff; y < state->ysize; y += tileLength) { - for (x = state->xoff; x < state->xsize; x += tileWidth) { - if (TIFFReadTile(tiff, (tdata_t)state->buffer, x, y, 0, 0) == -1) { + for (y = state->yoff; y < state->ysize; y += tile_length) { + for (x = state->xoff; x < state->xsize; x += tile_width) { + if (ReadTile(tiff, x, y, (UINT32*) state->buffer) == -1) { TRACE(("Decode Error, Tile at %dx%d\n", x, y)); state->errcode = IMAGING_CODEC_BROKEN; TIFFClose(tiff); @@ -282,53 +385,72 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int TRACE(("Read tile at %dx%d; \n\n", x, y)); + current_tile_width = min(tile_width, state->xsize - x); + // iterate over each line in the tile and stuff data into image - for (tile_y = 0; tile_y < min(tileLength, state->ysize - y); tile_y++) { + for (tile_y = 0; tile_y < min(tile_length, state->ysize - y); tile_y++) { + TRACE(("Writing tile data at %dx%d using tile_width: %d; \n", tile_y + y, x, current_tile_width)); - TRACE(("Writing tile data at %dx%d using tilwWidth: %d; \n", tile_y + y, x, min(tileWidth, state->xsize - x))); - - // UINT8 * bbb = state->buffer + tile_y * (state->bytes / tileLength); + // UINT8 * bbb = state->buffer + tile_y * row_byte_size; // TRACE(("chars: %x%x%x%x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); state->shuffle((UINT8*) im->image[tile_y + y] + x * im->pixelsize, - state->buffer + tile_y * (state->bytes / tileLength), - min(tileWidth, state->xsize - x) + state->buffer + tile_y * row_byte_size, + current_tile_width ); } } } } else { - tsize_t size; + UINT32 strip_row, row_byte_size; + UINT8 *new_data; + UINT32 rows_per_strip; + int ret; - size = TIFFScanlineSize(tiff); - TRACE(("ScanlineSize: %lu \n", size)); - if (size > state->bytes) { - TRACE(("Error, scanline size > buffer size\n")); - state->errcode = IMAGING_CODEC_BROKEN; + ret = TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); + if (ret != 1) { + rows_per_strip = state->ysize; + } + TRACE(("RowsPerStrip: %u \n", rows_per_strip)); + + // We could use TIFFStripSize, but for YCbCr data it returns subsampled data size + row_byte_size = (state->xsize * state->bits + 7) / 8; + state->bytes = rows_per_strip * row_byte_size; + + TRACE(("StripSize: %d \n", state->bytes)); + + /* realloc to fit whole strip */ + new_data = realloc (state->buffer, state->bytes); + if (!new_data) { + state->errcode = IMAGING_CODEC_MEMORY; TIFFClose(tiff); return -1; } - // Have to do this row by row and shove stuff into the buffer that way, - // with shuffle. (or, just alloc a buffer myself, then figure out how to get it - // back in. Can't use read encoded stripe. + state->buffer = new_data; - // This thing pretty much requires that I have the whole image in one shot. - // Perhaps a stub version would work better??? - while(state->y < state->ysize){ - if (TIFFReadScanline(tiff, (tdata_t)state->buffer, (uint32)state->y, 0) == -1) { - TRACE(("Decode Error, row %d\n", state->y)); + for (; state->y < state->ysize; state->y += rows_per_strip) { + if (ReadStrip(tiff, state->y, (UINT32 *)state->buffer) == -1) { + TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0))); state->errcode = IMAGING_CODEC_BROKEN; TIFFClose(tiff); return -1; } - /* TRACE(("Decoded row %d \n", state->y)); */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->buffer, - state->xsize); - state->y++; + TRACE(("Decoded strip for row %d \n", state->y)); + + // iterate over each row in the strip and stuff data into image + for (strip_row = 0; strip_row < min(rows_per_strip, state->ysize - state->y); strip_row++) { + TRACE(("Writing data into line %d ; \n", state->y + strip_row)); + + // UINT8 * bbb = state->buffer + strip_row * (state->bytes / rows_per_strip); + // TRACE(("chars: %x %x %x %x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); + + state->shuffle((UINT8*) im->image[state->y + state->yoff + strip_row] + + state->xoff * im->pixelsize, + state->buffer + strip_row * row_byte_size, + state->xsize); + } } } @@ -402,15 +524,33 @@ int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { } -int ImagingLibTiffMergeFieldInfo(ImagingCodecState state, TIFFDataType field_type, int key){ +int ImagingLibTiffMergeFieldInfo(ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length){ + // Refer to libtiff docs (http://www.simplesystems.org/libtiff/addingtags.html) TIFFSTATE *clientstate = (TIFFSTATE *)state->context; char field_name[10]; uint32 n; int status = 0; - const TIFFFieldInfo info[] = { - { key, 0, 1, field_type, FIELD_CUSTOM, 1, 0, field_name } + // custom fields added with ImagingLibTiffMergeFieldInfo are only used for + // decoding, ignore readcount; + int readcount = 0; + // we support writing a single value, or a variable number of values + int writecount = 1; + // whether the first value should encode the number of values. + int passcount = 0; + + TIFFFieldInfo info[] = { + { key, readcount, writecount, field_type, FIELD_CUSTOM, 1, passcount, field_name } }; + + if (is_var_length) { + info[0].field_writecount = -1; + } + + if (is_var_length && field_type != TIFF_ASCII) { + info[0].field_passcount = 1; + } + n = sizeof(info) / sizeof(info[0]); // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index e29a6c88f..08ef35cfd 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -43,9 +43,9 @@ typedef struct { -extern int ImagingLibTiffInit(ImagingCodecState state, int fp, int offset); +extern int ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset); extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); -extern int ImagingLibTiffMergeFieldInfo(ImagingCodecState state, TIFFDataType field_type, int key); +extern int ImagingLibTiffMergeFieldInfo(ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length); extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index e9921d2ca..ab0c8dc60 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -32,7 +32,6 @@ #include "Imaging.h" - #define R 0 #define G 1 #define B 2 @@ -327,11 +326,11 @@ static void unpackLA(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* LA, pixel interleaved */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[0], in[0], in[0], in[1]); - in += 2; + UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[1]); + memcpy(_out, &iv, sizeof(iv)); + in += 2; _out += 4; } } @@ -339,10 +338,10 @@ static void unpackLAL(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* LA, line interleaved */ - for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[i], in[i], in[i], in[i+pixels]); + for (i = 0; i < pixels; i++, _out+=4) { + UINT32 iv = MAKE_UINT32(in[i], in[i], in[i], in[i+pixels]); + memcpy(_out, &iv, sizeof(iv)); } } @@ -480,15 +479,18 @@ void ImagingUnpackRGB(UINT8* _out, const UINT8* in, int pixels) { int i = 0; - UINT32* out = (UINT32*) _out; /* RGB triplets */ for (; i < pixels-1; i++) { - out[i] = MASK_UINT32_CHANNEL_3 | *(UINT32*)&in[0]; - in += 3; + UINT32 iv; + memcpy(&iv, in, sizeof(iv)); + iv |= MASK_UINT32_CHANNEL_3; + memcpy(_out, &iv, sizeof(iv)); + in += 3; _out += 4; } for (; i < pixels; i++) { - out[i] = MAKE_UINT32(in[0], in[1], in[2], 255); - in += 3; + UINT32 iv = MAKE_UINT32(in[0], in[1], in[2], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 3; _out += 4; } } @@ -496,11 +498,11 @@ void unpackRGB16L(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* 16-bit RGB triplets, little-endian order */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[1], in[3], in[5], 255); - in += 6; + UINT32 iv = MAKE_UINT32(in[1], in[3], in[5], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 6; _out += 4; } } @@ -508,11 +510,11 @@ void unpackRGB16B(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* 16-bit RGB triplets, big-endian order */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[0], in[2], in[4], 255); - in += 6; + UINT32 iv = MAKE_UINT32(in[0], in[2], in[4], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 6; _out += 4; } } @@ -520,10 +522,10 @@ static void unpackRGBL(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGB, line interleaved */ - for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[i], in[i+pixels], in[i+pixels+pixels], 255); + for (i = 0; i < pixels; i++, _out+=4) { + UINT32 iv = MAKE_UINT32(in[i], in[i+pixels], in[i+pixels+pixels], 255); + memcpy(_out, &iv, sizeof(iv)); } } @@ -531,12 +533,12 @@ static void unpackRGBR(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGB, bit reversed */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(BITFLIP[in[0]], BITFLIP[in[1]], - BITFLIP[in[2]], 255); - in += 3; + UINT32 iv = MAKE_UINT32(BITFLIP[in[0]], BITFLIP[in[1]], + BITFLIP[in[2]], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 3; _out += 4; } } @@ -544,11 +546,11 @@ void ImagingUnpackBGR(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGB, reversed bytes */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[2], in[1], in[0], 255); - in += 3; + UINT32 iv = MAKE_UINT32(in[2], in[1], in[0], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 3; _out += 4; } } @@ -676,11 +678,11 @@ static void ImagingUnpackBGRX(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGB, reversed bytes with padding */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[2], in[1], in[0], 255); - in += 4; + UINT32 iv = MAKE_UINT32(in[2], in[1], in[0], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 4; _out += 4; } } @@ -688,11 +690,11 @@ static void ImagingUnpackXRGB(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGB, leading pad */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[1], in[2], in[3], 255); - in += 4; + UINT32 iv = MAKE_UINT32(in[1], in[2], in[3], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 4; _out += 4; } } @@ -700,11 +702,11 @@ static void ImagingUnpackXBGR(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGB, reversed bytes, leading pad */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[3], in[2], in[1], 255); - in += 4; + UINT32 iv = MAKE_UINT32(in[3], in[2], in[1], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 4; _out += 4; } } @@ -714,11 +716,11 @@ static void unpackRGBALA(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* greyscale with alpha */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[0], in[0], in[0], in[1]); - in += 2; + UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[1]); + memcpy(_out, &iv, sizeof(iv)); + in += 2; _out += 4; } } @@ -726,11 +728,11 @@ static void unpackRGBALA16B(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* 16-bit greyscale with alpha, big-endian */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[0], in[0], in[0], in[2]); - in += 4; + UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[2]); + memcpy(_out, &iv, sizeof(iv)); + in += 4; _out += 4; } } @@ -738,20 +740,21 @@ static void unpackRGBa16L(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* premultiplied 16-bit RGBA, little-endian */ for (i = 0; i < pixels; i++) { int a = in[7]; + UINT32 iv; if ( ! a) { - out[i] = 0; + iv = 0; } else if (a == 255) { - out[i] = MAKE_UINT32(in[1], in[3], in[5], a); + iv = MAKE_UINT32(in[1], in[3], in[5], a); } else { - out[i] = MAKE_UINT32(CLIP8(in[1] * 255 / a), - CLIP8(in[3] * 255 / a), - CLIP8(in[5] * 255 / a), a); + iv = MAKE_UINT32(CLIP8(in[1] * 255 / a), + CLIP8(in[3] * 255 / a), + CLIP8(in[5] * 255 / a), a); } - in += 8; + memcpy(_out, &iv, sizeof(iv)); + in += 8; _out += 4; } } @@ -759,20 +762,21 @@ static void unpackRGBa16B(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* premultiplied 16-bit RGBA, big-endian */ for (i = 0; i < pixels; i++) { int a = in[6]; + UINT32 iv; if ( ! a) { - out[i] = 0; + iv = 0; } else if (a == 255) { - out[i] = MAKE_UINT32(in[0], in[2], in[4], a); + iv = MAKE_UINT32(in[0], in[2], in[4], a); } else { - out[i] = MAKE_UINT32(CLIP8(in[0] * 255 / a), - CLIP8(in[2] * 255 / a), - CLIP8(in[4] * 255 / a), a); + iv = MAKE_UINT32(CLIP8(in[0] * 255 / a), + CLIP8(in[2] * 255 / a), + CLIP8(in[4] * 255 / a), a); } - in += 8; + memcpy(_out, &iv, sizeof(iv)); + in += 8; _out += 4; } } @@ -780,20 +784,21 @@ static void unpackRGBa(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* premultiplied RGBA */ for (i = 0; i < pixels; i++) { int a = in[3]; + UINT32 iv; if ( ! a) { - out[i] = 0; + iv = 0; } else if (a == 255) { - out[i] = MAKE_UINT32(in[0], in[1], in[2], a); + iv = MAKE_UINT32(in[0], in[1], in[2], a); } else { - out[i] = MAKE_UINT32(CLIP8(in[0] * 255 / a), - CLIP8(in[1] * 255 / a), - CLIP8(in[2] * 255 / a), a); + iv = MAKE_UINT32(CLIP8(in[0] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[2] * 255 / a), a); } - in += 4; + memcpy(_out, &iv, sizeof(iv)); + in += 4; _out += 4; } } @@ -843,20 +848,21 @@ static void unpackBGRa(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* premultiplied BGRA */ for (i = 0; i < pixels; i++) { int a = in[3]; + UINT32 iv; if ( ! a) { - out[i] = 0; + iv = 0; } else if (a == 255) { - out[i] = MAKE_UINT32(in[2], in[1], in[0], a); + iv = MAKE_UINT32(in[2], in[1], in[0], a); } else { - out[i] = MAKE_UINT32(CLIP8(in[2] * 255 / a), - CLIP8(in[1] * 255 / a), - CLIP8(in[0] * 255 / a), a); + iv = MAKE_UINT32(CLIP8(in[2] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[0] * 255 / a), a); } - in += 4; + memcpy(_out, &iv, sizeof(iv)); + in += 4; _out += 4; } } @@ -878,11 +884,11 @@ static void unpackRGBAL(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGBA, line interleaved */ - for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[i], in[i+pixels], in[i+pixels+pixels], - in[i+pixels+pixels+pixels]); + for (i = 0; i < pixels; i++, _out+=4) { + UINT32 iv = MAKE_UINT32(in[i], in[i+pixels], in[i+pixels+pixels], + in[i+pixels+pixels+pixels]); + memcpy(_out, &iv, sizeof(iv)); } } @@ -890,10 +896,10 @@ void unpackRGBA16L(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* 16-bit RGBA, little-endian order */ - for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[1], in[3], in[5], in[7]); + for (i = 0; i < pixels; i++, _out+=4) { + UINT32 iv = MAKE_UINT32(in[1], in[3], in[5], in[7]); + memcpy(_out, &iv, sizeof(iv)); in += 8; } } @@ -902,10 +908,10 @@ void unpackRGBA16B(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* 16-bit RGBA, big-endian order */ - for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[0], in[2], in[4], in[6]); + for (i = 0; i < pixels; i++, _out+=4) { + UINT32 iv = MAKE_UINT32(in[0], in[2], in[4], in[6]); + memcpy(_out, &iv, sizeof(iv)); in += 8; } } @@ -914,11 +920,11 @@ static void unpackARGB(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGBA, leading pad */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[1], in[2], in[3], in[0]); - in += 4; + UINT32 iv = MAKE_UINT32(in[1], in[2], in[3], in[0]); + memcpy(_out, &iv, sizeof(iv)); + in += 4; _out += 4; } } @@ -926,11 +932,11 @@ static void unpackABGR(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGBA, reversed bytes */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[3], in[2], in[1], in[0]); - in += 4; + UINT32 iv = MAKE_UINT32(in[3], in[2], in[1], in[0]); + memcpy(_out, &iv, sizeof(iv)); + in += 4; _out += 4; } } @@ -938,11 +944,11 @@ static void unpackBGRA(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGBA, reversed bytes */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[2], in[1], in[0], in[3]); - in += 4; + UINT32 iv = MAKE_UINT32(in[2], in[1], in[0], in[3]); + memcpy(_out, &iv, sizeof(iv)); + in += 4; _out += 4; } } @@ -953,11 +959,11 @@ static void unpackCMYKI(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* CMYK, inverted bytes (Photoshop 2.5) */ for (i = 0; i < pixels; i++) { - out[i] = ~MAKE_UINT32(in[0], in[1], in[2], in[3]); - in += 4; + UINT32 iv = ~MAKE_UINT32(in[0], in[1], in[2], in[3]); + memcpy(_out, &iv, sizeof(iv)); + in += 4; _out += 4; } } @@ -1031,30 +1037,30 @@ unpackI12_I16(UINT8* out, const UINT8* in, int pixels){ #ifdef WORDS_BIGENDIAN UINT8* tmp = (UINT8 *)&pixel; #endif - UINT16* out16 = (UINT16 *)out; for (i = 0; i < pixels-1; i+=2) { pixel = (((UINT16) in[0]) << 4 ) + (in[1] >>4); #ifdef WORDS_BIGENDIAN out[0] = tmp[1]; out[1] = tmp[0]; #else - out16[0] = pixel; + memcpy(out, &pixel, sizeof(pixel)); #endif + out+=2; pixel = (((UINT16) (in[1] & 0x0F)) << 8) + in[2]; #ifdef WORDS_BIGENDIAN - out[2] = tmp[1]; out[3] = tmp[0]; + out[0] = tmp[1]; out[1] = tmp[0]; #else - out16[1] = pixel; + memcpy(out, &pixel, sizeof(pixel)); #endif - in += 3; out16 += 2; out+=4; + in += 3; out+=2; } if (i == pixels-1) { pixel = (((UINT16) in[0]) << 4 ) + (in[1] >>4); #ifdef WORDS_BIGENDIAN out[0] = tmp[1]; out[1] = tmp[0]; #else - out16[0] = pixel; + memcpy(out, &pixel, sizeof(pixel)); #endif } } @@ -1085,10 +1091,9 @@ static void copy4skip1(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; for (i = 0; i < pixels; i++) { - out[i] = *(UINT32*)&in[0]; - in += 5; + memcpy(_out, in, 4); + in += 5; _out += 4; } } @@ -1096,10 +1101,9 @@ static void copy4skip2(UINT8* _out, const UINT8* in, int pixels) { int i; - UINT32* out = (UINT32*) _out; for (i = 0; i < pixels; i++) { - out[i] = *(UINT32*)&in[0]; - in += 6; + memcpy(_out, in, 4); + in += 6; _out += 4; } } @@ -1280,7 +1284,7 @@ static struct { {"1", "1;I", 1, unpack1I}, {"1", "1;R", 1, unpack1R}, {"1", "1;IR", 1, unpack1IR}, - {"1", "1;8", 1, unpack18}, + {"1", "1;8", 8, unpack18}, /* greyscale */ {"L", "L;2", 2, unpackL2}, @@ -1331,8 +1335,9 @@ static struct { {"RGB", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ {"RGB", "RGBX", 32, copy4}, {"RGB", "RGBX;L", 32, unpackRGBAL}, + {"RGB", "RGBA;L", 32, unpackRGBAL}, {"RGB", "BGRX", 32, ImagingUnpackBGRX}, - {"RGB", "XRGB", 24, ImagingUnpackXRGB}, + {"RGB", "XRGB", 32, ImagingUnpackXRGB}, {"RGB", "XBGR", 32, ImagingUnpackXBGR}, {"RGB", "YCC;P", 24, ImagingUnpackYCC}, {"RGB", "R", 8, band0}, @@ -1368,12 +1373,12 @@ static struct { {"RGBA", "A", 8, band3}, #ifdef WORDS_BIGENDIAN - {"RGB", "RGB;16N", 64, unpackRGB16B}, + {"RGB", "RGB;16N", 48, unpackRGB16B}, {"RGBA", "RGBa;16N", 64, unpackRGBa16B}, {"RGBA", "RGBA;16N", 64, unpackRGBA16B}, {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, #else - {"RGB", "RGB;16N", 64, unpackRGB16L}, + {"RGB", "RGB;16N", 48, unpackRGB16L}, {"RGBA", "RGBa;16N", 64, unpackRGBa16L}, {"RGBA", "RGBA;16N", 64, unpackRGBA16L}, {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, @@ -1402,7 +1407,7 @@ static struct { {"RGBX", "RGBX;16L", 64, unpackRGBA16L}, {"RGBX", "RGBX;16B", 64, unpackRGBA16B}, {"RGBX", "BGRX", 32, ImagingUnpackBGRX}, - {"RGBX", "XRGB", 24, ImagingUnpackXRGB}, + {"RGBX", "XRGB", 32, ImagingUnpackXRGB}, {"RGBX", "XBGR", 32, ImagingUnpackXBGR}, {"RGBX", "YCC;P", 24, ImagingUnpackYCC}, {"RGBX", "R", 8, band0}, @@ -1416,6 +1421,8 @@ static struct { {"CMYK", "CMYKXX", 48, copy4skip2}, {"CMYK", "CMYK;I", 32, unpackCMYKI}, {"CMYK", "CMYK;L", 32, unpackRGBAL}, + {"CMYK", "CMYK;16L", 64, unpackRGBA16L}, + {"CMYK", "CMYK;16B", 64, unpackRGBA16B}, {"CMYK", "C", 8, band0}, {"CMYK", "M", 8, band1}, {"CMYK", "Y", 8, band2}, @@ -1425,6 +1432,12 @@ static struct { {"CMYK", "Y;I", 8, band2I}, {"CMYK", "K;I", 8, band3I}, +#ifdef WORDS_BIGENDIAN + {"CMYK", "CMYK;16N", 64, unpackRGBA16B}, +#else + {"CMYK", "CMYK;16N", 64, unpackRGBA16L}, +#endif + /* video (YCbCr) */ {"YCbCr", "YCbCr", 24, ImagingUnpackRGB}, {"YCbCr", "YCbCr;L", 24, unpackRGBL}, diff --git a/src/libImaging/UnsharpMask.c b/src/libImaging/UnsharpMask.c index ec3bb23cc..a034bebf2 100644 --- a/src/libImaging/UnsharpMask.c +++ b/src/libImaging/UnsharpMask.c @@ -6,7 +6,6 @@ /* Originally released under LGPL. Graciously donated to PIL for distribution under the standard PIL license in 2009." */ -#include "Python.h" #include "Imaging.h" diff --git a/src/libImaging/XbmDecode.c b/src/libImaging/XbmDecode.c index 8a203841b..75b4961ab 100644 --- a/src/libImaging/XbmDecode.c +++ b/src/libImaging/XbmDecode.c @@ -21,7 +21,7 @@ (v >= 'A' && v <= 'F') ? v - 'A' + 10 : 0) int -ImagingXbmDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) +ImagingXbmDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { enum { BYTE = 1, SKIP }; diff --git a/src/libImaging/ZipDecode.c b/src/libImaging/ZipDecode.c index e96e3200c..43601c38e 100644 --- a/src/libImaging/ZipDecode.c +++ b/src/libImaging/ZipDecode.c @@ -41,7 +41,7 @@ static int get_row_len(ImagingCodecState state, int pass) /* -------------------------------------------------------------------- */ int -ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) +ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t bytes) { ZIPSTATE* context = (ZIPSTATE*) state->context; int err; 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 76b316012..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); @@ -339,7 +337,7 @@ PyImaging_MapBuffer(PyObject* self, PyObject* args) stride = xsize * 4; } - if (stride > 0 && ysize > INT_MAX / stride) { + if (stride > 0 && ysize > PY_SSIZE_T_MAX / stride) { PyErr_SetString(PyExc_MemoryError, "Integer overflow in ysize"); return NULL; } diff --git a/src/path.c b/src/path.c index eb1e065f9..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); @@ -133,8 +131,8 @@ PyPath_Flatten(PyObject* data, double **pxy) /* Assume the buffer contains floats */ Py_buffer buffer; if (PyImaging_GetBuffer(data, &buffer) == 0) { - int n = buffer.len / (2 * sizeof(float)); float *ptr = (float*) buffer.buf; + n = buffer.len / (2 * sizeof(float)); xy = alloc_array(n); if (!xy) return -1; @@ -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 fce4a4206..07d75be64 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ [tox] envlist = lint - py{27,35,36,37} + py{35,36,37,38,py3} minversion = 1.9 [testenv] @@ -24,9 +24,13 @@ deps = [testenv:lint] commands = + black --target-version py35 --check --diff . flake8 --statistics --count + isort --check-only --diff check-manifest deps = + black check-manifest flake8 + isort skip_install = true diff --git a/winbuild/appveyor_install_msys2_deps.sh b/winbuild/appveyor_install_msys2_deps.sh index f8cb8c641..4cc01082d 100644 --- a/winbuild/appveyor_install_msys2_deps.sh +++ b/winbuild/appveyor_install_msys2_deps.sh @@ -2,12 +2,13 @@ mkdir /var/cache/pacman/pkg pacman -S --noconfirm mingw32/mingw-w64-i686-python3-pip \ - mingw32/mingw-w64-i686-python3-setuptools \ - mingw32/mingw-w64-i686-python2-pip \ - mingw32/mingw-w64-i686-python2-setuptools \ - mingw-w64-i686-libjpeg-turbo + mingw32/mingw-w64-i686-python3-setuptools \ + mingw32/mingw-w64-i686-python3-pytest \ + mingw32/mingw-w64-i686-python3-pytest-cov \ + mingw-w64-i686-libjpeg-turbo \ + mingw-w64-i686-libimagequant C:/msys64/mingw32/bin/python3 -m pip install --upgrade pip -/mingw32/bin/pip install pytest pytest-cov olefile -/mingw32/bin/pip3 install pytest pytest-cov olefile +/mingw32/bin/pip install olefile +/mingw32/bin/pip3 install olefile diff --git a/winbuild/appveyor_install_pypy.cmd b/winbuild/appveyor_install_pypy.cmd deleted file mode 100644 index 3092fc617..000000000 --- a/winbuild/appveyor_install_pypy.cmd +++ /dev/null @@ -1,3 +0,0 @@ -curl -fsSL -o pypy2.zip https://bitbucket.org/pypy/pypy/downloads/pypy2.7-v7.0.0-win32.zip -7z x pypy2.zip -oc:\ -c:\Python37\Scripts\virtualenv.exe -p c:\pypy2.7-v7.0.0-win32\pypy.exe c:\vp\pypy2 diff --git a/winbuild/appveyor_install_pypy3.cmd b/winbuild/appveyor_install_pypy3.cmd new file mode 100644 index 000000000..3622ed6ec --- /dev/null +++ b/winbuild/appveyor_install_pypy3.cmd @@ -0,0 +1,3 @@ +curl -fsSL -o pypy3.zip https://bitbucket.org/pypy/pypy/downloads/pypy3.6-v7.2.0-win32.zip +7z x pypy3.zip -oc:\ +c:\Python37\Scripts\virtualenv.exe -p c:\pypy3.6-v7.2.0-win32\pypy3.exe c:\vp\pypy3 diff --git a/winbuild/build.py b/winbuild/build.py index 313a14ff4..e565226bd 100755 --- a/winbuild/build.py +++ b/winbuild/build.py @@ -1,23 +1,34 @@ #!/usr/bin/env python3 -import subprocess -import shutil -import sys import getopt import os +import shutil +import subprocess +import sys -from config import (compilers, compiler_from_env, pythons, pyversion_from_env, - bit_from_env, VIRT_BASE, X64_EXT) +from config import ( + VIRT_BASE, + X64_EXT, + bit_from_env, + compiler_from_env, + compilers, + pythons, + pyversion_from_env, +) def setup_vms(): ret = [] for py in pythons: - for arch in ('', X64_EXT): - ret.append("virtualenv -p c:/Python%s%s/python.exe --clear %s%s%s" - % (py, arch, VIRT_BASE, py, arch)) - ret.append(r"%s%s%s\Scripts\pip.exe install pytest pytest-cov" % - (VIRT_BASE, py, arch)) + for arch in ("", X64_EXT): + ret.append( + "virtualenv -p c:/Python%s%s/python.exe --clear %s%s%s" + % (py, arch, VIRT_BASE, py, arch) + ) + ret.append( + r"%s%s%s\Scripts\pip.exe install pytest pytest-cov" + % (VIRT_BASE, py, arch) + ) return "\n".join(ret) @@ -25,26 +36,27 @@ def run_script(params): (version, script) = params try: print("Running %s" % version) - filename = 'build_pillow_%s.cmd' % version - with open(filename, 'w') as f: + filename = "build_pillow_%s.cmd" % version + with open(filename, "w") as f: f.write(script) - command = ['powershell', "./%s" % filename] - proc = subprocess.Popen(command, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) + command = ["powershell", "./%s" % filename] + proc = subprocess.Popen( + command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) (trace, stderr) = proc.communicate() status = proc.returncode print("-- stderr --") - print(stderr) + print(stderr.decode()) print("-- stdout --") - print(trace) - print("Done with %s: %s" % (version, status)) + print(trace.decode()) + 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)) @@ -55,7 +67,9 @@ set MPLSRC=%%~dp0\.. set INCLIB=%%~dp0\depends set BLDOPT=%s cd /D %%MPLSRC%% -""" % (op) +""" % ( + op + ) def footer(): @@ -66,10 +80,14 @@ exit def vc_setup(compiler, bit): script = "" - if compiler['vc_version'] == '2015': + if compiler["vc_version"] == "2015": arch = "x86" if bit == 32 else "x86_amd64" - script = r""" -call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %s""" % arch + script = ( + r""" +call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %s +echo on""" + % arch + ) return script @@ -77,27 +95,24 @@ def build_one(py_ver, compiler, bit): # UNDONE virtual envs if we're not running on AppVeyor args = {} args.update(compiler) - if 'PYTHON' in os.environ: - args['python_path'] = "%PYTHON%" + if "PYTHON" in os.environ: + args["python_path"] = "%PYTHON%" else: - args['python_path'] = "%s%s\\Scripts" % (VIRT_BASE, py_ver) + args["python_path"] = "{}{}\\Scripts".format(VIRT_BASE, py_ver) - args['executable'] = "python.exe" - if 'EXECUTABLE' in os.environ: - args['executable'] = "%EXECUTABLE%" + args["executable"] = "python.exe" + if "EXECUTABLE" in os.environ: + args["executable"] = "%EXECUTABLE%" - args['py_ver'] = py_ver - if '27' in py_ver: - args['tcl_ver'] = '85' + args["py_ver"] = py_ver + args["tcl_ver"] = "86" + + if compiler["vc_version"] == "2015": + args["imaging_libs"] = " build_ext --add-imaging-libs=msvcrt" else: - args['tcl_ver'] = '86' + args["imaging_libs"] = "" - if compiler['vc_version'] == '2015': - args['imaging_libs'] = ' build_ext --add-imaging-libs=msvcrt' - else: - args['imaging_libs'] = '' - - args['vc_setup'] = vc_setup(compiler, bit) + args["vc_setup"] = vc_setup(compiler, bit) script = r""" setlocal EnableDelayedExpansion @@ -109,6 +124,7 @@ set INCLUDE=%%INCLUDE%%;%%INCLIB%%\%(inc_dir)s;%%INCLIB%%\tcl%(tcl_ver)s\include setlocal set LIB=%%LIB%%;C:\Python%(py_ver)s\tcl%(vc_setup)s call %(python_path)s\%(executable)s setup.py %(imaging_libs)s %%BLDOPT%% +call %(python_path)s\%(executable)s -c "from PIL import _webp;import os, shutil;shutil.copy(r'%%INCLIB%%\freetype.dll', os.path.dirname(_webp.__file__));" endlocal endlocal @@ -118,34 +134,44 @@ endlocal def clean(): try: - shutil.rmtree('../build') + shutil.rmtree("../build") except Exception: # could already be removed pass - run_script(('virtualenvs', setup_vms())) + run_script(("virtualenvs", setup_vms())) def main(op): scripts = [] for py_version, py_info in pythons.items(): - py_compilers = compilers[py_info['compiler']][py_info['vc']] - scripts.append((py_version, - "\n".join([header(op), - build_one(py_version, - py_compilers[32], 32), - footer()]))) + py_compilers = compilers[py_info["compiler"]][py_info["vc"]] + scripts.append( + ( + py_version, + "\n".join( + [header(op), build_one(py_version, py_compilers[32], 32), footer()] + ), + ) + ) - scripts.append(("%s%s" % (py_version, X64_EXT), - "\n".join([header(op), - build_one("%sx64" % py_version, - py_compilers[64], 64), - footer()]))) + scripts.append( + ( + "{}{}".format(py_version, X64_EXT), + "\n".join( + [ + header(op), + build_one("%sx64" % py_version, py_compilers[64], 64), + footer(), + ] + ), + ) + ) results = map(run_script, scripts) for (version, status, trace, err) in results: - print("Compiled %s: %s" % (version, status and 'ERR' or 'OK')) + print("Compiled {}: {}".format(version, status and "ERR" or "OK")) def run_one(op): @@ -154,27 +180,26 @@ def run_one(op): py_version = pyversion_from_env() bit = bit_from_env() - run_script((py_version, - "\n".join([header(op), - build_one(py_version, compiler, bit), - footer()]) - )) + run_script( + ( + py_version, + "\n".join([header(op), build_one(py_version, compiler, bit), footer()]), + ) + ) -if __name__ == '__main__': - opts, args = getopt.getopt(sys.argv[1:], '', ['clean', 'dist', 'wheel']) +if __name__ == "__main__": + opts, args = getopt.getopt(sys.argv[1:], "", ["clean", "wheel"]) opts = dict(opts) - if '--clean' in opts: + if "--clean" in opts: clean() - op = 'install' - if '--dist' in opts: - op = "bdist_wininst --user-access-control=auto" - elif '--wheel' in opts: + op = "install" + if "--wheel" in opts: op = "bdist_wheel" - if 'PYTHON' in os.environ: + if "PYTHON" in os.environ: run_one(op) else: main(op) diff --git a/winbuild/build.rst b/winbuild/build.rst index a56f43d1a..1d2084044 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -37,8 +37,8 @@ virtualenv as well, reducing the number of packages that we need to install.) Download the rest of the Pythons by opening a command window, changing -to the `winbuild` directory, and running `python -get_pythons.py`. +to the ``winbuild`` directory, and running ``python +get_pythons.py``. UNDONE -- gpg verify the signatures (note that we can download from https) @@ -65,8 +65,8 @@ Dependencies ------------ The script 'build_dep.py' downloads and builds the dependencies. Open -a command window, change directory into `winbuild` and run `python -build_dep.py`. +a command window, change directory into ``winbuild`` and run ``python +build_dep.py``. This will download libjpeg, libtiff, libz, and freetype. It will then compile 32 and 64-bit versions of the libraries, with both versions of @@ -78,9 +78,9 @@ UNDONE -- webp, jpeg2k not recognized Building Pillow --------------- -Once the dependencies are built, run `python build.py --clean` to -build and install Pillow in virtualenvs for each python -build. `build.py --dist` will build Windows installers instead of +Once the dependencies are built, run ``python build.py --clean`` to +build and install Pillow in virtualenvs for each Python +build. ``build.py --wheel`` will build wheels instead of installing into virtualenvs. UNDONE -- suppressed output, what about failures. @@ -88,6 +88,6 @@ UNDONE -- suppressed output, what about failures. Testing Pillow -------------- -Build and install Pillow, then run `python test.py` from the -`winbuild` directory. +Build and install Pillow, then run ``python test.py`` from the +``winbuild`` directory. diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index 13296dc5b..5762de5e5 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -1,19 +1,18 @@ -from unzip import unzip -from untar import untar import os -from fetch import fetch -from config import (compilers, all_compilers, compiler_from_env, bit_from_env, - libs) from build import vc_setup +from config import all_compilers, bit_from_env, compiler_from_env, compilers, libs +from fetch import fetch +from untar import untar +from unzip import unzip def _relpath(*args): return os.path.join(os.getcwd(), *args) -build_dir = _relpath('build') -inc_dir = _relpath('depends') +build_dir = _relpath("build") +inc_dir = _relpath("depends") def check_sig(filename, signame): @@ -32,52 +31,55 @@ def mkdirs(): pass for compiler in all_compilers(): try: - os.mkdir(os.path.join(inc_dir, compiler['inc_dir'])) + os.mkdir(os.path.join(inc_dir, compiler["inc_dir"])) except OSError: pass def extract(src, dest): - if '.zip' in src: + if ".zip" in src: return unzip(src, dest) - if '.tar.gz' in src or '.tgz' in src: + if ".tar.gz" in src or ".tgz" in src: return untar(src, dest) def extract_libs(): for name, lib in libs.items(): - filename = lib['filename'] - if not os.path.exists(filename): - filename = fetch(lib['url']) - if name == 'openjpeg': + filename = fetch(lib["url"]) + if name == "openjpeg": for compiler in all_compilers(): - if not os.path.exists(os.path.join( - build_dir, lib['dir']+compiler['inc_dir'])): + if not os.path.exists( + os.path.join(build_dir, lib["dir"] + compiler["inc_dir"]) + ): extract(filename, build_dir) - os.rename(os.path.join(build_dir, lib['dir']), - os.path.join( - build_dir, lib['dir']+compiler['inc_dir'])) + os.rename( + os.path.join(build_dir, lib["dir"]), + os.path.join(build_dir, lib["dir"] + compiler["inc_dir"]), + ) else: extract(filename, build_dir) def extract_openjpeg(compiler): - return r""" + return ( + r""" rem build openjpeg setlocal -@echo on cd %%BUILD%% mkdir %%INCLIB%%\openjpeg-2.0 copy /Y /B openjpeg-2.0.0-win32-x86\include\openjpeg-2.0 %%INCLIB%%\openjpeg-2.0 copy /Y /B openjpeg-2.0.0-win32-x86\bin\ %%INCLIB%% copy /Y /B openjpeg-2.0.0-win32-x86\lib\ %%INCLIB%% endlocal -""" % compiler +""" + % compiler + ) def cp_tk(ver_85, ver_86): - versions = {'ver_85': ver_85, 'ver_86': ver_86} - return r""" + versions = {"ver_85": ver_85, "ver_86": ver_86} + return ( + r""" mkdir %%INCLIB%%\tcl85\include\X11 copy /Y /B %%BUILD%%\tcl%(ver_85)s\generic\*.h %%INCLIB%%\tcl85\include\ copy /Y /B %%BUILD%%\tk%(ver_85)s\generic\*.h %%INCLIB%%\tcl85\include\ @@ -87,7 +89,9 @@ mkdir %%INCLIB%%\tcl86\include\X11 copy /Y /B %%BUILD%%\tcl%(ver_86)s\generic\*.h %%INCLIB%%\tcl86\include\ copy /Y /B %%BUILD%%\tk%(ver_86)s\generic\*.h %%INCLIB%%\tcl86\include\ copy /Y /B %%BUILD%%\tk%(ver_86)s\xlib\X11\* %%INCLIB%%\tcl86\include\X11\ -""" % versions +""" + % versions + ) def header(): @@ -96,15 +100,22 @@ set MSBUILD=C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe set CMAKE="cmake.exe" set INCLIB=%~dp0\depends set BUILD=%~dp0\build -""" + "\n".join(r'set %s=%%BUILD%%\%s' % (k.upper(), v['dir']) - for (k, v) in libs.items() if v['dir']) +""" + "\n".join( + r"set {}=%BUILD%\{}".format(k.upper(), v["dir"]) + for (k, v) in libs.items() + if v["dir"] + ) def setup_compiler(compiler): - return r"""setlocal EnableDelayedExpansion + return ( + r"""setlocal EnableDelayedExpansion call "%%ProgramFiles%%\Microsoft SDKs\Windows\%(env_version)s\Bin\SetEnv.Cmd" /Release %(env_flags)s +echo on set INCLIB=%%INCLIB%%\%(inc_dir)s -""" % compiler # noqa: E501 +""" # noqa: E501 + % compiler + ) def end_compiler(): @@ -113,35 +124,46 @@ endlocal """ -def nmake_openjpeg(compiler): - atts = {'op_ver': '2.1'} +def nmake_openjpeg(compiler, bit): + if compiler["env_version"] == "v7.0": + return "" + + atts = {"op_ver": "2.3.1"} atts.update(compiler) - return r""" + return ( + r""" rem build openjpeg setlocal -@echo on +""" + + vc_setup(compiler, bit) + + r""" cd /D %%OPENJPEG%%%(inc_dir)s -%%CMAKE%% -DBUILD_THIRDPARTY:BOOL=OFF -G "NMake Makefiles" . -nmake -f Makefile clean -nmake -f Makefile +%%CMAKE%% -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF -DCMAKE_BUILD_TYPE=Release -G "NMake Makefiles" . +nmake -nologo -f Makefile clean +nmake -nologo -f Makefile copy /Y /B bin\* %%INCLIB%% mkdir %%INCLIB%%\openjpeg-%(op_ver)s copy /Y /B src\lib\openjp2\*.h %%INCLIB%%\openjpeg-%(op_ver)s endlocal -""" % atts +""" # noqa: E501 + % atts + ) def nmake_libs(compiler, bit): # undone -- pre, makes, headers, libs - script = r""" + script = ( + r""" rem Build libjpeg setlocal -""" + vc_setup(compiler, bit) + r""" +""" + + vc_setup(compiler, bit) + + r""" cd /D %%JPEG%% -nmake -f makefile.vc setup-vc6 -nmake -f makefile.vc clean -nmake -f makefile.vc libjpeg.lib +nmake -nologo -f makefile.vc setup-vc6 +nmake -nologo -f makefile.vc clean +nmake -nologo -f makefile.vc nodebug=1 libjpeg.lib copy /Y /B *.dll %%INCLIB%% copy /Y /B *.lib %%INCLIB%% copy /Y /B j*.h %%INCLIB%% @@ -150,8 +172,8 @@ endlocal rem Build zlib setlocal cd /D %%ZLIB%% -nmake -f win32\Makefile.msc clean -nmake -f win32\Makefile.msc zlib.lib +nmake -nologo -f win32\Makefile.msc clean +nmake -nologo -f win32\Makefile.msc zlib.lib copy /Y /B *.dll %%INCLIB%% copy /Y /B *.lib %%INCLIB%% copy /Y /B zlib.lib %%INCLIB%%\z.lib @@ -161,10 +183,12 @@ endlocal rem Build webp setlocal -""" + vc_setup(compiler, bit) + r""" +""" + + vc_setup(compiler, bit) + + r""" cd /D %%WEBP%% rd /S /Q %%WEBP%%\output\release-static -nmake -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output all +nmake -nologo -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output all copy /Y /B output\release-static\%(webp_platform)s\lib\* %%INCLIB%% mkdir %%INCLIB%%\webp copy /Y /B src\webp\*.h %%INCLIB%%\\webp @@ -172,115 +196,108 @@ endlocal rem Build libtiff setlocal -""" + vc_setup(compiler, bit) + r""" +""" + + vc_setup(compiler, bit) + + r""" rem do after building jpeg and zlib -copy %%~dp0\nmake.opt %%TIFF%% +copy %%~dp0\tiff.opt %%TIFF%%\nmake.opt cd /D %%TIFF%% -nmake -f makefile.vc clean -nmake -f makefile.vc lib +nmake -nologo -f makefile.vc clean +nmake -nologo -f makefile.vc lib copy /Y /B libtiff\*.dll %%INCLIB%% copy /Y /B libtiff\*.lib %%INCLIB%% copy /Y /B libtiff\tiff*.h %%INCLIB%% endlocal """ + ) return script % compiler -def msbuild_freetype(compiler): - if compiler['env_version'] == 'v7.1': - return msbuild_freetype_71(compiler) - return msbuild_freetype_70(compiler) - - -def msbuild_freetype_71(compiler): - return r""" +def msbuild_freetype(compiler, bit): + script = r""" rem Build freetype setlocal rd /S /Q %%FREETYPE%%\objs -%%MSBUILD%% %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.sln /t:Clean;Build /p:Configuration="Release" /p:Platform=%(platform)s /m +set DefaultPlatformToolset=v100 +""" + properties = r"""/p:Configuration="Release" /p:Platform=%(platform)s""" + if bit == 64: + script += ( + r"copy /Y /B " + + r'"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Lib\x64\*.Lib" ' + + r"%%FREETYPE%%\builds\windows\vc2010" + ) + properties += r" /p:_IsNativeEnvironment=false" + script += ( + r""" +%%MSBUILD%% %%FREETYPE%%\builds\windows\vc2010\freetype.sln /t:Clean;Build """ + + properties + + r""" /m xcopy /Y /E /Q %%FREETYPE%%\include %%INCLIB%% -copy /Y /B %%FREETYPE%%\objs\vc%(vc_version)s\%(platform)s\*.lib %%INCLIB%%\freetype.lib +""" + ) + freetypeReleaseDir = r"%%FREETYPE%%\objs\%(platform)s\Release" + script += ( + r""" +copy /Y /B """ + + freetypeReleaseDir + + r"""\freetype.lib %%INCLIB%%\freetype.lib +copy /Y /B """ + + freetypeReleaseDir + + r"""\freetype.dll %%INCLIB%%\..\freetype.dll endlocal -""" % compiler # noqa: E501 - - -def msbuild_freetype_70(compiler): - return r""" -rem Build freetype -setlocal -py -3 %%~dp0\fixproj.py %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.sln %(platform)s -py -3 %%~dp0\fixproj.py %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.vcproj %(platform)s -rd /S /Q %%FREETYPE%%\objs -%%MSBUILD%% %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.sln /t:Clean;Build /p:Configuration="LIB Release";Platform=%(platform)s /m -xcopy /Y /E /Q %%FREETYPE%%\include %%INCLIB%% -xcopy /Y /E /Q %%FREETYPE%%\objs\win32\vc%(vc_version)s %%INCLIB%% -copy /Y /B %%FREETYPE%%\objs\win32\vc%(vc_version)s\*.lib %%INCLIB%%\freetype.lib -endlocal -""" % compiler # noqa: E501 +""" + ) + return script % compiler def build_lcms2(compiler): - if compiler['env_version'] == 'v7.1': + if compiler["env_version"] == "v7.1": return build_lcms_71(compiler) return build_lcms_70(compiler) def build_lcms_70(compiler): """Link error here on x64""" - if compiler['platform'] == 'x64': - return '' + if compiler["platform"] == "x64": + return "" """Build LCMS on VC2008. This version is only 32bit/Win32""" - return r""" + return ( + r""" rem Build lcms2 setlocal +set LCMS=%%LCMS-2.7%% rd /S /Q %%LCMS%%\Lib rd /S /Q %%LCMS%%\Projects\VC%(vc_version)s\Release %%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:Clean /p:Configuration="Release" /p:Platform=Win32 /m -%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:lcms2_static /p:Configuration="Release" /p:Platform=Win32 /m +%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:lcms2_static /p:Configuration="Release" /p:Platform=Win32 /p:PlatformToolset=v90 /m xcopy /Y /E /Q %%LCMS%%\include %%INCLIB%% -copy /Y /B %%LCMS%%\Projects\VC%(vc_version)s\Release\*.lib %%INCLIB%% +copy /Y /B %%LCMS%%\Lib\MS\*.lib %%INCLIB%% endlocal -""" % compiler # noqa: E501 +""" # noqa: E501 + % compiler + ) def build_lcms_71(compiler): - return r""" + return ( + r""" rem Build lcms2 setlocal +set LCMS=%%LCMS-2.8%% rd /S /Q %%LCMS%%\Lib rd /S /Q %%LCMS%%\Projects\VC%(vc_version)s\Release +powershell -Command "(gc Projects\VC2015\lcms2_static\lcms2_static.vcxproj) -replace 'MultiThreadedDLL', 'MultiThreaded' | Out-File -encoding ASCII Projects\VC2015\lcms2_static\lcms2_static.vcxproj" %%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:Clean /p:Configuration="Release" /p:Platform=%(platform)s /m %%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:lcms2_static /p:Configuration="Release" /p:Platform=%(platform)s /m xcopy /Y /E /Q %%LCMS%%\include %%INCLIB%% copy /Y /B %%LCMS%%\Lib\MS\*.lib %%INCLIB%% endlocal -""" % compiler # noqa: E501 - - -def build_ghostscript(compiler, bit): - script = r""" -rem Build gs -setlocal -""" + vc_setup(compiler, bit) + r""" -set MSVC_VERSION=""" + { - "2008": "9", - "2015": "14" - }[compiler['vc_version']] + r""" -set RCOMP="C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\RC.Exe" -cd /D %%GHOSTSCRIPT%% -""" - if bit == 64: - script += r""" -set WIN64="" -""" - script += r""" -nmake -f psi/msvc.mak -copy /Y /B bin\ C:\Python27\ -endlocal -""" - return script % compiler # noqa: E501 +""" # noqa: E501 + % compiler + ) def add_compiler(compiler, bit): @@ -289,26 +306,23 @@ def add_compiler(compiler, bit): # script.append(extract_openjpeg(compiler)) - script.append(msbuild_freetype(compiler)) + script.append(msbuild_freetype(compiler, bit)) script.append(build_lcms2(compiler)) - # script.append(nmake_openjpeg(compiler)) - script.append(build_ghostscript(compiler, bit)) + script.append(nmake_openjpeg(compiler, bit)) script.append(end_compiler()) mkdirs() extract_libs() -script = [header(), - cp_tk(libs['tk-8.5']['version'], - libs['tk-8.6']['version'])] +script = [header(), cp_tk(libs["tk-8.5"]["version"], libs["tk-8.6"]["version"])] -if 'PYTHON' in os.environ: +if "PYTHON" in os.environ: add_compiler(compiler_from_env(), bit_from_env()) else: # for compiler in all_compilers(): # add_compiler(compiler) - add_compiler(compilers[7.0][2008][32], 32) + add_compiler(compilers[7.0][2010][32], 32) -with open('build_deps.cmd', 'w') as f: +with open("build_deps.cmd", "w") as f: f.write("\n".join(script)) diff --git a/winbuild/config.py b/winbuild/config.py index da6f7855b..b68fb6936 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -1,147 +1,180 @@ import os -SF_MIRROR = 'http://iweb.dl.sourceforge.net' -PILLOW_DEPENDS_DIR = 'C:\\pillow-depends\\' +SF_MIRROR = "https://iweb.dl.sourceforge.net" -pythons = {'27': {'compiler': 7, 'vc': 2008}, - 'pypy2': {'compiler': 7, 'vc': 2008}, - '35': {'compiler': 7.1, 'vc': 2015}, - '36': {'compiler': 7.1, 'vc': 2015}, - '37': {'compiler': 7.1, 'vc': 2015}} +pythons = { + "pypy3": {"compiler": 7.1, "vc": 2015}, + # for AppVeyor + "35": {"compiler": 7.1, "vc": 2015}, + "36": {"compiler": 7.1, "vc": 2015}, + "37": {"compiler": 7.1, "vc": 2015}, + "38": {"compiler": 7.1, "vc": 2015}, + # for GitHub Actions + "3.5": {"compiler": 7.1, "vc": 2015}, + "3.6": {"compiler": 7.1, "vc": 2015}, + "3.7": {"compiler": 7.1, "vc": 2015}, + "3.8": {"compiler": 7.1, "vc": 2015}, +} VIRT_BASE = "c:/vp/" -X64_EXT = os.environ.get('X64_EXT', "x64") +X64_EXT = os.environ.get("X64_EXT", "x64") libs = { # 'openjpeg': { # 'filename': 'openjpeg-2.0.0-win32-x86.zip', # 'version': '2.0' # }, - 'zlib': { - 'url': 'http://zlib.net/zlib1211.zip', - 'filename': PILLOW_DEPENDS_DIR + 'zlib1211.zip', - 'dir': 'zlib-1.2.11', + "zlib": { + "url": "http://zlib.net/zlib1211.zip", + "filename": "zlib1211.zip", + "dir": "zlib-1.2.11", }, - 'jpeg': { - 'url': 'http://www.ijg.org/files/jpegsr9c.zip', - 'filename': PILLOW_DEPENDS_DIR + 'jpegsr9c.zip', - 'dir': 'jpeg-9c', + "jpeg": { + "url": "http://www.ijg.org/files/jpegsr9c.zip", + "filename": "jpegsr9c.zip", + "dir": "jpeg-9c", }, - 'tiff': { - 'url': 'ftp://download.osgeo.org/libtiff/tiff-4.0.10.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'tiff-4.0.10.tar.gz', - 'dir': 'tiff-4.0.10', + "tiff": { + "url": "ftp://download.osgeo.org/libtiff/tiff-4.1.0.tar.gz", + "filename": "tiff-4.1.0.tar.gz", + "dir": "tiff-4.1.0", }, - 'freetype': { - 'url': 'https://download.savannah.gnu.org/releases/freetype/freetype-2.9.1.tar.gz', # noqa: E501 - 'filename': PILLOW_DEPENDS_DIR + 'freetype-2.9.1.tar.gz', - 'dir': 'freetype-2.9.1', + "freetype": { + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.10.1.tar.gz", # noqa: E501 + "filename": "freetype-2.10.1.tar.gz", + "dir": "freetype-2.10.1", }, - 'lcms': { - '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.7": { + "url": SF_MIRROR + "/project/lcms/lcms/2.7/lcms2-2.7.zip", + "filename": "lcms2-2.7.zip", + "dir": "lcms2-2.7", }, - 'ghostscript': { - 'url': 'https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs926/ghostscript-9.26.tar.gz', # noqa: E501 - 'filename': PILLOW_DEPENDS_DIR + 'ghostscript-9.26.tar.gz', - 'dir': 'ghostscript-9.26', + "lcms-2.8": { + "url": SF_MIRROR + "/project/lcms/lcms/2.8/lcms2-2.8.zip", + "filename": "lcms2-2.8.zip", + "dir": "lcms2-2.8", }, - 'tcl-8.5': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.19/tcl8519-src.zip', - 'filename': PILLOW_DEPENDS_DIR + 'tcl8519-src.zip', - 'dir': '', + "tcl-8.5": { + "url": SF_MIRROR + "/project/tcl/Tcl/8.5.19/tcl8519-src.zip", + "filename": "tcl8519-src.zip", + "dir": "", }, - 'tk-8.5': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.19/tk8519-src.zip', - 'filename': PILLOW_DEPENDS_DIR + 'tk8519-src.zip', - 'dir': '', - 'version': '8.5.19', + "tk-8.5": { + "url": SF_MIRROR + "/project/tcl/Tcl/8.5.19/tk8519-src.zip", + "filename": "tk8519-src.zip", + "dir": "", + "version": "8.5.19", }, - 'tcl-8.6': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.9/tcl869-src.zip', - 'filename': PILLOW_DEPENDS_DIR + 'tcl869-src.zip', - 'dir': '', + "tcl-8.6": { + "url": SF_MIRROR + "/project/tcl/Tcl/8.6.10/tcl8610-src.zip", + "filename": "tcl8610-src.zip", + "dir": "", }, - 'tk-8.6': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.9/tk869-src.zip', - 'filename': PILLOW_DEPENDS_DIR + 'tk869-src.zip', - 'dir': '', - 'version': '8.6.9', + "tk-8.6": { + "url": SF_MIRROR + "/project/tcl/Tcl/8.6.10/tk8610-src.zip", + "filename": "tk8610-src.zip", + "dir": "", + "version": "8.6.10", }, - 'webp': { - 'url': 'http://downloads.webmproject.org/releases/webp/libwebp-1.0.2.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'libwebp-1.0.2.tar.gz', - 'dir': 'libwebp-1.0.2', + "webp": { + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.0.3.tar.gz", + "filename": "libwebp-1.0.3.tar.gz", + "dir": "libwebp-1.0.3", }, - 'openjpeg': { - 'url': SF_MIRROR+'/project/openjpeg/openjpeg/2.3.0/openjpeg-2.3.0.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'openjpeg-2.3.0.tar.gz', - 'dir': 'openjpeg-2.3.0', + "openjpeg": { + "url": "https://github.com/uclouvain/openjpeg/archive/v2.3.1.tar.gz", + "filename": "openjpeg-2.3.1.tar.gz", + "dir": "openjpeg-2.3.1", + }, + "jpeg-turbo": { + "url": SF_MIRROR + "/project/libjpeg-turbo/2.0.3/libjpeg-turbo-2.0.3.tar.gz", + "filename": "libjpeg-turbo-2.0.3.tar.gz", + "dir": "libjpeg-turbo-2.0.3", + }, + # ba653c8: Merge tag '2.12.5' into msvc + "imagequant": { + "url": "https://github.com/ImageOptim/libimagequant/archive/ba653c8ccb34dde4e21c6076d85a72d21ed9d971.zip", # noqa: E501 + "filename": "libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971.zip", + "dir": "libimagequant-ba653c8ccb34dde4e21c6076d85a72d21ed9d971", + }, + "harfbuzz": { + "url": "https://github.com/harfbuzz/harfbuzz/archive/2.6.1.zip", + "filename": "harfbuzz-2.6.1.zip", + "dir": "harfbuzz-2.6.1", + }, + "fribidi": { + "url": "https://github.com/fribidi/fribidi/archive/v1.0.7.zip", + "filename": "fribidi-1.0.7.zip", + "dir": "fribidi-1.0.7", + }, + "libraqm": { + "url": "https://github.com/HOST-Oman/libraqm/archive/v0.7.0.zip", + "filename": "libraqm-0.7.0.zip", + "dir": "libraqm-0.7.0", }, } compilers = { 7: { - 2008: { + 2010: { 64: { - 'env_version': 'v7.0', - 'vc_version': '2008', - 'env_flags': '/x64 /xp', - 'inc_dir': 'msvcr90-x64', - 'platform': 'x64', - 'webp_platform': 'x64', + "env_version": "v7.0", + "vc_version": "2010", + "env_flags": "/x64 /xp", + "inc_dir": "msvcr90-x64", + "platform": "x64", + "webp_platform": "x64", }, 32: { - 'env_version': 'v7.0', - 'vc_version': '2008', - 'env_flags': '/x86 /xp', - 'inc_dir': 'msvcr90-x32', - 'platform': 'Win32', - 'webp_platform': 'x86', - } + "env_version": "v7.0", + "vc_version": "2010", + "env_flags": "/x86 /xp", + "inc_dir": "msvcr90-x32", + "platform": "Win32", + "webp_platform": "x86", + }, } }, 7.1: { 2015: { 64: { - 'env_version': 'v7.1', - 'vc_version': '2015', - 'env_flags': '/x64 /vista', - 'inc_dir': 'msvcr10-x64', - 'platform': 'x64', - 'webp_platform': 'x64', + "env_version": "v7.1", + "vc_version": "2015", + "env_flags": "/x64 /vista", + "inc_dir": "msvcr10-x64", + "platform": "x64", + "webp_platform": "x64", }, 32: { - 'env_version': 'v7.1', - 'vc_version': '2015', - 'env_flags': '/x86 /vista', - 'inc_dir': 'msvcr10-x32', - 'platform': 'Win32', - 'webp_platform': 'x86', - } + "env_version": "v7.1", + "vc_version": "2015", + "env_flags": "/x86 /vista", + "inc_dir": "msvcr10-x32", + "platform": "Win32", + "webp_platform": "x86", + }, } - } + }, } def pyversion_from_env(): - py = os.environ['PYTHON'] + py = os.environ["PYTHON"] - py_version = '27' + py_version = "35" for k in pythons: if k in py: py_version = k break - if '64' in py: - py_version = '%s%s' % (py_version, X64_EXT) + if "64" in py: + py_version = "{}{}".format(py_version, X64_EXT) return py_version def compiler_from_env(): - py = os.environ['PYTHON'] + py = os.environ["PYTHON"] for k, v in pythons.items(): if k in py: @@ -149,13 +182,13 @@ def compiler_from_env(): break bit = bit_from_env() - return compilers[py_info['compiler']][py_info['vc']][bit] + return compilers[py_info["compiler"]][py_info["vc"]][bit] def bit_from_env(): - py = os.environ['PYTHON'] + py = os.environ["PYTHON"] - return 64 if '64' in py else 32 + return 64 if "64" in py else 32 def all_compilers(): diff --git a/winbuild/fetch.py b/winbuild/fetch.py index b7acb63ac..adc45429a 100644 --- a/winbuild/fetch.py +++ b/winbuild/fetch.py @@ -3,17 +3,42 @@ import sys import urllib.parse import urllib.request +from config import libs + def fetch(url): - name = urllib.parse.urlsplit(url)[2].split('/')[-1] + 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) - content = urllib.request.urlopen(url).read() - with open(name, 'wb') as fd: + + def retrieve(request_url): + print("Fetching", request_url) + try: + return urllib.request.urlopen(request_url) + except urllib.error.URLError: + return urllib.request.urlopen(request_url) + + try: + r = retrieve(url) + except urllib.error.HTTPError: + if depends_filename: + r = retrieve( + "https://github.com/python-pillow/pillow-depends/raw/master/" + + depends_filename + ) + name = depends_filename + content = r.read() + with open(name, "wb") as fd: fd.write(content) return name -if __name__ == '__main__': +if __name__ == "__main__": fetch(sys.argv[1]) diff --git a/winbuild/fixproj.py b/winbuild/fixproj.py deleted file mode 100644 index 9b5203fb8..000000000 --- a/winbuild/fixproj.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys - -with open(sys.argv[1], 'r') as fd: - content = '\n'.join(line.strip() for line in fd if line.strip()) -if len(sys.argv) == 3: - content = content.replace('Win32', sys.argv[2]).replace('x64', sys.argv[2]) -with open(sys.argv[1], 'w') as fd: - fd.write(content) diff --git a/winbuild/fribidi.cmake b/winbuild/fribidi.cmake new file mode 100644 index 000000000..247e79e4c --- /dev/null +++ b/winbuild/fribidi.cmake @@ -0,0 +1,102 @@ +cmake_minimum_required(VERSION 3.13) + +project(fribidi) + +add_definitions(-D_CRT_SECURE_NO_WARNINGS) + +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +include_directories(lib) + +function(extract_regex_1 var text regex) + string(REGEX MATCH ${regex} _ ${text}) + set(${var} "${CMAKE_MATCH_1}" PARENT_SCOPE) +endfunction() + + +function(fribidi_conf) + file(READ configure.ac FRIBIDI_CONF) + extract_regex_1(FRIBIDI_MAJOR_VERSION "${FRIBIDI_CONF}" "\\(fribidi_major_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_MINOR_VERSION "${FRIBIDI_CONF}" "\\(fribidi_minor_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_MICRO_VERSION "${FRIBIDI_CONF}" "\\(fribidi_micro_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_INTERFACE_VERSION "${FRIBIDI_CONF}" "\\(fribidi_interface_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_INTERFACE_AGE "${FRIBIDI_CONF}" "\\(fribidi_interface_age, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_BINARY_AGE "${FRIBIDI_CONF}" "\\(fribidi_binary_age, ([0-9]+)\\)") + set(FRIBIDI_VERSION "${FRIBIDI_MAJOR_VERSION}.${FRIBIDI_MINOR_VERSION}.${FRIBIDI_MICRO_VERSION}") + set(PACKAGE "fribidi") + set(PACKAGE_NAME "GNU FriBidi") + set(PACKAGE_BUGREPORT "https://github.com/fribidi/fribidi/issues/new") + set(SIZEOF_INT 4) + set(FRIBIDI_MSVC_BUILD_PLACEHOLDER "#define FRIBIDI_BUILT_WITH_MSVC") + message("detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}") + configure_file(lib/fribidi-config.h.in lib/fribidi-config.h @ONLY) +endfunction() +fribidi_conf() + + +function(prepend var prefix) + set(out "") + foreach(f ${ARGN}) + list(APPEND out "${prefix}${f}") + endforeach() + set(${var} "${out}" PARENT_SCOPE) +endfunction() + +macro(fribidi_definitions _TGT) + target_compile_definitions(${_TGT} PUBLIC + HAVE_MEMSET + HAVE_MEMMOVE + HAVE_STRDUP + HAVE_STDLIB_H=1 + HAVE_STRING_H=1 + HAVE_MEMORY_H=1 + #HAVE_STRINGS_H + #HAVE_SYS_TIMES_H + STDC_HEADERS=1 + HAVE_STRINGIZE=1) +endmacro() + +function(fribidi_gen _NAME _OUTNAME _PARAM) + set(_OUT lib/${_OUTNAME}) + prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN}) + add_executable(gen-${_NAME} + gen.tab/gen-${_NAME}.c + gen.tab/packtab.c) + fribidi_definitions(gen-${_NAME}) + target_compile_definitions(gen-${_NAME} + PUBLIC DONT_HAVE_FRIBIDI_CONFIG_H) + add_custom_command( + COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT} + DEPENDS ${_DEP} + OUTPUT ${_OUT}) + list(APPEND FRIBIDI_SOURCES_GENERATED "${_OUT}") + set(FRIBIDI_SOURCES_GENERATED ${FRIBIDI_SOURCES_GENERATED} PARENT_SCOPE) +endfunction() + +fribidi_gen(unicode-version fribidi-unicode-version.h "" + unidata/ReadMe.txt unidata/BidiMirroring.txt) + + +macro(fribidi_tab _NAME) + fribidi_gen(${_NAME}-tab ${_NAME}.tab.i 2 ${ARGN}) + target_sources(gen-${_NAME}-tab + PRIVATE lib/fribidi-unicode-version.h) +endmacro() + +fribidi_tab(bidi-type unidata/UnicodeData.txt) +fribidi_tab(joining-type unidata/UnicodeData.txt unidata/ArabicShaping.txt) +fribidi_tab(arabic-shaping unidata/UnicodeData.txt) +fribidi_tab(mirroring unidata/BidiMirroring.txt) +fribidi_tab(brackets unidata/BidiBrackets.txt unidata/UnicodeData.txt) +fribidi_tab(brackets-type unidata/BidiBrackets.txt) + + +file(GLOB FRIBIDI_SOURCES lib/*.c) +file(GLOB FRIBIDI_HEADERS lib/*.h) + +add_library(fribidi STATIC + ${FRIBIDI_SOURCES} + ${FRIBIDI_HEADERS} + ${FRIBIDI_SOURCES_GENERATED}) +fribidi_definitions(fribidi) +target_compile_definitions(fribidi + PUBLIC -DFRIBIDI_ENTRY=extern) diff --git a/winbuild/get_pythons.py b/winbuild/get_pythons.py index 376f056b7..a853fc6f7 100644 --- a/winbuild/get_pythons.py +++ b/winbuild/get_pythons.py @@ -1,12 +1,15 @@ -from fetch import fetch import os -if __name__ == '__main__': - for version in ['2.7.15', '3.4.4']: - for platform in ['', '.amd64']: - for extension in ['', '.asc']: - fetch('https://www.python.org/ftp/python/%s/python-%s%s.msi%s' - % (version, version, platform, extension)) +from fetch import fetch + +if __name__ == "__main__": + for version in ["3.4.4"]: + for platform in ["", ".amd64"]: + for extension in ["", ".asc"]: + fetch( + "https://www.python.org/ftp/python/%s/python-%s%s.msi%s" + % (version, version, platform, extension) + ) # find pip, if it's not in the path! - os.system('pip install virtualenv') + os.system("pip install virtualenv") diff --git a/winbuild/lcms2_patch.ps1 b/winbuild/lcms2_patch.ps1 new file mode 100644 index 000000000..7fc48c034 --- /dev/null +++ b/winbuild/lcms2_patch.ps1 @@ -0,0 +1,9 @@ + +Get-ChildItem .\Projects\VC2015\ *.vcxproj -recurse | + Foreach-Object { + $c = ($_ | Get-Content) + $c = $c -replace 'MultiThreaded<','MultiThreadedDLL<' + $c = $c -replace '8.1','10' + $c = $c -replace 'v140','v142' + [IO.File]::WriteAllText($_.FullName, ($c -join "`r`n")) + } diff --git a/winbuild/raqm.cmake b/winbuild/raqm.cmake new file mode 100644 index 000000000..88eb7f284 --- /dev/null +++ b/winbuild/raqm.cmake @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.13) + +project(libraqm) + + +find_library(fribidi NAMES fribidi) +find_library(harfbuzz NAMES harfbuzz) +find_library(freetype NAMES freetype) + +add_definitions(-DFRIBIDI_ENTRY=extern) + + +function(raqm_conf) + file(READ configure.ac RAQM_CONF) + string(REGEX MATCH "\\[([0-9]+)\\.([0-9]+)\\.([0-9]+)\\]," _ "${RAQM_CONF}") + set(RAQM_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(RAQM_VERSION_MINOR "${CMAKE_MATCH_2}") + set(RAQM_VERSION_MICRO "${CMAKE_MATCH_3}") + set(RAQM_VERSION "${RAQM_VERSION_MAJOR}.${RAQM_VERSION_MINOR}.${RAQM_VERSION_MICRO}") + message("detected libraqm version ${RAQM_VERSION}") + configure_file(src/raqm-version.h.in src/raqm-version.h @ONLY) +endfunction() +raqm_conf() + + +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +set(RAQM_SOURCES + src/raqm.c) +set(RAQM_HEADERS + src/raqm.h + src/raqm-version.h) + +add_library(libraqm SHARED + ${RAQM_SOURCES} + ${RAQM_HEADERS}) +target_link_libraries(libraqm + ${fribidi} + ${harfbuzz} + ${freetype}) diff --git a/winbuild/test.py b/winbuild/test.py index 84e071308..a05a20b18 100755 --- a/winbuild/test.py +++ b/winbuild/test.py @@ -1,46 +1,45 @@ #!/usr/bin/env python3 -import subprocess -import os import glob +import os +import subprocess import sys -from config import pythons, VIRT_BASE, X64_EXT +from config import VIRT_BASE, X64_EXT, pythons def test_one(params): python, architecture = params try: print("Running: %s, %s" % params) - command = [r'%s\%s%s\Scripts\python.exe' % - (VIRT_BASE, python, architecture), - 'test-installed.py', - '--processes=-0', - '--process-timeout=30', - ] - command.extend(glob.glob('Tests/test*.py')) - proc = subprocess.Popen(command, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE) + command = [ + r"{}\{}{}\Scripts\python.exe".format(VIRT_BASE, python, architecture), + "test-installed.py", + "--processes=-0", + "--process-timeout=30", + ] + command.extend(glob.glob("Tests/test*.py")) + proc = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE) (trace, stderr) = proc.communicate() status = proc.returncode - print("Done with %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)) -if __name__ == '__main__': +if __name__ == "__main__": - os.chdir('..') - matrix = [(python, architecture) for python in pythons - for architecture in ('', X64_EXT)] + os.chdir("..") + matrix = [ + (python, architecture) for python in pythons for architecture in ("", X64_EXT) + ] results = map(test_one, matrix) for (python, architecture, status, trace) in results: - print("%s%s: %s" % (python, architecture, status and 'ERR' or 'PASS')) + print("{}{}: {}".format(python, architecture, status and "ERR" or "PASS")) res = all(status for (python, architecture, status, trace) in results) sys.exit(res) diff --git a/winbuild/nmake.opt b/winbuild/tiff.opt similarity index 100% rename from winbuild/nmake.opt rename to winbuild/tiff.opt diff --git a/winbuild/untar.py b/winbuild/untar.py index d85be384c..f2713b2f2 100644 --- a/winbuild/untar.py +++ b/winbuild/untar.py @@ -3,9 +3,9 @@ import tarfile def untar(src, dest): - with tarfile.open(src, 'r:gz') as tgz: + with tarfile.open(src, "r:gz") as tgz: tgz.extractall(dest) -if __name__ == '__main__': +if __name__ == "__main__": untar(sys.argv[1], sys.argv[2]) diff --git a/winbuild/unzip.py b/winbuild/unzip.py index 5a464757c..eb17a2e63 100644 --- a/winbuild/unzip.py +++ b/winbuild/unzip.py @@ -7,5 +7,5 @@ def unzip(src, dest): zf.extractall(dest) -if __name__ == '__main__': +if __name__ == "__main__": unzip(sys.argv[1], sys.argv[2])