From 2203afeafa76519fcc01682d8c35e5c5d569d7c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 19:36:06 +1100 Subject: [PATCH 01/65] Do not set size unnecessarily if image failed to open --- src/PIL/EpsImagePlugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 1c88d22c7..2f7fee901 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -354,7 +354,6 @@ class EpsImageFile(ImageFile.ImageFile): check_required_header_comments() if not self._size: - self._size = 1, 1 # errors if this isn't set. why (1,1)? msg = "cannot determine EPS bounding box" raise OSError(msg) From dd15f15d08bf3fd32c41ef9f2502286778c9f993 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 09:06:20 +1000 Subject: [PATCH 02/65] Added further field sizes --- Tests/test_file_tiff.py | 9 ++++++++- src/PIL/TiffImagePlugin.py | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 97a02ac96..43181d1b3 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -96,10 +96,17 @@ class TestFileTiff: assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_bigtiff(self): + def test_bigtiff(self, tmp_path): with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") + with Image.open("Tests/images/hopper_bigtiff.tif") as im: + # multistrip support not yet implemented + del im.tag_v2[273] + + outfile = str(tmp_path / "temp.tif") + im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) + def test_set_legacy_api(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() with pytest.raises(Exception) as e: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d4d0910a..5b7d3f302 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1892,6 +1892,10 @@ class AppendingTiffWriter: 8, # srational 4, # float 8, # double + 4, # ifd + 2, # unicode + 4, # complex + 8, # long8 ] # StripOffsets = 273 From 8d3014b8bf94ee6d07276b227dfac4d6f7f5a859 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Apr 2023 21:03:59 +1000 Subject: [PATCH 03/65] Added inPlace argument to exif_transpose() --- Tests/images/orientation_rectangle.jpg | Bin 0 -> 669 bytes Tests/test_imageops.py | 12 ++++++ src/PIL/ImageOps.py | 51 +++++++++++++++---------- 3 files changed, 42 insertions(+), 21 deletions(-) create mode 100644 Tests/images/orientation_rectangle.jpg diff --git a/Tests/images/orientation_rectangle.jpg b/Tests/images/orientation_rectangle.jpg new file mode 100644 index 0000000000000000000000000000000000000000..85cfbd0a813a1b36d64318ead4f0fe5a3258a51f GIT binary patch literal 669 zcmex=wh=DOELf4NWZ*Q!{f5ODks= zS2uSLPp{yR(6I1`$f)F$)U@=B%&g*)(z5c3%Btp;*0%PJ&aO$5r%atTea6gLixw|g zx@`H1m8&*w-m-Pu_8mKS9XfpE=&|D`PM*4S`O4L6*Kgds_3+W-Cr_U}fAR9w$4{TX zeEs(Q$Io9Ne=#yJL%anfAwEO%mmttzOe`$SEbJhEF*22dJTAz>s%Xe2([0-9])", ): - transposed_image.info["XML:com.adobe.xmp"] = re.sub( - pattern, "", transposed_image.info["XML:com.adobe.xmp"] + exif_image.info["XML:com.adobe.xmp"] = re.sub( + pattern, "", exif_image.info["XML:com.adobe.xmp"] ) + if inPlace: + return return transposed_image + if inPlace: + return return image.copy() From bcb8dfc2fa9011c99d6332d5b0aa00c82e1c6cdf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Apr 2023 22:30:18 +1000 Subject: [PATCH 04/65] Rearranged code --- src/PIL/ImageOps.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 460a21ce2..5dc34f442 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -622,9 +622,7 @@ def exif_transpose(image, inPlace=False): exif_image.info["XML:com.adobe.xmp"] = re.sub( pattern, "", exif_image.info["XML:com.adobe.xmp"] ) - if inPlace: - return - return transposed_image - if inPlace: - return - return image.copy() + if not inPlace: + return transposed_image + elif not inPlace: + return image.copy() From fe8599c5d64b2932b430a0178f17009855397fe6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 16 Apr 2023 14:04:39 +1000 Subject: [PATCH 05/65] Use ExifTags --- src/PIL/Image.py | 4 ++-- src/PIL/ImageOps.py | 8 ++++---- src/PIL/TiffImagePlugin.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 5a43f6c4a..34b8bbcbd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1418,12 +1418,12 @@ class Image: self._exif.load(exif_info) # XMP tags - if 0x0112 not in self._exif: + if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") if xmp_tags: match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) if match: - self._exif[0x0112] = int(match[2]) + self._exif[ExifTags.Base.Orientation] = int(match[2]) return self._exif diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 5dc34f442..facc30ba0 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,7 +21,7 @@ import functools import operator import re -from . import Image, ImagePalette +from . import ExifTags, Image, ImagePalette # # helpers @@ -589,7 +589,7 @@ def exif_transpose(image, inPlace=False): image will be returned. """ image_exif = image.getexif() - orientation = image_exif.get(0x0112) + orientation = image_exif.get(ExifTags.Base.Orientation) method = { 2: Image.Transpose.FLIP_LEFT_RIGHT, 3: Image.Transpose.ROTATE_180, @@ -608,8 +608,8 @@ def exif_transpose(image, inPlace=False): exif_image = image if inPlace else transposed_image exif = exif_image.getexif() - if 0x0112 in exif: - del exif[0x0112] + if ExifTags.Base.Orientation in exif: + del exif[ExifTags.Base.Orientation] if "exif" in exif_image.info: exif_image.info["exif"] = exif.tobytes() elif "Raw profile type exif" in exif_image.info: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d4d0910a..7f8449ea6 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -49,7 +49,7 @@ from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational -from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags +from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 @@ -1183,7 +1183,7 @@ class TiffImageFile(ImageFile.ImageFile): :returns: Photoshop "Image Resource Blocks" in a dictionary. """ blocks = {} - val = self.tag_v2.get(0x8649) + val = self.tag_v2.get(ExifTags.Base.ImageResources) if val: while val[:4] == b"8BIM": id = i16(val[4:6]) @@ -1548,7 +1548,7 @@ class TiffImageFile(ImageFile.ImageFile): palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) - self._tile_orientation = self.tag_v2.get(0x0112) + self._tile_orientation = self.tag_v2.get(ExifTags.Base.Orientation) # From c5f90af56c7ca2b0e1ee3d3a95b5e7cd5291df5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 07:19:13 +1000 Subject: [PATCH 06/65] Updated xz to 5.4.3 --- 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 9b5fc5d18..05df77a68 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ deps = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.2.tar.gz/download", - "filename": "xz-5.4.2.tar.gz", - "dir": "xz-5.4.2", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.3.tar.gz/download", + "filename": "xz-5.4.3.tar.gz", + "dir": "xz-5.4.3", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), From f67fcf131a53f7436a2f4a540ed251c927af2c05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 May 2023 11:58:05 +1000 Subject: [PATCH 07/65] If the clipboard fails to open on Windows, wait and try again --- src/display.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/display.c b/src/display.c index e8e7b62c2..754a6ae78 100644 --- a/src/display.c +++ b/src/display.c @@ -437,8 +437,14 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL}; if (!OpenClipboard(NULL)) { - PyErr_SetString(PyExc_OSError, "failed to open clipboard"); - return NULL; + // Maybe the clipboard is temporarily in use by another process. + // Wait and try again + Sleep(500); + + if (!OpenClipboard(NULL)) { + PyErr_SetString(PyExc_OSError, "failed to open clipboard"); + return NULL; + } } // find best format as set by clipboard owner From a0b691a219d274a561784781476cc952e79c1b8b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 May 2023 12:12:16 +1000 Subject: [PATCH 08/65] Fixed combining single duration across duplicate PNG frames --- Tests/test_file_apng.py | 6 ++++++ src/PIL/PngImagePlugin.py | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index f78c086eb..c62231cd4 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -440,6 +440,12 @@ def test_apng_save_duration_loop(tmp_path): assert im.n_frames == 1 assert im.info.get("duration") == 750 + # test removal of duplicated frames with a single duration + frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert im.info.get("duration") == 1500 + # test info duration frame.info["duration"] = 750 frame.save(test_file, save_all=True) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 82a74b267..aaf242b1d 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1146,11 +1146,14 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) and prev_disposal == encoderinfo.get("disposal") and prev_blend == encoderinfo.get("blend") ): - if isinstance(duration, (list, tuple)): - previous["encoderinfo"]["duration"] += encoderinfo["duration"] + previous["encoderinfo"]["duration"] += encoderinfo.get( + "duration", duration + ) continue else: bbox = None + if "duration" not in encoderinfo: + encoderinfo["duration"] = duration im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) # animation control @@ -1175,7 +1178,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) im_frame = im_frame.crop(bbox) size = im_frame.size encoderinfo = frame_data["encoderinfo"] - frame_duration = int(round(encoderinfo.get("duration", duration))) + frame_duration = int(round(encoderinfo["duration"])) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) # frame control From b39c807dde8909b1ce4afd85f37563165224e073 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 May 2023 22:14:40 +1000 Subject: [PATCH 09/65] Removed rectangle example from co-ordinate system documentation --- docs/handbook/concepts.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index e40ed4687..e0975a121 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -95,9 +95,8 @@ in the upper left corner. Note that the coordinates refer to the implied pixel corners; the centre of a pixel addressed as (0, 0) actually lies at (0.5, 0.5). Coordinates are usually passed to the library as 2-tuples (x, y). Rectangles -are represented as 4-tuples, with the upper left corner given first. For -example, a rectangle covering all of an 800x600 pixel image is written as (0, -0, 800, 600). +are represented as 4-tuples, (x1, y1, x2, y2), with the upper left corner given +first. Palette ------- From 509671c53e31d2fc9af6f89d1ac158cce8160d62 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 17:59:33 -0500 Subject: [PATCH 10/65] fix INT64 def and add warning if not set --- src/libImaging/ImPlatform.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index af9996ca9..f183c3aa4 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -61,7 +61,9 @@ #if SIZEOF_LONG == 8 #define INT64 long #elif SIZEOF_LONG_LONG == 8 -#define INT64 long +#define INT64 long long +#else +#warning Cannot find required 64-bit integer type #endif #define INT8 signed char From 6de5e999bd7ee571877c975c9cb2a3038ee90f27 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:00:45 -0500 Subject: [PATCH 11/65] add UINT64 def if INT64 is defined --- src/libImaging/ImPlatform.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index f183c3aa4..522776b58 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -71,6 +71,9 @@ #define UINT16 unsigned INT16 #define UINT32 unsigned INT32 +#ifdef INT64 +#define UINT64 unsigned INT64 +#endif #endif From e9cfe4b6a2139923e3cab44bdd55cbf45ac16333 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:02:41 -0500 Subject: [PATCH 12/65] label preprocessor if..else..endif for clarity --- src/libImaging/ImPlatform.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 522776b58..6db251bf1 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -25,7 +25,7 @@ #endif #endif -#if defined(_WIN32) || defined(__CYGWIN__) +#if defined(_WIN32) || defined(__CYGWIN__) /* WIN */ #define WIN32_LEAN_AND_MEAN #include @@ -37,7 +37,7 @@ #undef WIN32 #endif -#else +#else /* WIN */ /* For System that are not Windows, we'll need to define these. */ #if SIZEOF_SHORT == 2 @@ -75,7 +75,7 @@ #define UINT64 unsigned INT64 #endif -#endif +#endif /* WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ #define FLOAT16 UINT16 From c2527348ecf4487be76fa55eefa440be1a96e9f5 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:04:10 -0500 Subject: [PATCH 13/65] add comment explaining why #define and not typedef --- src/libImaging/ImPlatform.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 6db251bf1..e2d2d597b 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -39,6 +39,9 @@ #else /* WIN */ /* For System that are not Windows, we'll need to define these. */ +/* We have to define them instead of using typedef because the JPEG lib also + defines their own types with the same names, so we need to be able to undef + ours before including the JPEG code. */ #if SIZEOF_SHORT == 2 #define INT16 short From fbec8f19dd1ec77e3cf741c11038f0229967830c Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:11:02 -0500 Subject: [PATCH 14/65] add check for C99+ to use their defs if possible --- src/libImaging/ImPlatform.h | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index e2d2d597b..9f736ed75 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -43,6 +43,23 @@ defines their own types with the same names, so we need to be able to undef ours before including the JPEG code. */ +#if __STDC_VERSION__ >= 199901L /* C99+ */ + +#include + +#define INT8 int8_t +#define UINT8 uint8_t +#define INT16 int16_t +#define UINT16 uint16_t +#define INT32 int32_t +#define UINT32 uint32_t +#ifdef INT64_MAX +#define INT64 int64_t +#define UINT64 uint64_t +#endif + +#else /* C99+ */ + #if SIZEOF_SHORT == 2 #define INT16 short #elif SIZEOF_INT == 2 @@ -78,6 +95,8 @@ #define UINT64 unsigned INT64 #endif +#endif /* C99+ */ + #endif /* WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ From 9da0b58eea9d59d725106a76702b592e302c9665 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:13:50 -0500 Subject: [PATCH 15/65] move INT8 def to top --- src/libImaging/ImPlatform.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 9f736ed75..303ebf987 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -60,6 +60,8 @@ #else /* C99+ */ +#define INT8 signed char + #if SIZEOF_SHORT == 2 #define INT16 short #elif SIZEOF_INT == 2 @@ -86,9 +88,7 @@ #warning Cannot find required 64-bit integer type #endif -#define INT8 signed char #define UINT8 unsigned char - #define UINT16 unsigned INT16 #define UINT32 unsigned INT32 #ifdef INT64 From 724f2664601708dd1355c8a03b3815d56d788609 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:14:55 -0500 Subject: [PATCH 16/65] change INT16 def failure to an error --- src/libImaging/ImPlatform.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 303ebf987..912b17855 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -67,7 +67,7 @@ #elif SIZEOF_INT == 2 #define INT16 int #else -#define INT16 short /* most things works just fine anyway... */ +#error Cannot find required 16-bit integer type #endif #if SIZEOF_SHORT == 4 From f6b516bb068f28134c781b42f4188931f9e11b6b Mon Sep 17 00:00:00 2001 From: Yay295 Date: Fri, 19 May 2023 08:01:02 -0500 Subject: [PATCH 17/65] Adjust C preprocessor block labels Co-authored-by: Hugo van Kemenade --- src/libImaging/ImPlatform.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 912b17855..10d6905b9 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -37,7 +37,7 @@ #undef WIN32 #endif -#else /* WIN */ +#else /* not WIN */ /* For System that are not Windows, we'll need to define these. */ /* We have to define them instead of using typedef because the JPEG lib also defines their own types with the same names, so we need to be able to undef @@ -58,7 +58,7 @@ #define UINT64 uint64_t #endif -#else /* C99+ */ +#else /* < C99 */ #define INT8 signed char @@ -95,9 +95,9 @@ #define UINT64 unsigned INT64 #endif -#endif /* C99+ */ +#endif /* < C99 */ -#endif /* WIN */ +#endif /* not WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ #define FLOAT16 UINT16 From 4f734d295f7ce2e31c650e1971b45dae7f19c8d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 May 2023 15:38:36 +1000 Subject: [PATCH 18/65] Use --config-settings instead of deprecated --global-option --- .github/workflows/test-mingw.yml | 2 +- MANIFEST.in | 1 + Makefile | 4 ++-- _custom_build/backend.py | 31 +++++++++++++++++++++++++++++++ docs/installation.rst | 24 ++++++++++++------------ pyproject.toml | 4 ++++ 6 files changed, 51 insertions(+), 15 deletions(-) create mode 100755 _custom_build/backend.py create mode 100644 pyproject.toml diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a109ec0d8..5a737a1ee 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -80,7 +80,7 @@ jobs: pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow - run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . + run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install . - name: Test Pillow run: | diff --git a/MANIFEST.in b/MANIFEST.in index f51551303..606e7e074 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,6 +15,7 @@ graft src graft depends graft winbuild graft docs +graft _custom_build # build/src control detritus exclude .appveyor.yml diff --git a/Makefile b/Makefile index e41f36411..776649b28 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ install: .PHONY: install-coverage install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install --global-option="build_ext" . + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install . python3 selftest.py .PHONY: debug @@ -74,7 +74,7 @@ debug: # for our stuff, kills optimization, and redirects to dev null so we # see any build failures. make clean > /dev/null - CFLAGS='-g -O0' python3 -m pip -v install --global-option="build_ext" . > /dev/null + CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null .PHONY: release-test release-test: diff --git a/_custom_build/backend.py b/_custom_build/backend.py new file mode 100755 index 000000000..31a954824 --- /dev/null +++ b/_custom_build/backend.py @@ -0,0 +1,31 @@ +import sys + +from setuptools.build_meta import * # noqa: F401, F403 +from setuptools.build_meta import _BuildMetaBackend + + +class _CustomBuildMetaBackend(_BuildMetaBackend): + def run_setup(self, setup_script="setup.py"): + if self.config_settings: + flags = [] + for key in ("enable", "disable", "vendor"): + settings = self.config_settings.get(key) + if settings: + if not isinstance(settings, list): + settings = [settings] + for value in settings: + flags.append("--" + key + "-" + value) + if self.config_settings.get("debug") == "true": + flags.append("--debug") + if flags: + sys.argv = sys.argv[:1] + ["build_ext"] + flags + sys.argv[1:] + return super().run_setup(setup_script) + + def build_wheel( + self, wheel_directory, config_settings=None, metadata_directory=None + ): + self.config_settings = config_settings + return super().build_wheel(wheel_directory, config_settings, metadata_directory) + + +build_wheel = _CustomBuildMetaBackend().build_wheel diff --git a/docs/installation.rst b/docs/installation.rst index ad27b67ee..514d20e74 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -380,40 +380,40 @@ Build Options using a setting of 1. By default, it uses 4 CPUs, or if 4 are not available, as many as are present. -* Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-raqm``, - ``--disable-lcms``, ``--disable-webp``, ``--disable-webpmux``, - ``--disable-jpeg2000``, ``--disable-imagequant``, ``--disable-xcb``. +* Config settings: ``-C disable=zlib``, ``-C disable=jpeg``, + ``-C disable=tiff``, ``-C disable=freetype``, ``-C disable=raqm``, + ``-C disable=lcms``, ``-C disable=webp``, ``-C disable=webpmux``, + ``-C disable=jpeg2000``, ``-C disable=imagequant``, ``-C disable=xcb``. Disable building the corresponding feature even if the development libraries are present on the building machine. -* Build flags: ``--enable-zlib``, ``--enable-jpeg``, - ``--enable-tiff``, ``--enable-freetype``, ``--enable-raqm``, - ``--enable-lcms``, ``--enable-webp``, ``--enable-webpmux``, - ``--enable-jpeg2000``, ``--enable-imagequant``, ``--enable-xcb``. +* Config settings: ``-C enable=zlib``, ``-C enable=jpeg``, + ``-C enable=tiff``, ``-C enable=freetype``, ``-C enable=raqm``, + ``-C enable=lcms``, ``-C enable=webp``, ``-C enable=webpmux``, + ``-C enable=jpeg2000``, ``-C enable=imagequant``, ``-C enable=xcb``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. -* Build flags: ``--vendor-raqm``, ``--vendor-fribidi``. +* Config settings: ``-C vendor=raqm``, ``-C vendor=fribidi``. These flags are used to compile a modified version of libraqm and a shim that dynamically loads libfribidi at runtime. These are used to compile the standard Pillow wheels. Compiling libraqm requires a C99-compliant compiler. -* Build flag: ``--disable-platform-guessing``. Skips all of the +* Build flag: ``-C disable=platform-guessing``. Skips all of the platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the environment variables (e.g. Buildroot). -* Build flag: ``--debug``. Adds a debugging flag to the include and +* Build flag: ``-C debug=true``. Adds a debugging flag to the include and library search process to dump all paths searched for and found to stdout. Sample usage:: - python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" + python3 -m pip install --upgrade Pillow -C enable=[feature] Platform Support ---------------- diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..cf31b6407 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[build-system] +requires = ["setuptools >= 40.8.0", "wheel"] +build-backend = "backend" +backend-path = ["_custom_build"] From 18da2d0b2d01ab7bf9371451416379c0edca84f4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 May 2023 15:19:58 +1000 Subject: [PATCH 19/65] Removed inplace target --- Makefile | 5 ----- tox.ini | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 776649b28..57d756b47 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,6 @@ help: @echo " docserve run an HTTP server on the docs directory" @echo " html make HTML docs" @echo " htmlview open the index page built by the html target in your browser" - @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" @echo " lint run the lint checks" @@ -54,10 +53,6 @@ help: @echo " release-test run code and package tests before release" @echo " test run tests on installed Pillow" -.PHONY: inplace -inplace: clean - python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" . - .PHONY: install install: python3 -m pip -v install . diff --git a/tox.ini b/tox.ini index 458a00107..a79089f51 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ extras = tests commands = make clean - {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . + {envpython} -m pip install . {envpython} selftest.py {envpython} -m pytest -W always {posargs} allowlist_externals = From 546f6cbc27e178bfc742d8216a1af508b3e26e02 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 May 2023 17:11:43 +1000 Subject: [PATCH 20/65] Replaced absolute PIL import with relative import --- src/PIL/IcnsImagePlugin.py | 4 ++-- src/PIL/ImageCms.py | 6 +++--- src/PIL/ImageShow.py | 2 +- src/PIL/SpiderImagePlugin.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index c2f050edd..27cb89f73 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -22,11 +22,11 @@ import os import struct import sys -from PIL import Image, ImageFile, PngImagePlugin, features +from . import Image, ImageFile, PngImagePlugin, features enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: - from PIL import Jpeg2KImagePlugin + from . import Jpeg2KImagePlugin MAGIC = b"icns" HEADERSIZE = 8 diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 38cbab19c..3a337f9f2 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -18,10 +18,10 @@ import sys from enum import IntEnum -from PIL import Image +from . import Image try: - from PIL import _imagingcms + from . import _imagingcms except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. @@ -271,7 +271,7 @@ def get_display_profile(handle=None): if sys.platform != "win32": return None - from PIL import ImageWin + from . import ImageWin if isinstance(handle, ImageWin.HDC): profile = core.get_display_profile_win32(handle, 1) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 3f68a2696..8b1c3f8bb 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -17,7 +17,7 @@ import subprocess import sys from shlex import quote -from PIL import Image +from . import Image _viewers = [] diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index eac27e679..5614957c1 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -36,7 +36,7 @@ import os import struct import sys -from PIL import Image, ImageFile +from . import Image, ImageFile def isInt(f): @@ -191,7 +191,7 @@ class SpiderImageFile(ImageFile.ImageFile): # returns a ImageTk.PhotoImage object, after rescaling to 0..255 def tkPhotoImage(self): - from PIL import ImageTk + from . import ImageTk return ImageTk.PhotoImage(self.convert2byte(), palette=256) From 053cb3de52dcc5008e5bac96942697a1b7e9e00d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 May 2023 14:38:05 +1000 Subject: [PATCH 21/65] Fixed finding dependencies on Cygwin --- .github/workflows/test-cygwin.yml | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 9a1e46705..e7ab6466e 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -104,7 +104,7 @@ jobs: - name: Build shell: bash.exe -eo pipefail -o igncr "{0}" run: | - SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh + .ci/build.sh - name: Test run: | diff --git a/setup.py b/setup.py index 0b6b02077..7c1ad6dc5 100755 --- a/setup.py +++ b/setup.py @@ -515,6 +515,7 @@ class pil_build_ext(build_ext): elif sys.platform == "cygwin": # pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory + self.compiler.shared_lib_extension = ".dll.a" _add_directory( library_dirs, os.path.join( From b8719033ca91ef57f58128d32df675457431bbce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 May 2023 22:53:16 +1000 Subject: [PATCH 22/65] Removed unused INT64 definition --- src/libImaging/ImPlatform.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index af9996ca9..94781f9ec 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -58,12 +58,6 @@ #error Cannot find required 32-bit integer type #endif -#if SIZEOF_LONG == 8 -#define INT64 long -#elif SIZEOF_LONG_LONG == 8 -#define INT64 long -#endif - #define INT8 signed char #define UINT8 unsigned char From 922e239cca2a45d239dd02f0a4b85b72a7918917 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 May 2023 08:55:14 +1000 Subject: [PATCH 23/65] Fixed saving multiple 1 mode images to GIF --- Tests/test_file_gif.py | 13 +++++++++++++ src/PIL/GifImagePlugin.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 8522f486a..0e50ee1ab 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -252,6 +252,19 @@ def test_roundtrip_save_all(tmp_path): assert reread.n_frames == 5 +def test_roundtrip_save_all_1(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("1", (1, 1)) + im2 = Image.new("1", (1, 1), 1) + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reloaded: + assert reloaded.getpixel((0, 0)) == 0 + + reloaded.seek(1) + assert reloaded.getpixel((0, 0)) == 255 + + @pytest.mark.parametrize( "path, mode", ( diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index eadee1560..2f92e9467 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -879,7 +879,7 @@ def _get_palette_bytes(im): :param im: Image object :returns: Bytes, len<=768 suitable for inclusion in gif header """ - return im.palette.palette + return im.palette.palette if im.palette else b"" def _get_background(im, info_background): From 07eccd9798387a79db84557102d34de2f2f4c28d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 May 2023 19:14:56 +1000 Subject: [PATCH 24/65] Fixed calling putpalette() on L and LA images before load() --- Tests/test_image_putpalette.py | 8 ++++++++ src/libImaging/Unpack.c | 2 ++ 2 files changed, 10 insertions(+) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 3b29769a7..665e08a7e 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -32,6 +32,14 @@ def test_putpalette(): with pytest.raises(ValueError): palette("YCbCr") + with Image.open("Tests/images/hopper_gray.jpg") as im: + assert im.mode == "L" + im.putpalette(list(range(256)) * 3) + + with Image.open("Tests/images/la.tga") as im: + assert im.mode == "LA" + im.putpalette(list(range(256)) * 3) + def test_imagepalette(): im = hopper("P") diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index a0fa22c7d..206403ba6 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1552,10 +1552,12 @@ static struct { {"P", "P;4L", 4, unpackP4L}, {"P", "P", 8, copy1}, {"P", "P;R", 8, unpackLR}, + {"P", "L", 8, copy1}, /* palette w. alpha */ {"PA", "PA", 16, unpackLA}, {"PA", "PA;L", 16, unpackLAL}, + {"PA", "LA", 16, unpackLA}, /* true colour */ {"RGB", "RGB", 24, ImagingUnpackRGB}, From c45019fe0ccbf54c925aba914329371a7f188a48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 May 2023 12:28:03 +1000 Subject: [PATCH 25/65] Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 --- src/_imagingft.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index 78e3f7f10..80f862bb7 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -132,6 +132,27 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { return NULL; } +#if PY_MAJOR_VERSION > 3 || PY_MINOR_VERSION > 11 + PyConfig config; + PyConfig_InitPythonConfig(&config); + if (!PyArg_ParseTupleAndKeywords( + args, + kw, + "etf|nsy#n", + kwlist, + config.filesystem_encoding, + &filename, + &size, + &index, + &encoding, + &font_bytes, + &font_bytes_size, + &layout_engine)) { + PyConfig_Clear(&config); + return NULL; + } + PyConfig_Clear(&config); +#else if (!PyArg_ParseTupleAndKeywords( args, kw, @@ -147,6 +168,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { &layout_engine)) { return NULL; } +#endif self = PyObject_New(FontObject, &Font_Type); if (!self) { From e01a0195dd9f54b8174f322d47d4f618f0cf6c50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Jun 2023 22:53:07 +1000 Subject: [PATCH 26/65] Removed duplicate config --- .editorconfig | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index 449530717..d74549fe2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,10 +13,6 @@ indent_style = space trim_trailing_whitespace = true -[*.rst] -# Four-space indentation -indent_size = 4 - [*.yml] # Two-space indentation indent_size = 2 From ea3e4242d8fd8bb5cfc9e528f863ce16e20b529f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jun 2023 08:07:05 +1000 Subject: [PATCH 27/65] Removed files and types override --- .pre-commit-config.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4882a317f..0ddc6beb4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,9 +4,6 @@ repos: hooks: - id: black args: [--target-version=py38] - # Only .py files, until https://github.com/psf/black/issues/402 resolved - files: \.py$ - types: [] - repo: https://github.com/PyCQA/isort rev: 5.12.0 From 3693b84ba0b44f71119cec73c8517ec32d1774b5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jun 2023 09:21:47 +1000 Subject: [PATCH 28/65] Lint fixes --- docs/Guardfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Guardfile b/docs/Guardfile index b689b079a..6cbf07b06 100755 --- a/docs/Guardfile +++ b/docs/Guardfile @@ -2,7 +2,7 @@ from livereload.compiler import shell from livereload.task import Task -Task.add('*.rst', shell('make html')) -Task.add('*/*.rst', shell('make html')) -Task.add('Makefile', shell('make html')) -Task.add('conf.py', shell('make html')) +Task.add("*.rst", shell("make html")) +Task.add("*/*.rst", shell("make html")) +Task.add("Makefile", shell("make html")) +Task.add("conf.py", shell("make html")) From 2d0b13b812eea5238a8df6dc03b3fa4a6c55559e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 4 Jun 2023 22:37:17 +1000 Subject: [PATCH 29/65] Swapped config key and value --- _custom_build/backend.py | 31 +++++++++++++++++++++++++++---- docs/installation.rst | 22 +++++++++++----------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/_custom_build/backend.py b/_custom_build/backend.py index 31a954824..86fe60817 100755 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -7,14 +7,37 @@ from setuptools.build_meta import _BuildMetaBackend class _CustomBuildMetaBackend(_BuildMetaBackend): def run_setup(self, setup_script="setup.py"): if self.config_settings: - flags = [] - for key in ("enable", "disable", "vendor"): + + def config_has(key, value): settings = self.config_settings.get(key) if settings: if not isinstance(settings, list): settings = [settings] - for value in settings: - flags.append("--" + key + "-" + value) + return value in settings + + flags = [] + for dependency in ( + "zlib", + "jpeg", + "tiff", + "freetype", + "raqm", + "lcms", + "webp", + "webpmux", + "jpeg2000", + "imagequant", + "xcb", + ): + if config_has(dependency, "enable"): + flags.append("--enable-" + dependency) + elif config_has(dependency, "disable"): + flags.append("--disable-" + dependency) + for dependency in ("raqm", "fribidi"): + if config_has(dependency, "vendor"): + flags.append("--vendor-" + dependency) + if self.config_settings.get("platform-guessing") == "disable": + flags.append("--disable-platform-guessing") if self.config_settings.get("debug") == "true": flags.append("--debug") if flags: diff --git a/docs/installation.rst b/docs/installation.rst index 514d20e74..6720d2dce 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -380,28 +380,28 @@ Build Options using a setting of 1. By default, it uses 4 CPUs, or if 4 are not available, as many as are present. -* Config settings: ``-C disable=zlib``, ``-C disable=jpeg``, - ``-C disable=tiff``, ``-C disable=freetype``, ``-C disable=raqm``, - ``-C disable=lcms``, ``-C disable=webp``, ``-C disable=webpmux``, - ``-C disable=jpeg2000``, ``-C disable=imagequant``, ``-C disable=xcb``. +* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, + ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, + ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, + ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. Disable building the corresponding feature even if the development libraries are present on the building machine. -* Config settings: ``-C enable=zlib``, ``-C enable=jpeg``, - ``-C enable=tiff``, ``-C enable=freetype``, ``-C enable=raqm``, - ``-C enable=lcms``, ``-C enable=webp``, ``-C enable=webpmux``, - ``-C enable=jpeg2000``, ``-C enable=imagequant``, ``-C enable=xcb``. +* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, + ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, + ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, + ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. -* Config settings: ``-C vendor=raqm``, ``-C vendor=fribidi``. +* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. These flags are used to compile a modified version of libraqm and a shim that dynamically loads libfribidi at runtime. These are used to compile the standard Pillow wheels. Compiling libraqm requires a C99-compliant compiler. -* Build flag: ``-C disable=platform-guessing``. Skips all of the +* Build flag: ``-C platform-guessing=disable``. Skips all of the platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the environment variables (e.g. Buildroot). @@ -413,7 +413,7 @@ Build Options Sample usage:: - python3 -m pip install --upgrade Pillow -C enable=[feature] + python3 -m pip install --upgrade Pillow -C [feature]=enable Platform Support ---------------- From 97bd53392ce136617ead36c11d50def9d32ab3e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Jun 2023 18:36:41 +1000 Subject: [PATCH 30/65] Do not use temporary file when grabbing clipboard on Linux --- src/PIL/ImageGrab.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 7f6d50af4..39ecdf420 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -15,6 +15,7 @@ # See the README file for information on usage and redistribution. # +import io import os import shutil import subprocess @@ -128,8 +129,6 @@ def grabclipboard(): files = data[o:].decode("mbcs").split("\0") return files[: files.index("")] if isinstance(data, bytes): - import io - data = io.BytesIO(data) if fmt == "png": from . import PngImagePlugin @@ -159,13 +158,12 @@ def grabclipboard(): else: msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" raise NotImplementedError(msg) - fh, filepath = tempfile.mkstemp() - err = subprocess.run(args, stdout=fh, stderr=subprocess.PIPE).stderr - os.close(fh) + p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + err = p.stderr if err: msg = f"{args[0]} error: {err.strip().decode()}" raise ChildProcessError(msg) - im = Image.open(filepath) + data = io.BytesIO(p.stdout) + im = Image.open(data) im.load() - os.unlink(filepath) return im From 3b65261c966648e5d4f87cd49bb12cba5345547d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 17:54:55 +1000 Subject: [PATCH 31/65] Remove temporary file when error is raised --- src/PIL/EpsImagePlugin.py | 7 +++++++ src/PIL/JpegImagePlugin.py | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 1c88d22c7..bdac874c4 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -134,6 +134,13 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): if gs_windows_binary is not None: if not gs_windows_binary: + try: + os.unlink(outfile) + if infile_temp: + os.unlink(infile_temp) + except OSError: + pass + msg = "Unable to locate Ghostscript on paths" raise OSError(msg) command[0] = gs_windows_binary diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 5dd1a61af..dfc7e6e9f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -457,6 +457,11 @@ class JpegImageFile(ImageFile.ImageFile): if os.path.exists(self.filename): subprocess.check_call(["djpeg", "-outfile", path, self.filename]) else: + try: + os.unlink(path) + except OSError: + pass + msg = "Invalid Filename" raise ValueError(msg) From 97df237dc81c930d983b4025b7b3a97d043dfd7c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 18:04:39 +1000 Subject: [PATCH 32/65] Moved test into separate function --- Tests/test_file_apng.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index c62231cd4..a22ac581d 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -440,12 +440,6 @@ def test_apng_save_duration_loop(tmp_path): assert im.n_frames == 1 assert im.info.get("duration") == 750 - # test removal of duplicated frames with a single duration - frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) - with Image.open(test_file) as im: - assert im.n_frames == 1 - assert im.info.get("duration") == 1500 - # test info duration frame.info["duration"] = 750 frame.save(test_file, save_all=True) @@ -453,6 +447,17 @@ def test_apng_save_duration_loop(tmp_path): assert im.info.get("duration") == 750 +def test_apng_save_duplicate_duration(tmp_path): + test_file = str(tmp_path / "temp.png") + frame = Image.new("RGB", (1, 1)) + + # Test a single duration is correctly combined across duplicate frames + frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert im.info.get("duration") == 1500 + + def test_apng_save_disposal(tmp_path): test_file = str(tmp_path / "temp.png") size = (128, 64) From 7c533276f28518ec2825e1cae3f0df427b5c565b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 19:53:50 +1000 Subject: [PATCH 33/65] Update CHANGES.rst [ci skip] --- CHANGES.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 190751ad2..c51f8fb94 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,27 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed combining single duration across duplicate APNG frames #7146 + [radarhere] + +- Remove temporary file when error is raised #7148 + [radarhere] + +- Do not use temporary file when grabbing clipboard on Linux #7200 + [radarhere] + +- If the clipboard fails to open on Windows, wait and try again #7141 + [radarhere] + +- Fixed saving multiple 1 mode frames to GIF #7181 + [radarhere] + +- Replaced absolute PIL import with relative import #7173 + [radarhere] + +- Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 #7192 + [radarhere] + - Improved wl-paste mimetype handling in ImageGrab #7094 [rrcgat, radarhere] From 15edb6d625f94e0f7e9047ab76ed08762ab2f53a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Jun 2023 22:33:55 +1000 Subject: [PATCH 34/65] Fixed signedness comparison warning --- src/libImaging/Jpeg2KEncode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 0d7e896b7..de8586706 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -464,7 +464,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { } if (!context->num_resolutions) { - while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) { + while (tile_width < (1U << (params.numresolution - 1U)) || tile_height < (1U << (params.numresolution - 1U))) { params.numresolution -= 1; } } From da6b2ec28506a132fad9674e1badb1624aed7a8b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Jun 2023 10:47:20 +1000 Subject: [PATCH 35/65] Document order of kernel weights --- src/PIL/ImageFilter.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 63d6dcf5c..33bc7cc2e 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -35,7 +35,7 @@ class BuiltinFilter(MultibandFilter): class Kernel(BuiltinFilter): """ - Create a convolution kernel. The current version only + Create a convolution kernel. The current version only supports 3x3 and 5x5 integer and floating point kernels. In the current version, kernels can only be applied to @@ -43,9 +43,10 @@ class Kernel(BuiltinFilter): :param size: Kernel size, given as (width, height). In the current version, this must be (3,3) or (5,5). - :param kernel: A sequence containing kernel weights. + :param kernel: A sequence containing kernel weights. The kernel will + be flipped vertically before being applied to the image. :param scale: Scale factor. If given, the result for each pixel is - divided by this value. The default is the sum of the + divided by this value. The default is the sum of the kernel weights. :param offset: Offset. If given, this value is added to the result, after it has been divided by the scale factor. From 748a4d0fcd517e0e6e86ae15f4be9b0bcf65747d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 14:26:28 +1000 Subject: [PATCH 36/65] Removed unused variable --- src/libImaging/Storage.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 7cf00ef35..128595f65 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -37,8 +37,6 @@ #include "Imaging.h" #include -int ImagingNewCount = 0; - /* -------------------------------------------------------------------- * Standard image object. */ From aeb6e9909e94d1ad6c86ebf04a6db6cd77e016a3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 15:57:05 +1000 Subject: [PATCH 37/65] Removed unused argument --- src/PIL/Image.py | 2 +- src/_imaging.c | 5 ++--- src/libImaging/Filter.c | 2 +- src/libImaging/Imaging.h | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e0fb6a885..fa70f674b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1254,7 +1254,7 @@ class Image: if ymargin is None: ymargin = xmargin self.load() - return self._new(self.im.expand(xmargin, ymargin, 0)) + return self._new(self.im.expand(xmargin, ymargin)) def filter(self, filter): """ diff --git a/src/_imaging.c b/src/_imaging.c index 281f3a4d2..5c6380fee 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1027,12 +1027,11 @@ _crop(ImagingObject *self, PyObject *args) { static PyObject * _expand_image(ImagingObject *self, PyObject *args) { int x, y; - int mode = 0; - if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) { + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { return NULL; } - return PyImagingNew(ImagingExpand(self->image, x, y, mode)); + return PyImagingNew(ImagingExpand(self->image, x, y)); } static PyObject * diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 4b8d2bf05..4dcd368ca 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -49,7 +49,7 @@ clip32(float in) { } Imaging -ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { +ImagingExpand(Imaging imIn, int xmargin, int ymargin) { Imaging imOut; int x, y; ImagingSectionCookie cookie; diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index d9ded1852..beec8a8f2 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -290,7 +290,7 @@ ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b); extern Imaging ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); extern Imaging -ImagingExpand(Imaging im, int x, int y, int mode); +ImagingExpand(Imaging im, int x, int y); extern Imaging ImagingFill(Imaging im, const void *ink); extern int From 389ad11693deb5ea39b8edf0eb47263582a3f5f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 17:10:42 +1000 Subject: [PATCH 38/65] Only call text_layout once in getmask2 --- src/PIL/ImageFont.py | 34 +++---- src/_imagingft.c | 215 ++++++++++++++++++++++++++----------------- 2 files changed, 144 insertions(+), 105 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ea4549cf5..7b4ca5814 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -26,7 +26,6 @@ # import base64 -import math import os import sys import warnings @@ -551,28 +550,23 @@ class FreeTypeFont: :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ - size, offset = self.font.getsize( - text, mode, direction, features, language, anchor - ) if start is None: start = (0, 0) - size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) - offset = offset[0] - stroke_width, offset[1] - stroke_width + im, size, offset = self.font.render( + text, + Image.core.fill, + mode, + direction, + features, + language, + stroke_width, + anchor, + ink, + start[0], + start[1], + Image.MAX_IMAGE_PIXELS, + ) Image._decompression_bomb_check(size) - im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0) - if min(size): - self.font.render( - text, - im.id, - mode, - direction, - features, - language, - stroke_width, - ink, - start[0], - start[1], - ) return im, offset def font_variant( diff --git a/src/_imagingft.c b/src/_imagingft.c index 80f862bb7..95f12eb5a 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -551,73 +551,25 @@ font_getlength(FontObject *self, PyObject *args) { return PyLong_FromLong(length); } -static PyObject * -font_getsize(FontObject *self, PyObject *args) { +static int +bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, size_t count, int load_flags, int *width, int *height, int *x_offset, int *y_offset) { int position; /* pen position along primary axis, in 26.6 precision */ int advanced; /* pen position along primary axis, in pixels */ int px, py; /* position of current glyph, in pixels */ int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ - int load_flags; /* FreeType load_flags parameter */ int error; - FT_Face face; FT_Glyph glyph; - FT_BBox bbox; /* glyph bounding box */ - GlyphInfo *glyph_info = NULL; /* computed text layout */ - size_t i, count; /* glyph_info index and length */ - int horizontal_dir; /* is primary axis horizontal? */ - int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ - int color = 0; /* is FT_LOAD_COLOR enabled? */ - const char *mode = NULL; - const char *dir = NULL; - const char *lang = NULL; - const char *anchor = NULL; - PyObject *features = Py_None; - PyObject *string; - - /* calculate size and bearing for a given string */ - - if (!PyArg_ParseTuple( - args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { - return NULL; - } - - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; - - if (anchor == NULL) { - anchor = horizontal_dir ? "la" : "lt"; - } - if (strlen(anchor) != 2) { - goto bad_anchor; - } - - count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); - if (PyErr_Occurred()) { - return NULL; - } - - load_flags = FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - + FT_BBox bbox; /* glyph bounding box */ + size_t i; /* glyph_info index */ /* * text bounds are given by: * - bounding boxes of individual glyphs * - pen line, i.e. 0 to `advanced` along primary axis * this means point (0, 0) is part of the text bounding box */ - face = NULL; position = x_min = x_max = y_min = y_max = 0; for (i = 0; i < count; i++) { - face = self->face; - if (horizontal_dir) { px = PIXEL(position + glyph_info[i].x_offset); py = PIXEL(glyph_info[i].y_offset); @@ -640,12 +592,14 @@ font_getsize(FontObject *self, PyObject *args) { error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); if (error) { - return geterror(error); + geterror(error); + return 1; } error = FT_Get_Glyph(face->glyph, &glyph); if (error) { - return geterror(error); + geterror(error); + return 1; } FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); @@ -669,13 +623,15 @@ font_getsize(FontObject *self, PyObject *args) { FT_Done_Glyph(glyph); } - if (glyph_info) { - PyMem_Free(glyph_info); - glyph_info = NULL; + if (anchor == NULL) { + anchor = horizontal_dir ? "la" : "lt"; + } + if (strlen(anchor) != 2) { + goto bad_anchor; } x_anchor = y_anchor = 0; - if (face) { + if (count) { if (horizontal_dir) { switch (anchor[0]) { case 'l': // left @@ -693,15 +649,15 @@ font_getsize(FontObject *self, PyObject *args) { } switch (anchor[1]) { case 'a': // ascender - y_anchor = PIXEL(self->face->size->metrics.ascender); + y_anchor = PIXEL(face->size->metrics.ascender); break; case 't': // top y_anchor = y_max; break; case 'm': // middle (ascender + descender) / 2 y_anchor = PIXEL( - (self->face->size->metrics.ascender + - self->face->size->metrics.descender) / + (face->size->metrics.ascender + + face->size->metrics.descender) / 2); break; case 's': // horizontal baseline @@ -711,7 +667,7 @@ font_getsize(FontObject *self, PyObject *args) { y_anchor = y_min; break; case 'd': // descender - y_anchor = PIXEL(self->face->size->metrics.descender); + y_anchor = PIXEL(face->size->metrics.descender); break; default: goto bad_anchor; @@ -751,17 +707,74 @@ font_getsize(FontObject *self, PyObject *args) { } } } - - return Py_BuildValue( - "(ii)(ii)", - (x_max - x_min), - (y_max - y_min), - (-x_anchor + x_min), - -(-y_anchor + y_max)); + *width = x_max - x_min; + *height = y_max - y_min; + *x_offset = -x_anchor + x_min; + *y_offset = -(-y_anchor + y_max); + return 0; bad_anchor: PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); - return NULL; + return 1; +} + +static PyObject * +font_getsize(FontObject *self, PyObject *args) { + int width, height, x_offset, y_offset; + int load_flags; /* FreeType load_flags parameter */ + int error; + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t count; /* glyph_info length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; + const char *dir = NULL; + const char *lang = NULL; + const char *anchor = NULL; + PyObject *features = Py_None; + PyObject *string; + + /* calculate size and bearing for a given string */ + + if (!PyArg_ParseTuple( + args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { + return NULL; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { + return NULL; + } + + load_flags = FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + if (glyph_info) { + PyMem_Free(glyph_info); + glyph_info = NULL; + } + if (error) { + return NULL; + } + + return Py_BuildValue( + "(ii)(ii)", + width, + height, + x_offset, + y_offset); } static PyObject * @@ -785,6 +798,7 @@ font_render(FontObject *self, PyObject *args) { unsigned int bitmap_y; /* glyph bitmap y index */ unsigned char *source; /* glyph bitmap source buffer */ unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ + PyObject *image; Imaging im; Py_ssize_t id; int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ @@ -795,27 +809,34 @@ font_render(FontObject *self, PyObject *args) { const char *mode = NULL; const char *dir = NULL; const char *lang = NULL; + const char *anchor = NULL; PyObject *features = Py_None; PyObject *string; + PyObject *fill; float x_start = 0; float y_start = 0; + int width, height, x_offset, y_offset; + int horizontal_dir; /* is primary axis horizontal? */ + PyObject *max_image_pixels = Py_None; /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ if (!PyArg_ParseTuple( args, - "On|zzOziLff:render", + "OO|zzOzizLffO:render", &string, - &id, + &fill, &mode, &dir, &features, &lang, &stroke_width, + &anchor, &foreground_ink_long, &x_start, - &y_start)) { + &y_start, + &max_image_pixels)) { return NULL; } @@ -841,8 +862,41 @@ font_render(FontObject *self, PyObject *args) { if (PyErr_Occurred()) { return NULL; } - if (count == 0) { - Py_RETURN_NONE; + + load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + if (error) { + PyMem_Del(glyph_info); + return NULL; + } + + width += stroke_width * 2 + ceil(x_start); + height += stroke_width * 2 + ceil(y_start); + if (max_image_pixels != Py_None) { + if (width * height > PyLong_AsLong(max_image_pixels) * 2) { + PyMem_Del(glyph_info); + return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); + } + } + + image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height); + id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id")); + im = (Imaging)id; + + x_offset -= stroke_width; + y_offset -= stroke_width; + if (count == 0 || width == 0 || height == 0) { + PyMem_Del(glyph_info); + return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); } if (stroke_width) { @@ -859,15 +913,6 @@ font_render(FontObject *self, PyObject *args) { 0); } - im = (Imaging)id; - load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - /* * calculate x_min and y_max * must match font_getsize or there may be clipping! @@ -1064,7 +1109,7 @@ font_render(FontObject *self, PyObject *args) { } FT_Stroker_Done(stroker); PyMem_Del(glyph_info); - Py_RETURN_NONE; + return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); glyph_error: if (stroker != NULL) { From 4dcca33d3099e29110b24cc507e8f0799e1d1ab7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 19:06:25 +1000 Subject: [PATCH 39/65] Removed unused arguments --- src/_imagingft.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 80f862bb7..8fc1fa7d0 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -254,9 +254,7 @@ text_layout_raqm( const char *dir, PyObject *features, const char *lang, - GlyphInfo **glyph_info, - int mask, - int color) { + GlyphInfo **glyph_info) { size_t i = 0, count = 0, start = 0; raqm_t *rq; raqm_glyph_t *glyphs = NULL; @@ -493,7 +491,7 @@ text_layout( #ifdef HAVE_RAQM if (have_raqm && self->layout_engine == LAYOUT_RAQM) { count = text_layout_raqm( - string, self, dir, features, lang, glyph_info, mask, color); + string, self, dir, features, lang, glyph_info); } else #endif { From 16d82c2dfd473836e7903165917584bf938b0345 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 19:37:54 +1000 Subject: [PATCH 40/65] Improved coverage --- Tests/test_imagefont.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 7ea485a55..4a40d1d1d 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -463,6 +463,11 @@ def test_default_font(): assert_image_equal_tofile(im, "Tests/images/default_font.png") +@pytest.mark.parametrize("mode", (None, "1", "RGBA")) +def test_getbbox(font, mode): + assert (0, 4, 12, 16) == font.getbbox("A", mode) + + def test_getbbox_empty(font): # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") From 1756df461561234184611dfe8b42f1b7f33de1f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 20:24:34 +1000 Subject: [PATCH 41/65] Removed unused private method --- src/PIL/ImageFont.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ea4549cf5..abcb88520 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -226,10 +226,6 @@ class FreeTypeFont: path, size, index, encoding, layout_engine = state self.__init__(path, size, index, encoding, layout_engine) - def _multiline_split(self, text): - split_character = "\n" if isinstance(text, str) else b"\n" - return text.split(split_character) - def getname(self): """ :return: A tuple of the font family (e.g. Helvetica) and the font style From 5a0fb8ec127b7b23d13d22d7a8ade5505835435f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 11 Jun 2023 00:05:47 +0300 Subject: [PATCH 42/65] Add Debian 12 Bookworm --- .github/workflows/test-docker.yml | 1 + docs/installation.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 4f01abe44..3bcb8cfbc 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -39,6 +39,7 @@ jobs: centos-stream-8-amd64, centos-stream-9-amd64, debian-11-bullseye-x86, + debian-12-bookworm-x86, fedora-37-amd64, fedora-38-amd64, gentoo, diff --git a/docs/installation.rst b/docs/installation.rst index ad27b67ee..ac54b037d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -448,6 +448,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ +| Debian 12 Bookworm | 3.11 | x86 | ++----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 38 | 3.11 | x86-64 | From c24c1ccf8163c90dfbc3110490846d3113d3418a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 08:52:55 +1000 Subject: [PATCH 43/65] Use "not in" Co-authored-by: Aarni Koskela --- src/PIL/GdImageFile.py | 2 +- src/PIL/Image.py | 2 +- src/PIL/features.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 7dda4f143..bafc43a19 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -47,7 +47,7 @@ class GdImageFile(ImageFile.ImageFile): # Header s = self.fp.read(1037) - if not i16(s) in [65534, 65535]: + if i16(s) not in [65534, 65535]: msg = "Not a valid GD 2.x .gd file" raise SyntaxError(msg) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index fa70f674b..66b0f0e06 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1731,7 +1731,7 @@ class Image: if not isinstance(dest, (list, tuple)): msg = "Destination must be a tuple" raise ValueError(msg) - if not len(source) in (2, 4): + if len(source) not in (2, 4): msg = "Source must be a 2 or 4-tuple" raise ValueError(msg) if not len(dest) == 2: diff --git a/src/PIL/features.py b/src/PIL/features.py index 80a16a75e..f14e60cf5 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -24,7 +24,7 @@ def check_module(feature): :returns: ``True`` if available, ``False`` otherwise. :raises ValueError: If the module is not defined in this version of Pillow. """ - if not (feature in modules): + if feature not in modules: msg = f"Unknown module {feature}" raise ValueError(msg) From 538971532da065ada6528d441ea39693e108ebf0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 08:55:21 +1000 Subject: [PATCH 44/65] Corrected error code Co-authored-by: nulano --- src/_imagingft.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 25a4d3517..dbea673f9 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -189,7 +189,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { /* Don't free this before FT_Done_Face */ self->font_bytes = PyMem_Malloc(font_bytes_size); if (!self->font_bytes) { - error = 65; // Out of Memory in Freetype. + error = FT_Err_Out_Of_Memory; } if (!error) { memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); From f338f35657baad30295b45353d699cace83d3699 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 10:01:36 +1000 Subject: [PATCH 45/65] Changed inPlace to be keyword-only argument --- src/PIL/ImageOps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index facc30ba0..752d132a3 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -576,13 +576,13 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image, inPlace=False): +def exif_transpose(image, *, inPlace=False): """ If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data. :param image: The image to transpose. - :param inPlace: Boolean. + :param inPlace: Boolean. Keyword-only argument. If ``True``, the original image is modified in-place, and ``None`` is returned. If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned with the transposition applied. If there is no transposition, a copy of the From 187e9a46af160b6510c0262ef1f52e4c35ce579e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 11:21:43 +1000 Subject: [PATCH 46/65] Improved documention of "corners" argument for rounded_rectangle --- docs/reference/ImageDraw.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 524f821fb..31f63695e 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -328,7 +328,7 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1) +.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1, corners=None) Draws a rounded rectangle. @@ -341,6 +341,7 @@ Methods :param width: The line width, in pixels. :param corners: A tuple of whether to round each corner, ``(top_left, top_right, bottom_right, bottom_left)``. + Keyword-only argument. .. versionadded:: 8.2.0 From 38d63868bf395ac1eb4869bc415de424f0863e45 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 15:46:24 +1000 Subject: [PATCH 47/65] Do not import internal class --- _custom_build/backend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/_custom_build/backend.py b/_custom_build/backend.py index 86fe60817..9b3265a94 100755 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -1,10 +1,12 @@ import sys from setuptools.build_meta import * # noqa: F401, F403 -from setuptools.build_meta import _BuildMetaBackend +from setuptools.build_meta import build_wheel + +backend_class = build_wheel.__self__.__class__ -class _CustomBuildMetaBackend(_BuildMetaBackend): +class _CustomBuildMetaBackend(backend_class): def run_setup(self, setup_script="setup.py"): if self.config_settings: From 7d97fa8b86a9f61069579fbedb2cc09fb437b12f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 16:12:47 +1000 Subject: [PATCH 48/65] Use snake case --- Tests/test_imageops.py | 4 ++-- src/PIL/ImageOps.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index e7d04cceb..b05785be0 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -404,13 +404,13 @@ def test_exif_transpose(): assert 0x0112 not in transposed_im.getexif() -def test_exif_transpose_inplace(): +def test_exif_transpose_in_place(): with Image.open("Tests/images/orientation_rectangle.jpg") as im: assert im.size == (2, 1) assert im.getexif()[0x0112] == 8 expected = im.rotate(90, expand=True) - ImageOps.exif_transpose(im, inPlace=True) + ImageOps.exif_transpose(im, in_place=True) assert im.size == (1, 2) assert 0x0112 not in im.getexif() assert_image_equal(im, expected) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 752d132a3..17702778c 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -576,13 +576,13 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image, *, inPlace=False): +def exif_transpose(image, *, in_place=False): """ If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data. :param image: The image to transpose. - :param inPlace: Boolean. Keyword-only argument. + :param in_place: Boolean. Keyword-only argument. If ``True``, the original image is modified in-place, and ``None`` is returned. If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned with the transposition applied. If there is no transposition, a copy of the @@ -601,11 +601,11 @@ def exif_transpose(image, *, inPlace=False): }.get(orientation) if method is not None: transposed_image = image.transpose(method) - if inPlace: + if in_place: image.im = transposed_image.im image.pyaccess = None image._size = transposed_image._size - exif_image = image if inPlace else transposed_image + exif_image = image if in_place else transposed_image exif = exif_image.getexif() if ExifTags.Base.Orientation in exif: @@ -622,7 +622,7 @@ def exif_transpose(image, *, inPlace=False): exif_image.info["XML:com.adobe.xmp"] = re.sub( pattern, "", exif_image.info["XML:com.adobe.xmp"] ) - if not inPlace: + if not in_place: return transposed_image - elif not inPlace: + elif not in_place: return image.copy() From b2b05f3b83ade2065b98d310214e6f49f04f57f5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 18:55:19 +1000 Subject: [PATCH 49/65] Moved QOI from Write-Only to Read-Only --- docs/handbook/image-file-formats.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 74ba883b1..bbcf48e42 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1380,6 +1380,12 @@ PSD Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. +QOI +^^^ + +.. versionadded:: 9.5.0 + +Pillow identifies and reads images in Quite OK Image format. SUN ^^^ @@ -1562,13 +1568,6 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 5.3.0 -QOI -^^^ - -.. versionadded:: 9.5.0 - -Pillow identifies and reads images in Quite OK Image format. - XV Thumbnails ^^^^^^^^^^^^^ From 594fbf79b8a30a9fd3be1171038b5d787f4d00cc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 23:01:45 +1000 Subject: [PATCH 50/65] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c51f8fb94..4af3fa516 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Added in_place argument to ImageOps.exif_transpose() #7092 + [radarhere] + +- Fixed calling putpalette() on L and LA images before load() #7187 + [radarhere] + +- Fixed saving TIFF multiframe images with LONG8 tag types #7078 + [radarhere] + - Fixed combining single duration across duplicate APNG frames #7146 [radarhere] From 618c00c4ea64ddd40f3485db549f1549f7f27b04 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Jun 2023 14:27:33 +1000 Subject: [PATCH 51/65] Return early if image is null --- src/_imagingft.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index dbea673f9..d4422a43d 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -887,6 +887,10 @@ font_render(FontObject *self, PyObject *args) { } image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height); + if (image == NULL) { + PyMem_Del(glyph_info); + return NULL; + } id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id")); im = (Imaging)id; From 98cc2e63ac9c78e57476d563a70a27397fea87e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Jun 2023 12:52:57 +1000 Subject: [PATCH 52/65] Destroy image on error --- src/_imagingft.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index d4422a43d..02d54fe23 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -904,7 +904,8 @@ font_render(FontObject *self, PyObject *args) { if (stroke_width) { error = FT_Stroker_New(library, &stroker); if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } FT_Stroker_Set( @@ -927,7 +928,8 @@ font_render(FontObject *self, PyObject *args) { error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } glyph_slot = self->face->glyph; @@ -958,7 +960,8 @@ font_render(FontObject *self, PyObject *args) { error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags); if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } glyph_slot = self->face->glyph; @@ -972,7 +975,8 @@ font_render(FontObject *self, PyObject *args) { error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1); } if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } bitmap_glyph = (FT_BitmapGlyph)glyph; @@ -1114,6 +1118,12 @@ font_render(FontObject *self, PyObject *args) { return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); glyph_error: + if (im->destroy) { + im->destroy(im); + } + if (im->image) { + free(im->image); + } if (stroker != NULL) { FT_Done_Glyph(glyph); } From 43b693972a4b2f5ffe00ecb21ec9cc46ab7a3352 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Jun 2023 00:25:59 +1000 Subject: [PATCH 53/65] Added PyPy 3.10 and removed PyPy 3.8 --- .github/workflows/test-windows.yml | 4 ++-- .github/workflows/test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 076b80839..cab47b01f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -28,10 +28,10 @@ jobs: architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows - - python-version: "pypy3.8" - architecture: "x64" - python-version: "pypy3.9" architecture: "x64" + - python-version: "pypy3.10" + architecture: "x64" timeout-minutes: 30 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index afb8fb56c..893c0d12c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,8 +29,8 @@ jobs: "ubuntu-latest", ] python-version: [ + "pypy3.10", "pypy3.9", - "pypy3.8", "3.12-dev", "3.11", "3.10", From 7044038e701fd777bf2c7dc38b02c4d944b086ba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Jun 2023 14:35:44 +1000 Subject: [PATCH 54/65] Fixed decompression bomb check --- ...om-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf | Bin 0 -> 30 bytes Tests/test_imagefont.py | 1 + src/_imagingft.c | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf diff --git a/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf b/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fe200842e41daa66ddb9a3ea5137726681bc2d2f GIT binary patch literal 30 kcmZQ%U}NO`&A`CG$jHcNpjXjl(8$C7=N4O!AW)0}08Ie}y8r+H literal 0 HcmV?d00001 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 4a40d1d1d..7fa8ff8cb 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1042,6 +1042,7 @@ def test_render_mono_size(): "test_file", [ "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf", + "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", ], ) def test_oom(test_file): diff --git a/src/_imagingft.c b/src/_imagingft.c index 02d54fe23..d421e5a0b 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { width += stroke_width * 2 + ceil(x_start); height += stroke_width * 2 + ceil(y_start); if (max_image_pixels != Py_None) { - if (width * height > PyLong_AsLong(max_image_pixels) * 2) { + if ((long long)width * height > PyLong_AsLong(max_image_pixels) * 2) { PyMem_Del(glyph_info); return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); } From fd9bea271a521e1ce054b13c723e9e47a759187e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 17 Jun 2023 14:39:34 +1000 Subject: [PATCH 55/65] Compare long long with long long MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/_imagingft.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index d421e5a0b..6cee021d4 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { width += stroke_width * 2 + ceil(x_start); height += stroke_width * 2 + ceil(y_start); if (max_image_pixels != Py_None) { - if ((long long)width * height > PyLong_AsLong(max_image_pixels) * 2) { + if ((long long)width * height > PyLong_AsLongLong(max_image_pixels) * 2) { PyMem_Del(glyph_info); return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); } From f72dd8576ee77adec988dc1fe9777ee25f581a05 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Sat, 17 Jun 2023 12:55:58 +0200 Subject: [PATCH 56/65] Changed `grabclipboard()` to use PNG compression on macOS Before, a lossy JPG compression was used. --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 39ecdf420..4de5c69fb 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -102,7 +102,7 @@ def grabclipboard(): + filepath + '" with write permission)', "try", - " write (the clipboard as JPEG picture) to theFile", + " write (the clipboard as «class PNGf») to theFile", "end try", "close access theFile", ] From 3c4ccdcff54a7890176e57b5daf6757891678110 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Sat, 17 Jun 2023 12:59:42 +0200 Subject: [PATCH 57/65] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4af3fa516..5da3986ba 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Changed grabclipboard() to use PNG instead of JPG compression on macOS #7219 + [abey79] + - Added in_place argument to ImageOps.exif_transpose() #7092 [radarhere] From e52fa8fe386d5503bd3abb64773be208edcc58ca Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Jun 2023 21:01:52 +1000 Subject: [PATCH 58/65] Use relevant extension for temporary file --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 4de5c69fb..927033c60 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -95,7 +95,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N def grabclipboard(): if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".jpg") + fh, filepath = tempfile.mkstemp(".png") os.close(fh) commands = [ 'set theFile to (open for access POSIX file "' From 0440df0d83a7bfd92df130893432830e7d9b4183 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Jun 2023 20:14:23 +1000 Subject: [PATCH 59/65] Clarify that the changelog should not be updated in PRs [ci skip] --- .github/CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ba2b7d8ed..d03fcf0d9 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,6 +19,7 @@ Please send a pull request to the `main` branch. Please include [documentation]( - Follow PEP 8. - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. - Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. +- Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged. ## Reporting Issues From f28ecc5808ba1c562ebf7c557d7817f3fe92823d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Jun 2023 17:45:24 +1000 Subject: [PATCH 60/65] Document how to install on MinGW when setuptools >= 60 --- docs/installation.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index ac54b037d..dc1cd8653 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -312,6 +312,11 @@ Many of Pillow's features require external libraries: mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libraqm + https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with + MSYS2. To workaround this, before installing Pillow you must run:: + + export SETUPTOOLS_USE_DISTUTILS=stdlib + .. tab:: FreeBSD .. Note:: Only FreeBSD 10 and 11 tested From 56a795c8ddaa203ebb3bed6df99c96d60b366199 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 22 Jun 2023 09:05:03 -0500 Subject: [PATCH 61/65] add units to bench_cffi_access.py output --- Tests/bench_cffi_access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 87cad699d..49ff34949 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -28,7 +28,7 @@ def timer(func, label, *args): func(*args) if time.time() - starttime > 10: print( - "{}: breaking at {} iterations, {:.6f} per iteration".format( + "{}: breaking at {} iterations, {:.6f}s per iteration".format( label, x + 1, (time.time() - starttime) / (x + 1.0) ) ) @@ -36,7 +36,7 @@ def timer(func, label, *args): if x == iterations - 1: endtime = time.time() print( - "{}: {:.4f} s {:.6f} per iteration".format( + "{}: {:.4f}s total, {:.6f}s per iteration".format( label, endtime - starttime, (endtime - starttime) / (x + 1.0) ) ) From ff4c7ffceaa1a67d036375d44020702347c65cbf Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 22 Jun 2023 09:16:18 -0500 Subject: [PATCH 62/65] use same print format regardless of iterations --- Tests/bench_cffi_access.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 49ff34949..69ebef9b4 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -27,25 +27,19 @@ def timer(func, label, *args): for x in range(iterations): func(*args) if time.time() - starttime > 10: - print( - "{}: breaking at {} iterations, {:.6f}s per iteration".format( - label, x + 1, (time.time() - starttime) / (x + 1.0) - ) - ) break - if x == iterations - 1: - endtime = time.time() - print( - "{}: {:.4f}s total, {:.6f}s per iteration".format( - label, endtime - starttime, (endtime - starttime) / (x + 1.0) - ) + endtime = time.time() + print( + "{}: completed {} iterations in {:.4f}s, {:.6f}s per iteration".format( + label, x + 1, endtime - starttime, (endtime - starttime) / (x + 1.0) ) + ) def test_direct(): im = hopper() im.load() - # im = Image.new( "RGB", (2000, 2000), (1, 3, 2)) + # im = Image.new("RGB", (2000, 2000), (1, 3, 2)) caccess = im.im.pixel_access(False) access = PyAccess.new(im, False) From 43a12542ad83c8da7aa138fbf376b22895775e37 Mon Sep 17 00:00:00 2001 From: Rozie <60040522+RoziePlaysPython@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:05:28 +0300 Subject: [PATCH 63/65] Update Image.show docs to list all viewers used on linux [ci skip] Accurate description of how default viewer is chosen --- 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 a785292f8..c19bdcac8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2451,7 +2451,7 @@ class Image: The image is first saved to a temporary file. By default, it will be in PNG format. - On Unix, the image is then opened using the **display**, **eog** or + On Unix, the image is then opened using the **xdg-open**, **display**, **gm**, **eog** or **xv** utility, depending on which one can be found. On macOS, the image is opened with the native Preview application. From b0b079882047ca2853714ba899ef719cc2ed70f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 23 Jun 2023 22:22:33 +1000 Subject: [PATCH 64/65] Lint fix --- 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 c19bdcac8..97f3f4926 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2451,8 +2451,8 @@ class Image: The image is first saved to a temporary file. By default, it will be in PNG format. - On Unix, the image is then opened using the **xdg-open**, **display**, **gm**, **eog** or - **xv** utility, depending on which one can be found. + On Unix, the image is then opened using the **xdg-open**, **display**, + **gm**, **eog** or **xv** utility, depending on which one can be found. On macOS, the image is opened with the native Preview application. From 6c464b810185494df6cd1c64ed21ae455ebc93c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 13:09:07 +1000 Subject: [PATCH 65/65] Update CHANGES.rst [ci skip] --- CHANGES.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5da3986ba..c05268772 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,8 +5,11 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed finding dependencies on Cygwin #7175 + [radarhere] + - Changed grabclipboard() to use PNG instead of JPG compression on macOS #7219 - [abey79] + [abey79, radarhere] - Added in_place argument to ImageOps.exif_transpose() #7092 [radarhere]