diff --git a/.ci/install.sh b/.ci/install.sh index 3c75526fe03..2143c06137a 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -32,7 +32,6 @@ pip install test-image-results pip install numpy # TODO Remove when 3.9-dev includes setuptools 49.3.2+: -if [ "$TRAVIS_PYTHON_VERSION" == "3.9-dev" ]; then pip install -U "setuptools>=49.3.2" ; fi if [ "$GHA_PYTHON_VERSION" == "3.9-dev" ]; then pip install -U "setuptools>=49.3.2" ; fi if [[ $TRAVIS_PYTHON_VERSION == 3.* ]]; then diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 0372b558629..a90a0a95487 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -198,15 +198,27 @@ jobs: msys: runs-on: windows-2019 + strategy: + fail-fast: false + matrix: + mingw: ["MINGW32", "MINGW64"] + include: + - mingw: "MINGW32" + name: "MSYS2 MinGW 32-bit" + package: "mingw-w64-i686" + - mingw: "MINGW64" + name: "MSYS2 MinGW 64-bit" + package: "mingw-w64-x86_64" + defaults: run: shell: bash.exe --login -eo pipefail "{0}" env: - MSYSTEM: MINGW64 + MSYSTEM: ${{ matrix.mingw }} CHERE_INVOKING: 1 timeout-minutes: 30 - name: MSYS2 MinGW 64-bit + name: ${{ matrix.name }} steps: - uses: actions/checkout@v2 @@ -218,23 +230,22 @@ jobs: - name: Install Dependencies run: | pacman -S --noconfirm \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools \ - mingw-w64-x86_64-python3-pytest \ - mingw-w64-x86_64-python3-pytest-cov \ - mingw-w64-x86_64-python3-cffi \ - mingw-w64-x86_64-python3-olefile \ - mingw-w64-x86_64-python3-numpy \ - mingw-w64-x86_64-python3-pyqt5 \ - mingw-w64-x86_64-python3-numpy \ - mingw-w64-x86_64-freetype \ - mingw-w64-x86_64-lcms2 \ - mingw-w64-x86_64-libwebp \ - mingw-w64-x86_64-libjpeg-turbo \ - mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm \ - mingw-w64-x86_64-ghostscript \ + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python3-pyqt5 \ + ${{ matrix.package }}-python3-pytest \ + ${{ matrix.package }}-python3-pytest-cov \ + ${{ matrix.package }}-python3-setuptools \ + ${{ matrix.package }}-freetype \ + ${{ matrix.package }}-ghostscript \ + ${{ matrix.package }}-lcms2 \ + ${{ matrix.package }}-libimagequant \ + ${{ matrix.package }}-libjpeg-turbo \ + ${{ matrix.package }}-libraqm \ + ${{ matrix.package }}-libwebp \ + ${{ matrix.package }}-openjpeg2 \ subversion python3 -m pip install pyroma @@ -256,4 +267,4 @@ jobs: python3 -m pip install codecov bash <(curl -s https://codecov.io/bash) -F GHA_Windows env: - CODECOV_NAME: MSYS2 MinGW 64-bit + CODECOV_NAME: ${{ matrix.name }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77c5f90f77b..459251d7733 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 6bedb5c58a7d8c25aa9509f8217bc24e9797e90d # frozen: 19.10b0 + rev: e66be67b9b6811913470f70c28b4d50f94d05b22 # frozen: 20.8b1 hooks: - id: black args: ["--target-version", "py36"] @@ -9,7 +9,7 @@ repos: types: [] - repo: https://github.com/timothycrosley/isort - rev: 9ae09866e278fbc6ec0383ccb16b5c84e78e6e4d # frozen: 5.3.2 + rev: 377d260ffa6f746693f97b46d95025afc4bd8275 # frozen: 5.4.2 hooks: - id: isort @@ -31,7 +31,7 @@ repos: additional_dependencies: [flake8-2020, flake8-implicit-str-concat] - repo: https://github.com/pre-commit/pygrep-hooks - rev: 20b9ac745c5adaab12b845b3564c773dcc051d0e # frozen: v1.5.2 + rev: eae6397e4c259ed3d057511f6dd5330b92867e62 # frozen: v1.6.0 hooks: - id: python-check-blanket-noqa - id: rst-backticks diff --git a/CHANGES.rst b/CHANGES.rst index c9389ddb1c9..263c2a82367 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,12 +5,33 @@ Changelog (Pillow) 8.0.0 (unreleased) ------------------ +- Fix IFDRational __eq__ bug #4888 + [luphord, radarhere] + +- Fixed duplicate variable name #4885 + [liZe, radarhere] + +- Added homebrew zlib include directory #4842 + [radarhere] + +- Corrected inverted PDF CMYK colors #4866 + [radarhere] + +- Do not try to close file pointer if file pointer is empty #4823 + [radarhere] + +- ImageOps.autocontrast: add mask parameter #4843 + [navneeth, hugovk] + - Read EXIF data tEXt chunk into info as bytes instead of string #4828 [radarhere] - Remove long-deprecated Image.py functions #4798 [hugovk, nulano, radarhere] +- Replaced most uses of distutils with setuptools #4797, #4809, #4814, #4817, #4829 + [hugovk, radarhere] + - Add MIME type to PsdImagePlugin #4788 [samamorgan] diff --git a/README.md b/README.md new file mode 100644 index 00000000000..6ca6cbf83d1 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +

+ Pillow logo +

+ +# Pillow + +## Python Imaging Library (Fork) + +Pillow is the friendly PIL fork by [Alex Clark and +Contributors](https://github.com/python-pillow/Pillow/graphs/contributors). +PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +As of 2019, Pillow development is +[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). + + + + + + + + + + + + + + + + + + +
docs + Documentation Status +
tests + Travis CI build status (Linux) + Travis CI build status (macOS) + AppVeyor CI build status (Windows) + GitHub Actions build status (Lint) + GitHub Actions build status (Test Linux and macOS) + GitHub Actions build status (Test Windows) + GitHub Actions build status (Test Docker) + Code coverage +
package + Zenodo + Tidelift + Newest PyPI version + Number of PyPI downloads +
social + Join the chat at https://gitter.im/python-pillow/Pillow + Follow on https://twitter.com/PythonPillow +
+ +## More Information + +- [Documentation](https://pillow.readthedocs.io/) + - [Installation](https://pillow.readthedocs.io/en/latest/installation.html) + - [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html) +- [Contribute](https://github.com/python-pillow/Pillow/blob/master/.github/CONTRIBUTING.md) + - [Issues](https://github.com/python-pillow/Pillow/issues) + - [Pull requests](https://github.com/python-pillow/Pillow/pulls) +- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst) + - [Pre-fork](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork) + +## Report a Vulnerability + +To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security). diff --git a/README.rst b/README.rst deleted file mode 100644 index c1d5be57912..00000000000 --- a/README.rst +++ /dev/null @@ -1,103 +0,0 @@ -Pillow -====== - -Python Imaging Library (Fork) ------------------------------ - -Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. As of 2019, Pillow development is `supported by Tidelift `_. - -.. start-badges - -.. list-table:: - :stub-columns: 1 - - * - docs - - |docs| - * - tests - - |linux| |macos| |windows| |gha_lint| |gha| |gha_windows| |gha_docker| |coverage| - * - package - - |zenodo| |tidelift| |version| |downloads| - * - social - - |gitter| |twitter| - -.. end-badges - -More Information ----------------- - -- `Documentation `_ - - - `Installation `_ - - `Handbook `_ - -- `Contribute `_ - - - `Issues `_ - - `Pull requests `_ - -- `Changelog `_ - - - `Pre-fork `_ - -Report a Vulnerability ----------------------- - -To report a security vulnerability, please follow the procedure described in the `Tidelift security policy `_. - -.. |docs| image:: https://readthedocs.org/projects/pillow/badge/?version=latest - :target: https://pillow.readthedocs.io/?badge=latest - :alt: Documentation Status - -.. |linux| image:: https://img.shields.io/travis/python-pillow/Pillow/master.svg?label=Linux%20build - :target: https://travis-ci.org/python-pillow/Pillow - :alt: Travis CI build status (Linux) - -.. |macos| image:: https://img.shields.io/travis/python-pillow/pillow-wheels/master.svg?label=macOS%20build - :target: https://travis-ci.org/python-pillow/pillow-wheels - :alt: Travis CI build status (macOS) - -.. |windows| image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/master.svg?label=Windows%20build - :target: https://ci.appveyor.com/project/python-pillow/Pillow - :alt: AppVeyor CI build status (Windows) - -.. |gha_lint| image:: https://github.com/python-pillow/Pillow/workflows/Lint/badge.svg - :target: https://github.com/python-pillow/Pillow/actions?query=workflow%3ALint - :alt: GitHub Actions build status (Lint) - -.. |gha_docker| image:: https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg - :target: https://github.com/python-pillow/Pillow/actions?query=workflow%3A%22Test+Docker%22 - :alt: GitHub Actions build status (Test Docker) - -.. |gha| image:: https://github.com/python-pillow/Pillow/workflows/Test/badge.svg - :target: https://github.com/python-pillow/Pillow/actions?query=workflow%3ATest - :alt: GitHub Actions build status (Test Linux and macOS) - -.. |gha_windows| image:: https://github.com/python-pillow/Pillow/workflows/Test%20Windows/badge.svg - :target: https://github.com/python-pillow/Pillow/actions?query=workflow%3A%22Test+Windows%22 - :alt: GitHub Actions build status (Test Windows) - -.. |coverage| image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg - :target: https://codecov.io/gh/python-pillow/Pillow - :alt: Code coverage - -.. |zenodo| image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg - :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow - -.. |tidelift| image:: https://tidelift.com/badges/package/pypi/Pillow?style=flat - :target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge - -.. |version| image:: https://img.shields.io/pypi/v/pillow.svg - :target: https://pypi.org/project/Pillow/ - :alt: Latest PyPI version - -.. |downloads| image:: https://img.shields.io/pypi/dm/pillow.svg - :target: https://pypi.org/project/Pillow/ - :alt: Number of PyPI downloads - -.. |gitter| image:: https://badges.gitter.im/python-pillow/Pillow.svg - :target: https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge - :alt: Join the chat at https://gitter.im/python-pillow/Pillow - -.. |twitter| image:: https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg - :target: https://twitter.com/PythonPillow - :alt: Follow on https://twitter.com/PythonPillow diff --git a/RELEASING.md b/RELEASING.md index 3f62a70c484..c9a0439d809 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -101,11 +101,7 @@ Released as needed privately to individual vendors for critical security-related cd pillow-wheels ./update-pillow-tag.sh [[release tag]] ``` -* [ ] Download distributions from the [Pillow Wheel Builder container](http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com/). - ```bash - wget -m -A 'Pillow--*' \ - http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com - ``` +* [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases). ## Publicize Release diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index b63fa2a1e59..ab8d7771992 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -119,60 +119,59 @@ def test_qtables_leak(): def test_exif_leak(): """ -pre patch: - - MB -177.1^ # - | @@@# - | :@@@@@@# - | ::::@@@@@@# - | ::::::::@@@@@@# - | @@::::: ::::@@@@@@# - | @@@@ ::::: ::::@@@@@@# - | @@@@@@@ ::::: ::::@@@@@@# - | @@::@@@@@@@ ::::: ::::@@@@@@# - | @@@@ : @@@@@@@ ::::: ::::@@@@@@# - | @@@@@@ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @@@@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @::@@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | ::::@: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | :@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | ::@@::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @@::: @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @::@ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | :::@: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @@@:: @: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - 0 +----------------------------------------------------------------------->Gi - 0 11.37 - - -post patch: - - MB -21.06^ ::::::::::::::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | ##::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - 0 +----------------------------------------------------------------------->Gi - 0 11.33 - -""" + pre patch: + + MB + 177.1^ # + | @@@# + | :@@@@@@# + | ::::@@@@@@# + | ::::::::@@@@@@# + | @@::::: ::::@@@@@@# + | @@@@ ::::: ::::@@@@@@# + | @@@@@@@ ::::: ::::@@@@@@# + | @@::@@@@@@@ ::::: ::::@@@@@@# + | @@@@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@@@@ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @::@@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | ::::@: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | :@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | ::@@::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@::: @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @::@ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | :::@: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@:: @: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + 0 +----------------------------------------------------------------------->Gi + 0 11.37 + + + post patch: + + MB + 21.06^ ::::::::::::::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | ##::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + 0 +----------------------------------------------------------------------->Gi + 0 11.33 + """ im = hopper("RGB") exif = b"12345678" * 4096 @@ -183,31 +182,30 @@ def test_exif_leak(): def test_base_save(): """ -base case: - MB -20.99^ ::::: :::::::::::::::::::::::::::::::::::::::::::@::: - | ##: : ::::::@::::::: :::: :::: : : : : : : :::::::::::: :::@::: - | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@# : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@@ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@@@@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - 0 +----------------------------------------------------------------------->Gi - 0 7.882 -""" + base case: + MB + 20.99^ ::::: :::::::::::::::::::::::::::::::::::::::::::@::: + | ##: : ::::::@::::::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@# : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@@ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@@@@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + 0 +----------------------------------------------------------------------->Gi + 0 7.882""" im = hopper("RGB") for _ in range(iterations): diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index 5a6116f4f6a..bd7f407e4ab 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -6,9 +6,9 @@ def test_libtiff_segfault(): - """ This test should not segfault. It will on Pillow <= 3.1.0 and - libtiff >= 4.0.0 - """ + """This test should not segfault. It will on Pillow <= 3.1.0 and + libtiff >= 4.0.0 + """ with pytest.raises(OSError): with Image.open(TEST_FILE) as im: diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index ca7e877b3c7..19602c1e78c 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -16,8 +16,8 @@ def get_files(d, ext=".bmp"): def test_bad(): - """ These shouldn't crash/dos, but they shouldn't return anything - either """ + """These shouldn't crash/dos, but they shouldn't return anything + either""" for f in get_files("b"): def open(f): @@ -32,8 +32,8 @@ def open(f): def test_questionable(): - """ These shouldn't crash/dos, but it's not well defined that these - are in spec """ + """These shouldn't crash/dos, but it's not well defined that these + are in spec""" supported = [ "pal8os2v2.bmp", "rgb24prof.bmp", @@ -57,8 +57,8 @@ def test_questionable(): def test_good(): - """ These should all work. There's a set of target files in the - html directory that we can compare against. """ + """These should all work. There's a set of target files in the + html directory that we can compare against.""" # Target files, if they're not just replacing the extension file_map = { diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 9c4d9c01f8f..2d50748bd6f 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -359,7 +359,10 @@ def test_apng_save_split_fdat(tmp_path): with Image.open("Tests/images/old-style-jpeg-compression.png") as im: frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] im.save( - test_file, save_all=True, default_image=True, append_images=frames, + test_file, + save_all=True, + default_image=True, + append_images=frames, ) with Image.open(test_file) as im: exception = None diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 03a2dba51a1..71779461465 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -39,7 +39,7 @@ def roundtrip(self, im, **options): return im def gen_random_image(self, size, mode="RGB"): - """ Generates a very hard to compress file + """Generates a very hard to compress file :param size: tuple :param mode: optional image mode @@ -99,7 +99,8 @@ def test_cmyk(self): assert k > 0.9 @pytest.mark.parametrize( - "test_image_path", [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], + "test_image_path", + [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], ) def test_dpi(self, test_image_path): def test(xdpi, ydpi=None): @@ -242,6 +243,16 @@ def test_exif_gps(self): # Assert assert exif[gps_index] == expected_exif_gps + def test_exif_equality(self): + # In 7.2.0, Exif rationals were changed to be read as + # TiffImagePlugin.IFDRational. This class had a bug in __eq__, + # breaking the self-equality of Exif data + exifs = [] + for i in range(2): + with Image.open("Tests/images/exif-200dpcm.jpg") as im: + exifs.append(im._getexif()) + assert exifs[0] == exifs[1] + def test_exif_rollback(self): # rolling back exif support in 3.1 to pre-3.0 formatting. # expected from 2.9, with b/u qualifiers switched for 3.2 compatibility diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index c4af94e2fdd..da955b3de7d 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -402,8 +402,8 @@ def test_g4_string_info(self, tmp_path): assert "temp.tif" == reread.tag[269][0] def test_12bit_rawmode(self): - """ Are we generating the same interpretation - of the image as Imagemagick is? """ + """Are we generating the same interpretation + of the image as Imagemagick is?""" TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/12bit.cropped.tif") as im: im.load() @@ -503,9 +503,9 @@ def test_palette_save(self, tmp_path): assert len(reloaded.tag_v2[320]) == 768 def xtest_bw_compression_w_rgb(self, tmp_path): - """ This test passes, but when running all tests causes a failure due - to output on stderr from the error thrown by libtiff. We need to - capture that but not now""" + """This test passes, but when running all tests causes a failure due + to output on stderr from the error thrown by libtiff. We need to + capture that but not now""" im = hopper("RGB") out = str(tmp_path / "temp.tif") @@ -768,7 +768,7 @@ def test_16bit_RGBa_tiff(self): assert im.mode == "RGBA" assert im.size == (100, 40) assert im.tile, [ - ("libtiff", (0, 0, 100, 40), 0, ("RGBa;16N", "tiff_lzw", False, 38236),) + ("libtiff", (0, 0, 100, 40), 0, ("RGBa;16N", "tiff_lzw", False, 38236)) ] im.load() diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 593a8eda83f..03137c8b603 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -7,13 +7,13 @@ class TestFileLibTiffSmall(LibTiffTestCase): - """ The small lena image was failing on open in the libtiff - decoder because the file pointer was set to the wrong place - by a spurious seek. It wasn't failing with the byteio method. + """The small lena image was failing on open in the libtiff + decoder because the file pointer was set to the wrong place + by a spurious seek. It wasn't failing with the byteio method. - It was fixed by forcing an lseek to the beginning of the - file just before reading in libtiff. These tests remain - to ensure that it stays fixed. """ + It was fixed by forcing an lseek to the beginning of the + file just before reading in libtiff. These tests remain + to ensure that it stays fixed.""" def test_g4_hopper_file(self, tmp_path): """Testing the open file load path""" diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index aefe6f9eaaa..59411504219 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -225,8 +225,8 @@ def test_16bit_s(self): assert im.getpixel((0, 1)) == 0 def test_12bit_rawmode(self): - """ Are we generating the same interpretation - of the image as Imagemagick is? """ + """Are we generating the same interpretation + of the image as Imagemagick is?""" with Image.open("Tests/images/12bit.cropped.tif") as im: # to make the target -- diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 57e14e9543e..0f7f8adf198 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -12,10 +12,10 @@ def test_rt_metadata(tmp_path): - """ Test writing arbitrary metadata into the tiff image directory - Use case is ImageJ private tags, one numeric, one arbitrary - data. https://github.com/python-pillow/Pillow/issues/291 - """ + """Test writing arbitrary metadata into the tiff image directory + Use case is ImageJ private tags, one numeric, one arbitrary + data. https://github.com/python-pillow/Pillow/issues/291 + """ img = hopper() diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index d10b1acfd66..3b9c8b07146 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -85,7 +85,10 @@ def test_wedge(): im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" ) assert_image_similar( - im.getchannel(1), comparable.getchannel(1), 1, "Saturation conversion is wrong", + im.getchannel(1), + comparable.getchannel(1), + 1, + "Saturation conversion is wrong", ) assert_image_similar( im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" @@ -113,7 +116,10 @@ def test_convert(): im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" ) assert_image_similar( - im.getchannel(1), comparable.getchannel(1), 1, "Saturation conversion is wrong", + im.getchannel(1), + comparable.getchannel(1), + 1, + "Saturation conversion is wrong", ) assert_image_similar( im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" @@ -126,11 +132,20 @@ def test_hsv_to_rgb(): comparable = to_rgb_colorsys(comparable) assert_image_similar( - converted.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong", + converted.getchannel(0), + comparable.getchannel(0), + 3, + "R conversion is wrong", ) assert_image_similar( - converted.getchannel(1), comparable.getchannel(1), 3, "G conversion is wrong", + converted.getchannel(1), + comparable.getchannel(1), + 3, + "G conversion is wrong", ) assert_image_similar( - converted.getchannel(2), comparable.getchannel(2), 3, "B conversion is wrong", + converted.getchannel(2), + comparable.getchannel(2), + 3, + "B conversion is wrong", ) diff --git a/Tests/test_image.py b/Tests/test_image.py index cc0edbdd7a7..6d188e7405a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -707,7 +707,8 @@ def test_exif_interop(self): } @pytest.mark.parametrize( - "test_module", [PIL, Image], + "test_module", + [PIL, Image], ) def test_pillow_version(self, test_module): with pytest.warns(DeprecationWarning): @@ -735,7 +736,7 @@ def test_pillow_version(self, test_module): assert test_module.PILLOW_VERSION > "7.0.0" def test_overrun(self): - """ For overrun completeness, test as: + """For overrun completeness, test as: valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c """ for file in [ diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 7dd3b294f27..f7fe99bb4c2 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,3 +1,4 @@ +import logging import os import pytest @@ -23,6 +24,14 @@ def test_close(): im.getpixel((0, 0)) +def test_close_after_load(caplog): + im = Image.open("Tests/images/hopper.gif") + im.load() + with caplog.at_level(logging.DEBUG): + im.close() + assert len(caplog.records) == 0 + + def test_contextmanager(): fn = None with Image.open("Tests/images/hopper.gif") as im: diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index fe868b7c2fe..51108ead2fd 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -24,9 +24,9 @@ def test_sanity(): def test_16bit_lut(): - """ Tests for 16 bit -> 8 bit lut for converting I->L images - see https://github.com/python-pillow/Pillow/issues/440 - """ + """Tests for 16 bit -> 8 bit lut for converting I->L images + see https://github.com/python-pillow/Pillow/issues/440 + """ im = hopper("I") im.point(list(range(256)) * 256, "L") diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 0c15b20844e..e9149b84332 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -437,7 +437,7 @@ def truncate_tuple(tuple_or_float): def test_profile_typesafety(): - """ Profile init type safety + """Profile init type safety prepatch, these would segfault, postpatch they should emit a typeerror """ diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 5f5ea0748dd..271a1629d87 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -667,7 +667,10 @@ def test_floodfill_border(): # Act ImageDraw.floodfill( - im, centre_point, ImageColor.getrgb("red"), border=ImageColor.getrgb("black"), + im, + centre_point, + ImageColor.getrgb("red"), + border=ImageColor.getrgb("black"), ) # Assert diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index e0dbd909a3d..b4107e8e3a6 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -244,3 +244,8 @@ def test_no_format(self): im = MockImageFile(buf) assert im.format is None assert im.get_format_mimetype() is None + + def test_oserror(self): + im = Image.new("RGB", (1, 1)) + with pytest.raises(OSError): + im.save(BytesIO(), "JPEG2000") diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py index 337d2096e01..0ba6828858d 100644 --- a/Tests/test_imagefont_bitmap.py +++ b/Tests/test_imagefont_bitmap.py @@ -34,6 +34,9 @@ def test_similar(): (0, size_final[1] - size_bitmap[1]), text, fill=(0, 0, 0), font=font_bitmap ) draw_outline.text( - (0, size_final[1] - size_outline[1]), text, fill=(0, 0, 0), font=font_outline, + (0, size_final[1] - size_outline[1]), + text, + fill=(0, 0, 0), + font=font_outline, ) assert_image_similar(im_bitmap, im_outline, 20) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 5fbc0d3db3a..f17bfdd2f60 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,6 +1,6 @@ import pytest -from PIL import Image, ImageOps, features +from PIL import Image, ImageDraw, ImageOps, ImageStat, features from .helper import ( assert_image_equal, @@ -25,7 +25,9 @@ def test_sanity(): ImageOps.autocontrast(hopper("RGB")) ImageOps.autocontrast(hopper("L"), cutoff=10) + ImageOps.autocontrast(hopper("L"), cutoff=(2, 10)) ImageOps.autocontrast(hopper("L"), ignore=[0, 255]) + ImageOps.autocontrast(hopper("L"), mask=hopper("L")) ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) ImageOps.colorize(hopper("L"), "black", "white") @@ -312,3 +314,51 @@ def autocontrast(cutoff): assert autocontrast(10) == autocontrast((10, 10)) assert autocontrast(10) != autocontrast((1, 10)) + + +def test_autocontrast_mask_toy_input(): + # Test the mask argument of autocontrast + with Image.open("Tests/images/bw_gradient.png") as img: + + rect_mask = Image.new("L", img.size, 0) + draw = ImageDraw.Draw(rect_mask) + x0 = img.size[0] // 4 + y0 = img.size[1] // 4 + x1 = 3 * img.size[0] // 4 + y1 = 3 * img.size[1] // 4 + draw.rectangle((x0, y0, x1, y1), fill=255) + + result = ImageOps.autocontrast(img, mask=rect_mask) + result_nomask = ImageOps.autocontrast(img) + + assert result != result_nomask + assert ImageStat.Stat(result, mask=rect_mask).median == [127] + assert ImageStat.Stat(result_nomask).median == [128] + + +def test_auto_contrast_mask_real_input(): + # Test the autocontrast with a rectangular mask + with Image.open("Tests/images/iptc.jpg") as img: + + rect_mask = Image.new("L", img.size, 0) + draw = ImageDraw.Draw(rect_mask) + x0, y0 = img.size[0] // 2, img.size[1] // 2 + x1, y1 = img.size[0] - 40, img.size[1] + draw.rectangle((x0, y0, x1, y1), fill=255) + + result = ImageOps.autocontrast(img, mask=rect_mask) + result_nomask = ImageOps.autocontrast(img) + + assert result_nomask != result + assert_tuple_approx_equal( + ImageStat.Stat(result, mask=rect_mask).median, + [195, 202, 184], + threshold=2, + msg="autocontrast with mask pixel incorrect", + ) + assert_tuple_approx_equal( + ImageStat.Stat(result_nomask).median, + [119, 106, 79], + threshold=2, + msg="autocontrast without mask pixel incorrect", + ) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 043a7aaaec5..78e80f521d6 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -19,7 +19,8 @@ def test_register(): @pytest.mark.parametrize( - "order", [-1, 0], + "order", + [-1, 0], ) def test_viewer_show(order): class TestViewer(ImageShow.Viewer): @@ -41,7 +42,8 @@ def show_image(self, image, **options): @pytest.mark.skipif( - not on_ci() or is_win32(), reason="Only run on CIs; hangs on Windows CIs", + not on_ci() or is_win32(), + reason="Only run on CIs; hangs on Windows CIs", ) def test_show(): for mode in ("1", "I;16", "LA", "RGB", "RGBA"): diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 707284d7b4f..1697a8d4946 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -29,6 +29,12 @@ def test_sanity(): _test_equal(1, 2, IFDRational(1, 2)) +def test_ranges(): + for num in range(1, 10): + for denom in range(1, 10): + assert IFDRational(num, denom) == IFDRational(num, denom) + + def test_nonetype(): # Fails if the _delegate function doesn't return a valid function diff --git a/docs/conf.py b/docs/conf.py index 78841cc15a4..a022e61cb07 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -77,7 +77,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ["_build"] +exclude_patterns = ["_build", "releasenotes/template.rst"] # The reST default role (used for this markup: `text`) to use for all # documents. diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 10ccec632eb..e3ad2a9e334 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -18,16 +18,16 @@ Image.show command parameter .. deprecated:: 7.2.0 The ``command`` parameter was deprecated and will be removed in a future release. -Use a subclass of ``ImageShow.Viewer`` instead. +Use a subclass of :py:class:`.ImageShow.Viewer` instead. Image._showxv ~~~~~~~~~~~~~ .. deprecated:: 7.2.0 -``Image._showxv`` has been deprecated. Use :py:meth:`~PIL.Image.Image.show` -instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add -a custom :py:class:`~PIL.ImageShow.Viewer` class. +``Image._showxv`` has been deprecated. Use :py:meth:`.Image.Image.show` +instead. If custom behaviour is required, use :py:func:`.ImageShow.register` to add +a custom :py:class:`.ImageShow.Viewer` class. ImageFile.raise_ioerror ~~~~~~~~~~~~~~~~~~~~~~~ @@ -61,7 +61,7 @@ im.offset .. deprecated:: 1.1.2 .. versionremoved:: 8.0.0 -``im.offset()`` has been removed, call ``ImageChops.offset()`` instead. +``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead. It was documented as deprecated in PIL 1.1.2, raised a ``DeprecationWarning`` since 1.1.5, @@ -88,20 +88,21 @@ ImageCms.CmsProfile attributes .. deprecated:: 3.2.0 .. versionremoved:: 8.0.0 -Some attributes in ``ImageCms.CmsProfile`` have been removed. From 6.0.0, they issued a -``DeprecationWarning``: +Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0, +they issued a ``DeprecationWarning``: + +======================== =================================================== -======================== =============================== Removed Use instead -======================== =============================== -``color_space`` Padded ``xcolor_space`` -``pcs`` Padded ``connection_space`` -``product_copyright`` Unicode ``copyright`` -``product_desc`` Unicode ``profile_description`` -``product_description`` Unicode ``profile_description`` -``product_manufacturer`` Unicode ``manufacturer`` -``product_model`` Unicode ``model`` -======================== =============================== +======================== =================================================== +``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space` +``pcs`` Padded :py:attr:`~.CmsProfile.connection_space` +``product_copyright`` Unicode :py:attr:`~.CmsProfile.copyright` +``product_desc`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_description`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_manufacturer`` Unicode :py:attr:`~.CmsProfile.manufacturer` +``product_model`` Unicode :py:attr:`~.CmsProfile.model` +======================== =================================================== Python 2.7 ~~~~~~~~~~ diff --git a/docs/installation.rst b/docs/installation.rst index 706cfb1d707..4c41cc9ee6f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -412,12 +412,12 @@ These platforms are built and tested for every change. | Windows Server 2016 | 3.8 |x86 | | +--------------------------+-----------------------+ | | 3.6 |x86-64 | -| +--------------------------+-----------------------+ -| | 3.7/MinGW |x86 | +----------------------------------+--------------------------+-----------------------+ | Windows Server 2019 | 3.6, 3.7, 3.8 |x86, x86-64 | | +--------------------------+-----------------------+ | | PyPy3 |x86 | +| +--------------------------+-----------------------+ +| | 3.8/MinGW |x86, x86-64 | +----------------------------------+--------------------------+-----------------------+ diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst new file mode 100644 index 00000000000..bbb954e25b0 --- /dev/null +++ b/docs/releasenotes/8.0.0.rst @@ -0,0 +1,81 @@ +8.0.0 +----- + +Backwards Incompatible Changes +============================== + +Python 3.5 +^^^^^^^^^^ + +Pillow has dropped support for Python 3.5, which reached end-of-life on 2020-09-13. + +im.offset +^^^^^^^^^ + +``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead. + +Image.fromstring, im.fromstring and im.tostring +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``Image.fromstring()`` has been removed, call :py:func:`.Image.frombytes()` instead. +* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead. +* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead. + +ImageCms.CmsProfile attributes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed: + +======================== =================================================== +Removed Use instead +======================== =================================================== +``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space` +``pcs`` Padded :py:attr:`~.CmsProfile.connection_space` +``product_copyright`` Unicode :py:attr:`~.CmsProfile.copyright` +``product_desc`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_description`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_manufacturer`` Unicode :py:attr:`~.CmsProfile.manufacturer` +``product_model`` Unicode :py:attr:`~.CmsProfile.model` +======================== =================================================== + +API Changes +=========== + +Add MIME type to PsdImagePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +"image/vnd.adobe.photoshop" is now registered as the +:py:class:`.PsdImagePlugin.PsdImageFile` MIME type. + +API Additions +============= + +ImageOps.autocontrast: add mask parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:func:`.ImageOps.autocontrast` can now take a ``mask`` parameter: + +* Histogram used in contrast operation is computed using pixels within the mask. + If no mask is given the entire image is used for histogram computation. + +ImageOps.autocontrast cutoffs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, the ``cutoff`` parameter of :py:func:`.ImageOps.autocontrast` could only +be a single number, used as the percent to cut off from the histogram on the low and +high ends. + +Now, it can also be a tuple ``(low, high)``. + +Security +======== + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 2d7747c3e9b..ba81fbaf814 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -13,6 +13,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 8.0.0 7.2.0 7.1.2 7.1.1 diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst new file mode 100644 index 00000000000..bf381114efa --- /dev/null +++ b/docs/releasenotes/template.rst @@ -0,0 +1,45 @@ +x.y.z +----- + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/setup.cfg b/setup.cfg index b91a522f59a..129adeee7a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [flake8] -extend-ignore = E203, W503 +extend-ignore = E203 max-line-length = 88 [isort] diff --git a/setup.py b/setup.py index 550dc6fc5c1..a6a39e57b13 100755 --- a/setup.py +++ b/setup.py @@ -243,11 +243,6 @@ def _cmd_exists(cmd): ) -def _read(file): - with open(file, "rb") as fp: - return fp.read() - - def _pkg_config(name): try: command = os.environ.get("PKG_CONFIG", "pkg-config") @@ -479,6 +474,9 @@ def build_extensions(self): # add Homebrew's include and lib directories _add_directory(library_dirs, os.path.join(prefix, "lib")) _add_directory(include_dirs, os.path.join(prefix, "include")) + _add_directory( + include_dirs, os.path.join(prefix, "opt", "zlib", "include") + ) ft_prefix = os.path.join(prefix, "opt", "freetype") if ft_prefix and os.path.isdir(ft_prefix): @@ -858,12 +856,17 @@ def debug_build(): Extension("PIL._imagingmath", ["src/_imagingmath.c"]), Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), ] + +with open("README.md") as f: + long_description = f.read() + try: setup( name=NAME, version=PILLOW_VERSION, description="Python Imaging Library (Fork)", - long_description=_read("README.rst").decode("utf-8"), + long_description=long_description, + long_description_content_type="text/markdown", license="HPND", author="Alex Clark (PIL Fork Author)", author_email="aclark@python-pillow.org", diff --git a/src/PIL/Image.py b/src/PIL/Image.py index aef101b2f1f..30ee8606a1f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -594,7 +594,8 @@ def close(self): try: if hasattr(self, "_close__fp"): self._close__fp() - self.fp.close() + if self.fp: + self.fp.close() self.fp = None except Exception as msg: logger.debug("Error closing: %s", msg) @@ -664,7 +665,7 @@ def __repr__(self): ) def _repr_png_(self): - """ iPython display hook support + """iPython display hook support :returns: png version of the image as bytes """ @@ -1180,7 +1181,7 @@ def filter(self, filter): available filters, see the :py:mod:`~PIL.ImageFilter` module. :param filter: Filter kernel. - :returns: An :py:class:`~PIL.Image.Image` object. """ + :returns: An :py:class:`~PIL.Image.Image` object.""" from . import ImageFilter @@ -1505,7 +1506,7 @@ def paste(self, im, box=None, mask=None): self.im.paste(im, box) def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): - """ 'In-place' analog of Image.alpha_composite. Composites an image + """'In-place' analog of Image.alpha_composite. Composites an image onto this image. :param im: image to composite over this one diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 357256b7251..3856cb84342 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -277,8 +277,8 @@ def get_display_profile(handle=None): class PyCMSError(Exception): - """ (pyCMS) Exception class. - This is used for all errors in the pyCMS API. """ + """(pyCMS) Exception class. + This is used for all errors in the pyCMS API.""" pass diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 6b666a4e2ea..49633ef5104 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -509,7 +509,7 @@ def _save(im, fp, tile, bufsize=0): try: fh = fp.fileno() fp.flush() - except (AttributeError, io.UnsupportedOperation) as e: + except (AttributeError, io.UnsupportedOperation) as exc: # compress to Python file-compatible object for e, b, o, a in tile: e = Image._getencoder(im.mode, e, a, im.encoderconfig) @@ -526,7 +526,7 @@ def _save(im, fp, tile, bufsize=0): if s: break if s < 0: - raise OSError(f"encoder error {s} when writing image file") from e + raise OSError(f"encoder error {s} when writing image file") from exc e.cleanup() else: # slight speedup: compress to real file object diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index d1ec09eace4..b76dfa01f4d 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -28,36 +28,36 @@ class LutBuilder: """A class for building a MorphLut from a descriptive language - The input patterns is a list of a strings sequences like these:: + The input patterns is a list of a strings sequences like these:: - 4:(... - .1. - 111)->1 + 4:(... + .1. + 111)->1 - (whitespaces including linebreaks are ignored). The option 4 - describes a series of symmetry operations (in this case a - 4-rotation), the pattern is described by: + (whitespaces including linebreaks are ignored). The option 4 + describes a series of symmetry operations (in this case a + 4-rotation), the pattern is described by: - - . or X - Ignore - - 1 - Pixel is on - - 0 - Pixel is off + - . or X - Ignore + - 1 - Pixel is on + - 0 - Pixel is off - The result of the operation is described after "->" string. + The result of the operation is described after "->" string. - The default is to return the current pixel value, which is - returned if no other match is found. + The default is to return the current pixel value, which is + returned if no other match is found. - Operations: + Operations: - - 4 - 4 way rotation - - N - Negate - - 1 - Dummy op for no other operation (an op must always be given) - - M - Mirroring + - 4 - 4 way rotation + - N - Negate + - 1 - Dummy op for no other operation (an op must always be given) + - M - Mirroring - Example:: + Example:: - lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) - lut = lb.build_lut() + lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) + lut = lb.build_lut() """ diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 3b1b737bd78..14602a5c8cc 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -61,10 +61,10 @@ def _lut(image, lut): # actions -def autocontrast(image, cutoff=0, ignore=None): +def autocontrast(image, cutoff=0, ignore=None, mask=None): """ Maximize (normalize) image contrast. This function calculates a - histogram of the input image, removes ``cutoff`` percent of the + histogram of the input image (or mask region), removes ``cutoff`` percent of the lightest and darkest pixels from the histogram, and remaps the image so that the darkest pixel becomes black (0), and the lightest becomes white (255). @@ -74,9 +74,12 @@ def autocontrast(image, cutoff=0, ignore=None): high ends. Either a tuple of (low, high), or a single number for both. :param ignore: The background pixel value (use None for no background). + :param mask: Histogram used in contrast operation is computed using pixels + within the mask. If no mask is given the entire image is used + for histogram computation. :return: An image. """ - histogram = image.histogram() + histogram = image.histogram(mask) lut = [] for layer in range(0, len(histogram), 256): h = histogram[layer : layer + 256] diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 9583f704a4d..36c8fb8494d 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -121,6 +121,7 @@ def _save(im, fp, filename, save_all=False): bits = 8 params = None + decode = None if im.mode == "1": filter = "ASCIIHexDecode" @@ -150,6 +151,7 @@ def _save(im, fp, filename, save_all=False): filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceCMYK") procset = "ImageC" # color images + decode = [1, 0, 1, 0, 1, 0, 1, 0] else: raise ValueError(f"cannot save mode {im.mode}") @@ -189,6 +191,7 @@ def _save(im, fp, filename, save_all=False): Height=height, # * 72.0 / resolution, Filter=PdfParser.PdfName(filter), BitsPerComponent=bits, + Decode=decode, DecodeParams=params, ColorSpace=colorspace, ) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index d3d053eb2b8..bcc6b1c988b 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1108,7 +1108,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode): # animation control chunk( - fp, b"acTL", o32(len(im_frames)), o32(loop), # 0: num_frames # 4: num_plays + fp, + b"acTL", + o32(len(im_frames)), # 0: num_frames + o32(loop), # 4: num_plays ) # default image IDAT (if it exists) @@ -1153,7 +1156,9 @@ def _write_multiple_frames(im, fp, chunk, rawmode): else: fdat_chunks = _fdat(fp, chunk, seq_num) ImageFile._save( - im_frame, fdat_chunks, [("zip", (0, 0) + im_frame.size, 0, rawmode)], + im_frame, + fdat_chunks, + [("zip", (0, 0) + im_frame.size, 0, rawmode)], ) seq_num = fdat_chunks.seq_num diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index f3dba74843a..6e1c45636f7 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -286,7 +286,7 @@ def _limit_signed_rational(val, max_val, min_val): class IFDRational(Rational): - """ Implements a rational class where 0/0 is a legal value to match + """Implements a rational class where 0/0 is a legal value to match the in the wild use of exif rationals. e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used @@ -353,6 +353,8 @@ def __hash__(self): return self._val.__hash__() def __eq__(self, other): + if isinstance(other, IFDRational): + other = other._val return self._val == other def _delegate(op): @@ -903,7 +905,7 @@ def __init__(self, *args, **kwargs): @classmethod def from_v2(cls, original): - """ Returns an + """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` instance with the same data as is contained in the original :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` @@ -920,7 +922,7 @@ def from_v2(cls, original): return ifd def to_v2(self): - """ Returns an + """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` instance with the same data as is contained in the original :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` @@ -1090,8 +1092,8 @@ def load_end(self): self._close_exclusive_fp_after_loading = True def _load_libtiff(self): - """ Overload method triggered when we detect a compressed tiff - Calls out to libtiff """ + """Overload method triggered when we detect a compressed tiff + Calls out to libtiff""" Image.Image.load(self) diff --git a/src/decode.c b/src/decode.c index 25ce7316bc3..f60fb490e26 100644 --- a/src/decode.c +++ b/src/decode.c @@ -807,7 +807,7 @@ PyImaging_XbmDecoderNew(PyObject* self, PyObject* args) #ifdef HAVE_LIBZ -#include "Zip.h" +#include "ZipCodecs.h" PyObject* PyImaging_ZipDecoderNew(PyObject* self, PyObject* args) diff --git a/src/encode.c b/src/encode.c index d64f47d2b13..62a6ec38706 100644 --- a/src/encode.c +++ b/src/encode.c @@ -578,7 +578,7 @@ PyImaging_XbmEncoderNew(PyObject* self, PyObject* args) #ifdef HAVE_LIBZ -#include "Zip.h" +#include "ZipCodecs.h" PyObject* PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) diff --git a/src/libImaging/Zip.h b/src/libImaging/ZipCodecs.h similarity index 100% rename from src/libImaging/Zip.h rename to src/libImaging/ZipCodecs.h diff --git a/src/libImaging/ZipDecode.c b/src/libImaging/ZipDecode.c index b0f8ad3261e..a09ee82f7cc 100644 --- a/src/libImaging/ZipDecode.c +++ b/src/libImaging/ZipDecode.c @@ -20,7 +20,7 @@ #ifdef HAVE_LIBZ -#include "Zip.h" +#include "ZipCodecs.h" static const int OFFSET[] = { 7, 3, 3, 1, 1, 0, 0 }; static const int STARTING_COL[] = { 0, 4, 0, 2, 0, 1, 0 }; diff --git a/src/libImaging/ZipEncode.c b/src/libImaging/ZipEncode.c index 84ccb14ea54..4e862af57a7 100644 --- a/src/libImaging/ZipEncode.c +++ b/src/libImaging/ZipEncode.c @@ -19,7 +19,7 @@ #ifdef HAVE_LIBZ -#include "Zip.h" +#include "ZipCodecs.h" int ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) diff --git a/winbuild/build.rst b/winbuild/build.rst index aaed2c43fa8..ba568a0303d 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -81,6 +81,9 @@ Pillow for the selected version of Python. ``winbuild\build\build_pillow.cmd bdist_wheel`` will build wheels instead of installing Pillow. +You can also use ``winbuild\build\build_pillow.cmd --inplace develop`` to build +and install Pillow in develop mode (instead of ``pip install --editable``). + Testing Pillow -------------- diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e6db4b2e457..e05ac1a2970 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -251,9 +251,9 @@ def cmd_msbuild( "libs": [r"*.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/2.7.1.zip", - "filename": "harfbuzz-2.7.1.zip", - "dir": "harfbuzz-2.7.1", + "url": "https://github.com/harfbuzz/harfbuzz/archive/2.7.2.zip", + "filename": "harfbuzz-2.7.2.zip", + "dir": "harfbuzz-2.7.2", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"),