From 97385d7cc7e983c7c1a22bf777152f5ce1dc917c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 19:54:12 +1100 Subject: [PATCH 01/65] Relaxed child images check to allow for libjpeg --- Tests/test_file_jpeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index eabc6bf75..fb8954125 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -447,7 +447,7 @@ class TestFileJpeg: ims = im.get_child_images() assert len(ims) == 1 - assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png") + assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1) def test_mp(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: From c8966013bd47fe59f729969463360f12763641c4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 27 Jan 2023 23:19:03 +0200 Subject: [PATCH 02/65] Replace SVN with Git for installing extra test images --- .appveyor.yml | 4 +++- .github/workflows/test-windows.yml | 5 ++++- depends/install_extra_test_images.sh | 21 +++++++++------------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index b817cd9d8..d4dd2dc95 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -21,9 +21,11 @@ environment: install: - '%PYTHON%\%EXECUTABLE% --version' - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip +- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-depends.zip -oc:\ +- 7z x pillow-test-images.zip -oc:\ - mv c:\pillow-depends-main c:\pillow-depends -- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images +- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ - ..\pillow-depends\gs1000w32.exe /S - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH% diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 487c3586f..48825dc30 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -62,7 +62,10 @@ jobs: winbuild\depends\gs1000w32.exe /S echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH - xcopy /S /Y winbuild\depends\test_images\* Tests\images\ + # Install extra test images + curl -fsSL -o pillow-test-images.zip https://github.com/hugovk/test-images/archive/main.zip + 7z x pillow-test-images.zip -oc:\ + xcopy /S /Y c:\test-images-main\* Tests\images\ # make cache key depend on VS version & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 02da12d61..7381d3767 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,15 +1,12 @@ -#!/bin/bash +#!/usr/bin/env bash # install extra test images -# Use SVN to just fetch a single Git subdirectory -svn_export() -{ - if [ ! -z $1 ]; then - echo "" - echo "Retrying svn export..." - echo "" - fi +archive=test-images-main - svn export --force https://github.com/python-pillow/pillow-depends/trunk/test_images ../Tests/images -} -svn_export || svn_export retry || svn_export retry || svn_export retry +./download-and-extract.sh $archive https://github.com/hugovk/test-images/archive/refs/heads/main.tar.gz + +mv $archive/* ../Tests/images/ + +# Cleanup old tarball and empty directory +rm $archive.tar.gz +rm -r $archive From 120d56b4ba49871a6d3032b7d0d4c8159e8273ec Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 28 Jan 2023 16:40:11 +0200 Subject: [PATCH 03/65] Sort dependencies --- .github/workflows/test-cygwin.yml | 28 ++++++++++++++++++++++------ .github/workflows/test-mingw.yml | 10 +++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7b8070d34..1dfb36f44 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -34,18 +34,34 @@ jobs: with: platform: x86_64 packages: > - ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel - libimagequant-devel libjpeg-devel liblapack-devel - liblcms2-devel libopenjp2-devel libraqm-devel - libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0 - make netpbm perl + gcc-g++ + ghostscript + ImageMagick + jpeg + libfreetype-devel + libimagequant-devel + libjpeg-devel + liblapack-devel + liblcms2-devel + libopenjp2-devel + libraqm-devel + libtiff-devel + libwebp-devel + libxcb-devel + libxcb-xinerama0 + make + netpbm + perl python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt5-devel-tools subversion xorg-server-extra zlib-devel + qt5-devel-tools + subversion + xorg-server-extra + zlib-devel - name: Add Lapack to PATH uses: egor-tensin/cleanup-path@v3 diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 6a60bc7f0..24575f6c7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,11 +45,6 @@ jobs: - name: Install dependencies run: | pacman -S --noconfirm \ - ${{ matrix.package }}-python3-cffi \ - ${{ matrix.package }}-python3-numpy \ - ${{ matrix.package }}-python3-olefile \ - ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools \ ${{ matrix.package }}-freetype \ ${{ matrix.package }}-gcc \ ${{ matrix.package }}-ghostscript \ @@ -60,6 +55,11 @@ jobs: ${{ matrix.package }}-libtiff \ ${{ matrix.package }}-libwebp \ ${{ matrix.package }}-openjpeg2 \ + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python3-setuptools \ subversion if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then From 7e35e15eeeca12a4eec61d39ff7fc819d08d3554 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 28 Jan 2023 16:40:25 +0200 Subject: [PATCH 04/65] Replace subversion with wget package --- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 1dfb36f44..451181434 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -59,7 +59,7 @@ jobs: python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter qt5-devel-tools - subversion + wget xorg-server-extra zlib-devel diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 24575f6c7..ef8214649 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -60,7 +60,7 @@ jobs: ${{ matrix.package }}-python3-olefile \ ${{ matrix.package }}-python3-pip \ ${{ matrix.package }}-python3-setuptools \ - subversion + ${{ matrix.package }}-wget if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then pacman -S --noconfirm \ From 772567a4ce9ca1890ecbee976bb777e82db2577a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 20:13:28 +1100 Subject: [PATCH 05/65] Switched to python-pillow repositories --- .github/workflows/test-windows.yml | 2 +- depends/install_extra_test_images.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 48825dc30..cf160a997 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -63,7 +63,7 @@ jobs: echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH # Install extra test images - curl -fsSL -o pillow-test-images.zip https://github.com/hugovk/test-images/archive/main.zip + curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip 7z x pillow-test-images.zip -oc:\ xcopy /S /Y c:\test-images-main\* Tests\images\ diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 7381d3767..ffdfe17f2 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -3,7 +3,7 @@ archive=test-images-main -./download-and-extract.sh $archive https://github.com/hugovk/test-images/archive/refs/heads/main.tar.gz +./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/refs/heads/main.tar.gz mv $archive/* ../Tests/images/ From cc71b4c1b2226330c77210f2243deaa8b254afdc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 4 Feb 2023 10:04:29 +0200 Subject: [PATCH 06/65] Simpler URL Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/install_extra_test_images.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index ffdfe17f2..941bfbe84 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -3,7 +3,7 @@ archive=test-images-main -./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/refs/heads/main.tar.gz +./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/main.tar.gz mv $archive/* ../Tests/images/ From e79460e775a69c2cd9896144c3b340c4f98e3ad5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 09:46:01 +1100 Subject: [PATCH 07/65] Removed wget dependency --- .github/workflows/test-mingw.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index ef8214649..737da7b94 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -59,8 +59,7 @@ jobs: ${{ matrix.package }}-python3-numpy \ ${{ matrix.package }}-python3-olefile \ ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools \ - ${{ matrix.package }}-wget + ${{ matrix.package }}-python3-setuptools if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then pacman -S --noconfirm \ From e7d2750997cfbf88a6c93cbe1e598c8054d1676a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 10:01:10 +1100 Subject: [PATCH 08/65] Updated test images origin --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 790404535..1dd6c9175 100644 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,7 @@ docs/_build/ # JetBrains .idea -# Extra test images installed from pillow-depends/test_images +# Extra test images installed from python-pillow/test-images Tests/images/README.md Tests/images/crash_1.tif Tests/images/crash_2.tif From ed1d6633a1db0bf7e894bd372dcccd647cc0ee85 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 10:53:59 +1100 Subject: [PATCH 09/65] Use checkout action for test-images repository --- .github/workflows/test-windows.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index cf160a997..e938e999d 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -38,6 +38,12 @@ jobs: repository: python-pillow/pillow-depends path: winbuild\depends + - name: Checkout extra test images + uses: actions/checkout@v3 + with: + repository: python-pillow/test-images + path: Tests\test-images + # sets env: pythonLocation - name: Set up Python uses: actions/setup-python@v4 @@ -63,9 +69,7 @@ jobs: echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH # Install extra test images - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-test-images.zip -oc:\ - xcopy /S /Y c:\test-images-main\* Tests\images\ + xcopy /S /Y Tests\test-images\* Tests\images # make cache key depend on VS version & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` From 165675314605d7363605918698df091b405ba283 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 7 Feb 2023 22:48:33 -0800 Subject: [PATCH 10/65] Add docstrings for getixif() and Exif --- src/PIL/Image.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 81123d070..d47c57334 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1432,6 +1432,11 @@ class Image: return {get_name(root.tag): get_value(root)} def getexif(self): + """ + Gets EXIF data of the image. + + :returns: an :py:class:`~PIL.Image.Exif` object. + """ if self._exif is None: self._exif = Exif() self._exif._loaded = False @@ -3601,6 +3606,20 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): + """ + Exif class provides read and write access to EXIF image data. + + Only basic information is available on the root level, in Exif object + itself. In order to access the rest, obtain their respective IFDs using + :py:meth:`~PIL.Image.Exif.get_ifd` method and one of + :py:class:`~PIL.ExifTags.IFD` members (most notably `Exif` and + `GPSInfo`). + + Both root Exif and child IFD objects support dict interface and can be + indexed by int values that are available as enum members of + :py:class:`~PIL.ExifTags.Base`, :py:class:`~PIL.ExifTags.GPS`, and + :py:class:`~PIL.ExifTags.Interop`. + """ endian = None bigtiff = False From ed3cd75630352b26ee3b90a05c3d9b7fc0de041f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Feb 2023 10:11:54 +0200 Subject: [PATCH 11/65] Use 'rmdir' instead of 'rm -r' Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/install_extra_test_images.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 941bfbe84..1ef6f4e97 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -9,4 +9,4 @@ mv $archive/* ../Tests/images/ # Cleanup old tarball and empty directory rm $archive.tar.gz -rm -r $archive +rmdir $archive From f679e410bd3ee7be8e4c6dce0df0ca4cb4d1fb47 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Feb 2023 10:12:14 +0200 Subject: [PATCH 12/65] Use 'rmdir' instead of 'rm -r' --- depends/download-and-extract.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index d9608e782..a318bfafd 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -8,5 +8,5 @@ if [ ! -f $archive.tar.gz ]; then wget -O $archive.tar.gz $url fi -rm -r $archive +rmdir $archive tar -xvzf $archive.tar.gz From c0a811e11678c3a6d8869fdc3739c7635b83ddd4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Feb 2023 08:36:41 +1100 Subject: [PATCH 13/65] Updated libjpeg-turbo to 2.1.5.1 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 89903c621..8a4494103 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -109,9 +109,9 @@ header = [ deps = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/2.1.5/libjpeg-turbo-2.1.5.tar.gz/download", - "filename": "libjpeg-turbo-2.1.5.tar.gz", - "dir": "libjpeg-turbo-2.1.5", + + "/libjpeg-turbo/files/2.1.5.1/libjpeg-turbo-2.1.5.1.tar.gz/download", + "filename": "libjpeg-turbo-2.1.5.1.tar.gz", + "dir": "libjpeg-turbo-2.1.5.1", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" From 0eac4f1942b278f618761c91ccec3f4422b90300 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 8 Feb 2023 20:34:45 -0800 Subject: [PATCH 14/65] Fix syntax --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d47c57334..f5fa4ec0d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3612,8 +3612,8 @@ class Exif(MutableMapping): Only basic information is available on the root level, in Exif object itself. In order to access the rest, obtain their respective IFDs using :py:meth:`~PIL.Image.Exif.get_ifd` method and one of - :py:class:`~PIL.ExifTags.IFD` members (most notably `Exif` and - `GPSInfo`). + :py:class:`~PIL.ExifTags.IFD` members (most notably ``Exif`` and + ``GPSInfo``). Both root Exif and child IFD objects support dict interface and can be indexed by int values that are available as enum members of From 074c6afdc7d6c6990b9738c68c0fa3951d4f0855 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 04:40:57 +0000 Subject: [PATCH 15/65] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/Image.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f5fa4ec0d..813c237ad 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1434,7 +1434,7 @@ class Image: def getexif(self): """ Gets EXIF data of the image. - + :returns: an :py:class:`~PIL.Image.Exif` object. """ if self._exif is None: @@ -3608,18 +3608,19 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): """ Exif class provides read and write access to EXIF image data. - + Only basic information is available on the root level, in Exif object itself. In order to access the rest, obtain their respective IFDs using :py:meth:`~PIL.Image.Exif.get_ifd` method and one of :py:class:`~PIL.ExifTags.IFD` members (most notably ``Exif`` and ``GPSInfo``). - + Both root Exif and child IFD objects support dict interface and can be indexed by int values that are available as enum members of :py:class:`~PIL.ExifTags.Base`, :py:class:`~PIL.ExifTags.GPS`, and :py:class:`~PIL.ExifTags.Interop`. """ + endian = None bigtiff = False From a45211b811f1a0e4313b672c1629bedca857745a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Feb 2023 20:33:08 +1100 Subject: [PATCH 16/65] Updated freetype to 2.13 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 8a4494103..ff95581fa 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -253,9 +253,9 @@ deps = { "libs": ["*.lib"], }, "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.gz", # noqa: E501 - "filename": "freetype-2.12.1.tar.gz", - "dir": "freetype-2.12.1", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.0.tar.gz", # noqa: E501 + "filename": "freetype-2.13.0.tar.gz", + "dir": "freetype-2.13.0", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { From 997932bc93c778919fe22f4838ef0929482fe520 Mon Sep 17 00:00:00 2001 From: Marcel Telka Date: Thu, 9 Feb 2023 23:23:29 +0100 Subject: [PATCH 17/65] Update HPND wording in LICENSE file --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 125bdcc44..cf65e86d7 100644 --- a/LICENSE +++ b/LICENSE @@ -13,8 +13,8 @@ By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: -Permission to use, copy, modify, and distribute this software and its -associated documentation for any purpose and without fee is hereby granted, +Permission to use, copy, modify and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be From a8e03e4dabd0fcb55fec922825dec9861e7ef103 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Feb 2023 20:11:50 +1100 Subject: [PATCH 18/65] Added Exif code examples --- src/PIL/Image.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 813c237ad..a280935a5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1433,7 +1433,7 @@ class Image: def getexif(self): """ - Gets EXIF data of the image. + Gets EXIF data from the image. :returns: an :py:class:`~PIL.Image.Exif` object. """ @@ -3607,18 +3607,36 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): """ - Exif class provides read and write access to EXIF image data. + This class provides read and write access to EXIF image data:: - Only basic information is available on the root level, in Exif object - itself. In order to access the rest, obtain their respective IFDs using - :py:meth:`~PIL.Image.Exif.get_ifd` method and one of - :py:class:`~PIL.ExifTags.IFD` members (most notably ``Exif`` and - ``GPSInfo``). + from PIL import Image + im = Image.open("exif.png") + exif = im.getexif() # Returns an instance of this class - Both root Exif and child IFD objects support dict interface and can be - indexed by int values that are available as enum members of - :py:class:`~PIL.ExifTags.Base`, :py:class:`~PIL.ExifTags.GPS`, and - :py:class:`~PIL.ExifTags.Interop`. + Information can be read and written, iterated over or deleted:: + + print(exif[274]) # 1 + exif[274] = 2 + for k, v in exif.items(): + print("Tag", k, "Value", v) # Tag 274 Value 2 + del exif[274] + + To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` + returns a dictionary:: + + from PIL import ExifTags + im = Image.open("exif_gps.jpg") + exif = im.getexif() + gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) + print(gps_ifd) + + Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, + ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. + + :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: + + print(exif[ExifTags.Base.Software]) # PIL + print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # '1999:99:99 99:99:99' """ endian = None From bb524018d35ea6450e56c3cd3db489eeb0f5a79c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Feb 2023 16:20:27 +1100 Subject: [PATCH 19/65] Raise an error when EXIF data is too long --- Tests/test_file_jpeg.py | 5 ++++- src/PIL/JpegImagePlugin.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index e3c5abcbd..b84661330 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -270,7 +270,10 @@ class TestFileJpeg: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") im = hopper() - im.save(f, "JPEG", quality=90, exif=b"1" * 65532) + im.save(f, "JPEG", quality=90, exif=b"1" * 65533) + + with pytest.raises(ValueError): + im.save(f, "JPEG", quality=90, exif=b"1" * 65534) def test_exif_typeerror(self): with Image.open("Tests/images/exif_typeerror.jpg") as im: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index d7ddbe0d9..71ae84c04 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -730,10 +730,10 @@ def _save(im, fp, filename): extra = info.get("extra", b"") + MAX_BYTES_IN_MARKER = 65533 icc_profile = info.get("icc_profile") if icc_profile: ICC_OVERHEAD_LEN = 14 - MAX_BYTES_IN_MARKER = 65533 MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN markers = [] while icc_profile: @@ -764,6 +764,9 @@ def _save(im, fp, filename): exif = info.get("exif", b"") if isinstance(exif, Image.Exif): exif = exif.tobytes() + if len(exif) > MAX_BYTES_IN_MARKER: + msg = "EXIF data is too long" + raise ValueError(msg) # get keyword arguments im.encoderconfig = ( From 20daa1d049b8b0e321c282af073e05b114fb216e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Feb 2023 20:49:09 +1100 Subject: [PATCH 20/65] Fixed typo --- src/PIL/TiffTags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 9b5277138..ac048ba56 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -312,7 +312,7 @@ TAGS = { 34910: "HylaFAX FaxRecvTime", 36864: "ExifVersion", 36867: "DateTimeOriginal", - 36868: "DateTImeDigitized", + 36868: "DateTimeDigitized", 37121: "ComponentsConfiguration", 37122: "CompressedBitsPerPixel", 37724: "ImageSourceData", From ac6b9632b4c90ff820c23420e699c68092166d63 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 10 Feb 2023 11:11:39 +0200 Subject: [PATCH 21/65] Test Python 3.12-dev on macOS and Ubuntu --- .ci/install.sh | 3 ++- .github/workflows/macos-install.sh | 3 ++- .github/workflows/test.yml | 1 + docs/installation.rst | 12 ++++++------ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 518b66acc..6aa122cc5 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -37,7 +37,8 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma if [[ $(uname) != CYGWIN* ]]; then - python3 -m pip install numpy + # TODO Remove condition when NumPy supports 3.12 + if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index dfd7d0553..1fc6262f4 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -13,7 +13,8 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -python3 -m pip install numpy +# TODO Remove condition when NumPy supports 3.12 +if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11c7b77be..8e06de4cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: python-version: [ "pypy3.9", "pypy3.8", + "3.12-dev", "3.11", "3.10", "3.9", diff --git a/docs/installation.rst b/docs/installation.rst index ea8722c56..93260e141 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -442,15 +442,15 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | PyPy3 | | +| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | PyPy3 | | -+----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.10 | arm64v8, ppc64le, | | | | s390x, x86-64 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.7 | x86-64 | From 826c98156ceaf619493af22808189500f9b0ae46 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 11 Feb 2023 16:36:13 +0200 Subject: [PATCH 22/65] Remove unused listwindows functions for Windows/3.12 support --- src/_imaging.c | 3 --- src/display.c | 73 -------------------------------------------------- 2 files changed, 76 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 05e1370f6..cece2e93a 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3984,8 +3984,6 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args); extern PyObject * PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args); extern PyObject * -PyImaging_ListWindowsWin32(PyObject *self, PyObject *args); -extern PyObject * PyImaging_EventLoopWin32(PyObject *self, PyObject *args); extern PyObject * PyImaging_DrawWmf(PyObject *self, PyObject *args); @@ -4069,7 +4067,6 @@ static PyMethodDef functions[] = { {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, METH_VARARGS}, {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, METH_VARARGS}, {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, METH_VARARGS}, - {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, METH_VARARGS}, {"drawwmf", (PyCFunction)PyImaging_DrawWmf, METH_VARARGS}, #endif #ifdef HAVE_XCB diff --git a/src/display.c b/src/display.c index 0ce10e249..a50fc3e24 100644 --- a/src/display.c +++ b/src/display.c @@ -421,79 +421,6 @@ error: return NULL; } -static BOOL CALLBACK -list_windows_callback(HWND hwnd, LPARAM lParam) { - PyObject *window_list = (PyObject *)lParam; - PyObject *item; - PyObject *title; - RECT inner, outer; - int title_size; - int status; - - /* get window title */ - title_size = GetWindowTextLength(hwnd); - if (title_size > 0) { - title = PyUnicode_FromStringAndSize(NULL, title_size); - if (title) { - GetWindowTextW(hwnd, PyUnicode_AS_UNICODE(title), title_size + 1); - } - } else { - title = PyUnicode_FromString(""); - } - if (!title) { - return 0; - } - - /* get bounding boxes */ - GetClientRect(hwnd, &inner); - GetWindowRect(hwnd, &outer); - - item = Py_BuildValue( - F_HANDLE "N(iiii)(iiii)", - hwnd, - title, - inner.left, - inner.top, - inner.right, - inner.bottom, - outer.left, - outer.top, - outer.right, - outer.bottom); - if (!item) { - return 0; - } - - status = PyList_Append(window_list, item); - - Py_DECREF(item); - - if (status < 0) { - return 0; - } - - return 1; -} - -PyObject * -PyImaging_ListWindowsWin32(PyObject *self, PyObject *args) { - PyObject *window_list; - - window_list = PyList_New(0); - if (!window_list) { - return NULL; - } - - EnumWindows(list_windows_callback, (LPARAM)window_list); - - if (PyErr_Occurred()) { - Py_DECREF(window_list); - return NULL; - } - - return window_list; -} - /* -------------------------------------------------------------------- */ /* Windows clipboard grabber */ From ab2809a44c2211741ce2eca132d9cc60619ea2b5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 10 Feb 2023 11:11:39 +0200 Subject: [PATCH 23/65] Test Python 3.12-dev on macOS and Ubuntu --- .github/workflows/test-windows.yml | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e938e999d..306e34ca9 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows diff --git a/docs/installation.rst b/docs/installation.rst index 93260e141..1bfc65f4b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -456,7 +456,7 @@ These platforms are built and tested for every change. | Windows Server 2016 | 3.7 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | -| | PyPy3 | | +| | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86, x86-64 | | +----------------------------+---------------------+ From f6040bc8792e355a0b62ba961093cde8127d39d4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 11 Feb 2023 23:14:47 +0200 Subject: [PATCH 24/65] Docker tests: enable gcov support for codecov/codecov-action --- .github/workflows/test-docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 7331cf8ee..7d2b20d65 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -87,6 +87,7 @@ jobs: with: flags: GHA_Docker name: ${{ matrix.docker }} + gcov: true success: permissions: From 0836c747f08fdfcdfd9be5ee858d3ad10bb5430f Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 11 Feb 2023 23:16:13 +0000 Subject: [PATCH 25/65] add gcov coverage to test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11c7b77be..87cad1f36 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -107,9 +107,9 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v3 with: - file: ./coverage.xml flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} + gcov: true success: permissions: From 42683781d6d25913cf9274dd7cd14153e542cfd1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Feb 2023 13:41:35 +1100 Subject: [PATCH 26/65] Updated CI targets --- docs/installation.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 1bfc65f4b..1b5719a8e 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -447,11 +447,13 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.10 | arm64v8, ppc64le, | -| | | s390x, x86-64 | +| | | s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.7 | x86-64 | +----------------------------------+----------------------------+---------------------+ From d9085541bf9ef82be182f209931e5326629df055 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Feb 2023 15:18:53 +1100 Subject: [PATCH 27/65] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e35a55965..626860e71 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Raise an error if EXIF data is too long when saving JPEG #6939 + [radarhere] + - Handle more than one directory returned by pkg-config #6896 [sebastic, radarhere] From da38395396c2f6322073a0f24a38b5780e46e89f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Feb 2023 21:56:23 +1100 Subject: [PATCH 28/65] Removed quotes from result in docstring --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a280935a5..63bad83a1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3636,7 +3636,7 @@ class Exif(MutableMapping): :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: print(exif[ExifTags.Base.Software]) # PIL - print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # '1999:99:99 99:99:99' + print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 """ endian = None From bbbb8e6e21108b13a3ae978b4aa0b783aa10bddf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Feb 2023 22:05:51 +1100 Subject: [PATCH 29/65] Updated harfbuzz to 7.0.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index ff95581fa..bef8afa9d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip", - "filename": "harfbuzz-6.0.0.zip", - "dir": "harfbuzz-6.0.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.0.zip", + "filename": "harfbuzz-7.0.0.zip", + "dir": "harfbuzz-7.0.0", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), From ad0e9dbaaf41e5c7abddf08d850cf0c0d98a3a92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Feb 2023 10:52:32 +1100 Subject: [PATCH 30/65] Fixed writing int as UNDEFINED tag --- Tests/test_file_tiff_metadata.py | 16 ++++++++++++++++ src/PIL/TiffImagePlugin.py | 2 ++ 2 files changed, 18 insertions(+) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index a4481d85f..fdabae3a3 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -216,6 +216,22 @@ def test_writing_other_types_to_bytes(value, tmp_path): assert reloaded.tag_v2[700] == b"\x01" +def test_writing_other_types_to_undefined(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[33723] + assert tag.type == TiffTags.UNDEFINED + + info[33723] = 1 + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[33723] == b"1" + + def test_undefined_zero(tmp_path): # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2cf5b173f..aaaf8fcb9 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -764,6 +764,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(7) def write_undefined(self, value): + if isinstance(value, int): + value = str(value).encode("ascii", "replace") return value @_register_loader(10, 8) From b7630bd675bd260dfac5eb53bc23afe1ab8e09d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Feb 2023 11:41:32 +1100 Subject: [PATCH 31/65] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 626860e71..aa03cfc40 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Fixed writing int as UNDEFINED tag #6950 + [radarhere] + - Raise an error if EXIF data is too long when saving JPEG #6939 [radarhere] From 06ba226e7b6b9fbcbf74839bf123b4095d6f9c92 Mon Sep 17 00:00:00 2001 From: James Zern Date: Tue, 14 Feb 2023 17:28:16 -0800 Subject: [PATCH 32/65] image-file-formats.rst: correct WebP quality range 0-100, not 1-100 --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a41ef7cf8..56937b5c7 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1126,7 +1126,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: If present and true, instructs the WebP writer to use lossless compression. **quality** - Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest + Integer, 0-100, Defaults to 80. For lossy, 0 gives the smallest size and 100 the largest. For lossless, this parameter is the amount of effort put into the compression: 0 is the fastest, but gives larger files compared to the slowest, but best, 100. From 8935dad32e31a121906a92bfc3eacdc3e410a711 Mon Sep 17 00:00:00 2001 From: James Zern Date: Tue, 14 Feb 2023 17:29:06 -0800 Subject: [PATCH 33/65] image-file-formats.rst: document WebP 'xmp' option --- docs/handbook/image-file-formats.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a41ef7cf8..a85ca59dd 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1147,6 +1147,10 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: The exif data to include in the saved file. Only supported if the system WebP library was built with webpmux support. +**xmp** + The XMP data to include in the saved file. Only supported if + the system WebP library was built with webpmux support. + Saving sequences ~~~~~~~~~~~~~~~~ From 0f2a4c1ae5d8492280af126dc7fda9eeef9b9d50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Feb 2023 19:19:17 +1100 Subject: [PATCH 34/65] Added "corners" argument to rounded_rectangle() --- ...agedraw_rounded_rectangle_corners_nnnn.png | Bin 0 -> 544 bytes ...agedraw_rounded_rectangle_corners_nnny.png | Bin 0 -> 685 bytes ...agedraw_rounded_rectangle_corners_nnyn.png | Bin 0 -> 649 bytes ...agedraw_rounded_rectangle_corners_nnyy.png | Bin 0 -> 755 bytes ...agedraw_rounded_rectangle_corners_nynn.png | Bin 0 -> 643 bytes ...agedraw_rounded_rectangle_corners_nyny.png | Bin 0 -> 775 bytes ...agedraw_rounded_rectangle_corners_nyyn.png | Bin 0 -> 741 bytes ...agedraw_rounded_rectangle_corners_nyyy.png | Bin 0 -> 844 bytes ...agedraw_rounded_rectangle_corners_ynnn.png | Bin 0 -> 656 bytes ...agedraw_rounded_rectangle_corners_ynny.png | Bin 0 -> 785 bytes ...agedraw_rounded_rectangle_corners_ynyn.png | Bin 0 -> 752 bytes ...agedraw_rounded_rectangle_corners_ynyy.png | Bin 0 -> 856 bytes ...agedraw_rounded_rectangle_corners_yynn.png | Bin 0 -> 737 bytes ...agedraw_rounded_rectangle_corners_yyny.png | Bin 0 -> 870 bytes ...agedraw_rounded_rectangle_corners_yyyn.png | Bin 0 -> 835 bytes ...agedraw_rounded_rectangle_corners_yyyy.png | Bin 0 -> 934 bytes Tests/test_imagedraw.py | 30 +++++ docs/reference/ImageDraw.rst | 2 + src/PIL/ImageDraw.py | 106 ++++++++++++------ 19 files changed, 104 insertions(+), 34 deletions(-) create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nynn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nyny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yynn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yyny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png new file mode 100644 index 0000000000000000000000000000000000000000..3e79e21aedb620a6d057f927610e1f1a791fd5da GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV0`ZB;uumf=j|0kzC#W?3d&6b&3+Jg_Su>JzjK?ym2`v- zDJkj*<%_Sao1Z3b)A4uC?n|AnP7x|CqXb1^;MZ!(KL6Y1T^!TqfCBT;qYZc3<~;7% zpmv|-{P9qaM|(CrdocO-gvmhJbv&mR9xY67H$BamTX;lO{hAK@mZM_tBUJQwvZpcg z>4wGDC7msDh^{HW8ftO6@$ZRl=Ii^C&z_pG?Z=nq>E9zIFN?|7eOp}+b}Xj*P1)|h zy!(>-eD}%EeY#Pnzb7*K%f?S;{CDaOsJGfS?rtxAWoWbf=A4}`z-oFP+I6(Ttoqx>dPD}f)_xZ95B<$(x=d#Wz Gp$P!^IRgOz literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png new file mode 100644 index 0000000000000000000000000000000000000000..d825ad2631717dc05270f8c211bae1793ce1c256 GIT binary patch literal 649 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU<&YbaSW-L^Y-S&pv49Z4i~R{ zJpB3juLnDJYq741xqiwrUsdwNVOgdU9-tN&_%TyY`tfILv4eYOg}ck`%NLx}xxs^@ zyJdri%{`OfnP#gW_{BWCH}hSDN{g%0C_!NuyeXZTUw?k7=XywB9@TsQ{h;olN5RjI z?Er;(uK8a6eF9>fi5nkYJN`}lP|{haQ2BlxL9y`KU8@vzlny1`49^R9j?np)6q?*~ z-1*wU^&9{2?Wic2YAseT8+uhhtX?MK>IPAxFuTS$>wV1|&SgKI{Oi{XvBbIO*6q*u zb|!MaS1seaGd9mmAKNWxb|y(>j|^&kMy5PUPrr z+2Fy^edK;dSP5 z4k>N4((yl>994Z^yk%>VQoBK(%y$iyjXG;SKApV$;l(w-sy|GY?LKmBLr&E3jc?=h z`CcdO+SZrwOfSC9IQ?v2sbOIB{@c%1slAVI32QUmSD$$K*V=bC*DcevzIL=E_pe=f zn5pmO%*UsSPuHx8nJsRz-}g*i<<)EDYj#e}iG829e$}$ward5IyL|2Ut&OWPe=q)U z?T}n|{*&WxA1r@A{mh+zms3w`ze&AYu_wm9s`y9j{nX4Xyte+pcH?+&cbiHAvXg)z4*}Q$iB}g)$Z3 literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png new file mode 100644 index 0000000000000000000000000000000000000000..f3e95d487498b4dec519e1b2e3745d5d1bda62f2 GIT binary patch literal 643 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIVDj>GaSW-L^Y-RJze5fp4uJ-B zKmJ(e`LN_>a_{Y^IJe1oW@6!IjfVw59Wc=Fyd)+}(xUT@FYN7BUz>5O#4|#r<*4tmL%TLaq`ubP1CnXk z5RrX3-#KxjfEec}K~WfVe)rwozxb!f9vNX^u!6%DmLh<3MVS20FPfX2{qNa=L_J;o KT-G@yGywp+4$-0j literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png new file mode 100644 index 0000000000000000000000000000000000000000..274d27984dcb7633223d2557d9d3cb44f109f99f GIT binary patch literal 775 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU^?gN;uumf=k3jdMT;Cn90KR+ zzqc##~$aFue*C+9jjj@ z{aLwZm9B6%|FOh!qm4fjWXhJ;3coJ8bFgOW=NknVr)B5O-}IUxaqpz5hw|R9yH}I? z|JS=iRi53gx@+?v$aY^&)#>TEc{I1>sQvM6syhFyH|Qo7rvBUbqcXL3!;Fs=(T9@m zHRp16w`_>`a^ixJ*!DYjr)`KRyuUxX#nmZd&1}B4hUFJc6DJCYl}l>>K9n?h@A?mi zloWM@4kfLQ-Ot+!RHOn@IZD97;MyUn@cHMGzbVZy+UUX2edOH+8{V5q&JoK0h4#qx zI|` v(89Ixy0atZ_db_BI=N^4Kcr-NAp1ABZ;XS5VFODSD5ZM3`njxgN@xNAqD46+ literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png new file mode 100644 index 0000000000000000000000000000000000000000..c5f40bfdbdd968e3392c9ec9e4103595519afe01 GIT binary patch literal 741 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU|Qqp;uumf=j~0$pxXuv4uO|n z9xN^1)gZ>y_f+Lyz@+bG6Ky7m*QcmQiUIY&zz=&{-)G0e@36#Y)f`njRF$(h>PPri z1N%cOZWSecJZF)ksxJ1v=Wy7rH%F@M@{;$RzI>zb;=Jr_9iOS3Tv{NX`0>?^Z7oMNa8$bSDxl1NnTV^80#9uEDEwmQN7u@%rJLU)^cvO>ghMBK_fTPx7>n zpUzo7-Z0gM%X6MLI6Xx~^rzQ+=Y^#>ApPbGyT~5PQvq{CEI^{3u6{1-oD!M5dtaSW-L^Y*59(JcoNhd@j1 z_j9eARYVP#YS-Hroa6D_v#cvxH_=}hs09XU{PVe=A5X90u)F%B$F=3_EjQC2;ifa> z6GL)uZrowuH#_C{qeB+Cm$u2i{=293r&fOQQP=Esm3vMtV{Q5BBgR!-Q&#xn(9hQ| z4^?@UbMG#%VLcz=^ZxOg|Trg8C01Z?}J@_v?6{t5bxES^LwxAG5aAz1Kc; z%VOem1F>_vMJGjwU)xay6wfmIEBEJcpKDTjgxT8b8{hAalf8DfBzJGq%>19Qyf<9R7W$>ejEFervLB)oaH@-N^mOzH_(i9qWB*&;D&+ z7af;)|JtFqPe0x~`mp`p-lxtQZ)Qh)-LdJ}tmCU?WD7FqSoW+pJSSQFD*oQ_nTLNc zJ>KHE{9VL!H`5uPdq2CUCH^(M^-;!j)xpAu>BpYy9#ww4+8&m$f%E~d{fw-szSh0M QadSYzp00i_>zopr09Uk5F8}}l literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png new file mode 100644 index 0000000000000000000000000000000000000000..efd27be4f9df7c8c7733920dbe1330f938a4c2f5 GIT binary patch literal 656 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU<&tiaSW-L^Y+HUzFQ6=42~V& z4&R@z-mWq6+Q%l|Jf@thXZt6NNuH)Se~<8C=xUyF(VRD4)!-kr*A>ui+Pbn{=n{Jo?+Vzb?~ z%jV_r5u1%Bi!9%5cP~A4nO5Cy(>0MZS9a~sv*Jp(oFKyeyL?R~U&zt@Wo@RX8&^e? zzOaA);rP4_X_gyeKTbWHc(!PPmfnt!8X=<7e;!lXsJrH8N$T!LTGnEnyotX5g!at8 zYdx)ag04)fN{j2GsEE%mPg$ODE7K7=q@*}XP!t9qH)emWoVESI+Z!7_IJ#RlcyMsT f5(fiAL;6?lyA?{unZGam3ljBo^>bP0l+XkKcMjwy literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png new file mode 100644 index 0000000000000000000000000000000000000000..d3acd01abe04c4577f0b930dda7611f10912112c GIT binary patch literal 785 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV7lSy;uumf=k3jddAA%y90FJT zdiehQZPzO+EZBH{Olq04`SHz98+ObpJn;~y2M*#df2{OA`+3?U&u4F@-k-nKV7)GP z_A2(zx^HB!z4>FaZ)JS7UCy@}iTk(wtgprFe|o;H`p2Wb&0o*i9@@2I#=N>^?T2T5 z-8ipq$MHk6rp{bqVe{paowjl4$7lC8q@^y?x_7r!Tg)=9z5wk!>5EJ(>KrYxSE56|)x8pVJ?F zy|X9TlKtkPKo6$~m36JA>htF`=cF80Qq&PTG)f?a!SzG4Ci@?c+{-x)7&ZVun{U$k#q-Iko=zcS`+7x6cm|Cbe>IvE#L^%Ke literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png new file mode 100644 index 0000000000000000000000000000000000000000..55ddbc033fbeea4f13492b24372bd501975c994e GIT binary patch literal 752 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIVA}5K;uumf=j~0$pv4Xh4uNw% zJ*fRWvvsN81P0F^7iWAgGYL8TDBN7}G7C@-4A@^j{Ws6_UAgwVlsUGaf5ngo}jmc@sq*v}tt{#E=W#o^opr*NOcHF(=hi0s*D9(*YPV@<_*^#9!^O@1pu=r<}=`-e4k5+RZmio*X zy79#Y{r4ZexoEl0O?*YWQY6yr?X`0Z{~-R&N_w0yHDfj-n?#ImXMfW;>I`Ao&p)&d26y} zExYbKuQ@*Xr}=|N545z!=J%}v%EtIlAR*N!ef5 z&Ch3j-F?OU!NH#7WgkDCt3Q@#e3Q%bn`)x%_CQ~qHP4Or{bqzEJ|JE8i~Wb8(fz*) Q5l29xp00i_>zopr07Mr|_y7O^ literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png new file mode 100644 index 0000000000000000000000000000000000000000..c000b26e9671cd748319fbab7e8921d6a98ef3ae GIT binary patch literal 856 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV3zlEaSW-L^Y+Hwyu}70Zh=O# z*zNQ0?Ob7%)xpg6=V|}ecLghCJ9THL%>wFyfd{6sa@#{}^EBm_ysP{4J(BzL&7{Q8 ztfYH~UTnzvP_t*Hy|Vi1iIYEGV|yF1weEEIaoasRyuaSFR9bWN=$8_|_QR{bZJ6ix zE+XQp#AK1n3#Wh2>RH*9UwwGhu^Fp=~sI?t3b; zqg%ov^T3lN;bO?wE9vT&d1;~-8h|<#1mCtAq6q#oO#|zgwmDKB6UuXZ15?v$g)EDZ6KVdjnSY zS!PYvtYxRqO^rTXlN&Qz{K`GGSEQ&`{hr~cvU_Ft|??`t^}=W_kE*LmC1t7Yas*e+!$ zc|I_CndGn2)xC3i)K?@L^WNQ9xBAMFp76)Zds@G)-E_G)WT_fo``2h-dY*P=^L|(& c2htA9>tEb*3t(Q@KN}?K>FVdQ&MBb@08oo#djJ3c literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png new file mode 100644 index 0000000000000000000000000000000000000000..7056b4fd9347bcce6ce3ddc9d0735efd12b5d0c4 GIT binary patch literal 737 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU|Qkn;uumf=j{#ezDEus42}|K z559h8ygKfJ*@Ji`9zkoLk6b5L8r_}F4b%bydzQa`y-jNU?JoCY+Gd@HrQTM2le+pq zXH6jQvx>(*Cc3wq^36N$-FsN=uVv09*U;zYe6vd~behi7|G7PZSHD1(TTD0dbH((% z%=2nFZTGHKZ?&!a{_;cY{C)fP&hvYcm-v01{>+_!7nhq&yW)BFxoLG_qpbbC=i5TA zR-V1T`(`(H4sUjzedye~jk&p1B7S9)uC&jr)bU$yv*q8BSvd<OMTnC*-R6jXT$lxi4I`@A$Urx8`Ddczh7^+(T08hSCOU69s?-BP_WwFnn0|kNK9j%}TLfU4=a(s-@L${y(L<-PHR0aqqKv#(y{8_F)g*yZ>hHvbkkzcGjO>FZ%rh)A7i{%d@KU z)_;1#@_k>+yt*~kt#|iTm48}Z_v_cEsw(Ah?%ihl(#{>P|8nZkq^jW4mAZWEJ{&py z`_iOURq?0K=M@&Tx*v{;@(+EVcYK?i6_{80-+gHmAnw+#}zDbA7H(5h`ZwPfb#KMH7Ahss6C(a}s(D3O+yQ zcbk$YRv75a7Tdm-FZ&$(mZQAUWosjI< z#yxIM#TVIZv140~#{2KtbN*LI%ynd{gTe~DWM4fqXlD9 literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png new file mode 100644 index 0000000000000000000000000000000000000000..7f1f003440063facdb2a8ae9bb6455d62b5f1b0b GIT binary patch literal 835 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIVCM34aSW-L^Y-TLqFV+G4vxz& z^IxC8oqy&QUxR%O{Ja(4d2ddf@llfVgb5>16BHcy{^i?_rNGpZ6=G|?~y;~v@w?<{9xN)&gT%XRZ{mHR93szk>{_Lag zF7}q^by8XA-c{DOt-eez|FJ=R&BxLMWnXW-ySc@zz0`2=s&fyXZOgOa3N4k}Zks)S zqel*Rc1>04ZmZer{cFzL{W4Rw`$*dcw|{CCcF9kFy^}p{S>abbfA``EEgL-6w3^!O zE#jMZbKU#4G995qN{XWdN*HXblAa&mzjL-7M>jCIIJ$lJ&1=qXIqG}tRB7Tw0kPL* zwemIUz%WZXdawWO`JO{=uCm%MWtMrNFZA{Hn$kOmx$mrcylh_ev~WwS zgty)=UgoY9+4kq%s}Iie>-O!L=l3Kt@%*~)XYTyJsLt*EYRRoiQ~ffF50Ce5U9#%j z>c4MhiCe$nUA=YQs&jb>(YyN&-SnFD%b+*8urBrDH`(lM4Xf<@AJ06uUgws?wVYk6 z?5-Z%*7onYY+VlLYB~KI>+Eh%^j&Se=B7(%?7?^2%H&#C?dq#Ne=A={>DQrEyTiAw zUR_!9=<2SQ^JKd_`Zj3&Q?-~MRayVJ@2Sk5Woysh4fJT)&=JAC`dyqQ`}&;A_uI;J zlv-SYVgh1pg*rZu#DecshQo!%8SzkmVTE(;2wjg~sgADZUY zzLQPNopVC<%BkOdPLLSzJG}blm+ueEt;Gbb4{yqhJ-#pft}HMnx2;?7amw@XsBLc| zHh13om9u;GtUt$;Qj#OSRvcXRb!A^p_~m)B*=DiP^PeY#UN*T~w(d^Uv+T#W_ALBn zc5%;+jQ9C#u6b=+w^=&$YRQY^)!DYWbFUv*wd}9^!&`^?jtW=qlC{}=&$#%%`|oMt zH*QB+#!b5)D_i&Ca`e8IL-$;+pPvdzun)FNS@!G?Og_f*_k4A)%rW^DiIdyjMeGmF zGI)IT$Ha{r_Jx+&xQAvHSgg4Z&K>`Lal^6= x1 - x0 - if full_x: - # The two left and two right corners are joined - d = x1 - x0 - full_y = d >= y1 - y0 - if full_y: - # The two top and two bottom corners are joined - d = y1 - y0 - if full_x and full_y: - # If all corners are joined, that is a circle - return self.ellipse(xy, fill, outline, width) + full_x, full_y = False, False + if all(corners): + full_x = d >= x1 - x0 + if full_x: + # The two left and two right corners are joined + d = x1 - x0 + full_y = d >= y1 - y0 + if full_y: + # The two top and two bottom corners are joined + d = y1 - y0 + if full_x and full_y: + # If all corners are joined, that is a circle + return self.ellipse(xy, fill, outline, width) - if d == 0: - # If the corners have no curve, that is a rectangle + if d == 0 or not any(corners): + # If the corners have no curve, + # or there are no corners, + # that is a rectangle return self.rectangle(xy, fill, outline, width) r = d // 2 @@ -338,12 +346,17 @@ class ImageDraw: ) else: # Draw four separate corners - parts = ( - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), - ((x0, y0, x0 + d, y0 + d), 180, 270), - ) + parts = [] + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) + ): + if corners[i]: + parts.append(part) for part in parts: if pieslice: self.draw.draw_pieslice(*(part + (fill, 1))) @@ -358,25 +371,50 @@ class ImageDraw: else: self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) if not full_x and not full_y: - self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1) - self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1) + left = [x0, y0, x0 + r, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, fill, 1) + + right = [x1 - r, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, fill, 1) if ink is not None and ink != fill and width != 0: draw_corners(False) if not full_x: - self.draw.draw_rectangle( - (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1 - ) + top = [x0, y0, x1, y0 + width - 1] + if corners[0]: + top[0] += r + 1 + if corners[1]: + top[2] -= r + 1 + self.draw.draw_rectangle(top, ink, 1) + + bottom = [x0, y1 - width + 1, x1, y1] + if corners[3]: + bottom[0] += r + 1 + if corners[2]: + bottom[2] -= r + 1 + self.draw.draw_rectangle(bottom, ink, 1) if not full_y: - self.draw.draw_rectangle( - (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1 - ) + left = [x0, y0, x0 + width - 1, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, ink, 1) + + right = [x1 - width + 1, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, ink, 1) def _multiline_check(self, text): """Draw text.""" From 60208a325083579361eaecc6d388e265aa52bea6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Feb 2023 10:32:55 +1100 Subject: [PATCH 35/65] Only allow "corners" to be used as a keyword argument --- src/PIL/ImageDraw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index dc77c06ea..a55ebbe8e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -296,7 +296,7 @@ class ImageDraw: self.draw.draw_rectangle(xy, ink, 0, width) def rounded_rectangle( - self, xy, radius=0, fill=None, outline=None, width=1, corners=None + self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None ): """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): From a55c2b42b9fdeb395cdc387ea768eb12e9a547bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 Feb 2023 20:34:52 +1100 Subject: [PATCH 36/65] If following colon, replace Python code-blocks with double colons --- docs/deprecations.rst | 16 +++-------- docs/handbook/image-file-formats.rst | 4 +-- .../writing-your-own-image-plugin.rst | 12 ++------ docs/reference/Image.rst | 28 +++++-------------- docs/reference/ImagePath.rst | 4 +-- docs/reference/ImageWin.rst | 4 +-- docs/reference/open_files.rst | 4 +-- docs/releasenotes/6.1.0.rst | 12 ++------ docs/releasenotes/7.0.0.rst | 12 ++------ docs/releasenotes/9.0.0.rst | 4 +-- docs/releasenotes/9.2.0.rst | 8 ++---- winbuild/build.rst | 4 +-- 12 files changed, 28 insertions(+), 84 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4d48b822a..0db19a64e 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -177,9 +177,7 @@ Deprecated Use :py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -194,9 +192,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont @@ -336,16 +332,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..d6b42589a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1393,9 +1393,7 @@ WMF, EMF Pillow can identify WMF and EMF files. On Windows, it can read WMF and EMF files. By default, it will load the image -at 72 dpi. To load it at another resolution: - -.. code-block:: python +at 72 dpi. To load it at another resolution:: from PIL import Image diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 59dfac588..75604e17a 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -108,9 +108,7 @@ Note that the image plugin must be explicitly registered using :py:func:`PIL.Image.register_open`. Although not required, it is also a good idea to register any extensions used by this format. -Once the plugin has been imported, it can be used: - -.. code-block:: python +Once the plugin has been imported, it can be used:: from PIL import Image import SpamImagePlugin @@ -169,9 +167,7 @@ The raw decoder The ``raw`` decoder is used to read uncompressed data from an image file. It can be used with most uncompressed file formats, such as PPM, BMP, uncompressed TIFF, and many others. To use the raw decoder with the -:py:func:`PIL.Image.frombytes` function, use the following syntax: - -.. code-block:: python +:py:func:`PIL.Image.frombytes` function, use the following syntax:: image = Image.frombytes( mode, size, data, "raw", @@ -281,9 +277,7 @@ decoder that can be used to read various packed formats into a floating point image memory. To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use -the following syntax: - -.. code-block:: python +the following syntax:: image = Image.frombytes( mode, size, data, "bit", diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index ad0abbbd9..976b148fc 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -127,9 +127,7 @@ methods. Unless otherwise stated, all methods return a new instance of the .. automethod:: PIL.Image.Image.convert The following example converts an RGB image (linearly calibrated according to -ITU-R 709, using the D65 luminant) to the CIE XYZ color space: - -.. code-block:: python +ITU-R 709, using the D65 luminant) to the CIE XYZ color space:: rgb2xyz = ( 0.412453, 0.357580, 0.180423, 0, @@ -140,9 +138,7 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.copy .. automethod:: PIL.Image.Image.crop -This crops the input image with the provided coordinates: - -.. code-block:: python +This crops the input image with the provided coordinates:: from PIL import Image @@ -162,9 +158,7 @@ This crops the input image with the provided coordinates: .. automethod:: PIL.Image.Image.entropy .. automethod:: PIL.Image.Image.filter -This blurs the input image using a filter from the ``ImageFilter`` module: - -.. code-block:: python +This blurs the input image using a filter from the ``ImageFilter`` module:: from PIL import Image, ImageFilter @@ -176,9 +170,7 @@ This blurs the input image using a filter from the ``ImageFilter`` module: .. automethod:: PIL.Image.Image.frombytes .. automethod:: PIL.Image.Image.getbands -This helps to get the bands of the input image: - -.. code-block:: python +This helps to get the bands of the input image:: from PIL import Image @@ -187,9 +179,7 @@ This helps to get the bands of the input image: .. automethod:: PIL.Image.Image.getbbox -This helps to get the bounding box coordinates of the input image: - -.. code-block:: python +This helps to get the bounding box coordinates of the input image:: from PIL import Image @@ -217,9 +207,7 @@ This helps to get the bounding box coordinates of the input image: .. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.resize -This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``: - -.. code-block:: python +This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``:: from PIL import Image @@ -231,9 +219,7 @@ This resizes the given image from ``(width, height)`` to ``(width/2, height/2)`` .. automethod:: PIL.Image.Image.rotate -This rotates the input image by ``theta`` degrees counter clockwise: - -.. code-block:: python +This rotates the input image by ``theta`` degrees counter clockwise:: from PIL import Image diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index b9bdfc507..7c1a3ad70 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -60,9 +60,7 @@ vector data. Path objects can be passed to the methods on the .. py:method:: PIL.ImagePath.Path.transform(matrix) Transforms the path in place, using an affine transform. The matrix is a - 6-tuple (a, b, c, d, e, f), and each point is mapped as follows: - - .. code-block:: python + 6-tuple (a, b, c, d, e, f), and each point is mapped as follows:: xOut = xIn * a + yIn * b + c yOut = xIn * d + yIn * e + f diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 2ee3cadb7..4151be4a7 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -9,9 +9,7 @@ Windows. ImageWin can be used with PythonWin and other user interface toolkits that provide access to Windows device contexts or window handles. For example, -Tkinter makes the window handle available via the winfo_id method: - -.. code-block:: python +Tkinter makes the window handle available via the winfo_id method:: from PIL import ImageWin diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 6bfd50588..f31941c9a 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -61,9 +61,7 @@ Image Lifecycle * ``Image.Image.close()`` Closes the file and destroys the core image object. The Pillow context manager will also close the file, but will not destroy - the core image object. e.g.: - -.. code-block:: python + the core image object. e.g.:: with Image.open("test.jpg") as img: img.load() diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index eb4304843..76e13b061 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -13,16 +13,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been dep Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Deprecated: - -.. code-block:: python +Deprecated:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") @@ -79,9 +75,7 @@ Image quality for JPEG compressed TIFF The TIFF encoder accepts a ``quality`` parameter for ``jpeg`` compressed TIFF files. A value from 0 (worst) to 100 (best) controls the image quality, similar to the JPEG -encoder. The default is 75. For example: - -.. code-block:: python +encoder. The default is 75. For example:: im.save("out.tif", compression="jpeg", quality=85) diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 80002b0ce..f2e235289 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -118,9 +118,7 @@ Loading WMF images at a given DPI ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ On Windows, Pillow can read WMF files, with a default DPI of 72. An image can -now also be loaded at another resolution: - -.. code-block:: python +now also be loaded at another resolution:: from PIL import Image with Image.open("drawing.wmf") as im: @@ -136,16 +134,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call :py:meth:`~PIL.Image.Image.close` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index a19da361a..616cf4aa3 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -155,9 +155,7 @@ altered slightly with this change. Added support for pickling TrueType fonts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TrueType fonts may now be pickled and unpickled. For example: - -.. code-block:: python +TrueType fonts may now be pickled and unpickled. For example:: import pickle from PIL import ImageFont diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 6dbfa2702..3dfb25840 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -59,9 +59,7 @@ Deprecated Use :py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -76,9 +74,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont diff --git a/winbuild/build.rst b/winbuild/build.rst index 716669771..d4275a274 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -96,9 +96,7 @@ directory. Example ------- -The following is a simplified version of the script used on AppVeyor: - -.. code-block:: +The following is a simplified version of the script used on AppVeyor:: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild From 43682de4bdb6c5f93340107386f86becbfbad25a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 21 Feb 2023 08:12:57 +1100 Subject: [PATCH 37/65] Updated harfbuzz to 7.0.1 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index bef8afa9d..35980f19c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.0.zip", - "filename": "harfbuzz-7.0.0.zip", - "dir": "harfbuzz-7.0.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.1.zip", + "filename": "harfbuzz-7.0.1.zip", + "dir": "harfbuzz-7.0.1", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), From 36bcc0a89866bf9ca3f79c61470d9ab4a58d74ec Mon Sep 17 00:00:00 2001 From: Jasper van der Neut Date: Tue, 21 Feb 2023 10:34:41 +0100 Subject: [PATCH 38/65] Support saving PDF with different X and Y resolution. Add a `dpi` parameter to the PDF save function, which accepts a tuple with X and Y dpi. This is useful for converting tiffg3 (fax) images to pdf, which have split dpi like (204,391), (204,196) or (204,98). --- Tests/test_file_pdf.py | 42 ++++++++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 5 ++++ src/PIL/PdfImagePlugin.py | 19 ++++++++----- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 216b93ca9..a45b22bf6 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -80,6 +80,48 @@ def test_resolution(tmp_path): assert size == (61.44, 61.44) +def test_dpi(tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, dpi=(75, 150)) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + +def test_resolution_and_dpi(tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, resolution=200, dpi=(75, 150)) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..081b84963 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1497,6 +1497,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum image, will determine the physical dimensions of the page that will be saved in the PDF. +**dpi** + A tuple of (x_resolution, y_resolution), with inches as the resolution + unit. If both the ``resolution`` parameter and the ``dpi`` parameter are + present, ``resolution`` will be ignored. + **title** The document’s title. If not appending to an existing PDF file, this will default to the filename. diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index baad4939f..d4f1ef93a 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -53,7 +53,12 @@ def _save(im, fp, filename, save_all=False): else: existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - resolution = im.encoderinfo.get("resolution", 72.0) + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) + + dpi = im.encoderinfo.get("dpi") + if dpi: + x_resolution = dpi[0] + y_resolution = dpi[1] info = { "title": None @@ -214,8 +219,8 @@ def _save(im, fp, filename, save_all=False): stream=stream, Type=PdfParser.PdfName("XObject"), Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / resolution, - Height=height, # * 72.0 / resolution, + Width=width, # * 72.0 / x_resolution, + Height=height, # * 72.0 / y_resolution, Filter=filter, BitsPerComponent=bits, Decode=decode, @@ -235,8 +240,8 @@ def _save(im, fp, filename, save_all=False): MediaBox=[ 0, 0, - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ], Contents=contents_refs[page_number], ) @@ -245,8 +250,8 @@ def _save(im, fp, filename, save_all=False): # page contents page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ) existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) From be489287d2d8923ad57695f1bdf0d28db8be5757 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Feb 2023 18:59:51 +1100 Subject: [PATCH 39/65] Parametrized test --- Tests/test_file_pdf.py | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index a45b22bf6..2afec9960 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -80,32 +80,18 @@ def test_resolution(tmp_path): assert size == (61.44, 61.44) -def test_dpi(tmp_path): +@pytest.mark.parametrize( + "params", + ( + {"dpi": (75, 150)}, + {"dpi": (75, 150), "resolution": 200}, + ), +) +def test_dpi(params, tmp_path): im = hopper() outfile = str(tmp_path / "temp.pdf") - im.save(outfile, dpi=(75, 150)) - - with open(outfile, "rb") as fp: - contents = fp.read() - - size = tuple( - float(d) - for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") - ) - assert size == (122.88, 61.44) - - size = tuple( - float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() - ) - assert size == (122.88, 61.44) - - -def test_resolution_and_dpi(tmp_path): - im = hopper() - - outfile = str(tmp_path / "temp.pdf") - im.save(outfile, resolution=200, dpi=(75, 150)) + im.save(outfile, **params) with open(outfile, "rb") as fp: contents = fp.read() From 0d667f5e0b756844e22dbb78b71c84279992ca2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Feb 2023 21:12:11 +1100 Subject: [PATCH 40/65] Do not read "resolution" parameter if it will not be used --- src/PIL/PdfImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index d4f1ef93a..4fa1998ba 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -53,12 +53,12 @@ def _save(im, fp, filename, save_all=False): else: existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) - dpi = im.encoderinfo.get("dpi") if dpi: x_resolution = dpi[0] y_resolution = dpi[1] + else: + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) info = { "title": None From 21d13e1dea032d734572a9bb954c827aba2b29d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Feb 2023 23:22:18 +1100 Subject: [PATCH 41/65] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 34609314c..f36ad5d46 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_similar(result, expected.convert("RGB"), 0.3) + assert_image_similar(result, expected.convert("RGB"), 0.5) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From acc7e0b469ac973bf1ab79bf8369a8b13769d169 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:00:24 +1100 Subject: [PATCH 42/65] Highlight code example --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..9a3ddcab7 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1104,7 +1104,7 @@ using the general tags available through tiffinfo. Either an integer or a float. **dpi** - A tuple of (x_resolution, y_resolution), with inches as the resolution + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution unit. For consistency with other image formats, the x and y resolutions of the dpi will be rounded to the nearest integer. From 742aff3718cebdad390ad808e3cae181ea749e27 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:17:10 +1100 Subject: [PATCH 43/65] Replace Python code-blocks with double colons --- docs/handbook/image-file-formats.rst | 4 +-- docs/handbook/text-anchors.rst | 2 +- docs/reference/Image.rst | 12 ++----- docs/reference/ImageDraw.rst | 18 ++++------ docs/reference/ImageEnhance.rst | 2 +- docs/reference/ImageFile.rst | 2 +- docs/reference/ImageFilter.rst | 2 +- docs/reference/ImageFont.rst | 2 +- docs/reference/ImageMath.rst | 2 +- docs/reference/ImageSequence.rst | 2 +- docs/reference/PixelAccess.rst | 8 ++--- docs/reference/PyAccess.rst | 8 ++--- docs/releasenotes/6.2.0.rst | 8 ++--- docs/releasenotes/7.1.0.rst | 4 +-- docs/releasenotes/8.2.0.rst | 4 +-- docs/releasenotes/8.4.0.rst | 4 +-- docs/releasenotes/9.1.0.rst | 8 ++--- src/PIL/ImageChops.py | 52 +++++++--------------------- src/PIL/ImageFont.py | 12 ++----- 19 files changed, 44 insertions(+), 112 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index d6b42589a..4c2af3db8 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1402,9 +1402,7 @@ at 72 dpi. To load it at another resolution:: To add other read or write support, use :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF -handler. - -.. code-block:: python +handler. :: from PIL import Image from PIL import WmfImagePlugin diff --git a/docs/handbook/text-anchors.rst b/docs/handbook/text-anchors.rst index 0aecd3483..3a9572ab2 100644 --- a/docs/handbook/text-anchors.rst +++ b/docs/handbook/text-anchors.rst @@ -29,7 +29,7 @@ For example, in the following image, the text is ``ms`` (middle-baseline) aligne :alt: ms (middle-baseline) aligned text. :align: left -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 976b148fc..0eba1141a 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -17,9 +17,7 @@ Open, rotate, and display an image (using the default viewer) The following script loads an image, rotates it 45 degrees, and displays it using an external viewer (usually xv on Unix, and the Paint program on -Windows). - -.. code-block:: python +Windows). :: from PIL import Image with Image.open("hopper.jpg") as im: @@ -29,9 +27,7 @@ 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 +current directory preserving aspect ratios with 128x128 max resolution. :: from PIL import Image import glob, os @@ -242,9 +238,7 @@ This rotates the input image by ``theta`` degrees counter clockwise:: .. automethod:: PIL.Image.Image.transpose This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT` -method. - -.. code-block:: python +method. :: from PIL import Image diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 9aa26916a..e325a0280 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -16,7 +16,7 @@ For a more advanced drawing library for PIL, see the `aggdraw module`_. Example: Draw a gray cross over an image ---------------------------------------- -.. code-block:: python +:: import sys from PIL import Image, ImageDraw @@ -78,7 +78,7 @@ libraries, and may not available in all PIL builds. Example: Draw Partial Opacity Text ---------------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -105,7 +105,7 @@ Example: Draw Partial Opacity Text Example: Draw Multiline Text ---------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -597,18 +597,14 @@ Methods string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = draw.textlength("Hello", font) world = draw.textlength("World", font) hello_world = hello + world # not adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # may fail - use - - .. code-block:: python + use :: hello = draw.textlength("HelloW", font) - draw.textlength( "W", font @@ -617,9 +613,7 @@ Methods hello_world = hello + world # adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 29ceee314..b27228ec9 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -10,7 +10,7 @@ for image enhancement. Example: Vary the sharpness of an image --------------------------------------- -.. code-block:: python +:: from PIL import ImageEnhance diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 3cf59c610..047990f1c 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -15,7 +15,7 @@ and **xmllib** modules. Example: Parse an image ----------------------- -.. code-block:: python +:: from PIL import ImageFile diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index c85da4fb5..044aede62 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -11,7 +11,7 @@ filters, which can be be used with the :py:meth:`Image.filter() Example: Filter an image ------------------------ -.. code-block:: python +:: from PIL import ImageFilter diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 516fa63a7..946bd3c4b 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -21,7 +21,7 @@ the imToolkit package. Example ------- -.. code-block:: python +:: from PIL import ImageFont, ImageDraw diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 63f88fddd..118d988d6 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -11,7 +11,7 @@ an expression string and one or more images. Example: Using the :py:mod:`~PIL.ImageMath` module -------------------------------------------------- -.. code-block:: python +:: from PIL import Image, ImageMath diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index f2e7d9edd..a27b2fb4e 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -10,7 +10,7 @@ iterate over the frames of an image sequence. Extracting frames from an animation ----------------------------------- -.. code-block:: python +:: from PIL import Image, ImageSequence diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index b234b7b4e..04d6f5dcd 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -18,9 +18,7 @@ Example ------- The following script loads an image, accesses one pixel from it, then -changes it. - -.. code-block:: python +changes it. :: from PIL import Image @@ -35,9 +33,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index f9eb9b524..ed58ca3a5 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -17,9 +17,7 @@ The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the Example ------- -The following script loads an image, accesses one pixel from it, then changes it. - -.. code-block:: python +The following script loads an image, accesses one pixel from it, then changes it. :: from PIL import Image @@ -34,9 +32,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 20a009cc1..0fb33de75 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -10,9 +10,7 @@ Text stroking ``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing operations. They allow text to be outlined, setting the width of the stroke and and the color respectively. If not provided, ``stroke_fill`` will default to -the ``fill`` parameter. - -.. code-block:: python +the ``fill`` parameter. :: from PIL import Image, ImageDraw, ImageFont @@ -28,9 +26,7 @@ the ``fill`` parameter. draw.multiline_text((10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0") -For example, - -.. code-block:: python +For example, :: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index 0024a537d..cb46f127c 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -10,9 +10,7 @@ Allow saving of zero quality JPEG images If no quality was specified when saving a JPEG, Pillow internally used a value of zero to indicate that the default quality should be used. However, this removed the ability to actually save a JPEG with zero quality. This has now -been resolved. - -.. code-block:: python +been resolved. :: from PIL import Image im = Image.open("hopper.jpg") diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index c902ccf71..f11953168 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -76,9 +76,7 @@ ImageDraw.rounded_rectangle Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as :py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius`` argument. ``radius`` is limited to half of the width or the height, so that users can -create a circle, but not any other ellipse. - -.. code-block:: python +create a circle, but not any other ellipse. :: from PIL import Image, ImageDraw im = Image.new("RGB", (200, 200)) diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst index 9becf9146..e61471e72 100644 --- a/docs/releasenotes/8.4.0.rst +++ b/docs/releasenotes/8.4.0.rst @@ -24,9 +24,7 @@ Added "transparency" argument for loading EPS images This new argument switches the Ghostscript device from "ppmraw" to "pngalpha", generating an RGBA image with a transparent background instead of an RGB image with a -white background. - -.. code-block:: python +white background. :: with Image.open("sample.eps") as im: im.load(transparency=True) diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index e97b58a41..19690ca59 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -182,17 +182,13 @@ GifImagePlugin loading strategy Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as -well. - -.. code-block:: python +well. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS Or subsequent frames can be kept in ``P`` mode as long as there is only a single -palette. - -.. code-block:: python +palette. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index fec4694b2..701200317 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -38,9 +38,7 @@ def duplicate(image): def invert(image): """ - Invert an image (channel). - - .. code-block:: python + Invert an image (channel). :: out = MAX - image @@ -54,9 +52,7 @@ def invert(image): def lighter(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the lighter values. - - .. code-block:: python + the lighter values. :: out = max(image1, image2) @@ -71,9 +67,7 @@ def lighter(image1, image2): def darker(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the darker values. - - .. code-block:: python + the darker values. :: out = min(image1, image2) @@ -88,9 +82,7 @@ def darker(image1, image2): def difference(image1, image2): """ Returns the absolute value of the pixel-by-pixel difference between the two - images. - - .. code-block:: python + images. :: out = abs(image1 - image2) @@ -107,9 +99,7 @@ def multiply(image1, image2): Superimposes two images on top of each other. If you multiply an image with a solid black image, the result is black. If - you multiply with a solid white image, the image is unaffected. - - .. code-block:: python + you multiply with a solid white image, the image is unaffected. :: out = image1 * image2 / MAX @@ -123,9 +113,7 @@ def multiply(image1, image2): def screen(image1, image2): """ - Superimposes two inverted images on top of each other. - - .. code-block:: python + Superimposes two inverted images on top of each other. :: out = MAX - ((MAX - image1) * (MAX - image2) / MAX) @@ -176,9 +164,7 @@ def overlay(image1, image2): def add(image1, image2, scale=1.0, offset=0): """ Adds two images, dividing the result by scale and adding the - offset. If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + offset. If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 + image2) / scale + offset) @@ -193,9 +179,7 @@ def add(image1, image2, scale=1.0, offset=0): def subtract(image1, image2, scale=1.0, offset=0): """ Subtracts two images, dividing the result by scale and adding the offset. - If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 - image2) / scale + offset) @@ -208,9 +192,7 @@ def subtract(image1, image2, scale=1.0, offset=0): def add_modulo(image1, image2): - """Add two images, without clipping the result. - - .. code-block:: python + """Add two images, without clipping the result. :: out = ((image1 + image2) % MAX) @@ -223,9 +205,7 @@ def add_modulo(image1, image2): def subtract_modulo(image1, image2): - """Subtract two images, without clipping the result. - - .. code-block:: python + """Subtract two images, without clipping the result. :: out = ((image1 - image2) % MAX) @@ -243,9 +223,7 @@ def logical_and(image1, image2): Both of the images must have mode "1". If you would like to perform a logical AND on an image with a mode other than "1", try :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask - as the second image. - - .. code-block:: python + as the second image. :: out = ((image1 and image2) % MAX) @@ -260,9 +238,7 @@ def logical_and(image1, image2): def logical_or(image1, image2): """Logical OR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((image1 or image2) % MAX) @@ -277,9 +253,7 @@ def logical_or(image1, image2): def logical_xor(image1, image2): """Logical XOR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((bool(image1) != bool(image2)) % MAX) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index bd13c391e..173b2926f 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -297,27 +297,21 @@ class FreeTypeFont: string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = font.getlength("Hello") world = font.getlength("World") hello_world = hello + world # not adjusted for kerning assert hello_world == font.getlength("HelloWorld") # may fail - use - - .. code-block:: python + use :: hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning world = font.getlength("World") hello_world = hello + world # adjusted for kerning assert hello_world == font.getlength("HelloWorld") # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) From 19299acbb6688d2aaf33e53f64c9cbb04545e274 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:39:48 +1100 Subject: [PATCH 44/65] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index f36ad5d46..4929fa933 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_similar(result, expected.convert("RGB"), 0.5) + assert_image_similar(result, expected.convert("RGB"), 1) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From 3c3d88845072b0b8b8a9a00787fb03d733def3b7 Mon Sep 17 00:00:00 2001 From: Jasper van der Neut - Stulen Date: Thu, 23 Feb 2023 23:19:13 +0100 Subject: [PATCH 45/65] Update docs/handbook/image-file-formats.rst Co-authored-by: Hugo van Kemenade --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 081b84963..597d1b644 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1498,7 +1498,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum saved in the PDF. **dpi** - A tuple of (x_resolution, y_resolution), with inches as the resolution + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution unit. If both the ``resolution`` parameter and the ``dpi`` parameter are present, ``resolution`` will be ignored. From 57acab55cbf0f00f3064d11ccfd3843c55cdceb0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 12:55:13 +1100 Subject: [PATCH 46/65] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index aa03cfc40..d37ba4ab3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Support saving PDF with different X and Y resolutions #6961 + [jvanderneutstulen, radarhere, hugovk] + - Fixed writing int as UNDEFINED tag #6950 [radarhere] From f52bbf895036f25932a479a46b20b07f2eb0c1de Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 09:58:51 +0200 Subject: [PATCH 47/65] Clarify variable names in BdfFontFile Co-authored-by: Yay295 --- src/PIL/BdfFontFile.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index e0dd4dede..3f7b760d6 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -64,16 +64,25 @@ def bdf_char(f): bitmap.append(s[:-1]) bitmap = b"".join(bitmap) - [x, y, l, d] = [int(p) for p in props["BBX"].split()] - [dx, dy] = [int(p) for p in props["DWIDTH"].split()] + # The word BBX followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBox, BBoy) of the lower left corner + # from the origin of the character. + width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] - bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y) + # The word DWIDTH followed by the width in x and y of the character in device units. + dx, dy = [int(p) for p in props["DWIDTH"].split()] + + bbox = ( + (dx, dy), + (x_disp, -y_disp - height, width + x_disp, -y_disp), + (0, 0, width, height), + ) try: - im = Image.frombytes("1", (x, y), bitmap, "hex", "1") + im = Image.frombytes("1", (width, height), bitmap, "hex", "1") except ValueError: # deal with zero-width characters - im = Image.new("1", (x, y)) + im = Image.new("1", (width, height)) return id, int(props["ENCODING"]), bbox, im From b6b72170a8d81255e6f491e657e0614102a0e347 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 09:59:54 +0200 Subject: [PATCH 48/65] Clarify variable names in Image Co-authored-by: Yay295 --- src/PIL/Image.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 63bad83a1..670907c67 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -767,12 +767,12 @@ class Image: data = [] while True: - l, s, d = e.encode(bufsize) - data.append(d) - if s: + length, error_code, chunk = e.encode(bufsize) + data.append(chunk) + if error_code: break - if s < 0: - msg = f"encoder error {s} in tobytes" + if error_code < 0: + msg = f"encoder error {error_code} in tobytes" raise RuntimeError(msg) return b"".join(data) From 04be46d484eccb633b1ad8058e1c4f39208589b0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 10:04:38 +0200 Subject: [PATCH 49/65] Clarify variable names in ImageFile Co-authored-by: Yay295 --- src/PIL/ImageFile.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 132490a8e..dfa715686 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -530,20 +530,20 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): encoder.setimage(im.im, b) if encoder.pushes_fd: encoder.setfd(fp) - l, s = encoder.encode_to_pyfd() + length, error_code = encoder.encode_to_pyfd() else: if exc: # compress to Python file-compatible object while True: - l, s, d = encoder.encode(bufsize) - fp.write(d) - if s: + length, error_code, chunk = encoder.encode(bufsize) + fp.write(chunk) + if error_code: break else: # slight speedup: compress to real file object - s = encoder.encode_to_file(fh, bufsize) - if s < 0: - msg = f"encoder error {s} when writing image file" + error_code = encoder.encode_to_file(fh, bufsize) + if error_code < 0: + msg = f"encoder error {error_code} when writing image file" raise OSError(msg) from exc finally: encoder.cleanup() From 6f79e653d68c7bae9c0303f8d8ddd68a3a1cb3f0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 10:07:29 +0200 Subject: [PATCH 50/65] Clarify variable names in PcfFontFile Co-authored-by: Yay295 --- src/PIL/PcfFontFile.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index d5f510f03..1a4b8f6d9 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -86,9 +86,23 @@ class PcfFontFile(FontFile.FontFile): for ch, ix in enumerate(encoding): if ix is not None: - x, y, l, r, w, a, d, f = metrics[ix] - glyph = (w, 0), (l, d - y, x + l, d), (0, 0, x, y), bitmaps[ix] - self.glyph[ch] = glyph + ix_metrics = metrics[ix] + ( + xsize, + ysize, + left, + right, + width, + ascent, + descent, + attributes, + ) = ix_metrics + self.glyph[ch] = ( + (width, 0), + (left, descent - ysize, xsize + left, descent), + (0, 0, xsize, ysize), + bitmaps[ix], + ) def _getformat(self, tag): format, size, offset = self.toc[tag] @@ -206,7 +220,7 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - x, y, l, r, w, a, d, f = metrics[i] + x, y = metrics[i][0], metrics[i][1] b, e = offsets[i], offsets[i + 1] bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) From 8e18415cc54c1c87183d36a184b5360d77fb8571 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 10:09:14 +0200 Subject: [PATCH 51/65] Clarify variable names in TiffImagePlugin Co-authored-by: Yay295 --- src/PIL/TiffImagePlugin.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index aaaf8fcb9..0491a736d 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1845,13 +1845,13 @@ def _save(im, fp, filename): e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - l, s, d = e.encode(16 * 1024) + length, error_code, chunk = e.encode(16 * 1024) if not _fp: - fp.write(d) - if s: + fp.write(chunk) + if error_code: break - if s < 0: - msg = f"encoder error {s} when writing image file" + if error_code < 0: + msg = f"encoder error {error_code} when writing image file" raise OSError(msg) else: From 8a1b9aa0adbd84cd944c15a667af4a65052ac522 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 16:44:07 +1100 Subject: [PATCH 52/65] Allow comments in FITS images --- Tests/test_file_fits.py | 6 ++++++ src/PIL/FitsImagePlugin.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index d2f5a6d17..3048827e0 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -44,6 +44,12 @@ def test_naxis_zero(): pass +def test_comment(): + image_data = b"SIMPLE = T / comment string" + with pytest.raises(OSError): + FitsImagePlugin.FitsImageFile(BytesIO(image_data)) + + def test_stub_deprecated(): class Handler: opened = False diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 1185ef2d3..1359aeb12 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -32,7 +32,7 @@ class FitsImageFile(ImageFile.ImageFile): keyword = header[:8].strip() if keyword == b"END": break - value = header[8:].strip() + value = header[8:].split(b"/")[0].strip() if value.startswith(b"="): value = value[1:].strip() if not headers and (not _accept(keyword) or value != b"T"): From dbcd7372e554ee420a156b968eb9a609ec66ffd1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 18:46:07 +1100 Subject: [PATCH 53/65] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d37ba4ab3..15decc7f0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Allow comments in FITS images #6973 + [radarhere] + - Support saving PDF with different X and Y resolutions #6961 [jvanderneutstulen, radarhere, hugovk] From 132fb9360b291eafedd3ea8238ceebd93a094e87 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 19:10:47 +1100 Subject: [PATCH 54/65] Added memoryview support to frombytes() --- Tests/test_image_frombytes.py | 11 +++++++++-- src/decode.c | 7 +++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 7fb05cda7..c299e4544 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,10 +1,17 @@ +import pytest + from PIL import Image from .helper import assert_image_equal, hopper -def test_sanity(): +@pytest.mark.parametrize("data_type", ("bytes", "memoryview")) +def test_sanity(data_type): im1 = hopper() - im2 = Image.frombytes(im1.mode, im1.size, im1.tobytes()) + + data = im1.tobytes() + if data_type == "memoryview": + data = memoryview(data) + im2 = Image.frombytes(im1.mode, im1.size, data) assert_image_equal(im1, im2) diff --git a/src/decode.c b/src/decode.c index 7a9b956c5..82a3af832 100644 --- a/src/decode.c +++ b/src/decode.c @@ -116,12 +116,11 @@ _dealloc(ImagingDecoderObject *decoder) { static PyObject * _decode(ImagingDecoderObject *decoder, PyObject *args) { - UINT8 *buffer; - Py_ssize_t bufsize; + Py_buffer buffer; int status; ImagingSectionCookie cookie; - if (!PyArg_ParseTuple(args, "y#", &buffer, &bufsize)) { + if (!PyArg_ParseTuple(args, "y*", &buffer)) { return NULL; } @@ -129,7 +128,7 @@ _decode(ImagingDecoderObject *decoder, PyObject *args) { ImagingSectionEnter(&cookie); } - status = decoder->decode(decoder->im, &decoder->state, buffer, bufsize); + status = decoder->decode(decoder->im, &decoder->state, buffer.buf, buffer.len); if (!decoder->pulls_fd) { ImagingSectionLeave(&cookie); From 36489c2c396d8801bfa632b6e8d00a094dab5347 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 20:54:35 +1100 Subject: [PATCH 55/65] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 15decc7f0..fe0230c34 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added memoryview support to frombytes() #6974 + [radarhere] + - Allow comments in FITS images #6973 [radarhere] From fcc59a4001bb1b1ad47d1a8c0b0a602336b2dcdd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 13:40:44 +1100 Subject: [PATCH 56/65] Use existing variable names from ImageFile --- src/PIL/Image.py | 14 +++++++------- src/PIL/ImageFile.py | 14 +++++++------- src/PIL/PcfFontFile.py | 11 ++++++----- src/PIL/TiffImagePlugin.py | 10 +++++----- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 670907c67..cf9ab2df6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -765,17 +765,17 @@ class Image: bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - data = [] + output = [] while True: - length, error_code, chunk = e.encode(bufsize) - data.append(chunk) - if error_code: + bytes_consumed, errcode, data = e.encode(bufsize) + output.append(data) + if errcode: break - if error_code < 0: - msg = f"encoder error {error_code} in tobytes" + if errcode < 0: + msg = f"encoder error {errcode} in tobytes" raise RuntimeError(msg) - return b"".join(data) + return b"".join(output) def tobitmap(self, name="image"): """ diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index dfa715686..8e4f7dfb2 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -530,20 +530,20 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): encoder.setimage(im.im, b) if encoder.pushes_fd: encoder.setfd(fp) - length, error_code = encoder.encode_to_pyfd() + errcode = encoder.encode_to_pyfd()[1] else: if exc: # compress to Python file-compatible object while True: - length, error_code, chunk = encoder.encode(bufsize) - fp.write(chunk) - if error_code: + errcode, data = encoder.encode(bufsize)[1:] + fp.write(data) + if errcode: break else: # slight speedup: compress to real file object - error_code = encoder.encode_to_file(fh, bufsize) - if error_code < 0: - msg = f"encoder error {error_code} when writing image file" + errcode = encoder.encode_to_file(fh, bufsize) + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) from exc finally: encoder.cleanup() diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 1a4b8f6d9..2300efe40 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -86,7 +86,6 @@ class PcfFontFile(FontFile.FontFile): for ch, ix in enumerate(encoding): if ix is not None: - ix_metrics = metrics[ix] ( xsize, ysize, @@ -96,7 +95,7 @@ class PcfFontFile(FontFile.FontFile): ascent, descent, attributes, - ) = ix_metrics + ) = metrics[ix] self.glyph[ch] = ( (width, 0), (left, descent - ysize, xsize + left, descent), @@ -220,9 +219,11 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - x, y = metrics[i][0], metrics[i][1] - b, e = offsets[i], offsets[i + 1] - bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) + left, right = metrics[i][:2] + b, e = offsets[i : i + 2] + bitmaps.append( + Image.frombytes("1", (left, right), data[b:e], "raw", mode, pad(left)) + ) return bitmaps diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0491a736d..04d246dd4 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1845,13 +1845,13 @@ def _save(im, fp, filename): e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - length, error_code, chunk = e.encode(16 * 1024) + errcode, data = e.encode(16 * 1024)[1:] if not _fp: - fp.write(chunk) - if error_code: + fp.write(data) + if errcode: break - if error_code < 0: - msg = f"encoder error {error_code} when writing image file" + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) else: From c799bd8a03f522dc6cc26f6c974140df60558484 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 14:04:10 +1100 Subject: [PATCH 57/65] Adjusted variable names and comments to better match specification --- src/PIL/BdfFontFile.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index 3f7b760d6..075d46290 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -64,16 +64,18 @@ def bdf_char(f): bitmap.append(s[:-1]) bitmap = b"".join(bitmap) - # The word BBX followed by the width in x (BBw), height in y (BBh), - # and x and y displacement (BBox, BBoy) of the lower left corner - # from the origin of the character. + # The word BBX + # followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBxoff0, BByoff0) + # of the lower left corner from the origin of the character. width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] - # The word DWIDTH followed by the width in x and y of the character in device units. - dx, dy = [int(p) for p in props["DWIDTH"].split()] + # The word DWIDTH + # followed by the width in x and y of the character in device pixels. + dwx, dwy = [int(p) for p in props["DWIDTH"].split()] bbox = ( - (dx, dy), + (dwx, dwy), (x_disp, -y_disp - height, width + x_disp, -y_disp), (0, 0, width, height), ) From bbbaf3c615e7a60e526e73f3dc6449780dce2271 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sun, 26 Feb 2023 13:03:29 +0200 Subject: [PATCH 58/65] Update src/PIL/PcfFontFile.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/PcfFontFile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 2300efe40..8db5822fe 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -219,10 +219,10 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - left, right = metrics[i][:2] + xsize, ysize = metrics[i][:2] b, e = offsets[i : i + 2] bitmaps.append( - Image.frombytes("1", (left, right), data[b:e], "raw", mode, pad(left)) + Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize)) ) return bitmaps From 9c98f4d515036d9bb8094bc5faac6a81eca0b147 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Feb 2023 09:48:41 +1100 Subject: [PATCH 59/65] Release buffer --- src/decode.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/decode.c b/src/decode.c index 82a3af832..7e3fadc04 100644 --- a/src/decode.c +++ b/src/decode.c @@ -134,6 +134,7 @@ _decode(ImagingDecoderObject *decoder, PyObject *args) { ImagingSectionLeave(&cookie); } + PyBuffer_Release(&buffer); return Py_BuildValue("ii", status, decoder->state.errcode); } From 17eadf07fa5315e6d904936fb052e90c49915962 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 28 Feb 2023 20:49:43 +1100 Subject: [PATCH 60/65] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fe0230c34..d5798d41b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added "corners" argument to ImageDraw rounded_rectangle() #6954 + [radarhere] + - Added memoryview support to frombytes() #6974 [radarhere] From 6e9c0ae5a09618afec077a39262d2337dd0a3fee Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 28 Feb 2023 20:04:26 +1100 Subject: [PATCH 61/65] Further document that x1 >= x0 and y1 >= y0 --- docs/reference/ImageDraw.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 81e3d8f46..9df4a5dad 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -318,8 +318,8 @@ Methods Draws a rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box - is inclusive of both endpoints. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param outline: Color to use for the outline. :param fill: Color to use for the fill. :param width: The line width, in pixels. @@ -331,8 +331,8 @@ Methods Draws a rounded rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box - is inclusive of both endpoints. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param radius: Radius of the corners. :param outline: Color to use for the outline. :param fill: Color to use for the fill. From 53fb3a9365feec24cb59196477639bf712849ef0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Mar 2023 11:04:14 +1100 Subject: [PATCH 62/65] Updated lcms2 to 2.15 --- docs/installation.rst | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 1b5719a8e..55d5ee832 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -150,7 +150,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.14**. + above uses liblcms2. Tested with **1.19** and **2.7-2.15**. * **libwebp** provides the WebP format. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 35980f19c..3a885afaf 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -289,9 +289,9 @@ deps = { # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.14/lcms2-2.14.tar.gz/download", - "filename": "lcms2-2.14.tar.gz", - "dir": "lcms2-2.14", + "url": SF_PROJECTS + "/lcms/files/lcms/2.15/lcms2-2.15.tar.gz/download", + "filename": "lcms2-2.15.tar.gz", + "dir": "lcms2-2.15", "license": "COPYING", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { From b84c29a035b2476ae1152fc2054107f25d562dfb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Mar 2023 11:22:35 +1100 Subject: [PATCH 63/65] Raise an error if co-ordinates are incorrectly ordered --- .../imagedraw_ellipse_various_sizes.png | Bin 21446 -> 21600 bytes ...imagedraw_ellipse_various_sizes_filled.png | Bin 20315 -> 20325 bytes Tests/test_imagedraw.py | 28 ++++++++++++++--- src/PIL/ImageDraw.py | 6 ++++ src/_imaging.c | 27 ++++++++++++++++ src/libImaging/Draw.c | 29 ++++++------------ 6 files changed, 66 insertions(+), 24 deletions(-) diff --git a/Tests/images/imagedraw_ellipse_various_sizes.png b/Tests/images/imagedraw_ellipse_various_sizes.png index 11a1be6faebea1a31854e4d13975f1a5c12c811e..5e3cf22b4ad8e19506b7c9551996f522bff75046 100644 GIT binary patch literal 21600 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

L*eghq9S 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 F{vQsP3wQtk literal 20315 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 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index ea0711ad5..a7267a87e 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -355,7 +355,13 @@ def ellipse_various_sizes_helper(filled): for w in ellipse_sizes: y = 1 for h in ellipse_sizes: - border = [x, y, x + w - 1, y + h - 1] + x1 = x + w + if w: + x1 -= 1 + y1 = y + h + if h: + y1 -= 1 + border = [x, y, x1, y1] if filled: draw.ellipse(border, fill="white") else: @@ -932,9 +938,6 @@ def test_square(): img, draw = create_base_image_draw((10, 10)) draw.rectangle((2, 2, 7, 7), BLACK) assert_image_equal_tofile(img, expected, "square as normal rectangle failed") - img, draw = create_base_image_draw((10, 10)) - draw.rectangle((7, 7, 2, 2), BLACK) - assert_image_equal_tofile(img, expected, "square as inverted rectangle failed") def test_triangle_right(): @@ -1499,3 +1502,20 @@ def test_polygon2(): draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") expected = "Tests/images/imagedraw_outline_polygon_RGB.png" assert_image_similar_tofile(im, expected, 1) + + +def test_incorrectly_ordered_coordinates(): + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + with pytest.raises(ValueError): + draw.arc((1, 1, 0, 0), 10, 260) + with pytest.raises(ValueError): + draw.chord((1, 1, 0, 0), 10, 260) + with pytest.raises(ValueError): + draw.ellipse((1, 1, 0, 0)) + with pytest.raises(ValueError): + draw.pieslice((1, 1, 0, 0), 10, 260) + with pytest.raises(ValueError): + draw.rectangle((1, 1, 0, 0)) + with pytest.raises(ValueError): + draw.rounded_rectangle((1, 1, 0, 0)) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index a55ebbe8e..2d0a98765 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -303,6 +303,12 @@ class ImageDraw: (x0, y0), (x1, y1) = xy else: x0, y0, x1, y1 = xy + if x1 < x0 or y1 < y0: + msg = ( + "x1 must be greater than or equal to x0," + " and y1 must be greater than or equal to y0" + ) + raise ValueError(msg) if corners is None: corners = (True, True, True, True) diff --git a/src/_imaging.c b/src/_imaging.c index cece2e93a..12d7f93a9 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -251,6 +251,8 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { static const char *must_be_sequence = "argument must be a sequence"; static const char *must_be_two_coordinates = "coordinate list must contain exactly 2 coordinates"; +static const char *incorrectly_ordered_coordinates = + "x1 must be greater than or equal to x0, and y1 must be greater than or equal to y0"; static const char *wrong_mode = "unrecognized image mode"; static const char *wrong_raw_mode = "unrecognized raw mode"; static const char *outside_image = "image index out of range"; @@ -2805,6 +2807,11 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawArc( self->image->image, @@ -2886,6 +2893,11 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawChord( self->image->image, @@ -2932,6 +2944,11 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawEllipse( self->image->image, @@ -3101,6 +3118,11 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawPieslice( self->image->image, @@ -3197,6 +3219,11 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawRectangle( self->image->image, diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 77343e583..82f290bd0 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -85,25 +85,22 @@ point32(Imaging im, int x, int y, int ink) { static inline void point32rgba(Imaging im, int x, int y, int ink) { - unsigned int tmp1; + unsigned int tmp; if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { UINT8 *out = (UINT8 *)im->image[y] + x * 4; UINT8 *in = (UINT8 *)&ink; - out[0] = BLEND(in[3], out[0], in[0], tmp1); - out[1] = BLEND(in[3], out[1], in[1], tmp1); - out[2] = BLEND(in[3], out[2], in[2], tmp1); + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); } } static inline void hline8(Imaging im, int x0, int y0, int x1, int ink) { - int tmp, pixelwidth; + int pixelwidth; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -126,13 +123,9 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) { static inline void hline32(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; INT32 *p; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -152,13 +145,9 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) { static inline void hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; - unsigned int tmp1; + unsigned int tmp; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -173,9 +162,9 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4; UINT8 *in = (UINT8 *)&ink; while (x0 <= x1) { - out[0] = BLEND(in[3], out[0], in[0], tmp1); - out[1] = BLEND(in[3], out[1], in[1], tmp1); - out[2] = BLEND(in[3], out[2], in[2], tmp1); + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); x0++; out += 4; } From a4965a7eaa0476ff415bac345d9965101be10ee0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Mar 2023 22:06:40 +1100 Subject: [PATCH 64/65] Split into x and y errors --- Tests/test_imagedraw.py | 15 ++++++------ src/PIL/ImageDraw.py | 10 ++++---- src/_imaging.c | 51 +++++++++++++++++++++++++++++++---------- 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index a7267a87e..5295021a3 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1504,18 +1504,19 @@ def test_polygon2(): assert_image_similar_tofile(im, expected, 1) -def test_incorrectly_ordered_coordinates(): +@pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) +def test_incorrectly_ordered_coordinates(xy): im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with pytest.raises(ValueError): - draw.arc((1, 1, 0, 0), 10, 260) + draw.arc(xy, 10, 260) with pytest.raises(ValueError): - draw.chord((1, 1, 0, 0), 10, 260) + draw.chord(xy, 10, 260) with pytest.raises(ValueError): - draw.ellipse((1, 1, 0, 0)) + draw.ellipse(xy) with pytest.raises(ValueError): - draw.pieslice((1, 1, 0, 0), 10, 260) + draw.pieslice(xy, 10, 260) with pytest.raises(ValueError): - draw.rectangle((1, 1, 0, 0)) + draw.rectangle(xy) with pytest.raises(ValueError): - draw.rounded_rectangle((1, 1, 0, 0)) + draw.rounded_rectangle(xy) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 2d0a98765..5a0df09cb 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -303,11 +303,11 @@ class ImageDraw: (x0, y0), (x1, y1) = xy else: x0, y0, x1, y1 = xy - if x1 < x0 or y1 < y0: - msg = ( - "x1 must be greater than or equal to x0," - " and y1 must be greater than or equal to y0" - ) + if x1 < x0: + msg = "x1 must be greater than or equal to x0" + raise ValueError(msg) + if y1 < y0: + msg = "y1 must be greater than or equal to y0" raise ValueError(msg) if corners is None: corners = (True, True, True, True) diff --git a/src/_imaging.c b/src/_imaging.c index 12d7f93a9..1c25ab00c 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -251,8 +251,10 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { static const char *must_be_sequence = "argument must be a sequence"; static const char *must_be_two_coordinates = "coordinate list must contain exactly 2 coordinates"; -static const char *incorrectly_ordered_coordinates = - "x1 must be greater than or equal to x0, and y1 must be greater than or equal to y0"; +static const char *incorrectly_ordered_x_coordinate = + "x1 must be greater than or equal to x0"; +static const char *incorrectly_ordered_y_coordinate = + "y1 must be greater than or equal to y0"; static const char *wrong_mode = "unrecognized image mode"; static const char *wrong_raw_mode = "unrecognized raw mode"; static const char *outside_image = "image index out of range"; @@ -2807,8 +2809,13 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } @@ -2893,8 +2900,13 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } @@ -2944,8 +2956,13 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } @@ -3118,8 +3135,13 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } @@ -3219,8 +3241,13 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } From 396dd820b937fa42f85a859acb303e29d7b37a76 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Mar 2023 23:04:21 +1100 Subject: [PATCH 65/65] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d5798d41b..90f97d89f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978 + [radarhere] + - Added "corners" argument to ImageDraw rounded_rectangle() #6954 [radarhere]