diff --git a/appveyor.yml b/.appveyor.yml similarity index 93% rename from appveyor.yml rename to .appveyor.yml index fe128f4a4..0cf10597a 100644 --- a/appveyor.yml +++ b/.appveyor.yml @@ -1,117 +1,125 @@ -version: '{build}' -clone_folder: c:\pillow -init: -- ECHO %PYTHON% -#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) -# Uncomment previous line to get RDP access during the build. - -environment: - X64_EXT: -x64 - EXECUTABLE: python.exe - PIP_DIR: Scripts - VENV: NO - TEST_OPTIONS: - DEPLOY: YES - matrix: - - PYTHON: C:/vp/pypy2 - EXECUTABLE: bin/pypy.exe - PIP_DIR: bin - VENV: YES - - PYTHON: C:/Python27-x64 - - PYTHON: C:/Python34 - - PYTHON: C:/Python27 - - PYTHON: C:/Python34-x64 - - PYTHON: C:/msys64/mingw32 - EXECUTABLE: bin/python3 - PIP_DIR: bin - TEST_OPTIONS: --processes=0 - DEPLOY: NO - - -install: -- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip -- 7z x pillow-depends.zip -oc:\ -- mv c:\pillow-depends-master c:\pillow-depends -- xcopy c:\pillow-depends\*.zip c:\pillow\winbuild\ -- xcopy c:\pillow-depends\*.tar.gz c:\pillow\winbuild\ -- xcopy /s c:\pillow-depends\test_images\* c:\pillow\tests\images -- cd c:\pillow\winbuild\ -- ps: | - if ($env:PYTHON -eq "c:/vp/pypy2") - { - c:\pillow\winbuild\appveyor_install_pypy.cmd - } -- ps: | - if ($env:PYTHON -eq "c:/msys64/mingw32") - { - c:\msys64\usr\bin\bash -l -c c:\\pillow\\winbuild\\appveyor_install_msys2_deps.sh - } - else - { - c:\python34\python.exe c:\pillow\winbuild\build_dep.py - c:\pillow\winbuild\build_deps.cmd - $host.SetShouldExit(0) - } - -build_script: -- ps: | - if ($env:PYTHON -eq "c:/msys64/mingw32") - { - c:\msys64\usr\bin\bash -l -c c:\\pillow\\winbuild\\appveyor_build_msys2.sh - Write-Host "through install" - $host.SetShouldExit(0) - } - else - { - & $env:PYTHON/$env:EXECUTABLE c:\pillow\winbuild\build.py - $host.SetShouldExit(0) - } -- cd c:\pillow -- '%PYTHON%\%EXECUTABLE% selftest.py --installed' - -test_script: -- cd c:\pillow -- '%PYTHON%\%PIP_DIR%\pip.exe install pytest pytest-cov' -- '%PYTHON%\%EXECUTABLE% -m 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? - -after_test: -- pip install codecov -- codecov --file coverage.xml --name %PYTHON% - -matrix: - fast_finish: true - -artifacts: -- path: pillow\dist\*.egg - name: egg -- path: pillow\dist\*.wheel - name: wheel - -before_deploy: - - cd c:\pillow - - '%PYTHON%\%PIP_DIR%\pip.exe install wheel' - - cd c:\pillow\winbuild\ - - '%PYTHON%\%EXECUTABLE% c:\pillow\winbuild\build.py --wheel' - - cd c:\pillow - - ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } - -deploy: - provider: S3 - region: us-west-2 - access_key_id: AKIAIRAXC62ZNTVQJMOQ - secret_access_key: - secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi - bucket: pillow-nightly - folder: win/$(APPVEYOR_BUILD_NUMBER)/ - artifact: /.*egg|wheel/ - on: - branch: master - deploy: YES - - -# Uncomment the following lines to get RDP access after the build/test and block for -# up to the timeout limit (~1hr) -# -#on_finish: -#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) +version: '{build}' +clone_folder: c:\pillow +init: +- ECHO %PYTHON% +#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) +# Uncomment previous line to get RDP access during the build. + +environment: + X64_EXT: -x64 + EXECUTABLE: python.exe + PIP_DIR: Scripts + VENV: NO + TEST_OPTIONS: + DEPLOY: YES + matrix: + - PYTHON: C:/vp/pypy2 + EXECUTABLE: bin/pypy.exe + PIP_DIR: bin + VENV: YES + - PYTHON: C:/Python27-x64 + - PYTHON: C:/Python37 + - PYTHON: C:/Python27 + - PYTHON: C:/Python37-x64 + - PYTHON: C:/Python36 + - PYTHON: C:/Python36-x64 + - PYTHON: C:/Python35 + - PYTHON: C:/Python35-x64 + - PYTHON: C:/msys64/mingw32 + EXECUTABLE: bin/python3 + PIP_DIR: bin + TEST_OPTIONS: --processes=0 + DEPLOY: NO + + +install: +- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip +- 7z x pillow-depends.zip -oc:\ +- mv c:\pillow-depends-master c:\pillow-depends +- xcopy c:\pillow-depends\*.zip c:\pillow\winbuild\ +- xcopy c:\pillow-depends\*.tar.gz c:\pillow\winbuild\ +- xcopy /s c:\pillow-depends\test_images\* c:\pillow\tests\images +- cd c:\pillow\winbuild\ +- ps: | + if ($env:PYTHON -eq "c:/vp/pypy2") + { + c:\pillow\winbuild\appveyor_install_pypy.cmd + } +- ps: | + if ($env:PYTHON -eq "c:/msys64/mingw32") + { + c:\msys64\usr\bin\bash -l -c c:\\pillow\\winbuild\\appveyor_install_msys2_deps.sh + } + else + { + c:\python34\python.exe c:\pillow\winbuild\build_dep.py + c:\pillow\winbuild\build_deps.cmd + $host.SetShouldExit(0) + } + +build_script: +- ps: | + if ($env:PYTHON -eq "c:/msys64/mingw32") + { + c:\msys64\usr\bin\bash -l -c c:\\pillow\\winbuild\\appveyor_build_msys2.sh + Write-Host "through install" + $host.SetShouldExit(0) + } + else + { + & $env:PYTHON/$env:EXECUTABLE c:\pillow\winbuild\build.py + $host.SetShouldExit(0) + } +- cd c:\pillow +- '%PYTHON%\%EXECUTABLE% selftest.py --installed' + +test_script: +- cd c:\pillow +- '%PYTHON%\%PIP_DIR%\pip.exe install pytest pytest-cov' +- '%PYTHON%\%EXECUTABLE% -m 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? + +after_test: +- pip install codecov +- codecov --file coverage.xml --name %PYTHON% + +matrix: + fast_finish: true + +cache: +- '%LOCALAPPDATA%\pip\Cache' + +artifacts: +- path: pillow\dist\*.egg + name: egg +- path: pillow\dist\*.wheel + name: wheel + +before_deploy: + - cd c:\pillow + - '%PYTHON%\%PIP_DIR%\pip.exe install wheel' + - cd c:\pillow\winbuild\ + - '%PYTHON%\%EXECUTABLE% c:\pillow\winbuild\build.py --wheel' + - cd c:\pillow + - ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + +deploy: + provider: S3 + region: us-west-2 + access_key_id: AKIAIRAXC62ZNTVQJMOQ + secret_access_key: + secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi + bucket: pillow-nightly + folder: win/$(APPVEYOR_BUILD_NUMBER)/ + artifact: /.*egg|wheel/ + on: + APPVEYOR_REPO_NAME: python-pillow/Pillow + branch: master + deploy: YES + + +# Uncomment the following lines to get RDP access after the build/test and block for +# up to the timeout limit (~1hr) +# +#on_finish: +#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 000000000..3e147d151 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,9 @@ +# Documentation: https://docs.codecov.io/docs/codecov-yaml + +codecov: + # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" + # https://github.com/codecov/support/issues/363 + # https://docs.codecov.io/v4.3.6/docs/comparing-commits + allow_coverage_offsets: true + +comment: off diff --git a/.gitattributes b/.gitattributes index 983b58729..2cf25ab1a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ +*.eps binary *.ppm binary *.container binary diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6c6e9b612..eef2a30bd 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -4,7 +4,7 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v ## Bug fixes, feature additions, etc. -Please send a pull request to the master branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new) or irc://irc.freenode.net#pil +Please send a pull request to the master branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil - Fork the Pillow repository. - Create a branch from master. @@ -21,7 +21,9 @@ Please send a pull request to the master branch. Please include [documentation]( ## Reporting Issues -When reporting issues, please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. The best reproductions are self-contained scripts with minimal dependencies. +When reporting issues, 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. ### Provide details diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6c91b6427..6cea87df2 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,7 +4,11 @@ ### What actually happened? -### What versions of Pillow and Python are you using? +### 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. diff --git a/.gitignore b/.gitignore index b68d28075..94181c653 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,7 @@ htmlcov/ .tox/ .coverage .cache -nosetests.xml +.pytest_cache coverage.xml # Test files @@ -56,6 +56,9 @@ test_images # Sphinx documentation docs/_build/ +# viewdoc output +.long-description.html + # Vim cruft .*.swp diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..73e1f8213 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,2 @@ +python: + pip_install: true diff --git a/.travis.yml b/.travis.yml index 1b8d854bc..9550eb8f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,88 +1,102 @@ +dist: xenial language: python +cache: pip notifications: irc: "chat.freenode.net#pil" -# Run slow PyPy* first, to give them a headstart and reduce waiting time. +# Run fast lint first to get fast feedback. +# Run slow PyPy* next, to give them a headstart and reduce waiting time. # Run latest 3.x and 2.x next, to get quick compatibility results. # Then run the remainder, with fastest Docker jobs last. matrix: fast_finish: true include: - - python: "pypy" - - python: "pypy3" - - python: '3.6' + - python: "3.6" + name: "Lint" + env: LINT="true" + - python: "pypy2.7-6.0" + name: "PyPy2 Xenial" + dist: xenial + - python: "pypy3.5-6.0" + name: "PyPy3 Xenial" + dist: xenial + - 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 + env: PYTHONOPTIMIZE=1 - python: '3.5' - - python: '3.4' - - python: '3.7-dev' - - env: DOCKER="alpine" DOCKER_TAG="pytest" - - env: DOCKER="arch" DOCKER_TAG="pytest" # contains PyQt5 - - env: DOCKER="ubuntu-trusty-x86" DOCKER_TAG="pytest" - - env: DOCKER="ubuntu-xenial-amd64" DOCKER_TAG="pytest" - - env: DOCKER="debian-stretch-x86" DOCKER_TAG="pytest" - - env: DOCKER="centos-6-amd64" DOCKER_TAG="pytest" - - env: DOCKER="centos-7-amd64" DOCKER_TAG="pytest" - - env: DOCKER="amazon-1-amd64" DOCKER_TAG="pytest" - - env: DOCKER="amazon-2-amd64" DOCKER_TAG="pytest" - - env: DOCKER="fedora-26-amd64" DOCKER_TAG="pytest" - - env: DOCKER="fedora-27-amd64" DOCKER_TAG="pytest" - -dist: trusty - -sudo: required + name: "3.5 Xenial" + - python: '3.5' + name: "3.5 Trusty PYTHONOPTIMIZE=2" + dist: trusty + env: PYTHONOPTIMIZE=2 + - python: "3.8-dev" + name: "3.8-dev Xenial" + - env: DOCKER="alpine" DOCKER_TAG="master" + - env: DOCKER="arch" DOCKER_TAG="master" # contains PyQt5 + - env: DOCKER="ubuntu-trusty-x86" DOCKER_TAG="master" + - env: DOCKER="ubuntu-xenial-amd64" DOCKER_TAG="master" + - env: DOCKER="debian-stretch-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" services: - docker -install: - - if [ "$DOCKER" == "" ]; then .travis/install.sh; fi - before_install: - - if [ "$DOCKER" ]; then docker pull pythonpillow/$DOCKER:$DOCKER_TAG; fi + - if [ "$DOCKER" ]; then travis_retry docker pull pythonpillow/$DOCKER:$DOCKER_TAG; fi + +install: + - | + if [ "$LINT" == "true" ]; then + pip install tox + elif [ "$DOCKER" == "" ]; then + .travis/install.sh; + fi before_script: # Qt needs a display for some of the tests, and it's only run on the system site packages install - - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" +- | + 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 [ "$DOCKER" == "" ]; then - .travis/script.sh - else - # the Pillow user in the docker container is UID 1000 - sudo chown -R 1000 $TRAVIS_BUILD_DIR - docker run -v $TRAVIS_BUILD_DIR:/Pillow pythonpillow/$DOCKER:$DOCKER_TAG - fi +- | + if [ "$LINT" == "true" ]; then + tox -e lint + elif [ "$DOCKER" == "" ]; then + .travis/script.sh + elif [ "$DOCKER" ]; then + # the Pillow user in the docker container is UID 1000 + sudo chown -R 1000 $TRAVIS_BUILD_DIR + docker run -v $TRAVIS_BUILD_DIR:/Pillow pythonpillow/$DOCKER:$DOCKER_TAG + fi after_success: - - .travis/after_success.sh - -after_failure: - - | - if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then - curl -Lo travis_after_all.py https://raw.github.com/dmakhno/travis_after_all/master/travis_after_all.py - python travis_after_all.py - export $(cat .to_export_back) - if [ "$BUILD_LEADER" = "YES" ]; then - if [ "$BUILD_AGGREGATE_STATUS" = "others_failed" ]; then - echo "All jobs failed" - else - echo "Some jobs failed" - fi - fi - fi - -after_script: - - | - if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then - echo leader=$BUILD_LEADER status=$BUILD_AGGREGATE_STATUS - fi - -env: - global: - # travis encrypt AUTH_TOKEN= - secure: "Vzm7aG1Qv0SDQcqiPzZMedNLn5ZmpL7IzF0DYnqcD+/l+zmKU22SnJBcX0uVXumo+r7eZfpsShpqfcdsZvMlvmQnwz+Y6AGKQru9tCKZbTMnuRjWKKXekC+tr8Xt9CKvRVtte5PyXW31paxUI3/e+fQGBwoFjEEC+6EpEOjeRfE=" +- | + if [ "$LINT" == "" ]; then + .travis/after_success.sh + fi diff --git a/.travis/after_success.sh b/.travis/after_success.sh index a18c095c9..ad1aeffa3 100755 --- a/.travis/after_success.sh +++ b/.travis/after_success.sh @@ -15,35 +15,9 @@ pip install coveralls-merge coveralls-merge coverage.c.json codecov -if [ "$DOCKER" == "" ]; then - pip install pyflakes pycodestyle - pyflakes *.py | tee >(wc -l) - pyflakes src/PIL/*.py | tee >(wc -l) - pyflakes Tests/*.py | tee >(wc -l) - pycodestyle --statistics --count src/PIL/*.py - pycodestyle --statistics --count Tests/*.py -fi - if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ] && [ "$DOCKER" == "" ]; then # Coverage and quality reports on just the latest diff. # (Installation is very slow on Py3, so just do it for Py2.) depends/diffcover-install.sh depends/diffcover-run.sh fi - -# after_all - -if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then - curl -Lo travis_after_all.py https://raw.github.com/dmakhno/travis_after_all/master/travis_after_all.py - python travis_after_all.py - export $(cat .to_export_back) - if [ "$BUILD_LEADER" = "YES" ]; then - if [ "$BUILD_AGGREGATE_STATUS" = "others_succeeded" ]; then - echo "All jobs succeeded! Triggering macOS build..." - # Trigger a macOS build at the pillow-wheels repo - ./build_children.sh - else - echo "Some jobs failed" - fi - fi -fi diff --git a/.travis/install.sh b/.travis/install.sh index cad0e3c32..b123fd302 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -7,7 +7,7 @@ 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 -pip install cffi +PYTHONOPTIMIZE=0 pip install cffi pip install check-manifest pip install coverage pip install olefile @@ -15,6 +15,7 @@ pip install -U pytest pip install -U pytest-cov pip install pyroma pip install test-image-results +pip install numpy # docs only on Python 2.7 if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then pip install -r requirements.txt ; fi diff --git a/CHANGES.rst b/CHANGES.rst index 434225d86..e1c08d8bd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,12 +2,516 @@ Changelog (Pillow) ================== -5.1.0 (unreleased) +6.0.0 (unreleased) ------------------ +- Remove unnecessary unittest.main() boilerplate from test files #3631 + [jdufresne] + +- Exif: Seek to IFD offset #3584 + [radarhere] + +- Deprecate PIL.*ImagePlugin.__version__ attributes #3628 + [jdufresne] + +- Docs: Add note about ImageDraw operations that exceed image bounds #3620 + [radarhere] + +- Allow for unknown PNG chunks after image data #3558 + [radarhere] + +- Changed EPS subprocess stdin from devnull to None #3611 + [radarhere] + +- Fix possible integer overflow #3609 + [cgohlke] + +- Catch BaseException for resource cleanup handlers #3574 + [jdufresne] + +- Improve pytest configuration to allow specific tests as CLI args #3579 + [jdufresne] + +- Drop support for Python 3.4 #3596 + [hugovk] + +- Remove deprecated PIL.OleFileIO #3598 + [hugovk] + +- Remove deprecated ImageOps undocumented functions #3599 + [hugovk] + +- Depends: Update libwebp to 1.0.2 #3602 + [radarhere] + +- Detect MIME types #3525 + [radarhere] + +5.4.1 (2019-01-06) +------------------ + +- File closing: Only close __fp if not fp #3540 + [radarhere] + +- Fix build for Termux #3529 + [pslacerda] + +- PNG: Detect MIME types #3525 + [radarhere] + +- PNG: Handle IDAT chunks after image end #3532 + [radarhere] + +5.4.0 (2019-01-01) +------------------ + +- Docs: Improved ImageChops documentation #3522 + [radarhere] + +- Allow RGB and RGBA values for P image putpixel #3519 + [radarhere] + +- Add APNG extension to PNG plugin #3501 + [pirate486743186, radarhere] + +- Lookup ld.so.cache instead of hardcoding search paths #3245 + [pslacerda] + +- Added custom string TIFF tags #3513 + [radarhere] + +- Improve setup.py configuration #3395 + [diorcety] + +- Read textual chunks located after IDAT chunks for PNG #3506 + [radarhere] + +- Performance: Don't try to hash value if enum is empty #3503 + [Glandos] + +- Added custom int and float TIFF tags #3350 + [radarhere] + +- Fixes for issues reported by static code analysis #3393 + [frenzymadness] + +- GIF: Wait until mode is normalized to copy im.info into encoderinfo #3187 + [radarhere] + +- Docs: Add page of deprecations and removals #3486 + [hugovk] + +- Travis CI: Upgrade PyPy from 5.8.0 to 6.0 #3488 + [hugovk] + +- Travis CI: Allow lint job to fail #3467 + [hugovk] + +- Resolve __fp when closing and deleting #3261 + [radarhere] + +- Close exclusive fp before discarding #3461 + [radarhere] + +- Updated open files documentation #3490 + [radarhere] + +- Added libjpeg_turbo to check_feature #3493 + [radarhere] + +- Change color table index background to tuple when saving as WebP #3471 + [radarhere] + +- Allow arbitrary number of comment extension subblocks #3479 + [radarhere] + +- Ensure previous FLI frame is loaded before seeking to the next #3478 + [radarhere] + +- ImageShow improvements #3450 + [radarhere] + +- Depends: Update libimagequant to 2.12.2 #3442, libtiff to 4.0.10 #3458, libwebp to 1.0.1 #3468, Tk Tcl to 8.6.9 #3465 + [radarhere] + +- Check quality_layers type #3464 + [radarhere] + +- Add context manager, __del__ and close methods to TarIO #3455 + [radarhere] + +- Test: Do not play sound when running screencapture command #3454 + [radarhere] + +- Close exclusive fp on open exception #3456 + [radarhere] + +- Only close existing fp in WebP if fp is exclusive #3418 + [radarhere] + +- Docs: Re-add the downloads badge #3443 + [hugovk] + +- Added negative index to PixelAccess #3406 + [Nazime] + +- Change tuple background to global color table index when saving as GIF #3385 + [radarhere] + +- Test: Improved ImageGrab tests #3424 + [radarhere] + +- Flake8 fixes #3422, #3440 + [radarhere, hugovk] + +- Only ask for YCbCr->RGB libtiff conversion for jpeg-compressed tiffs #3417 + [kkopachev] + +- Optimise ImageOps.fit by combining resize and crop #3409 + [homm] + +5.3.0 (2018-10-01) +------------------ + +- Changed Image size property to be read-only by default #3203 + [radarhere] + +- Add warnings if image file identification fails due to lack of WebP support #3169 + [radarhere, hugovk] + +- Hide the Ghostscript progress dialog popup on Windows #3378 + [hugovk] + +- Adding support to reading tiled and YcbCr jpeg tiffs through libtiff #3227 + [kkopachev] + +- Fixed None as TIFF compression argument #3310 + [radarhere] + +- Changed GIF seek to remove previous info items #3324 + [radarhere] + +- Improved PDF document info #3274 + [radarhere] + +- Add line width parameter to rectangle and ellipse-based shapes #3094 + [hugovk, radarhere] + +- Fixed decompression bomb check in _crop #3313 + [dinkolubina, hugovk] + +- Added support to ImageDraw.floodfill for non-RGB colors #3377 + [radarhere] + +- Tests: Avoid catching unexpected exceptions in tests #2203 + [jdufresne] + +- Use TextIOWrapper.detach() instead of NoCloseStream #2214 + [jdufresne] + +- Added transparency to matrix conversion #3205 + [radarhere] + +- Added ImageOps pad method #3364 + [radarhere] + +- Give correct extrema for I;16 format images #3359 + [bz2] + +- Added PySide2 #3279 + [radarhere] + +- Corrected TIFF tags #3369 + [radarhere] + +- CI: Install CFFI and pycparser without any PYTHONOPTIMIZE #3374 + [hugovk] + +- Read/Save RGB webp as RGB (instead of RGBX) #3298 + [kkopachev] + +- ImageDraw: Add line joints #3250 + [radarhere] + +- Improved performance of ImageDraw floodfill method #3294 + [yo1995] + +- Fix builds with --parallel #3272 + [hsoft] + +- Add more raw Tiff modes (RGBaX, RGBaXX, RGBAX, RGBAXX) #3335 + [homm] + +- Close existing WebP fp before setting new fp #3341 + [radarhere] + +- Add orientation, compression and id_section as TGA save keyword arguments #3327 + [radarhere] + +- Convert int values of RATIONAL TIFF tags to floats #3338 + [radarhere, wiredfool] + +- Fix code for PYTHONOPTIMIZE #3233 + [hugovk] + +- Changed ImageFilter.Kernel to subclass ImageFilter.BuiltinFilter, instead of the other way around #3273 + [radarhere] + +- Remove unused draw.draw_line, draw.draw_point and font.getabc methods #3232 + [hugovk] + +- Tests: Added ImageFilter tests #3295 + [radarhere] + +- Tests: Added ImageChops tests #3230 + [hugovk, radarhere] + +- AppVeyor: Download lib if not present in pillow-depends #3316 + [radarhere] + +- Travis CI: Add Python 3.7 and Xenial #3234 + [hugovk] + +- Docs: Added documentation for NumPy conversion #3301 + [radarhere] + +- Depends: Update libimagequant to 2.12.1 #3281 + [radarhere] + +- Add three-color support to ImageOps.colorize #3242 + [tsennott] + +- Tests: Add LA to TGA test modes #3222 + [danpla] + +- Skip outline if the draw operation fills with the same colour #2922 + [radarhere] + +- Flake8 fixes #3173, #3380 + [radarhere] + +- Avoid deprecated 'U' mode when opening files #2187 + [jdufresne] + +5.2.0 (2018-07-01) +------------------ + +- Fixed saving a multiframe image as a single frame PDF #3137 + [radarhere] + +- If a Qt version is already imported, attempt to use it first #3143 + [radarhere] + +- Fix transform fill color for alpha images #3147 + [fozcode] + +- TGA: Add support for writing RLE data #3186 + [danpla] + +- TGA: Read and write LA data #3178 + [danpla] + +- QuantOctree.c: Remove erroneous attempt to average over an empty range #3196 + [tkoeppe] + +- Changed ICNS format tests to pass on OS X 10.11 #3202 + [radarhere] + +- Fixed bug in ImageDraw.multiline_textsize() #3114 + [tianyu139] + +- Added getsize_multiline support for PIL.ImageFont #3113 + [tianyu139] + +- Added ImageFile get_format_mimetype method #3190 + [radarhere] + +- Changed mmap file pointer to use context manager #3216 + [radarhere] + +- Changed ellipse point calculations to be more evenly distributed #3142 + [radarhere] + +- Only extract first Exif segment #2946 + [hugovk] + +- Tests: Test ImageDraw2, WalImageFile #3135, #2989 + [hugovk] + +- Remove unnecessary '#if 0' code #3075 + [hugovk] + +- Tests: Added GD tests #1817 + [radarhere] + +- Fix collections ABCs DeprecationWarning in Python 3.7 #3123 + [hugovk] + +- unpack_from is faster than unpack of slice #3201 + [landfillbaby] + +- Docs: Add coordinate system links and file handling links in documentation #3204, #3214 + [radarhere] + +- Tests: TestFilePng: Fix test_save_l_transparency() #3182 + [danpla] + +- Docs: Correct argument name #3171 + [radarhere] + +- Docs: Update CMake download URL #3166 + [radarhere] + +- Docs: Improve Image.transform documentation #3164 + [radarhere] + +- Fix transform fillcolor argument when image mode is RGBA or LA #3163 + [radarhere] + +- Tests: More specific Exception testing #3158 + [radarhere] + +- Add getrgb HSB/HSV color strings #3148 + [radarhere] + +- Allow float values in getrgb HSL color string #3146 + [radarhere] + +- AppVeyor: Upgrade to Python 2.7.15 and 3.4.4 #3140 + [radarhere] + +- AppVeyor: Upgrade to PyPy 6.0.0 #3133 + [hugovk] + +- Deprecate PILLOW_VERSION and VERSION #3090 + [hugovk] + +- Support Python 3.7 #3076 + [hugovk] + +- Depends: Update freetype to 2.9.1, libjpeg to 9c, libwebp to 1.0.0 #3121, #3136, #3108 + [radarhere] + +- Build macOS wheels with Xcode 6.4, supporting older macOS versions #3068 + [wiredfool] + +- Fix _i2f compilation on some GCC versions #3067 + [homm] + +- Changed encoderinfo to have priority over info when saving GIF images #3086 + [radarhere] + +- Rename PIL.version to PIL._version and remove it from module #3083 + [homm] + +- Enable background colour parameter on rotate #3057 + [storesource] + +- Remove unnecessary `#if 1` directive #3072 + [jdufresne] + +- Remove unused Python class, Path #3070 + [jdufresne] + +- Fix dereferencing type-punned pointer will break strict-aliasing #3069 + [jdufresne] + +5.1.0 (2018-04-02) +------------------ + +- Close fp before return in ImagingSavePPM #3061 + [kathryndavies] + +- Added documentation for ICNS append_images #3051 + [radarhere] + +- Docs: Move intro text below its header #3021 + [hugovk] + +- CI: Rename appveyor.yml as .appveyor.yml #2978 + [hugovk] + +- Fix TypeError for JPEG2000 parser feed #3042 + [hugovk] + +- Certain corrupted jpegs can result in no data read #3023 + [kkopachev] + +- Add support for BLP file format #3007 + [jleclanche] + +- Simplify version checks #2998 + [hugovk] + +- Fix "invalid escape sequence" warning on Python 3.6+ #2996 + [timgraham] + +- Allow append_images to set .icns scaled images #3005 + [radarhere] + +- Support appending to existing PDFs #2965 + [vashek] + +- Fix and improve efficient saving of ICNS on macOS #3004 + [radarhere] + +- Build: Enable pip cache in AppVeyor build #3009 + [thijstriemstra] + +- Trim trailing whitespace #2985 + [Metallicow] + +- Docs: Correct reference to Image.new method #3000 + [radarhere] + +- Rearrange ImageFilter classes into alphabetical order #2990 + [radarhere] + +- Test: Remove duplicate line #2983 + [radarhere] + +- Build: Update AppVeyor PyPy version #3003 + [radarhere] + +- Tiff: Open 8 bit Tiffs with 5 or 6 channels, discarding extra channels #2938 + [homm] + +- Readme: Added Twitter badge #2930 + [hugovk] + +- Removed __main__ code from ImageCms #2942 + [radarhere] + +- Test: Changed assert statements to unittest calls #2961 + [radarhere] + +- Depends: Update libimagequant to 2.11.10, raqm to 0.5.0, freetype to 2.9 #3036, #3017, #2957 + [radarhere] + +- Remove _imaging.crc32 in favor of builtin Python crc32 implementation #2935 + [wiredfool] + +- Move Tk directory to src directory #2928 + [hugovk] + +- Enable pip cache in Travis CI #2933 + [jdufresne] + +- Remove unused and duplicate imports #2927 + [radarhere] + - Docs: Changed documentation references to 2.x to 2.7 #2921 [radarhere] +- Fix memory leak when opening webp files #2974 + [wiredfool] + +- Setup: Fix "TypeError: 'NoneType' object is not iterable" for PPC and CRUX #2951 + [hugovk] + +- Setup: Add libdirs for ppc64le and armv7l #2968 + [nehaljwani] + 5.0.0 (2018-01-01) ------------------ @@ -22,13 +526,13 @@ Changelog (Pillow) - Dynamically link libraqm #2753 [wiredfool] - + - Removed scripts directory #2901 [wiredfool] - + - TIFF: Run all compressed tiffs through libtiff decoder #2899 [wiredfool] - + - GIF: Add disposal option when saving GIFs #2902 [linnil1, wiredfool] @@ -45,7 +549,7 @@ Changelog (Pillow) [wiredfool] - Test: avoid random failure in test_effect_noise #2894 - [hugovk] + [hugovk] - Increased epsilon for test_file_eps.py:test_showpage due to Arch update. #2896 [wiredfool] @@ -89,7 +593,7 @@ Changelog (Pillow) - Add eog support for Ubuntu Image Viewer #2864 [NafisFaysal] -- Test: Test on 3.7-dev on Travis.ci #2870 +- Test: Test on 3.7-dev on Travis CI #2870 [hugovk] - Dependencies: Update libtiff to 4.0.9 #2871 @@ -128,7 +632,7 @@ Changelog (Pillow) - GIF: Permit LZW code lengths up to 12 bits in GIF decode #2813 [wiredfool] -- Fix unterminiated string and unchecked exception in _font_text_asBytes. #2825 +- Fix unterminated string and unchecked exception in _font_text_asBytes. #2825 [wiredfool] - PPM: Use fixed list of whitespace, rather relying on locale, fixes #272. #2831 @@ -218,7 +722,7 @@ Changelog (Pillow) - Fixed doc syntax in ImageDraw #2752 [radarhere] -- Fixed support for building on Windows/msys2. Added Appveyor CI coverage for python3 on msys2 #2476 +- Fixed support for building on Windows/msys2. Added Appveyor CI coverage for python3 on msys2 #2746 [wiredfool] - Fix ValueError in Exif/Tiff IFD #2719 @@ -290,7 +794,7 @@ Changelog (Pillow) - Use RGBX rawmode for RGB JPEG images where possible #1989 [homm] -- Remove palettes from non-palette modes in _new #2702 +- Remove palettes from non-palette modes in _new #2704 [wiredfool] - Delete transparency info when convert'ing RGB/L to RGBA #2633 @@ -410,7 +914,7 @@ Changelog (Pillow) - Doc: Clarified Image.save:append_images documentation #2604 [radarhere] -- CI: Amazon Linux and Centos6 docker images added to TravisCI #2585 +- CI: Amazon Linux and Centos6 docker images added to Travis CI #2585 [wiredfool] - Image.alpha_composite added #2595 @@ -479,7 +983,7 @@ Changelog (Pillow) - Update Feature Detection #2520 [wiredfool] -- CI: Update pypy on TravisCI #2573 +- CI: Update pypy on Travis CI #2573 [hugovk] - ImageMorph: Fix wrong expected size of MRLs read from disk #2561 @@ -581,7 +1085,7 @@ Changelog (Pillow) - Doc: Reordered operating systems in Compatibility Matrix #2436 [radarhere] -- Test: Additional tests for BurfStub, Eps, Container, GribStub, IPTC, Wmf, XVThumb, ImageDraw, ImageMorph ImageShow #2425 +- Test: Additional tests for BufrStub, Eps, Container, GribStub, IPTC, Wmf, XVThumb, ImageDraw, ImageMorph, ImageShow #2425 [radarhere] - Health fixes #2437 @@ -713,10 +1217,10 @@ Changelog (Pillow) - Add center and translate option to Image.rotate. #2328 [lambdafu] -- Test: Relax WMF test condition, fixes #2323 +- Test: Relax WMF test condition, fixes #2323. #2327 [wiredfool] -- Allow 0 size images, Fixes #2259, Reverts to pre-3.4 behavior. +- Allow 0 size images, Fixes #2259, Reverts to pre-3.4 behavior. #2262 [wiredfool] - SGI: Save uncompressed SGI/BW/RGB/RGBA files #2325 @@ -1047,10 +1551,10 @@ Changelog (Pillow) 3.3.2 (2016-10-03) ------------------ -- Fix negative image sizes in Storage.c #2105 +- Fix negative image sizes in Storage.c #2146 [wiredfool] -- Fix integer overflow in map.c #2105 +- Fix integer overflow in map.c #2146 [wiredfool] 3.3.1 (2016-08-18) @@ -1188,7 +1692,7 @@ Changelog (Pillow) - Skip tests that require libtiff if it is not installed #1893 (fixes #1866) [wiredfool] -- Skip test when icc profile is not available, fixes #1887 +- Skip test when icc profile is not available, fixes #1887. #1892 [doko42] - Make deprecated functions raise NotImplementedError instead of Exception. #1862, #1890 @@ -1793,7 +2297,7 @@ Changelog (Pillow) 2.8.1 (2015-04-02) ------------------ -- Bug fix: Catch struct.error on invalid JPEG, fixes #1163 +- Bug fix: Catch struct.error on invalid JPEG, fixes #1163. #1165 [wiredfool, hugovk] 2.8.0 (2015-04-01) @@ -1928,7 +2432,7 @@ Changelog (Pillow) - Updated manifest #957 [wiredfool] -- Fix PyPy 2.4 regression #952 +- Fix PyPy 2.4 regression #958 [wiredfool] - Webp Metadata Skip Test comments #954 @@ -1970,7 +2474,7 @@ Changelog (Pillow) - Use redistributable ICC profiles for testing, skip if not available #923 [wiredfool] -- Additional documentation for JPEG info and save options #890 +- Additional documentation for JPEG info and save options #922 [wiredfool] - Fix JPEG Encoding memory leak when exif or qtables were specified #921 @@ -3538,7 +4042,7 @@ Pre-fork (1.1.3 final released) + Made setup.py look for old versions of zlib. For some back- - ground, see: http://www.gzip.org/zlib/advisory-2002-03-11.txt + ground, see: https://zlib.net/advisory-2002-03-11.txt (1.1.3c2 released) diff --git a/LICENSE b/LICENSE index 80456a753..c106eeb1a 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2018 by Alex Clark and contributors + Copyright © 2010-2019 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/MANIFEST.in b/MANIFEST.in index 865e51697..809d0d667 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,21 +11,20 @@ include LICENSE include Makefile graft Tests graft src -graft Tk graft depends graft winbuild graft docs prune docs/_static # build/src control detritus +exclude .appveyor.yml exclude .coveragerc -exclude codecov.yml +exclude .codecov.yml exclude .editorconfig exclude .landscape.yaml +exclude .readthedocs.yml exclude .travis exclude .travis/* -exclude appveyor.yml -exclude build_children.sh exclude tox.ini global-exclude .git* global-exclude *.pyc diff --git a/Makefile b/Makefile index 763eec4ac..29ec51b9a 100644 --- a/Makefile +++ b/Makefile @@ -94,7 +94,7 @@ test: typecheck: mypy --ignore-missing-imports -2 src/PIL/Image.py -# https://docs.python.org/2/distutils/packageindex.html#the-pypirc-file +# https://docs.python.org/3/distutils/packageindex.html#the-pypirc-file upload-test: # [test] # username: diff --git a/README.rst b/README.rst index e217ba29d..381859c18 100644 --- a/README.rst +++ b/README.rst @@ -14,11 +14,11 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors 2.9.0" Pillow - $ git push -``` + ```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 + ``` * [ ] Download distributions from the [Pillow Wheel Builder container](http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com/). - + ```bash + wget -m -A 'Pillow-*' \ + http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com + ``` ## Publicize Release -* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/aclark4life/status/583366798302691328. +* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 ## Documentation -* [ ] Make sure the default version for Read the Docs is the latest release version, e.g. ``3.1.x`` rather than ``latest``: https://readthedocs.org/projects/pillow/versions/ +* [ ] Make sure the default version for Read the Docs is the latest tagged release e.g. `d2d43879` (5.4.0) diff --git a/Tests/README.rst b/Tests/README.rst index 44f6f4792..d64a34bd7 100644 --- a/Tests/README.rst +++ b/Tests/README.rst @@ -13,28 +13,20 @@ Install:: Execution --------- -**If Pillow has been built in-place** - To run an individual test:: - python Tests/test_image.py + pytest Tests/test_image.py -Run all the tests from the root of the Pillow source distribution:: +Or:: - pytest -vx Tests - -Or with coverage:: - - pytest -vx --cov PIL --cov-report term Tests - coverage html - open htmlcov/index.html - -**If Pillow has been installed** - -To run an individual test:: - - pytest -k Tests/test_image.py + pytest -k test_image.py Run all the tests from the root of the Pillow source distribution:: pytest + +Or with coverage:: + + pytest --cov PIL --cov-report term + coverage html + open htmlcov/index.html diff --git a/Tests/__init__.py b/Tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 300017168..79427dca3 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import unittest, PillowTestCase, hopper # Not running this test by default. No DOS against Travis CI. diff --git a/Tests/bench_get.py b/Tests/bench_get.py index 51f3a6aa2..68ac2c9a2 100644 --- a/Tests/bench_get.py +++ b/Tests/bench_get.py @@ -1,4 +1,4 @@ -import helper +from . import helper import timeit import sys @@ -14,6 +14,7 @@ def bench(mode): get(xy) print(mode, timeit.default_timer() - t0, "us") + bench("L") bench("I") bench("I;16") diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 9b370da3c..3f7c58015 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase from PIL import Image TEST_FILE = "Tests/images/fli_overflow.fli" diff --git a/Tests/check_icns_dos.py b/Tests/check_icns_dos.py index e56709bbb..d4c3cf7cb 100644 --- a/Tests/check_icns_dos.py +++ b/Tests/check_icns_dos.py @@ -2,10 +2,11 @@ # Run from anywhere that PIL is importable. from PIL import Image +from PIL._util import py3 from io import BytesIO -if bytes is str: - Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00'))) -else: +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'))) diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index a31cd2180..7fa0663e8 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,7 +1,7 @@ #!/usr/bin/env python from __future__ import division -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase import sys from PIL import Image @@ -9,7 +9,7 @@ min_iterations = 100 max_iterations = 10000 -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or macOS") class TestImagingLeaks(PillowTestCase): def _get_mem_usage(self): @@ -25,9 +25,8 @@ class TestImagingLeaks(PillowTestCase): if i < min_iterations: mem_limit = mem + 1 continue - self.assertLessEqual(mem, mem_limit, - 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)) @@ -40,5 +39,6 @@ class TestImagingLeaks(PillowTestCase): # Pass a new list at each iteration. lambda: im.point(range(256))) + if __name__ == '__main__': unittest.main() diff --git a/Tests/check_j2k_dos.py b/Tests/check_j2k_dos.py index 9f06888a3..4ea31cec2 100644 --- a/Tests/check_j2k_dos.py +++ b/Tests/check_j2k_dos.py @@ -2,12 +2,14 @@ # Run from anywhere that PIL is importable. from PIL import Image +from PIL._util import py3 from io import BytesIO -if bytes is str: - Image.open(BytesIO(bytes( - '\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang'))) -else: +if py3: Image.open(BytesIO(bytes( '\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang', 'latin-1'))) + +else: + Image.open(BytesIO(bytes( + '\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang'))) diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index 8e9c4ca20..d87b7f041 100755 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase import sys from PIL import Image from io import BytesIO @@ -11,7 +11,7 @@ codecs = dir(Image.core) test_file = "Tests/images/rgb_trns_ycbc.jp2" -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or macOS") class TestJpegLeaks(PillowTestCase): def setUp(self): if "jpeg2k_encoder" not in codecs or "jpeg2k_decoder" not in codecs: diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index bec4ea694..f456ebb32 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,5 +1,5 @@ from PIL import Image -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase class TestJ2kEncodeOverflow(PillowTestCase): @@ -10,5 +10,6 @@ class TestJ2kEncodeOverflow(PillowTestCase): with self.assertRaises(IOError): im.save(target) + if __name__ == '__main__': unittest.main() diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 7df2dfcc4..97a5650e0 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import unittest, PillowTestCase, hopper from io import BytesIO import sys @@ -9,13 +9,12 @@ iterations = 5000 When run on a system without the jpeg leak fixes, the valgrind runs look like this. -NOSE_PROCESSES=0 NOSE_TIMEOUT=600 valgrind --tool=massif \ - python test-installed.py -s -v Tests/check_jpeg_leaks.py +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(sys.platform.startswith('win32'), "requires Unix or macOS") class TestJpegLeaks(PillowTestCase): """ diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index ef0cd1f80..24d687ea6 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,6 +1,6 @@ import sys -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase # This test is not run automatically. # @@ -21,7 +21,7 @@ class LargeMemoryTest(PillowTestCase): def _write_png(self, xdim, ydim): f = self.tempfile('temp.png') - im = Image.new('L', (xdim, ydim), (0)) + im = Image.new('L', (xdim, ydim), 0) im.save(f) def test_large(self): diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index e48d98367..b66988fd5 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,6 +1,6 @@ import sys -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase # This test is not run automatically. # diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index 6611648a5..f8c4a3090 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase from PIL import Image TEST_FILE = "Tests/images/libtiff_segfault.tif" diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 8998f8c0f..9a446bf84 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase from PIL import Image, PngImagePlugin, ImageFile from io import BytesIO import zlib @@ -36,7 +36,7 @@ class TestPngDos(PillowTestCase): def test_dos_total_memory(self): im = Image.new('L', (1, 1)) - compressed_data = zlib.compress('a'*1024*1023) + compressed_data = zlib.compress(b'a'*1024*1023) info = PngImagePlugin.PngInfo() @@ -60,5 +60,6 @@ class TestPngDos(PillowTestCase): self.assertLess(total_len, 64*1024*1024, "Total text chunks greater than 64M") + if __name__ == '__main__': unittest.main() diff --git a/Tests/check_webp_leaks.py b/Tests/check_webp_leaks.py deleted file mode 100644 index 0f54f382d..000000000 --- a/Tests/check_webp_leaks.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import division -from helper import unittest, PillowTestCase -import sys -from PIL import Image -from io import BytesIO - -# Limits for testing the leak -mem_limit = 16 # max increase in MB -iterations = 5000 -test_file = "Tests/images/hopper.webp" - - -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") -class TestWebPLeaks(PillowTestCase): - - def setUp(self): - try: - from PIL import _webp - except ImportError: - self.skipTest('WebP support not installed') - - def _get_mem_usage(self): - from resource import getpagesize, getrusage, RUSAGE_SELF - mem = getrusage(RUSAGE_SELF).ru_maxrss - return mem * getpagesize() / 1024 / 1024 - - def test_leak_load(self): - with open(test_file, 'rb') as f: - im_data = f.read() - start_mem = self._get_mem_usage() - for _ in range(iterations): - with Image.open(BytesIO(im_data)) as im: - im.load() - mem = (self._get_mem_usage() - start_mem) - self.assertLess(mem, mem_limit, msg='memory usage limit exceeded') - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index 720fd0067..bace38a23 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -2,7 +2,6 @@ from __future__ import print_function import base64 import os -import sys if __name__ == "__main__": # create font data chunk for embedding @@ -10,7 +9,9 @@ if __name__ == "__main__": print(" f._load_pilfont_data(") print(" # %s" % os.path.basename(font)) print(" BytesIO(base64.decodestring(b'''") - base64.encode(open(font + ".pil", "rb"), sys.stdout) + with open(font + ".pil", "rb") as fp: + print(base64.b64encode(fp.read()).decode()) print("''')), Image.open(BytesIO(base64.decodestring(b'''") - base64.encode(open(font + ".pbm", "rb"), sys.stdout) + with open(font + ".pbm", "rb") as fp: + print(base64.b64encode(fp.read()).decode()) print("'''))))") diff --git a/Tests/helper.py b/Tests/helper.py index fdeb00c0c..b47604a60 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -8,6 +8,7 @@ import os import unittest from PIL import Image, ImageMath +from PIL._util import py3 import logging logger = logging.getLogger(__name__) @@ -16,8 +17,9 @@ logger = logging.getLogger(__name__) HAS_UPLOADER = False if os.environ.get('SHOW_ERRORS', None): - # local img.show for errors. - HAS_UPLOADER=True + # local img.show for errors. + HAS_UPLOADER = True + class test_image_results: @classmethod def upload(self, a, b): @@ -31,7 +33,6 @@ else: pass - def convert_to_comparable(a, b): new_a, new_b = a, b if a.mode == 'P': @@ -52,10 +53,6 @@ class PillowTestCase(unittest.TestCase): # holds last result object passed to run method: self.currentResult = None - # Nicer output for --verbose - def __str__(self): - return self.__class__.__name__ + "." + self._testMethodName - def run(self, result=None): self.currentResult = result # remember result for use later unittest.TestCase.run(self, result) # call superclass run method @@ -83,7 +80,7 @@ class PillowTestCase(unittest.TestCase): self.assertTrue( all(x == y for x, y in zip(a, b)), msg or "got %s, expected %s" % (a, b)) - except: + except Exception: self.assertEqual(a, b, msg) def assert_image(self, im, mode, size, msg=None): @@ -109,7 +106,7 @@ class PillowTestCase(unittest.TestCase): try: url = test_image_results.upload(a, b) logger.error("Url for test images: %s" % url) - except Exception as msg: + except Exception: pass self.fail(msg or "got different content") @@ -119,7 +116,7 @@ class PillowTestCase(unittest.TestCase): if mode: img = img.convert(mode) self.assert_image_equal(a, img, msg) - + def assert_image_similar(self, a, b, epsilon, msg=None): epsilon = float(epsilon) self.assertEqual( @@ -148,11 +145,12 @@ class PillowTestCase(unittest.TestCase): try: url = test_image_results.upload(a, b) logger.error("Url for test images: %s" % url) - except: + except Exception: 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) @@ -161,7 +159,6 @@ class PillowTestCase(unittest.TestCase): def assert_warning(self, warn_class, func, *args, **kwargs): import warnings - result = None with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") @@ -173,7 +170,7 @@ class PillowTestCase(unittest.TestCase): if warn_class is None: self.assertEqual(len(w), 0, "Expected no warnings, got %s" % - list(v.category for v in w)) + [v.category for v in w]) else: self.assertGreaterEqual(len(w), 1) found = False @@ -185,10 +182,20 @@ class PillowTestCase(unittest.TestCase): return result def assert_all_same(self, items, msg=None): - self.assertTrue(items.count(items[0]) == len(items), msg) + self.assertEqual(items.count(items[0]), len(items), msg) def assert_not_all_same(self, items, msg=None): - self.assertFalse(items.count(items[0]) == len(items), msg) + self.assertNotEqual(items.count(items[0]), len(items), msg) + + def assert_tuple_approx_equal(self, actuals, targets, threshold, msg): + """Tests if actuals has values within threshold from targets""" + + value = True + for i, target in enumerate(targets): + value *= (target - threshold <= actuals[i] <= target + threshold) + + self.assertTrue(value, + msg + ': ' + repr(actuals) + ' != ' + repr(targets)) def skipKnownBadTest(self, msg=None, platform=None, travis=None, interpreter=None): @@ -227,26 +234,27 @@ class PillowTestCase(unittest.TestCase): raise IOError() -@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/osx + # requires unix/macOS iterations = 100 # count mem_limit = 512 # k def _get_mem_usage(self): """ Gets the RUSAGE memory usage, returns in K. Encapsulates the difference - between OSX and Linux rss reporting + between macOS and Linux rss reporting - :returns; memory usage in kilobytes + :returns: memory usage in kilobytes """ from resource import getrusage, RUSAGE_SELF mem = getrusage(RUSAGE_SELF).ru_maxrss if sys.platform == 'darwin': # man 2 getrusage: - # ru_maxrss the maximum resident set size utilized (in bytes). - return mem / 1024 # Kb + # ru_maxrss + # This is the maximum resident set size utilized (in bytes). + return mem / 1024 # Kb else: # linux # man 2 getrusage @@ -259,13 +267,16 @@ class PillowLeakTestCase(PillowTestCase): for cycle in range(self.iterations): core() mem = (self._get_mem_usage() - start_mem) - self.assertLess(mem, self.mem_limit, - msg='memory usage limit exceeded in iteration %d' % cycle) + msg = 'memory usage limit exceeded in iteration %d' % cycle + self.assertLess(mem, self.mem_limit, msg) # helpers -py3 = (sys.version_info >= (3, 0)) +if not py3: + # Remove DeprecationWarning in Python 3 + PillowTestCase.assertRaisesRegex = PillowTestCase.assertRaisesRegexp + PillowTestCase.assertRegex = PillowTestCase.assertRegexpMatches def fromstring(data): diff --git a/Tests/images/16_bit_noise.tif b/Tests/images/16_bit_noise.tif new file mode 100644 index 000000000..19180638e Binary files /dev/null and b/Tests/images/16_bit_noise.tif differ diff --git a/Tests/images/a_fli.png b/Tests/images/a_fli.png new file mode 100644 index 000000000..93c3f1b12 Binary files /dev/null and b/Tests/images/a_fli.png differ diff --git a/Tests/images/balloon.jpf b/Tests/images/balloon.jpf new file mode 100644 index 000000000..767eab5dd Binary files /dev/null and b/Tests/images/balloon.jpf differ diff --git a/Tests/images/blp/blp1_jpeg.blp b/Tests/images/blp/blp1_jpeg.blp new file mode 100644 index 000000000..bdf7146ed Binary files /dev/null and b/Tests/images/blp/blp1_jpeg.blp differ diff --git a/Tests/images/blp/blp2_dxt1.blp b/Tests/images/blp/blp2_dxt1.blp new file mode 100644 index 000000000..73c0c91b5 Binary files /dev/null and b/Tests/images/blp/blp2_dxt1.blp differ diff --git a/Tests/images/blp/blp2_dxt1.png b/Tests/images/blp/blp2_dxt1.png new file mode 100644 index 000000000..f2a24618a Binary files /dev/null and b/Tests/images/blp/blp2_dxt1.png differ diff --git a/Tests/images/blp/blp2_dxt1a.blp b/Tests/images/blp/blp2_dxt1a.blp new file mode 100644 index 000000000..5bedc27d6 Binary files /dev/null and b/Tests/images/blp/blp2_dxt1a.blp differ diff --git a/Tests/images/blp/blp2_dxt1a.png b/Tests/images/blp/blp2_dxt1a.png new file mode 100644 index 000000000..d2cdea807 Binary files /dev/null and b/Tests/images/blp/blp2_dxt1a.png differ diff --git a/Tests/images/blp/blp2_raw.blp b/Tests/images/blp/blp2_raw.blp new file mode 100644 index 000000000..813d4bfae Binary files /dev/null and b/Tests/images/blp/blp2_raw.blp differ diff --git a/Tests/images/blp/blp2_raw.png b/Tests/images/blp/blp2_raw.png new file mode 100644 index 000000000..c77a3c048 Binary files /dev/null and b/Tests/images/blp/blp2_raw.png differ diff --git a/Tests/images/bw_gradient.png b/Tests/images/bw_gradient.png new file mode 100644 index 000000000..79c921486 Binary files /dev/null and b/Tests/images/bw_gradient.png differ diff --git a/Tests/images/exif-ifd-offset.jpg b/Tests/images/exif-ifd-offset.jpg new file mode 100644 index 000000000..e5dfc6807 Binary files /dev/null and b/Tests/images/exif-ifd-offset.jpg differ diff --git a/Tests/images/hopper.gd b/Tests/images/hopper.gd new file mode 100644 index 000000000..82d2408f9 Binary files /dev/null and b/Tests/images/hopper.gd differ diff --git a/Tests/images/hopper.wal b/Tests/images/hopper.wal new file mode 100644 index 000000000..f6260c6b3 Binary files /dev/null and b/Tests/images/hopper.wal differ diff --git a/Tests/images/hopper_idat_after_image_end.png b/Tests/images/hopper_idat_after_image_end.png new file mode 100644 index 000000000..70b4a6400 Binary files /dev/null and b/Tests/images/hopper_idat_after_image_end.png differ diff --git a/Tests/images/hopper_unknown_pixel_mode.tif b/Tests/images/hopper_unknown_pixel_mode.tif new file mode 100644 index 000000000..89a8c5e17 Binary files /dev/null and b/Tests/images/hopper_unknown_pixel_mode.tif differ diff --git a/Tests/images/hopper_webp_bits.ppm b/Tests/images/hopper_webp_bits.ppm index 6dce2da2e..f431bc7b1 100644 Binary files a/Tests/images/hopper_webp_bits.ppm and b/Tests/images/hopper_webp_bits.ppm differ diff --git a/Tests/images/hopper_zero_comment_subblocks.gif b/Tests/images/hopper_zero_comment_subblocks.gif new file mode 100644 index 000000000..5f482c042 Binary files /dev/null and b/Tests/images/hopper_zero_comment_subblocks.gif differ diff --git a/Tests/images/imagedraw2_text.png b/Tests/images/imagedraw2_text.png new file mode 100644 index 000000000..b22e6545b Binary files /dev/null and b/Tests/images/imagedraw2_text.png differ diff --git a/Tests/images/imagedraw_arc_width.png b/Tests/images/imagedraw_arc_width.png new file mode 100644 index 000000000..ff3f1f0b2 Binary files /dev/null and b/Tests/images/imagedraw_arc_width.png differ diff --git a/Tests/images/imagedraw_arc_width_fill.png b/Tests/images/imagedraw_arc_width_fill.png new file mode 100644 index 000000000..9572a6059 Binary files /dev/null and b/Tests/images/imagedraw_arc_width_fill.png differ diff --git a/Tests/images/imagedraw_chord_width.png b/Tests/images/imagedraw_chord_width.png new file mode 100644 index 000000000..33a59b487 Binary files /dev/null and b/Tests/images/imagedraw_chord_width.png differ diff --git a/Tests/images/imagedraw_chord_width_fill.png b/Tests/images/imagedraw_chord_width_fill.png new file mode 100644 index 000000000..809c3ea1c Binary files /dev/null and b/Tests/images/imagedraw_chord_width_fill.png differ diff --git a/Tests/images/imagedraw_ellipse_width.png b/Tests/images/imagedraw_ellipse_width.png new file mode 100644 index 000000000..ec0ca6731 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_width.png differ diff --git a/Tests/images/imagedraw_ellipse_width_fill.png b/Tests/images/imagedraw_ellipse_width_fill.png new file mode 100644 index 000000000..9b7be6029 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_width_fill.png differ diff --git a/Tests/images/imagedraw_floodfill_L.png b/Tests/images/imagedraw_floodfill_L.png new file mode 100644 index 000000000..4139e66d8 Binary files /dev/null and b/Tests/images/imagedraw_floodfill_L.png differ diff --git a/Tests/images/imagedraw_floodfill.png b/Tests/images/imagedraw_floodfill_RGB.png similarity index 100% rename from Tests/images/imagedraw_floodfill.png rename to Tests/images/imagedraw_floodfill_RGB.png diff --git a/Tests/images/imagedraw_floodfill_RGBA.png b/Tests/images/imagedraw_floodfill_RGBA.png new file mode 100644 index 000000000..5e02064d4 Binary files /dev/null and b/Tests/images/imagedraw_floodfill_RGBA.png differ diff --git a/Tests/images/imagedraw_line_joint_curve.png b/Tests/images/imagedraw_line_joint_curve.png new file mode 100644 index 000000000..ad729f528 Binary files /dev/null and b/Tests/images/imagedraw_line_joint_curve.png differ diff --git a/Tests/images/imagedraw_outline_chord_L.png b/Tests/images/imagedraw_outline_chord_L.png new file mode 100644 index 000000000..9c20ad217 Binary files /dev/null and b/Tests/images/imagedraw_outline_chord_L.png differ diff --git a/Tests/images/imagedraw_outline_chord_RGB.png b/Tests/images/imagedraw_outline_chord_RGB.png new file mode 100644 index 000000000..9e9cb5af0 Binary files /dev/null and b/Tests/images/imagedraw_outline_chord_RGB.png differ diff --git a/Tests/images/imagedraw_outline_ellipse_L.png b/Tests/images/imagedraw_outline_ellipse_L.png new file mode 100644 index 000000000..53b76b62b Binary files /dev/null and b/Tests/images/imagedraw_outline_ellipse_L.png differ diff --git a/Tests/images/imagedraw_outline_ellipse_RGB.png b/Tests/images/imagedraw_outline_ellipse_RGB.png new file mode 100644 index 000000000..37a519327 Binary files /dev/null and b/Tests/images/imagedraw_outline_ellipse_RGB.png differ diff --git a/Tests/images/imagedraw_outline_pieslice_L.png b/Tests/images/imagedraw_outline_pieslice_L.png new file mode 100644 index 000000000..92972d54c Binary files /dev/null and b/Tests/images/imagedraw_outline_pieslice_L.png differ diff --git a/Tests/images/imagedraw_outline_pieslice_RGB.png b/Tests/images/imagedraw_outline_pieslice_RGB.png new file mode 100644 index 000000000..4be4fc4af Binary files /dev/null and b/Tests/images/imagedraw_outline_pieslice_RGB.png differ diff --git a/Tests/images/imagedraw_outline_polygon_L.png b/Tests/images/imagedraw_outline_polygon_L.png new file mode 100644 index 000000000..57ed9d43b Binary files /dev/null and b/Tests/images/imagedraw_outline_polygon_L.png differ diff --git a/Tests/images/imagedraw_outline_polygon_RGB.png b/Tests/images/imagedraw_outline_polygon_RGB.png new file mode 100644 index 000000000..286b71c23 Binary files /dev/null and b/Tests/images/imagedraw_outline_polygon_RGB.png differ diff --git a/Tests/images/imagedraw_outline_rectangle_L.png b/Tests/images/imagedraw_outline_rectangle_L.png new file mode 100644 index 000000000..b9c47018f Binary files /dev/null and b/Tests/images/imagedraw_outline_rectangle_L.png differ diff --git a/Tests/images/imagedraw_outline_rectangle_RGB.png b/Tests/images/imagedraw_outline_rectangle_RGB.png new file mode 100644 index 000000000..41b92fb75 Binary files /dev/null and b/Tests/images/imagedraw_outline_rectangle_RGB.png differ diff --git a/Tests/images/imagedraw_outline_shape_L.png b/Tests/images/imagedraw_outline_shape_L.png new file mode 100644 index 000000000..20ebef157 Binary files /dev/null and b/Tests/images/imagedraw_outline_shape_L.png differ diff --git a/Tests/images/imagedraw_outline_shape_RGB.png b/Tests/images/imagedraw_outline_shape_RGB.png new file mode 100644 index 000000000..6fb6f623e Binary files /dev/null and b/Tests/images/imagedraw_outline_shape_RGB.png differ diff --git a/Tests/images/imagedraw_pieslice_width.png b/Tests/images/imagedraw_pieslice_width.png new file mode 100644 index 000000000..3bd69222c Binary files /dev/null and b/Tests/images/imagedraw_pieslice_width.png differ diff --git a/Tests/images/imagedraw_pieslice_width_fill.png b/Tests/images/imagedraw_pieslice_width_fill.png new file mode 100644 index 000000000..c5a34e0f3 Binary files /dev/null and b/Tests/images/imagedraw_pieslice_width_fill.png differ diff --git a/Tests/images/imagedraw_rectangle_width.png b/Tests/images/imagedraw_rectangle_width.png new file mode 100644 index 000000000..e39659921 Binary files /dev/null and b/Tests/images/imagedraw_rectangle_width.png differ diff --git a/Tests/images/imagedraw_rectangle_width_fill.png b/Tests/images/imagedraw_rectangle_width_fill.png new file mode 100644 index 000000000..d5243c608 Binary files /dev/null and b/Tests/images/imagedraw_rectangle_width_fill.png differ diff --git a/Tests/images/imageops_pad_h_0.jpg b/Tests/images/imageops_pad_h_0.jpg new file mode 100644 index 000000000..f9fcb1cdb Binary files /dev/null and b/Tests/images/imageops_pad_h_0.jpg differ diff --git a/Tests/images/imageops_pad_h_1.jpg b/Tests/images/imageops_pad_h_1.jpg new file mode 100644 index 000000000..4b9b9ebc4 Binary files /dev/null and b/Tests/images/imageops_pad_h_1.jpg differ diff --git a/Tests/images/imageops_pad_h_2.jpg b/Tests/images/imageops_pad_h_2.jpg new file mode 100644 index 000000000..2c8224892 Binary files /dev/null and b/Tests/images/imageops_pad_h_2.jpg differ diff --git a/Tests/images/imageops_pad_v_0.jpg b/Tests/images/imageops_pad_v_0.jpg new file mode 100644 index 000000000..caf435796 Binary files /dev/null and b/Tests/images/imageops_pad_v_0.jpg differ diff --git a/Tests/images/imageops_pad_v_1.jpg b/Tests/images/imageops_pad_v_1.jpg new file mode 100644 index 000000000..4a6698e91 Binary files /dev/null and b/Tests/images/imageops_pad_v_1.jpg differ diff --git a/Tests/images/imageops_pad_v_2.jpg b/Tests/images/imageops_pad_v_2.jpg new file mode 100644 index 000000000..792952bcd Binary files /dev/null and b/Tests/images/imageops_pad_v_2.jpg differ diff --git a/Tests/images/iss634.apng b/Tests/images/iss634.apng new file mode 100644 index 000000000..89e025906 Binary files /dev/null and b/Tests/images/iss634.apng differ diff --git a/Tests/images/la.tga b/Tests/images/la.tga new file mode 100644 index 000000000..8c83104ed Binary files /dev/null and b/Tests/images/la.tga differ diff --git a/Tests/images/rotate_45_no_fill.png b/Tests/images/rotate_45_no_fill.png new file mode 100644 index 000000000..3c9d03e6c Binary files /dev/null and b/Tests/images/rotate_45_no_fill.png differ diff --git a/Tests/images/rotate_45_with_fill.png b/Tests/images/rotate_45_with_fill.png new file mode 100644 index 000000000..05b2d34d5 Binary files /dev/null and b/Tests/images/rotate_45_with_fill.png differ diff --git a/Tests/images/sugarshack_ifd_offset.mpo b/Tests/images/sugarshack_ifd_offset.mpo new file mode 100644 index 000000000..2dcac876f Binary files /dev/null and b/Tests/images/sugarshack_ifd_offset.mpo differ diff --git a/Tests/images/tga/common/1x1_l.png b/Tests/images/tga/common/1x1_l.png new file mode 100644 index 000000000..d1a2cb813 Binary files /dev/null and b/Tests/images/tga/common/1x1_l.png differ diff --git a/Tests/images/tga/common/1x1_l_bl_raw.tga b/Tests/images/tga/common/1x1_l_bl_raw.tga new file mode 100644 index 000000000..c79e125ea Binary files /dev/null and b/Tests/images/tga/common/1x1_l_bl_raw.tga differ diff --git a/Tests/images/tga/common/1x1_l_bl_rle.tga b/Tests/images/tga/common/1x1_l_bl_rle.tga new file mode 100644 index 000000000..ee1a7d2d8 Binary files /dev/null and b/Tests/images/tga/common/1x1_l_bl_rle.tga differ diff --git a/Tests/images/tga/common/1x1_l_tl_raw.tga b/Tests/images/tga/common/1x1_l_tl_raw.tga new file mode 100644 index 000000000..6c9968758 Binary files /dev/null and b/Tests/images/tga/common/1x1_l_tl_raw.tga differ diff --git a/Tests/images/tga/common/1x1_l_tl_rle.tga b/Tests/images/tga/common/1x1_l_tl_rle.tga new file mode 100644 index 000000000..efd4e3af4 Binary files /dev/null and b/Tests/images/tga/common/1x1_l_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_l.png b/Tests/images/tga/common/200x32_l.png new file mode 100644 index 000000000..ff37cbe30 Binary files /dev/null and b/Tests/images/tga/common/200x32_l.png differ diff --git a/Tests/images/tga/common/200x32_l_bl_raw.tga b/Tests/images/tga/common/200x32_l_bl_raw.tga new file mode 100644 index 000000000..e629910a0 Binary files /dev/null and b/Tests/images/tga/common/200x32_l_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_l_bl_rle.tga b/Tests/images/tga/common/200x32_l_bl_rle.tga new file mode 100644 index 000000000..2e6f9377b Binary files /dev/null and b/Tests/images/tga/common/200x32_l_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_l_tl_raw.tga b/Tests/images/tga/common/200x32_l_tl_raw.tga new file mode 100644 index 000000000..f9ed8b9c2 Binary files /dev/null and b/Tests/images/tga/common/200x32_l_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_l_tl_rle.tga b/Tests/images/tga/common/200x32_l_tl_rle.tga new file mode 100644 index 000000000..03c797e53 Binary files /dev/null and b/Tests/images/tga/common/200x32_l_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_la.png b/Tests/images/tga/common/200x32_la.png new file mode 100644 index 000000000..a8c4f274f Binary files /dev/null and b/Tests/images/tga/common/200x32_la.png differ diff --git a/Tests/images/tga/common/200x32_la_bl_raw.tga b/Tests/images/tga/common/200x32_la_bl_raw.tga new file mode 100644 index 000000000..afdc97151 Binary files /dev/null and b/Tests/images/tga/common/200x32_la_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_la_bl_rle.tga b/Tests/images/tga/common/200x32_la_bl_rle.tga new file mode 100644 index 000000000..9fb8b06ab Binary files /dev/null and b/Tests/images/tga/common/200x32_la_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_la_tl_raw.tga b/Tests/images/tga/common/200x32_la_tl_raw.tga new file mode 100644 index 000000000..6af1fa053 Binary files /dev/null and b/Tests/images/tga/common/200x32_la_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_la_tl_rle.tga b/Tests/images/tga/common/200x32_la_tl_rle.tga new file mode 100644 index 000000000..fce83e3cf Binary files /dev/null and b/Tests/images/tga/common/200x32_la_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_p.png b/Tests/images/tga/common/200x32_p.png new file mode 100644 index 000000000..a57a8a22a Binary files /dev/null and b/Tests/images/tga/common/200x32_p.png differ diff --git a/Tests/images/tga/common/200x32_p_bl_raw.tga b/Tests/images/tga/common/200x32_p_bl_raw.tga new file mode 100644 index 000000000..89145aa81 Binary files /dev/null and b/Tests/images/tga/common/200x32_p_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_p_bl_rle.tga b/Tests/images/tga/common/200x32_p_bl_rle.tga new file mode 100644 index 000000000..bc53f2f93 Binary files /dev/null and b/Tests/images/tga/common/200x32_p_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_p_tl_raw.tga b/Tests/images/tga/common/200x32_p_tl_raw.tga new file mode 100644 index 000000000..247db20a2 Binary files /dev/null and b/Tests/images/tga/common/200x32_p_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_p_tl_rle.tga b/Tests/images/tga/common/200x32_p_tl_rle.tga new file mode 100644 index 000000000..3092ff923 Binary files /dev/null and b/Tests/images/tga/common/200x32_p_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_rgb.png b/Tests/images/tga/common/200x32_rgb.png new file mode 100644 index 000000000..6614141a5 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb.png differ diff --git a/Tests/images/tga/common/200x32_rgb_bl_raw.tga b/Tests/images/tga/common/200x32_rgb_bl_raw.tga new file mode 100644 index 000000000..ebcea6b03 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_rgb_bl_rle.tga b/Tests/images/tga/common/200x32_rgb_bl_rle.tga new file mode 100644 index 000000000..87eb71c75 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_rgb_tl_raw.tga b/Tests/images/tga/common/200x32_rgb_tl_raw.tga new file mode 100644 index 000000000..2122ffa10 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_rgb_tl_rle.tga b/Tests/images/tga/common/200x32_rgb_tl_rle.tga new file mode 100644 index 000000000..2122ffa10 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_rgba.png b/Tests/images/tga/common/200x32_rgba.png new file mode 100644 index 000000000..74def0b7c Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba.png differ diff --git a/Tests/images/tga/common/200x32_rgba_bl_raw.tga b/Tests/images/tga/common/200x32_rgba_bl_raw.tga new file mode 100644 index 000000000..148cc206a Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_rgba_bl_rle.tga b/Tests/images/tga/common/200x32_rgba_bl_rle.tga new file mode 100644 index 000000000..1727fe338 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_rgba_tl_raw.tga b/Tests/images/tga/common/200x32_rgba_tl_raw.tga new file mode 100644 index 000000000..92ab8940d Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_rgba_tl_rle.tga b/Tests/images/tga/common/200x32_rgba_tl_rle.tga new file mode 100644 index 000000000..2b593aee2 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba_tl_rle.tga differ diff --git a/Tests/images/tga/common/readme.txt b/Tests/images/tga/common/readme.txt new file mode 100644 index 000000000..4535d7fe6 --- /dev/null +++ b/Tests/images/tga/common/readme.txt @@ -0,0 +1,12 @@ +Images in this directory were created with GIMP. + +TGAs have names in the following format: + + {width}x{height}_{mode}_{origin}_{compression}.tga + +Where: + mode is PIL mode in lower case (L, P, RGB, etc.) + origin: + "bl" - bottom left + "tl" - top left + compression is either "raw" or "rle" diff --git a/Tests/images/tiff_strip_cmyk_jpeg.tif b/Tests/images/tiff_strip_cmyk_jpeg.tif new file mode 100644 index 000000000..0207d27c7 Binary files /dev/null and b/Tests/images/tiff_strip_cmyk_jpeg.tif differ diff --git a/Tests/images/tiff_strip_planar_raw.tif b/Tests/images/tiff_strip_planar_raw.tif new file mode 100644 index 000000000..ab8b3c3f3 Binary files /dev/null and b/Tests/images/tiff_strip_planar_raw.tif differ diff --git a/Tests/images/tiff_strip_planar_raw_with_overviews.tif b/Tests/images/tiff_strip_planar_raw_with_overviews.tif new file mode 100644 index 000000000..e032c5c36 Binary files /dev/null and b/Tests/images/tiff_strip_planar_raw_with_overviews.tif differ diff --git a/Tests/images/tiff_strip_raw.tif b/Tests/images/tiff_strip_raw.tif new file mode 100644 index 000000000..81bb42ce7 Binary files /dev/null and b/Tests/images/tiff_strip_raw.tif differ diff --git a/Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif b/Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif new file mode 100644 index 000000000..ca8b634bb Binary files /dev/null and b/Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif differ diff --git a/Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif b/Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif new file mode 100644 index 000000000..c3207e451 Binary files /dev/null and b/Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif differ diff --git a/Tests/images/tiff_tiled_cmyk_jpeg.tif b/Tests/images/tiff_tiled_cmyk_jpeg.tif new file mode 100644 index 000000000..0cc27b69c Binary files /dev/null and b/Tests/images/tiff_tiled_cmyk_jpeg.tif differ diff --git a/Tests/images/tiff_tiled_planar_raw.tif b/Tests/images/tiff_tiled_planar_raw.tif new file mode 100644 index 000000000..2e3ecc811 Binary files /dev/null and b/Tests/images/tiff_tiled_planar_raw.tif differ diff --git a/Tests/images/tiff_tiled_raw.tif b/Tests/images/tiff_tiled_raw.tif new file mode 100644 index 000000000..25803c395 Binary files /dev/null and b/Tests/images/tiff_tiled_raw.tif differ diff --git a/Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif b/Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif new file mode 100644 index 000000000..75ce833a1 Binary files /dev/null and b/Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif differ diff --git a/Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif b/Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif new file mode 100644 index 000000000..ff8b4a409 Binary files /dev/null and b/Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif differ diff --git a/Tests/images/truncated_jpeg.jpg b/Tests/images/truncated_jpeg.jpg new file mode 100644 index 000000000..f4fec450d Binary files /dev/null and b/Tests/images/truncated_jpeg.jpg differ diff --git a/Tests/make_hash.py b/Tests/make_hash.py index 4412f65be..c52e009ec 100644 --- a/Tests/make_hash.py +++ b/Tests/make_hash.py @@ -32,6 +32,7 @@ def check(size, i0): h[i] = m return h + min_start = 0 # 1) find the smallest table size with no collisions @@ -51,10 +52,5 @@ for i0 in range(65556): print() -# print(check(min_size, min_start)) - print("#define ACCESS_TABLE_SIZE", min_size) print("#define ACCESS_TABLE_HASH", min_start) - -# for m in modes: -# print(m, "=>", hash(m, min_start) % min_size) diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index 67aff8ecc..321a6b3ce 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase import PIL import PIL.Image @@ -9,7 +9,7 @@ class TestSanity(PillowTestCase): def test_sanity(self): # Make sure we have the binary extension - im = PIL.Image.core.new("L", (100, 100)) + PIL.Image.core.new("L", (100, 100)) self.assertEqual(PIL.Image.VERSION[:3], '1.1') @@ -19,12 +19,8 @@ class TestSanity(PillowTestCase): self.assertEqual(len(im.tobytes()), 1300) # Create images in all remaining major modes. - im = PIL.Image.new("L", (100, 100)) - im = PIL.Image.new("P", (100, 100)) - im = PIL.Image.new("RGB", (100, 100)) - im = PIL.Image.new("I", (100, 100)) - im = PIL.Image.new("F", (100, 100)) - - -if __name__ == '__main__': - unittest.main() + PIL.Image.new("L", (100, 100)) + PIL.Image.new("P", (100, 100)) + PIL.Image.new("RGB", (100, 100)) + PIL.Image.new("I", (100, 100)) + PIL.Image.new("F", (100, 100)) diff --git a/Tests/test_binary.py b/Tests/test_binary.py index 2fac9b3d5..bf9ba1e5f 100644 --- a/Tests/test_binary.py +++ b/Tests/test_binary.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import _binary @@ -22,6 +22,3 @@ class TestBinary(PillowTestCase): self.assertEqual(_binary.o16be(65535), b'\xff\xff') self.assertEqual(_binary.o32be(65535), b'\x00\x00\xff\xff') - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 8e84cc8f1..0e32c93dd 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,5 +1,5 @@ from __future__ import print_function -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image import os @@ -17,12 +17,15 @@ class TestBmpReference(PillowTestCase): """ These shouldn't crash/dos, but they shouldn't return anything either """ for f in self.get_files('b'): - try: - im = Image.open(f) - im.load() - except Exception: # as msg: - pass - # print("Bad Image %s: %s" %(f,msg)) + def open(f): + try: + im = Image.open(f) + im.load() + except Exception: # as msg: + pass + + # Assert that there is no unclosed file warning + self.assert_warning(None, open, f) def test_questionable(self): """ These shouldn't crash/dos, but it's not well defined that these @@ -43,11 +46,11 @@ class TestBmpReference(PillowTestCase): im = Image.open(f) im.load() if os.path.basename(f) not in supported: - print("Please add %s to the partially supported bmp specs." % f) + print("Please add %s to the partially supported" + " bmp specs." % f) except Exception: # as msg: if os.path.basename(f) in supported: raise - # print("Bad Image %s: %s" %(f,msg)) def test_good(self): """ These should all work. There's a set of target files in the @@ -100,7 +103,3 @@ class TestBmpReference(PillowTestCase): os.path.join(base, 'g', 'pal4rle.bmp')) if f not in unsupported: self.fail("Unsupported Image %s: %s" % (f, msg)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 622b842d0..b9939c5f2 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -1,6 +1,6 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase -from PIL import Image, ImageOps +from PIL import Image, ImageFilter sample = Image.new("L", (7, 5)) @@ -16,7 +16,7 @@ sample.putdata(sum([ class TestBoxBlurApi(PillowTestCase): def test_imageops_box_blur(self): - i = ImageOps.box_blur(sample, 1) + i = sample.filter(ImageFilter.BoxBlur(1)) self.assertEqual(i.mode, sample.mode) self.assertEqual(i.size, sample.size) self.assertIsInstance(i, Image.Image) @@ -215,7 +215,3 @@ class TestBoxBlur(PillowTestCase): passes=3, delta=1, ) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py new file mode 100644 index 000000000..97035c793 --- /dev/null +++ b/Tests/test_color_lut.py @@ -0,0 +1,521 @@ +from __future__ import division + +from array import array + +from PIL import Image, ImageFilter +from .helper import unittest, PillowTestCase + +try: + import numpy +except ImportError: + numpy = None + + +class TestColorLut3DCoreAPI(PillowTestCase): + def generate_identity_table(self, channels, size): + if isinstance(size, tuple): + size1D, size2D, size3D = size + else: + size1D, size2D, size3D = (size, size, size) + + table = [ + [ + r / float(size1D-1) if size1D != 1 else 0, + g / float(size2D-1) if size2D != 1 else 0, + b / float(size3D-1) if size3D != 1 else 0, + r / float(size1D-1) if size1D != 1 else 0, + g / float(size2D-1) if size2D != 1 else 0, + ][: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]) + + def test_wrong_args(self): + 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)) + + with self.assertRaisesRegex(ValueError, "image mode"): + 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)) + + with self.assertRaisesRegex(ValueError, "table_channels"): + 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)) + + with self.assertRaisesRegex(ValueError, "Table size"): + 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))) + + with self.assertRaisesRegex(ValueError, r"size1D \* size2D \* size3D"): + im.im.color_lut_3d('RGB', + Image.LINEAR, + 3, 2, 2, 2, [0, 0, 0] * 7) + + with self.assertRaisesRegex(ValueError, r"size1D \* size2D \* size3D"): + 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) + + with self.assertRaises(TypeError): + 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.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('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, (3, 65, 3))) + + 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)) + + 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)) + + 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)) + + 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)) + + 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)) + + 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(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('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)]) + + # 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)))) + + # 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))))) + + 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)]) + + # 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)))) + + 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)]) + + 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)]) + + # Reverse channels by splitting and using table + self.assert_image_equal( + Image.merge('RGB', im.split()[::-1]), + im._new(im.im.color_lut_3d('RGB', Image.LINEAR, + 3, 2, 2, 2, [ + 0, 0, 0, 0, 0, 1, + 0, 1, 0, 0, 1, 1, + + 1, 0, 0, 1, 0, 1, + 1, 1, 0, 1, 1, 1, + ]))) + + def test_overflow(self): + g = Image.linear_gradient('L') + im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), + g.transpose(Image.ROTATE_180)]) + + transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, + 3, 2, 2, 2, + [ + -1, -1, -1, 2, -1, -1, + -1, 2, -1, 2, 2, -1, + + -1, -1, 2, 2, -1, 2, + -1, 2, 2, 2, 2, 2, + ])).load() + self.assertEqual(transformed[0, 0], (0, 0, 255)) + self.assertEqual(transformed[50, 50], (0, 0, 255)) + self.assertEqual(transformed[255, 0], (0, 255, 255)) + self.assertEqual(transformed[205, 50], (0, 255, 255)) + self.assertEqual(transformed[0, 255], (255, 0, 0)) + self.assertEqual(transformed[50, 205], (255, 0, 0)) + self.assertEqual(transformed[255, 255], (255, 255, 0)) + self.assertEqual(transformed[205, 205], (255, 255, 0)) + + transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, + 3, 2, 2, 2, + [ + -3, -3, -3, 5, -3, -3, + -3, 5, -3, 5, 5, -3, + + -3, -3, 5, 5, -3, 5, + -3, 5, 5, 5, 5, 5, + ])).load() + self.assertEqual(transformed[0, 0], (0, 0, 255)) + self.assertEqual(transformed[50, 50], (0, 0, 255)) + self.assertEqual(transformed[255, 0], (0, 255, 255)) + self.assertEqual(transformed[205, 50], (0, 255, 255)) + self.assertEqual(transformed[0, 255], (255, 0, 0)) + self.assertEqual(transformed[50, 205], (255, 0, 0)) + self.assertEqual(transformed[255, 255], (255, 255, 0)) + self.assertEqual(transformed[205, 205], (255, 255, 0)) + + +class TestColorLut3DFilter(PillowTestCase): + def test_wrong_args(self): + with self.assertRaisesRegex(ValueError, "should be either an integer"): + ImageFilter.Color3DLUT("small", [1]) + + with self.assertRaisesRegex(ValueError, "should be either an integer"): + ImageFilter.Color3DLUT((11, 11), [1]) + + with self.assertRaisesRegex(ValueError, r"in \[2, 65\] range"): + ImageFilter.Color3DLUT((11, 11, 1), [1]) + + with self.assertRaisesRegex(ValueError, r"in \[2, 65\] range"): + ImageFilter.Color3DLUT((11, 11, 66), [1]) + + with self.assertRaisesRegex(ValueError, "table should have .+ items"): + ImageFilter.Color3DLUT((3, 3, 3), [1, 1, 1]) + + with self.assertRaisesRegex(ValueError, "table should have .+ items"): + ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 2) + + with self.assertRaisesRegex(ValueError, "should have a length of 4"): + ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 27, channels=4) + + with self.assertRaisesRegex(ValueError, "should have a length of 3"): + ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8) + + with self.assertRaisesRegex(ValueError, "Only 3 or 4 output"): + ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2) + + def test_convert_table(self): + lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) + self.assertEqual(tuple(lut.size), (2, 2, 2)) + self.assertEqual(lut.name, "Color 3D LUT") + + 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)]) + 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) + self.assertEqual(tuple(lut.size), (2, 2, 2)) + self.assertEqual(lut.table, list(range(4)) * 8) + + @unittest.skipIf(numpy is None, "Numpy is not installed") + def test_numpy_sources(self): + table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) + with self.assertRaisesRegex(ValueError, "should have either channels"): + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + + table = numpy.ones((7, 6, 5, 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + self.assertIsInstance(lut.table, numpy.ndarray) + self.assertEqual(lut.table.dtype, table.dtype) + self.assertEqual(lut.table.shape, (table.size,)) + + table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + self.assertEqual(lut.table.shape, (table.size,)) + + table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + self.assertEqual(lut.table.shape, (table.size,)) + + # Check application + Image.new('RGB', (10, 10), 0).filter(lut) + + # Check copy + table[0] = 33 + self.assertEqual(lut.table[0], 1) + + # Check not copy + table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table, _copy_table=False) + table[0] = 33 + self.assertEqual(lut.table[0], 33) + + @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)]) + + 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)) + 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.float16) + self.assert_image_equal(im, im.filter(lut)) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), + lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float32) + self.assert_image_equal(im, im.filter(lut)) + + 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.table = numpy.array(lut.table, dtype=numpy.int32) + im.filter(lut) + lut.table = numpy.array(lut.table, dtype=numpy.int8) + im.filter(lut) + + def test_repr(self): + lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) + 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) + self.assertEqual( + 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)) + + 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)) + + 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") + 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]) + + def test_4_channels(self): + lut = ImageFilter.Color3DLUT.generate( + 5, channels=4, callback=lambda r, g, b: (b, r, g, (r+g+b) / 2)) + self.assertEqual(tuple(lut.size), (5, 5, 5)) + self.assertEqual(lut.name, "Color 3D LUT") + 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 + ]) + + 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)]) + self.assertEqual(im, im.filter(lut)) + + +class TestTransformColorLut3D(PillowTestCase): + def test_wrong_args(self): + source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) + + with self.assertRaisesRegex(ValueError, "Only 3 or 4 output"): + source.transform(lambda r, g, b: (r, g, b), channels=8) + + with self.assertRaisesRegex(ValueError, "should have either channels"): + source.transform(lambda r, g, b: (r, g, b), channels=4) + + with self.assertRaisesRegex(ValueError, "should have either channels"): + source.transform(lambda r, g, b: (r, g, b, 1)) + + with self.assertRaises(TypeError): + source.transform(lambda r, g, b, a: (r, g, b)) + + def test_target_mode(self): + source = ImageFilter.Color3DLUT.generate( + 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') + + 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)) + 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]) + + def test_3_to_4_channels(self): + source = ImageFilter.Color3DLUT.generate( + (6, 5, 4), lambda r, g, b: (r, g, b)) + lut = source.transform(lambda r, g, b: (r*r, g*g, b*b, 1), channels=4) + self.assertEqual(tuple(lut.size), tuple(source.size)) + self.assertNotEqual(len(lut.table), len(source.table)) + self.assertNotEqual(lut.table, source.table) + 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]) + + 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) + self.assertEqual(tuple(lut.size), tuple(source.size)) + self.assertNotEqual(len(lut.table), len(source.table)) + self.assertNotEqual(lut.table, source.table) + 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]) + + 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)) + 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: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]) + + def test_with_normals_3_channels(self): + source = ImageFilter.Color3DLUT.generate( + (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) + 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: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]) + + 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) + lut = source.transform( + 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) + 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]) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 11f26d38e..fe9f5ccc4 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -2,7 +2,7 @@ from __future__ import division, print_function import sys -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 4da8760cd..bc0bab525 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -30,7 +30,7 @@ class TestDecompressionBomb(PillowTestCase): def test_warning(self): # Set limit to trigger warning on the test file - Image.MAX_IMAGE_PIXELS = 128 * 128 -1 + Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 self.assertEqual(Image.MAX_IMAGE_PIXELS, 128 * 128 - 1) self.assert_warning(Image.DecompressionBombWarning, @@ -38,12 +38,13 @@ class TestDecompressionBomb(PillowTestCase): def test_exception(self): # Set limit to trigger exception on the test file - Image.MAX_IMAGE_PIXELS = 64 * 128 -1 + 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)) + class TestDecompressionCrop(PillowTestCase): def setUp(self): @@ -60,6 +61,25 @@ class TestDecompressionCrop(PillowTestCase): self.assert_warning(Image.DecompressionBombWarning, self.src.crop, box) + def test_crop_decompression_checks(self): -if __name__ == '__main__': - unittest.main() + im = Image.new("RGB", (100, 100)) + + good_values = ((-9999, -9999, -9990, -9990), + (-999, -999, -990, -990)) + + warning_values = ((-160, -160, 99, 99), + (160, 160, -99, -99)) + + error_values = ((-99909, -99990, 99999, 99999), + (99909, 99990, -99999, -99999)) + + for value in good_values: + self.assertEqual(im.crop(value).size, (9, 9)) + + for value in warning_values: + self.assert_warning(Image.DecompressionBombWarning, im.crop, value) + + for value in error_values: + with self.assertRaises(Image.DecompressionBombError): + im.crop(value) diff --git a/Tests/test_features.py b/Tests/test_features.py index 54d668d2f..15b5982ce 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,11 +1,11 @@ -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase from PIL import features try: from PIL import _webp HAVE_WEBP = True -except: +except ImportError: HAVE_WEBP = False @@ -23,20 +23,20 @@ class TestFeatures(PillowTestCase): self.assertEqual(features.check_feature(feature), features.check(feature)) - @unittest.skipUnless(HAVE_WEBP, True) - def check_webp_transparency(self): + @unittest.skipUnless(HAVE_WEBP, "WebP not available") + def test_webp_transparency(self): self.assertEqual(features.check('transp_webp'), not _webp.WebPDecoderBuggyAlpha()) self.assertEqual(features.check('transp_webp'), _webp.HAVE_TRANSPARENCY) - @unittest.skipUnless(HAVE_WEBP, True) - def check_webp_mux(self): + @unittest.skipUnless(HAVE_WEBP, "WebP not available") + def test_webp_mux(self): self.assertEqual(features.check('webp_mux'), _webp.HAVE_WEBPMUX) - @unittest.skipUnless(HAVE_WEBP, True) - def check_webp_anim(self): + @unittest.skipUnless(HAVE_WEBP, "WebP not available") + def test_webp_anim(self): self.assertEqual(features.check('webp_anim'), _webp.HAVE_WEBPANIM) @@ -63,7 +63,3 @@ class TestFeatures(PillowTestCase): module = "unsupported_module" # Act / Assert self.assertRaises(ValueError, features.check_module, module) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py new file mode 100644 index 000000000..59951a890 --- /dev/null +++ b/Tests/test_file_blp.py @@ -0,0 +1,20 @@ +from PIL import Image + +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) + + def test_load_blp2_dxt1(self): + im = Image.open("Tests/images/blp/blp2_dxt1.blp") + target = Image.open("Tests/images/blp/blp2_dxt1.png") + self.assert_image_equal(im, target) + + def test_load_blp2_dxt1a(self): + im = Image.open("Tests/images/blp/blp2_dxt1a.blp") + target = Image.open("Tests/images/blp/blp2_dxt1a.png") + self.assert_image_equal(im, target) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index bfd97016f..c51e2bb4e 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image, BmpImagePlugin import io @@ -75,7 +75,3 @@ class TestFileBmp(PillowTestCase): im = BmpImagePlugin.DibImageFile('Tests/images/clipboard.dib') target = Image.open('Tests/images/clipboard_target.png') self.assert_image_equal(im, target) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 08980a996..307029136 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import BufrStubImagePlugin, Image @@ -40,7 +40,3 @@ class TestFileBufrStub(PillowTestCase): # Act / Assert: stub cannot save without an implemented handler self.assertRaises(IOError, im.save, tmpfile) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 55228be0c..8ca3310e5 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image from PIL import ContainerIO @@ -123,7 +123,3 @@ class TestFileContainer(PillowTestCase): # Assert self.assertEqual(data, expected) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 23055a0ad..734c330e6 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image, CurImagePlugin @@ -26,9 +26,6 @@ class TestFileCur(PillowTestCase): no_cursors_file = "Tests/images/no_cursors.cur" cur = CurImagePlugin.CurImageFile(TEST_FILE) + cur.fp.close() with open(no_cursors_file, "rb") as cur.fp: self.assertRaises(TypeError, cur._open) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 28ebb91dc..0da364648 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image, DcxImagePlugin @@ -20,6 +20,12 @@ class TestFileDcx(PillowTestCase): orig = hopper() self.assert_image_equal(im, orig) + def test_unclosed_file(self): + def open(): + im = Image.open(TEST_FILE) + im.load() + self.assert_warning(None, open) + def test_invalid_file(self): with open("Tests/images/flower.jpg", "rb") as fp: self.assertRaises(SyntaxError, @@ -58,7 +64,3 @@ class TestFileDcx(PillowTestCase): # Act / Assert self.assertRaises(EOFError, im.seek, frame) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 89d265ec2..af2524c4a 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,6 +1,6 @@ from io import BytesIO -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image, DdsImagePlugin TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" @@ -110,6 +110,3 @@ class TestFileDds(PillowTestCase): im.load() self.assertRaises(IOError, short_file) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 2313b292c..1f6025d69 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,8 +1,10 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import unittest, PillowTestCase, hopper from PIL import Image, EpsImagePlugin import io +HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() + # Our two EPS test files (they are identical except for their bounding boxes) file1 = "Tests/images/zero_bb.eps" file2 = "Tests/images/non_zero_bb.eps" @@ -20,10 +22,7 @@ file3 = "Tests/images/binary_preview_map.eps" class TestFileEps(PillowTestCase): - def setUp(self): - if not EpsImagePlugin.has_ghostscript(): - self.skipTest("Ghostscript not available") - + @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") def test_sanity(self): # Regular scale image1 = Image.open(file1) @@ -57,6 +56,7 @@ class TestFileEps(PillowTestCase): 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") @@ -71,28 +71,32 @@ class TestFileEps(PillowTestCase): 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 + # 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') + @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') + @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") def test_bytesio_object(self): with open(file1, 'rb') as f: img_bytes = io.BytesIO(f.read()) @@ -109,6 +113,7 @@ class TestFileEps(PillowTestCase): tmpfile = self.tempfile('temp.eps') self.assertRaises(ValueError, im.save, tmpfile) + @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") def test_render_scale1(self): # We need png support for these render test codecs = dir(Image.core) @@ -129,6 +134,7 @@ class TestFileEps(PillowTestCase): 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): # We need png support for these render test codecs = dir(Image.core) @@ -149,6 +155,7 @@ class TestFileEps(PillowTestCase): 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) @@ -166,6 +173,7 @@ class TestFileEps(PillowTestCase): self.assertEqual(image2.size, new_size) self.assertEqual(image3.size, new_size) + @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") def test_thumbnail(self): # Issue #619 # Arrange @@ -195,41 +203,15 @@ class TestFileEps(PillowTestCase): self.assertEqual(t.readline().strip('\r\n'), 'baz', ending) self.assertEqual(t.readline().strip('\r\n'), 'bif', ending) - def _test_readline_stringio(self, test_string, ending): - # check all the freaking line endings possible - try: - import StringIO - except ImportError: - # don't skip, it skips everything in the parent test - return - t = StringIO.StringIO(test_string) + def _test_readline_io_psfile(self, test_string, ending): + f = io.BytesIO(test_string.encode('latin-1')) + t = EpsImagePlugin.PSFile(f) self._test_readline(t, ending) - def _test_readline_io(self, test_string, ending): - if str is bytes: - t = io.StringIO(unicode(test_string)) - else: - t = io.StringIO(test_string) - self._test_readline(t, ending) - - def _test_readline_file_universal(self, test_string, ending): - f = self.tempfile('temp.txt') - with open(f, 'wb') as w: - if str is bytes: - w.write(test_string) - else: - w.write(test_string.encode('UTF-8')) - - with open(f, 'rU') as t: - 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: - if str is bytes: - w.write(test_string) - else: - w.write(test_string.encode('UTF-8')) + w.write(test_string.encode('latin-1')) with open(f, 'rb') as r: t = EpsImagePlugin.PSFile(r) @@ -238,29 +220,12 @@ class TestFileEps(PillowTestCase): 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'] - not_working_endings = ['\n\r', '\r'] + line_endings = ['\r\n', '\n', '\n\r', '\r'] strings = ['something', 'else', 'baz', 'bif'] for ending in line_endings: s = ending.join(strings) - # Native Python versions will pass these endings. - # self._test_readline_stringio(s, ending) - # self._test_readline_io(s, ending) - # self._test_readline_file_universal(s, ending) - - self._test_readline_file_psfile(s, ending) - - for ending in not_working_endings: - # these only work with the PSFile, while they're in spec, - # they're not likely to be used - s = ending.join(strings) - - # Native Python versions may fail on these endings. - # self._test_readline_stringio(s, ending) - # self._test_readline_io(s, ending) - # self._test_readline_file_universal(s, ending) - + self._test_readline_io_psfile(s, ending) self._test_readline_file_psfile(s, ending) def test_open_eps(self): @@ -271,13 +236,12 @@ class TestFileEps(PillowTestCase): "Tests/images/illuCS6_no_preview.eps", "Tests/images/illuCS6_preview.eps"] - # Act + # Act / Assert for filename in FILES: img = Image.open(filename) + self.assertEqual(img.mode, "RGB") - # Assert - 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" @@ -287,7 +251,3 @@ class TestFileEps(PillowTestCase): self.assertEqual(image.mode, "RGB") self.assertEqual(image.size, (460, 352)) self.assertEqual(image.format, "EPS") - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py index d74e983ce..6cb2de110 100644 --- a/Tests/test_file_fitsstub.py +++ b/Tests/test_file_fitsstub.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import FitsStubImagePlugin, Image @@ -44,7 +44,3 @@ class TestFileFitsStub(PillowTestCase): self.assertRaises( IOError, FitsStubImagePlugin._save, im, dummy_fp, dummy_filename) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 142af3cec..f67f0ada1 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image, FliImagePlugin @@ -27,6 +27,12 @@ class TestFileFli(PillowTestCase): self.assertEqual(im.info["duration"], 71) self.assertTrue(im.is_animated) + def test_unclosed_file(self): + def open(): + im = Image.open(static_test_file) + im.load() + self.assert_warning(None, open) + def test_tell(self): # Arrange im = Image.open(static_test_file) @@ -85,6 +91,9 @@ class TestFileFli(PillowTestCase): layer_number = im.tell() self.assertEqual(layer_number, 1) + def test_seek(self): + im = Image.open(animated_test_file) + im.seek(50) -if __name__ == '__main__': - unittest.main() + expected = Image.open("Tests/images/a_fli.png") + self.assert_image_equal(im, expected) diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index 441a3e635..7283ba088 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase try: from PIL import FpxImagePlugin @@ -21,7 +21,3 @@ class TestFileFpx(PillowTestCase): ole_file = "Tests/images/test-ole-file.doc" self.assertRaises(SyntaxError, FpxImagePlugin.FpxImageFile, ole_file) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index ed1116ad5..07e29d7e0 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image @@ -14,6 +14,3 @@ class TestFileFtex(PillowTestCase): 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) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index aacc193f4..1eb66264d 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image, GbrImagePlugin @@ -17,6 +17,3 @@ class TestFileGbr(PillowTestCase): target = Image.open('Tests/images/gbr.png') self.assert_image_equal(target, im) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py new file mode 100644 index 000000000..67c9fba7b --- /dev/null +++ b/Tests/test_file_gd.py @@ -0,0 +1,22 @@ +from .helper import PillowTestCase + +from PIL import GdImageFile + +TEST_GD_FILE = "Tests/images/hopper.gd" + + +class TestFileGd(PillowTestCase): + + def test_sanity(self): + im = GdImageFile.open(TEST_GD_FILE) + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "GD") + + def test_bad_mode(self): + 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) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 22a2c0072..6a4b14d40 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,9 +1,15 @@ -from helper import unittest, PillowTestCase, hopper, netpbm_available +from .helper import unittest, PillowTestCase, hopper, netpbm_available from PIL import Image, ImagePalette, GifImagePlugin from io import BytesIO +try: + from PIL import _webp + HAVE_WEBP = True +except ImportError: + HAVE_WEBP = False + codecs = dir(Image.core) # sample gif stream @@ -27,6 +33,12 @@ class TestFileGif(PillowTestCase): self.assertEqual(im.format, "GIF") self.assertEqual(im.info["version"], b"GIF89a") + def test_unclosed_file(self): + def open(): + im = Image.open(TEST_GIF) + im.load() + self.assert_warning(None, open) + def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -134,13 +146,15 @@ class TestFileGif(PillowTestCase): # Multiframe image im = Image.open("Tests/images/dispose_bgnd.gif") + info = im.info.copy() + out = self.tempfile('temp.gif') im.save(out, save_all=True) reread = Image.open(out) for header in important_headers: self.assertEqual( - im.info[header], + info[header], reread.info[header] ) @@ -207,6 +221,15 @@ class TestFileGif(PillowTestCase): except EOFError: self.assertEqual(framecount, 5) + def test_seek_info(self): + im = Image.open("Tests/images/iss634.gif") + info = im.info.copy() + + im.seek(1) + im.seek(0) + + self.assertEqual(im.info, info) + def test_n_frames(self): for path, n_frames in [ [TEST_GIF, 1], @@ -266,7 +289,7 @@ class TestFileGif(PillowTestCase): Image.new('L', (100, 100), '#111'), Image.new('L', (100, 100), '#222'), ] - for method in range(0,4): + for method in range(0, 4): im_list[0].save( out, save_all=True, @@ -278,7 +301,6 @@ class TestFileGif(PillowTestCase): img.seek(img.tell() + 1) self.assertEqual(img.disposal_method, method) - # check per frame disposal im_list[0].save( out, @@ -306,9 +328,12 @@ class TestFileGif(PillowTestCase): out = self.tempfile('temp.gif') im = Image.new('L', (100, 100), '#000') - im.save(out, duration=duration) - reread = Image.open(out) + # Check that the argument has priority over the info settings + im.info['duration'] = 100 + im.save(out, duration=duration) + + reread = Image.open(out) self.assertEqual(reread.info['duration'], duration) def test_multiple_duration(self): @@ -398,9 +423,15 @@ class TestFileGif(PillowTestCase): self.assertEqual(reread.info['background'], im.info['background']) + if HAVE_WEBP and _webp.HAVE_WEBPANIM: + im = Image.open("Tests/images/hopper.webp") + self.assertIsInstance(im.info['background'], tuple) + im.save(out) + def test_comment(self): im = Image.open(TEST_GIF) - self.assertEqual(im.info['comment'], b"File written by Adobe Photoshop\xa8 4.0") + 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') @@ -410,6 +441,23 @@ class TestFileGif(PillowTestCase): self.assertEqual(reread.info['comment'], im.info['comment']) + def test_comment_over_255(self): + out = self.tempfile('temp.gif') + im = Image.new('L', (100, 100), '#000') + comment = b"Test comment text" + while len(comment) < 256: + comment += comment + im.info['comment'] = comment + im.save(out) + reread = Image.open(out) + + self.assertEqual(reread.info['comment'], comment) + + def test_zero_comment_subblocks(self): + im = Image.open('Tests/images/hopper_zero_comment_subblocks.gif') + expected = Image.open(TEST_GIF) + self.assert_image_equal(im, expected) + def test_version(self): out = self.tempfile('temp.gif') @@ -475,8 +523,6 @@ class TestFileGif(PillowTestCase): # that's > 128 items where the transparent color is actually # the top palette entry to trigger the bug. - from PIL import ImagePalette - data = bytes(bytearray(range(1, 254))) palette = ImagePalette.ImagePalette("RGB", list(range(256))*3) @@ -490,6 +536,27 @@ class TestFileGif(PillowTestCase): self.assertEqual(reloaded.info['transparency'], 253) + def test_rgb_transparency(self): + out = self.tempfile('temp.gif') + + # Single frame + 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) + + # 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) + + reloaded = Image.open(out) + self.assertNotIn('transparency', reloaded.info) + def test_bbox(self): out = self.tempfile('temp.gif') @@ -584,9 +651,6 @@ class TestFileGif(PillowTestCase): # see https://github.com/python-pillow/Pillow/issues/2811 im = Image.open('Tests/images/issue_2811.gif') - self.assertEqual(im.tile[0][3][0], 11) # LZW bits + self.assertEqual(im.tile[0][3][0], 11) # LZW bits # codec error prepatch im.load() - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index b29f6f13b..c89440239 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import GimpGradientFile @@ -119,7 +119,3 @@ class TestImage(PillowTestCase): # load returns raw palette information self.assertEqual(len(palette[0]), 1024) self.assertEqual(palette[1], "RGBA") - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index 4ee5323bc..1ebac44e7 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL.GimpPaletteFile import GimpPaletteFile @@ -28,7 +28,3 @@ class TestImage(PillowTestCase): # Assert self.assertEqual(mode, "RGB") - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index b3a6f1a5a..79e826945 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import GribStubImagePlugin, Image @@ -40,7 +40,3 @@ class TestFileGribStub(PillowTestCase): # Act / Assert: stub cannot save without an implemented handler self.assertRaises(IOError, im.save, tmpfile) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 6cddd8d7b..598ef0c5c 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Hdf5StubImagePlugin, Image @@ -44,7 +44,3 @@ class TestFileHdf5Stub(PillowTestCase): self.assertRaises( IOError, Hdf5StubImagePlugin._save, im, dummy_fp, dummy_filename) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index d8508e579..ac60731f9 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase from PIL import Image, IcnsImagePlugin @@ -17,13 +17,15 @@ class TestFileIcns(PillowTestCase): # Loading this icon by default should result in the largest size # (512x512@2x) being loaded im = Image.open(TEST_FILE) - 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") - @unittest.skipIf(sys.platform != 'darwin', - "requires MacOS") + @unittest.skipIf(sys.platform != 'darwin', "requires macOS") def test_save(self): im = Image.open(TEST_FILE) @@ -36,6 +38,22 @@ class TestFileIcns(PillowTestCase): self.assertEqual(reread.size, (1024, 1024)) self.assertEqual(reread.format, "ICNS") + @unittest.skipIf(sys.platform != 'darwin', "requires macOS") + def test_save_append_images(self): + im = Image.open(TEST_FILE) + + temp_file = self.tempfile("temp.icns") + provided_im = Image.new('RGBA', (32, 32), (255, 0, 0, 128)) + im.save(temp_file, append_images=[provided_im]) + + reread = Image.open(temp_file) + self.assert_image_similar(reread, im, 1) + + reread = Image.open(temp_file) + reread.size = (16, 16, 2) + reread.load() + self.assert_image_equal(reread, provided_im) + def test_sizes(self): # Check that we can load all of the sizes, and that the final pixel # dimensions are as expected @@ -49,6 +67,10 @@ class TestFileIcns(PillowTestCase): self.assertEqual(im2.mode, 'RGBA') self.assertEqual(im2.size, (wr, hr)) + # 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). @@ -99,7 +121,3 @@ class TestFileIcns(PillowTestCase): with io.BytesIO(b'invalid\n') as fp: self.assertRaises(SyntaxError, IcnsImagePlugin.IcnsFile, fp) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 20f21db57..b0d33e33f 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper import io from PIL import Image, IcoImagePlugin @@ -28,7 +28,7 @@ class TestFileIco(PillowTestCase): # the default image output.seek(0) reloaded = Image.open(output) - self.assertEqual(reloaded.info['sizes'], set([(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) @@ -47,6 +47,11 @@ class TestFileIco(PillowTestCase): self.assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) + def test_incorrect_size(self): + im = Image.open(TEST_ICO_FILE) + with self.assertRaises(ValueError): + im.size = (1, 1) + def test_save_256x256(self): """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" # Arrange @@ -76,8 +81,4 @@ class TestFileIco(PillowTestCase): # Assert self.assertEqual( im_saved.info['sizes'], - set([(16, 16), (24, 24), (32, 32), (48, 48)])) - - -if __name__ == '__main__': - unittest.main() + {(16, 16), (24, 24), (32, 32), (48, 48)}) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index c99924767..8e774ce0a 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image, ImImagePlugin @@ -15,6 +15,12 @@ class TestFileIm(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "IM") + def test_unclosed_file(self): + def open(): + im = Image.open(TEST_IM) + im.load() + self.assert_warning(None, open) + def test_tell(self): # Arrange im = Image.open(TEST_IM) @@ -63,7 +69,3 @@ class TestFileIm(PillowTestCase): def test_number(self): self.assertEqual(1.2, ImImagePlugin.number("1.2")) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index e08d994a2..83b735464 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image, IptcImagePlugin @@ -69,7 +69,3 @@ class TestFileIptc(PillowTestCase): # Assert self.assertEqual(mystdout.getvalue(), "61 62 63 \n") - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 747c3d7de..cbe894f34 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,5 +1,5 @@ -from helper import unittest, PillowTestCase, hopper -from helper import djpeg_available, cjpeg_available +from .helper import unittest, PillowTestCase, hopper +from .helper import djpeg_available, cjpeg_available from io import BytesIO import os @@ -41,13 +41,14 @@ class TestFileJpeg(PillowTestCase): def test_sanity(self): # internal version number - self.assertRegexpMatches(Image.core.jpeglib_version, r"\d+\.\d+$") + self.assertRegex(Image.core.jpeglib_version, r"\d+\.\d+$") im = Image.open(TEST_FILE) im.load() self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "JPEG") + self.assertEqual(im.get_format_mimetype(), "image/jpeg") def test_app(self): # Test APP/COM reader (@PIL135) @@ -119,7 +120,7 @@ class TestFileJpeg(PillowTestCase): # using a 4-byte test code should allow us to detect out of # order issues. icc_profile = (b"Test"*int(n/4+1))[:n] - assert len(icc_profile) == n # sanity + 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) @@ -140,11 +141,9 @@ class TestFileJpeg(PillowTestCase): im = Image.open('Tests/images/icc_profile_big.jpg') f = self.tempfile("temp.jpg") icc_profile = im.info["icc_profile"] - try: - im.save(f, format='JPEG', progressive=True,quality=95, - icc_profile=icc_profile, optimize=True) - except IOError: - self.fail("Failed saving image with icc larger than image size") + # Should not raise IOError for image with icc larger than image size. + im.save(f, format='JPEG', progressive=True, quality=95, + icc_profile=icc_profile, optimize=True) def test_optimize(self): im1 = self.roundtrip(hopper()) @@ -348,6 +347,21 @@ class TestFileJpeg(PillowTestCase): filename = "Tests/images/jpeg_ff00_header.jpg" Image.open(filename) + def test_truncated_jpeg_should_read_all_the_data(self): + filename = "Tests/images/truncated_jpeg.jpg" + ImageFile.LOAD_TRUNCATED_IMAGES = True + im = Image.open(filename) + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False + self.assertIsNotNone(im.getbbox()) + + def test_truncated_jpeg_throws_IOError(self): + filename = "Tests/images/truncated_jpeg.jpg" + im = Image.open(filename) + + with self.assertRaises(IOError): + im.load() + def _n_qtables_helper(self, n, test_file): im = Image.open(test_file) f = self.tempfile('temp.jpg') @@ -423,17 +437,17 @@ class TestFileJpeg(PillowTestCase): self._n_qtables_helper(4, "Tests/images/pil_sample_cmyk.jpg") # not a sequence - self.assertRaises(Exception, self.roundtrip, im, qtables='a') + self.assertRaises(ValueError, self.roundtrip, im, qtables='a') # sequence wrong length - self.assertRaises(Exception, self.roundtrip, im, qtables=[]) + self.assertRaises(ValueError, self.roundtrip, im, qtables=[]) # sequence wrong length - self.assertRaises(Exception, + self.assertRaises(ValueError, self.roundtrip, im, qtables=[1, 2, 3, 4, 5]) # qtable entry not a sequence - self.assertRaises(Exception, self.roundtrip, im, qtables=[1]) + self.assertRaises(ValueError, self.roundtrip, im, qtables=[1]) # qtable entry has wrong number of items - self.assertRaises(Exception, + self.assertRaises(ValueError, self.roundtrip, im, qtables=[[1, 2, 3, 4]]) @unittest.skipUnless(djpeg_available(), "djpeg not available") @@ -567,6 +581,15 @@ class TestFileJpeg(PillowTestCase): # 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") + + # Act / Assert + self.assertEqual(im._getexif()[306], '2017:03:13 23:03:09') + @unittest.skipUnless(sys.platform.startswith('win32'), "Windows only") class TestFileCloseW32(PillowTestCase): @@ -576,7 +599,6 @@ class TestFileCloseW32(PillowTestCase): def test_fd_leak(self): tmpfile = self.tempfile("temp.jpg") - import os with Image.open("Tests/images/hopper.jpg") as im: im.save(tmpfile) @@ -584,12 +606,8 @@ class TestFileCloseW32(PillowTestCase): im = Image.open(tmpfile) fp = im.fp self.assertFalse(fp.closed) - self.assertRaises(Exception, os.remove, tmpfile) + self.assertRaises(WindowsError, os.remove, tmpfile) im.load() self.assertTrue(fp.closed) # this should not fail, as load should have closed the file. os.remove(tmpfile) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 753b50598..4b34354e2 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image, Jpeg2KImagePlugin from io import BytesIO @@ -31,7 +31,7 @@ class TestFileJpeg2k(PillowTestCase): def test_sanity(self): # Internal version number - self.assertRegexpMatches(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') px = im.load() @@ -39,6 +39,12 @@ class TestFileJpeg2k(PillowTestCase): self.assertEqual(im.mode, 'RGB') self.assertEqual(im.size, (640, 480)) self.assertEqual(im.format, 'JPEG2000') + self.assertEqual(im.get_format_mimetype(), 'image/jp2') + + def test_jpf(self): + im = Image.open('Tests/images/balloon.jpf') + self.assertEqual(im.format, 'JPEG2000') + self.assertEqual(im.get_format_mimetype(), 'image/jpx') def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -105,6 +111,22 @@ class TestFileJpeg2k(PillowTestCase): 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 + ]: + 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], @@ -146,13 +168,13 @@ class TestFileJpeg2k(PillowTestCase): self.assertEqual(j2k.mode, 'I;16') self.assertEqual(jp2.mode, 'I;16') - def test_16bit_monchrome_jp2_like_tiff(self): + def test_16bit_monochrome_jp2_like_tiff(self): tiff_16bit = Image.open('Tests/images/16bit.cropped.tif') jp2 = Image.open('Tests/images/16bit.cropped.jp2') self.assert_image_similar(jp2, tiff_16bit, 1e-3) - def test_16bit_monchrome_j2k_like_tiff(self): + 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') @@ -176,5 +198,15 @@ class TestFileJpeg2k(PillowTestCase): with self.assertRaises(IOError): Image.open('Tests/images/unbound_variable.jp2') -if __name__ == '__main__': - unittest.main() + def test_parser_feed(self): + # Arrange + from PIL import ImageFile + with open('Tests/images/test-card-lossless.jp2', 'rb') as f: + data = f.read() + + # Act + p = ImageFile.Parser() + p.feed(data) + + # Assert + self.assertEqual(p.image.size, (640, 480)) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index dc3b10845..56564ebde 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,12 +1,14 @@ from __future__ import print_function -from helper import unittest, PillowTestCase, hopper, py3 +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 distutils.version from PIL import Image, TiffImagePlugin, TiffTags @@ -30,7 +32,7 @@ class LibTiffTestCase(PillowTestCase): try: self.assertEqual(im._compression, 'group4') - except: + except AttributeError: print("No _compression") print(dir(im)) @@ -125,7 +127,8 @@ class TestFileLibTiff(LibTiffTestCase): im.tile[0][:3], ('tiff_adobe_deflate', (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 """ @@ -192,8 +195,8 @@ class TestFileLibTiff(LibTiffTestCase): im = Image.open('Tests/images/hopper_g4.tif') for tag in im.tag_v2: try: - del(core_items[tag]) - except: + del core_items[tag] + except KeyError: pass # Type codes: @@ -202,7 +205,7 @@ class TestFileLibTiff(LibTiffTestCase): # 4: "long", # 5: "rational", # 12: "double", - # type: dummy value + # Type: dummy value values = {2: 'test', 3: 1, 4: 2**20, @@ -216,10 +219,11 @@ 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]) + del new_ifd[338] out = self.tempfile("temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True @@ -228,6 +232,47 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False + def test_custom_metadata(self): + custom = { + 37000: 4, + 37001: 4.2, + 37002: 'custom tag value', + 37003: u'custom tag value', + 37004: b'custom tag value' + } + + libtiff_version = TiffImagePlugin._libtiff_version() + + libtiffs = [False] + if distutils.version.StrictVersion(libtiff_version) >= \ + distutils.version.StrictVersion("4.0"): + libtiffs.append(True) + + for libtiff in libtiffs: + TiffImagePlugin.WRITE_LIBTIFF = libtiff + + im = hopper() + + out = self.tempfile("temp.tif") + im.save(out, tiffinfo=custom) + TiffImagePlugin.WRITE_LIBTIFF = False + + 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) + + def test_int_dpi(self): + # issue #1765 + 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)) + def test_g3_compression(self): i = Image.open('Tests/images/hopper_g4_500.tif') out = self.tempfile("temp.tif") @@ -402,7 +447,7 @@ class TestFileLibTiff(LibTiffTestCase): im = Image.open('Tests/images/multipage.tiff') frames = im.n_frames self.assertEqual(frames, 3) - for idx in range(frames): + for _ in range(frames): im.seek(0) # Should not raise ValueError: I/O operation on closed file im.load() @@ -483,7 +528,7 @@ class TestFileLibTiff(LibTiffTestCase): pilim_load = Image.open(buffer_io) self.assert_image_similar(pilim, pilim_load, 0) - # save_bytesio() + save_bytesio() save_bytesio('raw') save_bytesio("packbits") save_bytesio("tiff_lzw") @@ -524,21 +569,19 @@ class TestFileLibTiff(LibTiffTestCase): f.write(src.read()) im = Image.open(tmpfile) - count = im.n_frames + im.n_frames im.close() - try: - os.remove(tmpfile) # Windows PermissionError here! - except: - self.fail("Should not get permission error here") + # Should not raise PermissionError. + os.remove(tmpfile) def test_read_icc(self): with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc = img.info.get('icc_profile') - self.assertNotEqual(icc, None) + self.assertIsNotNone(icc) TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc_libtiff = img.info.get('icc_profile') - self.assertNotEqual(icc_libtiff, None) + self.assertIsNotNone(icc_libtiff) TiffImagePlugin.READ_LIBTIFF = False self.assertEqual(icc, icc_libtiff) @@ -577,10 +620,14 @@ class TestFileLibTiff(LibTiffTestCase): self.assertEqual(im.mode, "RGBA") self.assertEqual(im.size, (100, 40)) - self.assertEqual(im.tile, [('tiff_lzw', (0, 0, 100, 40), 0, ('RGBa;16N', 'tiff_lzw', False))]) + self.assertEqual( + im.tile, + [('tiff_lzw', (0, 0, 100, 40), 0, ('RGBa;16N', 'tiff_lzw', False))] + ) 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] @@ -606,7 +653,8 @@ class TestFileLibTiff(LibTiffTestCase): im = Image.open("Tests/images/copyleft.tiff") 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") @@ -617,6 +665,38 @@ class TestFileLibTiff(LibTiffTestCase): im2 = hopper() self.assert_image_similar(im, im2, 5) + def test_strip_cmyk_jpeg(self): + infile = "Tests/images/tiff_strip_cmyk_jpeg.tif" + im = Image.open(infile) -if __name__ == '__main__': - unittest.main() + self.assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) + + def test_strip_ycbcr_jpeg_2x2_sampling(self): + infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" + im = Image.open(infile) + + self.assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) + + def test_strip_ycbcr_jpeg_1x1_sampling(self): + infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" + im = Image.open(infile) + + self.assert_image_equal_tofile(im, "Tests/images/flower2.jpg") + + def test_tiled_cmyk_jpeg(self): + infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" + im = Image.open(infile) + + self.assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) + + def test_tiled_ycbcr_jpeg_1x1_sampling(self): + infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" + im = Image.open(infile) + + self.assert_image_equal_tofile(im, "Tests/images/flower2.jpg") + + def test_tiled_ycbcr_jpeg_2x2_sampling(self): + infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" + im = Image.open(infile) + + self.assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index c402673d8..6b379718e 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,8 +1,6 @@ -from helper import unittest - from PIL import Image -from test_file_libtiff import LibTiffTestCase +from .test_file_libtiff import LibTiffTestCase class TestFileLibTiffSmall(LibTiffTestCase): @@ -46,7 +44,3 @@ class TestFileLibTiffSmall(LibTiffTestCase): self.assertEqual(im.size, (128, 128)) self._assert_noerr(im) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index 491d8ea03..e273faad9 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image, McIdasImagePlugin @@ -28,7 +28,3 @@ class TestFileMcIdas(PillowTestCase): self.assertEqual(im.size, (1800, 400)) im2 = Image.open(saved_file) self.assert_image_equal(im, im2) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index f4059f9c9..3a7daa459 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import unittest, PillowTestCase, hopper from PIL import Image, ImagePalette, features @@ -64,7 +64,3 @@ class TestFileMic(PillowTestCase): ole_file = "Tests/images/test-ole-file.doc" self.assertRaises(SyntaxError, MicImagePlugin.MicImageFile, ole_file) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 70bb9b105..45172472a 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from io import BytesIO from PIL import Image @@ -31,6 +31,12 @@ class TestFileMpo(PillowTestCase): self.assertEqual(im.size, (640, 480)) self.assertEqual(im.format, "MPO") + def test_unclosed_file(self): + def open(): + im = Image.open(test_files[0]) + im.load() + self.assert_warning(None, open) + def test_app(self): for test_file in test_files: # Test APP/COM reader (@PIL135) @@ -56,6 +62,14 @@ class TestFileMpo(PillowTestCase): 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) + def test_mp_attribute(self): for test_file in test_files: im = Image.open(test_file) @@ -136,7 +150,3 @@ class TestFileMpo(PillowTestCase): self.assertEqual(im.tell(), 1) jpg1 = self.frame_roundtrip(im) self.assert_image_similar(im, jpg1, 30) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 4aac88092..66af65fcd 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import unittest, PillowTestCase, hopper from PIL import Image, MspImagePlugin @@ -78,7 +78,3 @@ class TestFileMsp(PillowTestCase): # Act/Assert self.assertRaises(IOError, im.save, filename) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index b97a9b19e..a6491634a 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper, imagemagick_available +from .helper import PillowTestCase, hopper, imagemagick_available import os.path @@ -52,7 +52,3 @@ class TestFilePalm(PillowTestCase): # Act / Assert self.assertRaises(IOError, self.helper_save_as_palm, mode) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 06fd33043..7296a303f 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image @@ -16,7 +16,3 @@ class TestFilePcd(PillowTestCase): # target = hopper().resize((768,512)) # self.assert_image_similar(im, target, 10) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 415827e49..9e42a86f7 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image, ImageFile, PcxImagePlugin @@ -128,7 +128,3 @@ class TestFilePcx(PillowTestCase): for x in range(5): px[x, 3] = 0 self._test_buffer_overflow(im) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index ee02d0694..7b024426b 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,24 +1,33 @@ -from helper import unittest, PillowTestCase, hopper -from PIL import Image +from .helper import PillowTestCase, hopper +from PIL import Image, PdfParser +import io +import os import os.path +import tempfile +import time class TestFilePdf(PillowTestCase): - def helper_save_as_pdf(self, mode, save_all=False): + def helper_save_as_pdf(self, mode, **kwargs): # Arrange im = hopper(mode) outfile = self.tempfile("temp_" + mode + ".pdf") # Act - if save_all: - im.save(outfile, save_all=True) - else: - im.save(outfile) + im.save(outfile, **kwargs) # Assert self.assertTrue(os.path.isfile(outfile)) self.assertGreater(os.path.getsize(outfile), 0) + with PdfParser.PdfParser(outfile) as pdf: + if kwargs.get("append_images", False) or \ + kwargs.get("append", False): + self.assertGreater(len(pdf.pages), 1) + else: + self.assertGreater(len(pdf.pages), 0) + + return outfile def test_monochrome(self): # Arrange @@ -97,6 +106,163 @@ class TestFilePdf(PillowTestCase): self.assertTrue(os.path.isfile(outfile)) self.assertGreater(os.path.getsize(outfile), 0) + def test_multiframe_normal_save(self): + # Test saving a multiframe image without save_all + im = Image.open("Tests/images/dispose_bgnd.gif") -if __name__ == '__main__': - unittest.main() + outfile = self.tempfile('temp.pdf') + im.save(outfile) + + self.assertTrue(os.path.isfile(outfile)) + self.assertGreater(os.path.getsize(outfile), 0) + + def test_pdf_open(self): + # fail on a buffer full of null bytes + self.assertRaises( + PdfParser.PdfFormatError, + PdfParser.PdfParser, buf=bytearray(65536)) + + # make an empty PDF object + with PdfParser.PdfParser() as empty_pdf: + self.assertEqual(len(empty_pdf.pages), 0) + self.assertEqual(len(empty_pdf.info), 0) + self.assertFalse(empty_pdf.should_close_buf) + self.assertFalse(empty_pdf.should_close_file) + + # make a PDF file + pdf_filename = self.helper_save_as_pdf("RGB") + + # open the PDF file + with PdfParser.PdfParser(filename=pdf_filename) as hopper_pdf: + self.assertEqual(len(hopper_pdf.pages), 1) + self.assertTrue(hopper_pdf.should_close_buf) + self.assertTrue(hopper_pdf.should_close_file) + + # read a PDF file from a buffer with a non-zero offset + with open(pdf_filename, "rb") as f: + content = b"xyzzy" + f.read() + with PdfParser.PdfParser(buf=content, start_offset=5) as hopper_pdf: + self.assertEqual(len(hopper_pdf.pages), 1) + self.assertFalse(hopper_pdf.should_close_buf) + self.assertFalse(hopper_pdf.should_close_file) + + # read a PDF file from an already open file + with open(pdf_filename, "rb") as f: + with PdfParser.PdfParser(f=f) as hopper_pdf: + self.assertEqual(len(hopper_pdf.pages), 1) + self.assertTrue(hopper_pdf.should_close_buf) + self.assertFalse(hopper_pdf.should_close_file) + + def test_pdf_append_fails_on_nonexistent_file(self): + im = hopper("RGB") + temp_dir = tempfile.mkdtemp() + try: + self.assertRaises(IOError, + im.save, + os.path.join(temp_dir, "nonexistent.pdf"), + append=True) + finally: + os.rmdir(temp_dir) + + def check_pdf_pages_consistency(self, pdf): + pages_info = pdf.read_indirect(pdf.pages_ref) + self.assertNotIn(b"Parent", pages_info) + self.assertIn(b"Kids", pages_info) + kids_not_used = pages_info[b"Kids"] + for page_ref in pdf.pages: + while True: + if page_ref in kids_not_used: + kids_not_used.remove(page_ref) + page_info = pdf.read_indirect(page_ref) + self.assertIn(b"Parent", page_info) + page_ref = page_info[b"Parent"] + if page_ref == pdf.pages_ref: + break + self.assertEqual(pdf.pages_ref, page_info[b"Parent"]) + self.assertEqual(kids_not_used, []) + + def test_pdf_append(self): + # make a PDF file + pdf_filename = self.helper_save_as_pdf("RGB", producer="PdfParser") + + # open it, check pages and info + with PdfParser.PdfParser(pdf_filename, mode="r+b") as pdf: + self.assertEqual(len(pdf.pages), 1) + self.assertEqual(len(pdf.info), 4) + self.assertEqual(pdf.info.Title, os.path.splitext( + os.path.basename(pdf_filename) + )[0]) + self.assertEqual(pdf.info.Producer, "PdfParser") + self.assertIn(b"CreationDate", pdf.info) + self.assertIn(b"ModDate", pdf.info) + self.check_pdf_pages_consistency(pdf) + + # append some info + pdf.info.Title = "abc" + pdf.info.Author = "def" + pdf.info.Subject = u"ghi\uABCD" + pdf.info.Keywords = "qw)e\\r(ty" + pdf.info.Creator = "hopper()" + pdf.start_writing() + pdf.write_xref_and_trailer() + + # open it again, check pages and info again + with PdfParser.PdfParser(pdf_filename) as pdf: + self.assertEqual(len(pdf.pages), 1) + self.assertEqual(len(pdf.info), 8) + self.assertEqual(pdf.info.Title, "abc") + self.assertIn(b"CreationDate", pdf.info) + self.assertIn(b"ModDate", pdf.info) + self.check_pdf_pages_consistency(pdf) + + # append two images + mode_CMYK = hopper("CMYK") + mode_P = hopper("P") + mode_CMYK.save(pdf_filename, + append=True, save_all=True, append_images=[mode_P]) + + # open the PDF again, check pages and info again + with PdfParser.PdfParser(pdf_filename) as pdf: + self.assertEqual(len(pdf.pages), 3) + self.assertEqual(len(pdf.info), 8) + self.assertEqual(PdfParser.decode_text(pdf.info[b"Title"]), "abc") + self.assertEqual(pdf.info.Title, "abc") + self.assertEqual(pdf.info.Producer, "PdfParser") + self.assertEqual(pdf.info.Keywords, "qw)e\\r(ty") + self.assertEqual(pdf.info.Subject, u"ghi\uABCD") + self.assertIn(b"CreationDate", pdf.info) + self.assertIn(b"ModDate", pdf.info) + self.check_pdf_pages_consistency(pdf) + + def test_pdf_info(self): + # make a PDF file + pdf_filename = self.helper_save_as_pdf( + "RGB", title="title", author="author", subject="subject", + keywords="keywords", creator="creator", producer="producer", + creationDate=time.strptime("2000", "%Y"), + modDate=time.strptime("2001", "%Y")) + + # open it, check pages and info + with PdfParser.PdfParser(pdf_filename) as pdf: + self.assertEqual(len(pdf.info), 8) + self.assertEqual(pdf.info.Title, "title") + self.assertEqual(pdf.info.Author, "author") + self.assertEqual(pdf.info.Subject, "subject") + self.assertEqual(pdf.info.Keywords, "keywords") + self.assertEqual(pdf.info.Creator, "creator") + self.assertEqual(pdf.info.Producer, "producer") + self.assertEqual(pdf.info.CreationDate, + time.strptime("2000", "%Y")) + self.assertEqual(pdf.info.ModDate, time.strptime("2001", "%Y")) + self.check_pdf_pages_consistency(pdf) + + def test_pdf_append_to_bytesio(self): + im = hopper("RGB") + f = io.BytesIO() + im.save(f, format="PDF") + initial_size = len(f.getvalue()) + self.assertGreater(initial_size, 0) + im = hopper("P") + f = io.BytesIO(f.getvalue()) + im.save(f, format="PDF", append=True) + self.assertGreater(len(f.getvalue()), initial_size) diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index ae8c7d5f5..3b998c0b5 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -1,4 +1,4 @@ -from helper import hopper, unittest, PillowTestCase +from .helper import hopper, PillowTestCase from PIL import Image, PixarImagePlugin @@ -13,6 +13,7 @@ class TestFilePixar(PillowTestCase): self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "PIXAR") + self.assertIsNone(im.get_format_mimetype()) im2 = hopper() self.assert_image_similar(im, im2, 4.8) @@ -23,7 +24,3 @@ class TestFilePixar(PillowTestCase): self.assertRaises( SyntaxError, PixarImagePlugin.PixarImageFile, invalid_file) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ce2b3e608..2b80bf357 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,10 +1,17 @@ -from helper import unittest, PillowTestCase, PillowLeakTestCase, hopper +from .helper import unittest, PillowTestCase, PillowLeakTestCase, hopper from PIL import Image, ImageFile, PngImagePlugin +from PIL._util import py3 from io import BytesIO import zlib import sys +try: + from PIL import _webp + HAVE_WEBP = True +except ImportError: + HAVE_WEBP = False + codecs = dir(Image.core) @@ -22,6 +29,7 @@ def chunk(cid, *data): PngImagePlugin.putchunk(*(test_file, cid) + data) return test_file.getvalue() + o32 = PngImagePlugin.o32 IHDR = chunk(b"IHDR", o32(1), o32(1), b'\x08\x02', b'\0\0\0') @@ -67,8 +75,7 @@ class TestFilePng(PillowTestCase): def test_sanity(self): # internal version number - self.assertRegexpMatches( - Image.core.zlib_version, r"\d+\.\d+\.\d+(\.\d+)?$") + self.assertRegex(Image.core.zlib_version, r"\d+\.\d+\.\d+(\.\d+)?$") test_file = self.tempfile("temp.png") @@ -79,21 +86,22 @@ class TestFilePng(PillowTestCase): 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) - im = Image.open(test_file) + Image.open(test_file) hopper("L").save(test_file) - im = Image.open(test_file) + Image.open(test_file) hopper("P").save(test_file) - im = Image.open(test_file) + Image.open(test_file) hopper("RGB").save(test_file) - im = Image.open(test_file) + Image.open(test_file) hopper("I").save(test_file) - im = Image.open(test_file) + Image.open(test_file) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -291,15 +299,29 @@ class TestFilePng(PillowTestCase): 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 + 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) test_file = self.tempfile("temp.png") im.save(test_file) - # There are 559 transparent pixels. - im = im.convert('RGBA') - self.assertEqual(im.getchannel('A').getcolors()[0][0], 559) + 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) def test_save_rgb_single_transparency(self): in_file = "Tests/images/caption_6_33_22.png" @@ -312,7 +334,9 @@ class TestFilePng(PillowTestCase): # Check open/load/verify exception (@PIL150) im = Image.open(TEST_PNG_FILE) - im.verify() + + # Assert that there is no unclosed file warning + self.assert_warning(None, im.verify) im = Image.open(TEST_PNG_FILE) im.load() @@ -340,7 +364,8 @@ class TestFilePng(PillowTestCase): broken_crc_chunk_data = chunk_data[:-1] + b'q' # break CRC image_data = HEAD + broken_crc_chunk_data + TAIL - self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, BytesIO(image_data)) + self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, + BytesIO(image_data)) ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -356,7 +381,8 @@ class TestFilePng(PillowTestCase): 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 @@ -419,7 +445,7 @@ class TestFilePng(PillowTestCase): im = roundtrip(im, pnginfo=info) self.assertEqual(im.info, {"Text": value}) - if str is not bytes: + 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 @@ -538,11 +564,47 @@ class TestFilePng(PillowTestCase): chunks = PngImagePlugin.getchunks(im) self.assertEqual(len(chunks), 3) + def test_textual_chunks_after_idat(self): + im = Image.open("Tests/images/hopper.png") + self.assertIn('comment', im.text.keys()) + for k, v in { + 'date:create': '2014-09-04T09:37:08+03:00', + 'date:modify': '2014-09-04T09:37:08+03:00', + }.items(): + self.assertEqual(im.text[k], v) -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") + # Raises a SyntaxError in load_end + im = Image.open("Tests/images/broken_data_stream.png") + with self.assertRaises(IOError): + self.assertIsInstance(im.text, dict) + + # Raises a UnicodeDecodeError in load_end + im = Image.open("Tests/images/truncated_image.png") + # The file is truncated + self.assertRaises(IOError, lambda: im.text) + ImageFile.LOAD_TRUNCATED_IMAGES = True + self.assertIsInstance(im.text, dict) + ImageFile.LOAD_TRUNCATED_IMAGES = False + + # 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'}) + + @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') + + # 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") class TestTruncatedPngPLeaks(PillowLeakTestCase): mem_limit = 2*1024 # max increase in K - iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs + iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs def setUp(self): if "zip_encoder" not in codecs or "zip_decoder" not in codecs: @@ -564,7 +626,3 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): self._test_leak(core) finally: ImageFile.LOAD_TRUNCATED_IMAGES = False - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 937a9dc32..47f8b845e 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image @@ -49,7 +49,3 @@ class TestFilePpm(PillowTestCase): with self.assertRaises(IOError): Image.open('Tests/images/negative_size.ppm') - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index afc69694d..3b8a7add6 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,4 +1,4 @@ -from helper import hopper, unittest, PillowTestCase +from .helper import hopper, PillowTestCase from PIL import Image, PsdImagePlugin @@ -76,7 +76,3 @@ class TestImagePsd(PillowTestCase): im = Image.open("Tests/images/hopper_merged.psd") self.assertNotIn("icc_profile", im.info) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index f18fb13de..1ad70e24f 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image, SgiImagePlugin @@ -12,6 +12,7 @@ class TestFileSgi(PillowTestCase): im = Image.open(test_file) self.assert_image_equal(im, hopper()) + self.assertEqual(im.get_format_mimetype(), 'image/rgb') def test_rgb16(self): test_file = "Tests/images/hopper16.rgb" @@ -26,6 +27,7 @@ class TestFileSgi(PillowTestCase): im = Image.open(test_file) self.assert_image_similar(im, hopper('L'), 2) + self.assertEqual(im.get_format_mimetype(), 'image/sgi') def test_rgba(self): # Created with ImageMagick: @@ -35,6 +37,7 @@ class TestFileSgi(PillowTestCase): im = Image.open(test_file) target = Image.open('Tests/images/transparent.png') self.assert_image_equal(im, target) + self.assertEqual(im.get_format_mimetype(), 'image/sgi') def test_rle(self): # Created with ImageMagick: @@ -80,13 +83,9 @@ class TestFileSgi(PillowTestCase): reloaded = Image.open(out) self.assert_image_equal(im, reloaded) - + def test_unsupported_mode(self): im = hopper('LA') out = self.tempfile('temp.sgi') self.assertRaises(ValueError, im.save, out, format='sgi') - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index b54b92e04..f160272fd 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image from PIL import ImageSequence @@ -18,6 +18,12 @@ class TestImageSpider(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "SPIDER") + def test_unclosed_file(self): + def open(): + im = Image.open(TEST_FILE) + im.load() + self.assert_warning(None, open) + def test_save(self): # Arrange temp = self.tempfile('temp.spider') @@ -113,7 +119,3 @@ class TestImageSpider(PillowTestCase): for i, frame in enumerate(ImageSequence.Iterator(im)): if i > 1: self.fail("Non-stack DOS file test failed") - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 6bb6b98d4..65c00eea2 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import unittest, PillowTestCase, hopper from PIL import Image, SunImagePlugin @@ -45,6 +45,3 @@ class TestFileSun(PillowTestCase): # im.save(target_file) with Image.open(target_path) as target: self.assert_image_equal(im, target) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 3dd075042..cd2b95778 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image, TarIO @@ -12,25 +12,25 @@ 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 not available") + self.skipTest("neither jpeg nor zip support available") def test_sanity(self): - if "zip_decoder" in codecs: - tar = TarIO.TarIO(TEST_TAR_FILE, 'hopper.png') - im = Image.open(tar) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PNG") + for codec, test_path, format in [ + ['zip_decoder', 'hopper.png', 'PNG'], + ['jpeg_decoder', 'hopper.jpg', 'JPEG'] + ]: + if codec in codecs: + tar = TarIO.TarIO(TEST_TAR_FILE, test_path) + im = Image.open(tar) + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, format) - if "jpeg_decoder" in codecs: - tar = TarIO.TarIO(TEST_TAR_FILE, 'hopper.jpg') - im = Image.open(tar) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "JPEG") + def test_close(self): + tar = TarIO.TarIO(TEST_TAR_FILE, 'hopper.jpg') + tar.close() - -if __name__ == '__main__': - unittest.main() + def test_contextmanager(self): + with TarIO.TarIO(TEST_TAR_FILE, 'hopper.jpg'): + pass diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index ef3acfe65..0dbfb309c 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,10 +1,76 @@ -from helper import unittest, PillowTestCase +import os +from glob import glob +from itertools import product + +from .helper import PillowTestCase from PIL import Image +_TGA_DIR = os.path.join("Tests", "images", "tga") +_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") + + class TestFileTga(PillowTestCase): + _MODES = ("L", "LA", "P", "RGB", "RGBA") + _ORIGINS = ("tl", "bl") + + _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()))) + + for png_path in png_paths: + reference_im = Image.open(png_path) + self.assertEqual(reference_im.mode, mode) + + path_no_ext = os.path.splitext(png_path)[0] + for origin, rle in product(self._ORIGINS, (True, False)): + tga_path = "{}_{}_{}.tga".format( + path_no_ext, origin, "rle" if rle else "raw") + + original_im = Image.open(tga_path) + if rle: + self.assertEqual( + original_im.info["compression"], "tga_rle") + self.assertEqual( + original_im.info["orientation"], + self._ORIGIN_TO_ORIENTATION[origin]) + if mode == "P": + self.assertEqual( + original_im.getpalette(), + reference_im.getpalette()) + + self.assert_image_equal(original_im, reference_im) + + # Generate a new test name every time so the + # test will not fail with permission error + # on Windows. + out = self.tempfile("temp.tga") + + original_im.save(out, rle=rle) + saved_im = Image.open(out) + if rle: + self.assertEqual( + saved_im.info["compression"], + original_im.info["compression"]) + self.assertEqual( + saved_im.info["orientation"], + original_im.info["orientation"]) + if mode == "P": + self.assertEqual( + saved_im.getpalette(), + original_im.getpalette()) + + self.assert_image_equal(saved_im, original_im) + def test_id_field(self): # tga file with id field test_file = "Tests/images/tga_id_field.tga" @@ -29,40 +95,109 @@ class TestFileTga(PillowTestCase): test_file = "Tests/images/tga_id_field.tga" im = Image.open(test_file) - test_file = self.tempfile("temp.tga") + out = self.tempfile("temp.tga") # Save - im.save(test_file) - test_im = Image.open(test_file) + im.save(out) + test_im = Image.open(out) self.assertEqual(test_im.size, (100, 100)) + self.assertEqual(test_im.info["id_section"], im.info["id_section"]) # RGBA save - im.convert("RGBA").save(test_file) - test_im = Image.open(test_file) + im.convert("RGBA").save(out) + test_im = Image.open(out) self.assertEqual(test_im.size, (100, 100)) - # Unsupported mode save - self.assertRaises(IOError, lambda: im.convert("LA").save(test_file)) + def test_save_id_section(self): + test_file = "Tests/images/rgb32rle.tga" + im = Image.open(test_file) + + out = self.tempfile("temp.tga") + + # Check there is no id section + im.save(out) + test_im = Image.open(out) + self.assertNotIn("id_section", test_im.info) + + # Save with custom id section + im.save(out, id_section=b"Test content") + test_im = Image.open(out) + self.assertEqual(test_im.info["id_section"], b"Test content") + + # Save with custom id section 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]) + + test_file = "Tests/images/tga_id_field.tga" + im = Image.open(test_file) + + # Save with no id section + im.save(out, id_section="") + test_im = Image.open(out) + self.assertNotIn("id_section", test_im.info) + + def test_save_orientation(self): + test_file = "Tests/images/rgb32rle.tga" + im = Image.open(test_file) + self.assertEqual(im.info["orientation"], -1) + + out = self.tempfile("temp.tga") + + im.save(out, orientation=1) + test_im = Image.open(out) + self.assertEqual(test_im.info["orientation"], 1) def test_save_rle(self): test_file = "Tests/images/rgb32rle.tga" im = Image.open(test_file) + self.assertEqual(im.info["compression"], "tga_rle") - test_file = self.tempfile("temp.tga") + out = self.tempfile("temp.tga") # Save - im.save(test_file) - test_im = Image.open(test_file) + im.save(out) + test_im = Image.open(out) 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) # RGBA save - im.convert("RGBA").save(test_file) - test_im = Image.open(test_file) + im.convert("RGBA").save(out) + test_im = Image.open(out) self.assertEqual(test_im.size, (199, 199)) - # Unsupported mode save - self.assertRaises(IOError, lambda: im.convert("LA").save(test_file)) + test_file = "Tests/images/tga_id_field.tga" + im = Image.open(test_file) + self.assertNotIn("compression", im.info) + # Save with compression + im.save(out, compression="tga_rle") + test_im = Image.open(out) + self.assertEqual(test_im.info["compression"], "tga_rle") -if __name__ == '__main__': - unittest.main() + def test_save_l_transparency(self): + # There are 559 transparent pixels in la.tga. + num_transparent = 559 + + in_file = "Tests/images/la.tga" + im = Image.open(in_file) + self.assertEqual(im.mode, "LA") + self.assertEqual( + im.getchannel("A").getcolors()[0][0], num_transparent) + + 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.assert_image_equal(im, test_im) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 6edc94aed..9a4104b78 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,11 +1,11 @@ import logging from io import BytesIO -import struct import sys -from helper import unittest, PillowTestCase, hopper, py3 +from .helper import unittest, PillowTestCase, hopper from PIL import Image, TiffImagePlugin +from PIL._util import py3 from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION, RESOLUTION_UNIT logger = logging.getLogger(__name__) @@ -26,19 +26,25 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.format, "TIFF") hopper("1").save(filename) - im = Image.open(filename) + Image.open(filename) hopper("L").save(filename) - im = Image.open(filename) + Image.open(filename) hopper("P").save(filename) - im = Image.open(filename) + Image.open(filename) hopper("RGB").save(filename) - im = Image.open(filename) + Image.open(filename) hopper("I").save(filename) - im = Image.open(filename) + Image.open(filename) + + def test_unclosed_file(self): + def open(): + im = Image.open("Tests/images/multipage.tiff") + im.load() + self.assert_warning(None, open) def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] @@ -58,12 +64,24 @@ 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): - with self.assertRaises(Exception): - ImageFileDirectory_v2.legacy_api = None + 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) def test_xyres_tiff(self): filename = "Tests/images/pil168.tif" @@ -129,11 +147,8 @@ class TestFileTiff(PillowTestCase): def test_bad_exif(self): i = Image.open('Tests/images/hopper_bad_exif.jpg') - try: - self.assert_warning(UserWarning, i._getexif) - except struct.error: - self.fail( - "Bad EXIF data passed incorrect values to _binary unpack") + # Should not raise struct.error. + self.assert_warning(UserWarning, i._getexif) def test_save_rgba(self): im = hopper("RGBA") @@ -178,8 +193,8 @@ class TestFileTiff(PillowTestCase): im = Image.open('Tests/images/16bit.s.tif') im.load() self.assertEqual(im.mode, 'I') - self.assertEqual(im.getpixel((0,0)),32767) - self.assertEqual(im.getpixel((0,1)),0) + self.assertEqual(im.getpixel((0, 0)), 32767) + self.assertEqual(im.getpixel((0, 1)), 0) def test_12bit_rawmode(self): """ Are we generating the same interpretation @@ -205,6 +220,10 @@ class TestFileTiff(PillowTestCase): 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') + def test_n_frames(self): for path, n_frames in [ ['Tests/images/multipage-lastframe.tif', 1], @@ -388,7 +407,6 @@ class TestFileTiff(PillowTestCase): 'y_resolution': 36} filename = self.tempfile("temp.tif") hopper("RGB").save(filename, **kwargs) - from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION im = Image.open(filename) # legacy interface @@ -399,7 +417,6 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.tag_v2[X_RESOLUTION], 72) self.assertEqual(im.tag_v2[Y_RESOLUTION], 36) - def test_roundtrip_tiff_uint16(self): # Test an image of all '0' values pixel_value = 0x1234 @@ -414,6 +431,40 @@ class TestFileTiff(PillowTestCase): self.assert_image_equal(im, reloaded) + def test_strip_raw(self): + infile = "Tests/images/tiff_strip_raw.tif" + im = Image.open(infile) + + self.assert_image_equal_tofile(im, + "Tests/images/tiff_adobe_deflate.png") + + def test_strip_planar_raw(self): + # gdal_translate -of GTiff -co INTERLEAVE=BAND \ + # tiff_strip_raw.tif tiff_strip_planar_raw.tiff + infile = "Tests/images/tiff_strip_planar_raw.tif" + im = Image.open(infile) + + self.assert_image_equal_tofile(im, + "Tests/images/tiff_adobe_deflate.png") + + 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") + + def test_tiled_planar_raw(self): + # gdal_translate -of GTiff -co TILED=YES -co BLOCKXSIZE=32 \ + # -co BLOCKYSIZE=32 -co INTERLEAVE=BAND \ + # tiff_tiled_raw.tif tiff_tiled_planar_raw.tiff + infile = "Tests/images/tiff_tiled_planar_raw.tif" + im = Image.open(infile) + + self.assert_image_equal_tofile(im, + "Tests/images/tiff_adobe_deflate.png") + def test_tiff_save_all(self): import io import os @@ -442,13 +493,13 @@ class TestFileTiff(PillowTestCase): for im in ims: yield im mp = io.BytesIO() - im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims)) + im.save(mp, format="TIFF", save_all=True, + append_images=imGenerator(ims)) mp.seek(0, os.SEEK_SET) reread = Image.open(mp) self.assertEqual(reread.n_frames, 3) - def test_saving_icc_profile(self): # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs @@ -490,6 +541,7 @@ class TestFileTiff(PillowTestCase): im.load() self.assertFalse(fp.closed) + @unittest.skipUnless(sys.platform.startswith('win32'), "Windows only") class TestFileTiffW32(PillowTestCase): def test_fd_leak(self): @@ -503,7 +555,7 @@ class TestFileTiffW32(PillowTestCase): im = Image.open(tmpfile) fp = im.fp self.assertFalse(fp.closed) - self.assertRaises(Exception, os.remove, tmpfile) + self.assertRaises(WindowsError, os.remove, tmpfile) im.load() self.assertTrue(fp.closed) @@ -513,7 +565,3 @@ class TestFileTiffW32(PillowTestCase): # this should not fail, as load should have closed the file pointer, # and close should have closed the mmap os.remove(tmpfile) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index bb5768046..14ec3ab6c 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,7 +1,7 @@ import io import struct -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image, TiffImagePlugin, TiffTags from PIL.TiffImagePlugin import _limit_rational, IFDRational @@ -56,7 +56,8 @@ class TestFileTiffMetadata(PillowTestCase): loaded = Image.open(f) self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (len(bindata),)) + self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], + (len(bindata),)) self.assertEqual(loaded.tag[ImageJMetaData], bindata) self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata) @@ -70,14 +71,15 @@ class TestFileTiffMetadata(PillowTestCase): self.assertAlmostEqual(loaded_double, doubledata) # check with 2 element ImageJMetaDataByteCounts, issue #2006 - + info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8) img.save(f, tiffinfo=info) loaded = Image.open(f) - self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (8, len(bindata) - 8)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (8, len(bindata) - 8)) - + 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') @@ -133,9 +135,9 @@ class TestFileTiffMetadata(PillowTestCase): for k, v in original.items(): if isinstance(v, IFDRational): original[k] = IFDRational(*_limit_rational(v, 2**31)) - if isinstance(v, tuple) and isinstance(v[0], IFDRational): - original[k] = tuple([IFDRational( - *_limit_rational(elt, 2**31)) for elt in v]) + elif isinstance(v, tuple) and isinstance(v[0], IFDRational): + original[k] = tuple(IFDRational(*_limit_rational(elt, 2**31)) + for elt in v) ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets'] @@ -170,10 +172,8 @@ class TestFileTiffMetadata(PillowTestCase): f = io.BytesIO(b'II*\x00\x08\x00\x00\x00') head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) - try: - self.assert_warning(UserWarning, info.load, f) - except struct.error: - self.fail("Should not be struct errors there.") + # Should not raise struct.error. + self.assert_warning(UserWarning, info.load, f) def test_iccprofile(self): # https://github.com/python-pillow/Pillow/issues/1462 @@ -187,7 +187,8 @@ class TestFileTiffMetadata(PillowTestCase): 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. + # 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) @@ -224,10 +225,8 @@ class TestFileTiffMetadata(PillowTestCase): head = data.read(8) info = TiffImagePlugin.ImageFileDirectory_v2(head) info.load(data) - try: - info = dict(info) - except ValueError: - self.fail("Should not be struct value error there.") + # Should not raise ValueError. + info = dict(info) self.assertIn(33432, info) def test_PhotoshopInfo(self): @@ -243,14 +242,8 @@ class TestFileTiffMetadata(PillowTestCase): 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 - try: - self.assert_warning(UserWarning, lambda: ifd[277]) - except ValueError: - self.fail("Invalid Metadata count should not cause a Value Error.") - - -if __name__ == '__main__': - unittest.main() + # Should not raise ValueError. + self.assert_warning(UserWarning, lambda: ifd[277]) diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py new file mode 100644 index 000000000..1e0a835d2 --- /dev/null +++ b/Tests/test_file_wal.py @@ -0,0 +1,19 @@ +from .helper import PillowTestCase + +from PIL import WalImageFile + + +class TestFileWal(PillowTestCase): + + def test_open(self): + # Arrange + TEST_FILE = "Tests/images/hopper.wal" + + # Act + im = WalImageFile.open(TEST_FILE) + + # Assert + self.assertEqual(im.format, "WAL") + self.assertEqual(im.format_description, "Quake2 Texture") + self.assertEqual(im.mode, "P") + self.assertEqual(im.size, (128, 128)) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 06e274d0a..7e6fad93c 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,6 +1,6 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import unittest, PillowTestCase, hopper -from PIL import Image +from PIL import Image, WebPImagePlugin try: from PIL import _webp @@ -9,15 +9,26 @@ except ImportError: HAVE_WEBP = False +class TestUnsupportedWebp(PillowTestCase): + def test_unsupported(self): + if HAVE_WEBP: + WebPImagePlugin.SUPPORTED = False + + file_path = "Tests/images/hopper.webp" + self.assert_warning( + UserWarning, + lambda: self.assertRaises(IOError, Image.open, file_path) + ) + + if HAVE_WEBP: + WebPImagePlugin.SUPPORTED = True + + +@unittest.skipIf(not HAVE_WEBP, "WebP support not installed") class TestFileWebp(PillowTestCase): def setUp(self): - if not HAVE_WEBP: - self.skipTest('WebP support not installed') - return - - # WebPAnimDecoder only returns RGBA or RGBX, never RGB - self.rgb_mode = "RGBX" if _webp.HAVE_WEBPANIM else "RGB" + self.rgb_mode = "RGB" def test_version(self): _webp.WebPDecoderVersion() @@ -29,8 +40,7 @@ class TestFileWebp(PillowTestCase): Does it have the bits we expect? """ - file_path = "Tests/images/hopper.webp" - image = Image.open(file_path) + image = Image.open("Tests/images/hopper.webp") self.assertEqual(image.mode, self.rgb_mode) self.assertEqual(image.size, (128, 128)) @@ -40,9 +50,8 @@ class TestFileWebp(PillowTestCase): # generated with: # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm - target = Image.open('Tests/images/hopper_webp_bits.ppm') - target = target.convert(self.rgb_mode) - self.assert_image_similar(image, target, 20.0) + self.assert_image_similar_tofile( + image, 'Tests/images/hopper_webp_bits.ppm', 1.0) def test_write_rgb(self): """ @@ -61,13 +70,9 @@ class TestFileWebp(PillowTestCase): image.load() image.getdata() - # If we're using the exact same version of WebP, this test should pass. - # but it doesn't if the WebP is generated on Ubuntu and tested on - # Fedora. - # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm - # target = Image.open('Tests/images/hopper_webp_write.ppm') - # self.assert_image_equal(image, target) + self.assert_image_similar_tofile( + image, 'Tests/images/hopper_webp_write.ppm', 12.0) # This test asserts that the images are similar. If the average pixel # difference between the two images is less than the epsilon value, @@ -135,6 +140,35 @@ class TestFileWebp(PillowTestCase): self.assertRaises(TypeError, _webp.WebPAnimDecoder) self.assertRaises(TypeError, _webp.WebPDecode) + def test_no_resource_warning(self): + file_path = "Tests/images/hopper.webp" + image = Image.open(file_path) -if __name__ == '__main__': - unittest.main() + temp_file = self.tempfile("temp.webp") + self.assert_warning(None, image.save, temp_file) + + def test_file_pointer_could_be_reused(self): + file_path = "Tests/images/hopper.webp" + 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") + def test_background_from_gif(self): + im = Image.open("Tests/images/chi.gif") + 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 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)]) + self.assertLess(difference, 5) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 60a324d18..c868fa1df 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,22 +1,17 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import unittest, PillowTestCase, hopper from PIL import Image try: from PIL import _webp except ImportError: - pass - # Skip in setUp() + _webp = None +@unittest.skipIf(_webp is None, "WebP support not installed") class TestFileWebpAlpha(PillowTestCase): def setUp(self): - try: - from PIL import _webp - except ImportError: - self.skipTest('WebP support not installed') - if _webp.WebPDecoderBuggyAlpha(self): self.skipTest("Buggy early version of WebP installed, " "not testing transparency") @@ -120,7 +115,3 @@ class TestFileWebpAlpha(PillowTestCase): target = Image.open(file_path).convert("RGBA") self.assert_image_similar(image, target, 25.0) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index f98cde764..c751545c7 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image @@ -83,7 +83,8 @@ class TestFileWebpAnimation(PillowTestCase): temp_file1 = self.tempfile("temp.webp") frame1.copy().save(temp_file1, - save_all=True, append_images=[frame2], lossless=True) + save_all=True, append_images=[frame2], + lossless=True) check(temp_file1) # Tests appending using a generator @@ -92,7 +93,8 @@ class TestFileWebpAnimation(PillowTestCase): yield im temp_file2 = self.tempfile("temp_generator.webp") frame1.copy().save(temp_file2, - save_all=True, append_images=imGenerator([frame2]), lossless=True) + save_all=True, append_images=imGenerator([frame2]), + lossless=True) check(temp_file2) def test_timestamp_and_duration(self): @@ -149,7 +151,3 @@ class TestFileWebpAnimation(PillowTestCase): self.assertEqual(im.info["duration"], dur) self.assertEqual(im.info["timestamp"], ts) ts -= dur - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 10354c55f..528c9177d 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -16,11 +16,10 @@ class TestFileWebpLossless(PillowTestCase): self.skipTest('WebP support not installed') return - if (_webp.WebPDecoderVersion() < 0x0200): + if _webp.WebPDecoderVersion() < 0x0200: self.skipTest('lossless not included') - # WebPAnimDecoder only returns RGBA or RGBX, never RGB - self.rgb_mode = "RGBX" if _webp.HAVE_WEBPANIM else "RGB" + self.rgb_mode = "RGB" def test_write_lossless_rgb(self): temp_file = self.tempfile("temp.webp") @@ -37,7 +36,3 @@ class TestFileWebpLossless(PillowTestCase): image.getdata() self.assert_image_equal(image, hopper(self.rgb_mode)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index c04443f46..402b6ce42 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image @@ -133,7 +133,3 @@ class TestFileWebpMetadata(PillowTestCase): 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)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 1a15a514f..146888491 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image from PIL import WmfImagePlugin @@ -51,7 +51,3 @@ class TestFileWmf(PillowTestCase): for ext in [".wmf", ".emf"]: tmpfile = self.tempfile("temp"+ext) self.assertRaises(IOError, im.save, tmpfile) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 398dae98c..fbcb30e90 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image @@ -60,7 +60,3 @@ class TestFileXbm(PillowTestCase): # Assert self.assertEqual(im.mode, '1') self.assertEqual(im.size, (128, 128)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 4fa3f743f..b57cda2e3 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image, XpmImagePlugin @@ -33,7 +33,3 @@ class TestFileXpm(PillowTestCase): # Assert self.assertEqual(len(data), 16384) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index d0256cabf..11672ebae 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -1,4 +1,4 @@ -from helper import hopper, unittest, PillowTestCase +from .helper import hopper, PillowTestCase from PIL import Image, XVThumbImagePlugin @@ -34,7 +34,3 @@ class TestFileXVThumb(PillowTestCase): # Act / Assert self.assertRaises(SyntaxError, XVThumbImagePlugin.XVThumbImageFile, invalid_file) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 7c8fe579e..be49e818e 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import FontFile, BdfFontFile @@ -18,7 +18,3 @@ class TestFontBdf(PillowTestCase): def test_invalid_file(self): with open("Tests/images/flower.jpg", "rb") as fp: self.assertRaises(SyntaxError, BdfFontFile.BdfFontFile, fp) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 709339233..119012bc5 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,34 +1,33 @@ from __future__ import division -from helper import unittest, PillowLeakTestCase +from .helper import unittest, PillowLeakTestCase import sys from PIL import Image, features, ImageDraw, ImageFont -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") + +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or macOS") class TestTTypeFontLeak(PillowLeakTestCase): # fails at iteration 3 in master iterations = 10 - mem_limit = 4096 #k + 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 + self._test_leak(lambda: draw.text((0, 0), "some text "*1024, # ~10k font=font, fill="black")) - - @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) self._test_font(ttype) + class TestDefaultFontLeak(TestTTypeFontLeak): # fails at iteration 37 in master iterations = 100 - mem_limit = 1024 #k - + mem_limit = 1024 # k + def test_leak(self): default_font = ImageFont.load_default() self._test_font(default_font) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index dc4c586c0..d092634f3 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,7 +1,8 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image, FontFile, PcfFontFile from PIL import ImageFont, ImageDraw +from PIL._util import py3 codecs = dir(Image.core) @@ -20,7 +21,7 @@ class TestFontPcf(PillowTestCase): with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file) self.assertIsInstance(font, FontFile.FontFile) - #check the number of characters in the font + # check the number of characters in the font self.assertEqual(len([_f for _f in font.glyph if _f]), 223) tempname = self.tempfile("temp.pil") @@ -46,40 +47,35 @@ class TestFontPcf(PillowTestCase): def test_draw(self): tempname = self.save_font() font = ImageFont.load(tempname) - im = Image.new("L", (130,30), "white") + im = Image.new("L", (130, 30), "white") draw = ImageDraw.Draw(im) draw.text((0, 0), message, 'black', font=font) with Image.open('Tests/images/test_draw_pbm_target.png') as target: self.assert_image_similar(im, target, 0) - + def test_textsize(self): tempname = self.save_font() font = ImageFont.load(tempname) for i in range(255): - (dx,dy) = font.getsize(chr(i)) + (dx, dy) = font.getsize(chr(i)) self.assertEqual(dy, 20) - self.assertIn(dx, (0,10)) + self.assertIn(dx, (0, 10)) for l in range(len(message)): msg = message[:l+1] - self.assertEqual(font.getsize(msg), (len(msg)*10,20)) + self.assertEqual(font.getsize(msg), (len(msg)*10, 20)) def _test_high_characters(self, message): tempname = self.save_font() font = ImageFont.load(tempname) - im = Image.new("L", (750,30) , "white") + im = Image.new("L", (750, 30), "white") draw = ImageDraw.Draw(im) draw.text((0, 0), message, "black", font=font) with Image.open('Tests/images/high_ascii_chars.png') as target: self.assert_image_similar(im, target, 0) - def test_high_characters(self): message = "".join(chr(i+1) for i in range(140, 232)) self._test_high_characters(message) # accept bytes instances in Py3. - if bytes is not str: + if py3: self._test_high_characters(message.encode('latin1')) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 2cc54c910..3e1c00c9e 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -1,6 +1,7 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image +from PIL._util import py3 import colorsys import itertools @@ -12,15 +13,11 @@ class TestFormatHSV(PillowTestCase): return float(i)/255.0 def str_to_float(self, i): - return float(ord(i))/255.0 - def to_int(self, f): - return int(f*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)) @@ -46,10 +43,6 @@ class TestFormatHSV(PillowTestCase): img = Image.merge('RGB', (r, g, b)) - # print(("%d, %d -> "% (int(1.75*px),int(.25*px))) + \ - # "(%s, %s, %s)"%img.getpixel((1.75*px, .25*px))) - # print(("%d, %d -> "% (int(.75*px),int(.25*px))) + \ - # "(%s, %s, %s)"%img.getpixel((.75*px, .25*px))) return img def to_xxx_colorsys(self, im, func, mode): @@ -57,10 +50,10 @@ class TestFormatHSV(PillowTestCase): (r, g, b) = im.split() - if bytes is str: - conv_func = self.str_to_float - else: + if py3: conv_func = self.int_to_float + else: + conv_func = self.str_to_float if hasattr(itertools, 'izip'): iter_helper = itertools.izip @@ -72,11 +65,11 @@ class TestFormatHSV(PillowTestCase): for (_r, _g, _b) in iter_helper(r.tobytes(), g.tobytes(), b.tobytes())] - if str is bytes: - new_bytes = b''.join(chr(h)+chr(s)+chr(v) for ( + 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(bytes(chr(h)+chr(s)+chr(v), 'latin-1') for ( + new_bytes = b''.join(chr(h)+chr(s)+chr(v) for ( h, s, v) in converted) hsv = Image.frombytes(mode, r.size, new_bytes) @@ -94,15 +87,6 @@ class TestFormatHSV(PillowTestCase): im = src.convert('HSV') comparable = self.to_hsv_colorsys(src) - # print(im.getpixel((448, 64))) - # print(comparable.getpixel((448, 64))) - - # print(im.split()[0].histogram()) - # print(comparable.split()[0].histogram()) - - # im.split()[0].show() - # comparable.split()[0].show() - 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), @@ -110,16 +94,9 @@ class TestFormatHSV(PillowTestCase): self.assert_image_similar(im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong") - # print(im.getpixel((192, 64))) - comparable = src im = im.convert('RGB') - # im.split()[0].show() - # comparable.split()[0].show() - # print(im.getpixel((192, 64))) - # print(comparable.getpixel((192, 64))) - 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), @@ -131,12 +108,6 @@ class TestFormatHSV(PillowTestCase): im = hopper('RGB').convert('HSV') comparable = self.to_hsv_colorsys(hopper('RGB')) -# print([ord(x) for x in im.split()[0].tobytes()[:80]]) -# print([ord(x) for x in comparable.split()[0].tobytes()[:80]]) - -# print(im.split()[0].histogram()) -# print(comparable.split()[0].histogram()) - 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), @@ -149,12 +120,6 @@ class TestFormatHSV(PillowTestCase): converted = comparable.convert('RGB') comparable = self.to_rgb_colorsys(comparable) - # print(converted.split()[1].histogram()) - # print(target.split()[1].histogram()) - - # print([ord(x) for x in target.split()[1].tobytes()[:80]]) - # print([ord(x) for x in converted.split()[1].tobytes()[:80]]) - self.assert_image_similar(converted.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong") @@ -164,7 +129,3 @@ class TestFormatHSV(PillowTestCase): self.assert_image_similar(converted.getchannel(2), comparable.getchannel(2), 3, "B conversion is wrong") - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index a243afe62..8a096e672 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image @@ -40,7 +40,3 @@ class TestFormatLab(PillowTestCase): k = i.getpixel((0, 0)) self.assertEqual(k, (128, 228, 128)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image.py b/Tests/test_image.py index 6f6d1983e..330048057 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,6 +1,7 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import unittest, PillowTestCase, hopper from PIL import Image +from PIL._util import py3 import os @@ -55,17 +56,16 @@ class TestImage(PillowTestCase): self.assertEqual(im.width, 1) self.assertEqual(im.height, 2) - im.size = (3, 4) - self.assertEqual(im.width, 3) - self.assertEqual(im.height, 4) + with self.assertRaises(AttributeError): + im.size = (3, 4) def test_invalid_image(self): - if str is bytes: - import StringIO - im = StringIO.StringIO('') - else: + if py3: import io im = io.BytesIO(b'') + else: + import StringIO + im = StringIO.StringIO('') self.assertRaises(IOError, Image.open, im) def test_bad_mode(self): @@ -112,7 +112,6 @@ class TestImage(PillowTestCase): self.assertRaises(ValueError, im.save, temp_file) def test_internals(self): - im = Image.new("L", (100, 100)) im.readonly = 1 im._copy() @@ -122,8 +121,15 @@ class TestImage(PillowTestCase): im.paste(0, (0, 0, 100, 100)) self.assertFalse(im.readonly) - test_file = self.tempfile("temp.ppm") - im._dump(test_file) + def test_dump(self): + im = Image.new("L", (10, 10)) + im._dump(self.tempfile("temp_L.ppm")) + + im = Image.new("RGB", (10, 10)) + im._dump(self.tempfile("temp_RGB.ppm")) + + im = Image.new("HSV", (10, 10)) + self.assertRaises(ValueError, im._dump, self.tempfile("temp_HSV.ppm")) def test_comparison_with_other_type(self): # Arrange @@ -133,7 +139,6 @@ class TestImage(PillowTestCase): # Act/Assert # Shouldn't cause AttributeError (#774) self.assertFalse(item is None) - self.assertFalse(item == None) self.assertFalse(item == num) def test_expand_x(self): @@ -280,9 +285,9 @@ class TestImage(PillowTestCase): source.alpha_composite, over, (0, 0), "invalid destination") self.assertRaises(ValueError, - source.alpha_composite, over, (0)) + source.alpha_composite, over, 0) self.assertRaises(ValueError, - source.alpha_composite, over, (0, 0), (0)) + source.alpha_composite, over, (0, 0), 0) self.assertRaises(ValueError, source.alpha_composite, over, (0, -1)) self.assertRaises(ValueError, @@ -510,7 +515,7 @@ class TestImage(PillowTestCase): self.assertEqual(new_im.palette.tobytes(), palette_result.tobytes()) else: - self.assertEqual(new_im.palette, None) + self.assertIsNone(new_im.palette) _make_new(im, im_p, im_p.palette) _make_new(im_p, im, None) @@ -555,7 +560,3 @@ class TestRegistry(PillowTestCase): 'DoesNotExist', ('args',), extra=('extra',)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index d0fcea1d4..937584cff 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,15 +1,21 @@ -from helper import unittest, PillowTestCase, hopper, on_appveyor - -try: - from PIL import PyAccess -except ImportError: - # Skip in setUp() - pass +from .helper import unittest, PillowTestCase, hopper, on_appveyor from PIL import Image import sys import os +# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 +# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 +if os.environ.get("PYTHONOPTIMIZE") == "2": + cffi = None +else: + try: + from PIL import PyAccess + import cffi + except ImportError: + cffi = None + + class AccessTest(PillowTestCase): # initial value _init_cffi_access = Image.USE_CFFI_ACCESS @@ -58,6 +64,44 @@ class TestImagePutPixel(AccessTest): self.assert_image_equal(im1, im2) + def test_sanity_negative_index(self): + im1 = hopper() + im2 = Image.new(im1.mode, im1.size, 0) + + width, height = im1.size + self.assertEqual(im1.getpixel((0, 0)), im1.getpixel((-width, -height))) + self.assertEqual(im1.getpixel((-1, -1)), + im1.getpixel((width-1, height-1))) + + for y in range(-1, -im1.size[1]-1, -1): + for x in range(-1, -im1.size[0]-1, -1): + pos = x, y + im2.putpixel(pos, im1.getpixel(pos)) + + self.assert_image_equal(im1, im2) + + im2 = Image.new(im1.mode, im1.size, 0) + im2.readonly = 1 + + for y in range(-1, -im1.size[1]-1, -1): + for x in range(-1, -im1.size[0]-1, -1): + pos = x, y + im2.putpixel(pos, im1.getpixel(pos)) + + self.assertFalse(im2.readonly) + self.assert_image_equal(im1, im2) + + im2 = Image.new(im1.mode, im1.size, 0) + + pix1 = im1.load() + pix2 = im2.load() + + for y in range(-1, -im1.size[1]-1, -1): + for x in range(-1, -im1.size[0]-1, -1): + pix2[x, y] = pix1[x, y] + + self.assert_image_equal(im1, im2) + class TestImageGetPixel(AccessTest): @staticmethod @@ -79,23 +123,43 @@ class TestImageGetPixel(AccessTest): im.getpixel((0, 0)), c, "put/getpixel roundtrip failed for mode %s, color %s" % (mode, c)) + # check putpixel negative index + im.putpixel((-1, -1), c) + self.assertEqual( + im.getpixel((-1, -1)), c, + "put/getpixel roundtrip negative index failed" + " for mode %s, color %s" % (mode, c)) + # Check 0 im = Image.new(mode, (0, 0), None) with self.assertRaises(IndexError): im.putpixel((0, 0), c) with self.assertRaises(IndexError): im.getpixel((0, 0)) + # Check 0 negative index + with self.assertRaises(IndexError): + im.putpixel((-1, -1), c) + with self.assertRaises(IndexError): + im.getpixel((-1, -1)) # check initial color im = Image.new(mode, (1, 1), c) self.assertEqual( im.getpixel((0, 0)), c, "initial color failed for mode %s, color %s " % (mode, c)) + # check initial color negative index + self.assertEqual( + im.getpixel((-1, -1)), c, + "initial color failed with negative index" + "for mode %s, color %s " % (mode, c)) # Check 0 im = Image.new(mode, (0, 0), c) with self.assertRaises(IndexError): im.getpixel((0, 0)) + # Check 0 negative index + with self.assertRaises(IndexError): + im.getpixel((-1, -1)) def test_basic(self): for mode in ("1", "L", "LA", "I", "I;16", "I;16B", "F", @@ -111,39 +175,27 @@ class TestImageGetPixel(AccessTest): self.check(mode, 2**15+1) self.check(mode, 2**16-1) + def test_p_putpixel_rgb_rgba(self): + for color in [(255, 0, 0), (255, 0, 0, 255)]: + im = Image.new("P", (1, 1), 0) + im.putpixel((0, 0), color) + self.assertEqual(im.convert("RGB").getpixel((0, 0)), (255, 0, 0)) + +@unittest.skipIf(cffi is None, "No cffi") class TestCffiPutPixel(TestImagePutPixel): _need_cffi_access = True - def setUp(self): - try: - import cffi - assert cffi # silence warning - except ImportError: - self.skipTest("No cffi") - +@unittest.skipIf(cffi is None, "No cffi") class TestCffiGetPixel(TestImageGetPixel): _need_cffi_access = True - def setUp(self): - try: - import cffi - assert cffi # silence warning - except ImportError: - self.skipTest("No cffi") - +@unittest.skipIf(cffi is None, "No cffi") class TestCffi(AccessTest): _need_cffi_access = True - def setUp(self): - try: - import cffi - assert cffi # silence warning - except ImportError: - self.skipTest("No cffi") - def _test_get_access(self, im): """Do we get the same thing as the old pixel access @@ -248,17 +300,21 @@ class TestCffi(AccessTest): # pixels can contain garbage if image is released self.assertEqual(px[i, 0], 0) + def test_p_putpixel_rgb_rgba(self): + for color in [(255, 0, 0), (255, 0, 0, 255)]: + im = Image.new("P", (1, 1), 0) + access = PyAccess.new(im, False) + access.putpixel((0, 0), color) + self.assertEqual(im.convert("RGB").getpixel((0, 0)), (255, 0, 0)) + class TestEmbeddable(unittest.TestCase): @unittest.skipIf(not sys.platform.startswith('win32') or - sys.version_info[:2] == (3, 4) or - on_appveyor(), # failing on appveyor when run from - # subprocess, not from shell - "requires Python 2.7 or >=3.5 for Windows") + on_appveyor(), + "Failing on AppVeyor when run from subprocess, not from shell") def test_embeddable(self): import subprocess import ctypes - import setuptools from distutils import ccompiler, sysconfig with open('embed_pil.c', 'w') as fh: @@ -294,8 +350,9 @@ int main(int argc, char* argv[]) 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') - print (libdir) + 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') @@ -309,6 +366,3 @@ int main(int argc, char* argv[]) process = subprocess.Popen(['embed_pil.exe'], env=env) process.communicate() self.assertEqual(process.returncode, 0) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 11c2648bb..cf104217a 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -15,9 +15,11 @@ class TestImageArray(PillowTestCase): 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)) @@ -53,7 +55,3 @@ class TestImageArray(PillowTestCase): self.assertEqual(test("RGB"), ("RGB", (128, 100), True)) self.assertEqual(test("RGBA"), ("RGBA", (128, 100), True)) self.assertEqual(test("RGBX"), ("RGBA", (128, 100), True)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 9fd0463d4..1ba1794eb 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -67,13 +67,13 @@ class TestImageConvert(PillowTestCase): f = self.tempfile('temp.png') - l = im.convert('L') - self.assertEqual(l.info['transparency'], 0) # undone - l.save(f) + im_l = im.convert('L') + self.assertEqual(im_l.info['transparency'], 0) # undone + im_l.save(f) - rgb = im.convert('RGB') - self.assertEqual(rgb.info['transparency'], (0, 0, 0)) # undone - rgb.save(f) + 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 @@ -83,13 +83,12 @@ class TestImageConvert(PillowTestCase): im.info['transparency'] = 128 # Act - rgba = im.convert('RGBA') + im_rgba = im.convert('RGBA') # Assert - self.assertNotIn('transparency', rgba.info) + self.assertNotIn('transparency', im_rgba.info) # https://github.com/python-pillow/Pillow/issues/2702 - self.assertEqual(rgba.palette, None) - + self.assertIsNone(im_rgba.palette) def test_trns_l(self): im = hopper('L') @@ -97,19 +96,20 @@ class TestImageConvert(PillowTestCase): f = self.tempfile('temp.png') - rgb = im.convert('RGB') - self.assertEqual(rgb.info['transparency'], (128, 128, 128)) # undone - rgb.save(f) + im_rgb = im.convert('RGB') + self.assertEqual(im_rgb.info['transparency'], + (128, 128, 128)) # undone + im_rgb.save(f) - p = im.convert('P') - self.assertIn('transparency', p.info) - p.save(f) + im_p = im.convert('P') + self.assertIn('transparency', im_p.info) + im_p.save(f) - p = self.assert_warning( + im_p = self.assert_warning( UserWarning, im.convert, 'P', palette=Image.ADAPTIVE) - self.assertNotIn('transparency', p.info) - p.save(f) + self.assertNotIn('transparency', im_p.info) + im_p.save(f) def test_trns_RGB(self): im = hopper('RGB') @@ -117,23 +117,35 @@ class TestImageConvert(PillowTestCase): f = self.tempfile('temp.png') - l = im.convert('L') - self.assertEqual(l.info['transparency'], l.getpixel((0, 0))) # undone - l.save(f) + im_l = im.convert('L') + self.assertEqual(im_l.info['transparency'], + im_l.getpixel((0, 0))) # undone + im_l.save(f) - p = im.convert('P') - self.assertIn('transparency', p.info) - p.save(f) + im_p = im.convert('P') + self.assertIn('transparency', im_p.info) + im_p.save(f) - p = im.convert('RGBA') - self.assertNotIn('transparency', p.info) - p.save(f) + im_rgba = im.convert('RGBA') + self.assertNotIn('transparency', im_rgba.info) + im_rgba.save(f) - p = self.assert_warning( + im_p = self.assert_warning( UserWarning, im.convert, 'P', palette=Image.ADAPTIVE) - self.assertNotIn('transparency', p.info) - p.save(f) + 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') + + # Should not raise ValueError: unrecognized raw mode + im_p.load() def test_p_la(self): im = hopper('RGBA') @@ -175,6 +187,7 @@ class TestImageConvert(PillowTestCase): def matrix_convert(mode): # Arrange im = hopper('RGB') + im.info['transparency'] = (255, 0, 0) matrix = ( 0.412453, 0.357580, 0.180423, 0, 0.212671, 0.715160, 0.072169, 0, @@ -191,8 +204,12 @@ class TestImageConvert(PillowTestCase): target = Image.open('Tests/images/hopper-XYZ.png') if converted_im.mode == 'RGB': self.assert_image_similar(converted_im, target, 3) + self.assertEqual(converted_im.info['transparency'], + (105, 54, 4)) else: - self.assert_image_similar(converted_im, target.getchannel(0), 1) + self.assert_image_similar(converted_im, + target.getchannel(0), 1) + self.assertEqual(converted_im.info['transparency'], 105) matrix_convert('RGB') matrix_convert('L') @@ -213,7 +230,3 @@ class TestImageConvert(PillowTestCase): # Assert # No change self.assert_image_equal(converted_im, im) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index bb1246a73..19679fe22 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -40,7 +40,3 @@ class TestImageCopy(PillowTestCase): out = im.copy() self.assertEqual(out.mode, im.mode) self.assertEqual(out.size, im.size) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index fe92dd865..45123a631 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -102,7 +102,3 @@ class TestImageCrop(PillowTestCase): cropped = im.crop((10, 10, 20, 20)) self.assertEqual(cropped.size, (10, 10)) self.assertEqual(cropped.getdata()[2], (0, 0, 0)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 12f5e0e9f..9ebd68945 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, fromstring, tostring +from .helper import PillowTestCase, fromstring, tostring from PIL import Image @@ -69,6 +69,3 @@ class TestImageDraft(PillowTestCase): im = self.draft_roundtrip('L', (128, 128), None, (64, 64)) im.draft(None, (64, 64)) im.load() - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 3636a73f7..a91387f81 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image, ImageFilter @@ -94,10 +94,19 @@ class TestImageFilter(PillowTestCase): self.assertEqual(rankfilter.size, 1) self.assertEqual(rankfilter.rank, 2) + def test_builtinfilter_p(self): + builtinFilter = ImageFilter.BuiltinFilter() + + self.assertRaises(ValueError, builtinFilter.filter, hopper("P")) + + def test_kernel_not_enough_coefficients(self): + self.assertRaises(ValueError, + lambda: ImageFilter.Kernel((3, 3), (0, 0))) + def test_consistency_3x3(self): source = Image.open("Tests/images/hopper.bmp") reference = Image.open("Tests/images/hopper_emboss.bmp") - kernel = ImageFilter.Kernel((3, 3), + kernel = ImageFilter.Kernel((3, 3), # noqa: E127 (-1, -1, 0, -1, 0, 1, 0, 1, 1), .3) @@ -113,7 +122,7 @@ class TestImageFilter(PillowTestCase): 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), + kernel = ImageFilter.Kernel((5, 5), # noqa: E127 (-1, -1, -1, -1, 0, -1, -1, -1, 0, 1, -1, -1, 0, 1, 1, @@ -127,7 +136,3 @@ class TestImageFilter(PillowTestCase): Image.merge(mode, source[:len(mode)]).filter(kernel), Image.merge(mode, reference[:len(mode)]), ) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 2d48bb6b8..3b224e71a 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -13,7 +13,3 @@ class TestImageFromBytes(PillowTestCase): def test_not_implemented(self): self.assertRaises(NotImplementedError, Image.fromstring) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index 2e5d95aa7..8a29a2715 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -1,5 +1,5 @@ -from helper import unittest, PillowTestCase, hopper -from test_imageqt import PillowQtTestCase +from .helper import PillowTestCase, hopper +from .test_imageqt import PillowQtTestCase from PIL import ImageQt, Image @@ -42,7 +42,3 @@ class TestFromQImage(PillowQtTestCase, PillowTestCase): def test_sanity_p(self): for im in self.files_to_test: self.roundtrip(im.convert('P')) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index 5eecbf044..6d79bf280 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image @@ -18,7 +18,3 @@ class TestImageGetBands(PillowTestCase): Image.new("CMYK", (1, 1)).getbands(), ("C", "M", "Y", "K")) self.assertEqual( Image.new("YCbCr", (1, 1)).getbands(), ("Y", "Cb", "Cr")) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index f29032143..9bf39752b 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -37,7 +37,3 @@ class TestImageGetBbox(PillowTestCase): im.paste(255, (-10, -10, 110, 110)) self.assertEqual(im.getbbox(), (0, 0, 100, 100)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index ca7a9d93d..aa2a40976 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper class TestImageGetColors(PillowTestCase): @@ -65,7 +65,3 @@ class TestImageGetColors(PillowTestCase): A = im.getcolors(maxcolors=16) A.sort() self.assertEqual(A, expected) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index ef07844df..fe8f9adcd 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper class TestImageGetData(PillowTestCase): @@ -23,11 +23,7 @@ class TestImageGetData(PillowTestCase): self.assertEqual(getdata("L"), (16, 960, 960)) self.assertEqual(getdata("I"), (16, 960, 960)) self.assertEqual(getdata("F"), (16.0, 960, 960)) - self.assertEqual(getdata("RGB"), (((11, 13, 52), 960, 960))) + self.assertEqual(getdata("RGB"), ((11, 13, 52), 960, 960)) self.assertEqual(getdata("RGBA"), ((11, 13, 52, 255), 960, 960)) self.assertEqual(getdata("CMYK"), ((244, 242, 203, 0), 960, 960)) self.assertEqual(getdata("YCbCr"), ((16, 147, 123), 960, 960)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 0b0c31b86..1689744af 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -1,4 +1,5 @@ -from helper import unittest, PillowTestCase, hopper +from PIL import Image +from .helper import PillowTestCase, hopper class TestImageGetExtrema(PillowTestCase): @@ -18,8 +19,11 @@ class TestImageGetExtrema(PillowTestCase): self.assertEqual( extrema("RGBA"), ((0, 255), (0, 255), (0, 255), (255, 255))) self.assertEqual( - extrema("CMYK"), (((0, 255), (0, 255), (0, 255), (0, 0)))) + extrema("CMYK"), ((0, 255), (0, 255), (0, 255), (0, 0))) + self.assertEqual(extrema("I;16"), (0, 255)) - -if __name__ == '__main__': - unittest.main() + def test_true_16(self): + im = Image.open("Tests/images/16_bit_noise.tif") + 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 bc562de5a..6d3682caf 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,4 +1,5 @@ -from helper import unittest, PillowTestCase, hopper, py3 +from .helper import PillowTestCase, hopper +from PIL._util import py3 class TestImageGetIm(PillowTestCase): @@ -11,7 +12,3 @@ class TestImageGetIm(PillowTestCase): self.assertIn("PyCapsule", type_repr) self.assertIsInstance(im.im.id, int) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 01a6ac7ad..98f8142dc 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper class TestImageGetPalette(PillowTestCase): @@ -18,7 +18,3 @@ class TestImageGetPalette(PillowTestCase): self.assertIsNone(palette("RGBA")) self.assertIsNone(palette("CMYK")) self.assertIsNone(palette("YCbCr")) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index 9d3f2d9ed..85d40e859 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -30,7 +30,3 @@ class TestImageGetProjection(PillowTestCase): im.paste(255, (2, 4, 8, 6)) self.assertEqual(im.getprojection()[0], [0, 0, 1, 1, 1, 1, 1, 1, 0, 0]) self.assertEqual(im.getprojection()[1], [0, 0, 0, 0, 1, 1, 0, 0, 0, 0]) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 892e89328..0a023cd69 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper class TestImageHistogram(PillowTestCase): @@ -18,7 +18,3 @@ class TestImageHistogram(PillowTestCase): self.assertEqual(histogram("RGBA"), (1024, 0, 16384)) self.assertEqual(histogram("CMYK"), (1024, 0, 16384)) self.assertEqual(histogram("YCbCr"), (768, 0, 1908)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 15a92e339..d8f323eb8 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -28,6 +28,3 @@ class TestImageLoad(PillowTestCase): os.fstat(fn) self.assertRaises(OSError, os.fstat, fn) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 0596af397..26266611d 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -51,7 +51,3 @@ class TestImageMode(PillowTestCase): check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) check("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")) check("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index e782008a7..5586e8618 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, cached_property +from .helper import PillowTestCase, cached_property from PIL import Image @@ -250,7 +250,3 @@ class TestImagingPaste(PillowTestCase): im.copy().paste(im2) im.copy().paste(im2, (0, 0)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 977e98e83..90498fcff 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper class TestImagePoint(PillowTestCase): @@ -38,7 +38,3 @@ class TestImagePoint(PillowTestCase): def test_f_mode(self): im = hopper('F') self.assertRaises(ValueError, im.point, None) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index 823e0612f..7b66b8833 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image @@ -44,7 +44,3 @@ class TestImagePutAlpha(PillowTestCase): self.assertFalse(im.readonly) self.assertEqual(im.mode, 'RGBA') self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 1a7a6e7c7..1b57e38b7 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from array import array import sys @@ -83,6 +83,3 @@ class TestImagePutData(PillowTestCase): im.putdata(arr) self.assertEqual(len(im.getdata()), len(arr)) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index e173f0000..34b585f55 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import ImagePalette @@ -28,7 +28,3 @@ class TestImagePutPalette(PillowTestCase): im.putpalette(ImagePalette.random()) im.putpalette(ImagePalette.sepia()) im.putpalette(ImagePalette.wedge()) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index d9f52fb03..2f0b65758 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -27,26 +27,22 @@ class TestImageQuantize(PillowTestCase): raise self.assert_image(converted, 'P', converted.size) self.assert_image_similar(converted.convert('RGB'), image, 15) - assert len(converted.getcolors()) == 100 + 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) - assert len(converted.getcolors()) == 100 + self.assertEqual(len(converted.getcolors()), 100) def test_rgba_quantize(self): image = hopper('RGBA') image.quantize() - self.assertRaises(Exception, image.quantize, method=0) + self.assertRaises(ValueError, image.quantize, method=0) def test_quantize(self): image = Image.open('Tests/images/caption_6_33_22.png').convert('RGB') converted = image.quantize() self.assert_image(converted, 'P', converted.size) self.assert_image_similar(converted.convert('RGB'), image, 1) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index ee5a062c6..9ab8280ed 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -2,7 +2,7 @@ from __future__ import division, print_function from contextlib import contextmanager -from helper import unittest, PillowTestCase, hopper +from .helper import unittest, PillowTestCase, hopper from PIL import Image, ImageDraw @@ -196,7 +196,7 @@ class TestImagingCoreResampleAccuracy(PillowTestCase): class CoreResampleConsistencyTest(PillowTestCase): def make_case(self, mode, fill): im = Image.new(mode, (512, 9), fill) - return (im.resize((9, 512), Image.LANCZOS), im.load()[0, 0]) + return im.resize((9, 512), Image.LANCZOS), im.load()[0, 0] def run_case(self, case): channel, color = case @@ -245,8 +245,8 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): 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)) + '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): @@ -288,10 +288,14 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): def test_dirty_pixels_rgba(self): 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)) @@ -348,10 +352,8 @@ class CoreResamplePassesTest(PillowTestCase): class CoreResampleCoefficientsTest(PillowTestCase): def test_reduce(self): test_color = 254 - # print() for size in range(400000, 400010, 2): - # print(size) i = Image.new('L', (size, 1), 0) draw = ImageDraw.Draw(i) draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) @@ -359,7 +361,6 @@ class CoreResampleCoefficientsTest(PillowTestCase): px = i.resize((5, i.size[1]), Image.BICUBIC).load() if px[2, 0] != test_color // 2: self.assertEqual(test_color // 2, px[2, 0]) - # print('>', size, test_color // 2, px[2, 0]) def test_nonzero_coefficients(self): # regression test for the wrong coefficients calculation @@ -367,40 +368,45 @@ class CoreResampleCoefficientsTest(PillowTestCase): im = Image.new('RGBA', (1280, 1280), (0x20, 0x40, 0x60, 0xff)) histogram = im.resize((256, 256), Image.BICUBIC).histogram() - self.assertEqual(histogram[0x100 * 0 + 0x20], 0x10000) # first channel - self.assertEqual(histogram[0x100 * 1 + 0x40], 0x10000) # second channel - self.assertEqual(histogram[0x100 * 2 + 0x60], 0x10000) # third channel - self.assertEqual(histogram[0x100 * 3 + 0xff], 0x10000) # fourth channel + # first channel + self.assertEqual(histogram[0x100 * 0 + 0x20], 0x10000) + # second channel + self.assertEqual(histogram[0x100 * 1 + 0x40], 0x10000) + # third channel + self.assertEqual(histogram[0x100 * 2 + 0x60], 0x10000) + # fourth channel + 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.assertRaisesRegexp(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.assertRaisesRegexp(ValueError, "can't be negative"): + with self.assertRaisesRegex(ValueError, "can't be negative"): im.resize((32, 32), resample, (-20, 20, 100, 100)) - with self.assertRaisesRegexp(ValueError, "can't be negative"): + with self.assertRaisesRegex(ValueError, "can't be negative"): im.resize((32, 32), resample, (20, -20, 100, 100)) - with self.assertRaisesRegexp(ValueError, "can't be empty"): + with self.assertRaisesRegex(ValueError, "can't be empty"): im.resize((32, 32), resample, (20.1, 20, 20, 100)) - with self.assertRaisesRegexp(ValueError, "can't be empty"): + with self.assertRaisesRegex(ValueError, "can't be empty"): im.resize((32, 32), resample, (20, 20.1, 100, 20)) - with self.assertRaisesRegexp(ValueError, "can't be empty"): + with self.assertRaisesRegex(ValueError, "can't be empty"): im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) - with self.assertRaisesRegexp(ValueError, "can't exceed"): + with self.assertRaisesRegex(ValueError, "can't exceed"): im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) - with self.assertRaisesRegexp(ValueError, "can't exceed"): + with self.assertRaisesRegex(ValueError, "can't exceed"): im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) def resize_tiled(self, im, dst_size, xtiles, ytiles): @@ -422,7 +428,7 @@ class CoreResampleBoxTest(PillowTestCase): def test_tiles(self): im = Image.open("Tests/images/flower.jpg") - assert im.size == (480, 360) + self.assertEqual(im.size, (480, 360)) dst_size = (251, 188) reference = im.resize(dst_size, Image.BICUBIC) @@ -434,7 +440,7 @@ class CoreResampleBoxTest(PillowTestCase): # This test shows advantages of the subpixel resizing # after supersampling (e.g. during JPEG decoding). im = Image.open("Tests/images/flower.jpg") - assert im.size == (480, 360) + self.assertEqual(im.size, (480, 360)) dst_size = (48, 36) # Reference is cropped image resized to destination reference = im.crop((0, 0, 473, 353)).resize(dst_size, Image.BICUBIC) @@ -447,7 +453,7 @@ class CoreResampleBoxTest(PillowTestCase): # error with box should be much smaller than without self.assert_image_similar(reference, with_box, 6) - with self.assertRaisesRegexp(AssertionError, "difference 29\."): + with self.assertRaisesRegex(AssertionError, r"difference 29\."): self.assert_image_similar(reference, without_box, 5) def test_formats(self): @@ -490,7 +496,7 @@ class CoreResampleBoxTest(PillowTestCase): try: res = im.resize(size, Image.LANCZOS, box) self.assertEqual(res.size, size) - with self.assertRaisesRegexp(AssertionError, "difference \d"): + with self.assertRaisesRegex(AssertionError, r"difference \d"): # check that the difference at least that much self.assert_image_similar(res, im.crop(box), 20) except AssertionError: @@ -538,7 +544,3 @@ class CoreResampleBoxTest(PillowTestCase): except AssertionError: print('>>>', size, box, flt) raise - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 8d14b9008..c47c7317b 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -3,7 +3,7 @@ Tests for resize functionality. """ from itertools import permutations -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -39,15 +39,15 @@ 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)) @@ -66,8 +66,8 @@ class TestImagingCoreResize(PillowTestCase): } 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) @@ -90,8 +90,8 @@ 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]: + 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)) @@ -115,7 +115,3 @@ class TestImageResize(PillowTestCase): # Test unknown resampling filter im = hopper() self.assertRaises(ValueError, im.resize, (10, 10), "unknown") - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index fbcf9008d..05043cd06 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -95,8 +95,31 @@ class TestImageRotate(PillowTestCase): 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, center=(0, 0), + translate=(im.size[0]/2, 0)) + def test_rotate_no_fill(self): + im = Image.new('RGB', (100, 100), 'green') + target = Image.open('Tests/images/rotate_45_no_fill.png') + im = im.rotate(45) + self.assert_image_equal(im, target) -if __name__ == '__main__': - unittest.main() + def test_rotate_with_fill(self): + im = Image.new('RGB', (100, 100), 'green') + target = Image.open('Tests/images/rotate_45_with_fill.png') + im = im.rotate(45, fillcolor='white') + self.assert_image_equal(im, target) + + def test_alpha_rotate_no_fill(self): + # Alpha images are handled differently internally + im = Image.new('RGBA', (10, 10), 'green') + im = im.rotate(45, expand=1) + corner = im.getpixel((0, 0)) + self.assertEqual(corner, (0, 0, 0, 0)) + + def test_alpha_rotate_with_fill(self): + # Alpha images are handled differently internally + im = Image.new('RGBA', (10, 10), 'green') + im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) + corner = im.getpixel((0, 0)) + self.assertEqual(corner, (255, 0, 0, 255)) diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 6f312ff80..2d97dabc2 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -59,7 +59,3 @@ class TestImageSplit(PillowTestCase): self.assertEqual(split_open("RGB"), 3) if 'zip_encoder' in codecs: self.assertEqual(split_open("RGBA"), 4) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 6b92dbb24..d03ea4141 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper class TestImageThumbnail(PillowTestCase): @@ -35,7 +35,3 @@ class TestImageThumbnail(PillowTestCase): im = hopper().resize((128, 128)) im.thumbnail((100, 100)) self.assert_image(im, im.mode, (100, 100)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 5c47eade7..c5c06ba01 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper, fromstring +from .helper import PillowTestCase, hopper, fromstring class TestImageToBitmap(PillowTestCase): @@ -6,7 +6,6 @@ class TestImageToBitmap(PillowTestCase): def test_sanity(self): self.assertRaises(ValueError, lambda: hopper().tobitmap()) - hopper().convert("1").tobitmap() im1 = hopper().convert("1") @@ -14,7 +13,3 @@ class TestImageToBitmap(PillowTestCase): self.assertIsInstance(bitmap, bytes) self.assert_image_equal(im1, fromstring(bitmap)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index 2cae05e66..2bfee2da3 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper class TestImageToBytes(PillowTestCase): @@ -6,6 +6,3 @@ class TestImageToBytes(PillowTestCase): def test_sanity(self): data = hopper().tobytes() self.assertIsInstance(data, bytes) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index df8fc83e8..ae1cf6e3d 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,6 +1,6 @@ import math -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -53,15 +53,20 @@ class TestImageTransform(PillowTestCase): self.assert_image_equal(transformed, scaled) def test_fill(self): - im = hopper('RGB') - (w, h) = im.size - transformed = im.transform(im.size, Image.EXTENT, - (0, 0, - w*2, h*2), - Image.BILINEAR, - fillcolor = 'red') + for mode, pixel in [ + ['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') - self.assertEqual(transformed.getpixel((w-1,h-1)), (255,0,0)) + self.assertEqual(transformed.getpixel((w-1, h-1)), pixel) def test_mesh(self): # this should be a checkerboard of halfsized hoppers in ul, lr @@ -147,7 +152,7 @@ class TestImageTransform(PillowTestCase): ] # Yeah. Watch some JIT optimize this out. - pattern = None + pattern = None # noqa: F841 self.test_mesh() @@ -266,6 +271,3 @@ class TestImageTransformAffine(PillowTestCase): class TestImageTransformPerspective(TestImageTransformAffine): # Repeat all tests for AFFINE transformations with PERSPECTIVE transform = Image.PERSPECTIVE - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index a6b1191db..8ffb9e9bf 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,5 +1,5 @@ -import helper -from helper import unittest, PillowTestCase +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) @@ -146,7 +146,3 @@ class TestImageTranspose(PillowTestCase): im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM)) self.assert_image_equal( im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 4e30dc175..8aa784cdd 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,8 +1,18 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image from PIL import ImageChops +BLACK = (0, 0, 0) +BROWN = (127, 64, 0) +CYAN = (0, 255, 255) +DARK_GREEN = (0, 128, 0) +GREEN = (0, 255, 0) +ORANGE = (255, 128, 0) +WHITE = (255, 255, 255) + +GREY = 128 + class TestImageChops(PillowTestCase): @@ -35,6 +45,303 @@ class TestImageChops(PillowTestCase): ImageChops.offset(im, 10) ImageChops.offset(im, 10, 20) + def test_add(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") + im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + + # Act + new = ImageChops.add(im1, im2) + + # Assert + self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + self.assertEqual(new.getpixel((50, 50)), ORANGE) + + def test_add_scale_offset(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") + im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + + # Act + new = ImageChops.add(im1, im2, scale=2.5, offset=100) + + # Assert + self.assertEqual(new.getbbox(), (0, 0, 100, 100)) + self.assertEqual(new.getpixel((50, 50)), (202, 151, 100)) + + def test_add_clip(self): + # Arrange + im = hopper() + + # Act + new = ImageChops.add(im, im) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (255, 255, 254)) + + def test_add_modulo(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") + im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + + # Act + new = ImageChops.add_modulo(im1, im2) + + # Assert + self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + self.assertEqual(new.getpixel((50, 50)), ORANGE) + + def test_add_modulo_no_clip(self): + # Arrange + im = hopper() + + # Act + new = ImageChops.add_modulo(im, im) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (224, 76, 254)) + + def test_blend(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") + im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + + # Act + new = ImageChops.blend(im1, im2, 0.5) + + # Assert + self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + self.assertEqual(new.getpixel((50, 50)), BROWN) + + def test_constant(self): + # Arrange + im = Image.new("RGB", (20, 10)) + + # Act + new = ImageChops.constant(im, GREY) + + # Assert + self.assertEqual(new.size, im.size) + self.assertEqual(new.getpixel((0, 0)), GREY) + self.assertEqual(new.getpixel((19, 9)), GREY) + + def test_darker_image(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") + im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + + # Act + new = ImageChops.darker(im1, im2) + + # Assert + self.assert_image_equal(new, im2) + + def test_darker_pixel(self): + # Arrange + im1 = hopper() + im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + + # Act + new = ImageChops.darker(im1, im2) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (240, 166, 0)) + + def test_difference(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_arc_end_le_start.png") + im2 = Image.open("Tests/images/imagedraw_arc_no_loops.png") + + # Act + new = ImageChops.difference(im1, im2) + + # Assert + self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + + def test_difference_pixel(self): + # Arrange + im1 = hopper() + im2 = Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") + + # Act + new = ImageChops.difference(im1, im2) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (240, 166, 128)) + + def test_duplicate(self): + # Arrange + im = hopper() + + # Act + new = ImageChops.duplicate(im) + + # Assert + self.assert_image_equal(new, im) + + def test_invert(self): + # Arrange + im = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + + # Act + new = ImageChops.invert(im) + + # Assert + self.assertEqual(new.getbbox(), (0, 0, 100, 100)) + self.assertEqual(new.getpixel((0, 0)), WHITE) + self.assertEqual(new.getpixel((50, 50)), CYAN) + + def test_lighter_image(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") + im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + + # Act + new = ImageChops.lighter(im1, im2) + + # Assert + self.assert_image_equal(new, im1) + + def test_lighter_pixel(self): + # Arrange + im1 = hopper() + im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + + # Act + new = ImageChops.lighter(im1, im2) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (255, 255, 127)) + + def test_multiply_black(self): + """If you multiply an image with a solid black image, + the result is black.""" + # Arrange + im1 = hopper() + black = Image.new("RGB", im1.size, "black") + + # Act + new = ImageChops.multiply(im1, black) + + # Assert + self.assert_image_equal(new, black) + + def test_multiply_green(self): + # Arrange + im = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + green = Image.new("RGB", im.size, "green") + + # Act + new = ImageChops.multiply(im, green) + + # Assert + self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + self.assertEqual(new.getpixel((25, 25)), DARK_GREEN) + self.assertEqual(new.getpixel((50, 50)), BLACK) + + def test_multiply_white(self): + """If you multiply with a solid white image, + the image is unaffected.""" + # Arrange + im1 = hopper() + white = Image.new("RGB", im1.size, "white") + + # Act + new = ImageChops.multiply(im1, white) + + # Assert + self.assert_image_equal(new, im1) + + def test_offset(self): + # Arrange + im = Image.open("Tests/images/imagedraw_ellipse_RGB.png") + xoffset = 45 + yoffset = 20 + + # Act + new = ImageChops.offset(im, xoffset, yoffset) + + # Assert + self.assertEqual(new.getbbox(), (0, 45, 100, 96)) + self.assertEqual(new.getpixel((50, 50)), BLACK) + self.assertEqual(new.getpixel((50+xoffset, 50+yoffset)), DARK_GREEN) + + # Test no yoffset + self.assertEqual(ImageChops.offset(im, xoffset), + ImageChops.offset(im, xoffset, xoffset)) + + def test_screen(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") + im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + + # Act + new = ImageChops.screen(im1, im2) + + # Assert + self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + self.assertEqual(new.getpixel((50, 50)), ORANGE) + + def test_subtract(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") + im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + + # Act + new = ImageChops.subtract(im1, im2) + + # Assert + self.assertEqual(new.getbbox(), (25, 50, 76, 76)) + self.assertEqual(new.getpixel((50, 50)), GREEN) + self.assertEqual(new.getpixel((50, 51)), BLACK) + + def test_subtract_scale_offset(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") + im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + + # Act + new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) + + # Assert + self.assertEqual(new.getbbox(), (0, 0, 100, 100)) + self.assertEqual(new.getpixel((50, 50)), (100, 202, 100)) + + def test_subtract_clip(self): + # Arrange + im1 = hopper() + im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + + # Act + new = ImageChops.subtract(im1, im2) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (0, 0, 127)) + + def test_subtract_modulo(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") + im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + + # Act + new = ImageChops.subtract_modulo(im1, im2) + + # Assert + self.assertEqual(new.getbbox(), (25, 50, 76, 76)) + self.assertEqual(new.getpixel((50, 50)), GREEN) + self.assertEqual(new.getpixel((50, 51)), BLACK) + + def test_subtract_modulo_no_clip(self): + # Arrange + im1 = hopper() + im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + + # Act + new = ImageChops.subtract_modulo(im1, im2) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (241, 167, 127)) + def test_logical(self): def table(op, a, b): @@ -66,7 +373,3 @@ class TestImageChops(PillowTestCase): table(ImageChops.logical_or, 0, 255), (0, 255, 255, 255)) self.assertEqual( table(ImageChops.logical_xor, 0, 255), (0, 255, 255, 0)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 9e304ae01..cdd6f00c3 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper import datetime from PIL import Image, ImageMode @@ -10,7 +10,7 @@ try: from PIL import ImageCms from PIL.ImageCms import ImageCmsProfile ImageCms.core.profile_open -except ImportError as v: +except ImportError: # Skipped via setUp() pass @@ -43,7 +43,7 @@ class TestImageCms(PillowTestCase): self.assertEqual(list(map(type, v)), [str, str, str, str]) # internal version number - self.assertRegexpMatches(ImageCms.core.littlecms_version, r"\d+\.\d+$") + self.assertRegex(ImageCms.core.littlecms_version, r"\d+\.\d+$") self.skip_missing() i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) @@ -175,11 +175,11 @@ class TestImageCms(PillowTestCase): def test_unsupported_color_space(self): self.assertRaises(ImageCms.PyCMSError, - ImageCms.createProfile, "unsupported") + ImageCms.createProfile, "unsupported") def test_invalid_color_temperature(self): self.assertRaises(ImageCms.PyCMSError, - ImageCms.createProfile, "LAB", "invalid") + ImageCms.createProfile, "LAB", "invalid") def test_simple_lab(self): i = Image.new('RGB', (10, 10), (128, 128, 128)) @@ -196,13 +196,13 @@ class TestImageCms(PillowTestCase): # not a linear luminance map. so L != 128: self.assertEqual(k, (137, 128, 128)) - l = i_lab.getdata(0) - a = i_lab.getdata(1) - b = i_lab.getdata(2) + l_data = i_lab.getdata(0) + a_data = i_lab.getdata(1) + b_data = i_lab.getdata(2) - self.assertEqual(list(l), [137] * 100) - self.assertEqual(list(a), [128] * 100) - self.assertEqual(list(b), [128] * 100) + self.assertEqual(list(l_data), [137] * 100) + self.assertEqual(list(a_data), [128] * 100) + self.assertEqual(list(b_data), [128] * 100) def test_lab_color(self): psRGB = ImageCms.createProfile("sRGB") @@ -286,53 +286,111 @@ class TestImageCms(PillowTestCase): 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))) - assert_truncated_tuple_equal(p.blue_primary, ((0.14306641366715667, 0.06060790921083026, 0.7140960805782015), (0.15588475410450106, 0.06603820408959558, 0.06060790921083026))) - assert_truncated_tuple_equal(p.chromatic_adaptation, (((1.04791259765625, 0.0229339599609375, -0.050201416015625), (0.02960205078125, 0.9904632568359375, -0.0170745849609375), (-0.009246826171875, 0.0150604248046875, 0.7517852783203125)), ((1.0267159024652783, 0.022470062342089134, 0.0229339599609375), (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875)))) + assert_truncated_tuple_equal( + p.blue_colorant, + ((0.14306640625, 0.06060791015625, 0.7140960693359375), + (0.1558847490315394, 0.06603820639433387, 0.06060791015625))) + assert_truncated_tuple_equal( + p.blue_primary, + ((0.14306641366715667, 0.06060790921083026, 0.7140960805782015), + (0.15588475410450106, 0.06603820408959558, 0.06060790921083026))) + assert_truncated_tuple_equal( + p.chromatic_adaptation, + (((1.04791259765625, 0.0229339599609375, -0.050201416015625), + (0.02960205078125, 0.9904632568359375, -0.0170745849609375), + (-0.009246826171875, 0.0150604248046875, 0.7517852783203125)), + ((1.0267159024652783, 0.022470062342089134, 0.0229339599609375), + (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), + (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875)))) self.assertIsNone(p.chromaticity) - self.assertEqual(p.clut, {0: (False, False, True), 1: (False, False, True), 2: (False, False, True), 3: (False, False, True)}) + self.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.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.copyright, + 'Copyright International Color Consortium, 2009') + self.assertEqual(p.creation_date, + datetime.datetime(2009, 2, 27, 21, 36, 31)) self.assertEqual(p.device_class, 'mntr') - assert_truncated_tuple_equal(p.green_colorant, ((0.3851470947265625, 0.7168731689453125, 0.097076416015625), (0.32119769927720654, 0.5978443449048152, 0.7168731689453125))) - assert_truncated_tuple_equal(p.green_primary, ((0.3851470888162112, 0.7168731974161346, 0.09707641738998518), (0.32119768793686687, 0.5978443567149709, 0.7168731974161346))) + assert_truncated_tuple_equal( + p.green_colorant, + ((0.3851470947265625, 0.7168731689453125, 0.097076416015625), + (0.32119769927720654, 0.5978443449048152, 0.7168731689453125))) + assert_truncated_tuple_equal( + p.green_primary, + ((0.3851470888162112, 0.7168731974161346, 0.09707641738998518), + (0.32119768793686687, 0.5978443567149709, 0.7168731974161346))) self.assertEqual(p.header_flags, 0) self.assertEqual(p.header_manufacturer, '\x00\x00\x00\x00') self.assertEqual(p.header_model, '\x00\x00\x00\x00') - self.assertEqual(p.icc_measurement_condition, {'backing': (0.0, 0.0, 0.0), 'flare': 0.0, 'geo': 'unknown', 'observer': 1, 'illuminant_type': 'D65'}) + self.assertEqual(p.icc_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))) - assert_truncated_tuple_equal(p.media_white_point, ((0.964202880859375, 1.0, 0.8249053955078125), (0.3457029219802284, 0.3585375327567059, 1.0))) - assert_truncated_tuple_equal((p.media_white_point_temperature,), (5000.722328847392,)) - self.assertEqual(p.model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') + assert_truncated_tuple_equal( + p.media_black_point, + ((0.012054443359375, 0.0124969482421875, 0.01031494140625), + (0.34573304157549234, 0.35842450765864337, 0.0124969482421875))) + assert_truncated_tuple_equal( + p.media_white_point, + ((0.964202880859375, 1.0, 0.8249053955078125), + (0.3457029219802284, 0.3585375327567059, 1.0))) + assert_truncated_tuple_equal( + (p.media_white_point_temperature,), + (5000.722328847392,)) + self.assertEqual(p.model, + 'IEC 61966-2-1 Default RGB Colour Space - sRGB') self.assertEqual(p.pcs, 'XYZ') self.assertIsNone(p.perceptual_rendering_intent_gamut) - self.assertEqual(p.product_copyright, 'Copyright International Color Consortium, 2009') + 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_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') - assert_truncated_tuple_equal(p.red_colorant, ((0.436065673828125, 0.2224884033203125, 0.013916015625), (0.6484536316398539, 0.3308524880306778, 0.2224884033203125))) - assert_truncated_tuple_equal(p.red_primary, ((0.43606566581047446, 0.22248840582960838, 0.013916015621759925), (0.6484536250319214, 0.3308524944738204, 0.22248840582960838))) + self.assertEqual( + p.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') + assert_truncated_tuple_equal( + p.red_colorant, + ((0.436065673828125, 0.2224884033203125, 0.013916015625), + (0.6484536316398539, 0.3308524880306778, 0.2224884033203125))) + assert_truncated_tuple_equal( + p.red_primary, + ((0.43606566581047446, 0.22248840582960838, 0.013916015621759925), + (0.6484536250319214, 0.3308524944738204, 0.22248840582960838))) self.assertEqual(p.rendering_intent, 0) self.assertIsNone(p.saturation_rendering_intent_gamut) self.assertIsNone(p.screening_description) self.assertIsNone(p.target) self.assertEqual(p.technology, 'CRT ') self.assertEqual(p.version, 2.0) - self.assertEqual(p.viewing_condition, 'Reference Viewing Condition in IEC 61966-2-1') + self.assertEqual(p.viewing_condition, + 'Reference Viewing Condition in IEC 61966-2-1') self.assertEqual(p.xcolor_space, 'RGB ') def test_profile_typesafety(self): @@ -346,14 +404,15 @@ 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 = [ + nine_grid_deltas = [ # noqa: E128 (-1, -1), (-1, 0), (-1, 1), - ( 0, -1), ( 0, 0), ( 0, 1), - ( 1, -1), ( 1, 0), ( 1, 1), + (0, -1), (0, 0), (0, 1), + (1, -1), (1, 0), (1, 1), ] chans = [] bands = ImageMode.getmode(mode).bands @@ -369,7 +428,11 @@ class TestImageCms(PillowTestCase): ) 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))) + channel_data.paste( + channel_pattern, + tuple(paste_offset[c] + delta[c] * channel_pattern.size[c] + for c in range(2)), + ) chans.append(channel_data) return Image.merge(mode, chans) @@ -379,29 +442,35 @@ class TestImageCms(PillowTestCase): # create some transform, it doesn't matter which one source_profile = ImageCms.createProfile("sRGB") destination_profile = ImageCms.createProfile("sRGB") - t = ImageCms.buildTransform(source_profile, destination_profile, inMode=mode, outMode=mode) + t = ImageCms.buildTransform( + source_profile, destination_profile, inMode=mode, outMode=mode) # apply transform if transform_in_place: ImageCms.applyTransform(source_image, t, inPlace=True) result_image = source_image else: - result_image = ImageCms.applyTransform(source_image, t, inPlace=False) + result_image = 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') + self.assert_aux_channel_preserved( + mode='RGBA', transform_in_place=False, preserved_channel='A') def test_preserve_auxiliary_channels_rgba_in_place(self): - self.assert_aux_channel_preserved(mode='RGBA', transform_in_place=True, preserved_channel='A') + self.assert_aux_channel_preserved( + mode='RGBA', transform_in_place=True, preserved_channel='A') def test_preserve_auxiliary_channels_rgbx(self): - self.assert_aux_channel_preserved(mode='RGBX', transform_in_place=False, preserved_channel='X') + self.assert_aux_channel_preserved( + mode='RGBX', transform_in_place=False, preserved_channel='X') def test_preserve_auxiliary_channels_rgbx_in_place(self): - self.assert_aux_channel_preserved(mode='RGBX', transform_in_place=True, preserved_channel='X') + self.assert_aux_channel_preserved( + mode='RGBX', transform_in_place=True, preserved_channel='X') def test_auxiliary_channels_isolated(self): # test data in aux channels does not affect non-aux channels @@ -422,20 +491,26 @@ class TestImageCms(PillowTestCase): source_profile = ImageCms.createProfile(src_format[1]) destination_profile = ImageCms.createProfile(dst_format[1]) source_image = src_format[3] - test_transform = ImageCms.buildTransform(source_profile, destination_profile, inMode=src_format[0], outMode=dst_format[0]) + test_transform = ImageCms.buildTransform( + source_profile, destination_profile, + inMode=src_format[0], outMode=dst_format[0]) # test conversion from aux-ful source if transform_in_place: test_image = source_image.copy() - ImageCms.applyTransform(test_image, test_transform, inPlace=True) + ImageCms.applyTransform( + test_image, test_transform, inPlace=True) else: - test_image = ImageCms.applyTransform(source_image, test_transform, inPlace=False) + test_image = ImageCms.applyTransform( + source_image, test_transform, inPlace=False) # reference conversion from aux-less source - reference_transform = ImageCms.buildTransform(source_profile, destination_profile, inMode=src_format[2], outMode=dst_format[2]) - reference_image = ImageCms.applyTransform(source_image.convert(src_format[2]), reference_transform) + reference_transform = ImageCms.buildTransform( + source_profile, destination_profile, + inMode=src_format[2], outMode=dst_format[2]) + reference_image = ImageCms.applyTransform( + source_image.convert(src_format[2]), + reference_transform) - self.assert_image_equal(test_image.convert(dst_format[2]), reference_image) - -if __name__ == '__main__': - unittest.main() + 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 64e88cf9c..cb9c9843c 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image from PIL import ImageColor @@ -31,7 +31,8 @@ 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("#CDEF"), + ImageColor.getrgb("#cdef")) self.assertEqual(ImageColor.getrgb("#DEFDEF"), ImageColor.getrgb("#defdef")) self.assertEqual(ImageColor.getrgb("#CDEFCDEF"), @@ -78,6 +79,26 @@ class TestImageColor(PillowTestCase): self.assertEqual((255, 0, 0), ImageColor.getrgb("hsl(360,100%,50%)")) self.assertEqual((0, 255, 255), ImageColor.getrgb("hsl(180,100%,50%)")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("hsv(0,100%,100%)")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("hsv(360,100%,100%)")) + self.assertEqual((0, 255, 255), + ImageColor.getrgb("hsv(180,100%,100%)")) + + # alternate format + self.assertEqual(ImageColor.getrgb("hsb(0,100%,50%)"), + ImageColor.getrgb("hsv(0,100%,50%)")) + + # floats + self.assertEqual((254, 3, 3), + ImageColor.getrgb("hsl(0.1,99.2%,50.3%)")) + self.assertEqual((255, 0, 0), + ImageColor.getrgb("hsl(360.,100.0%,50%)")) + + self.assertEqual((253, 2, 2), + ImageColor.getrgb("hsv(0.1,99.2%,99.3%)")) + self.assertEqual((255, 0, 0), + ImageColor.getrgb("hsv(360.,100.0%,100%)")) + # case insensitivity self.assertEqual(ImageColor.getrgb("RGB(255,0,0)"), ImageColor.getrgb("rgb(255,0,0)")) @@ -87,6 +108,10 @@ class TestImageColor(PillowTestCase): 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), @@ -97,6 +122,8 @@ class TestImageColor(PillowTestCase): 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)") @@ -116,6 +143,12 @@ class TestImageColor(PillowTestCase): self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0,100,50%)") self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0,100%,50)") + self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0,100%)") + self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0,100%,0%,0%)") + self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0%,100%,50%)") + self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0,100,50%)") + self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0,100%,50)") + # look for rounding errors (based on code by Tim Hatch) def test_rounding_errors(self): @@ -159,7 +192,3 @@ class TestImageColor(PillowTestCase): self.assertEqual( (162, 33), ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA")) Image.new("LA", (1, 1), "white") - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index a79a75ca0..bceb0e3d4 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,11 +1,7 @@ -from helper import unittest, PillowTestCase, hopper - -from PIL import Image -from PIL import ImageColor -from PIL import ImageDraw import os.path -import sys +from .helper import PillowTestCase, hopper +from PIL import Image, ImageColor, ImageDraw BLACK = (0, 0, 0) WHITE = (255, 255, 255) @@ -50,7 +46,7 @@ class TestImageDraw(PillowTestCase): im = Image.open("Tests/images/chi.gif") draw = ImageDraw.Draw(im) - draw.line(((0, 0)), fill=(0, 0, 0)) + draw.line((0, 0), fill=(0, 0, 0)) def test_mode_mismatch(self): im = hopper("RGB").copy() @@ -106,6 +102,30 @@ class TestImageDraw(PillowTestCase): self.assert_image_similar( im, Image.open("Tests/images/imagedraw_arc_no_loops.png"), 1) + def test_arc_width(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_arc_width.png" + + # Act + draw.arc(BBOX1, 10, 260, width=5) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + + def test_arc_width_fill(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_arc_width_fill.png" + + # Act + draw.arc(BBOX1, 10, 260, fill="yellow", width=5) + + # 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)) @@ -141,6 +161,30 @@ class TestImageDraw(PillowTestCase): self.helper_chord(mode, BBOX2, 0, 180) self.helper_chord(mode, BBOX2, 0.5, 180.4) + def test_chord_width(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_chord_width.png" + + # Act + draw.chord(BBOX1, 10, 260, outline="yellow", width=5) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + + def test_chord_width_fill(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_chord_width_fill.png" + + # Act + draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + def helper_ellipse(self, mode, bbox): # Arrange im = Image.new(mode, (W, H)) @@ -173,6 +217,40 @@ class TestImageDraw(PillowTestCase): self.assert_image_similar( 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) + ]: + im = Image.new("RGB", (101, 101)) + draw = ImageDraw.Draw(im) + draw.ellipse(bbox, fill="green", outline="blue") + self.assert_image_equal(im, im.transpose(Image.FLIP_LEFT_RIGHT)) + + def test_ellipse_width(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_ellipse_width.png" + + # Act + draw.ellipse(BBOX1, outline="blue", width=5) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + + def test_ellipse_width_fill(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_ellipse_width_fill.png" + + # Act + draw.ellipse(BBOX1, fill="green", outline="blue", width=5) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + def helper_line(self, points): # Arrange im = Image.new("RGB", (W, H)) @@ -253,6 +331,30 @@ class TestImageDraw(PillowTestCase): self.helper_pieslice(BBOX2, -90, 45) self.helper_pieslice(BBOX2, -90.5, 45.4) + def test_pieslice_width(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_pieslice_width.png" + + # Act + draw.pieslice(BBOX1, 10, 260, outline="blue", width=5) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + + def test_pieslice_width_fill(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_pieslice_width_fill.png" + + # Act + draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + def helper_point(self, points): # Arrange im = Image.new("RGB", (W, H)) @@ -337,20 +439,51 @@ class TestImageDraw(PillowTestCase): # Assert self.assert_image_similar(im, Image.open(expected), 1) - def test_floodfill(self): + def test_rectangle_width(self): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") - centre_point = (int(W/2), int(H/2)) - red = ImageColor.getrgb("red") - im_floodfill = Image.open("Tests/images/imagedraw_floodfill.png") + expected = "Tests/images/imagedraw_rectangle_width.png" # Act - ImageDraw.floodfill(im, centre_point, red) + draw.rectangle(BBOX1, outline="green", width=5) # Assert - self.assert_image_equal(im, im_floodfill) + self.assert_image_equal(im, Image.open(expected)) + + def test_rectangle_width_fill(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_rectangle_width_fill.png" + + # Act + draw.rectangle(BBOX1, fill="blue", outline="green", width=5) + + # Assert + self.assert_image_equal(im, Image.open(expected)) + + def test_floodfill(self): + red = ImageColor.getrgb("red") + + for mode, value in [ + ("L", 1), + ("RGBA", (255, 0, 0, 0)), + ("RGB", red) + ]: + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + draw.rectangle(BBOX2, outline="yellow", fill="green") + centre_point = (int(W/2), int(H/2)) + + # Act + ImageDraw.floodfill(im, centre_point, value) + + # Assert + expected = "Tests/images/imagedraw_floodfill_"+mode+".png" + im_floodfill = Image.open(expected) + self.assert_image_equal(im, im_floodfill) # Test that using the same colour does not change the image ImageDraw.floodfill(im, centre_point, red) @@ -360,8 +493,11 @@ class TestImageDraw(PillowTestCase): ImageDraw.floodfill(im, (W, H), red) self.assert_image_equal(im, im_floodfill) - @unittest.skipIf(hasattr(sys, 'pypy_version_info'), - "Causes fatal RPython error on PyPy") + # Test filling at the edge of an image + im = Image.new("RGB", (1, 1)) + ImageDraw.floodfill(im, (0, 0), red) + self.assert_image_equal(im, Image.new("RGB", (1, 1), red)) + def test_floodfill_border(self): # floodfill() is experimental @@ -407,7 +543,7 @@ class TestImageDraw(PillowTestCase): for y in range(0, size[1]): if (x + y) % 2 == 0: img.putpixel((x, y), background2) - return (img, ImageDraw.Draw(img)) + return img, ImageDraw.Draw(img) def test_square(self): expected = Image.open(os.path.join(IMAGES_PATH, 'square.png')) @@ -559,6 +695,20 @@ class TestImageDraw(PillowTestCase): # Assert self.assert_image_similar(im, Image.open(expected), 1) + def test_line_joint(self): + im = Image.new("RGB", (500, 325)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_line_joint_curve.png" + + # Act + xy = [(400, 280), (380, 280), (450, 280), (440, 120), (350, 200), + (310, 280), (300, 280), (250, 280), (250, 200), (150, 200), + (150, 260), (50, 200), (150, 50), (250, 100)] + draw.line(xy, GRAY, 50, "curve") + + # Assert + self.assert_image_similar(im, Image.open(expected), 3) + def test_textsize_empty_string(self): # https://github.com/python-pillow/Pillow/issues/2783 # Arrange @@ -572,6 +722,43 @@ class TestImageDraw(PillowTestCase): draw.textsize("\n") draw.textsize("test\n") + def test_same_color_outline(self): + # Prepare shape + x0, y0 = 5, 5 + x1, y1 = 5, 50 + x2, y2 = 95, 50 + x3, y3 = 95, 5 -if __name__ == '__main__': - unittest.main() + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + # Begin + for mode in ["RGB", "L"]: + for fill, outline in [ + ["red", None], + ["red", "red"], + ["red", "#f00"] + ]: + for operation, args in { + 'chord': [BBOX1, 0, 180], + 'ellipse': [BBOX1], + 'shape': [s], + 'pieslice': [BBOX1, -90, 45], + 'polygon': [[(18, 30), (85, 30), (60, 72)]], + 'rectangle': [BBOX1] + }.items(): + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw_method = getattr(draw, operation) + args += [fill, outline] + draw_method(*args) + + # Assert + expected = ("Tests/images/imagedraw_outline" + "_{}_{}.png".format(operation, mode)) + self.assert_image_similar(im, Image.open(expected), 1) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py new file mode 100644 index 000000000..97033c8a7 --- /dev/null +++ b/Tests/test_imagedraw2.py @@ -0,0 +1,225 @@ +import os.path + +from .helper import PillowTestCase, hopper, unittest +from PIL import Image, ImageDraw2, features + +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +GRAY = (190, 190, 190) +DEFAULT_MODE = "RGB" +IMAGES_PATH = os.path.join("Tests", "images", "imagedraw") + +# Image size +W, H = 100, 100 + +# Bounding box points +X0 = int(W / 4) +X1 = int(X0 * 3) +Y0 = int(H / 4) +Y1 = int(X0 * 3) + +# Two kinds of bounding box +BBOX1 = [(X0, Y0), (X1, Y1)] +BBOX2 = [X0, Y0, X1, Y1] + +# Two kinds of coordinate sequences +POINTS1 = [(10, 10), (20, 40), (30, 30)] +POINTS2 = [10, 10, 20, 40, 30, 30] + +KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] + +HAS_FREETYPE = features.check("freetype2") +FONT_PATH = "Tests/fonts/FreeMono.ttf" + + +class TestImageDraw(PillowTestCase): + + def test_sanity(self): + im = hopper("RGB").copy() + + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=7) + draw.line(list(range(10)), pen) + + from PIL import ImageDraw + + draw, handler = ImageDraw.getdraw(im) + pen = ImageDraw2.Pen("blue", width=7) + draw.line(list(range(10)), pen) + + def helper_ellipse(self, mode, bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=2) + brush = ImageDraw2.Brush("green") + expected = "Tests/images/imagedraw_ellipse_{}.png".format(mode) + + # Act + draw.ellipse(bbox, pen, brush) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + + def test_ellipse1(self): + self.helper_ellipse("RGB", BBOX1) + + def test_ellipse2(self): + self.helper_ellipse("RGB", BBOX2) + + def test_ellipse_edge(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + brush = ImageDraw2.Brush("white") + + # Act + draw.ellipse(((0, 0), (W-1, H)), brush) + + # Assert + self.assert_image_similar( + im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) + + def helper_line(self, points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("yellow", width=2) + + # Act + draw.line(points, pen) + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_line.png")) + + def test_line1_pen(self): + self.helper_line(POINTS1) + + def test_line2_pen(self): + self.helper_line(POINTS2) + + def test_line_pen_as_brush(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = None + brush = ImageDraw2.Pen("yellow", width=2) + + # Act + # Pass in the pen as the brush parameter + draw.line(POINTS1, pen, brush) + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_line.png")) + + def helper_polygon(self, points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=2) + brush = ImageDraw2.Brush("red") + + # Act + draw.polygon(points, pen, brush) + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_polygon.png")) + + def test_polygon1(self): + self.helper_polygon(POINTS1) + + def test_polygon2(self): + self.helper_polygon(POINTS2) + + def helper_rectangle(self, bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("green", width=2) + brush = ImageDraw2.Brush("black") + + # Act + draw.rectangle(bbox, pen, brush) + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_rectangle.png")) + + def test_rectangle1(self): + self.helper_rectangle(BBOX1) + + def test_rectangle2(self): + self.helper_rectangle(BBOX2) + + def test_big_rectangle(self): + # Test drawing a rectangle bigger than the image + # Arrange + im = Image.new("RGB", (W, H)) + bbox = [(-1, -1), (W + 1, H + 1)] + brush = ImageDraw2.Brush("orange") + draw = ImageDraw2.Draw(im) + expected = "Tests/images/imagedraw_big_rectangle.png" + + # Act + draw.rectangle(bbox, brush) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_text(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + expected = "Tests/images/imagedraw2_text.png" + + # Act + draw.text((5, 5), "ImageDraw2", font) + + # Assert + self.assert_image_similar(im, Image.open(expected), 13) + + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_textsize(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + size = draw.textsize("ImageDraw2", font) + + # Assert + self.assertEqual(size[1], 12) + + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_textsize_empty_string(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + # Should not cause 'SystemError: returned NULL without setting an error' + draw.textsize("", font) + draw.textsize("\n", font) + draw.textsize("test\n", font) + + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_flush(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + draw.text((5, 5), "ImageDraw2", font) + im2 = draw.flush() + + # Assert + self.assert_image_equal(im, im2) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index e9727613b..0e4e8c4f3 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image from PIL import ImageEnhance @@ -45,9 +45,6 @@ class TestImageEnhance(PillowTestCase): 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) - - -if __name__ == '__main__': - unittest.main() + self._check_alpha( + getattr(ImageEnhance, op)(original).enhance(amount), + original, op, amount) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index b50dcc943..5853fb28f 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper, fromstring, tostring +from .helper import PillowTestCase, hopper, fromstring, tostring from io import BytesIO @@ -144,7 +144,8 @@ class TestImageFile(PillowTestCase): class MockPyDecoder(ImageFile.PyDecoder): def decode(self, buffer): # eof - return (-1, 0) + return -1, 0 + xoff, yoff, xsize, ysize = 10, 20, 100, 100 @@ -153,7 +154,7 @@ class MockImageFile(ImageFile.ImageFile): def _open(self): self.rawmode = 'RGBA' self.mode = 'RGBA' - self.size = (200, 200) + self._size = (200, 200) self.tile = [("MOCK", (xoff, yoff, xoff+xsize, yoff+ysize), 32, None)] @@ -203,7 +204,7 @@ class TestPyDecoder(PillowTestCase): im = MockImageFile(buf) im.tile = [("MOCK", (xoff, yoff, -10, yoff+ysize), 32, None)] - d = self.get_decoder() + self.get_decoder() self.assertRaises(ValueError, im.load) @@ -214,13 +215,21 @@ class TestPyDecoder(PillowTestCase): buf = BytesIO(b'\x00'*255) im = MockImageFile(buf) - im.tile = [("MOCK", (xoff, yoff, xoff+xsize + 100, yoff+ysize), 32, None)] - d = self.get_decoder() + 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) -if __name__ == '__main__': - unittest.main() + def test_no_format(self): + buf = BytesIO(b'\x00'*255) + + im = MockImageFile(buf) + self.assertIsNone(im.format) + self.assertIsNone(im.get_format_mimetype()) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index c437e76b2..be8667211 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase from PIL import Image, ImageDraw, ImageFont, features from io import BytesIO @@ -42,7 +42,7 @@ class SimplePatcher(object): delattr(self._parent_obj, self._attr_name) -@unittest.skipUnless(HAS_FREETYPE, "ImageFont not Available") +@unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") class TestImageFont(PillowTestCase): LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC @@ -58,22 +58,27 @@ class TestImageFont(PillowTestCase): ('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)}, } def setUp(self): - freetype_version = tuple(ImageFont.core.freetype2_version.split('.'))[:2] - self.metrics = self.METRICS.get(freetype_version, self.METRICS['Default']) + freetype_version = tuple( + ImageFont.core.freetype2_version.split('.') + )[:2] + self.metrics = self.METRICS.get(freetype_version, + self.METRICS['Default']) def get_font(self): return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE) def test_sanity(self): - self.assertRegexpMatches( - ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") + self.assertRegex(ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") def test_font_properties(self): ttf = self.get_font() @@ -200,7 +205,8 @@ class TestImageFont(PillowTestCase): 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)) @@ -208,10 +214,9 @@ class TestImageFont(PillowTestCase): ttf = self.get_font() # Act/Assert - self.assertRaises(AssertionError, - draw.multiline_text, (0, 0), TEST_TEXT, - font=ttf, - align="unknown") + self.assertRaises( + ValueError, + draw.multiline_text, (0, 0), TEST_TEXT, font=ttf, align="unknown") def test_draw_align(self): im = Image.new('RGB', (300, 100), 'white') @@ -229,6 +234,11 @@ class TestImageFont(PillowTestCase): 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)) + # Test that textsize() can pass on additional arguments # to multiline_textsize() draw.textsize(TEST_TEXT, font=ttf, spacing=4) @@ -406,10 +416,10 @@ class TestImageFont(PillowTestCase): im = Image.new(mode='RGB', size=(300, 100)) target = im.copy() draw = ImageDraw.Draw(im) - #should not crash here. + # should not crash here. draw.text((10, 10), '', font=font) self.assert_image_equal(im, target) - + def test_unicode_pilfont(self): # should not segfault, should return UnicodeDecodeError # issue #2826 @@ -417,12 +427,12 @@ class TestImageFont(PillowTestCase): with self.assertRaises(UnicodeEncodeError): font.getsize(u"’") - 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): + def loadable_font(filepath, size, index, encoding, + *args, **kwargs): if filepath == path_to_fake: return ImageFont._FreeTypeFont(FONT_PATH, size, index, encoding, *args, **kwargs) @@ -435,7 +445,7 @@ class TestImageFont(PillowTestCase): self.assertEqual(('FreeMono', 'Regular'), name) @unittest.skipIf(sys.platform.startswith('win32'), - "requires Unix or MacOS") + "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 @@ -469,7 +479,7 @@ class TestImageFont(PillowTestCase): font_directory+'/Duplicate.ttf', 'Duplicate') @unittest.skipIf(sys.platform.startswith('win32'), - "requires Unix or MacOS") + "requires Unix or macOS") def test_find_macos_font(self): # Like the linux test, more cover hitting code rather than testing # correctness. @@ -508,12 +518,14 @@ class TestImageFont(PillowTestCase): 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)) @unittest.skipUnless(HAS_RAQM, "Raqm not Available") class TestImageFont_RaqmLayout(TestImageFont): LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py index 7ee5f8a2b..eb44957e4 100644 --- a/Tests/test_imagefont_bitmap.py +++ b/Tests/test_imagefont_bitmap.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase from PIL import Image, ImageFont, ImageDraw @@ -19,7 +19,8 @@ class TestImageFontBitmap(PillowTestCase): 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]) + 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) @@ -33,6 +34,3 @@ class TestImageFontBitmap(PillowTestCase): 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) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 79122f6c1..d23f6d86f 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,22 +1,22 @@ # -*- coding: utf-8 -*- -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase from PIL import Image, ImageDraw, ImageFont, features FONT_SIZE = 20 FONT_PATH = "Tests/fonts/DejaVuSans.ttf" + @unittest.skipUnless(features.check('raqm'), "Raqm Library is not installed.") class TestImagecomplextext(PillowTestCase): def test_english(self): - #smoke test, this should not fail + # smoke test, this should not fail ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode='RGB', size=(300, 100)) draw = ImageDraw.Draw(im) draw.text((0, 0), 'TEST', font=ttf, fill=500, direction='ltr') - - + def test_complex_text(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -30,7 +30,8 @@ class TestImagecomplextext(PillowTestCase): self.assert_image_similar(im, target_img, .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)) draw = ImageDraw.Draw(im) @@ -70,7 +71,8 @@ class TestImagecomplextext(PillowTestCase): 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_img = Image.open(target) @@ -82,7 +84,8 @@ class TestImagecomplextext(PillowTestCase): 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_img = Image.open(target) @@ -101,8 +104,8 @@ class TestImagecomplextext(PillowTestCase): self.assert_image_similar(im, target_img, .5) liga_size = ttf.getsize('fi', features=['-liga']) - self.assertEqual(liga_size,(13,19)) - + self.assertEqual(liga_size, (13, 19)) + def test_kerning_features(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -120,14 +123,10 @@ class TestImagecomplextext(PillowTestCase): 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_img = Image.open(target) self.assert_image_similar(im, target_img, .5) - -if __name__ == '__main__': - unittest.main() - -# End of file diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index b2edffa57..a2e7a028d 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,20 +1,30 @@ -from helper import unittest, PillowTestCase, on_appveyor +from .helper import PillowTestCase import sys +import subprocess try: from PIL import ImageGrab class TestImageGrab(PillowTestCase): - @unittest.skipIf(on_appveyor(), "Test fails on appveyor") def test_grab(self): im = ImageGrab.grab() self.assert_image(im, im.mode, im.size) - @unittest.skipIf(on_appveyor(), "Test fails on appveyor") - def test_grab2(self): - im = ImageGrab.grab() + def test_grabclipboard(self): + if sys.platform == "darwin": + subprocess.call(['screencapture', '-cx']) + else: + p = subprocess.Popen(['powershell', '-command', '-'], + stdin=subprocess.PIPE) + p.stdin.write(b'''[Reflection.Assembly]::LoadWithPartialName("System.Drawing") +[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") +$bmp = New-Object Drawing.Bitmap 200, 200 +[Windows.Forms.Clipboard]::SetImage($bmp)''') + p.communicate() + + im = ImageGrab.grabclipboard() self.assert_image(im, im.mode, im.size) except ImportError: @@ -43,7 +53,3 @@ class TestImageGrabImport(PillowTestCase): self.assertIsInstance(exception, ImportError) self.assertEqual(str(exception), "ImageGrab is macOS and Windows only") - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 4f99eda93..8273b63c1 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,5 +1,5 @@ from __future__ import print_function -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image from PIL import ImageMath @@ -13,11 +13,12 @@ def pixel(im): return int(im) # hack to deal with booleans print(im) + A = Image.new("L", (1, 1), 1) B = Image.new("L", (1, 1), 2) Z = Image.new("L", (1, 1), 0) # Z for zero F = Image.new("F", (1, 1), 3) -I = Image.new("I", (1, 1), 4) +I = Image.new("I", (1, 1), 4) # noqa: E741 A2 = A.resize((2, 2)) B2 = B.resize((2, 2)) @@ -181,7 +182,3 @@ class TestImageMath(PillowTestCase): 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") - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index b51b212e0..0cf15bd6c 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,8 +1,7 @@ # Test the ImageMorphology functionality -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper -from PIL import Image -from PIL import ImageMorph +from PIL import Image, ImageMorph, _imagingmorph class MorphTests(PillowTestCase): @@ -81,9 +80,15 @@ class MorphTests(PillowTestCase): def test_no_operator_loaded(self): mop = ImageMorph.MorphOp() - self.assertRaises(Exception, mop.apply, None) - self.assertRaises(Exception, mop.match, None) - self.assertRaises(Exception, mop.save_lut, None) + with self.assertRaises(Exception) as e: + mop.apply(None) + self.assertEqual(str(e.exception), 'No operator loaded') + with self.assertRaises(Exception) as e: + mop.match(None) + self.assertEqual(str(e.exception), 'No operator loaded') + with self.assertRaises(Exception) as e: + mop.save_lut(None) + self.assertEqual(str(e.exception), 'No operator loaded') # Test the named patterns def test_erosion8(self): @@ -214,9 +219,18 @@ class MorphTests(PillowTestCase): im = hopper('RGB') mop = ImageMorph.MorphOp(op_name="erosion8") - self.assertRaises(Exception, mop.apply, im) - self.assertRaises(Exception, mop.match, im) - self.assertRaises(Exception, mop.get_on_pixels, im) + with self.assertRaises(Exception) as e: + mop.apply(im) + self.assertEqual(str(e.exception), + 'Image must be binary, meaning it must use mode L') + with self.assertRaises(Exception) as e: + mop.match(im) + self.assertEqual(str(e.exception), + 'Image must be binary, meaning it must use mode L') + with self.assertRaises(Exception) as e: + mop.get_on_pixels(im) + self.assertEqual(str(e.exception), + 'Image must be binary, meaning it must use mode L') def test_add_patterns(self): # Arrange @@ -249,7 +263,11 @@ class MorphTests(PillowTestCase): lb.add_patterns(new_patterns) # Act / Assert - self.assertRaises(Exception, lb.build_lut) + with self.assertRaises(Exception) as e: + lb.build_lut() + self.assertEqual( + str(e.exception), + 'Syntax error in pattern "a pattern with a syntax error"') def test_load_invalid_mrl(self): # Arrange @@ -257,7 +275,10 @@ class MorphTests(PillowTestCase): mop = ImageMorph.MorphOp() # Act / Assert - self.assertRaises(Exception, mop.load_lut, invalid_mrl) + with self.assertRaises(Exception) as e: + mop.load_lut(invalid_mrl) + self.assertEqual(str(e.exception), + 'Wrong size operator file!') def test_roundtrip_mrl(self): # Arrange @@ -284,6 +305,19 @@ class MorphTests(PillowTestCase): # Assert self.assertEqual(mop.lut, lut) + def test_wrong_mode(self): + lut = ImageMorph.LutBuilder(op_name='corner').build_lut() + imrgb = Image.new('RGB', (10, 10)) + iml = Image.new('L', (10, 10)) -if __name__ == '__main__': - unittest.main() + with self.assertRaises(RuntimeError): + _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) + + with self.assertRaises(RuntimeError): + _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) + + with self.assertRaises(RuntimeError): + _imagingmorph.match(bytes(lut), imrgb.im.id) + + # Should not raise + _imagingmorph.match(bytes(lut), iml.im.id) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 11cf3619d..c5e48c431 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,6 +1,7 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import ImageOps +from PIL import Image class TestImageOps(PillowTestCase): @@ -23,6 +24,9 @@ class TestImageOps(PillowTestCase): ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) ImageOps.colorize(hopper("L"), "black", "white") + ImageOps.pad(hopper("L"), (128, 128)) + ImageOps.pad(hopper("RGB"), (128, 128)) + ImageOps.crop(hopper("L"), 1) ImageOps.crop(hopper("RGB"), 1) @@ -69,6 +73,26 @@ class TestImageOps(PillowTestCase): newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) self.assertEqual(newimg.size, (35, 35)) + def test_pad(self): + # Same ratio + im = hopper() + new_size = (im.width * 2, im.height * 2) + new_im = ImageOps.pad(im, new_size) + self.assertEqual(new_im.size, new_size) + + for label, color, new_size in [ + ("h", None, (im.width * 4, im.height * 2)), + ("v", "#f00", (im.width * 2, im.height * 4)) + ]: + for i, centering in enumerate([(0, 0), (0.5, 0.5), (1, 1)]): + new_im = ImageOps.pad(im, new_size, + color=color, centering=centering) + self.assertEqual(new_im.size, new_size) + + target = Image.open( + "Tests/images/imageops_pad_"+label+"_"+str(i)+".jpg") + self.assert_image_similar(new_im, target, 6) + def test_pil163(self): # Division by zero in equalize if < 255 pixels in image (@PIL163) @@ -94,6 +118,103 @@ class TestImageOps(PillowTestCase): newimg = ImageOps.scale(i, 0.5) self.assertEqual(newimg.size, (25, 25)) + def test_colorize_2color(self): + # Test the colorizing function with 2-color functionality -if __name__ == '__main__': - unittest.main() + # Open test image (256px by 10px, black to white) + im = Image.open("Tests/images/bw_gradient.png") + im = im.convert("L") + + # Create image with original 2-color functionality + im_test = ImageOps.colorize(im, 'red', 'green') + + # Test output image (2-color) + left = (0, 1) + middle = (127, 1) + right = (255, 1) + self.assert_tuple_approx_equal(im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg='black test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg='mid test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg='white test pixel incorrect') + + def test_colorize_2color_offset(self): + # Test the colorizing function with 2-color functionality and offset + + # Open test image (256px by 10px, black to white) + im = Image.open("Tests/images/bw_gradient.png") + im = im.convert("L") + + # Create image with original 2-color functionality with offsets + im_test = ImageOps.colorize(im, + black='red', + white='green', + blackpoint=50, + whitepoint=100) + + # Test output image (2-color) with offsets + left = (25, 1) + middle = (75, 1) + right = (125, 1) + self.assert_tuple_approx_equal(im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg='black test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg='mid test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg='white test pixel incorrect') + + def test_colorize_3color_offset(self): + # Test the colorizing function with 3-color functionality and offset + + # Open test image (256px by 10px, black to white) + im = Image.open("Tests/images/bw_gradient.png") + im = im.convert("L") + + # Create image with new three color functionality with offsets + im_test = ImageOps.colorize(im, + black='red', + white='green', + mid='blue', + blackpoint=50, + whitepoint=200, + midpoint=100) + + # Test output image (3-color) with offsets + left = (25, 1) + left_middle = (75, 1) + middle = (100, 1) + right_middle = (150, 1) + right = (225, 1) + self.assert_tuple_approx_equal(im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg='black test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(left_middle), + (127, 0, 127), + threshold=1, + msg='low-mid test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(middle), + (0, 0, 255), + threshold=1, + msg='mid incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(right_middle), + (0, 63, 127), + threshold=1, + msg='high-mid test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg='white test pixel incorrect') diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index cd7dcae5f..a867e5430 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,7 +1,6 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image -from PIL import ImageOps from PIL import ImageFilter im = Image.open("Tests/images/hopper.ppm") @@ -10,28 +9,6 @@ snakes = Image.open("Tests/images/color_snakes.png") class TestImageOpsUsm(PillowTestCase): - def test_ops_api(self): - - i = self.assert_warning(DeprecationWarning, - ImageOps.gaussian_blur, im, 2.0) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - - i = self.assert_warning(DeprecationWarning, - ImageOps.gblur, im, 2.0) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - - i = self.assert_warning(DeprecationWarning, - ImageOps.unsharp_mask, im, 2.0, 125, 8) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - - i = self.assert_warning(DeprecationWarning, - ImageOps.usm, im, 2.0, 125, 8) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - def test_filter_api(self): test_filter = ImageFilter.GaussianBlur(2.0) @@ -96,6 +73,3 @@ class TestImageOpsUsm(PillowTestCase): self.assertTrue(236 <= gp(8, 5)[2] <= 239) self.assertTrue(236 <= gp(8, 6)[2] <= 239) self.assertTrue(236 <= gp(8, 7)[1] <= 239) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 889f022ae..e4b5b7f72 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import ImagePalette, Image @@ -9,7 +9,7 @@ class TestImagePalette(PillowTestCase): ImagePalette.ImagePalette("RGB", list(range(256))*3) self.assertRaises(ValueError, - ImagePalette.ImagePalette, "RGB", list(range(256))*2) + ImagePalette.ImagePalette, "RGB", list(range(256))*2) def test_getcolor(self): @@ -66,7 +66,7 @@ class TestImagePalette(PillowTestCase): # Act self.assertRaises(NotImplementedError, - ImagePalette.make_linear_lut, black, white) + ImagePalette.make_linear_lut, black, white) def test_make_gamma_lut(self): # Arrange @@ -133,8 +133,4 @@ class TestImagePalette(PillowTestCase): def test_invalid_palette(self): self.assertRaises(IOError, - ImagePalette.load, "Tests/images/hopper.jpg") - - -if __name__ == '__main__': - unittest.main() + ImagePalette.load, "Tests/images/hopper.jpg") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 14cc4d14b..8cf88b7c1 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,6 +1,7 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import ImagePath, Image +from PIL._util import py3 import array import struct @@ -17,6 +18,11 @@ class TestImagePath(PillowTestCase): self.assertEqual(p[0], (0.0, 1.0)) self.assertEqual(p[-1], (8.0, 9.0)) self.assertEqual(list(p[:1]), [(0.0, 1.0)]) + with self.assertRaises(TypeError) as cm: + p['foo'] + self.assertEqual( + str(cm.exception), + "Path indices must be integers, not str") self.assertEqual( list(p), [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)]) @@ -72,10 +78,10 @@ class TestImagePath(PillowTestCase): # This fails due to the invalid malloc above, # and segfaults for i in range(200000): - if str is bytes: - x[i] = "0"*16 - else: + if py3: x[i] = b'0'*16 + else: + x[i] = "0"*16 class evil: @@ -88,7 +94,3 @@ class evil: def __setitem__(self, i, x): self.corrupt[i] = struct.unpack("dd", x) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index d3de5875b..2ded37c09 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import ImageQt @@ -33,6 +33,8 @@ class PillowQPixmapTestCase(PillowQtTestCase): from PyQt4.QtGui import QGuiApplication elif ImageQt.qt_version == 'side': from PySide.QtGui import QGuiApplication + elif ImageQt.qt_version == 'side2': + from PySide2.QtGui import QGuiApplication except ImportError: self.skipTest('QGuiApplication not installed') @@ -46,7 +48,7 @@ class PillowQPixmapTestCase(PillowQtTestCase): class TestImageQt(PillowQtTestCase, PillowTestCase): def test_rgb(self): - # from https://doc.qt.io/qt-4.8/qcolor.html + # 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. @@ -56,6 +58,8 @@ class TestImageQt(PillowQtTestCase, PillowTestCase): from PyQt4.QtGui import qRgb elif ImageQt.qt_version == 'side': from PySide.QtGui import qRgb + elif ImageQt.qt_version == 'side2': + from PySide2.QtGui import qRgb self.assertEqual(qRgb(0, 0, 0), qRgba(0, 0, 0, 255)) @@ -74,7 +78,3 @@ class TestImageQt(PillowQtTestCase, PillowTestCase): def test_image(self): for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): ImageQt.ImageQt(hopper(mode)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 2a4a358ba..9fbf3fed8 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image, ImageSequence, TiffImagePlugin @@ -69,6 +69,3 @@ class TestImageSequence(PillowTestCase): im.seek(0) color2 = im.getpalette()[0:3] self.assertEqual(color1, color2) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index da91e35c7..899c057d6 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image from PIL import ImageShow @@ -14,6 +14,9 @@ class TestImageShow(PillowTestCase): # Test registering a viewer that is not a class ImageShow.register("not a class") + # Restore original state + ImageShow._viewers.pop() + def test_show(self): class TestViewer: methodCalled = False @@ -28,6 +31,9 @@ class TestImageShow(PillowTestCase): self.assertTrue(ImageShow.show(im)) self.assertTrue(viewer.methodCalled) + # Restore original state + ImageShow._viewers.pop(0) + def test_viewer(self): viewer = ImageShow.Viewer() @@ -35,6 +41,6 @@ class TestImageShow(PillowTestCase): self.assertRaises(NotImplementedError, viewer.get_command, None) - -if __name__ == '__main__': - unittest.main() + def test_viewers(self): + for viewer in ImageShow._viewers: + viewer.get_command('test.jpg') diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 77eb0aac1..c2580a1b1 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image from PIL import ImageStat @@ -55,7 +55,3 @@ class TestImageStat(PillowTestCase): self.assertEqual(st.rms[0], 128) self.assertEqual(st.var[0], 0) self.assertEqual(st.stddev[0], 0) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index fbf48a1b6..a6a4dd4ea 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,29 +1,32 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import unittest, PillowTestCase, hopper from PIL import Image +from PIL._util import py3 try: from PIL import ImageTk - import Tkinter as tk + if py3: + import tkinter as tk + else: + import Tkinter as tk dir(ImageTk) HAS_TK = True -except (OSError, ImportError) as v: +except (OSError, ImportError): # Skipped via setUp() HAS_TK = False TK_MODES = ('1', 'L', 'P', 'RGB', 'RGBA') +@unittest.skipIf(not HAS_TK, "Tk not installed") class TestImageTk(PillowTestCase): def setUp(self): - if not HAS_TK: - self.skipTest("Tk not installed") try: # setup tk - app = tk.Frame() + tk.Frame() # root = tk.Tk() - except (tk.TclError) as v: + except tk.TclError as v: self.skipTest("TCL Error: %s" % v) def test_kw(self): @@ -84,7 +87,3 @@ class TestImageTk(PillowTestCase): # reloaded = ImageTk.getimage(im_tk) # self.assert_image_equal(reloaded, im) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 70bf28247..16d681f2c 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import unittest, PillowTestCase, hopper from PIL import ImageWin import sys @@ -106,7 +106,3 @@ class TestImageWinDib(PillowTestCase): # Assert # Confirm they're the same self.assertEqual(dib1.tobytes(), dib2.tobytes()) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index 7178d8cb8..64f921916 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image, ImageWin import sys @@ -96,7 +96,6 @@ if sys.platform.startswith('win32'): hdr.biClrImportant = 0 hdc = CreateCompatibleDC(None) - # print('hdc:',hex(hdc)) pixels = ctypes.c_void_p() dib = CreateDIBSection(hdc, ctypes.byref(hdr), DIB_RGB_COLORS, ctypes.byref(pixels), None, 0) @@ -108,6 +107,3 @@ if sys.platform.startswith('win32'): DeleteDC(hdc) Image.open(BytesIO(bitmap)).save(opath) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index aefee2e08..466c43f88 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index c5eb2686c..543d151ac 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,6 +1,6 @@ import sys -from helper import unittest, PillowTestCase, py3 +from .helper import PillowTestCase from PIL import Image @@ -17,33 +17,34 @@ class TestLibPack(PillowTestCase): for x, pixel in enumerate(pixels): im.putpixel((x, 0), pixel) - if isinstance(data, (int)): + if isinstance(data, int): data_len = data * len(pixels) data = bytes(bytearray(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", 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) 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)) + 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) @@ -52,132 +53,163 @@ class TestLibPack(PillowTestCase): self.assert_pack("P", "P", 1, 1, 2, 3, 4) def test_PA(self): - self.assert_pack("PA", "PA", 2, (1,2), (3,4), (5,6)) - self.assert_pack("PA", "PA;L", 2, (1,4), (2,5), (3,6)) + self.assert_pack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) + self.assert_pack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) 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)) - self.assert_pack("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)) - self.assert_pack("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)) + 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)) + self.assert_pack( + "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)) + self.assert_pack( + "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;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)) + self.assert_pack( + "RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + 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)) 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;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", "BGRX", + self.assert_pack( + "RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack( + "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", "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", + (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)) + (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;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)) + 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)) + 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)) 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", "YCbCrX", + 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", "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", + (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)) + (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", "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)) + 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)) def test_HSV(self): - self.assert_pack("HSV", "HSV", 3, (1,2,3), (4,5,6), (7,8,9)) - self.assert_pack("HSV", "H", 1, (1,9,9), (2,9,9), (3,9,9)) - self.assert_pack("HSV", "S", 1, (9,1,9), (9,2,9), (9,3,9)) - self.assert_pack("HSV", "V", 1, (9,9,1), (9,9,2), (9,9,3)) + self.assert_pack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_pack("HSV", "H", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) + self.assert_pack("HSV", "S", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) + self.assert_pack("HSV", "V", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) 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) + self.assert_pack( + "I", "I;32S", + b'\x83\x00\x00\x01\x01\x00\x00\x83', 0x01000083, -2097151999) 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) + self.assert_pack( + "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) + self.assert_pack( + "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': - 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) + 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) else: - 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) + 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) class TestLibUnpack(PillowTestCase): @@ -185,11 +217,12 @@ class TestLibUnpack(PillowTestCase): """ data - either raw bytes with data or just number of bytes in rawmode. """ - if isinstance(data, (int)): + if isinstance(data, int): data_len = data * len(pixels) data = bytes(bytearray(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))) @@ -223,10 +256,9 @@ class TestLibUnpack(PillowTestCase): 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;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)) @@ -234,9 +266,11 @@ class TestLibUnpack(PillowTestCase): 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;2L", b'\xe4', 1, 1, 1, 0) # erroneous? + # 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;4L", b'\x02\xef', 2, 10, 10, 0) # erroneous? + # erroneous? + # self.assert_unpack("P", "P;4L", b'\x02\xef', 2, 10, 10, 0) self.assert_unpack("P", "P", 1, 1, 2, 3, 4) self.assert_unpack("P", "P;R", 1, 128, 64, 192, 32) @@ -245,152 +279,218 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) def test_RGB(self): - self.assert_unpack("RGB", "RGB", 3, (1,2,3), (4,5,6), (7,8,9)) - self.assert_unpack("RGB", "RGB;L", 3, (1,4,7), (2,5,8), (3,6,9)) - self.assert_unpack("RGB", "RGB;R", 3, (128,64,192), (32,160,96)) - self.assert_unpack("RGB", "RGB;16L", 6, (2,4,6), (8,10,12)) - self.assert_unpack("RGB", "RGB;16B", 6, (1,3,5), (7,9,11)) - self.assert_unpack("RGB", "BGR", 3, (3,2,1), (6,5,4), (9,8,7)) - self.assert_unpack("RGB", "RGB;15", 2, (8,131,0), (24,0,8)) - self.assert_unpack("RGB", "BGR;15", 2, (0,131,8), (8,0,24)) - self.assert_unpack("RGB", "RGB;16", 2, (8,64,0), (24,129,0)) - self.assert_unpack("RGB", "BGR;16", 2, (0,64,8), (0,129,24)) - self.assert_unpack("RGB", "RGB;4B", 2, (17,0,34), (51,0,68)) - 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", "YCC;P", + self.assert_unpack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_unpack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_unpack("RGB", "RGB;R", 3, (128, 64, 192), (32, 160, 96)) + self.assert_unpack("RGB", "RGB;16L", 6, (2, 4, 6), (8, 10, 12)) + self.assert_unpack("RGB", "RGB;16B", 6, (1, 3, 5), (7, 9, 11)) + self.assert_unpack("RGB", "BGR", 3, (3, 2, 1), (6, 5, 4), (9, 8, 7)) + self.assert_unpack("RGB", "RGB;15", 2, (8, 131, 0), (24, 0, 8)) + self.assert_unpack("RGB", "BGR;15", 2, (0, 131, 8), (8, 0, 24)) + self.assert_unpack("RGB", "RGB;16", 2, (8, 64, 0), (24, 129, 0)) + self.assert_unpack("RGB", "BGR;16", 2, (0, 64, 8), (0, 129, 24)) + self.assert_unpack("RGB", "RGB;4B", 2, (17, 0, 34), (51, 0, 68)) + 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", "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)) + (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;16B", 4, - (1,1,1,3), (5,5,5,7), (9,9,9,11)) - self.assert_unpack("RGBA", "RGBA", 4, - (1,2,3,4), (5,6,7,8), (9,10,11,12)) - self.assert_unpack("RGBA", "RGBa", 4, - (63,127,191,4), (159,191,223,8), (191,212,233,12)) - self.assert_unpack("RGBA", "RGBa", - b'\x01\x02\x03\x00\x10\x20\x30\xff', - (0,0,0,0), (16,32,48,255)) - self.assert_unpack("RGBA", "RGBa;16L", 8, - (63,127,191,8), (159,191,223,16), (191,212,233,24)) - self.assert_unpack("RGBA", "RGBa;16L", + self.assert_unpack( + "RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) + self.assert_unpack( + "RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11)) + self.assert_unpack( + "RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_unpack( + "RGBA", "RGBAX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14)) + self.assert_unpack( + "RGBA", "RGBAXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16)) + self.assert_unpack( + "RGBA", "RGBa", 4, + (63, 127, 191, 4), (159, 191, 223, 8), (191, 212, 233, 12)) + 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)) + 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)) + 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)) + self.assert_unpack( + "RGBA", "RGBa;16L", 8, + (63, 127, 191, 8), (159, 191, 223, 16), (191, 212, 233, 24)) + 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)) - self.assert_unpack("RGBA", "RGBa;16B", 8, - (36,109,182,7), (153,187,221,15), (188,210,232,23)) - self.assert_unpack("RGBA", "RGBa;16B", + (0, 0, 0, 0), (16, 32, 48, 255)) + self.assert_unpack( + "RGBA", "RGBa;16B", 8, + (36, 109, 182, 7), (153, 187, 221, 15), (188, 210, 232, 23)) + 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)) - self.assert_unpack("RGBA", "BGRa", 4, - (191,127,63,4), (223,191,159,8), (233,212,191,12)) - self.assert_unpack("RGBA", "BGRa", + (0, 0, 0, 0), (16, 32, 48, 255)) + self.assert_unpack( + "RGBA", "BGRa", 4, + (191, 127, 63, 4), (223, 191, 159, 8), (233, 212, 191, 12)) + self.assert_unpack( + "RGBA", "BGRa", b'\x01\x02\x03\x00\x10\x20\x30\xff', - (0,0,0,0), (48,32,16,255)) - 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)) - 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", "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", + (0, 0, 0, 0), (48, 32, 16, 255)) + 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)) + 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", "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)) + (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)) - 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", "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)) + 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)) 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;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", "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)) - 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)) - self.assert_unpack("RGBX", "YCC;P", + 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", "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)) + self.assert_unpack( + "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)) + 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)) + 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)) + (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)) - self.assert_unpack("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)) - self.assert_unpack("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)) - self.assert_unpack("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)) + self.assert_unpack( + "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)) + self.assert_unpack( + "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)) + 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)) + self.assert_unpack( + "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)) + self.assert_unpack( + "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)) 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", "YCbCrX", 4, (1,2,3), (5,6,7), (9,10,11)) - self.assert_unpack("YCbCr", "YCbCrK", 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", "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)) + 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)) def test_HSV(self): - self.assert_unpack("HSV", "HSV", 3, (1,2,3), (4,5,6), (7,8,9)) - self.assert_unpack("HSV", "H", 1, (1,0,0), (2,0,0), (3,0,0)) - self.assert_unpack("HSV", "S", 1, (0,1,0), (0,2,0), (0,3,0)) - self.assert_unpack("HSV", "V", 1, (0,0,1), (0,0,2), (0,0,3)) + self.assert_unpack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_unpack("HSV", "H", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) + self.assert_unpack("HSV", "S", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) + self.assert_unpack("HSV", "V", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) def test_I(self): self.assert_unpack("I", "I;8", 1, 0x01, 0x02, 0x03, 0x04) @@ -400,30 +500,32 @@ class TestLibUnpack(PillowTestCase): 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;32", 4, 0x04030201, 0x08070605) - self.assert_unpack("I", "I;32S", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 0x01000083, -2097151999) + self.assert_unpack( + "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) + self.assert_unpack( + "I", "I;32BS", + b'\x83\x00\x00\x01\x01\x00\x00\x83', -2097151999, 0x01000083) 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) + self.assert_unpack( + "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) + self.assert_unpack( + "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) @@ -433,55 +535,70 @@ class TestLibUnpack(PillowTestCase): 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;32", 4, 67305984, 134678016) - self.assert_unpack("F", "F;32S", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 16777348, -2097152000) + self.assert_unpack( + "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) + self.assert_unpack( + "F", "F;32BS", + b'\x83\x00\x00\x01\x01\x00\x00\x83', -2097152000, 16777348) 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) + self.assert_unpack( + "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", + self.assert_unpack( + "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, + self.assert_unpack( + "F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_unpack("F", "F;32BF", 4, + self.assert_unpack( + "F", "F;32BF", 4, 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_unpack("F", "F;64F", + self.assert_unpack( + "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", + self.assert_unpack( + "F", "F;64BF", b'?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00', # by struct.pack 0.15000000596046448, -1234.5) if sys.byteorder == 'little': - self.assert_unpack("F", "F", 4, + self.assert_unpack( + "F", "F", 4, 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_unpack("F", "F;32NF", 4, + self.assert_unpack( + "F", "F;32NF", 4, 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_unpack("F", "F;64NF", + self.assert_unpack( + "F", "F;64NF", b'333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0', 0.15000000596046448, -1234.5) else: - self.assert_unpack("F", "F", 4, + self.assert_unpack( + "F", "F", 4, 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_unpack("F", "F;32NF", 4, + self.assert_unpack( + "F", "F;32NF", 4, 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_unpack("F", "F;64NF", + self.assert_unpack( + "F", "F;64NF", b'?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00', 0.15000000596046448, -1234.5) @@ -503,7 +620,3 @@ class TestLibUnpack(PillowTestCase): self.assertRaises(ValueError, self.assert_unpack, "L", "L", 0, 0) self.assertRaises(ValueError, self.assert_unpack, "RGB", "RGB", 2, 0) self.assertRaises(ValueError, self.assert_unpack, "CMYK", "CMYK", 2, 0) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 142753791..d40019e59 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,5 +1,5 @@ from __future__ import print_function -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase from PIL import Image @@ -29,10 +29,6 @@ class TestLocale(PillowTestCase): Image.open(path) try: locale.setlocale(locale.LC_ALL, "polish") - except: + except locale.Error: unittest.skip('Polish locale not available') Image.open(path) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_map.py b/Tests/test_map.py index 14bd835a2..2eeb1fc5f 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,10 +1,11 @@ -from helper import PillowTestCase, unittest +from .helper import PillowTestCase, unittest import sys from PIL import Image -@unittest.skipIf(sys.platform.startswith('win32'), "Win32 does not call map_buffer") +@unittest.skipIf(sys.platform.startswith('win32'), + "Win32 does not call map_buffer") class TestMap(PillowTestCase): def test_overflow(self): # There is the potential to overflow comparisons in map.c @@ -22,7 +23,3 @@ class TestMap(PillowTestCase): im.load() Image.MAX_IMAGE_PIXELS = max_pixels - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index d51847199..80730a312 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import Image @@ -105,7 +105,3 @@ class TestModeI16(PillowTestCase): self.verify(im.convert("I;16B")) self.verify(im.convert("I;16B").convert("L")) self.verify(im.convert("I;16B").convert("I")) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 7eeee3a83..c9c3b0f1e 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,39 +1,19 @@ from __future__ import print_function -import sys -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper, unittest from PIL import Image try: - import site import numpy - assert site # silence warning - assert numpy # silence warning except ImportError: - # Skip via setUp() - pass + numpy = None + TEST_IMAGE_SIZE = (10, 10) -# Numpy on pypy as of pypy 5.3.1 is corrupting the numpy.array(Image) -# call such that it's returning a object of type numpy.ndarray, but -# the repr is that of a PIL.Image. Size and shape are 1 and (), not the -# size and shape of the array. This causes failures in several tests. -SKIP_NUMPY_ON_PYPY = hasattr(sys, 'pypy_version_info') and ( - sys.pypy_version_info <= (5, 3, 1, 'final', 0)) - +@unittest.skipIf(numpy is None, "Numpy is not installed") class TestNumpy(PillowTestCase): - - def setUp(self): - try: - import site - import numpy - assert site # silence warning - assert numpy # silence warning - except ImportError: - self.skipTest("ImportError") - def test_numpy_to_image(self): def to_image(dtype, bands=1, boolean=0): @@ -54,7 +34,6 @@ class TestNumpy(PillowTestCase): i = Image.fromarray(a) if list(i.getchannel(0).getdata()) != list(range(100)): print("data mismatch for", dtype) - # print(dtype, list(i.getdata())) return i # Check supported 1-bit integer formats @@ -121,7 +100,6 @@ class TestNumpy(PillowTestCase): for y in range(0, img.size[1], int(img.size[1]/10)): self.assert_deep_equal(px[x, y], np[y, x]) - @unittest.skipIf(SKIP_NUMPY_ON_PYPY, "numpy.array(Image) is flaky on PyPy") def test_16bit(self): img = Image.open('Tests/images/16bit.cropped.tif') np_img = numpy.array(img) @@ -145,14 +123,15 @@ class TestNumpy(PillowTestCase): def test_save_tiff_uint16(self): # Tests that we're getting the pixel value in the right byte order. pixel_value = 0x1234 - a = numpy.array([pixel_value] * TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1], dtype=numpy.uint16) + a = numpy.array( + [pixel_value] * TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1], + dtype=numpy.uint16) a.shape = TEST_IMAGE_SIZE img = Image.fromarray(a) img_px = img.load() self.assertEqual(img_px[0, 0], pixel_value) - @unittest.skipIf(SKIP_NUMPY_ON_PYPY, "numpy.array(Image) is flaky on PyPy") def test_to_array(self): def _to_array(mode, dtype): @@ -214,7 +193,7 @@ class TestNumpy(PillowTestCase): def test_bool(self): # https://github.com/python-pillow/Pillow/issues/2044 - a = numpy.zeros((10,2), dtype=numpy.bool) + a = numpy.zeros((10, 2), dtype=numpy.bool) a[0][0] = True im2 = Image.fromarray(a) @@ -229,7 +208,3 @@ class TestNumpy(PillowTestCase): # Act/Assert self.assert_warning(None, lambda: array(im)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py new file mode 100644 index 000000000..da69d258d --- /dev/null +++ b/Tests/test_pdfparser.py @@ -0,0 +1,124 @@ +from .helper import PillowTestCase + +from PIL.PdfParser import IndirectObjectDef, IndirectReference, PdfBinary, \ + PdfDict, PdfFormatError, PdfName, PdfParser, \ + PdfStream, decode_text, encode_text, pdf_repr +import time + + +class TestPdfParser(PillowTestCase): + + def test_text_encode_decode(self): + self.assertEqual(encode_text("abc"), b"\xFE\xFF\x00a\x00b\x00c") + self.assertEqual(decode_text(b"\xFE\xFF\x00a\x00b\x00c"), "abc") + self.assertEqual(decode_text(b"abc"), "abc") + self.assertEqual(decode_text(b"\x1B a \x1C"), u"\u02D9 a \u02DD") + + def test_indirect_refs(self): + self.assertEqual(IndirectReference(1, 2), IndirectReference(1, 2)) + self.assertNotEqual(IndirectReference(1, 2), IndirectReference(1, 3)) + self.assertNotEqual(IndirectReference(1, 2), IndirectObjectDef(1, 2)) + self.assertNotEqual(IndirectReference(1, 2), (1, 2)) + self.assertEqual(IndirectObjectDef(1, 2), IndirectObjectDef(1, 2)) + self.assertNotEqual(IndirectObjectDef(1, 2), IndirectObjectDef(1, 3)) + self.assertNotEqual(IndirectObjectDef(1, 2), IndirectReference(1, 2)) + self.assertNotEqual(IndirectObjectDef(1, 2), (1, 2)) + + def test_parsing(self): + self.assertEqual(PdfParser.interpret_name(b"Name#23Hash"), + b"Name#Hash") + self.assertEqual(PdfParser.interpret_name( + b"Name#23Hash", as_text=True + ), "Name#Hash") + self.assertEqual(PdfParser.get_value(b"1 2 R ", 0), + (IndirectReference(1, 2), 5)) + self.assertEqual(PdfParser.get_value(b"true[", 0), (True, 4)) + self.assertEqual(PdfParser.get_value(b"false%", 0), (False, 5)) + self.assertEqual(PdfParser.get_value(b"null<", 0), (None, 4)) + 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"(\\0053)", 0), (b"\x053", 7)) + self.assertEqual(PdfParser.get_value(b"(\\053)", 0), (b"\x2B", 6)) + self.assertEqual(PdfParser.get_value(b"(\\53)", 0), (b"\x2B", 5)) + self.assertEqual(PdfParser.get_value(b"(\\53a)", 0), (b"\x2Ba", 6)) + self.assertEqual(PdfParser.get_value(b"(\\1111)", 0), (b"\x491", 7)) + self.assertEqual(PdfParser.get_value(b" 123 (", 0), (123, 4)) + self.assertAlmostEqual(PdfParser.get_value(b" 123.4 %", 0)[0], 123.4) + self.assertEqual(PdfParser.get_value(b" 123.4 %", 0)[1], 6) + self.assertRaises(PdfFormatError, PdfParser.get_value, b"]", 0) + d = PdfParser.get_value(b"<>", 0)[0] + self.assertIsInstance(d, PdfDict) + self.assertEqual(len(d), 2) + self.assertEqual(d.Name, "value") + self.assertEqual(d[b"Name"], b"value") + self.assertEqual(d.N, PdfName("V")) + a = PdfParser.get_value(b"[/Name (value) /N /V]", 0)[0] + self.assertIsInstance(a, list) + self.assertEqual(len(a), 4) + self.assertEqual(a[0], PdfName("Name")) + s = PdfParser.get_value( + b"<>\nstream\nabcde\nendstream<<...", 0 + )[0] + self.assertIsInstance(s, PdfStream) + self.assertEqual(s.dictionary.Name, "value") + self.assertEqual(s.decode(), b"abcde") + for name in ["CreationDate", "ModDate"]: + for date, value in { + b"20180729214124": "20180729214124", + b"D:20180729214124": "20180729214124", + b"D:2018072921": "20180729210000", + b"D:20180729214124Z": "20180729214124", + b"D:20180729214124+08'00'": "20180729134124", + b"D:20180729214124-05'00'": "20180730024124" + }.items(): + d = PdfParser.get_value( + b"<>", 0)[0] + self.assertEqual( + time.strftime("%Y%m%d%H%M%S", getattr(d, name)), value) + + def test_pdf_repr(self): + self.assertEqual(bytes(IndirectReference(1, 2)), b"1 2 R") + self.assertEqual(bytes(IndirectObjectDef(*IndirectReference(1, 2))), + b"1 2 obj") + self.assertEqual(bytes(PdfName(b"Name#Hash")), b"/Name#23Hash") + self.assertEqual(bytes(PdfName("Name#Hash")), b"/Name#23Hash") + self.assertEqual(bytes(PdfDict({b"Name": IndirectReference(1, 2)})), + b"<<\n/Name 1 2 R\n>>") + self.assertEqual(bytes(PdfDict({"Name": IndirectReference(1, 2)})), + b"<<\n/Name 1 2 R\n>>") + self.assertEqual(pdf_repr(IndirectReference(1, 2)), b"1 2 R") + self.assertEqual(pdf_repr(IndirectObjectDef(*IndirectReference(1, 2))), + b"1 2 obj") + self.assertEqual(pdf_repr(PdfName(b"Name#Hash")), b"/Name#23Hash") + self.assertEqual(pdf_repr(PdfName("Name#Hash")), b"/Name#23Hash") + self.assertEqual(pdf_repr(PdfDict({b"Name": IndirectReference(1, 2)})), + b"<<\n/Name 1 2 R\n>>") + self.assertEqual(pdf_repr(PdfDict({"Name": IndirectReference(1, 2)})), + b"<<\n/Name 1 2 R\n>>") + self.assertEqual(pdf_repr(123), b"123") + self.assertEqual(pdf_repr(True), b"true") + self.assertEqual(pdf_repr(False), b"false") + self.assertEqual(pdf_repr(None), b"null") + self.assertEqual(pdf_repr(b"a)/b\\(c"), br"(a\)/b\\\(c)") + self.assertEqual(pdf_repr([123, True, {"a": PdfName(b"b")}]), + b"[ 123 true <<\n/a /b\n>> ]") + self.assertEqual(pdf_repr(PdfBinary(b"\x90\x1F\xA0")), b"<901FA0>") diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 69eb60949..46958c085 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image @@ -91,6 +91,3 @@ class TestPickle(PillowTestCase): for protocol in range(0, cPickle.HIGHEST_PROTOCOL + 1): self.helper_pickle_string(cPickle, protocol, mode="L") self.helper_pickle_file(cPickle, protocol, mode="L") - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 17fa3662b..73ef5e2b2 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import Image, PSDraw import os @@ -61,7 +61,3 @@ class TestPsDraw(PillowTestCase): sys.stdout = old_stdout self.assertNotEqual(mystdout.getvalue(), "") - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 962535f03..3c4e3d9a2 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,23 +1,16 @@ -from helper import unittest, PillowTestCase +from .helper import unittest, PillowTestCase -from PIL import PILLOW_VERSION +from PIL import __version__ try: import pyroma except ImportError: - # Skip via setUp() - pass + pyroma = None +@unittest.skipIf(pyroma is None, "Pyroma is not installed") class TestPyroma(PillowTestCase): - def setUp(self): - try: - import pyroma - assert pyroma # Ignore warning - except ImportError: - self.skipTest("ImportError") - def test_pyroma(self): # Arrange data = pyroma.projectdata.get_data(".") @@ -26,7 +19,7 @@ class TestPyroma(PillowTestCase): rating = pyroma.ratings.rate(data) # Assert - if 'rc' in PILLOW_VERSION: + if 'rc' in __version__: # Pyroma needs to chill about RC versions # and not kill all our tests. self.assertEqual(rating, (9, [ @@ -35,7 +28,3 @@ class TestPyroma(PillowTestCase): else: # Should have a perfect score self.assertEqual(rating, (10, [])) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_fromqpixmap.py b/Tests/test_qt_image_fromqpixmap.py similarity index 75% rename from Tests/test_image_fromqpixmap.py rename to Tests/test_qt_image_fromqpixmap.py index 543b74bbf..358f1573d 100644 --- a/Tests/test_image_fromqpixmap.py +++ b/Tests/test_qt_image_fromqpixmap.py @@ -1,5 +1,5 @@ -from helper import unittest, PillowTestCase, hopper -from test_imageqt import PillowQtTestCase, PillowQPixmapTestCase +from .helper import PillowTestCase, hopper +from .test_imageqt import PillowQPixmapTestCase from PIL import ImageQt @@ -7,7 +7,6 @@ from PIL import ImageQt class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase): def roundtrip(self, expected): - PillowQtTestCase.setUp(self) result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb self.assert_image_equal(result, expected.convert('RGB')) @@ -26,7 +25,3 @@ class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase): def test_sanity_p(self): self.roundtrip(hopper('P')) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_toqimage.py b/Tests/test_qt_image_toqimage.py similarity index 67% rename from Tests/test_image_toqimage.py rename to Tests/test_qt_image_toqimage.py index 6d7715c80..c1aa64b57 100644 --- a/Tests/test_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,5 +1,5 @@ -from helper import unittest, PillowTestCase, hopper -from test_imageqt import PillowQtTestCase +from .helper import PillowTestCase, hopper +from .test_imageqt import PillowQtTestCase from PIL import ImageQt, Image @@ -13,19 +13,26 @@ if ImageQt.qt_is_installed: QT_VERSION = 5 except (ImportError, RuntimeError): try: - from PyQt4 import QtGui - from PyQt4.QtGui import QWidget, QHBoxLayout, QLabel, QApplication - QT_VERSION = 4 + from PySide2 import QtGui + from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, \ + QApplication + QT_VERSION = 5 except (ImportError, RuntimeError): - from PySide import QtGui - from PySide.QtGui import QWidget, QHBoxLayout, QLabel, QApplication - QT_VERSION = 4 + try: + from PyQt4 import QtGui + from PyQt4.QtGui import QWidget, QHBoxLayout, QLabel, \ + QApplication + QT_VERSION = 4 + except (ImportError, RuntimeError): + from PySide import QtGui + from PySide.QtGui import QWidget, QHBoxLayout, QLabel, \ + QApplication + QT_VERSION = 4 class TestToQImage(PillowQtTestCase, PillowTestCase): def test_sanity(self): - PillowQtTestCase.setUp(self) for mode in ('RGB', 'RGBA', 'L', 'P', '1'): src = hopper(mode) data = ImageQt.toqimage(src) @@ -43,8 +50,9 @@ class TestToQImage(PillowQtTestCase, PillowTestCase): if mode == '1': # BW appears to not save correctly on QT4 and QT5 # kicks out errors on console: - # libpng warning: Invalid color type/bit depth combination in IHDR - # libpng error: Invalid IHDR data + # libpng warning: Invalid color type/bit depth combination + # in IHDR + # libpng error: Invalid IHDR data continue # Test saving the file @@ -60,12 +68,10 @@ class TestToQImage(PillowQtTestCase, PillowTestCase): self.assert_image_equal(reloaded, src) def test_segfault(self): - PillowQtTestCase.setUp(self) - app = QApplication([]) ex = Example() - assert(app) # Silence warning - assert(ex) # Silence warning + assert app # Silence warning + assert ex # Silence warning if ImageQt.qt_is_installed: @@ -80,12 +86,8 @@ if ImageQt.qt_is_installed: pixmap1 = QtGui.QPixmap.fromImage(qimage) - hbox = QHBoxLayout(self) + QHBoxLayout(self) # hbox lbl = QLabel(self) # Segfault in the problem lbl.setPixmap(pixmap1.copy()) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_toqpixmap.py b/Tests/test_qt_image_toqpixmap.py similarity index 71% rename from Tests/test_image_toqpixmap.py rename to Tests/test_qt_image_toqpixmap.py index c6555d7ff..9bb7183b7 100644 --- a/Tests/test_image_toqpixmap.py +++ b/Tests/test_qt_image_toqpixmap.py @@ -1,5 +1,5 @@ -from helper import unittest, PillowTestCase, hopper -from test_imageqt import PillowQtTestCase, PillowQPixmapTestCase +from .helper import PillowTestCase, hopper +from .test_imageqt import PillowQPixmapTestCase from PIL import ImageQt @@ -10,8 +10,6 @@ if ImageQt.qt_is_installed: class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase): def test_sanity(self): - PillowQtTestCase.setUp(self) - for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): data = ImageQt.toqpixmap(hopper(mode)) @@ -21,7 +19,3 @@ class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase): # Test saving the file tempfile = self.tempfile('temp_{}.png'.format(mode)) data.save(tempfile) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_scipy.py b/Tests/test_scipy.py deleted file mode 100644 index 18c4403a0..000000000 --- a/Tests/test_scipy.py +++ /dev/null @@ -1,53 +0,0 @@ -from helper import unittest, PillowTestCase -from distutils.version import LooseVersion -try: - import numpy as np - from numpy.testing import assert_equal - - from scipy import misc - import scipy - HAS_SCIPY = True -except ImportError: - HAS_SCIPY = False - - -class Test_scipy_resize(PillowTestCase): - """ Tests for scipy regression in Pillow 2.6.0 - - Tests from https://github.com/scipy/scipy/blob/master/scipy/misc/pilutil.py - """ - - def setUp(self): - if not HAS_SCIPY: - self.skipTest("Scipy Required") - - def test_imresize(self): - im = np.random.random((10, 20)) - for T in np.sctypes['float'] + [float]: - # 1.1 rounds to below 1.1 for float16, 1.101 works - im1 = misc.imresize(im, T(1.101)) - self.assertEqual(im1.shape, (11, 22)) - - # this test fails prior to scipy 0.14.0b1 - # https://github.com/scipy/scipy/commit/855ff1fff805fb91840cf36b7082d18565fc8352 - @unittest.skipIf(HAS_SCIPY and - (LooseVersion(scipy.__version__) < LooseVersion('0.14.0')), - "Test fails on scipy < 0.14.0") - def test_imresize4(self): - im = np.array([[1, 2], - [3, 4]]) - res = np.array([[1., 1.25, 1.75, 2.], - [1.5, 1.75, 2.25, 2.5], - [2.5, 2.75, 3.25, 3.5], - [3., 3.25, 3.75, 4.]], dtype=np.float32) - # Check that resizing by target size, float and int are the same - im2 = misc.imresize(im, (4, 4), mode='F') # output size - im3 = misc.imresize(im, 2., mode='F') # fraction - im4 = misc.imresize(im, 200, mode='F') # percentage - assert_equal(im2, res) - assert_equal(im3, res) - assert_equal(im4, res) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index acfea3bae..77fd67f01 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,5 +1,5 @@ -from helper import unittest, PillowTestCase -from helper import djpeg_available, cjpeg_available, netpbm_available +from .helper import unittest, PillowTestCase +from .helper import djpeg_available, cjpeg_available, netpbm_available import sys import shutil @@ -18,7 +18,7 @@ test_filenames = ( ) -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or macOS") class TestShellInjection(PillowTestCase): def assert_save_filename_check(self, src_img, save_func): @@ -51,7 +51,3 @@ class TestShellInjection(PillowTestCase): def test_save_netpbm_filename_l_mode(self): im = Image.open(TEST_GIF).convert("L") self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 0bf4503c4..fae4d7ed6 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import PillowTestCase, hopper from PIL import TiffImagePlugin, Image from PIL.TiffImagePlugin import IFDRational @@ -30,7 +30,7 @@ class Test_IFDRational(PillowTestCase): self._test_equal(1, 2, IFDRational(1, 2)) def test_nonetype(self): - " Fails if the _delegate function doesn't return a valid function" + # Fails if the _delegate function doesn't return a valid function xres = IFDRational(72) yres = IFDRational(72) @@ -58,6 +58,3 @@ class Test_IFDRational(PillowTestCase): reloaded = Image.open(out) self.assertEqual(float(IFDRational(301, 1)), float(reloaded.tag_v2[282])) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index b52ea10f6..e40e7fb86 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -1,6 +1,4 @@ -from helper import unittest, PillowTestCase, hopper - -from PIL import Image +from .helper import PillowTestCase, hopper class TestUploader(PillowTestCase): @@ -13,6 +11,3 @@ class TestUploader(PillowTestCase): result = hopper('P').convert('RGB') target = hopper('RGB') self.assert_image_similar(result, target, 0) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_util.py b/Tests/test_util.py index 9901de357..4471b75bd 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from .helper import PillowTestCase from PIL import _util @@ -30,7 +30,7 @@ class TestUtil(PillowTestCase): fp = "filename.ext" # Act - it_is = _util.isStringType(fp) + it_is = _util.isPath(fp) # Assert self.assertTrue(it_is) @@ -74,6 +74,3 @@ class TestUtil(PillowTestCase): # Assert self.assertRaises(ValueError, lambda: thing.some_attr) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py new file mode 100644 index 000000000..03befd507 --- /dev/null +++ b/Tests/test_webp_leaks.py @@ -0,0 +1,22 @@ +from .helper import unittest, PillowLeakTestCase +from PIL import Image, features +from io import BytesIO + +test_file = "Tests/images/hopper.webp" + + +@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: + im_data = f.read() + + def core(): + with Image.open(BytesIO(im_data)) as im: + im.load() + + self._test_leak(core) diff --git a/Tests/threaded_save.py b/Tests/threaded_save.py index ba8b17dbc..d8faffa0c 100644 --- a/Tests/threaded_save.py +++ b/Tests/threaded_save.py @@ -34,6 +34,7 @@ class Worker(threading.Thread): sys.stdout.write(".") queue.task_done() + t0 = time.time() threads = 20 diff --git a/Tests/versions.py b/Tests/versions.py index 89be1d7c8..835865b37 100644 --- a/Tests/versions.py +++ b/Tests/versions.py @@ -7,8 +7,10 @@ def version(module, version): if v: print(version, v) + version(Image, "jpeglib") version(Image, "zlib") +version(Image, "libtiff") try: from PIL import ImageFont diff --git a/build_children.sh b/build_children.sh deleted file mode 100755 index c4ed4ebfa..000000000 --- a/build_children.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -# Get last child project build number from branch named "latest" -BUILD_NUM=$(curl -s 'https://api.travis-ci.org/repos/python-pillow/pillow-wheels/branches/latest' | grep -o '^{"branch":{"id":[0-9]*,' | grep -o '[0-9]' | tr -d '\n') - -# Restart last child project build -curl -X POST https://api.travis-ci.org/builds/$BUILD_NUM/restart --header "Authorization: token "$AUTH_TOKEN diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index db2472009..000000000 --- a/codecov.yml +++ /dev/null @@ -1 +0,0 @@ -comment: off diff --git a/depends/README.rst b/depends/README.rst index 779e956f4..069d2b81f 100644 --- a/depends/README.rst +++ b/depends/README.rst @@ -1,9 +1,12 @@ Depends ======= -``install_openjpeg.sh``, ``install_webp.sh`` and ``install_imagequant.sh`` can -be used to download, build & install non-packaged dependencies; useful for -testing with Travis CI. +``install_openjpeg.sh``, ``install_webp.sh``, ``install_imagequant.sh``, +``install_raqm.sh`` and ``install_raqm_cmake.sh`` can be used to download, +build & install non-packaged dependencies; useful for testing with Travis CI. + +``install_extra_test_images.sh`` can be used to install additional test images +that are used for Travis CI and AppVeyor. The other scripts can be used to install all of the dependencies for the listed operating systems/distros. The ``ubuntu_14.04.sh`` and diff --git a/depends/diffcover-install.sh b/depends/diffcover-install.sh index 850d368f8..a0b462b56 100755 --- a/depends/diffcover-install.sh +++ b/depends/diffcover-install.sh @@ -1,7 +1,8 @@ +#!/usr/bin/env bash # Fetch the remote master branch before running diff-cover on Travis CI. # https://github.com/Bachmann1234/diff-cover#troubleshooting git fetch origin master:refs/remotes/origin/master # CFLAGS=-O0 means build with no optimisation. # Makes build much quicker for lxml and other dependencies. -time CFLAGS=-O0 pip install --use-wheel diff_cover +time CFLAGS=-O0 pip install diff_cover diff --git a/depends/diffcover-run.sh b/depends/diffcover-run.sh index 02efab6ae..b007494e9 100755 --- a/depends/diffcover-run.sh +++ b/depends/diffcover-run.sh @@ -1,4 +1,5 @@ +#!/usr/bin/env bash coverage xml diff-cover coverage.xml diff-quality --violation=pyflakes -diff-quality --violation=pep8 +diff-quality --violation=pycodestyle diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index 7cc905e85..d9608e782 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Usage: ./download-and-extract.sh something.tar.gz https://example.com/something.tar.gz +# Usage: ./download-and-extract.sh something https://example.com/something.tar.gz archive=$1 url=$2 diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 667c74e6d..0a98fc9d9 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,9 +1,19 @@ #!/bin/bash # install extra test images -rm -r test_images +rm -rf test_images -# Use SVN to just fetch a single git subdirectory -svn checkout https://github.com/python-pillow/pillow-depends/trunk/test_images +# Use SVN to just fetch a single Git subdirectory +svn_checkout() +{ + if [ ! -z $1 ]; then + echo "" + echo "Retrying svn checkout..." + echo "" + fi + + svn checkout https://github.com/python-pillow/pillow-depends/trunk/test_images +} +svn_checkout || svn_checkout retry || svn_checkout retry || svn_checkout retry cp -r test_images/* ../Tests/images diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 1ab3a6981..e284bb0e4 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-2.11.4 +archive=libimagequant-2.12.2 ./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 7b10df7d4..655e841d9 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,7 +2,7 @@ # install raqm -archive=raqm-0.3.0 +archive=raqm-0.5.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 37a772436..ead5637ee 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-0.6.1 +archive=libwebp-1.0.2 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz diff --git a/docs/COPYING b/docs/COPYING index 754527885..a1e258129 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2018 by Alex Clark and contributors + Copyright © 2010-2019 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/PIL.rst b/docs/PIL.rst index 67edb9901..fe69fed62 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -62,8 +62,6 @@ can be found here. :undoc-members: :show-inheritance: -.. intentionally skipped documenting this because it's deprecated - :mod:`ImageShow` Module ----------------------- diff --git a/docs/_templates/sidebarhelp.html b/docs/_templates/sidebarhelp.html index da3882e8d..8a33f18dc 100644 --- a/docs/_templates/sidebarhelp.html +++ b/docs/_templates/sidebarhelp.html @@ -1,4 +1,4 @@

Need help?

- You can get help via IRC at irc://irc.freenode.net#pil or Stack Overflow here and here. Please report issues on GitHub. + 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 dd6ca9a98..323593a36 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -13,7 +13,7 @@ The fork author's goal is to foster and support active development of PIL throug .. _Travis CI: https://travis-ci.org/python-pillow/Pillow .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow .. _GitHub: https://github.com/python-pillow/Pillow -.. _Python Package Index: https://pypi.python.org/pypi/Pillow +.. _Python Package Index: https://pypi.org/project/Pillow/ License ------- @@ -35,7 +35,7 @@ What about PIL? Prior to Pillow 2.0.0, very few image code changes were made. Pillow 2.0.0 added Python 3 support and includes many bug fixes from many contributors. -As more time passes since the last PIL release, the likelihood of a new PIL release decreases. However, we've yet to hear an official "PIL is dead" announcement. So if you still want to support PIL, please `report issues here first`_, then `open corresponding Pillow tickets here`_. +As more time passes since the last PIL release (1.1.7 in 2009), the likelihood of a new PIL release decreases. However, we've yet to hear an official "PIL is dead" announcement. So if you still want to support PIL, please `report issues here first`_, then `open corresponding Pillow tickets here`_. .. _report issues here first: https://bitbucket.org/effbot/pil-2009-raclette/issues diff --git a/docs/conf.py b/docs/conf.py index 4053e24e6..2c25588a0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,12 +15,16 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) + +import sphinx_rtd_theme + +import PIL # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -37,14 +41,14 @@ templates_path = ['_templates'] source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Pillow (PIL Fork)' -copyright = u'1995-2011 Fredrik Lundh, 2010-2018 Alex Clark and Contributors' +copyright = u'1995-2011 Fredrik Lundh, 2010-2019 Alex Clark and Contributors' author = u'Fredrik Lundh, Alex Clark and Contributors' # The version info for the project you're documenting, acts as replacement for @@ -52,10 +56,9 @@ author = u'Fredrik Lundh, Alex Clark and Contributors' # built documents. # # The short X.Y version. -import PIL -version = PIL.PILLOW_VERSION +version = PIL.__version__ # The full version, including alpha/beta/rc tags. -release = PIL.PILLOW_VERSION +release = PIL.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -66,9 +69,9 @@ language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -76,27 +79,27 @@ exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -107,98 +110,97 @@ todo_include_todos = False # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -import sphinx_rtd_theme html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # 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'] +html_static_path = ['_static', 'resources'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'PillowPILForkdoc' @@ -206,17 +208,17 @@ htmlhelp_basename = 'PillowPILForkdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples @@ -229,23 +231,23 @@ latex_documents = [ # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -258,7 +260,7 @@ man_pages = [ ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -268,18 +270,23 @@ man_pages = [ # 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.', + author, 'PillowPILFork', + 'Pillow is the friendly PIL fork by Alex Clark and Contributors.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False + + +def setup(app): + app.add_javascript('js/script.js') diff --git a/docs/deprecations.rst b/docs/deprecations.rst new file mode 100644 index 000000000..b8131ac05 --- /dev/null +++ b/docs/deprecations.rst @@ -0,0 +1,94 @@ +.. _deprecations: + +Deprecations and removals +========================= + +This page lists Pillow features that are deprecated, or have been removed in +past major releases, and gives the alternatives to use instead. + +Deprecated features +------------------- + +Below are features which are considered deprecated. Where appropriate, +a ``DeprecationWarning`` is issued. + +PIL.*ImagePlugin.__version__ attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.0.0 + +The version constants of individual plugins have been deprecated and will be removed in +a future version. Use ``PIL.__version__`` instead. + +=============================== ================================= ================================== +Deprecated Deprecated Deprecated +=============================== ================================= ================================== +``BmpImagePlugin.__version__`` ``Jpeg2KImagePlugin.__version__`` ``PngImagePlugin.__version__`` +``CurImagePlugin.__version__`` ``JpegImagePlugin.__version__`` ``PpmImagePlugin.__version__`` +``DcxImagePlugin.__version__`` ``McIdasImagePlugin.__version__`` ``PsdImagePlugin.__version__`` +``EpsImagePlugin.__version__`` ``MicImagePlugin.__version__`` ``SgiImagePlugin.__version__`` +``FliImagePlugin.__version__`` ``MpegImagePlugin.__version__`` ``SunImagePlugin.__version__`` +``FpxImagePlugin.__version__`` ``MpoImagePlugin.__version__`` ``TgaImagePlugin.__version__`` +``GdImageFile.__version__`` ``MspImagePlugin.__version__`` ``TiffImagePlugin.__version__`` +``GifImagePlugin.__version__`` ``PalmImagePlugin.__version__`` ``WmfImagePlugin.__version__`` +``IcoImagePlugin.__version__`` ``PcdImagePlugin.__version__`` ``XbmImagePlugin.__version__`` +``ImImagePlugin.__version__`` ``PcxImagePlugin.__version__`` ``XpmImagePlugin.__version__`` +``ImtImagePlugin.__version__`` ``PdfImagePlugin.__version__`` ``XVThumbImagePlugin.__version__`` +``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` +=============================== ================================= ================================== + +Setting the size of TIFF images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.3.0 + +Setting the image size of a TIFF image (eg. ``im.size = (256, 256)``) issues +a ``DeprecationWarning``: + +.. code-block:: none + + Setting the size of a TIFF image directly is deprecated, and will + be removed in a future version. Use the resize method instead. + +PILLOW_VERSION 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. + +Undocumented ImageOps functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 6.0.0.* + +Several undocumented functions in ``ImageOps`` have been removed. Use the equivalents +in ``ImageFilter`` instead: + +========================== ============================ +Removed Use instead +========================== ============================ +``ImageOps.box_blur`` ``ImageFilter.BoxBlur`` +``ImageOps.gaussian_blur`` ``ImageFilter.GaussianBlur`` +``ImageOps.gblur`` ``ImageFilter.GaussianBlur`` +``ImageOps.usm`` ``ImageFilter.UnsharpMask`` +``ImageOps.unsharp_mask`` ``ImageFilter.UnsharpMask`` +========================== ============================ + +PIL.OleFileIO +~~~~~~~~~~~~~ + +*Removed in version 6.0.0.* + +PIL.OleFileIO was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of +the upstream olefile Python package, and replaced with an ``ImportError`` in 5.0.0 +(2018-01). The deprecated file has now been removed from Pillow. If needed, install from +PyPI (eg. ``pip install olefile``). diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 29e13b920..631dc2d61 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -3,7 +3,7 @@ A Pillow loader for .dds files (S3TC-compressed aka DXTC) Jerome Leclanche Documentation: - http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt The contents of this file are hereby released in the public domain (CC0) Full text of the CC0 license: @@ -221,11 +221,11 @@ class DdsImageFile(ImageFile.ImageFile): header = BytesIO(header_bytes) flags, height, width = struct.unpack("<3I", header.read(12)) - self.size = (width, height) + self._size = (width, height) self.mode = "RGBA" pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) - reserved = struct.unpack("<11I", header.read(44)) + struct.unpack("<11I", header.read(44)) # reserved # pixel format pfsize, pfflags = struct.unpack("<2I", header.read(8)) @@ -235,10 +235,8 @@ class DdsImageFile(ImageFile.ImageFile): if fourcc == b"DXT1": self.decoder = "DXT1" - codec = _dxt1 elif fourcc == b"DXT5": self.decoder = "DXT5" - codec = _dxt5 else: raise NotImplementedError("Unimplemented pixel format %r" % fourcc) @@ -271,6 +269,7 @@ class DXT5Decoder(ImageFile.PyDecoder): raise IOError("Truncated DDS file") return 0, 0 + Image.register_decoder('DXT1', DXT1Decoder) Image.register_decoder('DXT5', DXT5Decoder) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index fd410afe0..b2611c11b 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -58,6 +58,8 @@ You can read the image size through the :py:attr:`~PIL.Image.Image.size` attribute. This is a 2-tuple, containing the horizontal and vertical size in pixels. +.. _coordinate-system: + Coordinate System ----------------- diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1ee6540ea..705438d4a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -84,7 +84,14 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following of the GIF, in milliseconds. **loop** - May not be present. The number of times the GIF should loop. + May not be present. The number of times the GIF should loop. 0 means that + it will loop forever. + +**comment** + May not be present. A comment about the image. + +**extension** + May not be present. Contains application specific information. Reading sequences ~~~~~~~~~~~~~~~~~ @@ -110,27 +117,17 @@ are available:: **append_images** A list of images to append as additional frames. Each of the images in the list can be single or multiframe images. - This is currently only supported for GIF, PDF, TIFF, and WebP. + This is currently supported for GIF, PDF, TIFF, and WebP. -**duration** - The display duration of each frame of the multiframe gif, in - milliseconds. Pass a single integer for a constant duration, or a - list or tuple to set the duration for each frame separately. + It is also supported for ICNS. If images are passed in of relevant sizes, + they will be used instead of scaling down the main image. -**loop** - Integer number of times the GIF should loop. +**include_color_table** + Whether or not to include local color table. -**optimize** - If present and true, attempt to compress the palette by - eliminating unused colors. This is only useful if the palette can - be compressed to the next smaller power of 2 elements. - -**palette** - Use the specified palette for the saved image. The palette should - be a bytes or bytearray object containing the palette entries in - RGBRGB... form. It should be no more than 768 bytes. Alternately, - the palette can be passed in as an - :py:class:`PIL.ImagePalette.ImagePalette` object. +**interlace** + Whether or not the image is interlaced. By default, it is, unless the image + is less than 16 pixels in width or height. **disposal** Indicates the way in which the graphic is to be treated after being displayed. @@ -143,6 +140,38 @@ are available:: Pass a single integer for a constant disposal, or a list or tuple to set the disposal for each frame separately. +**palette** + Use the specified palette for the saved image. The palette should + be a bytes or bytearray object containing the palette entries in + RGBRGB... form. It should be no more than 768 bytes. Alternately, + the palette can be passed in as an + :py:class:`PIL.ImagePalette.ImagePalette` object. + +**optimize** + If present and true, attempt to compress the palette by + eliminating unused colors. This is only useful if the palette can + be compressed to the next smaller power of 2 elements. + +Note that if the image you are saving comes from an existing GIF, it may have +the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary. +For these options, if you do not pass them in, they will default to +their :py:attr:`~PIL.Image.Image.info` values. + +**transparency** + Transparency color index. + +**duration** + The display duration of each frame of the multiframe gif, in + milliseconds. Pass a single integer for a constant duration, or a + list or tuple to set the duration for each frame separately. + +**loop** + Integer number of times the GIF should loop. 0 means that it will loop + forever. By default, the image will not loop. + +**comment** + A comment about the image. + Reading local images ~~~~~~~~~~~~~~~~~~~~ @@ -179,6 +208,15 @@ sets the following :py:attr:`~PIL.Image.Image.info` property: ask for ``(512, 512, 2)``, the final value of :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**append_images** + A list of images to replace the scaled down versions of the image. + The order of the images does not matter, as their use is determined by + the size of each image. + + .. versionadded:: 5.1.0 + ICO ^^^ @@ -444,7 +482,7 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following This key is omitted if the image is not a transparent palette image. -``Open`` also sets ``Image.text`` to a list of the values of the +``Open`` also sets ``Image.text`` to a dictionary of the values of the ``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. Individual compressed chunks are limited to a decompressed size of ``PngImagePlugin.MAX_TEXT_CHUNK``, by default 1MB, to prevent @@ -491,8 +529,8 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. note:: To enable PNG support, you need to build and install the ZLIB compression - library before building the Python Imaging Library. See the installation - documentation for details. + library before building the Python Imaging Library. See the `installation + documentation <../installation.html>`_ for details. PPM ^^^ @@ -546,6 +584,13 @@ For more information about the SPIDER image processing package, see the .. _SPIDER homepage: https://spider.wadsworth.org/spider_doc/spider/docs/spider.html .. _Wadsworth Center: https://www.wadsworth.org/ +TGA +^^^ + +PIL reads and writes TGA images containing ``L``, ``LA``, ``P``, +``RGB``, and ``RGBA`` data. PIL can read and write both uncompressed and +run-length encoded TGAs. + TIFF ^^^^ @@ -612,6 +657,14 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 3.4.0 +**append_images** + A list of images to append as additional frames. Each of the + images in the list can be single or multiframe images. Note however, that for + correct results, all the appended images should have the same + ``encoderinfo`` and ``encoderconfig`` properties. + + .. versionadded:: 4.2.0 + **tiffinfo** A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object or dict object containing tiff tags and values. The TIFF field type is @@ -698,7 +751,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **method** Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 0. -**icc_procfile** +**icc_profile** The ICC Profile to include in the saved file. Only supported if the system WebP library was built with webpmux support. @@ -759,6 +812,13 @@ PIL reads and writes X bitmap files (mode ``1``). Read-only formats ----------------- +BLP +^^^ + +BLP is the Blizzard Mipmap Format, a texture format used in World of +Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1`` +images, and all types of ``BLP2`` images. + CUR ^^^ @@ -836,9 +896,8 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following GD ^^ -PIL reads uncompressed GD files. Note that this file format cannot be -automatically identified, so you must use :py:func:`PIL.GdImageFile.open` to -read such a file. +PIL reads uncompressed GD2 files. Note that you must use +:py:func:`PIL.GdImageFile.open` to read such a file. The :py:meth:`~PIL.Image.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: @@ -900,11 +959,6 @@ PSD PIL identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. -TGA -^^^ - -PIL reads 24- and 32-bit uncompressed and run-length encoded TGA files. - WAL ^^^ @@ -944,14 +998,81 @@ The format code is ``Palm``, the extension is ``.palm``. PDF ^^^ -PIL can write PDF (Acrobat) images. Such images are written as binary PDF 1.1 +PIL can write PDF (Acrobat) images. Such images are written as binary PDF 1.4 files, using either JPEG or HEX encoding depending on the image mode (and whether JPEG support is available or not). -When calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used, -by default, only the first image will be saved. To save all frames, each frame -to a separate page of the PDF, the ``save_all`` parameter must be present and -set to ``True``. +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**save_all** + If a multiframe image is used, by default, only the first image will be saved. + To save all frames, each frame to a separate page of the PDF, the ``save_all`` + parameter must be present and set to ``True``. + + .. versionadded:: 3.0.0 + +**append_images** + A list of images to append as additional pages. Each of the + images in the list can be single or multiframe images. + + .. versionadded:: 4.2.0 + +**append** + Set to True to append pages to an existing PDF file. If the file doesn't + exist, an :py:exc:`IOError` will be raised. + + .. versionadded:: 5.1.0 + +**resolution** + Image resolution in DPI. This, together with the number of pixels in the + image, will determine the physical dimensions of the page that will be + saved in the PDF. + +**title** + The document’s title. If not appending to an existing PDF file, this will + default to the filename. + + .. versionadded:: 5.1.0 + +**author** + The name of the person who created the document. + + .. versionadded:: 5.1.0 + +**subject** + The subject of the document. + + .. versionadded:: 5.1.0 + +**keywords** + Keywords associated with the document. + + .. versionadded:: 5.1.0 + +**creator** + If the document was converted to PDF from another format, the name of the + conforming product that created the original document from which it was + converted. + + .. versionadded:: 5.1.0 + +**producer** + If the document was converted to PDF from another format, the name of the + conforming product that converted it to PDF. + + .. versionadded:: 5.1.0 + +**creationDate** + The creation date of the document. If not appending to an existing PDF + file, this will default to the current time. + + .. versionadded:: 5.3.0 + +**modDate** + The modification date of the document. If not appending to an existing PDF + file, this will default to the current time. + + .. versionadded:: 5.3.0 XV Thumbnails ^^^^^^^^^^^^^ diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index e822f5a08..13a7deba6 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -41,10 +41,10 @@ example, let’s display the image we just loaded:: .. note:: The standard version of :py:meth:`~PIL.Image.Image.show` is not very - efficient, since it saves the image to a temporary file and calls the - :command:`xv` utility to display the image. If you don’t have :command:`xv` - installed, it won’t even work. When it does work though, it is very handy - for debugging and tests. + efficient, since it saves the image to a temporary file and calls a utility + to display the image. If you don’t have an appropriate utility installed, + it won’t even work. When it does work though, it is very handy for + debugging and tests. The following sections provide an overview of the different functions provided in this library. @@ -175,8 +175,7 @@ Rolling an image :: def roll(image, delta): - "Roll an image sideways" - + """Roll an image sideways.""" xsize, ysize = image.size delta = delta % xsize @@ -184,20 +183,11 @@ Rolling an image part1 = image.crop((0, 0, delta, ysize)) part2 = image.crop((delta, 0, xsize, ysize)) - part1.load() - part2.load() - image.paste(part2, (0, 0, xsize-delta, ysize)) image.paste(part1, (xsize-delta, 0, xsize, ysize)) + image.paste(part2, (0, 0, xsize-delta, ysize)) return image -Note that when pasting it back from the :py:meth:`~PIL.Image.Image.crop` -operation, :py:meth:`~PIL.Image.Image.load` is called first. This is because -cropping is a lazy operation. If :py:meth:`~PIL.Image.Image.load` was not -called, then the crop operation would not be performed until the images were -used in the paste commands. This would mean that ``part1`` would be cropped from -the version of ``image`` already modified by the first paste. - For more advanced tricks, the paste method can also take a transparency mask as an optional argument. In this mask, the value 255 indicates that the pasted image is opaque in that position (that is, the pasted image should be used as diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst index aa2463bd1..2e68656ca 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-file-decoder.rst @@ -69,7 +69,7 @@ true color. header = string.split(header) # size in pixels (width, height) - self.size = int(header[1]), int(header[2]) + self._size = int(header[1]), int(header[2]) # mode setting bits = int(header[3]) @@ -171,7 +171,6 @@ The fields are used as follows: stride defaults to 0. **orientation** - Whether the first line in the image is the top line on the screen (1), or the bottom line (-1). If omitted, the orientation defaults to 1. @@ -204,7 +203,7 @@ table describes some commonly used **raw modes**: +-----------+-----------------------------------------------------------------+ | ``RGBX`` | 24-bit true colour, stored as (red, green, blue, pad). | +-----------+-----------------------------------------------------------------+ -| ``RGB;L`` | 24-bit true colour, line interleaved (first all red pixels, the | +| ``RGB;L`` | 24-bit true colour, line interleaved (first all red pixels, then| | | all green pixels, finally all blue pixels). | +-----------+-----------------------------------------------------------------+ diff --git a/docs/index.rst b/docs/index.rst index 8ee910bbd..b300dc16d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,11 +10,11 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors = 2.0.0 < 4.0.0 supports Python versions 2.6, 2.7, 3.2, 3.3, 3.4, 3.5 - -.. note:: Pillow >= 4.0.0 < 5.0.0 supports Python versions 2.7, 3.3, 3.4, 3.5, 3.6 - -.. note:: Pillow >= 5.0.0 supports Python versions 2.7, 3.4, 3.5, 3.6 ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|**Python** |**2.4**|**2.5**|**2.6**|**2.7**|**3.2**|**3.3**|**3.4**|**3.5**|**3.6**|**3.7**| ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow < 2.0.0 | Yes | Yes | Yes | Yes | | | | | | | ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 2.x - 3.x | | | Yes | Yes | Yes | Yes | Yes | Yes | | | ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 4.x | | | | Yes | | Yes | Yes | Yes | Yes | | ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 5.0.x - 5.1.x| | | | Yes | | | Yes | Yes | Yes | | ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 5.2.x - 5.4.x| | | | Yes | | | Yes | Yes | Yes | Yes | ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow >= 6.0.0 | | | | Yes | | | | Yes | Yes | Yes | ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ Basic Installation ------------------ @@ -96,7 +106,7 @@ Building From Source Download and extract the `compressed archive from PyPI`_. -.. _compressed archive from PyPI: https://pypi.python.org/pypi/Pillow +.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ .. _external-libraries: @@ -120,8 +130,8 @@ Many of Pillow's features require external libraries: * **libjpeg** provides JPEG functionality. - * Pillow has been tested with libjpeg versions **6b**, **8**, **9**, **9a**, - and **9b** and libjpeg-turbo version **8**. + * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9c** and + libjpeg-turbo version **8**. * Starting with Pillow 3.0.0, libjpeg is required by default, but may be disabled with the ``--disable-jpeg`` flag. @@ -157,12 +167,12 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-2.11** + * Pillow has been tested with libimagequant **2.6-2.12.2** * 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, - so it is unlikely to work with any Python prior to 3.5 on Windows. + so it is unlikely to work with Python 2.7 on Windows. * **libraqm** provides complex text layout support. @@ -170,12 +180,12 @@ Many of Pillow's features require external libraries: shaping (using HarfBuzz), and proper script itemization. As a result, Raqm can support most writing systems covered by Unicode. * libraqm depends on the following libraries: FreeType, HarfBuzz, - FriBiDi, make sure that you install them before install libraqm + FriBiDi, make sure that you install them before installing libraqm if not available as package in your system. * setting text direction or font features is not supported without libraqm. * libraqm is dynamically loaded in Pillow 5.0.0 and above, so support - is available if all the libraries are installed. + is available if all the libraries are installed. * Windows support: Raqm support is currently unsupported on Windows. Once you have installed the prerequisites, run:: @@ -207,17 +217,15 @@ Build Options parallel building. * Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``, - ``--disable-tk``, ``--disable-lcms``, ``--disable-webp``, - ``--disable-webpmux``, ``--disable-jpeg2000``, + ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, + ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``, ``--disable-imagequant``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Build flags: ``--enable-zlib``, ``--enable-jpeg``, - ``--enable-tiff``, ``--enable-freetype``, ``--enable-tcl``, - ``--enable-tk``, ``--enable-lcms``, ``--enable-webp``, - ``--enable-webpmux``, ``--enable-jpeg2000``, + ``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``, + ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``, ``--enable-imagequant``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) @@ -384,22 +392,22 @@ These platforms are built and tested for every change. +----------------------------------+-------------------------------+-----------------------+ | Debian Stretch | 2.7 |x86 | +----------------------------------+-------------------------------+-----------------------+ -| Fedora 25 | 2.7 |x86-64 | +| Fedora 28 | 2.7 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| Fedora 26 | 2.7 |x86-64 | +| Fedora 29 | 2.7 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| Mac OS X 10.10 Yosemite* | 2.7, 3.4, 3.5, 3.6 |x86-64 | +| Mac OS X 10.10 Yosemite* | 2.7, 3.5, 3.6, 3.7 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| Ubuntu Linux 16.04 LTS | 2.7 |x86-64 | +| Ubuntu Linux 16.04 LTS | 2.7, 3.5, 3.6, 3.7, |x86-64 | +| | PyPy, PyPy3 | | +----------------------------------+-------------------------------+-----------------------+ -| Ubuntu Linux 14.04 LTS | 2.7, 3.4, 3.5, 3.6, |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.4 |x86, x86-64 | -| | | | -| | pypy, 3.5/mingw |x86 | +| Windows Server 2012 R2 | 2.7, 3.5, 3.6, 3.7 |x86, x86-64 | +| +-------------------------------+-----------------------+ +| | PyPy, 3.7/MinGW |x86 | +----------------------------------+-------------------------------+-----------------------+ \* Mac OS X CI is not run for every commit, but is run for every release. @@ -417,11 +425,15 @@ 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.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ | macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Mac OS X 10.11 El Capitan | 2.7, 3.3, 3.4, 3.5 | 4.1.0 |x86-64 | +| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | +| +------------------------------+--------------------------------+ + +| | 3.3 | 4.1.0 | | +----------------------------------+------------------------------+--------------------------------+-----------------------+ | Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ @@ -435,16 +447,18 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+------------------------------+--------------------------------+-----------------------+ | Ubuntu Linux 12.04 LTS | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | | | PyPy5.3.1, PyPy3 v2.4.0 | | | -| | | | | +| +------------------------------+--------------------------------+-----------------------+ | | 2.7 | 4.3.0 |x86-64 | -| | | | | +| +------------------------------+--------------------------------+-----------------------+ | | 2.7, 3.2 | 3.4.1 |ppc | +----------------------------------+------------------------------+--------------------------------+-----------------------+ | Ubuntu Linux 10.04 LTS | 2.6 | 2.3.0 |x86,x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ | Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Raspian Jessie | 2.7, 3.4 | 3.1.0 |arm | +| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | +----------------------------------+------------------------------+--------------------------------+-----------------------+ | Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ @@ -466,8 +480,6 @@ These platforms have been reported to work at the versions mentioned. Old Versions ------------ -You can download old distributions from `PyPI -`_. Only the latest major -releases for Python 2.x and 3.x are visible, but all releases are -available by direct URL access -e.g. https://pypi.python.org/pypi/Pillow/1.0. +You can download old distributions from the `release history at PyPI +`_ and by direct URL access +eg. https://pypi.org/project/Pillow/1.0/. diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 915f61c04..388116a10 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -12,25 +12,25 @@ images. Examples -------- +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 Windows). -Open, rotate, and display an image (using the default viewer) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - .. code-block:: python from PIL import Image im = Image.open("bride.jpg") im.rotate(45).show() -The following script creates nice thumbnails of all JPEG images in the -current directory preserving aspect ratios with 128x128 max resolution. - Create thumbnails ^^^^^^^^^^^^^^^^^ +The following script creates nice thumbnails of all JPEG images in the +current directory preserving aspect ratios with 128x128 max resolution. + .. code-block:: python from PIL import Image @@ -59,7 +59,7 @@ Functions documentation`_ to have warnings output to the logging facility instead of stderr. .. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb - .. _the logging documentation: https://docs.python.org/2/library/logging.html?highlight=logging#integration-with-the-warnings-module + .. _the logging documentation: https://docs.python.org/3/library/logging.html#integration-with-the-warnings-module Image processing ^^^^^^^^^^^^^^^^ @@ -168,10 +168,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 + The filename or path of the source file. Only images created with the + factory function `open` have a filename attribute. If the input is a file like object, the filename attribute is set to an empty string. - + :type: :py:class: `string` .. py:attribute:: format diff --git a/docs/reference/ImageChops.rst b/docs/reference/ImageChops.rst index 2e4e21f19..6c8f11253 100644 --- a/docs/reference/ImageChops.rst +++ b/docs/reference/ImageChops.rst @@ -34,6 +34,7 @@ operations in this module). .. autofunction:: PIL.ImageChops.lighter .. autofunction:: PIL.ImageChops.logical_and .. autofunction:: PIL.ImageChops.logical_or +.. autofunction:: PIL.ImageChops.logical_xor .. autofunction:: PIL.ImageChops.multiply .. py:method:: PIL.ImageChops.offset(image, xoffset, yoffset=None) diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst index f4bd9bd0e..187306f1b 100644 --- a/docs/reference/ImageColor.rst +++ b/docs/reference/ImageColor.rst @@ -6,7 +6,7 @@ The :py:mod:`ImageColor` module contains color tables and converters from CSS3-style color specifiers to RGB tuples. This module is used by -:py:meth:`PIL.Image.Image.new` and the :py:mod:`~PIL.ImageDraw` module, among +:py:meth:`PIL.Image.new` and the :py:mod:`~PIL.ImageDraw` module, among others. .. _color-names: @@ -31,6 +31,13 @@ The ImageColor module supports the following string formats: (black=0%, normal=50%, white=100%). For example, ``hsl(0,100%,50%)`` is pure red. +* Hue-Saturation-Value (HSV) functions, given as ``hsv(hue, saturation%, + value%)`` where hue and saturation are the same as HSL, and value is between + 0% and 100% (black=0%, normal=100%). For example, ``hsv(0,100%,100%)`` is + pure red. This format is also known as Hue-Saturation-Brightness (HSB), and + can be given as ``hsb(hue, saturation%, brightness%)``, where each of the + values are used as they are in HSV. + * Common HTML color names. The :py:mod:`~PIL.ImageColor` module provides some 140 standard color names, based on the colors supported by the X Window system and most web browsers. color names are case insensitive. For example, diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 7e4987971..7c24bae93 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -25,7 +25,6 @@ Example: Draw a gray cross over an image draw = ImageDraw.Draw(im) draw.line((0, 0) + im.size, fill=128) draw.line((0, im.size[1], im.size[0], 0), fill=128) - del draw # write to stdout im.save(sys.stdout, "PNG") @@ -38,13 +37,14 @@ Coordinates ^^^^^^^^^^^ The graphics interface uses the same coordinate system as PIL itself, with (0, -0) in the upper left corner. +0) in the upper left corner. Any pixels drawn outside of the image bounds will +be discarded. Colors ^^^^^^ To specify colors, you can use numbers or tuples just as you would use with -:py:meth:`PIL.Image.Image.new` or :py:meth:`PIL.Image.Image.putpixel`. For “1”, +:py:meth:`PIL.Image.new` or :py:meth:`PIL.Image.Image.putpixel`. For “1”, “L”, and “I” images, use integers. For “RGB” images, use a 3-tuple containing integer values. For “F” images, use integer or floating point values. @@ -127,17 +127,21 @@ Methods :returns: An image font. -.. py:method:: PIL.ImageDraw.ImageDraw.arc(xy, start, end, fill=None) +.. py:method:: PIL.ImageDraw.ImageDraw.arc(xy, start, end, fill=None, width=0) Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. :param xy: Two points to define the bounding box. Sequence of - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. + ``[(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. + + .. versionadded:: 5.3.0 .. py:method:: PIL.ImageDraw.ImageDraw.bitmap(xy, bitmap, fill=None) @@ -150,51 +154,66 @@ Methods To paste pixel data into an image, use the :py:meth:`~PIL.Image.Image.paste` method on the image itself. -.. py:method:: PIL.ImageDraw.ImageDraw.chord(xy, start, end, fill=None, outline=None) +.. py:method:: PIL.ImageDraw.ImageDraw.chord(xy, start, end, fill=None, outline=None, width=0) 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]``. + ``[(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. -.. py:method:: PIL.ImageDraw.ImageDraw.ellipse(xy, fill=None, outline=None) + .. versionadded:: 5.3.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.ellipse(xy, fill=None, outline=None, width=0) 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]``. + ``[(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. -.. py:method:: PIL.ImageDraw.ImageDraw.line(xy, fill=None, width=0) + .. versionadded:: 5.3.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.line(xy, fill=None, width=0, joint=None) Draws a line between the coordinates in the **xy** list. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. :param fill: Color to use for the line. - :param width: The line width, in pixels. Note that line - joins are not handled well, so wide polylines will not look good. + :param width: The line width, in pixels. .. versionadded:: 1.1.5 .. note:: This option was broken until version 1.1.6. + :param joint: Joint type between a sequence of lines. It can be "curve", + for rounded edges, or None. -.. py:method:: PIL.ImageDraw.ImageDraw.pieslice(xy, start, end, fill=None, outline=None) + .. versionadded:: 5.3.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=0) 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]``. + ``[(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. + :param width: The line width, in pixels. + + .. versionadded:: 5.3.0 .. py:method:: PIL.ImageDraw.ImageDraw.point(xy, fill=None) @@ -217,7 +236,7 @@ Methods :param outline: Color to use for the outline. :param fill: Color to use for the fill. -.. py:method:: PIL.ImageDraw.ImageDraw.rectangle(xy, fill=None, outline=None) +.. py:method:: PIL.ImageDraw.ImageDraw.rectangle(xy, fill=None, outline=None, width=0) Draws a rectangle. @@ -226,6 +245,9 @@ Methods is just outside the drawn rectangle. :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param width: The line width, in pixels. + + .. versionadded:: 5.3.0 .. py:method:: PIL.ImageDraw.ImageDraw.shape(shape, fill=None, outline=None) @@ -247,9 +269,8 @@ Methods :param align: If the text is passed on to multiline_text(), "left", "center" or "right". :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 @@ -261,7 +282,7 @@ Methods example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 @@ -277,9 +298,8 @@ Methods :param spacing: The number of pixels between lines. :param align: "left", "center" or "right". :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 @@ -291,7 +311,7 @@ Methods example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 @@ -306,9 +326,8 @@ Methods :param spacing: If the text is passed on to multiline_textsize(), the number of pixels between lines. :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 @@ -320,7 +339,7 @@ Methods example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 @@ -333,9 +352,8 @@ Methods :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param spacing: The number of pixels between lines. :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 @@ -347,7 +365,7 @@ Methods example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index bc1868667..3368f799f 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -33,12 +33,13 @@ image enhancement filters: * **EDGE_ENHANCE_MORE** * **EMBOSS** * **FIND_EDGES** +* **SHARPEN** * **SMOOTH** * **SMOOTH_MORE** -* **SHARPEN** -.. autoclass:: PIL.ImageFilter.GaussianBlur +.. autoclass:: PIL.ImageFilter.Color3DLUT .. autoclass:: PIL.ImageFilter.BoxBlur +.. autoclass:: PIL.ImageFilter.GaussianBlur .. autoclass:: PIL.ImageFilter.UnsharpMask .. autoclass:: PIL.ImageFilter.Kernel .. autoclass:: PIL.ImageFilter.RankFilter diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 76fde44ff..55ce3d382 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -67,9 +67,8 @@ Methods .. versionadded:: 1.1.5 :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 @@ -81,7 +80,7 @@ Methods example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 7bc426eec..386401075 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 or -PySide QImage objects from PIL images. +The :py:mod:`ImageQt` module contains support for creating PyQt4, PyQt5, PySide or +PySide2 QImage objects from PIL images. .. versionadded:: 1.1.6 diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 5389dab33..8a8569922 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -28,6 +28,13 @@ Results in the following:: (23, 24, 68) (0, 0, 0) +Access using negative indexes is also possible. + +.. code-block:: python + + px[-1,-1] = (0,0,0) + print (px[-1,-1]) + :py:class:`PixelAccess` Class @@ -58,7 +65,8 @@ Results in the following:: Modifies the pixel at x,y. The color is given as a single numerical value for single band images, and a tuple for - multi-band images + multi-band images. In addition to this, RGB and RGBA tuples + are accepted for P images. :param xy: The pixel coordinate, given as (x, y). :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode) diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index 8bd8af9ff..6a492cd86 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -29,6 +29,13 @@ Results in the following:: (23, 24, 68) (0, 0, 0) +Access using negative indexes is also possible. + +.. code-block:: python + + px[-1,-1] = (0,0,0) + print (px[-1,-1]) + :py:class:`PyAccess` Class diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst index da5b3b8d8..400f236dc 100644 --- a/docs/reference/block_allocator.rst +++ b/docs/reference/block_allocator.rst @@ -8,7 +8,7 @@ Historically there have been two image allocators in Pillow: ``ImagingAllocateBlock`` and ``ImagingAllocateArray``. The first works for images smaller than 16MB of data and allocates one large chunk of memory of ``im->linesize * im->ysize`` bytes. The second works for -large images and make one allocation for each scan line of size +large images and makes one allocation for each scan line of size ``im->linesize`` bytes. This makes for a very sharp transition between one allocation and potentially thousands of small allocations, leading to unpredictable performance penalties around the transition. @@ -40,8 +40,8 @@ variables: * ``PILLOW_BLOCK_SIZE``, in bytes, K, or M. Specifies the maximum block size for ``ImagingAllocateArray``. Valid values are - integers, with an optional `k` or `m` suffix. Defaults to 16M. + integers, with an optional `k` or `m` suffix. Defaults to 16M. * ``PILLOW_BLOCKS_MAX`` Specifies the number of freed blocks to retain to fill future memory requests. Any freed blocks over this - threshold will be returned to the OS immediately. Defaults to 0. + threshold will be returned to the OS immediately. Defaults to 0. diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 4c0fbb85d..bbc9050cf 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -3,8 +3,8 @@ Internal Reference Docs .. toctree:: :maxdepth: 2 - + open_files limits block_allocator - + diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 143eb7209..511eb97bf 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -1,10 +1,12 @@ +.. _file-handling: + 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 +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. +be treated as a file-like object. The first four of these items are equivalent, the last is dangerous and may fail:: @@ -12,14 +14,14 @@ and may fail:: from PIL import Image import io import pathlib - + im = Image.open('test.jpg') im2 = Image.open(pathlib.Path('test.jpg')) f = open('test.jpg', 'rb') im3 = Image.open(f) - + with open('test.jpg', 'rb') as f: im4 = Image.open(io.BytesIO(f.read())) @@ -28,64 +30,55 @@ and may fail:: im5 = Image.open(f) im5.load() # FAILS, closed file -The documentation specifies that the file will be closed after the -``Image.Image.load()`` method is called. This is an aspirational -specification rather than an accurate reflection of the state of the -code. +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 +``Image.Image.load()`` method is called, provided the associated image does not +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. +that file needs to be prior to the close. Issues ------ -The current open file handling is inconsistent at best: - -* Most of the image plugins do not close the input file. -* Multi-frame images behave badly when seeking through the file, as - it's legal to seek backward in the file until the last image is - read, and then it's not. * 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. + the context of the file. Image Lifecycle --------------- -* ``Image.open()`` called. 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()`` Path-like 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 +* ``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. + image file. -* ``Image.Image.seek()`` in the case of multi-frame images - (e.g. multipage TIFF and animated GIF) the image file left open so - that seek can load the appropriate frame. When the last frame is - read, the image file is closed (at least in some image plugins), and - no more seeks can occur. + 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. * ``Image.Image.close()`` Closes the file pointer 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. + ... # image operations here. -The lifecycle of a single frame image is relatively simple. The file +The lifecycle of a single-frame image is relatively simple. The file must remain open until the ``load()`` or ``close()`` function is -called. +called. Multi-frame images are more complicated. The ``load()`` method is not -a terminal method, so it should not close the underlying file. The -current behavior of ``seek()`` closing the underlying file on -accessing the last frame is presumably a heuristic for closing the -file after iterating through the entire sequence. In general, Pillow -does not know if there are going to be any requests for additional -data until the caller has explicitly closed the image. +a terminal method, so it should not close the underlying file. In general, +Pillow does not know if there are going to be any requests for additional +data until the caller has explicitly closed the image. Complications @@ -94,7 +87,7 @@ 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. + 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 @@ -118,8 +111,7 @@ Proposed File Handling * ``Image.Image.load()`` should close the image file, unless there are multiple frames. -* ``Image.Image.seek()`` should never close the image file. +* ``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. - + multi-frame image to ensure that the underlying file is closed. diff --git a/docs/releasenotes/4.0.0.rst b/docs/releasenotes/4.0.0.rst index 4d21a2e54..cbf131c93 100644 --- a/docs/releasenotes/4.0.0.rst +++ b/docs/releasenotes/4.0.0.rst @@ -23,17 +23,17 @@ redirected to the olefile package. Direct accesses to ``PIL.OlefileIO`` raises a deprecation warning, then patches the upstream olefile into ``sys.modules`` in its place. -SGI image save +SGI image save ============== It is now possible to save images in modes ``L``, ``RGB``, and -``RGBA`` to the uncompressed SGI image format. +``RGBA`` to the uncompressed SGI image format. Zero sized images ================= Pillow 3.4.0 removed support for creating images with (0,0) size. This -has been reenabled, restoring pre 3.4 behavior. +has been reenabled, restoring pre 3.4 behavior. Internal handles_eof flag ========================= @@ -41,11 +41,11 @@ Internal handles_eof flag The ``handles_eof flag`` for decoding images has been removed, as there were no internal users of the flag. Anyone maintaining image decoders outside of the Pillow source tree should consider using the cleanup -function pointers instead. +function pointers instead. Image.core.stretch removed ========================== The stretch function on the core image object has been removed. This used to be for enlarging the image, but has been aliased to resize -recently. +recently. diff --git a/docs/releasenotes/4.1.0.rst b/docs/releasenotes/4.1.0.rst index a6fb9d2af..dc5d73479 100644 --- a/docs/releasenotes/4.1.0.rst +++ b/docs/releasenotes/4.1.0.rst @@ -12,7 +12,7 @@ Several deprecated items have been removed. * The methods :py:meth:`PIL.ImageDraw.ImageDraw.setink`, :py:meth:`PIL.ImageDraw.ImageDraw.setfill`, and - :py:meth:`PIL.ImageDraw.ImageDraw.setfont` have been removed. + :py:meth:`PIL.ImageDraw.ImageDraw.setfont` have been removed. Closing Files When Opening Images @@ -27,7 +27,7 @@ is specified: responsibility of the calling code to close the file. * For images where Pillow opens the file and the file is known to have - only one frame, the file is closed after loading. + only one frame, the file is closed after loading. * If the file has more than one frame, or if it can't be determined, then the file is left open to permit seeking to subsequent @@ -36,7 +36,7 @@ is specified: * If the image is memory mapped, then we can't close the mapping to the underlying file until we are done with the image. The mapping - will be closed in the ``close`` or ``__del__`` method. + will be closed in the ``close`` or ``__del__`` method. Changes to GIF Handling When Saving @@ -50,7 +50,7 @@ saving images. There are two external changes that arise from this: * The image to be saved is no longer modified in place by any of the operations of the save function. Previously it was modified when - optimizing the image palette. + optimizing the image palette. This refactor fixed some bugs with palette handling when saving multiple frame GIFs. @@ -60,7 +60,7 @@ New Method: Image.remap_palette The method :py:meth:`PIL.Image.Image.remap_palette()` has been added. This method was hoisted from the GifImagePlugin code used to -optimize the palette. +optimize the palette. Added Decoder Registry and Support for Python Based Decoders ============================================================ @@ -76,7 +76,7 @@ Tests ===== Many tests have been added, including correctness tests for image -formats that have been previously untested. +formats that have been previously untested. We are now running automated tests in Docker containers against more Linux versions than are provided on Travis CI, which is currently diff --git a/docs/releasenotes/4.2.0.rst b/docs/releasenotes/4.2.0.rst index 1b41580a7..e07fd9071 100644 --- a/docs/releasenotes/4.2.0.rst +++ b/docs/releasenotes/4.2.0.rst @@ -6,9 +6,10 @@ Added Complex Text Rendering Pillow now supports complex text rendering for scripts requiring glyph composition and bidirectional flow. This optional feature adds three -dependencies: harfbuzz, fribidi, and raqm. See the install -documentation for further details. This feature is tested and works on -Unix and Mac, but has not yet been built on Windows platforms. +dependencies: harfbuzz, fribidi, and raqm. See the `install +documentation <../installation.html>`_ for further details. This feature is +tested and works on Unix and Mac, but has not yet been built on Windows +platforms. New Optional Parameters ======================= diff --git a/docs/releasenotes/4.3.0.rst b/docs/releasenotes/4.3.0.rst index 649135922..6fa554e23 100644 --- a/docs/releasenotes/4.3.0.rst +++ b/docs/releasenotes/4.3.0.rst @@ -9,7 +9,7 @@ Deprecations Several undocumented functions in ImageOps have been deprecated: ``gaussian_blur``, ``gblur``, ``unsharp_mask``, ``usm`` and -``box_blur``. Use the equivalent operations in ImageFilter +``box_blur``. Use the equivalent operations in ``ImageFilter`` instead. These functions will be removed in a future release. TIFF Metadata Changes @@ -20,7 +20,7 @@ TIFF Metadata Changes single element tuple. This is only with the new api, not the legacy api. This normalizes the handling of fields, so that the metadata with inferred or image specified counts are handled the same as - metadata with count specified in the TIFF spec. + metadata with count specified in the TIFF spec. * The ``PhotoshopInfo``, ``XMP``, and ``JPEGTables`` tags now have a defined type (bytes) and a count of 1. * The ``ImageJMetaDataByteCounts`` tag now has an arbitrary number of @@ -85,7 +85,7 @@ There is a new :py:class:`PIL.ImageFilter.MultibandFilter` base class for image filters that can run on all channels of an image in one operation. The original :py:class:`PIL.ImageFilter.Filter` class remains for image filters that can process only single band images, or -require splitting of channels prior to filtering. +require splitting of channels prior to filtering. Other Changes ============= @@ -109,7 +109,7 @@ images to and from RGB and RGBA formats. The image data is truncated to 8-bit precision. Pillow can now read RLE encoded SGI images in both 8 and 16-bit -precision. +precision. Performance ^^^^^^^^^^^ @@ -124,7 +124,7 @@ This release contains several performance improvements: * ``Image.transpose`` has been accelerated 15% or more by using a cache friendly algorithm. * ImageFilters based on Kernel convolution are significantly faster - due to the new MultibandFilter feature. + due to the new MultibandFilter feature. * All memory allocation for images is now done in blocks, rather than falling back to an allocation for each scan line for images larger than the block size. diff --git a/docs/releasenotes/5.0.0.rst b/docs/releasenotes/5.0.0.rst index 2bbeed11f..509edbe6d 100644 --- a/docs/releasenotes/5.0.0.rst +++ b/docs/releasenotes/5.0.0.rst @@ -17,7 +17,7 @@ Decompression Bombs now raise Exceptions Pillow has previously emitted warnings for images that are unexpectedly large and may be a denial of service. These warnings are -now upgraded to ``DecompressionBombError``s for images that are twice +now upgraded to ``DecompressionBombError``\s for images that are twice the size of images that trigger the ``DecompressionBombWarning``. The default threshold is 128Mpx, or 0.5GB for an ``RGB`` or ``RGBA`` image. This can be disabled or changed by setting @@ -69,7 +69,7 @@ GIF Disposal ^^^^^^^^^^^^ Multiframe GIF images now take an optional disposal parameter to -specify the disposal option for changed pixels. +specify the disposal option for changed pixels. Other Changes ============= @@ -88,7 +88,7 @@ Libraqm is now Dynamically Linked The libraqm dependency for complex text scripts is now linked dynamically at runtime rather than at packaging time. This allows us to release binaries with support for libraqm if it is installed on the -user's machine. +user's machine. Source Layout Changes ^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/5.1.0.rst b/docs/releasenotes/5.1.0.rst new file mode 100644 index 000000000..2a4c64ac5 --- /dev/null +++ b/docs/releasenotes/5.1.0.rst @@ -0,0 +1,36 @@ +5.1.0 +----- + +New File Format +=============== + +BLP File Format +^^^^^^^^^^^^^^^ + +Pillow now supports reading the BLP "Blizzard Mipmap" file format used +for tiles in Blizzard's engine. + +API Changes +=========== + +Optional channels for TIFF files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow can now open TIFF files with base modes of ``RGB``, ``YCbCr``, +and ``CMYK`` with up to 6 8-bit channels, discarding any extra +channels if the content is tagged as UNSPECIFIED. Pillow still does +not store more than 4 8-bit channels of image data. + +Append to PDF Files +^^^^^^^^^^^^^^^^^^^ + +Images can now be appended to PDF files in place by passing in +``append=True`` when saving the image. + +Other Changes +============= + +WebP memory leak +^^^^^^^^^^^^^^^^ + +A memory leak when opening ``WebP`` files has been fixed. diff --git a/docs/releasenotes/5.2.0.rst b/docs/releasenotes/5.2.0.rst new file mode 100644 index 000000000..75e8da655 --- /dev/null +++ b/docs/releasenotes/5.2.0.rst @@ -0,0 +1,113 @@ +5.2.0 +----- + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +These version constants have been deprecated. ``VERSION`` will be removed in +Pillow 6.0.0, and ``PILLOW_VERSION`` will be removed after that. + +* ``PIL.VERSION`` (old PIL version 1.1.7) +* ``PIL.PILLOW_VERSION`` +* ``PIL.Image.VERSION`` +* ``PIL.Image.PILLOW_VERSION`` + +Use ``PIL.__version__`` instead. + +API Additions +============= + +3D color lookup tables +^^^^^^^^^^^^^^^^^^^^^^ + +Support for 3D color lookup table transformations has been added. + +* https://en.wikipedia.org/wiki/3D_lookup_table + +``Color3DLUT.generate`` transforms 3-channel pixels using the values of the +channels as coordinates in the 3D lookup table and interpolating the nearest +elements. + +It allows you to apply almost any color transformation in constant time by +using pre-calculated decimated tables. + +``Color3DLUT.transform()`` allows altering table values with a callback. + +If NumPy is installed, the performance of argument conversion is dramatically +improved when a source table supports buffer interface (NumPy && arrays in +Python >= 3). + +ImageColor.getrgb +^^^^^^^^^^^^^^^^^ + +Previously ``Image.rotate`` only supported HSL color strings. Now HSB and HSV +strings are also supported, as well as float values. For example, +``ImageColor.getrgb("hsv(180,100%,99.5%)")``. + +ImageFile.get_format_mimetype +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageFile.get_format_mimetype`` has been added to return the MIME type of an +image file, where available. For example, +``Image.open("hopper.jpg").get_format_mimetype()`` returns ``"image/jpeg"``. + +ImageFont.getsize_multiline +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method to return the size of multiline text, for example +``font.getsize_multiline("ABC\nAaaa")`` + +Image.rotate +^^^^^^^^^^^^ + +A new named parameter, ``fillcolor``, has been added to ``Image.rotate``. This +color specifies the background color to use in the area outside the rotated +image. This parameter takes the same color specifications as used in +``Image.new``. + + +TGA file format +^^^^^^^^^^^^^^^ + +Pillow can now read and write LA data (in addition to L, P, RGB and RGBA), and +write RLE data (in addition to uncompressed). + +Other Changes +============= + +Support added for Python 3.7 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 5.2 supports Python 3.7. + +Build macOS wheels with Xcode 6.4, supporting older macOS versions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The macOS wheels for Pillow 5.1.0 were built with Xcode 9.2, meaning 10.12 +Sierra was the lowest supported version. + +Prior to Pillow 5.1.0, Xcode 8 was used, supporting El Capitan 10.11. + +Instead, Pillow 5.2.0 is built with the oldest available Xcode 6.4 to support +at least 10.10 Yosemite. + +Fix _i2f compilation with some GCC versions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For example, this allows compilation with GCC 4.8 on NetBSD. + +Resolve confusion getting PIL / Pillow version string +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Re: "version constants deprecated" listed above, as user gnbl notes in #3082: + +- it's confusing that PIL.VERSION returns the version string of the former PIL instead of Pillow's +- there does not seem to be documentation on this version number (why this, will it ever change, ..) e.g. at https://pillow.readthedocs.io/en/5.1.x/about.html#why-a-fork +- it's confusing that PIL.version is a module and does not return the version information directly or hints on how to get it +- the package information header is essentially useless (placeholder, does not even mention Pillow, nor the version) +- PIL._version module documentation comment could explain how to access the version information + +We have attempted to resolve these issues in #3083, #3090 and #3218. diff --git a/docs/releasenotes/5.3.0.rst b/docs/releasenotes/5.3.0.rst new file mode 100644 index 000000000..cce671c32 --- /dev/null +++ b/docs/releasenotes/5.3.0.rst @@ -0,0 +1,67 @@ +5.3.0 +----- + +API Changes +=========== + +Image size +^^^^^^^^^^ + +If you attempt to set the size of an image directly, e.g. +``im.size = (100, 100)``, you will now receive an ``AttributeError``. This is +not about removing existing functionality, but instead about raising an +explicit error to prevent later consequences. The ``resize`` method is the +correct way to change an image's size. + +The exceptions to this are: + +* The ICO and ICNS image formats, which use ``im.size = (100, 100)`` to select a subimage. +* The TIFF image format, which now has a ``DeprecationWarning`` for this action, as direct image size setting was previously necessary to work around an issue with tile extents. + + +API Additions +============= + +Added line width parameter to rectangle and ellipse-based shapes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An optional line ``width`` parameter has been added to ``ImageDraw.Draw.arc``, +``chord``, ``ellipse``, ``pieslice`` and ``rectangle``. + +Curved joints for line sequences +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageDraw.Draw.line`` draws a line, or lines, between points. Previously, +when multiple points are given, for a larger ``width``, the joints between +these lines looked unsightly. There is now an additional optional argument, +``joint``, defaulting to ``None``. When it is set to ``curved``, the joints +between the lines will become rounded. + +ImageOps.colorize +^^^^^^^^^^^^^^^^^ + +Previously ``ImageOps.colorize`` only supported two-color mapping with +``black`` and ``white`` arguments being mapped to 0 and 255 respectively. +Now it supports three-color mapping with the optional ``mid`` parameter, and +the positions for all three color arguments can each be optionally specified +(``blackpoint``, ``whitepoint`` and ``midpoint``). +For example, with all optional arguments:: + + ImageOps.colorize(im, black=(32, 37, 79), white='white', mid=(59, 101, 175), + blackpoint=15, whitepoint=240, midpoint=100) + +ImageOps.pad +^^^^^^^^^^^^ + +While ``ImageOps.fit`` allows users to crop images to a requested aspect ratio +and size, new method ``ImageOps.pad`` pads images to fill a requested aspect +ratio and size, filling new space with a provided ``color`` and positioning the +image within the new area through a ``centering`` argument. + +Other Changes +============= + +Added support for reading tiled TIFF images through LibTIFF. Compressed TIFF +images are now read through LibTIFF. + +RGB WebP images are now read as RGB mode, rather than RGBX. diff --git a/docs/releasenotes/5.4.0.rst b/docs/releasenotes/5.4.0.rst new file mode 100644 index 000000000..6d7277c70 --- /dev/null +++ b/docs/releasenotes/5.4.0.rst @@ -0,0 +1,67 @@ +5.4.0 +----- + +API Changes +=========== + +APNG extension to PNG plugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Animated Portable Network Graphics (APNG) images are not fully supported but +can be opened via the PNG plugin to get some basic info:: + + im = Image.open("image.apng") + print(im.mode) # "RGBA" + print(im.size) # (245, 245) + im.show() # Shows a single frame + +Check for libjpeg-turbo +^^^^^^^^^^^^^^^^^^^^^^^ + +You can check if Pillow has been built against the libjpeg-turbo version of the +libjpeg library:: + + from PIL import features + features.check_feature("libjpeg_turbo") # True or False + +Negative indexes in pixel access +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When accessing individual image pixels, negative indexes are now also accepted. +For example, to get or set the farthest pixel in the lower right of an image:: + + px = im.load() + print(px[-1, -1]) + px[-1, -1] = (0, 0, 0) + + +New custom TIFF tags +^^^^^^^^^^^^^^^^^^^^ + +TIFF images can now be saved with custom integer, float and string TIFF tags:: + + im = Image.new("RGB", (200, 100)) + custom = { + 37000: 4, + 37001: 4.2, + 37002: "custom tag value", + 37003: u"custom tag value", + 37004: b"custom tag value", + } + im.save("output.tif", tiffinfo=custom) + + im2 = Image.open("output.tif") + print(im2.tag_v2[37000]) # 4 + print(im2.tag_v2[37002]) # "custom tag value" + print(im2.tag_v2[37004]) # b"custom tag value" + +Other Changes +============= + +ImageOps.fit +^^^^^^^^^^^^ + +Now uses one resize operation with ``box`` parameter internally +instead of a crop and scale operations sequence. +This improves the performance and accuracy of cropping since +the ``box`` parameter accepts float values. diff --git a/docs/releasenotes/5.4.1.rst b/docs/releasenotes/5.4.1.rst new file mode 100644 index 000000000..78f483db6 --- /dev/null +++ b/docs/releasenotes/5.4.1.rst @@ -0,0 +1,36 @@ +5.4.1 +----- + +This release fixes regressions in 5.4.0. + +Installation on Termux +^^^^^^^^^^^^^^^^^^^^^^ + +A change to the way Pillow detects libraries during installed prevented +installation on Termux, which does not have ``/sbin/ldconfig``. This is now +fixed. + +PNG: Handle IDAT chunks after image end +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some PNG images have multiple IDAT chunks. In some cases, Pillow will stop +reading image data before the IDAT chunks finish. A regression caused an +``EOFError`` exception when previously there was none. This is now fixed, and +file reading continues in case there are subsequent text chunks. + +PNG: MIME type +^^^^^^^^^^^^^^ + +The addition of limited APNG support to the PNG plugin also overwrote the MIME +type for PNG files, causing "image/apng" to be returned as the MIME type of +both APNG and PNG files. This has been fixed so the MIME type of PNG files is +"image/png". + +File closing +^^^^^^^^^^^^ + +A regression caused an unsupported image file to report a +``ValueError: seek of closed file`` exception instead of an ``OSError``. This +has been fixed by ensuring that image plugins only close their internal ``__fp`` +if they are not the same as ``ImageFile``'s ``fp``, allowing each to manage their own +file pointers. diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst new file mode 100644 index 000000000..e1fa83cce --- /dev/null +++ b/docs/releasenotes/6.0.0.rst @@ -0,0 +1,90 @@ +6.0.0 +----- + +Backwards Incompatible Changes +============================== + +Python 3.4 dropped +^^^^^^^^^^^^^^^^^^ + +Python 3.4 is EOL since 2019-03-16 and no longer supported. We will not be creating +binaries, testing, or retaining compatibility with this version. The final version of +Pillow for Python 3.4 is 5.4.1. + +Removed deprecated PIL.OleFileIO +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +PIL.OleFileIO was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of +the upstream olefile Python package, and replaced with an ``ImportError``. The +deprecated file has now been removed from Pillow. If needed, install from PyPI (eg. +``pip install olefile``). + +Removed deprecated ImageOps functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Several undocumented functions in ``ImageOps`` were deprecated in Pillow 4.3.0 (2017-10) +and have now been removed: ``gaussian_blur``, ``gblur``, ``unsharp_mask``, ``usm`` and +``box_blur``. Use the equivalent operations in ``ImageFilter`` instead. + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +These version constants have been deprecated and will be removed in a future +version. + +* ``BmpImagePlugin.__version__`` +* ``CurImagePlugin.__version__`` +* ``DcxImagePlugin.__version__`` +* ``EpsImagePlugin.__version__`` +* ``FliImagePlugin.__version__`` +* ``FpxImagePlugin.__version__`` +* ``GdImageFile.__version__`` +* ``GifImagePlugin.__version__`` +* ``IcoImagePlugin.__version__`` +* ``ImImagePlugin.__version__`` +* ``ImtImagePlugin.__version__`` +* ``IptcImagePlugin.__version__`` +* ``Jpeg2KImagePlugin.__version__`` +* ``JpegImagePlugin.__version__`` +* ``McIdasImagePlugin.__version__`` +* ``MicImagePlugin.__version__`` +* ``MpegImagePlugin.__version__`` +* ``MpoImagePlugin.__version__`` +* ``MspImagePlugin.__version__`` +* ``PalmImagePlugin.__version__`` +* ``PcdImagePlugin.__version__`` +* ``PcxImagePlugin.__version__`` +* ``PdfImagePlugin.__version__`` +* ``PixarImagePlugin.__version__`` +* ``PngImagePlugin.__version__`` +* ``PpmImagePlugin.__version__`` +* ``PsdImagePlugin.__version__`` +* ``SgiImagePlugin.__version__`` +* ``SunImagePlugin.__version__`` +* ``TgaImagePlugin.__version__`` +* ``TiffImagePlugin.__version__`` +* ``WmfImagePlugin.__version__`` +* ``XbmImagePlugin.__version__`` +* ``XpmImagePlugin.__version__`` +* ``XVThumbImagePlugin.__version__`` + +Use ``PIL.__version__`` instead. + +API Additions +============= + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 0ee853fca..9a088375b 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,6 +6,12 @@ Release Notes .. toctree:: :maxdepth: 2 + 6.0.0 + 5.4.1 + 5.4.0 + 5.3.0 + 5.2.0 + 5.1.0 5.0.0 4.3.0 4.2.1 diff --git a/docs/resources/js/script.js b/docs/resources/js/script.js new file mode 100644 index 000000000..3bc216c2d --- /dev/null +++ b/docs/resources/js/script.js @@ -0,0 +1,60 @@ +jQuery(document).ready(function ($) { + setTimeout(function () { + var sectionID = 'base'; + var search = function ($section, $sidebarItem) { + $section.children('.section, .function, .method').each(function () { + if ($(this).hasClass('section')) { + sectionID = $(this).attr('id'); + search($(this), $sidebarItem.parent().find('[href="#'+sectionID+'"]')); + } else { + var $dt = $(this).children('dt'); + var id = $dt.attr('id'); + if (id === undefined) { + return; + } + + var $functionsUL = $sidebarItem.siblings('[data-sectionID='+sectionID+']'); + if (!$functionsUL.length) { + $functionsUL = $('