From 7f977f65229cd7037bfc4896fccc22b2c279d749 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 May 2024 09:10:28 +1000 Subject: [PATCH 001/652] Updated xz to 5.6.2 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0d45d5a20..0df3bf179 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -20,7 +20,7 @@ HARFBUZZ_VERSION=8.4.0 LIBPNG_VERSION=1.6.43 JPEGTURBO_VERSION=3.0.2 OPENJPEG_VERSION=2.5.2 -XZ_VERSION=5.4.5 +XZ_VERSION=5.6.2 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 if [[ -n "$IS_MACOS" ]]; then diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0d6da7754..1df8db17b 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -120,7 +120,7 @@ V = { "LIBWEBP": "1.3.2", "OPENJPEG": "2.5.2", "TIFF": "4.6.0", - "XZ": "5.4.5", + "XZ": "5.6.2", "ZLIB": "1.3.1", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") From 142cb11dcce7b82df56c018a457e6d9876c7397e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 13 Jun 2024 10:58:51 +1000 Subject: [PATCH 002/652] Use GitHub URL for xz --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1df8db17b..0bc739ea8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -176,7 +176,7 @@ DEPS = { "libs": [r"*.lib"], }, "xz": { - "url": f"{SF_PROJECTS}/lzmautils/files/xz-{V['XZ']}.tar.gz/download", + "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/xz-{V['XZ']}.tar.gz", "filename": f"xz-{V['XZ']}.tar.gz", "dir": f"xz-{V['XZ']}", "license": "COPYING", From d3f343bb9b658ff97cbac826f9d1d4763a352e85 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jul 2024 19:36:15 +1000 Subject: [PATCH 003/652] Skip truncated mask if LOAD_TRUNCATED_IMAGES is enabled --- Tests/test_file_ico.py | 28 +++++++++++++++++++++++- src/PIL/IcoImagePlugin.py | 45 +++++++++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index fa8c11d5a..37770498a 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -6,7 +6,7 @@ from pathlib import Path import pytest -from PIL import IcoImagePlugin, Image, ImageDraw +from PIL import IcoImagePlugin, Image, ImageDraw, ImageFile from .helper import assert_image_equal, assert_image_equal_tofile, hopper @@ -241,3 +241,29 @@ def test_draw_reloaded(tmp_path: Path) -> None: with Image.open(outfile) as im: assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") + + +def test_truncated_mask() -> None: + # 1 bpp + with open("Tests/images/hopper_mask.ico", "rb") as fp: + data = fp.read() + + ImageFile.LOAD_TRUNCATED_IMAGES = True + data = data[:-3] + + try: + with Image.open(io.BytesIO(data)) as im: + with Image.open("Tests/images/hopper_mask.png") as expected: + assert im.mode == "1" + + # 32 bpp + output = io.BytesIO() + expected = hopper("RGBA") + expected.save(output, "ico", bitmap_format="bmp") + + data = output.getvalue()[:-1] + + with Image.open(io.BytesIO(data)) as im: + assert im.mode == "RGB" + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 086c87b1a..f331caad7 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -235,13 +235,19 @@ class IcoFile: alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4] # convert to an 8bpp grayscale image - mask = Image.frombuffer( - "L", # 8bpp - im.size, # (w, h) - alpha_bytes, # source chars - "raw", # raw decoder - ("L", 0, -1), # 8bpp inverted, unpadded, reversed - ) + try: + mask = Image.frombuffer( + "L", # 8bpp + im.size, # (w, h) + alpha_bytes, # source chars + "raw", # raw decoder + ("L", 0, -1), # 8bpp inverted, unpadded, reversed + ) + except ValueError: + if ImageFile.LOAD_TRUNCATED_IMAGES: + mask = None + else: + raise else: # get AND image from end of bitmap w = im.size[0] @@ -259,19 +265,26 @@ class IcoFile: mask_data = self.buf.read(total_bytes) # convert raw data to image - mask = Image.frombuffer( - "1", # 1 bpp - im.size, # (w, h) - mask_data, # source chars - "raw", # raw decoder - ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed - ) + try: + mask = Image.frombuffer( + "1", # 1 bpp + im.size, # (w, h) + mask_data, # source chars + "raw", # raw decoder + ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed + ) + except ValueError: + if ImageFile.LOAD_TRUNCATED_IMAGES: + mask = None + else: + raise # now we have two images, im is XOR image and mask is AND image # apply mask image as alpha channel - im = im.convert("RGBA") - im.putalpha(mask) + if mask: + im = im.convert("RGBA") + im.putalpha(mask) return im From a34ac045c3cf191a55e52797263beb352c8edb2c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jul 2024 21:05:48 +1000 Subject: [PATCH 004/652] Conditionally define type hint to avoid requiring core --- src/PIL/ImageCms.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index ec10230f1..ce5b6101b 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -31,6 +31,10 @@ from ._typing import SupportsRead try: from . import _imagingcms as core + + _CmsProfileCompatible = Union[ + str, SupportsRead[bytes], core.CmsProfile, "ImageCmsProfile" + ] except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. @@ -391,10 +395,6 @@ def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile | # pyCMS compatible layer # --------------------------------------------------------------------. -_CmsProfileCompatible = Union[ - str, SupportsRead[bytes], core.CmsProfile, ImageCmsProfile -] - class PyCMSError(Exception): """(pyCMS) Exception class. From 42bfcaa2d4371b7a1312848c0f5f6cfefcaaa9cb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jul 2024 12:46:15 +1000 Subject: [PATCH 005/652] When IFD is missing, connect get_ifd() dictionary to Exif --- Tests/test_image.py | 8 ++++++++ src/PIL/Image.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 4fdc41791..77be58261 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -769,6 +769,14 @@ class TestImage: exif.load(b"Exif\x00\x00") assert not dict(exif) + def test_empty_get_ifd(self) -> None: + exif = Image.Exif() + ifd = exif.get_ifd(0x8769) + assert ifd == {} + + ifd[36864] = b"0220" + assert exif.get_ifd(0x8769) == {36864: b"0220"} + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 565abe71d..9a3e4efa4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -4073,7 +4073,7 @@ class Exif(_ExifBase): else: # Interop self._ifds[tag] = self._get_ifd_dict(tag_data, tag) - ifd = self._ifds.get(tag, {}) + ifd = self._ifds.setdefault(tag, {}) if tag == ExifTags.IFD.Exif and self._hidden_data: ifd = { k: v From 8e730e17ae5608616e57194a25629c48ad033cfa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jul 2024 13:00:58 +1000 Subject: [PATCH 006/652] Allow saving I;16 images as PPM --- Tests/test_file_ppm.py | 2 ++ src/PIL/PpmImagePlugin.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index d6451ec18..fa5027448 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -95,7 +95,9 @@ def test_16bit_pgm_write(tmp_path: Path) -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: filename = str(tmp_path / "temp.pgm") im.save(filename, "PPM") + assert_image_equal_tofile(im, filename) + im.convert("I;16").save(filename, "PPM") assert_image_equal_tofile(im, filename) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 16c9ccbba..211577e16 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -333,7 +333,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: rawmode, head = "1;I", b"P4" elif im.mode == "L": rawmode, head = "L", b"P5" - elif im.mode == "I": + elif im.mode in ("I", "I;16"): rawmode, head = "I;16B", b"P5" elif im.mode in ("RGB", "RGBA"): rawmode, head = "RGB", b"P6" From ea7b5c5b667b0bc80a70f9ca4ee123e82450c123 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 16 Jul 2024 10:13:26 +0200 Subject: [PATCH 007/652] Lock around usages of imaging memory arenas --- src/_imaging.c | 48 +++++++++++++++++++++++++++++++--------- src/libImaging/Imaging.h | 12 ++++++++++ src/libImaging/Storage.c | 20 ++++++++++++----- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index ac6310a44..66709d9bc 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -92,6 +92,7 @@ #define _USE_MATH_DEFINES #include +#include /* Configuration stuff. Feel free to undef things you don't need. */ #define WITH_IMAGECHOPS /* ImageChops support */ @@ -3934,7 +3935,6 @@ static PyObject * _get_stats(PyObject *self, PyObject *args) { PyObject *d; PyObject *v; - ImagingMemoryArena arena = &ImagingDefaultArena; if (!PyArg_ParseTuple(args, ":get_stats")) { return NULL; @@ -3944,6 +3944,10 @@ _get_stats(PyObject *self, PyObject *args) { if (!d) { return NULL; } + + MUTEX_LOCK(&ImagingDefaultArena.mutex); + ImagingMemoryArena arena = &ImagingDefaultArena; + v = PyLong_FromLong(arena->stats_new_count); PyDict_SetItemString(d, "new_count", v ? v : Py_None); Py_XDECREF(v); @@ -3967,22 +3971,25 @@ _get_stats(PyObject *self, PyObject *args) { v = PyLong_FromLong(arena->blocks_cached); PyDict_SetItemString(d, "blocks_cached", v ? v : Py_None); Py_XDECREF(v); + + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); return d; } static PyObject * _reset_stats(PyObject *self, PyObject *args) { - ImagingMemoryArena arena = &ImagingDefaultArena; - if (!PyArg_ParseTuple(args, ":reset_stats")) { return NULL; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); + ImagingMemoryArena arena = &ImagingDefaultArena; arena->stats_new_count = 0; arena->stats_allocated_blocks = 0; arena->stats_reused_blocks = 0; arena->stats_reallocated_blocks = 0; arena->stats_freed_blocks = 0; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); Py_INCREF(Py_None); return Py_None; @@ -3994,7 +4001,10 @@ _get_alignment(PyObject *self, PyObject *args) { return NULL; } - return PyLong_FromLong(ImagingDefaultArena.alignment); + MUTEX_LOCK(&ImagingDefaultArena.mutex); + int alignment = ImagingDefaultArena.alignment; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + return PyLong_FromLong(alignment); } static PyObject * @@ -4003,7 +4013,10 @@ _get_block_size(PyObject *self, PyObject *args) { return NULL; } - return PyLong_FromLong(ImagingDefaultArena.block_size); + MUTEX_LOCK(&ImagingDefaultArena.mutex); + int block_size = ImagingDefaultArena.block_size; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + return PyLong_FromLong(block_size); } static PyObject * @@ -4012,7 +4025,10 @@ _get_blocks_max(PyObject *self, PyObject *args) { return NULL; } - return PyLong_FromLong(ImagingDefaultArena.blocks_max); + MUTEX_LOCK(&ImagingDefaultArena.mutex); + int blocks_max = ImagingDefaultArena.blocks_max; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + return PyLong_FromLong(blocks_max); } static PyObject * @@ -4032,7 +4048,9 @@ _set_alignment(PyObject *self, PyObject *args) { return NULL; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingDefaultArena.alignment = alignment; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); Py_INCREF(Py_None); return Py_None; @@ -4055,7 +4073,9 @@ _set_block_size(PyObject *self, PyObject *args) { return NULL; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingDefaultArena.block_size = block_size; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); Py_INCREF(Py_None); return Py_None; @@ -4071,14 +4091,20 @@ _set_blocks_max(PyObject *self, PyObject *args) { if (blocks_max < 0) { PyErr_SetString(PyExc_ValueError, "blocks_max should be greater than 0"); return NULL; - } else if ( - (unsigned long)blocks_max > - SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { + } + + MUTEX_LOCK(&ImagingDefaultArena.mutex); + size_t blocksize = sizeof(ImagingDefaultArena.blocks_pool[0]); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + if ((unsigned long)blocks_max > SIZE_MAX / blocksize) { PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); return NULL; } - if (!ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max)) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); + int status = ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + if (!status) { return ImagingError_MemoryError(); } @@ -4094,7 +4120,9 @@ _clear_cache(PyObject *self, PyObject *args) { return NULL; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingMemoryClearCache(&ImagingDefaultArena, i); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); Py_INCREF(Py_None); return Py_None; diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 1f2c03e93..283111216 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -161,6 +161,9 @@ typedef struct ImagingMemoryArena { int stats_reallocated_blocks; /* Number of blocks which were actually reallocated after retrieving */ int stats_freed_blocks; /* Number of freed blocks */ +#ifdef Py_GIL_DISABLED + PyMutex mutex; +#endif } *ImagingMemoryArena; /* Objects */ @@ -690,6 +693,15 @@ _imaging_tell_pyFd(PyObject *fd); #include "ImagingUtils.h" extern UINT8 *clip8_lookups; +/* Mutex lock/unlock helpers */ +#ifdef Py_GIL_DISABLED +#define MUTEX_LOCK(m) PyMutex_Lock(m) +#define MUTEX_UNLOCK(m) PyMutex_Unlock(m) +#else +#define MUTEX_LOCK(m) +#define MUTEX_UNLOCK(m) +#endif + #if defined(__cplusplus) } #endif diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index b27195a35..9331ed5d5 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -219,7 +219,9 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { break; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingDefaultArena.stats_new_count += 1; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); return im; } @@ -267,7 +269,8 @@ struct ImagingMemoryArena ImagingDefaultArena = { 0, 0, 0, - 0 // Stats + 0, // Stats + {0}, }; int @@ -365,7 +368,9 @@ ImagingDestroyArray(Imaging im) { if (im->blocks) { while (im->blocks[y].ptr) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); memory_return_block(&ImagingDefaultArena, im->blocks[y]); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); y += 1; } free(im->blocks); @@ -373,9 +378,8 @@ ImagingDestroyArray(Imaging im) { } Imaging -ImagingAllocateArray(Imaging im, int dirty, int block_size) { +ImagingAllocateArray(Imaging im, ImagingMemoryArena arena, int dirty, int block_size) { int y, line_in_block, current_block; - ImagingMemoryArena arena = &ImagingDefaultArena; ImagingMemoryBlock block = {NULL, 0}; int aligned_linesize, lines_per_block, blocks_count; char *aligned_ptr = NULL; @@ -498,14 +502,20 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { return NULL; } - if (ImagingAllocateArray(im, dirty, ImagingDefaultArena.block_size)) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); + Imaging tmp = ImagingAllocateArray(im, &ImagingDefaultArena, dirty, ImagingDefaultArena.block_size); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + if (tmp) { return im; } ImagingError_Clear(); // Try to allocate the image once more with smallest possible block size - if (ImagingAllocateArray(im, dirty, IMAGING_PAGE_SIZE)) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); + tmp = ImagingAllocateArray(im, &ImagingDefaultArena, dirty, IMAGING_PAGE_SIZE); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + if (tmp) { return im; } From 9f110aa702b0faef7f1ecc218fce3984d89b389a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:47:12 +0000 Subject: [PATCH 008/652] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/libImaging/Storage.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 9331ed5d5..8b6d46da6 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -503,7 +503,8 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { } MUTEX_LOCK(&ImagingDefaultArena.mutex); - Imaging tmp = ImagingAllocateArray(im, &ImagingDefaultArena, dirty, ImagingDefaultArena.block_size); + Imaging tmp = ImagingAllocateArray( + im, &ImagingDefaultArena, dirty, ImagingDefaultArena.block_size); MUTEX_UNLOCK(&ImagingDefaultArena.mutex); if (tmp) { return im; From 5999b9b0ccefe916c364490d7de86a71ea383b96 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 16 Jul 2024 16:56:15 +0200 Subject: [PATCH 009/652] Initialize PyMutex only under the free-threaded build --- src/libImaging/Storage.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 8b6d46da6..3a49113b6 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -270,7 +270,9 @@ struct ImagingMemoryArena ImagingDefaultArena = { 0, 0, 0, // Stats +#ifdef Py_GIL_DISABLED {0}, +#endif }; int From 06767fc3257fdd7932c2c04d7bc83cc26a0899a5 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 16 Jul 2024 17:00:14 +0200 Subject: [PATCH 010/652] Address feedback; do not lock in a loop --- src/libImaging/Storage.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 3a49113b6..a680fe63c 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -369,12 +369,12 @@ ImagingDestroyArray(Imaging im) { int y = 0; if (im->blocks) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); while (im->blocks[y].ptr) { - MUTEX_LOCK(&ImagingDefaultArena.mutex); memory_return_block(&ImagingDefaultArena, im->blocks[y]); - MUTEX_UNLOCK(&ImagingDefaultArena.mutex); y += 1; } + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); free(im->blocks); } } From 98b173928ad434d53c0e9ced859fcf1f14cf8e8f Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 16 Jul 2024 21:31:29 +0200 Subject: [PATCH 011/652] Address more feedback; don't unlock around sizeof --- src/_imaging.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 66709d9bc..15520040f 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4093,10 +4093,8 @@ _set_blocks_max(PyObject *self, PyObject *args) { return NULL; } - MUTEX_LOCK(&ImagingDefaultArena.mutex); - size_t blocksize = sizeof(ImagingDefaultArena.blocks_pool[0]); - MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - if ((unsigned long)blocks_max > SIZE_MAX / blocksize) { + if ((unsigned long)blocks_max > SIZE_MAX / + sizeof(ImagingDefaultArena.blocks_pool[0])) { PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); return NULL; } From e144707520f2db6256927028cbaee02f8337b73d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 19:31:59 +0000 Subject: [PATCH 012/652] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_imaging.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 15520040f..f1071b1ce 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4093,8 +4093,8 @@ _set_blocks_max(PyObject *self, PyObject *args) { return NULL; } - if ((unsigned long)blocks_max > SIZE_MAX / - sizeof(ImagingDefaultArena.blocks_pool[0])) { + if ((unsigned long)blocks_max > + SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); return NULL; } From aa8d87696b27068a0e8f4303d7e631e00c3a5225 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:14:51 +0000 Subject: [PATCH 013/652] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_imaging.c | 2 +- src/libImaging/Storage.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 21c6b0a02..a2606df15 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4131,7 +4131,7 @@ _set_blocks_max(PyObject *self, PyObject *args) { } if ((unsigned long)blocks_max > - SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { + SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); return NULL; } diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index ae4bf72ff..522e9f375 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -506,7 +506,8 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { MUTEX_LOCK(&ImagingDefaultArena.mutex); Imaging tmp = ImagingAllocateArray( - im, &ImagingDefaultArena, dirty, ImagingDefaultArena.block_size); + im, &ImagingDefaultArena, dirty, ImagingDefaultArena.block_size + ); MUTEX_UNLOCK(&ImagingDefaultArena.mutex); if (tmp) { return im; From 77df988d06fee6e3ba87d125912870eed55c5da0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Jul 2024 19:51:19 +1000 Subject: [PATCH 014/652] Support RGBA palettes --- Tests/test_file_jpeg2k.py | 7 +++++++ src/PIL/Jpeg2KImagePlugin.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a5cfa7c6c..d7100a761 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -391,6 +391,13 @@ def test_pclr() -> None: assert len(im.palette.colors) == 256 assert im.palette.colors[(255, 255, 255)] == 0 + with Image.open( + f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2" + ) as im: + assert im.mode == "P" + assert len(im.palette.colors) == 139 + assert im.palette.colors[(0, 0, 0, 0)] == 0 + def test_comment() -> None: with Image.open("Tests/images/comment.jp2") as im: diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index eeec41686..c5e33b905 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -205,7 +205,7 @@ def _parse_jp2_header( if bitdepth > max_bitdepth: max_bitdepth = bitdepth if max_bitdepth <= 8: - palette = ImagePalette.ImagePalette() + palette = ImagePalette.ImagePalette("RGBA" if npc == 4 else "RGB") for i in range(ne): color: list[int] = [] for value in header.read_fields(">" + ("B" * npc)): From d0d4227515c91a779520881a8adf164835691859 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jul 2024 18:45:38 +1000 Subject: [PATCH 015/652] Require font parameter in FreeTypeFont --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index d260eef69..2f41c29cd 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -203,7 +203,7 @@ class FreeTypeFont: def __init__( self, - font: StrOrBytesPath | BinaryIO | None = None, + font: StrOrBytesPath | BinaryIO, size: float = 10, index: int = 0, encoding: str = "", From 68328ccc6adadab538c559acfd6506b5c6998cd4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jul 2024 18:52:04 +1000 Subject: [PATCH 016/652] Require font parameter for truetype() --- src/PIL/ImageFont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 2f41c29cd..b2d79c25f 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -756,7 +756,7 @@ def load(filename: str) -> ImageFont: def truetype( - font: StrOrBytesPath | BinaryIO | None = None, + font: StrOrBytesPath | BinaryIO, size: float = 10, index: int = 0, encoding: str = "", @@ -827,7 +827,7 @@ def truetype( :exception ValueError: If the font size is not greater than zero. """ - def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: + def freetype(font: StrOrBytesPath | BinaryIO) -> FreeTypeFont: return FreeTypeFont(font, size, index, encoding, layout_engine) try: From c85eb0cae5568ba9286ac15c3e8f1709f7bbe9e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Jul 2024 12:53:02 +1000 Subject: [PATCH 017/652] Added type hints --- Tests/test_file_png.py | 2 ++ src/PIL/Image.py | 30 +++++++++++++++++------------- src/PIL/ImageDraw2.py | 14 +++++++------- src/PIL/PngImagePlugin.py | 38 +++++++++++++++++++------------------- src/PIL/TiffImagePlugin.py | 8 +++++--- 5 files changed, 50 insertions(+), 42 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 4958b2222..0abf9866f 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -424,8 +424,10 @@ class TestFilePng: im = roundtrip(im, pnginfo=info) assert im.info == {"spam": "Eggs", "eggs": "Spam"} assert im.text == {"spam": "Eggs", "eggs": "Spam"} + assert isinstance(im.text["spam"], PngImagePlugin.iTXt) assert im.text["spam"].lang == "en" assert im.text["spam"].tkey == "Spam" + assert isinstance(im.text["eggs"], PngImagePlugin.iTXt) assert im.text["eggs"].lang == "en" assert im.text["eggs"].tkey == "Eggs" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 72d167a8a..eb3dd5a36 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -38,7 +38,7 @@ import struct import sys import tempfile import warnings -from collections.abc import Callable, MutableMapping, Sequence +from collections.abc import Callable, Iterator, MutableMapping, Sequence from enum import IntEnum from types import ModuleType from typing import ( @@ -744,11 +744,11 @@ class Image: new["shape"], new["typestr"] = _conv_type_shape(self) return new - def __getstate__(self): + def __getstate__(self) -> list[Any]: im_data = self.tobytes() # load image first return [self.info, self.mode, self.size, self.getpalette(), im_data] - def __setstate__(self, state) -> None: + def __setstate__(self, state: list[Any]) -> None: Image.__init__(self) info, mode, size, palette, data = state self.info = info @@ -1683,7 +1683,9 @@ class Image: x, y = self.im.getprojection() return list(x), list(y) - def histogram(self, mask: Image | None = None, extrema=None) -> list[int]: + def histogram( + self, mask: Image | None = None, extrema: tuple[float, float] | None = None + ) -> list[int]: """ Returns a histogram for the image. The histogram is returned as a list of pixel counts, one for each pixel value in the source @@ -1709,12 +1711,14 @@ class Image: mask.load() return self.im.histogram((0, 0), mask.im) if self.mode in ("I", "F"): - if extrema is None: - extrema = self.getextrema() - return self.im.histogram(extrema) + return self.im.histogram( + extrema if extrema is not None else self.getextrema() + ) return self.im.histogram() - def entropy(self, mask: Image | None = None, extrema=None): + def entropy( + self, mask: Image | None = None, extrema: tuple[float, float] | None = None + ) -> float: """ Calculates and returns the entropy for the image. @@ -1735,9 +1739,9 @@ class Image: mask.load() return self.im.entropy((0, 0), mask.im) if self.mode in ("I", "F"): - if extrema is None: - extrema = self.getextrema() - return self.im.entropy(extrema) + return self.im.entropy( + extrema if extrema is not None else self.getextrema() + ) return self.im.entropy() def paste( @@ -3881,7 +3885,7 @@ class Exif(_ExifBase): # returns a dict with any single item tuples/lists as individual values return {k: self._fixup(v) for k, v in src_dict.items()} - def _get_ifd_dict(self, offset: int, group=None): + def _get_ifd_dict(self, offset: int, group: int | None = None): try: # an offset pointer to the location of the nested embedded IFD. # It should be a long, but may be corrupted. @@ -4136,7 +4140,7 @@ class Exif(_ExifBase): else: del self._data[tag] - def __iter__(self): + def __iter__(self) -> Iterator[int]: keys = set(self._data) if self._info is not None: keys.update(self._info) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index c0d89e4aa..1ad239382 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -24,7 +24,7 @@ """ from __future__ import annotations -from typing import AnyStr, BinaryIO +from typing import Any, AnyStr, BinaryIO from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath from ._typing import Coords, StrOrBytesPath @@ -111,7 +111,7 @@ class Draw: (xoffset, yoffset) = offset self.transform = (1, 0, xoffset, 0, 1, yoffset) - def arc(self, xy: Coords, start, end, *options) -> None: + def arc(self, xy: Coords, start, end, *options: Any) -> None: """ Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. @@ -120,7 +120,7 @@ class Draw: """ self.render("arc", xy, start, end, *options) - def chord(self, xy: Coords, start, end, *options) -> None: + def chord(self, xy: Coords, start, end, *options: Any) -> None: """ Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points with a straight line. @@ -129,7 +129,7 @@ class Draw: """ self.render("chord", xy, start, end, *options) - def ellipse(self, xy: Coords, *options) -> None: + def ellipse(self, xy: Coords, *options: Any) -> None: """ Draws an ellipse inside the given bounding box. @@ -137,7 +137,7 @@ class Draw: """ self.render("ellipse", xy, *options) - def line(self, xy: Coords, *options) -> None: + def line(self, xy: Coords, *options: Any) -> None: """ Draws a line between the coordinates in the ``xy`` list. @@ -145,7 +145,7 @@ class Draw: """ self.render("line", xy, *options) - def pieslice(self, xy: Coords, start, end, *options) -> None: + def pieslice(self, xy: Coords, start, end, *options: Any) -> None: """ Same as arc, but also draws straight lines between the end points and the center of the bounding box. @@ -154,7 +154,7 @@ class Draw: """ self.render("pieslice", xy, start, end, *options) - def polygon(self, xy: Coords, *options) -> None: + def polygon(self, xy: Coords, *options: Any) -> None: """ Draws a polygon. diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 58db7777c..52cfcd13a 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -38,6 +38,7 @@ import re import struct import warnings import zlib +from collections.abc import Callable from enum import IntEnum from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn @@ -135,7 +136,7 @@ class Blend(IntEnum): """ -def _safe_zlib_decompress(s): +def _safe_zlib_decompress(s: bytes) -> bytes: dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) if dobj.unconsumed_tail: @@ -783,7 +784,7 @@ class PngImageFile(ImageFile.ImageFile): self._mode = self.png.im_mode self._size = self.png.im_size self.info = self.png.im_info - self._text = None + self._text: dict[str, str | iTXt] | None = None self.tile = self.png.im_tile self.custom_mimetype = self.png.im_custom_mimetype self.n_frames = self.png.im_n_frames or 1 @@ -810,7 +811,7 @@ class PngImageFile(ImageFile.ImageFile): self.is_animated = self.n_frames > 1 @property - def text(self): + def text(self) -> dict[str, str | iTXt]: # experimental if self._text is None: # iTxt, tEXt and zTXt chunks may appear at the end of the file @@ -822,6 +823,7 @@ class PngImageFile(ImageFile.ImageFile): self.load() if self.is_animated: self.seek(frame) + assert self._text is not None return self._text def verify(self) -> None: @@ -1105,7 +1107,7 @@ def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None: class _idat: # wrap output from the encoder in IDAT chunks - def __init__(self, fp, chunk) -> None: + def __init__(self, fp: IO[bytes], chunk: Callable[..., None]) -> None: self.fp = fp self.chunk = chunk @@ -1116,7 +1118,7 @@ class _idat: class _fdat: # wrap encoder output in fdAT chunks - def __init__(self, fp: IO[bytes], chunk, seq_num: int) -> None: + def __init__(self, fp: IO[bytes], chunk: Callable[..., None], seq_num: int) -> None: self.fp = fp self.chunk = chunk self.seq_num = seq_num @@ -1135,7 +1137,7 @@ class _Frame(NamedTuple): def _write_multiple_frames( im: Image.Image, fp: IO[bytes], - chunk, + chunk: Callable[..., None], mode: str, rawmode: str, default_image: Image.Image | None, @@ -1275,7 +1277,11 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save( - im: Image.Image, fp, filename: str | bytes, chunk=putchunk, save_all: bool = False + im: Image.Image, + fp: IO[bytes], + filename: str | bytes, + chunk: Callable[..., None] = putchunk, + save_all: bool = False, ) -> None: # save an image to disk (called by the save method) @@ -1483,22 +1489,16 @@ def _save( def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]: """Return a list of PNG chunks representing this image.""" + from io import BytesIO - class collector: - data = [] + chunks = [] - def write(self, data: bytes) -> None: - pass - - def append(self, chunk: tuple[bytes, bytes, bytes]) -> None: - self.data.append(chunk) - - def append(fp: collector, cid: bytes, *data: bytes) -> None: + def append(fp: IO[bytes], cid: bytes, *data: bytes) -> None: byte_data = b"".join(data) crc = o32(_crc32(byte_data, _crc32(cid))) - fp.append((cid, byte_data, crc)) + chunks.append((cid, byte_data, crc)) - fp = collector() + fp = BytesIO() try: im.encoderinfo = params @@ -1506,7 +1506,7 @@ def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes] finally: del im.encoderinfo - return fp.data + return chunks # -------------------------------------------------------------------- diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ad050ffe4..318e9825c 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1020,7 +1020,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): .. deprecated:: 3.0.0 """ - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._legacy_api = True @@ -1142,13 +1142,15 @@ class TiffImageFile(ImageFile.ImageFile): self._seek(0) @property - def n_frames(self): - if self._n_frames is None: + def n_frames(self) -> int: + current_n_frames = self._n_frames + if current_n_frames is None: current = self.tell() self._seek(len(self._frame_pos)) while self._n_frames is None: self._seek(self.tell() + 1) self.seek(current) + assert self._n_frames is not None return self._n_frames def seek(self, frame: int) -> None: From a03033e7f3cdc1bdf9a6aabc09d6dc2aca97cff7 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 15:52:46 +0400 Subject: [PATCH 018/652] Remove all WITH_* flags from _imaging.c --- src/_imaging.c | 89 +++----------------------------------------------- 1 file changed, 4 insertions(+), 85 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 5c2f7b4b6..d675cf22e 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -93,19 +93,6 @@ #define _USE_MATH_DEFINES #include -/* Configuration stuff. Feel free to undef things you don't need. */ -#define WITH_IMAGECHOPS /* ImageChops support */ -#define WITH_IMAGEDRAW /* ImageDraw support */ -#define WITH_MAPPING /* use memory mapping to read some file formats */ -#define WITH_IMAGEPATH /* ImagePath stuff */ -#define WITH_ARROW /* arrow graphics stuff (experimental) */ -#define WITH_EFFECTS /* special effects */ -#define WITH_QUANTIZE /* quantization support */ -#define WITH_RANKFILTER /* rank filter */ -#define WITH_MODEFILTER /* mode filter */ -#define WITH_THREADING /* "friendly" threading support */ -#define WITH_UNSHARPMASK /* Kevin Cazabon's unsharpmask module */ - #undef VERBOSE #define B16(p, i) ((((int)p[(i)]) << 8) + p[(i) + 1]) @@ -123,8 +110,6 @@ typedef struct { static PyTypeObject Imaging_Type; -#ifdef WITH_IMAGEDRAW - typedef struct { /* to write a character, cut out sxy from glyph data, place at current position plus dxy, and advance by (dx, dy) */ @@ -151,8 +136,6 @@ typedef struct { static PyTypeObject ImagingDraw_Type; -#endif - typedef struct { PyObject_HEAD ImagingObject *image; int readonly; @@ -215,16 +198,12 @@ PyImaging_AsImaging(PyObject *op) { void ImagingSectionEnter(ImagingSectionCookie *cookie) { -#ifdef WITH_THREADING *cookie = (PyThreadState *)PyEval_SaveThread(); -#endif } void ImagingSectionLeave(ImagingSectionCookie *cookie) { -#ifdef WITH_THREADING PyEval_RestoreThread((PyThreadState *)*cookie); -#endif } /* -------------------------------------------------------------------- */ @@ -1091,7 +1070,6 @@ _filter(ImagingObject *self, PyObject *args) { return imOut; } -#ifdef WITH_UNSHARPMASK static PyObject * _gaussian_blur(ImagingObject *self, PyObject *args) { Imaging imIn; @@ -1116,7 +1094,6 @@ _gaussian_blur(ImagingObject *self, PyObject *args) { return PyImagingNew(imOut); } -#endif static PyObject * _getpalette(ImagingObject *self, PyObject *args) { @@ -1374,7 +1351,6 @@ _entropy(ImagingObject *self, PyObject *args) { return PyFloat_FromDouble(-entropy); } -#ifdef WITH_MODEFILTER static PyObject * _modefilter(ImagingObject *self, PyObject *args) { int size; @@ -1384,7 +1360,6 @@ _modefilter(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingModeFilter(self->image, size)); } -#endif static PyObject * _offset(ImagingObject *self, PyObject *args) { @@ -1716,8 +1691,6 @@ _putdata(ImagingObject *self, PyObject *args) { return Py_None; } -#ifdef WITH_QUANTIZE - static PyObject * _quantize(ImagingObject *self, PyObject *args) { int colours = 256; @@ -1734,7 +1707,6 @@ _quantize(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans)); } -#endif static PyObject * _putpalette(ImagingObject *self, PyObject *args) { @@ -1870,7 +1842,6 @@ _putpixel(ImagingObject *self, PyObject *args) { return Py_None; } -#ifdef WITH_RANKFILTER static PyObject * _rankfilter(ImagingObject *self, PyObject *args) { int size, rank; @@ -1880,7 +1851,6 @@ _rankfilter(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingRankFilter(self->image, size, rank)); } -#endif static PyObject * _resize(ImagingObject *self, PyObject *args) { @@ -2162,7 +2132,6 @@ _transpose(ImagingObject *self, PyObject *args) { return PyImagingNew(imOut); } -#ifdef WITH_UNSHARPMASK static PyObject * _unsharp_mask(ImagingObject *self, PyObject *args) { Imaging imIn; @@ -2186,7 +2155,6 @@ _unsharp_mask(ImagingObject *self, PyObject *args) { return PyImagingNew(imOut); } -#endif static PyObject * _box_blur(ImagingObject *self, PyObject *args) { @@ -2463,9 +2431,7 @@ _split(ImagingObject *self) { return list; } -/* -------------------------------------------------------------------- */ - -#ifdef WITH_IMAGECHOPS +/* Channel operations (ImageChops) ------------------------------------ */ static PyObject * _chop_invert(ImagingObject *self) { @@ -2646,11 +2612,8 @@ _chop_overlay(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingOverlay(self->image, imagep->image)); } -#endif -/* -------------------------------------------------------------------- */ - -#ifdef WITH_IMAGEDRAW +/* Graphics (ImageDraw) ----------------------------------------------- */ static PyObject * _font_new(PyObject *self_, PyObject *args) { @@ -3233,8 +3196,6 @@ _draw_points(ImagingDrawObject *self, PyObject *args) { return Py_None; } -#ifdef WITH_ARROW - /* from outline.c */ extern ImagingOutline PyOutline_AsOutline(PyObject *outline); @@ -3264,8 +3225,6 @@ _draw_outline(ImagingDrawObject *self, PyObject *args) { return Py_None; } -#endif - static PyObject * _draw_pieslice(ImagingDrawObject *self, PyObject *args) { double *xy; @@ -3431,12 +3390,9 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { } static struct PyMethodDef _draw_methods[] = { -#ifdef WITH_IMAGEDRAW /* Graphics (ImageDraw) */ {"draw_lines", (PyCFunction)_draw_lines, METH_VARARGS}, -#ifdef WITH_ARROW {"draw_outline", (PyCFunction)_draw_outline, METH_VARARGS}, -#endif {"draw_polygon", (PyCFunction)_draw_polygon, METH_VARARGS}, {"draw_rectangle", (PyCFunction)_draw_rectangle, METH_VARARGS}, {"draw_points", (PyCFunction)_draw_points, METH_VARARGS}, @@ -3446,11 +3402,9 @@ static struct PyMethodDef _draw_methods[] = { {"draw_ellipse", (PyCFunction)_draw_ellipse, METH_VARARGS}, {"draw_pieslice", (PyCFunction)_draw_pieslice, METH_VARARGS}, {"draw_ink", (PyCFunction)_draw_ink, METH_VARARGS}, -#endif {NULL, NULL} /* sentinel */ }; -#endif static PyObject * pixel_access_new(ImagingObject *imagep, PyObject *args) { @@ -3532,11 +3486,9 @@ pixel_access_setitem(PixelAccessObject *self, PyObject *xy, PyObject *color) { } /* -------------------------------------------------------------------- */ -/* EFFECTS (experimental) */ +/* EFFECTS (experimental) */ /* -------------------------------------------------------------------- */ -#ifdef WITH_EFFECTS - static PyObject * _effect_mandelbrot(ImagingObject *self, PyObject *args) { int xsize = 512; @@ -3588,8 +3540,6 @@ _effect_spread(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingEffectSpread(self->image, dist)); } -#endif - /* -------------------------------------------------------------------- */ /* UTILITIES */ /* -------------------------------------------------------------------- */ @@ -3670,20 +3620,14 @@ static struct PyMethodDef methods[] = { {"filter", (PyCFunction)_filter, METH_VARARGS}, {"histogram", (PyCFunction)_histogram, METH_VARARGS}, {"entropy", (PyCFunction)_entropy, METH_VARARGS}, -#ifdef WITH_MODEFILTER {"modefilter", (PyCFunction)_modefilter, METH_VARARGS}, -#endif {"offset", (PyCFunction)_offset, METH_VARARGS}, {"paste", (PyCFunction)_paste, METH_VARARGS}, {"point", (PyCFunction)_point, METH_VARARGS}, {"point_transform", (PyCFunction)_point_transform, METH_VARARGS}, {"putdata", (PyCFunction)_putdata, METH_VARARGS}, -#ifdef WITH_QUANTIZE {"quantize", (PyCFunction)_quantize, METH_VARARGS}, -#endif -#ifdef WITH_RANKFILTER {"rankfilter", (PyCFunction)_rankfilter, METH_VARARGS}, -#endif {"resize", (PyCFunction)_resize, METH_VARARGS}, {"reduce", (PyCFunction)_reduce, METH_VARARGS}, {"transpose", (PyCFunction)_transpose, METH_VARARGS}, @@ -3709,7 +3653,6 @@ static struct PyMethodDef methods[] = { {"putpalettealpha", (PyCFunction)_putpalettealpha, METH_VARARGS}, {"putpalettealphas", (PyCFunction)_putpalettealphas, METH_VARARGS}, -#ifdef WITH_IMAGECHOPS /* Channel operations (ImageChops) */ {"chop_invert", (PyCFunction)_chop_invert, METH_NOARGS}, {"chop_lighter", (PyCFunction)_chop_lighter, METH_VARARGS}, @@ -3728,20 +3671,14 @@ static struct PyMethodDef methods[] = { {"chop_hard_light", (PyCFunction)_chop_hard_light, METH_VARARGS}, {"chop_overlay", (PyCFunction)_chop_overlay, METH_VARARGS}, -#endif - -#ifdef WITH_UNSHARPMASK - /* Kevin Cazabon's unsharpmask extension */ + /* Unsharpmask extension */ {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, -#endif {"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, -#ifdef WITH_EFFECTS /* Special effects */ {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, -#endif /* Misc. */ {"new_block", (PyCFunction)_new_block, METH_VARARGS}, @@ -3870,8 +3807,6 @@ static PyTypeObject Imaging_Type = { getsetters, /*tp_getset*/ }; -#ifdef WITH_IMAGEDRAW - static PyTypeObject ImagingFont_Type = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingFont", /*tp_name*/ sizeof(ImagingFontObject), /*tp_basicsize*/ @@ -3938,8 +3873,6 @@ static PyTypeObject ImagingDraw_Type = { 0, /*tp_getset*/ }; -#endif - static PyMappingMethods pixel_access_as_mapping = { (lenfunc)NULL, /*mp_length*/ (binaryfunc)pixel_access_getitem, /*mp_subscript*/ @@ -4283,9 +4216,7 @@ static PyMethodDef functions[] = { #endif /* Memory mapping */ -#ifdef WITH_MAPPING {"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS}, -#endif /* Display support */ #ifdef _WIN32 @@ -4305,29 +4236,21 @@ static PyMethodDef functions[] = { {"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS}, /* Special effects (experimental) */ -#ifdef WITH_EFFECTS {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS}, {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ -#endif /* Drawing support stuff */ -#ifdef WITH_IMAGEDRAW {"font", (PyCFunction)_font_new, METH_VARARGS}, {"draw", (PyCFunction)_draw_new, METH_VARARGS}, -#endif /* Experimental path stuff */ -#ifdef WITH_IMAGEPATH {"path", (PyCFunction)PyPath_Create, METH_VARARGS}, -#endif /* Experimental arrow graphics stuff */ -#ifdef WITH_ARROW {"outline", (PyCFunction)PyOutline_Create, METH_VARARGS}, -#endif /* Resource management */ {"get_stats", (PyCFunction)_get_stats, METH_VARARGS}, @@ -4352,16 +4275,12 @@ setup_module(PyObject *m) { if (PyType_Ready(&Imaging_Type) < 0) { return -1; } - -#ifdef WITH_IMAGEDRAW if (PyType_Ready(&ImagingFont_Type) < 0) { return -1; } - if (PyType_Ready(&ImagingDraw_Type) < 0) { return -1; } -#endif if (PyType_Ready(&PixelAccess_Type) < 0) { return -1; } From cfce566d17999698e6f8fa2ccd815c8146b5de20 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 16:08:52 +0400 Subject: [PATCH 019/652] codestyle --- src/_imaging.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index d675cf22e..91cae2e5d 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3405,7 +3405,6 @@ static struct PyMethodDef _draw_methods[] = { {NULL, NULL} /* sentinel */ }; - static PyObject * pixel_access_new(ImagingObject *imagep, PyObject *args) { PixelAccessObject *self; @@ -4215,11 +4214,11 @@ static PyMethodDef functions[] = { {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, METH_VARARGS}, #endif -/* Memory mapping */ + /* Memory mapping */ {"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS}, -/* Display support */ #ifdef _WIN32 + /* Display support */ {"display", (PyCFunction)PyImaging_DisplayWin32, METH_VARARGS}, {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, METH_VARARGS}, {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, METH_VARARGS}, @@ -4235,21 +4234,21 @@ static PyMethodDef functions[] = { /* Utilities */ {"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS}, -/* Special effects (experimental) */ + /* Special effects (experimental) */ {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS}, {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ -/* Drawing support stuff */ + /* Drawing support stuff */ {"font", (PyCFunction)_font_new, METH_VARARGS}, {"draw", (PyCFunction)_draw_new, METH_VARARGS}, -/* Experimental path stuff */ + /* Experimental path stuff */ {"path", (PyCFunction)PyPath_Create, METH_VARARGS}, -/* Experimental arrow graphics stuff */ + /* Experimental arrow graphics stuff */ {"outline", (PyCFunction)PyOutline_Create, METH_VARARGS}, /* Resource management */ From dc53356c1a9dd222e504d390861fdac7368bb300 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 23:47:21 +0400 Subject: [PATCH 020/652] Not needed since memcpy is used here --- src/libImaging/Pack.c | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index f3b714215..c29473d90 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -258,16 +258,6 @@ void ImagingPackRGB(UINT8 *out, const UINT8 *in, int pixels) { int i = 0; /* RGB triplets */ -#ifdef __sparc - /* SPARC CPUs cannot read integers from nonaligned addresses. */ - for (; i < pixels; i++) { - out[0] = in[R]; - out[1] = in[G]; - out[2] = in[B]; - out += 3; - in += 4; - } -#else for (; i < pixels - 1; i++) { memcpy(out, in + i * 4, 4); out += 3; @@ -278,7 +268,6 @@ ImagingPackRGB(UINT8 *out, const UINT8 *in, int pixels) { out[2] = in[i * 4 + B]; out += 3; } -#endif } void From d00fb87fa3b06776c14b882d0b39f737c39ec8da Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 8 Jul 2024 11:08:32 +0400 Subject: [PATCH 021/652] Rename Not NO_OUTPUT to DEBUG, remove TEST_MERGESORT and TEST_SPLIT* flags --- src/libImaging/Quant.c | 106 +++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 56 deletions(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 197f9f3ee..595ee42a0 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -36,7 +36,8 @@ #define UINT32_MAX 0xffffffff #endif -#define NO_OUTPUT +// #define DEBUG +// #define TEST_NEAREST_NEIGHBOUR typedef struct { uint32_t scale; @@ -144,7 +145,7 @@ create_pixel_hash(Pixel *pixelData, uint32_t nPixels) { PixelHashData *d; HashTable *hash; uint32_t i; -#ifndef NO_OUTPUT +#ifdef DEBUG uint32_t timer, timer2, timer3; #endif @@ -156,7 +157,7 @@ create_pixel_hash(Pixel *pixelData, uint32_t nPixels) { hash = hashtable_new(pixel_hash, pixel_cmp); hashtable_set_user_data(hash, d); d->scale = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG timer = timer3 = clock(); #endif for (i = 0; i < nPixels; i++) { @@ -167,22 +168,22 @@ create_pixel_hash(Pixel *pixelData, uint32_t nPixels) { } while (hashtable_get_count(hash) > MAX_HASH_ENTRIES) { d->scale++; -#ifndef NO_OUTPUT +#ifdef DEBUG printf("rehashing - new scale: %d\n", (int)d->scale); timer2 = clock(); #endif hashtable_rehash_compute(hash, rehash_collide); -#ifndef NO_OUTPUT +#ifdef DEBUG timer2 = clock() - timer2; printf("rehash took %f sec\n", timer2 / (double)CLOCKS_PER_SEC); timer += timer2; #endif } } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("inserts took %f sec\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif -#ifndef NO_OUTPUT +#ifdef DEBUG printf("total %f sec\n", (clock() - timer3) / (double)CLOCKS_PER_SEC); #endif return hash; @@ -304,7 +305,7 @@ mergesort_pixels(PixelList *head, int i) { return head; } -#if defined(TEST_MERGESORT) || defined(TEST_SORTED) +#ifdef DEBUG static int test_sorted(PixelList *pl[3]) { int i, n, l; @@ -347,12 +348,12 @@ splitlists( PixelList *l, *r, *c, *n; int i; int nRight; -#ifndef NO_OUTPUT +#ifdef DEBUG int nLeft; #endif int splitColourVal; -#ifdef TEST_SPLIT +#ifdef DEBUG { PixelList *_prevTest, *_nextTest; int _i, _nextCount[3], _prevCount[3]; @@ -402,14 +403,14 @@ splitlists( #endif nCount[0] = nCount[1] = 0; nRight = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG nLeft = 0; #endif for (left = 0, c = h[axis]; c;) { left = left + c->count; nCount[0] += c->count; c->flag = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG nLeft++; #endif c = c->next[axis]; @@ -424,7 +425,7 @@ splitlists( break; } c->flag = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG nLeft++; #endif nCount[0] += c->count; @@ -442,14 +443,14 @@ splitlists( } c->flag = 1; nRight++; -#ifndef NO_OUTPUT +#ifdef DEBUG nLeft--; #endif nCount[0] -= c->count; nCount[1] += c->count; } } -#ifndef NO_OUTPUT +#ifdef DEBUG if (!nLeft) { for (c = h[axis]; c; c = c->next[axis]) { printf("[%d %d %d]\n", c->p.c.r, c->p.c.g, c->p.c.b); @@ -511,7 +512,7 @@ split(BoxNode *node) { gl = node->tail[1]->p.c.g; bh = node->head[2]->p.c.b; bl = node->tail[2]->p.c.b; -#ifdef TEST_SPLIT +#ifdef DEBUG printf("splitting node [%d %d %d] [%d %d %d] ", rl, gl, bl, rh, gh, bh); #endif f[0] = (rh - rl) * 77; @@ -526,11 +527,8 @@ split(BoxNode *node) { axis = i; } } -#ifdef TEST_SPLIT +#ifdef DEBUG printf("along axis %d\n", axis + 1); -#endif - -#ifdef TEST_SPLIT { PixelList *_prevTest, *_nextTest; int _i, _nextCount[3], _prevCount[3]; @@ -592,12 +590,12 @@ split(BoxNode *node) { if (!splitlists( node->head, node->tail, heads, tails, newCounts, axis, node->pixelCount )) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("list split failed.\n"); #endif return 0; } -#ifdef TEST_SPLIT +#ifdef DEBUG if (!test_sorted(heads[0])) { printf("bug in split"); exit(1); @@ -623,7 +621,7 @@ split(BoxNode *node) { node->head[i] = NULL; node->tail[i] = NULL; } -#ifdef TEST_SPLIT +#ifdef DEBUG if (left->head[0]) { rh = left->head[0]->p.c.r; rl = left->tail[0]->p.c.r; @@ -687,7 +685,7 @@ median_cut(PixelList *hl[3], uint32_t imPixelCount, int nPixels) { } } while (compute_box_volume(thisNode) == 1); if (!split(thisNode)) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("Oops, split failed...\n"); #endif exit(1); @@ -716,16 +714,14 @@ free_box_tree(BoxNode *n) { free(n); } -#ifdef TEST_SPLIT_INTEGRITY +#ifdef DEBUG static int checkContained(BoxNode *n, Pixel *pp) { if (n->l && n->r) { return checkContained(n->l, pp) + checkContained(n->r, pp); } if (n->l || n->r) { -#ifndef NO_OUTPUT printf("box tree is dead\n"); -#endif return 0; } if (pp->c.r <= n->head[0]->p.c.r && pp->c.r >= n->tail[0]->p.c.r && @@ -746,7 +742,7 @@ annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) { return annotate_hash_table(n->l, h, box) && annotate_hash_table(n->r, h, box); } if (n->l || n->r) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("box tree is dead\n"); #endif return 0; @@ -754,7 +750,7 @@ annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) { for (p = n->head[0]; p; p = p->next[0]) { PIXEL_UNSCALE(&(p->p), &q, d->scale); if (!hashtable_insert(h, q, *box)) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("hashtable insert failed\n"); #endif return 0; @@ -978,7 +974,7 @@ map_image_pixels_from_median_box( continue; } if (!hashtable_lookup(medianBoxHash, pixelData[i], &pixelVal)) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("pixel lookup failed\n"); #endif return 0; @@ -1043,7 +1039,7 @@ compute_palette_from_median_cut( } } for (i = 0; i < nPixels; i++) { -#ifdef TEST_SPLIT_INTEGRITY +#ifdef DEBUG if (!(i % 100)) { printf("%05d\r", i); fflush(stdout); @@ -1058,7 +1054,7 @@ compute_palette_from_median_cut( } #endif if (!hashtable_lookup(medianBoxHash, pixelData[i], &paletteEntry)) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("pixel lookup failed\n"); #endif for (i = 0; i < 3; i++) { @@ -1068,7 +1064,7 @@ compute_palette_from_median_cut( return 0; } if (paletteEntry >= nPaletteEntries) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf( "panic - paletteEntry>=nPaletteEntries (%d>=%d)\n", (int)paletteEntry, @@ -1140,7 +1136,7 @@ compute_palette_from_quantized_pixels( } for (i = 0; i < nPixels; i++) { if (qp[i] >= nPaletteEntries) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("scream\n"); #endif return 0; @@ -1208,7 +1204,7 @@ k_means( goto error_2; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("["); fflush(stdout); #endif @@ -1243,7 +1239,7 @@ k_means( if (changes < 0) { goto error_3; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf(".(%d)", changes); fflush(stdout); #endif @@ -1251,7 +1247,7 @@ k_means( break; } } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("]\n"); #endif if (avgDistSortKey) { @@ -1311,32 +1307,32 @@ quantize( uint32_t **avgDistSortKey; Pixel *p; -#ifndef NO_OUTPUT +#ifdef DEBUG uint32_t timer, timer2; #endif -#ifndef NO_OUTPUT +#ifdef DEBUG timer2 = clock(); printf("create hash table..."); fflush(stdout); timer = clock(); #endif h = create_pixel_hash(pixelData, nPixels); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif if (!h) { goto error_0; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("create lists from hash table..."); fflush(stdout); timer = clock(); #endif hl[0] = hl[1] = hl[2] = NULL; hashtable_foreach(h, hash_to_list, hl); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif @@ -1344,7 +1340,7 @@ quantize( goto error_1; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("mergesort lists..."); fflush(stdout); timer = clock(); @@ -1352,39 +1348,37 @@ quantize( for (i = 0; i < 3; i++) { hl[i] = mergesort_pixels(hl[i], i); } -#ifdef TEST_MERGESORT +#ifdef DEBUG if (!test_sorted(hl)) { printf("bug in mergesort\n"); goto error_1; } -#endif -#ifndef NO_OUTPUT printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif -#ifndef NO_OUTPUT +#ifdef DEBUG printf("median cut..."); fflush(stdout); timer = clock(); #endif root = median_cut(hl, nPixels, nQuantPixels); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif if (!root) { goto error_1; } nPaletteEntries = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG printf("median cut tree to hash table..."); fflush(stdout); timer = clock(); #endif annotate_hash_table(root, h, &nPaletteEntries); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif -#ifndef NO_OUTPUT +#ifdef DEBUG printf("compute palette...\n"); fflush(stdout); timer = clock(); @@ -1392,7 +1386,7 @@ quantize( if (!compute_palette_from_median_cut(pixelData, nPixels, h, &p, nPaletteEntries)) { goto error_3; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif @@ -1479,7 +1473,7 @@ quantize( hashtable_free(h2); } #endif -#ifndef NO_OUTPUT +#ifdef DEBUG printf("k means...\n"); fflush(stdout); timer = clock(); @@ -1487,7 +1481,7 @@ quantize( if (kmeans > 0) { k_means(pixelData, nPixels, p, nPaletteEntries, qp, kmeans - 1); } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif @@ -1495,7 +1489,7 @@ quantize( *palette = p; *paletteLength = nPaletteEntries; -#ifndef NO_OUTPUT +#ifdef DEBUG printf("cleanup..."); fflush(stdout); timer = clock(); @@ -1507,7 +1501,7 @@ quantize( free(avgDistSortKey); } destroy_pixel_hash(h); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); printf("-----\ntotal time %f\n", (clock() - timer2) / (double)CLOCKS_PER_SEC); #endif From a6d83ec9ccc23284bf5274325591499500e269d7 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 28 Jul 2024 17:09:31 +0400 Subject: [PATCH 022/652] Add relesenotes --- docs/releasenotes/11.0.0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index bb6179de8..936a441d9 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -77,3 +77,10 @@ others prepare for 3.13, and to ensure Pillow could be used immediately at the r of 3.13.0 final (2024-10-01, :pep:`719`). Pillow 11.0.0 now officially supports Python 3.13. + +C-level Flags +^^^^^^^^^^^^^ + +Some compiling flags like ``WITH_THREADING``, ``WITH_IMAGECHOPS``, and other +``WITH_*`` were removed. These flags were not available in the build system, +but they could be edited in the C source. From 302962dae10b7dcae7e373ba8193e846211f3922 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 28 Jul 2024 17:19:50 +0400 Subject: [PATCH 023/652] Updated comments by @radarhere, lost during rebase --- docs/releasenotes/11.0.0.rst | 2 +- src/_imaging.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 936a441d9..f30442aef 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -82,5 +82,5 @@ C-level Flags ^^^^^^^^^^^^^ Some compiling flags like ``WITH_THREADING``, ``WITH_IMAGECHOPS``, and other -``WITH_*`` were removed. These flags were not available in the build system, +``WITH_*`` were removed. These flags were not available through the build system, but they could be edited in the C source. diff --git a/src/_imaging.c b/src/_imaging.c index 91cae2e5d..b24776ead 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2613,7 +2613,7 @@ _chop_overlay(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingOverlay(self->image, imagep->image)); } -/* Graphics (ImageDraw) ----------------------------------------------- */ +/* Fonts (ImageDraw and ImageFont) ------------------------------------ */ static PyObject * _font_new(PyObject *self_, PyObject *args) { @@ -2842,7 +2842,7 @@ static struct PyMethodDef _font_methods[] = { {NULL, NULL} /* sentinel */ }; -/* -------------------------------------------------------------------- */ +/* Graphics (ImageDraw) ----------------------------------------------- */ static PyObject * _draw_new(PyObject *self_, PyObject *args) { From 6420f73613dce5533e2d8a417f8d7fede98de07e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Jul 2024 23:46:07 +1000 Subject: [PATCH 024/652] Added type hints --- src/PIL/Image.py | 6 +++- src/PIL/ImageDraw2.py | 2 +- src/PIL/ImageFont.py | 6 ++-- src/PIL/ImagePalette.py | 2 +- src/PIL/Jpeg2KImagePlugin.py | 2 +- src/PIL/PdfImagePlugin.py | 45 +++++++++++++++----------- src/PIL/TiffImagePlugin.py | 61 ++++++++++++++++++++---------------- 7 files changed, 71 insertions(+), 53 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index eb3dd5a36..ae8634539 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2052,7 +2052,11 @@ class Image: msg = "illegal image mode" raise ValueError(msg) if isinstance(data, ImagePalette.ImagePalette): - palette = ImagePalette.raw(data.rawmode, data.palette) + if data.rawmode is not None: + palette = ImagePalette.raw(data.rawmode, data.palette) + else: + palette = ImagePalette.ImagePalette(palette=data.palette) + palette.dirty = 1 else: if not isinstance(data, bytes): data = bytes(data) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 1ad239382..58302f950 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -167,7 +167,7 @@ class Draw: """ self.render("polygon", xy, *options) - def rectangle(self, xy: Coords, *options) -> None: + def rectangle(self, xy: Coords, *options: Any) -> None: """ Draws a rectangle. diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 24490348c..2ab65bfef 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -269,12 +269,12 @@ class FreeTypeFont: else: load_from_bytes(font) - def __getstate__(self): + def __getstate__(self) -> list[Any]: return [self.path, self.size, self.index, self.encoding, self.layout_engine] - def __setstate__(self, state): + def __setstate__(self, state: list[Any]) -> None: path, size, index, encoding, layout_engine = state - self.__init__(path, size, index, encoding, layout_engine) + FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine) def getname(self) -> tuple[str | None, str | None]: """ diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 8ccecbd07..183f85526 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -208,7 +208,7 @@ class ImagePalette: # Internal -def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette: +def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette: palette = ImagePalette() palette.rawmode = rawmode palette.palette = data diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index eeec41686..ef9107f00 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -324,7 +324,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): return self._reduce or super().reduce @reduce.setter - def reduce(self, value): + def reduce(self, value: int) -> None: self._reduce = value def load(self) -> Image.core.PixelAccess | None: diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index e0f732199..7fc1108bb 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -25,7 +25,7 @@ import io import math import os import time -from typing import IO +from typing import IO, Any from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features @@ -48,7 +48,12 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # (Internal) Image save plugin for the PDF format. -def _write_image(im, filename, existing_pdf, image_refs): +def _write_image( + im: Image.Image, + filename: str | bytes, + existing_pdf: PdfParser.PdfParser, + image_refs: list[PdfParser.IndirectReference], +) -> tuple[PdfParser.IndirectReference, str]: # FIXME: Should replace ASCIIHexDecode with RunLengthDecode # (packbits) or LZWDecode (tiff/lzw compression). Note that # PDF 1.2 also supports Flatedecode (zip compression). @@ -61,10 +66,10 @@ def _write_image(im, filename, existing_pdf, image_refs): width, height = im.size - dict_obj = {"BitsPerComponent": 8} + dict_obj: dict[str, Any] = {"BitsPerComponent": 8} if im.mode == "1": if features.check("libtiff"): - filter = "CCITTFaxDecode" + decode_filter = "CCITTFaxDecode" dict_obj["BitsPerComponent"] = 1 params = PdfParser.PdfArray( [ @@ -79,22 +84,23 @@ def _write_image(im, filename, existing_pdf, image_refs): ] ) else: - filter = "DCTDecode" + decode_filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "L": - filter = "DCTDecode" + decode_filter = "DCTDecode" # params = f"<< /Predictor 15 /Columns {width-2} >>" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "LA": - filter = "JPXDecode" + decode_filter = "JPXDecode" # params = f"<< /Predictor 15 /Columns {width-2} >>" procset = "ImageB" # grayscale dict_obj["SMaskInData"] = 1 elif im.mode == "P": - filter = "ASCIIHexDecode" + decode_filter = "ASCIIHexDecode" palette = im.getpalette() + assert palette is not None dict_obj["ColorSpace"] = [ PdfParser.PdfName("Indexed"), PdfParser.PdfName("DeviceRGB"), @@ -110,15 +116,15 @@ def _write_image(im, filename, existing_pdf, image_refs): image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0] dict_obj["SMask"] = image_ref elif im.mode == "RGB": - filter = "DCTDecode" + decode_filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") procset = "ImageC" # color images elif im.mode == "RGBA": - filter = "JPXDecode" + decode_filter = "JPXDecode" procset = "ImageC" # color images dict_obj["SMaskInData"] = 1 elif im.mode == "CMYK": - filter = "DCTDecode" + decode_filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") procset = "ImageC" # color images decode = [1, 0, 1, 0, 1, 0, 1, 0] @@ -131,9 +137,9 @@ def _write_image(im, filename, existing_pdf, image_refs): op = io.BytesIO() - if filter == "ASCIIHexDecode": + if decode_filter == "ASCIIHexDecode": ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) - elif filter == "CCITTFaxDecode": + elif decode_filter == "CCITTFaxDecode": im.save( op, "TIFF", @@ -141,21 +147,22 @@ def _write_image(im, filename, existing_pdf, image_refs): # use a single strip strip_size=math.ceil(width / 8) * height, ) - elif filter == "DCTDecode": + elif decode_filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) - elif filter == "JPXDecode": + elif decode_filter == "JPXDecode": del dict_obj["BitsPerComponent"] Image.SAVE["JPEG2000"](im, op, filename) else: - msg = f"unsupported PDF filter ({filter})" + msg = f"unsupported PDF filter ({decode_filter})" raise ValueError(msg) stream = op.getvalue() - if filter == "CCITTFaxDecode": + filter: PdfParser.PdfArray | PdfParser.PdfName + if decode_filter == "CCITTFaxDecode": stream = stream[8:] - filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) + filter = PdfParser.PdfArray([PdfParser.PdfName(decode_filter)]) else: - filter = PdfParser.PdfName(filter) + filter = PdfParser.PdfName(decode_filter) image_ref = image_refs.pop(0) existing_pdf.write_obj( diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 318e9825c..dd013a0cf 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -47,16 +47,18 @@ import math import os import struct import warnings -from collections.abc import MutableMapping +from collections.abc import Iterator, MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn +from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn, cast from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 from ._deprecate import deprecate +from ._typing import StrOrBytesPath +from ._util import is_path from .TiffTags import TYPES logger = logging.getLogger(__name__) @@ -313,7 +315,7 @@ _load_dispatch = {} _write_dispatch = {} -def _delegate(op): +def _delegate(op: str): def delegate(self, *args): return getattr(self._val, op)(*args) @@ -334,7 +336,9 @@ class IFDRational(Rational): __slots__ = ("_numerator", "_denominator", "_val") - def __init__(self, value, denominator: int = 1) -> None: + def __init__( + self, value: float | Fraction | IFDRational, denominator: int = 1 + ) -> None: """ :param value: either an integer numerator, a float/rational/other number, or an IFDRational @@ -358,18 +362,20 @@ class IFDRational(Rational): self._val = float("nan") elif denominator == 1: self._val = Fraction(value) + elif int(value) == value: + self._val = Fraction(int(value), denominator) else: - self._val = Fraction(value, denominator) + self._val = Fraction(value / denominator) @property def numerator(self): return self._numerator @property - def denominator(self): + def denominator(self) -> int: return self._denominator - def limit_rational(self, max_denominator): + def limit_rational(self, max_denominator: int) -> tuple[float, int]: """ :param max_denominator: Integer, the maximum denominator value @@ -379,6 +385,7 @@ class IFDRational(Rational): if self.denominator == 0: return self.numerator, self.denominator + assert isinstance(self._val, Fraction) f = self._val.limit_denominator(max_denominator) return f.numerator, f.denominator @@ -396,14 +403,15 @@ class IFDRational(Rational): val = float(val) return val == other - def __getstate__(self): + def __getstate__(self) -> list[float | Fraction]: return [self._val, self._numerator, self._denominator] - def __setstate__(self, state): + def __setstate__(self, state: list[float | Fraction]) -> None: IFDRational.__init__(self, 0) _val, _numerator, _denominator = state self._val = _val self._numerator = _numerator + assert isinstance(_denominator, int) self._denominator = _denominator """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', @@ -730,13 +738,13 @@ class ImageFileDirectory_v2(_IFDv2Base): self._tags_v1.pop(tag, None) self._tagdata.pop(tag, None) - def __iter__(self): + def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v2)) def _unpack(self, fmt: str, data: bytes): return struct.unpack(self._endian + fmt, data) - def _pack(self, fmt: str, *values): + def _pack(self, fmt: str, *values) -> bytes: return struct.pack(self._endian + fmt, *values) list( @@ -787,7 +795,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def load_rational(self, data, legacy_api: bool = True): vals = self._unpack(f"{len(data) // 4}L", data) - def combine(a, b): + def combine(a: int, b: int) -> tuple[int, int] | IFDRational: return (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -814,7 +822,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def load_signed_rational(self, data: bytes, legacy_api: bool = True): vals = self._unpack(f"{len(data) // 4}l", data) - def combine(a, b): + def combine(a: int, b: int) -> tuple[int, int] | IFDRational: return (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -903,11 +911,11 @@ class ImageFileDirectory_v2(_IFDv2Base): warnings.warn(str(msg)) return - def tobytes(self, offset=0): + def tobytes(self, offset: int = 0) -> bytes: # FIXME What about tagdata? result = self._pack("H", len(self._tags_v2)) - entries = [] + entries: list[tuple[int, int, int, bytes, bytes]] = [] offset = offset + len(result) + len(self._tags_v2) * 12 + 4 stripoffsets = None @@ -916,7 +924,7 @@ class ImageFileDirectory_v2(_IFDv2Base): for tag, value in sorted(self._tags_v2.items()): if tag == STRIPOFFSETS: stripoffsets = len(entries) - typ = self.tagtype.get(tag) + typ = self.tagtype[tag] logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) is_ifd = typ == TiffTags.LONG and isinstance(value, dict) if is_ifd: @@ -1072,7 +1080,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): def __len__(self) -> int: return len(set(self._tagdata) | set(self._tags_v1)) - def __iter__(self): + def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v1)) def __setitem__(self, tag: int, value) -> None: @@ -1943,17 +1951,18 @@ class AppendingTiffWriter: 521, # JPEGACTables } - def __init__(self, fn, new: bool = False) -> None: - if hasattr(fn, "read"): - self.f = fn - self.close_fp = False - else: + def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None: + self.f: IO[bytes] + if is_path(fn): self.name = fn self.close_fp = True try: self.f = open(fn, "w+b" if new else "r+b") except OSError: self.f = open(fn, "w+b") + else: + self.f = cast(IO[bytes], fn) + self.close_fp = False self.beginning = self.f.tell() self.setup() @@ -1961,7 +1970,7 @@ class AppendingTiffWriter: # Reset everything. self.f.seek(self.beginning, os.SEEK_SET) - self.whereToWriteNewIFDOffset = None + self.whereToWriteNewIFDOffset: int | None = None self.offsetOfNewPage = 0 self.IIMM = iimm = self.f.read(4) @@ -2000,6 +2009,7 @@ class AppendingTiffWriter: ifd_offset = self.readLong() ifd_offset += self.offsetOfNewPage + assert self.whereToWriteNewIFDOffset is not None self.f.seek(self.whereToWriteNewIFDOffset) self.writeLong(ifd_offset) self.f.seek(ifd_offset) @@ -2020,7 +2030,7 @@ class AppendingTiffWriter: def tell(self) -> int: return self.f.tell() - self.offsetOfNewPage - def seek(self, offset: int, whence=io.SEEK_SET) -> int: + def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: if whence == os.SEEK_SET: offset += self.offsetOfNewPage @@ -2111,7 +2121,6 @@ class AppendingTiffWriter: field_size = self.fieldSizes[field_type] total_size = field_size * count is_local = total_size <= 4 - offset: int | None if not is_local: offset = self.readLong() + self.offsetOfNewPage self.rewriteLastLong(offset) @@ -2131,8 +2140,6 @@ class AppendingTiffWriter: ) self.f.seek(cur_pos) - offset = cur_pos = None - elif is_local: # skip the locally stored value that is not an offset self.f.seek(4, os.SEEK_CUR) From db5c4fbb2cb0978f58d503aadc6901c01c023a47 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jul 2024 18:41:37 +1000 Subject: [PATCH 025/652] Include required arguments --- src/PIL/ImageDraw2.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 58302f950..6e848663b 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -80,7 +80,7 @@ class Draw: return self.image def render( - self, op: str, xy: Coords, pen: Pen | Brush, brush: Brush | Pen | None = None + self, op: str, xy: Coords, pen: Pen | Brush | None, brush: Brush | Pen | None = None ) -> None: # handle color arguments outline = fill = None @@ -111,50 +111,50 @@ class Draw: (xoffset, yoffset) = offset self.transform = (1, 0, xoffset, 0, 1, yoffset) - def arc(self, xy: Coords, start, end, *options: Any) -> None: + def arc(self, xy: Coords, pen: Pen | Brush | None, start, end, *options: Any) -> None: """ Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` """ - self.render("arc", xy, start, end, *options) + self.render("arc", xy, pen, start, end, *options) - def chord(self, xy: Coords, start, end, *options: Any) -> None: + def chord(self, xy: Coords, pen: Pen | Brush | None, start, end, *options: Any) -> None: """ Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points with a straight line. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` """ - self.render("chord", xy, start, end, *options) + self.render("chord", xy, pen, start, end, *options) - def ellipse(self, xy: Coords, *options: Any) -> None: + def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ Draws an ellipse inside the given bounding box. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` """ - self.render("ellipse", xy, *options) + self.render("ellipse", xy, pen, *options) - def line(self, xy: Coords, *options: Any) -> None: + def line(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ Draws a line between the coordinates in the ``xy`` list. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` """ - self.render("line", xy, *options) + self.render("line", xy, pen, *options) - def pieslice(self, xy: Coords, start, end, *options: Any) -> None: + def pieslice(self, xy: Coords, pen: Pen | Brush | None, start, end, *options: Any) -> None: """ Same as arc, but also draws straight lines between the end points and the center of the bounding box. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` """ - self.render("pieslice", xy, start, end, *options) + self.render("pieslice", xy, pen, start, end, *options) - def polygon(self, xy: Coords, *options: Any) -> None: + def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ Draws a polygon. @@ -165,15 +165,15 @@ class Draw: .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` """ - self.render("polygon", xy, *options) + self.render("polygon", xy, pen, *options) - def rectangle(self, xy: Coords, *options: Any) -> None: + def rectangle(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ Draws a rectangle. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` """ - self.render("rectangle", xy, *options) + self.render("rectangle", xy, pen, *options) def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None: """ From b84e2a9935a49197012002122ad84a856940133a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jul 2024 18:55:17 +1000 Subject: [PATCH 026/652] Do not pass outline to arc --- src/PIL/ImageDraw2.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 6e848663b..97898ffd9 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -101,7 +101,9 @@ class Draw: path.transform(self.transform) xy = path # render the item - if op == "line": + if op == "arc": + self.draw.arc(xy, fill=outline) + elif op == "line": self.draw.line(xy, fill=outline, width=width) else: getattr(self.draw, op)(xy, fill=fill, outline=outline) From 955854728acf3456866bbfa152920b5d81eb449a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jul 2024 18:44:13 +1000 Subject: [PATCH 027/652] Pass start and end to arc, chord and pieslice --- Tests/test_imagedraw2.py | 46 +++++++++++++++++++++++++++++++++++ src/PIL/ImageDraw2.py | 52 ++++++++++++++++++++++++++++++---------- 2 files changed, 86 insertions(+), 12 deletions(-) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index c80aa739c..e0d368228 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -65,6 +65,36 @@ def test_mode() -> None: ImageDraw2.Draw("L") +@pytest.mark.parametrize("bbox", BBOX) +@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) +def test_arc(bbox: Coords, start: float, end: float) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("white", width=1) + + # Act + draw.arc(bbox, pen, start, end) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_chord(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("yellow") + brush = ImageDraw2.Brush("red") + + # Act + draw.chord(bbox, pen, 0, 180, brush) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_RGB.png", 1) + + @pytest.mark.parametrize("bbox", BBOX) def test_ellipse(bbox: Coords) -> None: # Arrange @@ -123,6 +153,22 @@ def test_line_pen_as_brush(points: Coords) -> None: assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") +@pytest.mark.parametrize("bbox", BBOX) +@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) +def test_pieslice(bbox: Coords, start: float, end: float) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue") + brush = ImageDraw2.Brush("white") + + # Act + draw.pieslice(bbox, pen, start, end, brush) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) + + @pytest.mark.parametrize("points", POINTS) def test_polygon(points: Coords) -> None: # Arrange diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 97898ffd9..3d68658ed 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -80,7 +80,12 @@ class Draw: return self.image def render( - self, op: str, xy: Coords, pen: Pen | Brush | None, brush: Brush | Pen | None = None + self, + op: str, + xy: Coords, + pen: Pen | Brush | None, + brush: Brush | Pen | None = None, + **kwargs: Any, ) -> None: # handle color arguments outline = fill = None @@ -101,35 +106,51 @@ class Draw: path.transform(self.transform) xy = path # render the item - if op == "arc": - self.draw.arc(xy, fill=outline) - elif op == "line": - self.draw.line(xy, fill=outline, width=width) + if op in ("arc", "line"): + kwargs.setdefault("fill", outline) else: - getattr(self.draw, op)(xy, fill=fill, outline=outline) + kwargs.setdefault("fill", fill) + kwargs.setdefault("outline", outline) + if op == "line": + kwargs.setdefault("width", width) + getattr(self.draw, op)(xy, **kwargs) def settransform(self, offset: tuple[float, float]) -> None: """Sets a transformation offset.""" (xoffset, yoffset) = offset self.transform = (1, 0, xoffset, 0, 1, yoffset) - def arc(self, xy: Coords, pen: Pen | Brush | None, start, end, *options: Any) -> None: + def arc( + self, + xy: Coords, + pen: Pen | Brush | None, + start: float, + end: float, + *options: Any, + ) -> None: """ Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` """ - self.render("arc", xy, pen, start, end, *options) + self.render("arc", xy, pen, *options, start=start, end=end) - def chord(self, xy: Coords, pen: Pen | Brush | None, start, end, *options: Any) -> None: + def chord( + self, + xy: Coords, + pen: Pen | Brush | None, + start: float, + end: float, + *options: Any, + ) -> None: """ Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points with a straight line. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` """ - self.render("chord", xy, pen, start, end, *options) + self.render("chord", xy, pen, *options, start=start, end=end) def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ @@ -147,14 +168,21 @@ class Draw: """ self.render("line", xy, pen, *options) - def pieslice(self, xy: Coords, pen: Pen | Brush | None, start, end, *options: Any) -> None: + def pieslice( + self, + xy: Coords, + pen: Pen | Brush | None, + start: float, + end: float, + *options: Any, + ) -> None: """ Same as arc, but also draws straight lines between the end points and the center of the bounding box. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` """ - self.render("pieslice", xy, pen, start, end, *options) + self.render("pieslice", xy, pen, *options, start=start, end=end) def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ From accfaf1c092517f7f3918c33b8f9fc38f9ae6a62 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Jul 2024 20:20:09 +1000 Subject: [PATCH 028/652] Added type hints --- Tests/test_file_jpeg.py | 2 +- Tests/test_image.py | 2 +- docs/reference/plugins.rst | 8 +++++++ src/PIL/EpsImagePlugin.py | 20 ++++++++++++---- src/PIL/Image.py | 48 +++++++++++++++++++++----------------- src/PIL/ImageFile.py | 19 +++++++++++---- src/PIL/JpegImagePlugin.py | 25 ++++++++++++-------- src/PIL/PngImagePlugin.py | 16 +++++++++---- src/PIL/TiffImagePlugin.py | 14 +++++++---- 9 files changed, 101 insertions(+), 53 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 9660e2828..b4b3f71cb 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -154,7 +154,7 @@ class TestFileJpeg: assert k > 0.9 def test_rgb(self) -> None: - def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]: + def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]: return tuple(v[0] for v in im.layer) im = hopper() diff --git a/Tests/test_image.py b/Tests/test_image.py index 4fdc41791..d8372789b 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -99,7 +99,7 @@ class TestImage: im = Image.new("L", (100, 100)) p = Pretty() - im._repr_pretty_(p, None) + im._repr_pretty_(p, False) assert p.pretty_output == "" def test_open_formats(self) -> None: diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 18cd99cf3..454b94d8c 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -185,6 +185,14 @@ Plugin reference :undoc-members: :show-inheritance: +:mod:`~PIL.MpoImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.MpoImagePlugin + :members: + :undoc-members: + :show-inheritance: + :mod:`~PIL.MspImagePlugin` Module --------------------------------- diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 7a73d1f69..35915e652 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -65,16 +65,24 @@ def has_ghostscript() -> bool: return gs_binary is not False -def Ghostscript(tile, size, fp, scale=1, transparency: bool = False) -> Image.Image: +def Ghostscript( + tile: list[ImageFile._Tile], + size: tuple[int, int], + fp: IO[bytes], + scale: int = 1, + transparency: bool = False, +) -> Image.Image: """Render an image using Ghostscript""" global gs_binary if not has_ghostscript(): msg = "Unable to locate Ghostscript on paths" raise OSError(msg) + assert isinstance(gs_binary, str) # Unpack decoder tile - decoder, tile, offset, data = tile[0] - length, bbox = data + args = tile[0].args + assert isinstance(args, tuple) + length, bbox = args # Hack to support hi-res rendering scale = int(scale) or 1 @@ -227,7 +235,11 @@ class EpsImageFile(ImageFile.ImageFile): # put floating point values there anyway. box = [int(float(i)) for i in v.split()] self._size = box[2] - box[0], box[3] - box[1] - self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] + self.tile = [ + ImageFile._Tile( + "eps", (0, 0) + self.size, offset, (length, box) + ) + ] except Exception: pass return True diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ae8634539..ebf4f46c4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -220,7 +220,7 @@ if hasattr(core, "DEFAULT_STRATEGY"): if TYPE_CHECKING: from xml.etree.ElementTree import Element - from . import ImageFile, ImagePalette + from . import ImageFile, ImagePalette, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] OPEN: dict[ @@ -676,7 +676,7 @@ class Image: id(self), ) - def _repr_pretty_(self, p, cycle) -> None: + def _repr_pretty_(self, p, cycle: bool) -> None: """IPython plain text display support""" # Same as __repr__ but without unpredictable id(self), @@ -1551,6 +1551,7 @@ class Image: ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) if ifd1 and ifd1.get(513): + assert exif._info is not None ifds.append((ifd1, exif._info.next)) offset = None @@ -1560,12 +1561,13 @@ class Image: offset = current_offset fp = self.fp - thumbnail_offset = ifd.get(513) - if thumbnail_offset is not None: - thumbnail_offset += getattr(self, "_exif_offset", 0) - self.fp.seek(thumbnail_offset) - data = self.fp.read(ifd.get(514)) - fp = io.BytesIO(data) + if ifd is not None: + thumbnail_offset = ifd.get(513) + if thumbnail_offset is not None: + thumbnail_offset += getattr(self, "_exif_offset", 0) + self.fp.seek(thumbnail_offset) + data = self.fp.read(ifd.get(514)) + fp = io.BytesIO(data) with open(fp) as im: from . import TiffImagePlugin @@ -3869,14 +3871,14 @@ class Exif(_ExifBase): bigtiff = False _loaded = False - def __init__(self): - self._data = {} - self._hidden_data = {} - self._ifds = {} - self._info = None - self._loaded_exif = None + def __init__(self) -> None: + self._data: dict[int, Any] = {} + self._hidden_data: dict[int, Any] = {} + self._ifds: dict[int, dict[int, Any]] = {} + self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None + self._loaded_exif: bytes | None = None - def _fixup(self, value): + def _fixup(self, value: Any) -> Any: try: if len(value) == 1 and isinstance(value, tuple): return value[0] @@ -3884,24 +3886,26 @@ class Exif(_ExifBase): pass return value - def _fixup_dict(self, src_dict): + def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]: # Helper function # returns a dict with any single item tuples/lists as individual values return {k: self._fixup(v) for k, v in src_dict.items()} - def _get_ifd_dict(self, offset: int, group: int | None = None): + def _get_ifd_dict( + self, offset: int, group: int | None = None + ) -> dict[int, Any] | None: try: # an offset pointer to the location of the nested embedded IFD. # It should be a long, but may be corrupted. self.fp.seek(offset) except (KeyError, TypeError): - pass + return None else: from . import TiffImagePlugin info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) info.load(self.fp) - return self._fixup_dict(info) + return self._fixup_dict(dict(info)) def _get_head(self) -> bytes: version = b"\x2B" if self.bigtiff else b"\x2A" @@ -3966,7 +3970,7 @@ class Exif(_ExifBase): self.fp.seek(offset) self._info.load(self.fp) - def _get_merged_dict(self): + def _get_merged_dict(self) -> dict[int, Any]: merged_dict = dict(self) # get EXIF extension @@ -4124,7 +4128,7 @@ class Exif(_ExifBase): keys.update(self._info) return len(keys) - def __getitem__(self, tag: int): + def __getitem__(self, tag: int) -> Any: if self._info is not None and tag not in self._data and tag in self._info: self._data[tag] = self._fixup(self._info[tag]) del self._info[tag] @@ -4133,7 +4137,7 @@ class Exif(_ExifBase): def __contains__(self, tag: object) -> bool: return tag in self._data or (self._info is not None and tag in self._info) - def __setitem__(self, tag: int, value) -> None: + def __setitem__(self, tag: int, value: Any) -> None: if self._info is not None and tag in self._info: del self._info[tag] self._data[tag] = value diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index e4a7dba44..2a8846c1a 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -86,7 +86,7 @@ def raise_oserror(error: int) -> OSError: raise _get_oserror(error, encoder=False) -def _tilesort(t) -> int: +def _tilesort(t: _Tile) -> int: # sort on offset return t[2] @@ -161,7 +161,7 @@ class ImageFile(Image.Image): return Image.MIME.get(self.format.upper()) return None - def __setstate__(self, state) -> None: + def __setstate__(self, state: list[Any]) -> None: self.tile = [] super().__setstate__(state) @@ -525,7 +525,7 @@ class Parser: # -------------------------------------------------------------------- -def _save(im, fp, tile, bufsize: int = 0) -> None: +def _save(im: Image.Image, fp: IO[bytes], tile, bufsize: int = 0) -> None: """Helper to save image based on tile list :param im: Image object. @@ -554,7 +554,12 @@ def _save(im, fp, tile, bufsize: int = 0) -> None: def _encode_tile( - im, fp: IO[bytes], tile: list[_Tile], bufsize: int, fh, exc=None + im: Image.Image, + fp: IO[bytes], + tile: list[_Tile], + bufsize: int, + fh, + exc: BaseException | None = None, ) -> None: for encoder_name, extents, offset, args in tile: if offset > 0: @@ -664,7 +669,11 @@ class PyCodec: """ self.fd = fd - def setimage(self, im, extents=None): + def setimage( + self, + im: Image.core.ImagingCore, + extents: tuple[int, int, int, int] | None = None, + ) -> None: """ Called from ImageFile to set the core output image for the codec diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index d83d60b7b..3a83f9ca6 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -42,7 +42,7 @@ import subprocess import sys import tempfile import warnings -from typing import IO, Any +from typing import IO, TYPE_CHECKING, Any from . import Image, ImageFile from ._binary import i16be as i16 @@ -51,6 +51,9 @@ from ._binary import o8 from ._binary import o16be as o16 from .JpegPresets import presets +if TYPE_CHECKING: + from .MpoImagePlugin import MpoImageFile + # # Parser @@ -329,7 +332,7 @@ class JpegImageFile(ImageFile.ImageFile): format = "JPEG" format_description = "JPEG (ISO 10918)" - def _open(self): + def _open(self) -> None: s = self.fp.read(3) if not _accept(s): @@ -342,13 +345,13 @@ class JpegImageFile(ImageFile.ImageFile): self._exif_offset = 0 # JPEG specifics (internal) - self.layer = [] - self.huffman_dc = {} - self.huffman_ac = {} - self.quantization = {} - self.app = {} # compatibility - self.applist = [] - self.icclist = [] + self.layer: list[tuple[int, int, int, int]] = [] + self.huffman_dc: dict[Any, Any] = {} + self.huffman_ac: dict[Any, Any] = {} + self.quantization: dict[int, list[int]] = {} + self.app: dict[str, bytes] = {} # compatibility + self.applist: list[tuple[str, bytes]] = [] + self.icclist: list[bytes] = [] while True: i = s[0] @@ -831,7 +834,9 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ## # Factory for making JPEG and MPO instances -def jpeg_factory(fp: IO[bytes] | None = None, filename: str | bytes | None = None): +def jpeg_factory( + fp: IO[bytes] | None = None, filename: str | bytes | None = None +) -> JpegImageFile | MpoImageFile: im = JpegImageFile(fp, filename) try: mpheader = im._getmp() diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 52cfcd13a..910fa9755 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -40,7 +40,7 @@ import warnings import zlib from collections.abc import Callable from enum import IntEnum -from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn +from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -1223,7 +1223,11 @@ def _write_multiple_frames( if default_image: if im.mode != mode: im = im.convert(mode) - ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + ImageFile._save( + im, + cast(IO[bytes], _idat(fp, chunk)), + [("zip", (0, 0) + im.size, 0, rawmode)], + ) seq_num = 0 for frame, frame_data in enumerate(im_frames): @@ -1258,14 +1262,14 @@ def _write_multiple_frames( # first frame must be in IDAT chunks for backwards compatibility ImageFile._save( im_frame, - _idat(fp, chunk), + cast(IO[bytes], _idat(fp, chunk)), [("zip", (0, 0) + im_frame.size, 0, rawmode)], ) else: fdat_chunks = _fdat(fp, chunk, seq_num) ImageFile._save( im_frame, - fdat_chunks, + cast(IO[bytes], fdat_chunks), [("zip", (0, 0) + im_frame.size, 0, rawmode)], ) seq_num = fdat_chunks.seq_num @@ -1465,7 +1469,9 @@ def _save( ) if single_im: ImageFile._save( - single_im, _idat(fp, chunk), [("zip", (0, 0) + single_im.size, 0, rawmode)] + single_im, + cast(IO[bytes], _idat(fp, chunk)), + [("zip", (0, 0) + single_im.size, 0, rawmode)], ) if info: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index dd013a0cf..d9d1bab5a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -288,8 +288,10 @@ def _accept(prefix: bytes) -> bool: return prefix[:4] in PREFIXES -def _limit_rational(val, max_val): - inv = abs(val) > 1 +def _limit_rational( + val: float | Fraction | IFDRational, max_val: int +) -> tuple[float, float]: + inv = abs(float(val)) > 1 n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) return n_d[::-1] if inv else n_d @@ -792,7 +794,9 @@ class ImageFileDirectory_v2(_IFDv2Base): return value + b"\0" @_register_loader(5, 8) - def load_rational(self, data, legacy_api: bool = True): + def load_rational( + self, data: bytes, legacy_api: bool = True + ) -> tuple[tuple[int, int] | IFDRational, ...]: vals = self._unpack(f"{len(data) // 4}L", data) def combine(a: int, b: int) -> tuple[int, int] | IFDRational: @@ -801,7 +805,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(5) - def write_rational(self, *values) -> bytes: + def write_rational(self, *values: IFDRational) -> bytes: return b"".join( self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values ) @@ -828,7 +832,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(10) - def write_signed_rational(self, *values) -> bytes: + def write_signed_rational(self, *values: IFDRational) -> bytes: return b"".join( self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) for frac in values From e6fd8359d3184e09b8194c8ae8ec87c5a8c27c7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Aug 2024 12:27:45 +1000 Subject: [PATCH 029/652] Deprecate huffman_ac and huffman_dc --- Tests/test_file_jpeg.py | 7 +++++++ docs/deprecations.rst | 8 ++++++++ docs/releasenotes/11.0.0.rst | 8 ++++++++ src/PIL/JpegImagePlugin.py | 11 +++++++++-- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index b4b3f71cb..df589e24a 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1045,6 +1045,13 @@ class TestFileJpeg: assert im._repr_jpeg_() is None + def test_deprecation(self) -> None: + with Image.open(TEST_FILE) as im: + with pytest.warns(DeprecationWarning): + assert im.huffman_ac == {} + with pytest.warns(DeprecationWarning): + assert im.huffman_dc == {} + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 2f5800e07..a03f274a9 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -118,6 +118,14 @@ The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword arguments can be used instead. +JpegImageFile.huffman_ac and JpegImageFile.huffman_dc +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused, and have +been deprecated. + Removed features ---------------- diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index bb6179de8..579821ba9 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -50,6 +50,14 @@ The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword arguments can be used instead. +JpegImageFile.huffman_ac and JpegImageFile.huffman_dc +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused, and have +been deprecated. + API Changes =========== diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 3a83f9ca6..fc897e2b9 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -49,6 +49,7 @@ from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 +from ._deprecate import deprecate from .JpegPresets import presets if TYPE_CHECKING: @@ -346,8 +347,8 @@ class JpegImageFile(ImageFile.ImageFile): # JPEG specifics (internal) self.layer: list[tuple[int, int, int, int]] = [] - self.huffman_dc: dict[Any, Any] = {} - self.huffman_ac: dict[Any, Any] = {} + self._huffman_dc: dict[Any, Any] = {} + self._huffman_ac: dict[Any, Any] = {} self.quantization: dict[int, list[int]] = {} self.app: dict[str, bytes] = {} # compatibility self.applist: list[tuple[str, bytes]] = [] @@ -386,6 +387,12 @@ class JpegImageFile(ImageFile.ImageFile): self._read_dpi_from_exif() + def __getattr__(self, name: str) -> Any: + if name in ("huffman_ac", "huffman_dc"): + deprecate(name, 12) + return getattr(self, "_" + name) + raise AttributeError(name) + def load_read(self, read_bytes: int) -> bytes: """ internal: read more image data From 488e1982bd8d1152c202652260ce8f239178c8f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Aug 2024 16:29:46 +1000 Subject: [PATCH 030/652] Added removal version and date --- docs/deprecations.rst | 4 ++-- docs/releasenotes/11.0.0.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a03f274a9..058468cfe 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -123,8 +123,8 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc .. deprecated:: 11.0.0 -The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused, and have -been deprecated. +The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They +have been deprecated, and will be removed in Pillow 12 (2025-10-15). Removed features ---------------- diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 579821ba9..0175ae52d 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -55,8 +55,8 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc .. deprecated:: 11.0.0 -The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused, and have -been deprecated. +The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They +have been deprecated, and will be removed in Pillow 12 (2025-10-15). API Changes =========== From 8f1157a61d70ee003df5a9a91f77219beb8038a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Aug 2024 23:30:27 +1000 Subject: [PATCH 031/652] Added type hints to Image.__init__() --- Tests/test_box_blur.py | 1 + Tests/test_color_lut.py | 16 ++++++ Tests/test_file_webp.py | 1 + Tests/test_image.py | 7 +++ Tests/test_image_array.py | 2 + Tests/test_image_convert.py | 1 + Tests/test_image_filter.py | 4 +- Tests/test_image_getim.py | 1 + Tests/test_image_putpalette.py | 1 + Tests/test_image_quantize.py | 4 ++ Tests/test_image_resample.py | 1 + Tests/test_image_resize.py | 5 ++ Tests/test_imagemorph.py | 2 + Tests/test_imageops_usm.py | 2 + Tests/test_lib_image.py | 3 ++ src/PIL/BlpImagePlugin.py | 1 + src/PIL/BmpImagePlugin.py | 3 ++ src/PIL/EpsImagePlugin.py | 8 +-- src/PIL/FpxImagePlugin.py | 2 + src/PIL/GifImagePlugin.py | 48 ++++++++++++------ src/PIL/IcnsImagePlugin.py | 1 + src/PIL/ImImagePlugin.py | 1 + src/PIL/Image.py | 90 +++++++++++++++++++++++++++------- src/PIL/ImageChops.py | 17 +++++++ src/PIL/ImageCms.py | 3 ++ src/PIL/ImageDraw.py | 5 +- src/PIL/ImageFile.py | 2 + src/PIL/ImageFont.py | 1 + src/PIL/ImageMath.py | 5 ++ src/PIL/ImageMorph.py | 4 ++ src/PIL/ImageTk.py | 9 ++-- src/PIL/IptcImagePlugin.py | 11 ++++- src/PIL/JpegImagePlugin.py | 1 + src/PIL/PalmImagePlugin.py | 1 + src/PIL/PcdImagePlugin.py | 1 + src/PIL/PngImagePlugin.py | 15 ++++-- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 1 + 38 files changed, 232 insertions(+), 51 deletions(-) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 1f6ed6127..dd5fe0290 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -24,6 +24,7 @@ def test_imageops_box_blur() -> None: def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image: + assert image.im is not None return image._new(image.im.box_blur((radius, radius), n)) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 0d9c0b419..574ccd124 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -47,6 +47,7 @@ class TestColorLut3DCoreAPI: def test_wrong_args(self) -> None: im = Image.new("RGB", (10, 10), 0) + assert im.im is not None with pytest.raises(ValueError, match="filter"): im.im.color_lut_3d( @@ -107,6 +108,7 @@ class TestColorLut3DCoreAPI: def test_correct_args(self) -> None: im = Image.new("RGB", (10, 10), 0) + assert im.im is not None im.im.color_lut_3d( "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) @@ -143,51 +145,60 @@ class TestColorLut3DCoreAPI: def test_wrong_mode(self) -> None: with pytest.raises(ValueError, match="wrong mode"): im = Image.new("L", (10, 10), 0) + assert im.im is not None im.im.color_lut_3d( "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ) with pytest.raises(ValueError, match="wrong mode"): im = Image.new("RGB", (10, 10), 0) + assert im.im is not None im.im.color_lut_3d( "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ) with pytest.raises(ValueError, match="wrong mode"): im = Image.new("L", (10, 10), 0) + assert im.im is not None im.im.color_lut_3d( "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ) with pytest.raises(ValueError, match="wrong mode"): im = Image.new("RGB", (10, 10), 0) + assert im.im is not None im.im.color_lut_3d( "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ) with pytest.raises(ValueError, match="wrong mode"): im = Image.new("RGB", (10, 10), 0) + assert im.im is not None im.im.color_lut_3d( "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) def test_correct_mode(self) -> None: im = Image.new("RGBA", (10, 10), 0) + assert im.im is not None im.im.color_lut_3d( "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ) im = Image.new("RGBA", (10, 10), 0) + assert im.im is not None im.im.color_lut_3d( "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) im = Image.new("RGB", (10, 10), 0) + assert im.im is not None im.im.color_lut_3d( "HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ) im = Image.new("RGB", (10, 10), 0) + assert im.im is not None im.im.color_lut_3d( "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) @@ -202,6 +213,7 @@ class TestColorLut3DCoreAPI: g.transpose(Image.Transpose.ROTATE_180), ], ) + assert im.im is not None # Fast test with small cubes for size in [2, 3, 5, 7, 11, 16, 17]: @@ -238,6 +250,7 @@ class TestColorLut3DCoreAPI: g.transpose(Image.Transpose.ROTATE_180), ], ) + assert im.im is not None # Red channel copied to alpha assert_image_equal( @@ -262,6 +275,7 @@ class TestColorLut3DCoreAPI: g.transpose(Image.Transpose.ROTATE_270), ], ) + assert im.im is not None assert_image_equal( im, @@ -284,6 +298,7 @@ class TestColorLut3DCoreAPI: g.transpose(Image.Transpose.ROTATE_180), ], ) + assert im.im is not None # Reverse channels by splitting and using table # fmt: off @@ -309,6 +324,7 @@ class TestColorLut3DCoreAPI: g.transpose(Image.Transpose.ROTATE_180), ], ) + assert im.im is not None # fmt: off transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index cbc905de4..3fb416ffe 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -250,6 +250,7 @@ class TestFileWebp: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGBA", (1, 1)).convert("P") assert im.mode == "P" + assert im.palette is not None assert im.palette.mode == "RGBA" im.save(temp_file) diff --git a/Tests/test_image.py b/Tests/test_image.py index d8372789b..d4ab8ad38 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -695,11 +695,13 @@ class TestImage: image: Image.Image, palette_result: ImagePalette.ImagePalette | None = None, ) -> None: + assert image.im is not None new_image = base_image._new(image.im) assert new_image.mode == image.mode assert new_image.size == image.size assert new_image.info == base_image.info if palette_result is not None: + assert new_image.palette is not None assert new_image.palette.tobytes() == palette_result.tobytes() else: assert new_image.palette is None @@ -990,12 +992,14 @@ class TestImage: # P mode with RGBA palette im = Image.new("RGBA", (1, 1)).convert("P") assert im.mode == "P" + assert im.palette is not None assert im.palette.mode == "RGBA" assert im.has_transparency_data def test_apply_transparency(self) -> None: im = Image.new("P", (1, 1)) im.putpalette((0, 0, 0, 1, 1, 1)) + assert im.palette is not None assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} # Test that no transformation is applied without transparency @@ -1013,13 +1017,16 @@ class TestImage: im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA") im.info["transparency"] = 0 im.apply_transparency() + assert im.palette is not None assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1} # Test that transparency bytes are applied with Image.open("Tests/images/pil123p.png") as im: assert isinstance(im.info["transparency"], bytes) + assert im.palette is not None assert im.palette.colors[(27, 35, 6)] == 24 im.apply_transparency() + assert im.palette is not None assert im.palette.colors[(27, 35, 6, 214)] == 24 def test_constants(self) -> None: diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index bb6064882..4d8ffbf4a 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -113,4 +113,6 @@ def test_fromarray_palette() -> None: out = Image.fromarray(a, "P") # Assert that the Python and C palettes match + assert out.palette is not None + assert out.im is not None assert len(out.palette.colors) == len(out.im.getpalette()) / 3 diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index ebb7f2822..6a925975e 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -218,6 +218,7 @@ def test_trns_RGB(tmp_path: Path) -> None: def test_l_macro_rounding(convert_mode: str) -> None: for mode in ("P", "PA"): im = Image.new(mode, (1, 1)) + assert im.palette is not None im.palette.getcolor((0, 1, 2)) converted_im = im.convert(convert_mode) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 412ab44c3..ea5438f3d 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -137,7 +137,9 @@ def test_builtinfilter_p() -> None: builtin_filter = ImageFilter.BuiltinFilter() with pytest.raises(ValueError): - builtin_filter.filter(hopper("P").im) + im = hopper("P").im + assert im is not None + builtin_filter.filter(im) def test_kernel_not_enough_coefficients() -> None: diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 9afa02b0a..e62d6e0ec 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -8,4 +8,5 @@ def test_sanity() -> None: type_repr = repr(type(im.getim())) assert "PyCapsule" in type_repr + assert im.im is not None assert isinstance(im.im.id, int) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 75d9d2fc1..f2c447f71 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -86,6 +86,7 @@ def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: im = Image.new("P", (1, 1)) im.putpalette(palette, mode) assert im.getpalette() == [1, 2, 3] + assert im.palette is not None assert im.palette.colors == {(1, 2, 3, 4): 0} diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 903cd8550..7c564d967 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -69,6 +69,7 @@ def test_quantize_no_dither() -> None: converted = image.quantize(dither=Image.Dither.NONE, palette=palette) assert converted.mode == "P" + assert converted.palette is not None assert converted.palette.palette == palette.palette.palette @@ -81,6 +82,7 @@ def test_quantize_no_dither2() -> None: palette.putpalette(data) quantized = im.quantize(dither=Image.Dither.NONE, palette=palette) + assert quantized.palette is not None assert tuple(quantized.palette.palette) == data px = quantized.load() @@ -117,6 +119,7 @@ def test_colors() -> None: im = hopper() colors = 2 converted = im.quantize(colors) + assert converted.palette is not None assert len(converted.palette.palette) == colors * len("RGB") @@ -147,6 +150,7 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None: converted = im.quantize(method=method) converted_px = converted.load() assert converted_px is not None + assert converted.palette is not None assert converted_px[0, 0] == converted.palette.colors[color] diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index ce6209c0d..4a5901d47 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -27,6 +27,7 @@ class TestImagingResampleVulnerability: ): with pytest.raises(MemoryError): # any resampling filter will do here + assert im.im is not None im.im.resize((xsize, ysize), Image.Resampling.BILINEAR) def test_invalid_size(self) -> None: diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index c9e304512..5700b3e06 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -27,6 +27,7 @@ class TestImagingCoreResize: ) -> Image.Image: # Image class independent version of resize. im.load() + assert im.im is not None return im._new(im.im.resize(size, f)) @pytest.mark.parametrize( @@ -37,6 +38,8 @@ class TestImagingCoreResize: r = self.resize(im, (15, 12), Image.Resampling.NEAREST) assert r.mode == mode assert r.size == (15, 12) + assert r.im is not None + assert im.im is not None assert r.im.bands == im.im.bands def test_convolution_modes(self) -> None: @@ -51,6 +54,8 @@ class TestImagingCoreResize: r = self.resize(im, (15, 12), Image.Resampling.BILINEAR) assert r.mode == mode assert r.size == (15, 12) + assert r.im is not None + assert im.im is not None assert r.im.bands == im.im.bands @pytest.mark.parametrize( diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 4363f456e..b40de7f98 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -325,6 +325,8 @@ def test_wrong_mode() -> None: lut = ImageMorph.LutBuilder(op_name="corner").build_lut() imrgb = Image.new("RGB", (10, 10)) iml = Image.new("L", (10, 10)) + assert imrgb.im is not None + assert iml.im is not None with pytest.raises(RuntimeError): _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 920012d86..64116bf70 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -98,10 +98,12 @@ def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None: (4, 3, 2), (4, 2, 2), ]: + assert i.im is not None assert i.im.getpixel((x, y))[c] >= 250 # Fuzzy match. def gp(x: int, y: int) -> tuple[int, ...]: + assert i.im is not None return i.im.getpixel((x, y)) assert 236 <= gp(7, 4)[0] <= 239 diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 31548bbc9..0a36af8b6 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -7,18 +7,21 @@ from PIL import Image def test_setmode() -> None: im = Image.new("L", (1, 1), 255) + assert im.im is not None im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 im.im.setmode("L") assert im.im.getpixel((0, 0)) == 255 im = Image.new("1", (1, 1), 1) + assert im.im is not None im.im.setmode("L") assert im.im.getpixel((0, 0)) == 255 im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 im = Image.new("RGB", (1, 1), (1, 2, 3)) + assert im.im is not None im.im.setmode("RGB") assert im.im.getpixel((0, 0)) == (1, 2, 3) im.im.setmode("RGBA") diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 6d71049a9..dc7192b99 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -467,6 +467,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2" fp.write(magic) + assert im.palette is not None fp.write(struct.pack("= 16 + assert isinstance(file_info["width"], int) + assert isinstance(file_info["height"], int) self._size = file_info["width"], file_info["height"] # ------- If color count was not found in the header, compute from bits @@ -443,6 +445,7 @@ def _save( elif im.mode == "L": palette = b"".join(o8(i) * 4 for i in range(256)) elif im.mode == "P": + assert im.im is not None palette = im.im.getpalette("RGB", "BGRX") colors = len(palette) // 4 else: diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 35915e652..c5ca8dadc 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -71,7 +71,7 @@ def Ghostscript( fp: IO[bytes], scale: int = 1, transparency: bool = False, -) -> Image.Image: +) -> Image.core.ImagingCore: """Render an image using Ghostscript""" global gs_binary if not has_ghostscript(): @@ -161,6 +161,7 @@ def Ghostscript( except OSError: pass + assert out_im.im is not None im = out_im.im.copy() out_im.close() return im @@ -190,7 +191,6 @@ class EpsImageFile(ImageFile.ImageFile): self.fp.seek(offset) self._mode = "RGB" - self._size = None byte_arr = bytearray(255) bytes_mv = memoryview(byte_arr) @@ -228,7 +228,7 @@ class EpsImageFile(ImageFile.ImageFile): if k == "BoundingBox": if v == "(atend)": reading_trailer_comments = True - elif not self._size or (trailer_reached and reading_trailer_comments): + elif not self.tile or (trailer_reached and reading_trailer_comments): try: # Note: The DSC spec says that BoundingBox # fields should be integers, but some drivers @@ -346,7 +346,7 @@ class EpsImageFile(ImageFile.ImageFile): trailer_reached = True bytes_read = 0 - if not self._size: + if not self.tile: msg = "cannot determine EPS bounding box" raise OSError(msg) diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 386e37233..64d48635e 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -81,6 +81,8 @@ class FpxImageFile(ImageFile.ImageFile): # size (highest resolution) + assert isinstance(prop[0x1000002], int) + assert isinstance(prop[0x1000003], int) self._size = prop[0x1000002], prop[0x1000003] size = max(self.size) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index bf74f9356..55f927d40 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -307,6 +307,7 @@ class GifImageFile(ImageFile.ImageFile): self.tile = [] if self.dispose: + assert self.im is not None self.im.paste(self.dispose, self.dispose_extent) self._frame_palette = palette if palette is not None else self.global_palette @@ -320,17 +321,21 @@ class GifImageFile(ImageFile.ImageFile): else: self._mode = "L" - if not palette and self.global_palette: + if palette: + self.palette = palette + elif self.global_palette: from copy import copy - palette = copy(self.global_palette) - self.palette = palette + self.palette = copy(self.global_palette) + else: + self.palette = None else: if self.mode == "P": if ( LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY or palette ): + assert self.im is not None if "transparency" in self.info: self.im.putpalettealpha(self.info["transparency"], 0) self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) @@ -432,6 +437,7 @@ class GifImageFile(ImageFile.ImageFile): self._prev_im = self.im if self._frame_palette: self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) + assert self.im is not None self.im.putpalette("RGB", *self._frame_palette.getdata()) else: self.im = None @@ -441,6 +447,7 @@ class GifImageFile(ImageFile.ImageFile): super().load_prepare() def load_end(self) -> None: + assert self.im is not None if self.__frame == 0: if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: if self._frame_transparency is not None: @@ -495,6 +502,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image: return im if Image.getmodebase(im.mode) == "RGB": im = im.convert("P", palette=Image.Palette.ADAPTIVE) + assert im.palette is not None if im.palette.mode == "RGBA": for rgba in im.palette.colors: if rgba[3] == 0: @@ -531,16 +539,17 @@ def _normalize_palette( if im.mode == "P": if not source_palette: + assert im.im is not None source_palette = im.im.getpalette("RGB")[:768] else: # L-mode if not source_palette: source_palette = bytearray(i // 3 for i in range(768)) im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) + assert source_palette is not None - used_palette_colors: list[int] | None if palette: - used_palette_colors = [] - assert source_palette is not None + used_palette_colors: list[int | None] = [] + assert im.palette is not None for i in range(0, len(source_palette), 3): source_color = tuple(source_palette[i : i + 3]) index = im.palette.colors.get(source_color) @@ -553,20 +562,25 @@ def _normalize_palette( if j not in used_palette_colors: used_palette_colors[i] = j break - im = im.remap_palette(used_palette_colors) + dest_map: list[int] = [] + for index in used_palette_colors: + assert index is not None + dest_map.append(index) + im = im.remap_palette(dest_map) else: - used_palette_colors = _get_optimize(im, info) - if used_palette_colors is not None: - im = im.remap_palette(used_palette_colors, source_palette) + optimized_palette_colors = _get_optimize(im, info) + if optimized_palette_colors is not None: + im = im.remap_palette(optimized_palette_colors, source_palette) if "transparency" in info: try: - info["transparency"] = used_palette_colors.index( + info["transparency"] = optimized_palette_colors.index( info["transparency"] ) except ValueError: del info["transparency"] return im + assert im.palette is not None im.palette.palette = source_palette return im @@ -578,7 +592,8 @@ def _write_single_frame( ) -> None: im_out = _normalize_mode(im) for k, v in im_out.info.items(): - im.encoderinfo.setdefault(k, v) + if isinstance(k, str): + im.encoderinfo.setdefault(k, v) im_out = _normalize_palette(im_out, palette, im.encoderinfo) for s in _get_global_header(im_out, im.encoderinfo): @@ -630,7 +645,8 @@ def _write_multiple_frames( for k, v in im_frame.info.items(): if k == "transparency": continue - im.encoderinfo.setdefault(k, v) + if isinstance(k, str): + im.encoderinfo.setdefault(k, v) encoderinfo = im.encoderinfo.copy() if "transparency" in im_frame.info: @@ -660,10 +676,12 @@ def _write_multiple_frames( ) background = _get_background(im_frame, color) background_im = Image.new("P", im_frame.size, background) + assert im_frames[0].im.palette is not None background_im.putpalette(im_frames[0].im.palette) bbox = _getbbox(background_im, im_frame)[1] elif encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: + assert im_frame.palette is not None try: encoderinfo["transparency"] = ( im_frame.palette._new_color_index(im_frame) @@ -901,6 +919,7 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: if optimise or max(used_palette_colors) >= len(used_palette_colors): return used_palette_colors + assert im.palette is not None num_palette_colors = len(im.palette.palette) // Image.getmodebands( im.palette.mode ) @@ -950,7 +969,7 @@ def _get_palette_bytes(im: Image.Image) -> bytes: :param im: Image object :returns: Bytes, len<=768 suitable for inclusion in gif header """ - return im.palette.palette if im.palette else b"" + return bytes(im.palette.palette) if im.palette else b"" def _get_background( @@ -963,6 +982,7 @@ def _get_background( # WebPImagePlugin stores an RGBA value in info["background"] # So it must be converted to the same format as GifImagePlugin's # info["background"] - a global color table index + assert im.palette is not None try: background = im.palette.getcolor(info_background, im) except ValueError as e: diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 8729f7643..58d4a90a0 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -92,6 +92,7 @@ def read_32( msg = f"Error reading channel [{repr(bytesleft)} left]" raise SyntaxError(msg) band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) + assert im.im is not None im.im.putband(band.im, band_ix) return {"RGB": im} diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 2fb7ecd52..9d60011be 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -353,6 +353,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"Lut: 1\r\n") fp.write(b"\000" * (511 - fp.tell()) + b"\032") if im.mode in ["P", "PA"]: + assert im.im is not None im_palette = im.im.getpalette("RGB", "RGB;L") colors = len(im_palette) // 3 palette = b"" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ebf4f46c4..7532a49a2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -532,16 +532,16 @@ class Image: format_description: str | None = None _close_exclusive_fp_after_loading = True - def __init__(self): + def __init__(self) -> None: # FIXME: take "new" parameters / other image? # FIXME: turn mode and size into delegating properties? - self.im = None + self.im: core.ImagingCore | None = None self._mode = "" self._size = (0, 0) - self.palette = None - self.info = {} + self.palette: ImagePalette.ImagePalette | None = None + self.info: dict[str | tuple[int, int], Any] = {} self.readonly = 0 - self._exif = None + self._exif: Exif | None = None @property def width(self) -> int: @@ -617,11 +617,12 @@ class Image: # Instead of simply setting to None, we're setting up a # deferred error that will better explain that the core image # object is gone. - self.im = DeferredError(ValueError("Operation on closed image")) + self.im = DeferredError(ValueError("Operation on closed image")) # type: ignore[assignment] def _copy(self) -> None: self.load() - self.im = self.im.copy() + if self.im: + self.im = self.im.copy() self.readonly = 0 def _ensure_mutable(self) -> None: @@ -648,6 +649,7 @@ class Image: self.load() if not format or format == "PPM": + assert self.im is not None self.im.save_ppm(filename) else: self.save(filename, format, **options) @@ -796,6 +798,7 @@ class Image: # unpack data e = _getencoder(self.mode, encoder_name, encoder_args) + assert self.im is not None e.setimage(self.im) bufsize = max(65536, self.size[0] * 4) # see RawEncode.c @@ -862,6 +865,7 @@ class Image: # unpack data d = _getdecoder(self.mode, decoder_name, decoder_args) + assert self.im is not None d.setimage(self.im) s = d.decode(data) @@ -975,6 +979,7 @@ class Image: deprecate(mode, 12) self.load() + assert self.im is not None has_transparency = "transparency" in self.info if not mode and self.mode == "P": @@ -1045,9 +1050,11 @@ class Image: # use existing conversions trns_im = new(self.mode, (1, 1)) if self.mode == "P": + assert self.palette is not None trns_im.putpalette(self.palette) if isinstance(t, tuple): err = "Couldn't allocate a palette color for transparency" + assert trns_im.palette is not None try: t = trns_im.palette.getcolor(t, self) except ValueError as e: @@ -1087,6 +1094,7 @@ class Image: new_im = self._new(im) from . import ImagePalette + assert new_im.im is not None new_im.palette = ImagePalette.ImagePalette( "RGB", new_im.im.getpalette("RGB") ) @@ -1149,7 +1157,9 @@ class Image: if trns is not None: if new_im.mode == "P" and new_im.palette: try: - new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) + new_im.info["transparency"] = new_im.palette.getcolor( + cast(tuple[int, ...], trns), new_im # trns was converted to RGB + ) except ValueError as e: del new_im.info["transparency"] if str(e) != "cannot allocate more than 256 colors": @@ -1216,6 +1226,7 @@ class Image: ) raise ValueError(msg) + assert self.im is not None if palette: # use palette from reference image palette.load() @@ -1227,6 +1238,7 @@ class Image: raise ValueError(msg) im = self.im.convert("P", dither, palette.im) new_im = self._new(im) + assert palette.palette is not None new_im.palette = palette.palette.copy() return new_im @@ -1253,6 +1265,7 @@ class Image: :returns: An :py:class:`~PIL.Image.Image` object. """ self.load() + assert self.im is not None return self._new(self.im.copy()) __copy__ = copy @@ -1281,6 +1294,7 @@ class Image: raise ValueError(msg) self.load() + assert self.im is not None return self._new(self._crop(self.im, box)) def _crop( @@ -1334,6 +1348,7 @@ class Image: if ymargin is None: ymargin = xmargin self.load() + assert self.im is not None return self._new(self.im.expand(xmargin, ymargin)) if TYPE_CHECKING: @@ -1358,6 +1373,7 @@ class Image: raise TypeError(msg) multiband = isinstance(filter, ImageFilter.MultibandFilter) + assert self.im is not None if self.im.bands == 1 or multiband: return self._new(filter.filter(self.im)) @@ -1393,6 +1409,7 @@ class Image: """ self.load() + assert self.im is not None return self.im.getbbox(alpha_only) def getcolors( @@ -1412,6 +1429,7 @@ class Image: """ self.load() + assert self.im is not None if self.mode in ("1", "L", "P"): h = self.im.histogram() out: list[tuple[int, float]] = [(h[i], i) for i in range(256) if h[i]] @@ -1439,6 +1457,7 @@ class Image: """ self.load() + assert self.im is not None if band is not None: return self.im.getband(band) return self.im # could be abused @@ -1454,6 +1473,7 @@ class Image: """ self.load() + assert self.im is not None if self.im.bands > 1: return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) return self.im.getextrema() @@ -1592,6 +1612,7 @@ class Image: """ self.load() + assert self.im is not None return self.im.ptr def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: @@ -1608,6 +1629,7 @@ class Image: """ self.load() + assert self.im is not None try: mode = self.im.getpalettemode() except ValueError: @@ -1628,11 +1650,15 @@ class Image: :returns: A boolean. """ - return ( + if ( self.mode in ("LA", "La", "PA", "RGBA", "RGBa") - or (self.mode == "P" and self.palette.mode.endswith("A")) or "transparency" in self.info - ) + ): + return True + if self.mode == "P": + assert self.palette is not None + return self.palette.mode.endswith("A") + return False def apply_transparency(self) -> None: """ @@ -1671,6 +1697,7 @@ class Image: """ self.load() + assert self.im is not None return self.im.getpixel(tuple(xy)) def getprojection(self) -> tuple[list[int], list[int]]: @@ -1682,6 +1709,7 @@ class Image: """ self.load() + assert self.im is not None x, y = self.im.getprojection() return list(x), list(y) @@ -1709,6 +1737,7 @@ class Image: :returns: A list containing pixel counts. """ self.load() + assert self.im is not None if mask: mask.load() return self.im.histogram((0, 0), mask.im) @@ -1737,6 +1766,7 @@ class Image: :returns: A float value representing the image entropy """ self.load() + assert self.im is not None if mask: mask.load() return self.im.entropy((0, 0), mask.im) @@ -1815,26 +1845,32 @@ class Image: raise ValueError(msg) box += (box[0] + size[0], box[1] + size[1]) + source: core.ImagingCore | str | float | tuple[float, ...] if isinstance(im, str): from . import ImageColor - im = ImageColor.getcolor(im, self.mode) - + source = ImageColor.getcolor(im, self.mode) elif isImageType(im): im.load() if self.mode != im.mode: if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): # should use an adapter for this! im = im.convert(self.mode) - im = im.im + assert im.im is not None + source = im.im + elif isinstance(im, tuple): + source = im + else: + source = cast(float, im) self._ensure_mutable() + assert self.im is not None if mask: mask.load() - self.im.paste(im, box, mask.im) + self.im.paste(source, box, mask.im) else: - self.im.paste(im, box) + self.im.paste(source, box) def alpha_composite( self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0) @@ -1924,6 +1960,7 @@ class Image: if isinstance(lut, ImagePointHandler): return lut.point(self) + assert self.im is not None if callable(lut): # if it isn't a list, it should be a function if self.mode in ("I", "I;16", "F"): @@ -1960,17 +1997,20 @@ class Image: if self.mode not in ("LA", "PA", "RGBA"): # attempt to promote self to a matching alpha mode + assert self.im is not None try: mode = getmodebase(self.mode) + "A" try: self.im.setmode(mode) except (AttributeError, ValueError) as e: # do things the hard way + assert self.im is not None im = self.im.convert(mode) if im.mode not in ("LA", "PA", "RGBA"): msg = "alpha channel could not be added" raise ValueError(msg) from e # sanity check self.im = im + assert self.im is not None self._mode = self.im.mode except KeyError as e: msg = "illegal image mode" @@ -1981,6 +2021,7 @@ class Image: else: band = 3 + assert self.im is not None if isImageType(alpha): # alpha layer if alpha.mode not in ("1", "L"): @@ -2023,6 +2064,7 @@ class Image: self._ensure_mutable() + assert self.im is not None self.im.putdata(data, scale, offset) def putpalette( @@ -2105,8 +2147,10 @@ class Image: if self.mode == "PA": alpha = value[3] if len(value) == 4 else 255 value = value[:3] - palette_index = self.palette.getcolor(value, self) + assert self.palette is not None + palette_index = self.palette.getcolor(tuple(value), self) value = (palette_index, alpha) if self.mode == "PA" else palette_index + assert self.im is not None return self.im.putpixel(xy, value) def remap_palette( @@ -2133,6 +2177,7 @@ class Image: if source_palette is None: if self.mode == "P": self.load() + assert self.im is not None palette_mode = self.im.getpalettemode() if palette_mode == "RGBA": bands = 4 @@ -2180,6 +2225,7 @@ class Image: # m_im.putpalette(mapping_palette, 'L') # converts to 'P' # or just force it. # UNDONE -- this is part of the general issue with palettes + assert m_im.im is not None m_im.im.putpalette(palette_mode, palette_mode + ";L", m_im.palette.tobytes()) m_im = m_im.convert("L") @@ -2325,6 +2371,7 @@ class Image: (box[3] - reduce_box[1]) / factor_y, ) + assert self.im is not None return self._new(self.im.resize(size, resample, box)) def reduce( @@ -2359,7 +2406,7 @@ class Image: return im.convert(self.mode) self.load() - + assert self.im is not None return self._new(self.im.reduce(factor, box)) def rotate( @@ -2637,6 +2684,7 @@ class Image: """ self.load() + assert self.im is not None if self.im.bands == 1: return (self.copy(),) return tuple(map(self._new, self.im.split())) @@ -2661,6 +2709,7 @@ class Image: msg = f'The image has no channel "{channel}"' raise ValueError(msg) from e + assert self.im is not None return self._new(self.im.getband(channel)) def tell(self) -> int: @@ -2765,6 +2814,7 @@ class Image: self.im = im.im self._size = final_size + assert self.im is not None self._mode = self.im.mode self.readonly = 0 @@ -2949,6 +2999,7 @@ class Image: if image.mode in ("1", "P"): resample = Resampling.NEAREST + assert self.im is not None self.im.transform(box, image.im, method, data, resample, fill) def transpose(self, method: Transpose) -> Image: @@ -2963,6 +3014,7 @@ class Image: """ self.load() + assert self.im is not None return self._new(self.im.transpose(method)) def effect_spread(self, distance: int) -> Image: @@ -2972,6 +3024,7 @@ class Image: :param distance: Distance to spread pixels. """ self.load() + assert self.im is not None return self._new(self.im.effect_spread(distance)) def toqimage(self): @@ -3205,6 +3258,7 @@ def frombuffer( if mode == "P": from . import ImagePalette + assert im.im is not None im.palette = ImagePalette.ImagePalette("RGB", im.im.getpalette("RGB")) im.readonly = 1 return im diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index 29a5c995f..b1b616d5c 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -48,6 +48,7 @@ def invert(image: Image.Image) -> Image.Image: """ image.load() + assert image.im is not None return image._new(image.im.chop_invert()) @@ -63,6 +64,7 @@ def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_lighter(image2.im)) @@ -78,6 +80,7 @@ def darker(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_darker(image2.im)) @@ -93,6 +96,7 @@ def difference(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_difference(image2.im)) @@ -110,6 +114,7 @@ def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_multiply(image2.im)) @@ -124,6 +129,7 @@ def screen(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_screen(image2.im)) @@ -136,6 +142,7 @@ def soft_light(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_soft_light(image2.im)) @@ -148,6 +155,7 @@ def hard_light(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_hard_light(image2.im)) @@ -160,6 +168,7 @@ def overlay(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_overlay(image2.im)) @@ -177,6 +186,7 @@ def add( image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_add(image2.im, scale, offset)) @@ -194,6 +204,7 @@ def subtract( image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_subtract(image2.im, scale, offset)) @@ -207,6 +218,7 @@ def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_add_modulo(image2.im)) @@ -220,6 +232,7 @@ def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_subtract_modulo(image2.im)) @@ -238,6 +251,7 @@ def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_and(image2.im)) @@ -253,6 +267,7 @@ def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_or(image2.im)) @@ -268,6 +283,7 @@ def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() + assert image1.im is not None return image1._new(image1.im.chop_xor(image2.im)) @@ -308,4 +324,5 @@ def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Imag if yoffset is None: yoffset = xoffset image.load() + assert image.im is not None return image._new(image.im.offset(xoffset, yoffset)) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index ec10230f1..3a6d52949 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -352,6 +352,8 @@ class ImageCmsTransform(Image.ImagePointHandler): im.load() if imOut is None: imOut = Image.new(self.output_mode, im.size, None) + assert im.im is not None + assert imOut.im is not None self.transform.apply(im.im.id, imOut.im.id) imOut.info["icc_profile"] = self.output_profile.tobytes() return imOut @@ -361,6 +363,7 @@ class ImageCmsTransform(Image.ImagePointHandler): if im.mode != self.output_mode: msg = "mode mismatch" raise ValueError(msg) # wrong output mode + assert im.im is not None self.transform.apply(im.im.id, im.im.id) im.info["icc_profile"] = self.output_profile.tobytes() return im diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 6f56d0236..988e3d7c5 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -32,7 +32,6 @@ from __future__ import annotations import math -import numbers import struct from collections.abc import Sequence from types import ModuleType @@ -160,13 +159,13 @@ class ImageDraw: if ink is not None: if isinstance(ink, str): ink = ImageColor.getcolor(ink, self.mode) - if self.palette and not isinstance(ink, numbers.Number): + if self.palette and isinstance(ink, tuple): ink = self.palette.getcolor(ink, self._image) result_ink = self.draw.draw_ink(ink) if fill is not None: if isinstance(fill, str): fill = ImageColor.getcolor(fill, self.mode) - if self.palette and not isinstance(fill, numbers.Number): + if self.palette and isinstance(fill, tuple): fill = self.palette.getcolor(fill, self._image) result_fill = self.draw.draw_ink(fill) return result_ink, result_fill diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 2a8846c1a..babf453cb 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -475,6 +475,7 @@ class Parser: d, e, o, a = im.tile[0] im.tile = [] self.decoder = Image._getdecoder(im.mode, d, a, im.decoderconfig) + assert im.im is not None self.decoder.setimage(im.im, e) # calculate decoder offset @@ -566,6 +567,7 @@ def _encode_tile( fp.seek(offset) encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig) try: + assert im.im is not None encoder.setimage(im.im, extents) if encoder.pushes_fd: encoder.setfd(fp) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 2ab65bfef..2d71b4df4 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -142,6 +142,7 @@ class ImageFont: raise TypeError(msg) image.load() + assert image.im is not None self.font = Image.core.font(image.im, data) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 191cc2a5f..c8f0406ed 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -65,6 +65,8 @@ class _Operand: except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e + assert out.im is not None + assert im_1.im is not None _imagingmath.unop(op, out.im.id, im_1.im.id) else: # binary operation @@ -93,6 +95,9 @@ class _Operand: except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e + assert out.im is not None + assert im_1.im is not None + assert im_2.im is not None _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) return _Operand(out) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 6a43983d3..cd4fa3cfe 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -213,6 +213,8 @@ class MorphOp: msg = "Image mode must be L" raise ValueError(msg) outimage = Image.new(image.mode, image.size, None) + assert image.im is not None + assert outimage.im is not None count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage @@ -229,6 +231,7 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) + assert image.im is not None return _imagingmorph.match(bytes(self.lut), image.im.id) def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: @@ -240,6 +243,7 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) + assert image.im is not None return _imagingmorph.get_on_pixels(image.im.id) def load_lut(self, filename: str) -> None: diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 6b13e57a0..6344220ef 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -43,6 +43,7 @@ def _pilbitmap_check() -> int: if _pilbitmap_ok is None: try: im = Image.new("1", (1, 1)) + assert im.im is not None tkinter.BitmapImage(data=f"PIL:{im.im.id}") _pilbitmap_ok = 1 except tkinter.TclError: @@ -127,10 +128,7 @@ class PhotoImage: # palette mapped data image.apply_transparency() image.load() - try: - mode = image.palette.mode - except AttributeError: - mode = "RGB" # default + mode = image.palette.mode if image.palette else "RGB" size = image.size kw["width"], kw["height"] = size @@ -190,6 +188,7 @@ class PhotoImage: # convert to blittable im.load() image = im.im + assert image is not None if image.isblock() and im.mode == self.__mode: block = image else: @@ -231,6 +230,7 @@ class BitmapImage: if _pilbitmap_check(): # fast way (requires the pilbitmap booster patch) image.load() + assert image.im is not None kw["data"] = f"PIL:{image.im.id}" self.__im = image # must keep a reference else: @@ -277,6 +277,7 @@ def getimage(photo: PhotoImage) -> Image.Image: """Copies the contents of a PhotoImage to a PIL image memory.""" im = Image.new("RGBA", (photo.width(), photo.height())) block = im.im + assert block is not None _pyimagingtkcall("PyImagingPhotoGet", photo, block.id) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 17243e705..03712d24c 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -199,9 +199,13 @@ def getiptcinfo( data = None + info: dict[tuple[int, int], bytes | list[bytes]] = {} if isinstance(im, IptcImageFile): # return info dictionary right away - return im.info + for k, v in im.info.items(): + if isinstance(k, tuple): + info[k] = v + return info elif isinstance(im, JpegImagePlugin.JpegImageFile): # extract the IPTC/NAA resource @@ -237,4 +241,7 @@ def getiptcinfo( except (IndexError, KeyError): pass # expected failure - return iptc_im.info + for k, v in iptc_im.info.items(): + if isinstance(k, tuple): + info[k] = v + return info diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index fc897e2b9..04fe8ba85 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -473,6 +473,7 @@ class JpegImageFile(ImageFile.ImageFile): except OSError: pass + assert self.im is not None self._mode = self.im.mode self._size = self.im.size diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 1735070f8..9ca93c531 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -173,6 +173,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: flags = 0 if im.mode == "P" and "custom-colormap" in im.info: + assert im.palette is not None flags = flags & _FLAGS["custom-colormap"] colormapsize = 4 * 256 + 2 colormapmode = im.palette.mode diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 1cd5c4a9d..64898e235 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -55,6 +55,7 @@ class PcdImageFile(ImageFile.ImageFile): assert self.im is not None self.im = self.im.rotate(self.tile_post_rotate) + assert self.im is not None self._size = self.im.size diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 910fa9755..59b5d0a8b 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -863,6 +863,7 @@ class PngImageFile(ImageFile.ImageFile): assert self.png is not None self.dispose: _imaging.ImagingCore | None + dispose_extent = None if frame == 0: if rewind: self._fp.seek(self.__rewind) @@ -877,7 +878,7 @@ class PngImageFile(ImageFile.ImageFile): self.default_image = self.info.get("default_image", False) self.dispose_op = self.info.get("disposal") self.blend_op = self.info.get("blend") - self.dispose_extent = self.info.get("bbox") + dispose_extent = self.info.get("bbox") self.__frame = 0 else: if frame != self.__frame + 1: @@ -886,6 +887,7 @@ class PngImageFile(ImageFile.ImageFile): # ensure previous frame was loaded self.load() + assert self.im is not None if self.dispose: self.im.paste(self.dispose, self.dispose_extent) @@ -935,11 +937,13 @@ class PngImageFile(ImageFile.ImageFile): self.tile = self.png.im_tile self.dispose_op = self.info.get("disposal") self.blend_op = self.info.get("blend") - self.dispose_extent = self.info.get("bbox") + dispose_extent = self.info.get("bbox") if not self.tile: msg = "image not found in APNG frame" raise EOFError(msg) + if dispose_extent: + self.dispose_extent: tuple[float, float, float, float] = dispose_extent # setup frame disposal (actual disposal done when needed in the next _seek()) if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: @@ -1046,6 +1050,7 @@ class PngImageFile(ImageFile.ImageFile): self.png = None else: if self._prev_im and self.blend_op == Blend.OP_OVER: + assert self.im is not None updated = self._crop(self.im, self.dispose_extent) if self.im.mode == "RGB" and "transparency" in self.info: mask = updated.convert_transparent( @@ -1400,6 +1405,7 @@ def _save( chunk(fp, cid, data) if im.mode == "P": + assert im.im is not None palette_byte_number = colors * 3 palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] while len(palette_bytes) < palette_byte_number: @@ -1430,8 +1436,9 @@ def _save( # and it's in the info dict. It's probably just stale. msg = "cannot use transparency for this mode" raise OSError(msg) - else: - if im.mode == "P" and im.im.getpalettemode() == "RGBA": + elif im.mode == "P": + assert im.im is not None + if im.im.getpalettemode() == "RGBA": alpha = im.im.getpalette("RGBA", "A") alpha_bytes = colors chunk(fp, b"tRNS", alpha[:alpha_bytes]) diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 202ef52d0..c81d381fb 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -26,7 +26,7 @@ class QoiImageFile(ImageFile.ImageFile): msg = "not a QOI file" raise SyntaxError(msg) - self._size = tuple(i32(self.fp.read(4)) for i in range(2)) + self._size = i32(self.fp.read(4)), i32(self.fp.read(4)) channels = self.fp.read(1)[0] self._mode = "RGB" if channels == 3 else "RGBA" diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index d9d1bab5a..2b53c7dfa 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1313,6 +1313,7 @@ class TiffImageFile(ImageFile.ImageFile): decoder = Image._getdecoder( self.mode, "libtiff", tuple(args), self.decoderconfig ) + assert self.im is not None try: decoder.setimage(self.im, extents) except ValueError as e: From 3e93b806cc68aedb887fa9071cfe35a2144cd960 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Aug 2024 02:29:19 +0000 Subject: [PATCH 032/652] Update dependency mypy to v1.11.1 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 776bb0dbb..ebb80e03b 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1 +1 @@ -mypy==1.11.0 +mypy==1.11.1 From 69076fa3d9c04e00ab20cc7ba13ab046547b5138 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 4 Aug 2024 06:21:13 +0000 Subject: [PATCH 033/652] Update dependency cibuildwheel to v2.20.0 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index a2bf2a7b0..b8e6c3947 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.19.2 +cibuildwheel==2.20.0 From 2722cfd72b6b458f709d71c2ef06e9949cf3bbe2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Aug 2024 20:20:24 +1000 Subject: [PATCH 034/652] Added writing XMP bytes to JPEG --- Tests/test_file_jpeg.py | 7 +++++++ src/PIL/JpegImagePlugin.py | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index df589e24a..8feae2edd 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -991,6 +991,13 @@ class TestFileJpeg: else: assert im.getxmp() == {"xmpmeta": None} + def test_save_xmp(self, tmp_path: Path) -> None: + f = str(tmp_path / "temp.jpg") + hopper().save(f, xmp=b"XMP test") + + with Image.open(f) as reloaded: + assert reloaded.info["xmp"] == b"XMP test" + @pytest.mark.timeout(timeout=1) def test_eof(self) -> None: # Even though this decoder never says that it is finished diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index fc897e2b9..f15b62f0f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -746,6 +746,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: extra = info.get("extra", b"") + xmp = info.get("xmp") + if xmp: + size = o16(31 + len(xmp)) + extra += b"\xFF\xE1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp + MAX_BYTES_IN_MARKER = 65533 icc_profile = info.get("icc_profile") if icc_profile: From d49884e40c204db7ce89b621823b2ac37ad51a10 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Aug 2024 21:48:36 +1000 Subject: [PATCH 035/652] Raise ValueError is XMP data is too long --- Tests/test_file_jpeg.py | 8 +++++++- src/PIL/JpegImagePlugin.py | 19 ++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 8feae2edd..f9c5fbc0b 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -993,11 +993,17 @@ class TestFileJpeg: def test_save_xmp(self, tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") - hopper().save(f, xmp=b"XMP test") + im = hopper() + im.save(f, xmp=b"XMP test") with Image.open(f) as reloaded: assert reloaded.info["xmp"] == b"XMP test" + im.save(f, xmp=b"1" * 65504) + + with pytest.raises(ValueError): + im.save(f, xmp=b"1" * 65505) + @pytest.mark.timeout(timeout=1) def test_eof(self) -> None: # Even though this decoder never says that it is finished diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index f15b62f0f..75c0ae384 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -746,23 +746,28 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: extra = info.get("extra", b"") + MAX_BYTES_IN_MARKER = 65533 xmp = info.get("xmp") if xmp: - size = o16(31 + len(xmp)) + overhead_len = 29 + max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len + if len(xmp) > max_data_bytes_in_marker: + msg = "XMP data is too long" + raise ValueError(msg) + size = o16(2 + overhead_len + len(xmp)) extra += b"\xFF\xE1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp - MAX_BYTES_IN_MARKER = 65533 icc_profile = info.get("icc_profile") if icc_profile: - ICC_OVERHEAD_LEN = 14 - MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN + overhead_len = 14 + max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len markers = [] while icc_profile: - markers.append(icc_profile[:MAX_DATA_BYTES_IN_MARKER]) - icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:] + markers.append(icc_profile[:max_data_bytes_in_marker]) + icc_profile = icc_profile[max_data_bytes_in_marker:] i = 1 for marker in markers: - size = o16(2 + ICC_OVERHEAD_LEN + len(marker)) + size = o16(2 + overhead_len + len(marker)) extra += ( b"\xFF\xE2" + size From 554f8bb23a9a35fbfd638b7677f1af12043ce9d0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:37:42 +0000 Subject: [PATCH 036/652] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.5.6) - [github.com/python-jsonschema/check-jsonschema: 0.28.6 → 0.29.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.6...0.29.1) - [github.com/tox-dev/pyproject-fmt: 2.1.3 → 2.2.1](https://github.com/tox-dev/pyproject-fmt/compare/2.1.3...2.2.1) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 659409de4..74764752e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.0 + rev: v0.5.6 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -50,7 +50,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.6 + rev: 0.29.1 hooks: - id: check-github-workflows - id: check-readthedocs @@ -62,7 +62,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.1.3 + rev: 2.2.1 hooks: - id: pyproject-fmt From c056406f21e2a5aa87a8e4560fc33e079bed59e8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Aug 2024 06:19:15 +1000 Subject: [PATCH 037/652] Added comments to explain overhead_len --- src/PIL/JpegImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 75c0ae384..0cb50e0bf 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -749,7 +749,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: MAX_BYTES_IN_MARKER = 65533 xmp = info.get("xmp") if xmp: - overhead_len = 29 + overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len if len(xmp) > max_data_bytes_in_marker: msg = "XMP data is too long" @@ -759,7 +759,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: icc_profile = info.get("icc_profile") if icc_profile: - overhead_len = 14 + overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len markers = [] while icc_profile: From 1fd4d1478226e0b745b42ed33a9ade993cd393df Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Aug 2024 06:31:47 +1000 Subject: [PATCH 038/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bd8d3af03..ae995e0a0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,18 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Remove all WITH_* flags from _imaging.c and other flags #8211 + [homm] + +- Improve ImageDraw2 shape methods #8265 + [radarhere] + +- Lock around usages of imaging memory arenas #8238 + [lysnikolaou] + +- Deprecate JpegImageFile huffman_ac and huffman_dc #8274 + [radarhere] + - Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242 [radarhere] From 13c00bab86df263a83874304972951bd847439ac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Aug 2024 11:16:55 +1000 Subject: [PATCH 039/652] Updated libimagequant to 4.3.3 --- depends/install_imagequant.sh | 2 +- docs/installation/building-from-source.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 9dd7742ed..867b31e4d 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -2,7 +2,7 @@ # install libimagequant archive_name=libimagequant -archive_version=4.3.1 +archive_version=4.3.3 archive=$archive_name-$archive_version diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 7f7dfa6ff..fc02bb382 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -68,7 +68,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.3.1** + * Pillow has been tested with libimagequant **2.6-4.3.3** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From 5e8dbbbf9e22b7615157172fa11bc7f7bf86f4c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Aug 2024 13:32:46 +1000 Subject: [PATCH 040/652] Removed unused variable --- src/libImaging/Quant.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 595ee42a0..55b5e7a55 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -308,11 +308,10 @@ mergesort_pixels(PixelList *head, int i) { #ifdef DEBUG static int test_sorted(PixelList *pl[3]) { - int i, n, l; + int i, l; PixelList *t; for (i = 0; i < 3; i++) { - n = 0; l = 256; for (t = pl[i]; t; t = t->next[i]) { if (l < t->p.a.v[i]) From 9d6d16d5cc7bcf6070f5ad002de8642cbb2a17ab Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 6 Aug 2024 15:19:40 +0300 Subject: [PATCH 041/652] Parametrize some color_lut tests for DRYness --- Tests/test_color_lut.py | 133 +++++++++++++++++----------------------- 1 file changed, 55 insertions(+), 78 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 0d9c0b419..152a7a1e6 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -105,91 +105,68 @@ class TestColorLut3DCoreAPI: with pytest.raises(TypeError): im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) - def test_correct_args(self) -> None: + @pytest.mark.parametrize( + ("lut_mode", "table_size"), + [ + ("RGB", (3, 3)), + ("CMYK", (4, 3)), + ("RGB", (3, (2, 3, 3))), + ("RGB", (3, (65, 3, 3))), + ("RGB", (3, (3, 65, 3))), + ("RGB", (3, (3, 3, 65))), + ], + ) + def test_correct_args( + self, lut_mode: str, table_size: tuple[int, int | tuple[int, int, int]] + ) -> None: im = Image.new("RGB", (10, 10), 0) - + assert im.im is not None im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - im.im.color_lut_3d( - "CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) - ) - - im.im.color_lut_3d( - "RGB", + lut_mode, Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (2, 3, 3)), + *self.generate_identity_table(*table_size), ) + @pytest.mark.parametrize( + ("image_mode", "lut_mode", "table_size"), + [ + ("L", "RGB", (3, 3)), + ("RGB", "L", (3, 3)), + ("L", "L", (3, 3)), + ("RGB", "RGBA", (3, 3)), + ("RGB", "RGB", (4, 3)), + ], + ) + def test_wrong_mode( + self, image_mode: str, lut_mode: str, table_size: tuple[int, int] + ) -> None: + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new(image_mode, (10, 10), 0) + assert im.im is not None + im.im.color_lut_3d( + lut_mode, + Image.Resampling.BILINEAR, + *self.generate_identity_table(*table_size), + ) + + @pytest.mark.parametrize( + ("image_mode", "lut_mode", "table_size"), + [ + ("RGBA", "RGB", (3, 3)), + ("RGBA", "RGBA", (4, 3)), + ("RGB", "HSV", (3, 3)), + ("RGB", "RGBA", (4, 3)), + ], + ) + def test_correct_mode( + self, image_mode: str, lut_mode: str, table_size: tuple[int, int] + ) -> None: + im = Image.new(image_mode, (10, 10), 0) + assert im.im is not None im.im.color_lut_3d( - "RGB", + lut_mode, Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (65, 3, 3)), - ) - - im.im.color_lut_3d( - "RGB", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (3, 65, 3)), - ) - - im.im.color_lut_3d( - "RGB", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (3, 3, 65)), - ) - - def test_wrong_mode(self) -> None: - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("L", (10, 10), 0) - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("L", (10, 10), 0) - im.im.color_lut_3d( - "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) - ) - - def test_correct_mode(self) -> None: - im = Image.new("RGBA", (10, 10), 0) - im.im.color_lut_3d( - "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - im = Image.new("RGBA", (10, 10), 0) - im.im.color_lut_3d( - "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) - ) - - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + *self.generate_identity_table(*table_size), ) def test_identities(self) -> None: From 70298d3be948e4245e2f24481b33d8d877c45e1d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Aug 2024 06:38:46 +1000 Subject: [PATCH 042/652] Fix undefined variable --- src/libImaging/Quant.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 55b5e7a55..a489a882d 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -314,8 +314,9 @@ test_sorted(PixelList *pl[3]) { for (i = 0; i < 3; i++) { l = 256; for (t = pl[i]; t; t = t->next[i]) { - if (l < t->p.a.v[i]) + if (l < t->p.a.v[i]) { return 0; + } l = t->p.a.v[i]; } } @@ -1009,7 +1010,8 @@ compute_palette_from_median_cut( uint32_t nPixels, HashTable *medianBoxHash, Pixel **palette, - uint32_t nPaletteEntries + uint32_t nPaletteEntries, + BoxNode *root ) { uint32_t i; uint32_t paletteEntry; @@ -1382,7 +1384,9 @@ quantize( fflush(stdout); timer = clock(); #endif - if (!compute_palette_from_median_cut(pixelData, nPixels, h, &p, nPaletteEntries)) { + if (!compute_palette_from_median_cut( + pixelData, nPixels, h, &p, nPaletteEntries, root + )) { goto error_3; } #ifdef DEBUG From 59c69f8d71d7f3e43cb09bd6bd2271c6e332c7ba Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 5 Aug 2024 19:12:12 +0300 Subject: [PATCH 043/652] Move auxiliary mypy requirements to a requirements file For easier installation outside tox --- .ci/requirements-mypy.txt | 9 +++++++++ tox.ini | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index ebb80e03b..23792281b 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1 +1,10 @@ mypy==1.11.1 +IceSpringPySideStubs-PyQt6 +IceSpringPySideStubs-PySide6 +ipython +numpy +packaging +pytest +types-defusedxml +types-olefile +types-setuptools diff --git a/tox.ini b/tox.ini index c1bc3b17d..4b4059455 100644 --- a/tox.ini +++ b/tox.ini @@ -33,15 +33,6 @@ commands = skip_install = true deps = -r .ci/requirements-mypy.txt - IceSpringPySideStubs-PyQt6 - IceSpringPySideStubs-PySide6 - ipython - numpy - packaging - pytest - types-defusedxml - types-olefile - types-setuptools extras = typing commands = From 7581b48706cb3ce8af112305bae22af705134608 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Aug 2024 18:44:43 +1000 Subject: [PATCH 044/652] Fixed sign comparison warning --- src/_imagingft.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index da03e3ba9..f8143e0cc 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1243,7 +1243,7 @@ font_getvarnames(FontObject *self) { return PyErr_NoMemory(); } - for (int i = 0; i < num_namedstyles; i++) { + for (unsigned int i = 0; i < num_namedstyles; i++) { list_names_filled[i] = 0; } From 4fddc625f177d8b7cd2120fa2c7880f664e25e67 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Aug 2024 18:26:24 +1000 Subject: [PATCH 045/652] Corrected lut mode --- Tests/test_color_lut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 152a7a1e6..1543faf01 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -152,7 +152,7 @@ class TestColorLut3DCoreAPI: @pytest.mark.parametrize( ("image_mode", "lut_mode", "table_size"), [ - ("RGBA", "RGB", (3, 3)), + ("RGBA", "RGBA", (3, 3)), ("RGBA", "RGBA", (4, 3)), ("RGB", "HSV", (3, 3)), ("RGB", "RGBA", (4, 3)), From 5c4aeaa3296c748facfafc265e29fbb1c7c8e518 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Aug 2024 18:24:50 +1000 Subject: [PATCH 046/652] Concatenate parameters into single string --- Tests/test_color_lut.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 1543faf01..346ea749a 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -106,7 +106,7 @@ class TestColorLut3DCoreAPI: im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) @pytest.mark.parametrize( - ("lut_mode", "table_size"), + "lut_mode, table_size", [ ("RGB", (3, 3)), ("CMYK", (4, 3)), @@ -128,7 +128,7 @@ class TestColorLut3DCoreAPI: ) @pytest.mark.parametrize( - ("image_mode", "lut_mode", "table_size"), + "image_mode, lut_mode, table_size", [ ("L", "RGB", (3, 3)), ("RGB", "L", (3, 3)), @@ -150,7 +150,7 @@ class TestColorLut3DCoreAPI: ) @pytest.mark.parametrize( - ("image_mode", "lut_mode", "table_size"), + "image_mode, lut_mode, table_size", [ ("RGBA", "RGBA", (3, 3)), ("RGBA", "RGBA", (4, 3)), From a06529a3a89d3e57e584b85c25fa24cf075f4dcd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Aug 2024 18:28:51 +1000 Subject: [PATCH 047/652] Added channels parameter --- Tests/test_color_lut.py | 48 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 346ea749a..084cb19e1 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -106,39 +106,39 @@ class TestColorLut3DCoreAPI: im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) @pytest.mark.parametrize( - "lut_mode, table_size", + "lut_mode, table_channels, table_size", [ - ("RGB", (3, 3)), - ("CMYK", (4, 3)), - ("RGB", (3, (2, 3, 3))), - ("RGB", (3, (65, 3, 3))), - ("RGB", (3, (3, 65, 3))), - ("RGB", (3, (3, 3, 65))), + ("RGB", 3, 3), + ("CMYK", 4, 3), + ("RGB", 3, (2, 3, 3)), + ("RGB", 3, (65, 3, 3)), + ("RGB", 3, (3, 65, 3)), + ("RGB", 3, (2, 3, 65)), ], ) def test_correct_args( - self, lut_mode: str, table_size: tuple[int, int | tuple[int, int, int]] + self, lut_mode: str, table_channels: int, table_size: int | tuple[int, int, int] ) -> None: im = Image.new("RGB", (10, 10), 0) assert im.im is not None im.im.color_lut_3d( lut_mode, Image.Resampling.BILINEAR, - *self.generate_identity_table(*table_size), + *self.generate_identity_table(table_channels, table_size), ) @pytest.mark.parametrize( - "image_mode, lut_mode, table_size", + "image_mode, lut_mode, table_channels, table_size", [ - ("L", "RGB", (3, 3)), - ("RGB", "L", (3, 3)), - ("L", "L", (3, 3)), - ("RGB", "RGBA", (3, 3)), - ("RGB", "RGB", (4, 3)), + ("L", "RGB", 3, 3), + ("RGB", "L", 3, 3), + ("L", "L", 3, 3), + ("RGB", "RGBA", 3, 3), + ("RGB", "RGB", 4, 3), ], ) def test_wrong_mode( - self, image_mode: str, lut_mode: str, table_size: tuple[int, int] + self, image_mode: str, lut_mode: str, table_channels: int, table_size: int ) -> None: with pytest.raises(ValueError, match="wrong mode"): im = Image.new(image_mode, (10, 10), 0) @@ -146,27 +146,27 @@ class TestColorLut3DCoreAPI: im.im.color_lut_3d( lut_mode, Image.Resampling.BILINEAR, - *self.generate_identity_table(*table_size), + *self.generate_identity_table(table_channels, table_size), ) @pytest.mark.parametrize( - "image_mode, lut_mode, table_size", + "image_mode, lut_mode, table_channels, table_size", [ - ("RGBA", "RGBA", (3, 3)), - ("RGBA", "RGBA", (4, 3)), - ("RGB", "HSV", (3, 3)), - ("RGB", "RGBA", (4, 3)), + ("RGBA", "RGBA", 3, 3), + ("RGBA", "RGBA", 4, 3), + ("RGB", "HSV", 3, 3), + ("RGB", "RGBA", 4, 3), ], ) def test_correct_mode( - self, image_mode: str, lut_mode: str, table_size: tuple[int, int] + self, image_mode: str, lut_mode: str, table_channels: int, table_size: int ) -> None: im = Image.new(image_mode, (10, 10), 0) assert im.im is not None im.im.color_lut_3d( lut_mode, Image.Resampling.BILINEAR, - *self.generate_identity_table(*table_size), + *self.generate_identity_table(table_channels, table_size), ) def test_identities(self) -> None: From 8ca53b312d7fbac101a688c6864b4eeed9bae168 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 7 Aug 2024 12:17:49 +0300 Subject: [PATCH 048/652] Enforce CSV parametrize names style --- Tests/test_file_dds.py | 6 +++--- Tests/test_file_eps.py | 4 +--- Tests/test_image_access.py | 2 +- Tests/test_pickle.py | 2 +- pyproject.toml | 3 +++ 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index ebc0e89a1..9a826ebe8 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -152,7 +152,7 @@ def test_sanity_ati2_bc5u(image_path: str) -> None: @pytest.mark.parametrize( - ("image_path", "expected_path"), + "image_path, expected_path", ( # hexeditted to be typeless (TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM), @@ -248,7 +248,7 @@ def test_dx10_r8g8b8a8_unorm_srgb() -> None: @pytest.mark.parametrize( - ("mode", "size", "test_file"), + "mode, size, test_file", [ ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), @@ -373,7 +373,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None: @pytest.mark.parametrize( - ("mode", "test_file"), + "mode, test_file", [ ("L", "Tests/images/linear_gradient.png"), ("LA", "Tests/images/uncompressed_la.png"), diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index b54238132..d54deb515 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -80,9 +80,7 @@ simple_eps_file_with_long_binary_data = ( @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -@pytest.mark.parametrize( - ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) -) +@pytest.mark.parametrize("filename, size", ((FILE1, (460, 352)), (FILE2, (360, 252)))) @pytest.mark.parametrize("scale", (1, 2)) def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: expected_size = tuple(s * scale for s in size) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 854c79dae..bb30b462d 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -230,7 +230,7 @@ class TestImagePutPixelError: im.putpixel((0, 0), v) # type: ignore[arg-type] @pytest.mark.parametrize( - ("mode", "band_numbers", "match"), + "mode, band_numbers, match", ( ("L", (0, 2), "color must be int or single-element tuple"), ("LA", (0, 3), "color must be int, or tuple of one or two elements"), diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index ed415953f..be143e9c6 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -46,7 +46,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non @pytest.mark.parametrize( - ("test_file", "test_mode"), + "test_file, test_mode", [ ("Tests/images/hopper.jpg", None), ("Tests/images/hopper.jpg", "L"), diff --git a/pyproject.toml b/pyproject.toml index 0940664f3..d81f79c5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,6 +109,7 @@ lint.select = [ "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging "PGH", # pygrep-hooks + "PT006", # pytest-parametrize-names-wrong-type "PYI", # flake8-pyi "RUF100", # unused noqa (yesqa) "UP", # pyupgrade @@ -129,6 +130,8 @@ lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [ lint.per-file-ignores."Tests/oss-fuzz/fuzz_pillow.py" = [ "I002", ] +lint.flake8-pytest-style.parametrize-names-type = "csv" + lint.isort.known-first-party = [ "PIL", ] From 1c998d7f7c5378ae2ed3a029ef7c4724633050e1 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 8 Aug 2024 14:10:18 +0300 Subject: [PATCH 049/652] Update pyproject.toml Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d81f79c5f..8bb21019c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,7 +131,6 @@ lint.per-file-ignores."Tests/oss-fuzz/fuzz_pillow.py" = [ "I002", ] lint.flake8-pytest-style.parametrize-names-type = "csv" - lint.isort.known-first-party = [ "PIL", ] From e938283eb9ff28fbd18432eae3aa5b0ec79967d3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Aug 2024 20:56:14 +1000 Subject: [PATCH 050/652] Allow docs to build without lcms2 --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 41f9c0e38..75e4c2a20 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -121,7 +121,7 @@ nitpicky = True # generating warnings in “nitpicky mode”. Note that type should include the domain name # if present. Example entries would be ('py:func', 'int') or # ('envvar', 'LD_LIBRARY_PATH'). -# nitpick_ignore = [] +nitpick_ignore = [("py:class", "_CmsProfileCompatible")] # -- Options for HTML output ---------------------------------------------- From 4d0e8e74b4998422141e77525d9cfe30ccb7dc2e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Aug 2024 10:20:08 +1000 Subject: [PATCH 051/652] Use im property --- Tests/test_box_blur.py | 1 - Tests/test_color_lut.py | 9 ------ Tests/test_image.py | 1 - Tests/test_image_array.py | 1 - Tests/test_image_filter.py | 4 +-- Tests/test_image_getim.py | 1 - Tests/test_image_resample.py | 1 - Tests/test_image_resize.py | 5 --- Tests/test_imagemorph.py | 2 -- Tests/test_imageops_usm.py | 2 -- Tests/test_lib_image.py | 3 -- selftest.py | 2 +- src/PIL/BmpImagePlugin.py | 1 - src/PIL/EpsImagePlugin.py | 1 - src/PIL/GbrImagePlugin.py | 2 +- src/PIL/GifImagePlugin.py | 11 ++----- src/PIL/IcnsImagePlugin.py | 3 +- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/ImImagePlugin.py | 1 - src/PIL/Image.py | 63 ++++++++++-------------------------- src/PIL/ImageChops.py | 17 ---------- src/PIL/ImageCms.py | 3 -- src/PIL/ImageFile.py | 4 +-- src/PIL/ImageFont.py | 1 - src/PIL/ImageMath.py | 5 --- src/PIL/ImageMorph.py | 4 --- src/PIL/ImageTk.py | 4 --- src/PIL/JpegImagePlugin.py | 1 - src/PIL/PcdImagePlugin.py | 3 -- src/PIL/PcxImagePlugin.py | 2 -- src/PIL/PngImagePlugin.py | 10 ++---- src/PIL/TgaImagePlugin.py | 2 -- src/PIL/TiffImagePlugin.py | 1 - src/PIL/WalImageFile.py | 2 +- 34 files changed, 30 insertions(+), 145 deletions(-) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index dd5fe0290..1f6ed6127 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -24,7 +24,6 @@ def test_imageops_box_blur() -> None: def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image: - assert image.im is not None return image._new(image.im.box_blur((radius, radius), n)) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 4b083d689..36ab187f2 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -47,7 +47,6 @@ class TestColorLut3DCoreAPI: def test_wrong_args(self) -> None: im = Image.new("RGB", (10, 10), 0) - assert im.im is not None with pytest.raises(ValueError, match="filter"): im.im.color_lut_3d( @@ -121,7 +120,6 @@ class TestColorLut3DCoreAPI: self, lut_mode: str, table_channels: int, table_size: int | tuple[int, int, int] ) -> None: im = Image.new("RGB", (10, 10), 0) - assert im.im is not None im.im.color_lut_3d( lut_mode, Image.Resampling.BILINEAR, @@ -143,7 +141,6 @@ class TestColorLut3DCoreAPI: ) -> None: with pytest.raises(ValueError, match="wrong mode"): im = Image.new(image_mode, (10, 10), 0) - assert im.im is not None im.im.color_lut_3d( lut_mode, Image.Resampling.BILINEAR, @@ -163,7 +160,6 @@ class TestColorLut3DCoreAPI: self, image_mode: str, lut_mode: str, table_channels: int, table_size: int ) -> None: im = Image.new(image_mode, (10, 10), 0) - assert im.im is not None im.im.color_lut_3d( lut_mode, Image.Resampling.BILINEAR, @@ -180,7 +176,6 @@ class TestColorLut3DCoreAPI: g.transpose(Image.Transpose.ROTATE_180), ], ) - assert im.im is not None # Fast test with small cubes for size in [2, 3, 5, 7, 11, 16, 17]: @@ -217,7 +212,6 @@ class TestColorLut3DCoreAPI: g.transpose(Image.Transpose.ROTATE_180), ], ) - assert im.im is not None # Red channel copied to alpha assert_image_equal( @@ -242,7 +236,6 @@ class TestColorLut3DCoreAPI: g.transpose(Image.Transpose.ROTATE_270), ], ) - assert im.im is not None assert_image_equal( im, @@ -265,7 +258,6 @@ class TestColorLut3DCoreAPI: g.transpose(Image.Transpose.ROTATE_180), ], ) - assert im.im is not None # Reverse channels by splitting and using table # fmt: off @@ -291,7 +283,6 @@ class TestColorLut3DCoreAPI: g.transpose(Image.Transpose.ROTATE_180), ], ) - assert im.im is not None # fmt: off transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, diff --git a/Tests/test_image.py b/Tests/test_image.py index d4ab8ad38..12b6c1fdd 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -695,7 +695,6 @@ class TestImage: image: Image.Image, palette_result: ImagePalette.ImagePalette | None = None, ) -> None: - assert image.im is not None new_image = base_image._new(image.im) assert new_image.mode == image.mode assert new_image.size == image.size diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 4d8ffbf4a..2b9ccd04e 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -114,5 +114,4 @@ def test_fromarray_palette() -> None: # Assert that the Python and C palettes match assert out.palette is not None - assert out.im is not None assert len(out.palette.colors) == len(out.im.getpalette()) / 3 diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index ea5438f3d..412ab44c3 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -137,9 +137,7 @@ def test_builtinfilter_p() -> None: builtin_filter = ImageFilter.BuiltinFilter() with pytest.raises(ValueError): - im = hopper("P").im - assert im is not None - builtin_filter.filter(im) + builtin_filter.filter(hopper("P").im) def test_kernel_not_enough_coefficients() -> None: diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index e62d6e0ec..9afa02b0a 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -8,5 +8,4 @@ def test_sanity() -> None: type_repr = repr(type(im.getim())) assert "PyCapsule" in type_repr - assert im.im is not None assert isinstance(im.im.id, int) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 4a5901d47..ce6209c0d 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -27,7 +27,6 @@ class TestImagingResampleVulnerability: ): with pytest.raises(MemoryError): # any resampling filter will do here - assert im.im is not None im.im.resize((xsize, ysize), Image.Resampling.BILINEAR) def test_invalid_size(self) -> None: diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 5700b3e06..c9e304512 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -27,7 +27,6 @@ class TestImagingCoreResize: ) -> Image.Image: # Image class independent version of resize. im.load() - assert im.im is not None return im._new(im.im.resize(size, f)) @pytest.mark.parametrize( @@ -38,8 +37,6 @@ class TestImagingCoreResize: r = self.resize(im, (15, 12), Image.Resampling.NEAREST) assert r.mode == mode assert r.size == (15, 12) - assert r.im is not None - assert im.im is not None assert r.im.bands == im.im.bands def test_convolution_modes(self) -> None: @@ -54,8 +51,6 @@ class TestImagingCoreResize: r = self.resize(im, (15, 12), Image.Resampling.BILINEAR) assert r.mode == mode assert r.size == (15, 12) - assert r.im is not None - assert im.im is not None assert r.im.bands == im.im.bands @pytest.mark.parametrize( diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index b40de7f98..4363f456e 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -325,8 +325,6 @@ def test_wrong_mode() -> None: lut = ImageMorph.LutBuilder(op_name="corner").build_lut() imrgb = Image.new("RGB", (10, 10)) iml = Image.new("L", (10, 10)) - assert imrgb.im is not None - assert iml.im is not None with pytest.raises(RuntimeError): _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 64116bf70..920012d86 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -98,12 +98,10 @@ def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None: (4, 3, 2), (4, 2, 2), ]: - assert i.im is not None assert i.im.getpixel((x, y))[c] >= 250 # Fuzzy match. def gp(x: int, y: int) -> tuple[int, ...]: - assert i.im is not None return i.im.getpixel((x, y)) assert 236 <= gp(7, 4)[0] <= 239 diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 0a36af8b6..31548bbc9 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -7,21 +7,18 @@ from PIL import Image def test_setmode() -> None: im = Image.new("L", (1, 1), 255) - assert im.im is not None im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 im.im.setmode("L") assert im.im.getpixel((0, 0)) == 255 im = Image.new("1", (1, 1), 1) - assert im.im is not None im.im.setmode("L") assert im.im.getpixel((0, 0)) == 255 im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 im = Image.new("RGB", (1, 1), (1, 2, 3)) - assert im.im is not None im.im.setmode("RGB") assert im.im.getpixel((0, 0)) == (1, 2, 3) im.im.setmode("RGBA") diff --git a/selftest.py b/selftest.py index 9e049367e..e9b5689a0 100755 --- a/selftest.py +++ b/selftest.py @@ -52,7 +52,7 @@ def testimage() -> None: or you call the "load" method: >>> im = Image.open("Tests/images/hopper.ppm") - >>> print(im.im) # internal image attribute + >>> print(im._im) # internal image attribute None >>> a = im.load() >>> type(im.im) # doctest: +ELLIPSIS diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index ce2ee464a..da9f2eba5 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -445,7 +445,6 @@ def _save( elif im.mode == "L": palette = b"".join(o8(i) * 4 for i in range(256)) elif im.mode == "P": - assert im.im is not None palette = im.im.getpalette("RGB", "BGRX") colors = len(palette) // 4 else: diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index c5ca8dadc..28aa9689d 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -161,7 +161,6 @@ def Ghostscript( except OSError: pass - assert out_im.im is not None im = out_im.im.copy() out_im.close() return im diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 3c8feea5f..f319d7e84 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -89,7 +89,7 @@ class GbrImageFile(ImageFile.ImageFile): self._data_size = width * height * color_depth def load(self) -> Image.core.PixelAccess | None: - if not self.im: + if self._im is None: self.im = Image.core.new(self.mode, self.size) self.frombytes(self.fp.read(self._data_size)) return Image.Image.load(self) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 55f927d40..74aebcb1f 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -155,7 +155,7 @@ class GifImageFile(ImageFile.ImageFile): if not self._seek_check(frame): return if frame < self.__frame: - self.im = None + self._im = None self._seek(0) last_frame = self.__frame @@ -307,7 +307,6 @@ class GifImageFile(ImageFile.ImageFile): self.tile = [] if self.dispose: - assert self.im is not None self.im.paste(self.dispose, self.dispose_extent) self._frame_palette = palette if palette is not None else self.global_palette @@ -335,7 +334,6 @@ class GifImageFile(ImageFile.ImageFile): LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY or palette ): - assert self.im is not None if "transparency" in self.info: self.im.putpalettealpha(self.info["transparency"], 0) self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) @@ -381,7 +379,7 @@ class GifImageFile(ImageFile.ImageFile): self.dispose = Image.core.fill(dispose_mode, dispose_size, color) else: # replace with previous contents - if self.im is not None: + if self._im is not None: # only dispose the extent in this frame self.dispose = self._crop(self.im, self.dispose_extent) elif frame_transparency is not None: @@ -437,17 +435,15 @@ class GifImageFile(ImageFile.ImageFile): self._prev_im = self.im if self._frame_palette: self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) - assert self.im is not None self.im.putpalette("RGB", *self._frame_palette.getdata()) else: - self.im = None + self._im = None self._mode = temp_mode self._frame_palette = None super().load_prepare() def load_end(self) -> None: - assert self.im is not None if self.__frame == 0: if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: if self._frame_transparency is not None: @@ -539,7 +535,6 @@ def _normalize_palette( if im.mode == "P": if not source_palette: - assert im.im is not None source_palette = im.im.getpalette("RGB")[:768] else: # L-mode if not source_palette: diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 58d4a90a0..ca66aa0fd 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -92,7 +92,6 @@ def read_32( msg = f"Error reading channel [{repr(bytesleft)} left]" raise SyntaxError(msg) band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) - assert im.im is not None im.im.putband(band.im, band_ix) return {"RGB": im} @@ -309,7 +308,7 @@ class IcnsImageFile(ImageFile.ImageFile): ) px = Image.Image.load(self) - if self.im is not None and self.im.size == self.size: + if self._im is not None and self.im.size == self.size: # Already loaded return px self.load_prepare() diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index c891024f5..4613379b6 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -328,7 +328,7 @@ class IcoImageFile(ImageFile.ImageFile): self._size = value def load(self) -> Image.core.PixelAccess | None: - if self.im is not None and self.im.size == self.size: + if self._im is not None and self.im.size == self.size: # Already loaded return Image.Image.load(self) im = self.ico.getimage(self.size) diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 9d60011be..2fb7ecd52 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -353,7 +353,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"Lut: 1\r\n") fp.write(b"\000" * (511 - fp.tell()) + b"\032") if im.mode in ["P", "PA"]: - assert im.im is not None im_palette = im.im.getpalette("RGB", "RGB;L") colors = len(im_palette) // 3 palette = b"" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7532a49a2..5ec416081 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -535,7 +535,7 @@ class Image: def __init__(self) -> None: # FIXME: take "new" parameters / other image? # FIXME: turn mode and size into delegating properties? - self.im: core.ImagingCore | None = None + self._im: core.ImagingCore | DeferredError | None = None self._mode = "" self._size = (0, 0) self.palette: ImagePalette.ImagePalette | None = None @@ -543,6 +543,17 @@ class Image: self.readonly = 0 self._exif: Exif | None = None + @property + def im(self) -> core.ImagingCore: + if isinstance(self._im, DeferredError): + raise self._im.ex + assert self._im is not None + return self._im + + @im.setter + def im(self, im: core.ImagingCore) -> None: + self._im = im + @property def width(self) -> int: return self.size[0] @@ -617,12 +628,11 @@ class Image: # Instead of simply setting to None, we're setting up a # deferred error that will better explain that the core image # object is gone. - self.im = DeferredError(ValueError("Operation on closed image")) # type: ignore[assignment] + self._im = DeferredError(ValueError("Operation on closed image")) def _copy(self) -> None: self.load() - if self.im: - self.im = self.im.copy() + self.im = self.im.copy() self.readonly = 0 def _ensure_mutable(self) -> None: @@ -649,7 +659,6 @@ class Image: self.load() if not format or format == "PPM": - assert self.im is not None self.im.save_ppm(filename) else: self.save(filename, format, **options) @@ -798,7 +807,6 @@ class Image: # unpack data e = _getencoder(self.mode, encoder_name, encoder_args) - assert self.im is not None e.setimage(self.im) bufsize = max(65536, self.size[0] * 4) # see RawEncode.c @@ -865,7 +873,6 @@ class Image: # unpack data d = _getdecoder(self.mode, decoder_name, decoder_args) - assert self.im is not None d.setimage(self.im) s = d.decode(data) @@ -891,7 +898,7 @@ class Image: :returns: An image access object. :rtype: :py:class:`.PixelAccess` """ - if self.im is not None and self.palette and self.palette.dirty: + if self._im is not None and self.palette and self.palette.dirty: # realize palette mode, arr = self.palette.getdata() self.im.putpalette(self.palette.mode, mode, arr) @@ -908,7 +915,7 @@ class Image: self.palette.mode, self.palette.mode ) - if self.im is not None: + if self._im is not None: return self.im.pixel_access(self.readonly) return None @@ -979,7 +986,6 @@ class Image: deprecate(mode, 12) self.load() - assert self.im is not None has_transparency = "transparency" in self.info if not mode and self.mode == "P": @@ -1094,7 +1100,6 @@ class Image: new_im = self._new(im) from . import ImagePalette - assert new_im.im is not None new_im.palette = ImagePalette.ImagePalette( "RGB", new_im.im.getpalette("RGB") ) @@ -1226,7 +1231,6 @@ class Image: ) raise ValueError(msg) - assert self.im is not None if palette: # use palette from reference image palette.load() @@ -1265,7 +1269,6 @@ class Image: :returns: An :py:class:`~PIL.Image.Image` object. """ self.load() - assert self.im is not None return self._new(self.im.copy()) __copy__ = copy @@ -1294,7 +1297,6 @@ class Image: raise ValueError(msg) self.load() - assert self.im is not None return self._new(self._crop(self.im, box)) def _crop( @@ -1348,7 +1350,6 @@ class Image: if ymargin is None: ymargin = xmargin self.load() - assert self.im is not None return self._new(self.im.expand(xmargin, ymargin)) if TYPE_CHECKING: @@ -1373,7 +1374,6 @@ class Image: raise TypeError(msg) multiband = isinstance(filter, ImageFilter.MultibandFilter) - assert self.im is not None if self.im.bands == 1 or multiband: return self._new(filter.filter(self.im)) @@ -1409,7 +1409,6 @@ class Image: """ self.load() - assert self.im is not None return self.im.getbbox(alpha_only) def getcolors( @@ -1429,7 +1428,6 @@ class Image: """ self.load() - assert self.im is not None if self.mode in ("1", "L", "P"): h = self.im.histogram() out: list[tuple[int, float]] = [(h[i], i) for i in range(256) if h[i]] @@ -1457,7 +1455,6 @@ class Image: """ self.load() - assert self.im is not None if band is not None: return self.im.getband(band) return self.im # could be abused @@ -1473,7 +1470,6 @@ class Image: """ self.load() - assert self.im is not None if self.im.bands > 1: return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) return self.im.getextrema() @@ -1612,7 +1608,6 @@ class Image: """ self.load() - assert self.im is not None return self.im.ptr def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: @@ -1629,7 +1624,6 @@ class Image: """ self.load() - assert self.im is not None try: mode = self.im.getpalettemode() except ValueError: @@ -1697,7 +1691,6 @@ class Image: """ self.load() - assert self.im is not None return self.im.getpixel(tuple(xy)) def getprojection(self) -> tuple[list[int], list[int]]: @@ -1709,7 +1702,6 @@ class Image: """ self.load() - assert self.im is not None x, y = self.im.getprojection() return list(x), list(y) @@ -1737,7 +1729,6 @@ class Image: :returns: A list containing pixel counts. """ self.load() - assert self.im is not None if mask: mask.load() return self.im.histogram((0, 0), mask.im) @@ -1766,7 +1757,6 @@ class Image: :returns: A float value representing the image entropy """ self.load() - assert self.im is not None if mask: mask.load() return self.im.entropy((0, 0), mask.im) @@ -1856,7 +1846,6 @@ class Image: if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): # should use an adapter for this! im = im.convert(self.mode) - assert im.im is not None source = im.im elif isinstance(im, tuple): source = im @@ -1864,7 +1853,6 @@ class Image: source = cast(float, im) self._ensure_mutable() - assert self.im is not None if mask: mask.load() @@ -1960,7 +1948,6 @@ class Image: if isinstance(lut, ImagePointHandler): return lut.point(self) - assert self.im is not None if callable(lut): # if it isn't a list, it should be a function if self.mode in ("I", "I;16", "F"): @@ -1997,20 +1984,17 @@ class Image: if self.mode not in ("LA", "PA", "RGBA"): # attempt to promote self to a matching alpha mode - assert self.im is not None try: mode = getmodebase(self.mode) + "A" try: self.im.setmode(mode) except (AttributeError, ValueError) as e: # do things the hard way - assert self.im is not None im = self.im.convert(mode) if im.mode not in ("LA", "PA", "RGBA"): msg = "alpha channel could not be added" raise ValueError(msg) from e # sanity check self.im = im - assert self.im is not None self._mode = self.im.mode except KeyError as e: msg = "illegal image mode" @@ -2021,7 +2005,6 @@ class Image: else: band = 3 - assert self.im is not None if isImageType(alpha): # alpha layer if alpha.mode not in ("1", "L"): @@ -2064,7 +2047,6 @@ class Image: self._ensure_mutable() - assert self.im is not None self.im.putdata(data, scale, offset) def putpalette( @@ -2150,7 +2132,6 @@ class Image: assert self.palette is not None palette_index = self.palette.getcolor(tuple(value), self) value = (palette_index, alpha) if self.mode == "PA" else palette_index - assert self.im is not None return self.im.putpixel(xy, value) def remap_palette( @@ -2177,7 +2158,6 @@ class Image: if source_palette is None: if self.mode == "P": self.load() - assert self.im is not None palette_mode = self.im.getpalettemode() if palette_mode == "RGBA": bands = 4 @@ -2225,7 +2205,6 @@ class Image: # m_im.putpalette(mapping_palette, 'L') # converts to 'P' # or just force it. # UNDONE -- this is part of the general issue with palettes - assert m_im.im is not None m_im.im.putpalette(palette_mode, palette_mode + ";L", m_im.palette.tobytes()) m_im = m_im.convert("L") @@ -2371,7 +2350,6 @@ class Image: (box[3] - reduce_box[1]) / factor_y, ) - assert self.im is not None return self._new(self.im.resize(size, resample, box)) def reduce( @@ -2406,7 +2384,7 @@ class Image: return im.convert(self.mode) self.load() - assert self.im is not None + return self._new(self.im.reduce(factor, box)) def rotate( @@ -2684,7 +2662,6 @@ class Image: """ self.load() - assert self.im is not None if self.im.bands == 1: return (self.copy(),) return tuple(map(self._new, self.im.split())) @@ -2709,7 +2686,6 @@ class Image: msg = f'The image has no channel "{channel}"' raise ValueError(msg) from e - assert self.im is not None return self._new(self.im.getband(channel)) def tell(self) -> int: @@ -2814,7 +2790,6 @@ class Image: self.im = im.im self._size = final_size - assert self.im is not None self._mode = self.im.mode self.readonly = 0 @@ -2999,7 +2974,6 @@ class Image: if image.mode in ("1", "P"): resample = Resampling.NEAREST - assert self.im is not None self.im.transform(box, image.im, method, data, resample, fill) def transpose(self, method: Transpose) -> Image: @@ -3014,7 +2988,6 @@ class Image: """ self.load() - assert self.im is not None return self._new(self.im.transpose(method)) def effect_spread(self, distance: int) -> Image: @@ -3024,7 +2997,6 @@ class Image: :param distance: Distance to spread pixels. """ self.load() - assert self.im is not None return self._new(self.im.effect_spread(distance)) def toqimage(self): @@ -3258,7 +3230,6 @@ def frombuffer( if mode == "P": from . import ImagePalette - assert im.im is not None im.palette = ImagePalette.ImagePalette("RGB", im.im.getpalette("RGB")) im.readonly = 1 return im diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index b1b616d5c..29a5c995f 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -48,7 +48,6 @@ def invert(image: Image.Image) -> Image.Image: """ image.load() - assert image.im is not None return image._new(image.im.chop_invert()) @@ -64,7 +63,6 @@ def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_lighter(image2.im)) @@ -80,7 +78,6 @@ def darker(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_darker(image2.im)) @@ -96,7 +93,6 @@ def difference(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_difference(image2.im)) @@ -114,7 +110,6 @@ def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_multiply(image2.im)) @@ -129,7 +124,6 @@ def screen(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_screen(image2.im)) @@ -142,7 +136,6 @@ def soft_light(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_soft_light(image2.im)) @@ -155,7 +148,6 @@ def hard_light(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_hard_light(image2.im)) @@ -168,7 +160,6 @@ def overlay(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_overlay(image2.im)) @@ -186,7 +177,6 @@ def add( image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_add(image2.im, scale, offset)) @@ -204,7 +194,6 @@ def subtract( image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_subtract(image2.im, scale, offset)) @@ -218,7 +207,6 @@ def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_add_modulo(image2.im)) @@ -232,7 +220,6 @@ def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_subtract_modulo(image2.im)) @@ -251,7 +238,6 @@ def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_and(image2.im)) @@ -267,7 +253,6 @@ def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_or(image2.im)) @@ -283,7 +268,6 @@ def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image: image1.load() image2.load() - assert image1.im is not None return image1._new(image1.im.chop_xor(image2.im)) @@ -324,5 +308,4 @@ def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Imag if yoffset is None: yoffset = xoffset image.load() - assert image.im is not None return image._new(image.im.offset(xoffset, yoffset)) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 3a6d52949..ec10230f1 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -352,8 +352,6 @@ class ImageCmsTransform(Image.ImagePointHandler): im.load() if imOut is None: imOut = Image.new(self.output_mode, im.size, None) - assert im.im is not None - assert imOut.im is not None self.transform.apply(im.im.id, imOut.im.id) imOut.info["icc_profile"] = self.output_profile.tobytes() return imOut @@ -363,7 +361,6 @@ class ImageCmsTransform(Image.ImagePointHandler): if im.mode != self.output_mode: msg = "mode mismatch" raise ValueError(msg) # wrong output mode - assert im.im is not None self.transform.apply(im.im.id, im.im.id) im.info["icc_profile"] = self.output_profile.tobytes() return im diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index babf453cb..24b3a9eb1 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -315,7 +315,7 @@ class ImageFile(Image.Image): def load_prepare(self) -> None: # create image memory if necessary - if not self.im or self.im.mode != self.mode or self.im.size != self.size: + if self._im is None or self.im.mode != self.mode or self.im.size != self.size: self.im = Image.core.new(self.mode, self.size) # create palette (optional) if self.mode == "P": @@ -475,7 +475,6 @@ class Parser: d, e, o, a = im.tile[0] im.tile = [] self.decoder = Image._getdecoder(im.mode, d, a, im.decoderconfig) - assert im.im is not None self.decoder.setimage(im.im, e) # calculate decoder offset @@ -567,7 +566,6 @@ def _encode_tile( fp.seek(offset) encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig) try: - assert im.im is not None encoder.setimage(im.im, extents) if encoder.pushes_fd: encoder.setfd(fp) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 2d71b4df4..2ab65bfef 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -142,7 +142,6 @@ class ImageFont: raise TypeError(msg) image.load() - assert image.im is not None self.font = Image.core.font(image.im, data) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index c8f0406ed..191cc2a5f 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -65,8 +65,6 @@ class _Operand: except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - assert out.im is not None - assert im_1.im is not None _imagingmath.unop(op, out.im.id, im_1.im.id) else: # binary operation @@ -95,9 +93,6 @@ class _Operand: except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - assert out.im is not None - assert im_1.im is not None - assert im_2.im is not None _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) return _Operand(out) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index cd4fa3cfe..6a43983d3 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -213,8 +213,6 @@ class MorphOp: msg = "Image mode must be L" raise ValueError(msg) outimage = Image.new(image.mode, image.size, None) - assert image.im is not None - assert outimage.im is not None count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage @@ -231,7 +229,6 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) - assert image.im is not None return _imagingmorph.match(bytes(self.lut), image.im.id) def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: @@ -243,7 +240,6 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) - assert image.im is not None return _imagingmorph.get_on_pixels(image.im.id) def load_lut(self, filename: str) -> None: diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 6344220ef..a788c9d1f 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -43,7 +43,6 @@ def _pilbitmap_check() -> int: if _pilbitmap_ok is None: try: im = Image.new("1", (1, 1)) - assert im.im is not None tkinter.BitmapImage(data=f"PIL:{im.im.id}") _pilbitmap_ok = 1 except tkinter.TclError: @@ -188,7 +187,6 @@ class PhotoImage: # convert to blittable im.load() image = im.im - assert image is not None if image.isblock() and im.mode == self.__mode: block = image else: @@ -230,7 +228,6 @@ class BitmapImage: if _pilbitmap_check(): # fast way (requires the pilbitmap booster patch) image.load() - assert image.im is not None kw["data"] = f"PIL:{image.im.id}" self.__im = image # must keep a reference else: @@ -277,7 +274,6 @@ def getimage(photo: PhotoImage) -> Image.Image: """Copies the contents of a PhotoImage to a PIL image memory.""" im = Image.new("RGBA", (photo.width(), photo.height())) block = im.im - assert block is not None _pyimagingtkcall("PyImagingPhotoGet", photo, block.id) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 04fe8ba85..fc897e2b9 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -473,7 +473,6 @@ class JpegImageFile(ImageFile.ImageFile): except OSError: pass - assert self.im is not None self._mode = self.im.mode self._size = self.im.size diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 64898e235..d9970405c 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -52,10 +52,7 @@ class PcdImageFile(ImageFile.ImageFile): def load_end(self) -> None: if self.tile_post_rotate: # Handle rotated PCDs - assert self.im is not None - self.im = self.im.rotate(self.tile_post_rotate) - assert self.im is not None self._size = self.im.size diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index dd42003b5..2b8ced8af 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -202,8 +202,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.mode == "P": # colour palette - assert im.im is not None - fp.write(o8(12)) palette = im.im.getpalette("RGB", "RGB") palette += b"\x00" * (768 - len(palette)) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 59b5d0a8b..a0ac24571 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -869,7 +869,7 @@ class PngImageFile(ImageFile.ImageFile): self._fp.seek(self.__rewind) self.png.rewind() self.__prepare_idat = self.__rewind_idat - self.im = None + self._im = None self.info = self.png.im_info self.tile = self.png.im_tile self.fp = self._fp @@ -887,7 +887,6 @@ class PngImageFile(ImageFile.ImageFile): # ensure previous frame was loaded self.load() - assert self.im is not None if self.dispose: self.im.paste(self.dispose, self.dispose_extent) @@ -1050,7 +1049,6 @@ class PngImageFile(ImageFile.ImageFile): self.png = None else: if self._prev_im and self.blend_op == Blend.OP_OVER: - assert self.im is not None updated = self._crop(self.im, self.dispose_extent) if self.im.mode == "RGB" and "transparency" in self.info: mask = updated.convert_transparent( @@ -1405,7 +1403,6 @@ def _save( chunk(fp, cid, data) if im.mode == "P": - assert im.im is not None palette_byte_number = colors * 3 palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] while len(palette_bytes) < palette_byte_number: @@ -1436,9 +1433,8 @@ def _save( # and it's in the info dict. It's probably just stale. msg = "cannot use transparency for this mode" raise OSError(msg) - elif im.mode == "P": - assert im.im is not None - if im.im.getpalettemode() == "RGBA": + else: + if im.mode == "P" and im.im.getpalettemode() == "RGBA": alpha = im.im.getpalette("RGBA", "A") alpha_bytes = colors chunk(fp, b"tRNS", alpha[:alpha_bytes]) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 39104aece..c2a0ee4e9 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -158,7 +158,6 @@ class TgaImageFile(ImageFile.ImageFile): def load_end(self) -> None: if self._flip_horizontally: - assert self.im is not None self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) @@ -200,7 +199,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: warnings.warn("id_section has been trimmed to 255 characters") if colormaptype: - assert im.im is not None palette = im.im.getpalette("RGB", "BGR") colormaplength, colormapentry = len(palette) // 3, 24 else: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2b53c7dfa..d9d1bab5a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1313,7 +1313,6 @@ class TiffImageFile(ImageFile.ImageFile): decoder = Image._getdecoder( self.mode, "libtiff", tuple(args), self.decoderconfig ) - assert self.im is not None try: decoder.setimage(self.im, extents) except ValueError as e: diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index ec5c74900..87e32878b 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -54,7 +54,7 @@ class WalImageFile(ImageFile.ImageFile): self.info["next_name"] = next_name def load(self) -> Image.core.PixelAccess | None: - if not self.im: + if self._im is None: self.im = Image.core.new(self.mode, self.size) self.frombytes(self.fp.read(self.size[0] * self.size[1])) self.putpalette(quake2palette) From de441ee09107a2ac544eee905f6e94aeaa21cb55 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Aug 2024 21:05:14 +1000 Subject: [PATCH 052/652] Updated to Python 3.10 --- Tests/oss-fuzz/python.supp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp index 94cc87db9..36385d672 100644 --- a/Tests/oss-fuzz/python.supp +++ b/Tests/oss-fuzz/python.supp @@ -1,5 +1,5 @@ { - + Memcheck:Cond ... fun:encode_current_locale From 0d79a38e77ddcf5e386d2153e791f12e5d67a2f5 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 11 Aug 2024 15:14:29 +0400 Subject: [PATCH 053/652] Add missing TIFF CMYK;16B reader --- src/PIL/TiffImagePlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index d9d1bab5a..deae199d5 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -259,6 +259,7 @@ OPEN_INFO = { (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), + (MM, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16B"), (II, 6, (1,), 1, (8,), ()): ("L", "L"), (MM, 6, (1,), 1, (8,), ()): ("L", "L"), # JPEG compressed images handled by LibTiff and auto-converted to RGBX From f801649513f1e1a89505cc6c481d5b26202dc2f4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 06:11:32 +1000 Subject: [PATCH 054/652] Updated freetype to 2.13.3 --- winbuild/build_prepare.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9837589b2..241737f92 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -111,7 +111,7 @@ ARCHITECTURES = { V = { "BROTLI": "1.1.0", - "FREETYPE": "2.13.2", + "FREETYPE": "2.13.3", "FRIBIDI": "1.0.15", "HARFBUZZ": "8.5.0", "JPEGTURBO": "3.0.3", @@ -293,12 +293,8 @@ DEPS = { }, "build": [ cmd_rmdir("objs"), - cmd_msbuild( - r"builds\windows\vc2010\freetype.sln", "Release Static", "Clean" - ), - cmd_msbuild( - r"builds\windows\vc2010\freetype.sln", "Release Static", "Build" - ), + cmd_msbuild("MSBuild.sln", "Release Static", "Clean"), + cmd_msbuild("MSBuild.sln", "Release Static", "Build"), cmd_xcopy("include", "{inc_dir}"), ], "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], From c1e8375af834b190386cd2cfa30703c7975675a8 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 21:51:14 +0400 Subject: [PATCH 055/652] Require webpmux and webpdemux --- Tests/check_wheel.py | 1 - Tests/test_features.py | 5 --- Tests/test_file_webp_metadata.py | 5 +-- docs/handbook/image-file-formats.rst | 9 ++--- docs/installation/building-from-source.rst | 8 ++-- docs/reference/features.rst | 1 - setup.py | 46 ++++++++-------------- src/PIL/features.py | 2 - src/_webp.c | 33 ---------------- winbuild/build_prepare.py | 2 +- 10 files changed, 26 insertions(+), 86 deletions(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 4b91984f5..2e4498db6 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -28,7 +28,6 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { "webp_anim", - "webp_mux", "transp_webp", "raqm", "fribidi", diff --git a/Tests/test_features.py b/Tests/test_features.py index b7eefa09a..33509a346 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -57,11 +57,6 @@ def test_webp_transparency() -> None: assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY -@skip_unless_feature("webp") -def test_webp_mux() -> None: - assert features.check("webp_mux") == _webp.HAVE_WEBPMUX - - @skip_unless_feature("webp") def test_webp_anim() -> None: assert features.check("webp_anim") == _webp.HAVE_WEBPANIM diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index c3df4ad7b..52a84a83e 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -10,10 +10,7 @@ from PIL import Image from .helper import mark_if_feature_version, skip_unless_feature -pytestmark = [ - skip_unless_feature("webp"), - skip_unless_feature("webp_mux"), -] +pytestmark = [skip_unless_feature("webp")] ElementTree: ModuleType | None try: diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1ec972149..8561cc4aa 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1252,16 +1252,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: Requires libwebp 0.5.0 or later. **icc_profile** - The ICC Profile to include in the saved file. Only supported if - the system WebP library was built with webpmux support. + The ICC Profile to include in the saved file. **exif** - The exif data to include in the saved file. Only supported if - the system WebP library was built with webpmux support. + The exif data to include in the saved file. **xmp** - The XMP data to include in the saved file. Only supported if - the system WebP library was built with webpmux support. + The XMP data to include in the saved file. Saving sequences ~~~~~~~~~~~~~~~~ diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 7f7dfa6ff..f47edbc5b 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -275,18 +275,18 @@ Build Options * 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 lcms=disable``, ``-C webp=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 zlib=enable``, ``-C jpeg=enable``, ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, - ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, + ``-C lcms=enable``, ``-C webp=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. + an exception if the libraries are not found. Tcl and Tk must be used + together. * Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. These flags are used to compile a modified version of libraqm and diff --git a/docs/reference/features.rst b/docs/reference/features.rst index c66193061..55c0b1200 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -55,7 +55,6 @@ Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. * ``transp_webp``: Support for transparency in WebP images. -* ``webp_mux``: (compile time) Support for EXIF data in WebP images. * ``webp_anim``: (compile time) Support for animated WebP images. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. diff --git a/setup.py b/setup.py index b26852b0b..8d0dfb6fa 100644 --- a/setup.py +++ b/setup.py @@ -295,7 +295,6 @@ class pil_build_ext(build_ext): "raqm", "lcms", "webp", - "webpmux", "jpeg2000", "imagequant", "xcb", @@ -794,29 +793,22 @@ class pil_build_ext(build_ext): if feature.want("webp"): _dbg("Looking for webp") - if _find_include_file(self, "webp/encode.h") and _find_include_file( - self, "webp/decode.h" + if all( + _find_include_file(self, src) + for src in ["webp/encode.h", "webp/mux.h", "webp/demux.h"] ): # In Google's precompiled zip it is call "libwebp": - if _find_library_file(self, "webp"): + if all( + _find_library_file(self, lib) + for lib in ["webp", "webpmux", "webpdemux"] + ): feature.webp = "webp" - elif _find_library_file(self, "libwebp"): + elif all( + _find_library_file(self, lib) + for lib in ["libwebp", "libwebpmux", "libwebpdemux"] + ): feature.webp = "libwebp" - if feature.want("webpmux"): - _dbg("Looking for webpmux") - if _find_include_file(self, "webp/mux.h") and _find_include_file( - self, "webp/demux.h" - ): - if _find_library_file(self, "webpmux") and _find_library_file( - self, "webpdemux" - ): - feature.webpmux = "webpmux" - if _find_library_file(self, "libwebpmux") and _find_library_file( - self, "libwebpdemux" - ): - feature.webpmux = "libwebpmux" - if feature.want("xcb"): _dbg("Looking for xcb") if _find_include_file(self, "xcb/xcb.h"): @@ -904,15 +896,12 @@ class pil_build_ext(build_ext): self._remove_extension("PIL._imagingcms") if feature.webp: - libs = [feature.webp] - defs = [] - - if feature.webpmux: - defs.append(("HAVE_WEBPMUX", None)) - libs.append(feature.webpmux) - libs.append(feature.webpmux.replace("pmux", "pdemux")) - - self._update_extension("PIL._webp", libs, defs) + libs = [ + feature.webp, + feature.webp + "mux", + feature.webp + "demux", + ] + self._update_extension("PIL._webp", libs, []) else: self._remove_extension("PIL._webp") @@ -953,7 +942,6 @@ class pil_build_ext(build_ext): (feature.raqm, "RAQM (Text shaping)", raqm_extra_info), (feature.lcms, "LITTLECMS2"), (feature.webp, "WEBP"), - (feature.webpmux, "WEBPMUX"), (feature.xcb, "XCB (X protocol)"), ] diff --git a/src/PIL/features.py b/src/PIL/features.py index 13908c4eb..7f6cdb161 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -120,7 +120,6 @@ def get_supported_codecs() -> list[str]: features = { "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), - "webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None), "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), @@ -272,7 +271,6 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), ("transp_webp", "WEBP Transparency"), - ("webp_mux", "WEBPMUX"), ("webp_anim", "WEBP Animation"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), diff --git a/src/_webp.c b/src/_webp.c index e686ec820..ab229058b 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -4,8 +4,6 @@ #include #include #include - -#ifdef HAVE_WEBPMUX #include #include @@ -19,8 +17,6 @@ #define HAVE_WEBPANIM #endif -#endif - void ImagingSectionEnter(ImagingSectionCookie *cookie) { *cookie = (PyThreadState *)PyEval_SaveThread(); @@ -35,8 +31,6 @@ ImagingSectionLeave(ImagingSectionCookie *cookie) { /* WebP Muxer Error Handling */ /* -------------------------------------------------------------------- */ -#ifdef HAVE_WEBPMUX - static const char *const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = { "WEBP_MUX_NOT_FOUND", "WEBP_MUX_INVALID_ARGUMENT", @@ -89,8 +83,6 @@ HandleMuxError(WebPMuxError err, char *chunk) { return NULL; } -#endif - /* -------------------------------------------------------------------- */ /* WebP Animation Support */ /* -------------------------------------------------------------------- */ @@ -693,13 +685,6 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { output = writer.mem; ret_size = writer.size; -#ifndef HAVE_WEBPMUX - if (ret_size > 0) { - PyObject *ret = PyBytes_FromStringAndSize((char *)output, ret_size); - free(output); - return ret; - } -#else { /* I want to truncate the *_size items that get passed into WebP data. Pypy2.1.0 had some issues where the Py_ssize_t items had @@ -775,7 +760,6 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { return ret; } } -#endif Py_RETURN_NONE; } @@ -809,9 +793,6 @@ WebPDecode_wrapper(PyObject *self, PyObject *args) { mode = "RGBA"; } -#ifndef HAVE_WEBPMUX - vp8_status_code = WebPDecode(webp, size, &config); -#else { int copy_data = 0; WebPData data = {webp, size}; @@ -849,7 +830,6 @@ WebPDecode_wrapper(PyObject *self, PyObject *args) { WebPDataClear(&image.bitstream); WebPMuxDelete(mux); } -#endif } if (vp8_status_code != VP8_STATUS_OK) { @@ -949,18 +929,6 @@ static PyMethodDef webpMethods[] = { {NULL, NULL} }; -void -addMuxFlagToModule(PyObject *m) { - PyObject *have_webpmux; -#ifdef HAVE_WEBPMUX - have_webpmux = Py_True; -#else - have_webpmux = Py_False; -#endif - Py_INCREF(have_webpmux); - PyModule_AddObject(m, "HAVE_WEBPMUX", have_webpmux); -} - void addAnimFlagToModule(PyObject *m) { PyObject *have_webpanim; @@ -991,7 +959,6 @@ setup_module(PyObject *m) { } #endif PyObject *d = PyModule_GetDict(m); - addMuxFlagToModule(m); addAnimFlagToModule(m); addTransparencyFlagToModule(m); diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9837589b2..7129699eb 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -201,7 +201,7 @@ DEPS = { }, "build": [ *cmds_cmake( - "webp webpdemux webpmux", + "webp webpmux webpdemux", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DWEBP_LINK_STATIC:BOOL=OFF", ), From 9bed5b426437ad4aacd36288fcfb9422cac8c9c9 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 22:05:57 +0400 Subject: [PATCH 056/652] Remove _webp.WebPDecoderBuggyAlpha and _webp.HAVE_TRANSPARENCY --- Tests/check_wheel.py | 1 - Tests/test_features.py | 6 ------ Tests/test_file_webp.py | 1 - Tests/test_file_webp_alpha.py | 8 -------- docs/reference/features.rst | 1 - src/PIL/features.py | 2 -- src/_webp.c | 27 --------------------------- 7 files changed, 46 deletions(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 2e4498db6..f862e4fff 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -28,7 +28,6 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { "webp_anim", - "transp_webp", "raqm", "fribidi", "harfbuzz", diff --git a/Tests/test_features.py b/Tests/test_features.py index 33509a346..577e7a5d5 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -51,12 +51,6 @@ def test_version() -> None: test(feature, features.version_feature) -@skip_unless_feature("webp") -def test_webp_transparency() -> None: - assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() - assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY - - @skip_unless_feature("webp") def test_webp_anim() -> None: assert features.check("webp_anim") == _webp.HAVE_WEBPANIM diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index cbc905de4..f132d0a64 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -49,7 +49,6 @@ class TestFileWebp: def test_version(self) -> None: _webp.WebPDecoderVersion() - _webp.WebPDecoderBuggyAlpha() version = features.version_module("webp") assert version is not None assert re.search(r"\d+\.\d+\.\d+$", version) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index c74452121..e80ef7d4f 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -16,11 +16,6 @@ from .helper import ( _webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") -def setup_module() -> None: - if _webp.WebPDecoderBuggyAlpha(): - pytest.skip("Buggy early version of WebP installed, not testing transparency") - - def test_read_rgba() -> None: """ Can we read an RGBA mode file without error? @@ -81,9 +76,6 @@ def test_write_rgba(tmp_path: Path) -> None: pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) pil_image.save(temp_file) - if _webp.WebPDecoderBuggyAlpha(): - return - with Image.open(temp_file) as image: image.load() diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 55c0b1200..d4fb340bd 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -54,7 +54,6 @@ Feature version numbers are available only where stated. Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. -* ``transp_webp``: Support for transparency in WebP images. * ``webp_anim``: (compile time) Support for animated WebP images. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. diff --git a/src/PIL/features.py b/src/PIL/features.py index 7f6cdb161..60590dc09 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -120,7 +120,6 @@ def get_supported_codecs() -> list[str]: features = { "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), - "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), @@ -270,7 +269,6 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), - ("transp_webp", "WEBP Transparency"), ("webp_anim", "WEBP Animation"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), diff --git a/src/_webp.c b/src/_webp.c index ab229058b..a10eb111c 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -896,20 +896,6 @@ WebPDecoderVersion_str(void) { return version; } -/* - * The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well. - * Files that are valid with 0.3 are reported as being invalid. - */ -int -WebPDecoderBuggyAlpha(void) { - return WebPGetDecoderVersion() == 0x0103; -} - -PyObject * -WebPDecoderBuggyAlpha_wrapper() { - return Py_BuildValue("i", WebPDecoderBuggyAlpha()); -} - /* -------------------------------------------------------------------- */ /* Module Setup */ /* -------------------------------------------------------------------- */ @@ -922,10 +908,6 @@ static PyMethodDef webpMethods[] = { {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_NOARGS, "WebPVersion"}, - {"WebPDecoderBuggyAlpha", - WebPDecoderBuggyAlpha_wrapper, - METH_NOARGS, - "WebPDecoderBuggyAlpha"}, {NULL, NULL} }; @@ -941,14 +923,6 @@ addAnimFlagToModule(PyObject *m) { PyModule_AddObject(m, "HAVE_WEBPANIM", have_webpanim); } -void -addTransparencyFlagToModule(PyObject *m) { - PyObject *have_transparency = PyBool_FromLong(!WebPDecoderBuggyAlpha()); - if (PyModule_AddObject(m, "HAVE_TRANSPARENCY", have_transparency)) { - Py_DECREF(have_transparency); - } -} - static int setup_module(PyObject *m) { #ifdef HAVE_WEBPANIM @@ -960,7 +934,6 @@ setup_module(PyObject *m) { #endif PyObject *d = PyModule_GetDict(m); addAnimFlagToModule(m); - addTransparencyFlagToModule(m); PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); From a3468996c0b7b6df2b685ff21c4f515f5105ff8c Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 22:47:06 +0400 Subject: [PATCH 057/652] Remove webp animations flags and conditions Removed: _webp.WebPDecode _webp.HAVE_WEBPANIM features.webp_anim --- Tests/check_wheel.py | 1 - Tests/test_features.py | 10 -- Tests/test_file_gif.py | 2 +- Tests/test_file_webp.py | 24 ++--- Tests/test_file_webp_alpha.py | 7 +- Tests/test_file_webp_animated.py | 5 +- Tests/test_file_webp_lossless.py | 3 - Tests/test_file_webp_metadata.py | 3 +- Tests/test_image.py | 1 - Tests/test_imagefile.py | 1 - Tests/test_imageops.py | 2 +- docs/handbook/image-file-formats.rst | 9 +- docs/reference/features.rst | 1 - setup.py | 7 +- src/PIL/WebPImagePlugin.py | 47 +++------ src/PIL/features.py | 2 - src/_webp.c | 150 +-------------------------- 17 files changed, 34 insertions(+), 241 deletions(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index f862e4fff..8fcb75dad 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -27,7 +27,6 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { - "webp_anim", "raqm", "fribidi", "harfbuzz", diff --git a/Tests/test_features.py b/Tests/test_features.py index 577e7a5d5..40466082f 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -10,11 +10,6 @@ from PIL import features from .helper import skip_unless_feature -try: - from PIL import _webp -except ImportError: - pass - def test_check() -> None: # Check the correctness of the convenience function @@ -51,11 +46,6 @@ def test_version() -> None: test(feature, features.version_feature) -@skip_unless_feature("webp") -def test_webp_anim() -> None: - assert features.check("webp_anim") == _webp.HAVE_WEBPANIM - - @skip_unless_feature("libjpeg_turbo") def test_libjpeg_turbo_version() -> None: version = features.version("libjpeg_turbo") diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 85b017d29..8cefdb628 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -978,7 +978,7 @@ def test_webp_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Test opaque WebP background - if features.check("webp") and features.check("webp_anim"): + if features.check("webp"): with Image.open("Tests/images/hopper.webp") as im: assert im.info["background"] == (255, 255, 255, 255) im.save(out) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index f132d0a64..6e9eea4ae 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -48,7 +48,6 @@ class TestFileWebp: self.rgb_mode = "RGB" def test_version(self) -> None: - _webp.WebPDecoderVersion() version = features.version_module("webp") assert version is not None assert re.search(r"\d+\.\d+\.\d+$", version) @@ -116,7 +115,6 @@ class TestFileWebp: hopper().save(buffer_method, format="WEBP", method=6) assert buffer_no_args.getbuffer() != buffer_method.getbuffer() - @skip_unless_feature("webp_anim") def test_save_all(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (1, 1)) @@ -131,10 +129,9 @@ class TestFileWebp: def test_icc_profile(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) - if _webp.HAVE_WEBPANIM: - self._roundtrip( - tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} - ) + self._roundtrip( + tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} + ) def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: """ @@ -164,10 +161,8 @@ class TestFileWebp: """ Calling encoder functions with no arguments should result in an error. """ - - if _webp.HAVE_WEBPANIM: - with pytest.raises(TypeError): - _webp.WebPAnimEncoder() + with pytest.raises(TypeError): + _webp.WebPAnimEncoder() with pytest.raises(TypeError): _webp.WebPEncode() @@ -175,12 +170,8 @@ class TestFileWebp: """ Calling decoder functions with no arguments should result in an error. """ - - if _webp.HAVE_WEBPANIM: - with pytest.raises(TypeError): - _webp.WebPAnimDecoder() with pytest.raises(TypeError): - _webp.WebPDecode() + _webp.WebPAnimDecoder() def test_no_resource_warning(self, tmp_path: Path) -> None: file_path = "Tests/images/hopper.webp" @@ -199,7 +190,6 @@ class TestFileWebp: "background", (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), ) - @skip_unless_feature("webp_anim") def test_invalid_background( self, background: int | tuple[int, ...], tmp_path: Path ) -> None: @@ -208,7 +198,6 @@ class TestFileWebp: with pytest.raises(OSError): im.save(temp_file, save_all=True, append_images=[im], background=background) - @skip_unless_feature("webp_anim") def test_background_from_gif(self, tmp_path: Path) -> None: # Save L mode GIF with background with Image.open("Tests/images/no_palette_with_background.gif") as im: @@ -233,7 +222,6 @@ class TestFileWebp: difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) assert difference < 5 - @skip_unless_feature("webp_anim") def test_duration(self, tmp_path: Path) -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: assert im.info["duration"] == 1000 diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index e80ef7d4f..53cdfff03 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -85,12 +85,7 @@ def test_write_rgba(tmp_path: Path) -> None: image.load() image.getdata() - # Early versions of WebP are known to produce higher deviations: - # deal with it - if _webp.WebPDecoderVersion() <= 0x201: - assert_image_similar(image, pil_image, 3.0) - else: - assert_image_similar(image, pil_image, 1.0) + assert_image_similar(image, pil_image, 1.0) def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index e0d7999e3..967a0aae8 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -15,10 +15,7 @@ from .helper import ( skip_unless_feature, ) -pytestmark = [ - skip_unless_feature("webp"), - skip_unless_feature("webp_anim"), -] +pytestmark = skip_unless_feature("webp") def test_n_frames() -> None: diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 32e29de56..dc98ad3f0 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -13,9 +13,6 @@ RGB_MODE = "RGB" def test_write_lossless_rgb(tmp_path: Path) -> None: - if _webp.WebPDecoderVersion() < 0x0200: - pytest.skip("lossless not included") - temp_file = str(tmp_path / "temp.webp") hopper(RGB_MODE).save(temp_file, lossless=True) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 52a84a83e..4ef3d95f2 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -10,7 +10,7 @@ from PIL import Image from .helper import mark_if_feature_version, skip_unless_feature -pytestmark = [skip_unless_feature("webp")] +pytestmark = skip_unless_feature("webp") ElementTree: ModuleType | None try: @@ -133,7 +133,6 @@ def test_getxmp() -> None: ) -@skip_unless_feature("webp_anim") def test_write_animated_metadata(tmp_path: Path) -> None: iccp_data = b"" exif_data = b"" diff --git a/Tests/test_image.py b/Tests/test_image.py index d8372789b..719732d12 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -817,7 +817,6 @@ class TestImage: assert reloaded_exif[305] == "Pillow test" @skip_unless_feature("webp") - @skip_unless_feature("webp_anim") def test_exif_webp(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.webp") as im: exif = im.getexif() diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 44a6e6a42..fe7d44785 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -94,7 +94,6 @@ class TestImageFile: assert (48, 48) == p.image.size @skip_unless_feature("webp") - @skip_unless_feature("webp_anim") def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: with open("Tests/images/hopper.webp", "rb") as f: diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index e33e6d4c8..2fb2a60b6 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -390,7 +390,7 @@ def test_colorize_3color_offset() -> None: def test_exif_transpose() -> None: exts = [".jpg"] - if features.check("webp") and features.check("webp_anim"): + if features.check("webp"): exts.append(".webp") for ext in exts: with Image.open("Tests/images/hopper" + ext) as base_im: diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 8561cc4aa..49a17bb5c 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1220,8 +1220,7 @@ using the general tags available through tiffinfo. WebP ^^^^ -Pillow reads and writes WebP files. The specifics of Pillow's capabilities with -this format are currently undocumented. +Pillow reads and writes WebP files. Requires libwebp v0.5.0 or later. .. _webp-saving: @@ -1263,12 +1262,6 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: Saving sequences ~~~~~~~~~~~~~~~~ -.. note:: - - Support for animated WebP files will only be enabled if the system WebP - library is v0.5.0 or later. You can check webp animation support at - runtime by calling ``features.check("webp_anim")``. - When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default only the first frame of a multiframe image will be saved. If the ``save_all`` argument is present and true, then all frames will be saved, and the following diff --git a/docs/reference/features.rst b/docs/reference/features.rst index d4fb340bd..26c8ab8cd 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -54,7 +54,6 @@ Feature version numbers are available only where stated. Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. -* ``webp_anim``: (compile time) Support for animated WebP images. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. diff --git a/setup.py b/setup.py index 8d0dfb6fa..edfa9ee28 100644 --- a/setup.py +++ b/setup.py @@ -795,7 +795,12 @@ class pil_build_ext(build_ext): _dbg("Looking for webp") if all( _find_include_file(self, src) - for src in ["webp/encode.h", "webp/mux.h", "webp/demux.h"] + for src in [ + "webp/encode.h", + "webp/decode.h", + "webp/mux.h", + "webp/demux.h", + ] ): # In Google's precompiled zip it is call "libwebp": if all( diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index cec796340..fc8f4fc2e 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -45,22 +45,6 @@ class WebPImageFile(ImageFile.ImageFile): __logical_frame = 0 def _open(self) -> None: - if not _webp.HAVE_WEBPANIM: - # Legacy mode - data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode( - self.fp.read() - ) - if icc_profile: - self.info["icc_profile"] = icc_profile - if exif: - self.info["exif"] = exif - self._size = width, height - self.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] - self.n_frames = 1 - self.is_animated = False - return - # Use the newer AnimDecoder API to parse the (possibly) animated file, # and access muxed chunks like ICC/EXIF/XMP. self._decoder = _webp.WebPAnimDecoder(self.fp.read()) @@ -145,21 +129,20 @@ class WebPImageFile(ImageFile.ImageFile): self._get_next() # Advance to the requested frame def load(self) -> Image.core.PixelAccess | None: - if _webp.HAVE_WEBPANIM: - if self.__loaded != self.__logical_frame: - self._seek(self.__logical_frame) + if self.__loaded != self.__logical_frame: + self._seek(self.__logical_frame) - # We need to load the image data for this frame - data, timestamp, duration = self._get_next() - self.info["timestamp"] = timestamp - self.info["duration"] = duration - self.__loaded = self.__logical_frame + # We need to load the image data for this frame + data, timestamp, duration = self._get_next() + self.info["timestamp"] = timestamp + self.info["duration"] = duration + self.__loaded = self.__logical_frame - # Set tile - if self.fp and self._exclusive_fp: - self.fp.close() - self.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] + # Set tile + if self.fp and self._exclusive_fp: + self.fp.close() + self.fp = BytesIO(data) + self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] return super().load() @@ -167,9 +150,6 @@ class WebPImageFile(ImageFile.ImageFile): pass def tell(self) -> int: - if not _webp.HAVE_WEBPANIM: - return super().tell() - return self.__logical_frame @@ -357,7 +337,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: Image.register_open(WebPImageFile.format, WebPImageFile, _accept) if SUPPORTED: Image.register_save(WebPImageFile.format, _save) - if _webp.HAVE_WEBPANIM: - Image.register_save_all(WebPImageFile.format, _save_all) + Image.register_save_all(WebPImageFile.format, _save_all) Image.register_extension(WebPImageFile.format, ".webp") Image.register_mime(WebPImageFile.format, "image/webp") diff --git a/src/PIL/features.py b/src/PIL/features.py index 60590dc09..bbf7c641b 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -119,7 +119,6 @@ def get_supported_codecs() -> list[str]: features = { - "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), @@ -269,7 +268,6 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), - ("webp_anim", "WEBP Animation"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), ("zlib", "ZLIB (PNG/ZIP)"), diff --git a/src/_webp.c b/src/_webp.c index a10eb111c..8302925b0 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -13,8 +13,8 @@ * very early versions had some significant differences, so we require later * versions, before enabling animation support. */ -#if WEBP_MUX_ABI_VERSION >= 0x0104 && WEBP_DEMUX_ABI_VERSION >= 0x0105 -#define HAVE_WEBPANIM +#if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107 +#error libwebp 0.5.0 and above is required. Upgrade libwebp or build with --disable-webp flag #endif void @@ -87,8 +87,6 @@ HandleMuxError(WebPMuxError err, char *chunk) { /* WebP Animation Support */ /* -------------------------------------------------------------------- */ -#ifdef HAVE_WEBPANIM - // Encoder type typedef struct { PyObject_HEAD WebPAnimEncoder *enc; @@ -568,8 +566,6 @@ static PyTypeObject WebPAnimDecoder_Type = { 0, /*tp_getset*/ }; -#endif - /* -------------------------------------------------------------------- */ /* Legacy WebP Support */ /* -------------------------------------------------------------------- */ @@ -644,10 +640,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { config.quality = quality_factor; config.alpha_quality = alpha_quality_factor; config.method = method; -#if WEBP_ENCODER_ABI_VERSION >= 0x0209 - // the "exact" flag is only available in libwebp 0.5.0 and later config.exact = exact; -#endif // Validate the config if (!WebPValidateConfig(&config)) { @@ -763,124 +756,6 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { Py_RETURN_NONE; } -PyObject * -WebPDecode_wrapper(PyObject *self, PyObject *args) { - PyBytesObject *webp_string; - const uint8_t *webp; - Py_ssize_t size; - PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, - *exif = NULL; - WebPDecoderConfig config; - VP8StatusCode vp8_status_code = VP8_STATUS_OK; - char *mode = "RGB"; - - if (!PyArg_ParseTuple(args, "S", &webp_string)) { - return NULL; - } - - if (!WebPInitDecoderConfig(&config)) { - Py_RETURN_NONE; - } - - PyBytes_AsStringAndSize((PyObject *)webp_string, (char **)&webp, &size); - - vp8_status_code = WebPGetFeatures(webp, size, &config.input); - if (vp8_status_code == VP8_STATUS_OK) { - // If we don't set it, we don't get alpha. - // Initialized to MODE_RGB - if (config.input.has_alpha) { - config.output.colorspace = MODE_RGBA; - mode = "RGBA"; - } - - { - int copy_data = 0; - WebPData data = {webp, size}; - WebPMuxFrameInfo image; - WebPData icc_profile_data = {0}; - WebPData exif_data = {0}; - - WebPMux *mux = WebPMuxCreate(&data, copy_data); - if (NULL == mux) { - goto end; - } - - if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image)) { - WebPMuxDelete(mux); - goto end; - } - - webp = image.bitstream.bytes; - size = image.bitstream.size; - - vp8_status_code = WebPDecode(webp, size, &config); - - if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) { - icc_profile = PyBytes_FromStringAndSize( - (const char *)icc_profile_data.bytes, icc_profile_data.size - ); - } - - if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) { - exif = PyBytes_FromStringAndSize( - (const char *)exif_data.bytes, exif_data.size - ); - } - - WebPDataClear(&image.bitstream); - WebPMuxDelete(mux); - } - } - - if (vp8_status_code != VP8_STATUS_OK) { - goto end; - } - - if (config.output.colorspace < MODE_YUV) { - bytes = PyBytes_FromStringAndSize( - (char *)config.output.u.RGBA.rgba, config.output.u.RGBA.size - ); - } else { - // Skipping YUV for now. Need Test Images. - // UNDONE -- unclear if we'll ever get here if we set mode_rgb* - bytes = PyBytes_FromStringAndSize( - (char *)config.output.u.YUVA.y, config.output.u.YUVA.y_size - ); - } - - pymode = PyUnicode_FromString(mode); - ret = Py_BuildValue( - "SiiSSS", - bytes, - config.output.width, - config.output.height, - pymode, - NULL == icc_profile ? Py_None : icc_profile, - NULL == exif ? Py_None : exif - ); - -end: - WebPFreeDecBuffer(&config.output); - - Py_XDECREF(bytes); - Py_XDECREF(pymode); - Py_XDECREF(icc_profile); - Py_XDECREF(exif); - - if (Py_None == ret) { - Py_RETURN_NONE; - } - - return ret; -} - -// Return the decoder's version number, packed in hexadecimal using 8bits for -// each of major/minor/revision. E.g: v2.5.7 is 0x020507. -PyObject * -WebPDecoderVersion_wrapper() { - return Py_BuildValue("i", WebPGetDecoderVersion()); -} - // Version as string const char * WebPDecoderVersion_str(void) { @@ -901,39 +776,20 @@ WebPDecoderVersion_str(void) { /* -------------------------------------------------------------------- */ static PyMethodDef webpMethods[] = { -#ifdef HAVE_WEBPANIM {"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"}, {"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"}, -#endif {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, - {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, - {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_NOARGS, "WebPVersion"}, {NULL, NULL} }; -void -addAnimFlagToModule(PyObject *m) { - PyObject *have_webpanim; -#ifdef HAVE_WEBPANIM - have_webpanim = Py_True; -#else - have_webpanim = Py_False; -#endif - Py_INCREF(have_webpanim); - PyModule_AddObject(m, "HAVE_WEBPANIM", have_webpanim); -} - static int setup_module(PyObject *m) { -#ifdef HAVE_WEBPANIM + PyObject *d = PyModule_GetDict(m); /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || PyType_Ready(&WebPAnimEncoder_Type) < 0) { return -1; } -#endif - PyObject *d = PyModule_GetDict(m); - addAnimFlagToModule(m); PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); From 56ca359c657ee2f4e81426f780c4b4e2e281e404 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 8 Jul 2024 20:07:43 +0400 Subject: [PATCH 058/652] Bring back removed features, add deprecations and Release notes --- Tests/check_wheel.py | 3 +++ Tests/test_features.py | 12 ++++++++++++ docs/reference/features.rst | 3 +++ docs/releasenotes/11.0.0.rst | 8 ++++++++ src/PIL/features.py | 10 +++++++++- 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 8fcb75dad..4b91984f5 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -27,6 +27,9 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { + "webp_anim", + "webp_mux", + "transp_webp", "raqm", "fribidi", "harfbuzz", diff --git a/Tests/test_features.py b/Tests/test_features.py index 40466082f..cb8fd8688 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -46,6 +46,18 @@ def test_version() -> None: test(feature, features.version_feature) +def test_webp_transparency() -> None: + assert features.check("transp_webp") == features.check_module("webp") + + +def test_webp_mux() -> None: + assert features.check("webp_mux") == features.check_module("webp") + + +def test_webp_anim() -> None: + assert features.check("webp_anim") == features.check_module("webp") + + @skip_unless_feature("libjpeg_turbo") def test_libjpeg_turbo_version() -> None: version = features.version("libjpeg_turbo") diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 26c8ab8cd..fcff96735 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -57,6 +57,9 @@ Support for the following features can be checked: * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. +* ``transp_webp``: Deprecated. Always ``True`` if WebP module is installed. +* ``webp_mux``: Deprecated. Always ``True`` if WebP module is installed. +* ``webp_anim``: Deprecated. Always ``True`` if WebP module is installed. .. autofunction:: PIL.features.check_feature .. autofunction:: PIL.features.version_feature diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 1d1afcde5..bb707a044 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -58,6 +58,14 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They have been deprecated, and will be removed in Pillow 12 (2025-10-15). +Specific WebP Feature Checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following features ``features.check("transp_webp")``, +``features.check("webp_mux")``, and ``features.check("webp_anim")`` are now +always ``True`` if the WebP module is installed and should not be used. +These checks will be removed in Pillow 12.0.0 (2025-10-15). + API Changes =========== diff --git a/src/PIL/features.py b/src/PIL/features.py index bbf7c641b..f594a2cdc 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -7,6 +7,7 @@ import warnings from typing import IO import PIL +from PIL import _deprecate from . import Image @@ -119,6 +120,9 @@ def get_supported_codecs() -> list[str]: features = { + "webp_anim": ("PIL._webp", True, None), + "webp_mux": ("PIL._webp", True, None), + "transp_webp": ("PIL._webp", True, None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), @@ -144,7 +148,11 @@ def check_feature(feature: str) -> bool | None: try: imported_module = __import__(module, fromlist=["PIL"]) - return getattr(imported_module, flag) + if isinstance(flag, str): + return getattr(imported_module, flag) + else: + _deprecate.deprecate(f'check_feature("{feature}")', 12) + return flag except ModuleNotFoundError: return None except ImportError as ex: From 6180abc75c3dfa950276b2586cd1a8aab1bede70 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 8 Jul 2024 20:32:35 +0400 Subject: [PATCH 059/652] Remove WebP versions notes from docs --- docs/handbook/image-file-formats.rst | 1 - docs/installation/building-from-source.rst | 4 ---- 2 files changed, 5 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 49a17bb5c..861e09a43 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1248,7 +1248,6 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **exact** If true, preserve the transparent RGB values. Otherwise, discard invisible RGB values for better compression. Defaults to false. - Requires libwebp 0.5.0 or later. **icc_profile** The ICC Profile to include in the saved file. diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index f47edbc5b..71787311f 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -55,10 +55,6 @@ Many of Pillow's features require external libraries: * **libwebp** provides the WebP format. - * Pillow has been tested with version **0.1.3**, which does not read - transparent WebP files. Versions **0.3.0** and above support - transparency. - * **openjpeg** provides JPEG 2000 functionality. * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, From 924df9e60b39c6ced91553895497fa43db2d232d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 06:46:46 +1000 Subject: [PATCH 060/652] Moved line after early return Improve compiler advice Update src/PIL/features.py --- src/PIL/features.py | 5 ++--- src/_webp.c | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index f594a2cdc..a5487042e 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -148,11 +148,10 @@ def check_feature(feature: str) -> bool | None: try: imported_module = __import__(module, fromlist=["PIL"]) - if isinstance(flag, str): - return getattr(imported_module, flag) - else: + if isinstance(flag, bool): _deprecate.deprecate(f'check_feature("{feature}")', 12) return flag + return getattr(imported_module, flag) except ModuleNotFoundError: return None except ImportError as ex: diff --git a/src/_webp.c b/src/_webp.c index 8302925b0..306a290bd 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -14,7 +14,7 @@ * versions, before enabling animation support. */ #if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107 -#error libwebp 0.5.0 and above is required. Upgrade libwebp or build with --disable-webp flag +#error libwebp 0.5.0 and above is required. Upgrade libwebp or build Pillow with --disable-webp flag #endif void @@ -784,13 +784,13 @@ static PyMethodDef webpMethods[] = { static int setup_module(PyObject *m) { - PyObject *d = PyModule_GetDict(m); /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || PyType_Ready(&WebPAnimEncoder_Type) < 0) { return -1; } + PyObject *d = PyModule_GetDict(m); PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); Py_XDECREF(v); From 93ce9ce0048639706eecd523fc54c7d6cfc15bef Mon Sep 17 00:00:00 2001 From: Alexander Karpinsky Date: Tue, 13 Aug 2024 09:52:07 +0400 Subject: [PATCH 061/652] Update features type Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index a5487042e..9421dbd3c 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -119,7 +119,7 @@ def get_supported_codecs() -> list[str]: return [f for f in codecs if check_codec(f)] -features = { +features: dict[str, tuple[str, str | bool, str | None]] = { "webp_anim": ("PIL._webp", True, None), "webp_mux": ("PIL._webp", True, None), "transp_webp": ("PIL._webp", True, None), From 55469948282fe2dd763dcb8e66a767b5c63adb54 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:03:14 +1000 Subject: [PATCH 062/652] Removed unnecessary variable --- Tests/test_file_webp_alpha.py | 2 +- Tests/test_file_webp_lossless.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 53cdfff03..c88fe3589 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -13,7 +13,7 @@ from .helper import ( hopper, ) -_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") +pytest.importorskip("PIL._webp", reason="WebP support not installed") def test_read_rgba() -> None: diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index dc98ad3f0..80429715e 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -8,7 +8,7 @@ from PIL import Image from .helper import assert_image_equal, hopper -_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") +pytest.importorskip("PIL._webp", reason="WebP support not installed") RGB_MODE = "RGB" From f3aec6dd383bb27676e7e02a7749a5bff1cc30c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:05:32 +1000 Subject: [PATCH 063/652] Simplified code --- setup.py | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/setup.py b/setup.py index edfa9ee28..863d102cf 100644 --- a/setup.py +++ b/setup.py @@ -794,25 +794,17 @@ class pil_build_ext(build_ext): if feature.want("webp"): _dbg("Looking for webp") if all( - _find_include_file(self, src) - for src in [ - "webp/encode.h", - "webp/decode.h", - "webp/mux.h", - "webp/demux.h", - ] + _find_include_file(self, "webp/" + include) + for include in ("encode.h", "decode.h", "mux.h", "demux.h") ): - # In Google's precompiled zip it is call "libwebp": - if all( - _find_library_file(self, lib) - for lib in ["webp", "webpmux", "webpdemux"] - ): - feature.webp = "webp" - elif all( - _find_library_file(self, lib) - for lib in ["libwebp", "libwebpmux", "libwebpdemux"] - ): - feature.webp = "libwebp" + # In Google's precompiled zip it is called "libwebp" + for prefix in ("", "lib"): + if all( + _find_library_file(self, prefix + library) + for library in ("webp", "webpmux", "webpdemux") + ): + feature.webp = prefix + "webp" + break if feature.want("xcb"): _dbg("Looking for xcb") @@ -901,12 +893,8 @@ class pil_build_ext(build_ext): self._remove_extension("PIL._imagingcms") if feature.webp: - libs = [ - feature.webp, - feature.webp + "mux", - feature.webp + "demux", - ] - self._update_extension("PIL._webp", libs, []) + libs = [feature.webp, feature.webp + "mux", feature.webp + "demux"] + self._update_extension("PIL._webp", libs) else: self._remove_extension("PIL._webp") From c7e6289b36d4eb1823b6bfcace27674826a58e43 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:07:01 +1000 Subject: [PATCH 064/652] Use relative import --- src/PIL/features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index 9421dbd3c..e505adae0 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -7,9 +7,9 @@ import warnings from typing import IO import PIL -from PIL import _deprecate from . import Image +from ._deprecate import deprecate modules = { "pil": ("PIL._imaging", "PILLOW_VERSION"), @@ -149,7 +149,7 @@ def check_feature(feature: str) -> bool | None: try: imported_module = __import__(module, fromlist=["PIL"]) if isinstance(flag, bool): - _deprecate.deprecate(f'check_feature("{feature}")', 12) + deprecate(f'check_feature("{feature}")', 12) return flag return getattr(imported_module, flag) except ModuleNotFoundError: From ba82dff7bc6c489f299a63d2f2d3722dfe11852e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:10:54 +1000 Subject: [PATCH 065/652] Updated test name --- Tests/test_file_webp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 6e9eea4ae..6ccd489bb 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -166,7 +166,7 @@ class TestFileWebp: with pytest.raises(TypeError): _webp.WebPEncode() - def test_WebPDecode_with_invalid_args(self) -> None: + def test_WebPAnimDecoder_with_invalid_args(self) -> None: """ Calling decoder functions with no arguments should result in an error. """ From 66319fcce764f4f09e53900b2896ec15457f691f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:17:36 +1000 Subject: [PATCH 066/652] Animation support is no longer conditionally enabled --- src/_webp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_webp.c b/src/_webp.c index 306a290bd..d1943b3e0 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -11,7 +11,7 @@ * Check the versions from mux.h and demux.h, to ensure the WebPAnimEncoder and * WebPAnimDecoder APIs are present (initial support was added in 0.5.0). The * very early versions had some significant differences, so we require later - * versions, before enabling animation support. + * versions. */ #if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107 #error libwebp 0.5.0 and above is required. Upgrade libwebp or build Pillow with --disable-webp flag From 45552b5b4f6ea470639e5c7ff0d9efb96ad7db68 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:24:34 +1000 Subject: [PATCH 067/652] Updated documentation --- docs/deprecations.rst | 10 ++++++++++ docs/releasenotes/11.0.0.rst | 8 ++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 058468cfe..a9498d5ed 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -126,6 +126,16 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They have been deprecated, and will be removed in Pillow 12 (2025-10-15). +Specific WebP Feature Checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``features.check("transp_webp")``, ``features.check("webp_mux")`` and +``features.check("webp_anim")`` are now deprecated. They will always return +``True`` if the WebP module is installed, until they are removed in Pillow +12.0.0 (2025-10-15). + Removed features ---------------- diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index bb707a044..ac9237acf 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -61,10 +61,10 @@ have been deprecated, and will be removed in Pillow 12 (2025-10-15). Specific WebP Feature Checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The following features ``features.check("transp_webp")``, -``features.check("webp_mux")``, and ``features.check("webp_anim")`` are now -always ``True`` if the WebP module is installed and should not be used. -These checks will be removed in Pillow 12.0.0 (2025-10-15). +``features.check("transp_webp")``, ``features.check("webp_mux")`` and +``features.check("webp_anim")`` are now deprecated. They will always return +``True`` if the WebP module is installed, until they are removed in Pillow +12.0.0 (2025-10-15). API Changes =========== From 359d7592c7861729092434154d131310c7bf1d71 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Aug 2024 18:41:39 +1000 Subject: [PATCH 068/652] Test deprecation warnings --- Tests/test_features.py | 21 ++++++++++++++++----- src/PIL/features.py | 12 +++++++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index cb8fd8688..807782847 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -18,7 +18,11 @@ def test_check() -> None: for codec in features.codecs: assert features.check_codec(codec) == features.check(codec) for feature in features.features: - assert features.check_feature(feature) == features.check(feature) + if "webp" in feature: + with pytest.warns(DeprecationWarning): + assert features.check_feature(feature) == features.check(feature) + else: + assert features.check_feature(feature) == features.check(feature) def test_version() -> None: @@ -43,19 +47,26 @@ def test_version() -> None: for codec in features.codecs: test(codec, features.version_codec) for feature in features.features: - test(feature, features.version_feature) + if "webp" in feature: + with pytest.warns(DeprecationWarning): + test(feature, features.version_feature) + else: + test(feature, features.version_feature) def test_webp_transparency() -> None: - assert features.check("transp_webp") == features.check_module("webp") + with pytest.warns(DeprecationWarning): + assert features.check("transp_webp") == features.check_module("webp") def test_webp_mux() -> None: - assert features.check("webp_mux") == features.check_module("webp") + with pytest.warns(DeprecationWarning): + assert features.check("webp_mux") == features.check_module("webp") def test_webp_anim() -> None: - assert features.check("webp_anim") == features.check_module("webp") + with pytest.warns(DeprecationWarning): + assert features.check("webp_anim") == features.check_module("webp") @skip_unless_feature("libjpeg_turbo") diff --git a/src/PIL/features.py b/src/PIL/features.py index e505adae0..24c5ee978 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -180,7 +180,17 @@ def get_supported_features() -> list[str]: """ :returns: A list of all supported features. """ - return [f for f in features if check_feature(f)] + supported_features = [] + for f, (module, flag, _) in features.items(): + if flag is True: + for feature, (feature_module, _) in modules.items(): + if feature_module == module: + if check_module(feature): + supported_features.append(f) + break + elif check_feature(f): + supported_features.append(f) + return supported_features def check(feature: str) -> bool | None: From 4cf5688cfd42caed6f25db2d5ea29271ea8c1c9f Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 2 Jul 2024 16:43:34 +0400 Subject: [PATCH 069/652] Optimize getbbox() and getextrema() --- src/libImaging/GetBBox.c | 62 +++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index c61a27d3b..680d1cf77 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -30,26 +30,45 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { bbox[1] = -1; bbox[2] = bbox[3] = 0; -#define GETBBOX(image, mask) \ - for (y = 0; y < im->ysize; y++) { \ - has_data = 0; \ - for (x = 0; x < im->xsize; x++) { \ - if (im->image[y][x] & mask) { \ - has_data = 1; \ - if (x < bbox[0]) { \ - bbox[0] = x; \ - } \ - if (x >= bbox[2]) { \ - bbox[2] = x + 1; \ - } \ - } \ - } \ - if (has_data) { \ - if (bbox[1] < 0) { \ - bbox[1] = y; \ - } \ - bbox[3] = y + 1; \ - } \ +#define GETBBOX(image, mask) \ + /* first stage: looking for any px from top */ \ + for (y = 0; y < im->ysize; y++) { \ + has_data = 0; \ + for (x = 0; x < im->xsize; x++) \ + if (im->image[y][x] & mask) { \ + has_data = 1; \ + bbox[0] = x; \ + bbox[1] = y; \ + break; \ + } \ + if (has_data) break; \ + } \ + /* Check that we got a box */ \ + if (bbox[1] < 0) \ + return 0; /* no data */ \ + /* second stage: looking for any px from bottom */ \ + for (y = im->ysize - 1; y >= bbox[1]; y--) { \ + has_data = 0; \ + for (x = 0; x < im->xsize; x++) \ + if (im->image[y][x] & mask) { \ + has_data = 1; \ + bbox[3] = y + 1; \ + break; \ + } \ + if (has_data) break; \ + } \ + /* third stage: looking for left and right boundaries */ \ + for (y = bbox[1]; y < bbox[3]; y++) { \ + for (x = 0; x < bbox[0]; x++) \ + if (im->image[y][x] & mask) { \ + bbox[0] = x; \ + break; \ + } \ + for (x = im->xsize - 1; x >= bbox[2]; x--) \ + if (im->image[y][x] & mask) { \ + bbox[2] = x + 1; \ + break; \ + } \ } if (im->image8) { @@ -144,6 +163,9 @@ ImagingGetExtrema(Imaging im, void *extrema) { imax = in[x]; } } + if (imin == 0 && imax == 255) { + break; + } } ((UINT8 *)extrema)[0] = (UINT8)imin; ((UINT8 *)extrema)[1] = (UINT8)imax; From 6a87df2c1fdd0dea1a3bdd5fb85e388e535e063b Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 2 Jul 2024 18:40:19 +0400 Subject: [PATCH 070/652] clang-format --- src/libImaging/GetBBox.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 680d1cf77..46a678078 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -30,7 +30,7 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { bbox[1] = -1; bbox[2] = bbox[3] = 0; -#define GETBBOX(image, mask) \ +#define GETBBOX(image, mask) \ /* first stage: looking for any px from top */ \ for (y = 0; y < im->ysize; y++) { \ has_data = 0; \ @@ -41,7 +41,8 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { bbox[1] = y; \ break; \ } \ - if (has_data) break; \ + if (has_data) \ + break; \ } \ /* Check that we got a box */ \ if (bbox[1] < 0) \ @@ -55,7 +56,8 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { bbox[3] = y + 1; \ break; \ } \ - if (has_data) break; \ + if (has_data) \ + break; \ } \ /* third stage: looking for left and right boundaries */ \ for (y = bbox[1]; y < bbox[3]; y++) { \ From 5fb44ab694bc5576c6cd7e7ee54dfce660f497af Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 2 Jul 2024 18:45:13 +0400 Subject: [PATCH 071/652] This check is useless, since moved after the first stage --- src/libImaging/GetBBox.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 46a678078..2e8afc8d5 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -92,11 +92,6 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { GETBBOX(image32, mask); } - /* Check that we got a box */ - if (bbox[1] < 0) { - return 0; /* no data */ - } - return 1; /* ok */ } From 8256b9bb7fd22abfe61ed0013c02fa18255f398f Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 2 Jul 2024 19:05:24 +0400 Subject: [PATCH 072/652] Correct left boundary on the second stage --- src/libImaging/GetBBox.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 2e8afc8d5..50b2253a0 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -53,6 +53,8 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { for (x = 0; x < im->xsize; x++) \ if (im->image[y][x] & mask) { \ has_data = 1; \ + if (x < bbox[0]) \ + bbox[0] = x; \ bbox[3] = y + 1; \ break; \ } \ From 2b6c5a27a1a6813fef4f06d45cf63428b2ae2da6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jul 2024 22:42:52 +1000 Subject: [PATCH 073/652] Added braces --- src/libImaging/GetBBox.c | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 50b2253a0..1303617ad 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -34,45 +34,53 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { /* first stage: looking for any px from top */ \ for (y = 0; y < im->ysize; y++) { \ has_data = 0; \ - for (x = 0; x < im->xsize; x++) \ + for (x = 0; x < im->xsize; x++) { \ if (im->image[y][x] & mask) { \ has_data = 1; \ bbox[0] = x; \ bbox[1] = y; \ break; \ } \ - if (has_data) \ + } \ + if (has_data) { \ break; \ + } \ } \ /* Check that we got a box */ \ - if (bbox[1] < 0) \ + if (bbox[1] < 0) { \ return 0; /* no data */ \ + } \ /* second stage: looking for any px from bottom */ \ for (y = im->ysize - 1; y >= bbox[1]; y--) { \ has_data = 0; \ - for (x = 0; x < im->xsize; x++) \ + for (x = 0; x < im->xsize; x++) { \ if (im->image[y][x] & mask) { \ has_data = 1; \ - if (x < bbox[0]) \ + if (x < bbox[0]) { \ bbox[0] = x; \ + } \ bbox[3] = y + 1; \ break; \ } \ - if (has_data) \ + } \ + if (has_data) { \ break; \ + } \ } \ /* third stage: looking for left and right boundaries */ \ for (y = bbox[1]; y < bbox[3]; y++) { \ - for (x = 0; x < bbox[0]; x++) \ + for (x = 0; x < bbox[0]; x++) { \ if (im->image[y][x] & mask) { \ bbox[0] = x; \ break; \ } \ - for (x = im->xsize - 1; x >= bbox[2]; x--) \ + } \ + for (x = im->xsize - 1; x >= bbox[2]; x--) { \ if (im->image[y][x] & mask) { \ bbox[2] = x + 1; \ break; \ } \ + } \ } if (im->image8) { From e4e2cd65642fa3dc02793fe426ba22a00d163db9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jul 2024 22:45:10 +1000 Subject: [PATCH 074/652] Updated comments --- src/libImaging/GetBBox.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 1303617ad..d430893dd 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -31,7 +31,7 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { bbox[2] = bbox[3] = 0; #define GETBBOX(image, mask) \ - /* first stage: looking for any px from top */ \ + /* first stage: looking for any pixels from top */ \ for (y = 0; y < im->ysize; y++) { \ has_data = 0; \ for (x = 0; x < im->xsize; x++) { \ @@ -46,11 +46,11 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { break; \ } \ } \ - /* Check that we got a box */ \ + /* Check that we have a box */ \ if (bbox[1] < 0) { \ return 0; /* no data */ \ } \ - /* second stage: looking for any px from bottom */ \ + /* second stage: looking for any pixels from bottom */ \ for (y = im->ysize - 1; y >= bbox[1]; y--) { \ has_data = 0; \ for (x = 0; x < im->xsize; x++) { \ From b14142462e1d4f0205b7e77c647a02c760b99a05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 05:11:00 +1000 Subject: [PATCH 075/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ae995e0a0..25dcb1887 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Remove WebP support without anim, mux/demux, and with buggy alpha #8213 + [homm, radarhere] + +- Add missing TIFF CMYK;16B reader #8298 + [homm] + - Remove all WITH_* flags from _imaging.c and other flags #8211 [homm] From 873770978197484b284a267d247d1b832afc2a24 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Aug 2024 13:32:59 +1000 Subject: [PATCH 076/652] Added return type to ImageFile.load() --- src/PIL/Image.py | 8 +++----- src/PIL/ImageFile.py | 19 ++++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ebf4f46c4..a6eefff56 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -218,9 +218,10 @@ if hasattr(core, "DEFAULT_STRATEGY"): # Registries if TYPE_CHECKING: + import mmap from xml.etree.ElementTree import Element - from . import ImageFile, ImagePalette, TiffImagePlugin + from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] OPEN: dict[ @@ -612,7 +613,7 @@ class Image: logger.debug("Error closing: %s", msg) if getattr(self, "map", None): - self.map = None + self.map: mmap.mmap | None = None # Instead of simply setting to None, we're setting up a # deferred error that will better explain that the core image @@ -1336,9 +1337,6 @@ class Image: self.load() return self._new(self.im.expand(xmargin, ymargin)) - if TYPE_CHECKING: - from . import ImageFilter - def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: """ Filters this image using the given filter. For a list of diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 2a8846c1a..829082e94 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -174,7 +174,7 @@ class ImageFile(Image.Image): self.fp.close() self.fp = None - def load(self): + def load(self) -> Image.core.PixelAccess | None: """Load image data based on tile list""" if self.tile is None: @@ -185,7 +185,7 @@ class ImageFile(Image.Image): if not self.tile: return pixel - self.map = None + self.map: mmap.mmap | None = None use_mmap = self.filename and len(self.tile) == 1 # As of pypy 2.1.0, memory mapping was failing here. use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") @@ -193,17 +193,17 @@ class ImageFile(Image.Image): readonly = 0 # look for read/seek overrides - try: + if hasattr(self, "load_read"): read = self.load_read # don't use mmap if there are custom read/seek functions use_mmap = False - except AttributeError: + else: read = self.fp.read - try: + if hasattr(self, "load_seek"): seek = self.load_seek use_mmap = False - except AttributeError: + else: seek = self.fp.seek if use_mmap: @@ -243,11 +243,8 @@ class ImageFile(Image.Image): # sort tiles in file order self.tile.sort(key=_tilesort) - try: - # FIXME: This is a hack to handle TIFF's JpegTables tag. - prefix = self.tile_prefix - except AttributeError: - prefix = b"" + # FIXME: This is a hack to handle TIFF's JpegTables tag. + prefix = getattr(self, "tile_prefix", b"") # Remove consecutive duplicates that only differ by their offset self.tile = [ From 497080f63b62c9acbb427c7e554f222de70fe3a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Aug 2024 15:20:34 +1000 Subject: [PATCH 077/652] Added type hint to ImageFile._save tile parameter --- Tests/test_imagefile.py | 30 ++++++++++++++++++++++++------ src/PIL/BlpImagePlugin.py | 2 +- src/PIL/BmpImagePlugin.py | 4 +++- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/GifImagePlugin.py | 8 ++++++-- src/PIL/IcoImagePlugin.py | 4 +++- src/PIL/ImImagePlugin.py | 4 +++- src/PIL/ImageFile.py | 4 ++-- src/PIL/Jpeg2KImagePlugin.py | 2 +- src/PIL/JpegImagePlugin.py | 4 +++- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PalmImagePlugin.py | 4 +++- src/PIL/PcxImagePlugin.py | 4 +++- src/PIL/PdfImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 8 ++++---- src/PIL/PpmImagePlugin.py | 4 +++- src/PIL/SpiderImagePlugin.py | 4 +++- src/PIL/TgaImagePlugin.py | 8 ++++++-- src/PIL/XbmImagePlugin.py | 2 +- 19 files changed, 72 insertions(+), 30 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index fe7d44785..95e91db83 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -317,7 +317,13 @@ class TestPyEncoder(CodecsTest): fp = BytesIO() ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] + im, + fp, + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB" + ) + ], ) assert MockPyEncoder.last @@ -333,7 +339,7 @@ class TestPyEncoder(CodecsTest): im.tile = [("MOCK", None, 32, None)] fp = BytesIO() - ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")]) assert MockPyEncoder.last assert MockPyEncoder.last.state.xoff == 0 @@ -350,7 +356,9 @@ class TestPyEncoder(CodecsTest): MockPyEncoder.last = None with pytest.raises(ValueError): ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] + im, + fp, + [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")], ) last: MockPyEncoder | None = MockPyEncoder.last assert last @@ -358,7 +366,9 @@ class TestPyEncoder(CodecsTest): with pytest.raises(ValueError): ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] + im, + fp, + [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")], ) def test_oversize(self) -> None: @@ -371,14 +381,22 @@ class TestPyEncoder(CodecsTest): ImageFile._save( im, fp, - [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB" + ) + ], ) with pytest.raises(ValueError): ImageFile._save( im, fp, - [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB" + ) + ], ) def test_encode(self) -> None: diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 6d71049a9..569f2c9bf 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -477,7 +477,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(struct.pack(" None: if bits != 32: and_mask = Image.new("1", size) ImageFile._save( - and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] + and_mask, + image_io, + [ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))], ) else: frame.save(image_io, "png") diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 2fb7ecd52..b94165089 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -360,7 +360,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: palette += im_palette[colors * i : colors * (i + 1)] palette += b"\x00" * (256 - colors) fp.write(palette) # 768 bytes - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))] + ) # diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 829082e94..73554fa53 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -93,7 +93,7 @@ def _tilesort(t: _Tile) -> int: class _Tile(NamedTuple): codec_name: str - extents: tuple[int, int, int, int] + extents: tuple[int, int, int, int] | None offset: int args: tuple[Any, ...] | str | None @@ -522,7 +522,7 @@ class Parser: # -------------------------------------------------------------------- -def _save(im: Image.Image, fp: IO[bytes], tile, bufsize: int = 0) -> None: +def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None: """Helper to save image based on tile list :param im: Image object. diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index ef9107f00..02c2e48cf 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -419,7 +419,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: plt, ) - ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) + ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)]) # ------------------------------------------------------------ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index fc897e2b9..bd4539be4 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -826,7 +826,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Ensure that our buffer is big enough. Same with the icc_profile block. bufsize = max(bufsize, len(exif) + 5, len(extra) + 1) - ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) + ImageFile._save( + im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize + ) def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 0a75c868b..40e5fa435 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(o16(h)) # image body - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) # diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 1735070f8..62bf5f542 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -213,7 +213,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ) # now convert data to raw form - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))] + ) if hasattr(fp, "flush"): fp.flush() diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index dd42003b5..4fb04715b 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -198,7 +198,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: assert fp.tell() == 128 - ImageFile._save(im, fp, [("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))]) + ImageFile._save( + im, fp, [ImageFile._Tile("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))] + ) if im.mode == "P": # colour palette diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 7fc1108bb..e9c20ddc1 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -138,7 +138,7 @@ def _write_image( op = io.BytesIO() if decode_filter == "ASCIIHexDecode": - ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) + ImageFile._save(im, op, [ImageFile._Tile("hex", (0, 0) + im.size, 0, im.mode)]) elif decode_filter == "CCITTFaxDecode": im.save( op, diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 910fa9755..fc20b18a8 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1226,7 +1226,7 @@ def _write_multiple_frames( ImageFile._save( im, cast(IO[bytes], _idat(fp, chunk)), - [("zip", (0, 0) + im.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)], ) seq_num = 0 @@ -1263,14 +1263,14 @@ def _write_multiple_frames( ImageFile._save( im_frame, cast(IO[bytes], _idat(fp, chunk)), - [("zip", (0, 0) + im_frame.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], ) else: fdat_chunks = _fdat(fp, chunk, seq_num) ImageFile._save( im_frame, cast(IO[bytes], fdat_chunks), - [("zip", (0, 0) + im_frame.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], ) seq_num = fdat_chunks.seq_num return None @@ -1471,7 +1471,7 @@ def _save( ImageFile._save( single_im, cast(IO[bytes], _idat(fp, chunk)), - [("zip", (0, 0) + single_im.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)], ) if info: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 16c9ccbba..7bdaa9fe7 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -353,7 +353,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: elif head == b"Pf": fp.write(b"-1.0\n") row_order = -1 if im.mode == "F" else 1 - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))] + ) # diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index a07101e54..7045ab566 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -278,7 +278,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.writelines(hdr) rawmode = "F;32NF" # 32-bit native floating point - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] + ) def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 39104aece..a43aae1ec 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -238,11 +238,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if rle: ImageFile._save( - im, fp, [("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))] + im, + fp, + [ImageFile._Tile("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))], ) else: ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))] + im, + fp, + [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))], ) # write targa version 2 footer diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 6d11bbfcf..6c2e32804 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -85,7 +85,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [("xbm", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0, None)]) fp.write(b"};\n") From 8afb7ddb4ed265caf67213f4fd5416cd29ae24a1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 08:08:43 +1000 Subject: [PATCH 078/652] Added type hints --- .ci/requirements-mypy.txt | 1 + docs/example/DdsImagePlugin.py | 29 +++++++++++++++------------ src/PIL/TiffImagePlugin.py | 36 ++++++++++++++++++++-------------- tox.ini | 2 +- winbuild/build_prepare.py | 12 ++++++------ 5 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 23792281b..47fc64399 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -5,6 +5,7 @@ ipython numpy packaging pytest +sphinx types-defusedxml types-olefile types-setuptools diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 2a2a0ba29..caa852b1f 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -14,6 +14,7 @@ from __future__ import annotations import struct from io import BytesIO +from typing import IO from PIL import Image, ImageFile @@ -94,26 +95,26 @@ DXT3_FOURCC = 0x33545844 DXT5_FOURCC = 0x35545844 -def _decode565(bits): +def _decode565(bits: int) -> tuple[int, int, int]: a = ((bits >> 11) & 0x1F) << 3 b = ((bits >> 5) & 0x3F) << 2 c = (bits & 0x1F) << 3 return a, b, c -def _c2a(a, b): +def _c2a(a: int, b: int) -> int: return (2 * a + b) // 3 -def _c2b(a, b): +def _c2b(a: int, b: int) -> int: return (a + b) // 2 -def _c3(a, b): +def _c3(a: int, b: int) -> int: return (2 * b + a) // 3 -def _dxt1(data, width, height): +def _dxt1(data: IO[bytes], width: int, height: int) -> bytes: # TODO implement this function as pixel format in decode.c ret = bytearray(4 * width * height) @@ -151,7 +152,7 @@ def _dxt1(data, width, height): return bytes(ret) -def _dxtc_alpha(a0, a1, ac0, ac1, ai): +def _dxtc_alpha(a0: int, a1: int, ac0: int, ac1: int, ai: int) -> int: if ai <= 12: ac = (ac0 >> ai) & 7 elif ai == 15: @@ -175,7 +176,7 @@ def _dxtc_alpha(a0, a1, ac0, ac1, ai): return alpha -def _dxt5(data, width, height): +def _dxt5(data: IO[bytes], width: int, height: int) -> bytes: # TODO implement this function as pixel format in decode.c ret = bytearray(4 * width * height) @@ -211,7 +212,7 @@ class DdsImageFile(ImageFile.ImageFile): format = "DDS" format_description = "DirectDraw Surface" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(4)): msg = "not a DDS file" raise SyntaxError(msg) @@ -242,19 +243,20 @@ class DdsImageFile(ImageFile.ImageFile): elif fourcc == b"DXT5": self.decoder = "DXT5" else: - msg = f"Unimplemented pixel format {fourcc}" + msg = f"Unimplemented pixel format {repr(fourcc)}" raise NotImplementedError(msg) self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: pass class DXT1Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None try: self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) except struct.error as e: @@ -266,7 +268,8 @@ class DXT1Decoder(ImageFile.PyDecoder): class DXT5Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None try: self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) except struct.error as e: @@ -279,7 +282,7 @@ Image.register_decoder("DXT1", DXT1Decoder) Image.register_decoder("DXT5", DXT5Decoder) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"DDS " diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index deae199d5..2ce8a6c2a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -456,8 +456,11 @@ class IFDRational(Rational): __int__ = _delegate("__int__") -def _register_loader(idx: int, size: int): - def decorator(func): +_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any] + + +def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]: + def decorator(func: _LoaderFunc) -> _LoaderFunc: from .TiffTags import TYPES if func.__name__.startswith("load_"): @@ -482,12 +485,13 @@ def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None: idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize(f"={fmt}") - _load_dispatch[idx] = ( # noqa: F821 - size, - lambda self, data, legacy_api=True: ( - self._unpack(f"{len(data) // size}{fmt}", data) - ), - ) + + def basic_handler( + self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True + ) -> tuple[Any, ...]: + return self._unpack(f"{len(data) // size}{fmt}", data) + + _load_dispatch[idx] = size, basic_handler # noqa: F821 _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 b"".join(self._pack(fmt, value) for value in values) ) @@ -560,7 +564,7 @@ class ImageFileDirectory_v2(_IFDv2Base): """ - _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} + _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {} _write_dispatch: dict[int, Callable[..., Any]] = {} def __init__( @@ -653,10 +657,10 @@ class ImageFileDirectory_v2(_IFDv2Base): def __contains__(self, tag: object) -> bool: return tag in self._tags_v2 or tag in self._tagdata - def __setitem__(self, tag: int, value) -> None: + def __setitem__(self, tag: int, value: Any) -> None: self._setitem(tag, value, self.legacy_api) - def _setitem(self, tag: int, value, legacy_api: bool) -> None: + def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None: basetypes = (Number, bytes, str) info = TiffTags.lookup(tag, self.group) @@ -744,10 +748,10 @@ class ImageFileDirectory_v2(_IFDv2Base): def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v2)) - def _unpack(self, fmt: str, data: bytes): + def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]: return struct.unpack(self._endian + fmt, data) - def _pack(self, fmt: str, *values) -> bytes: + def _pack(self, fmt: str, *values: Any) -> bytes: return struct.pack(self._endian + fmt, *values) list( @@ -824,7 +828,9 @@ class ImageFileDirectory_v2(_IFDv2Base): return value @_register_loader(10, 8) - def load_signed_rational(self, data: bytes, legacy_api: bool = True): + def load_signed_rational( + self, data: bytes, legacy_api: bool = True + ) -> tuple[tuple[int, int] | IFDRational, ...]: vals = self._unpack(f"{len(data) // 4}l", data) def combine(a: int, b: int) -> tuple[int, int] | IFDRational: @@ -1088,7 +1094,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v1)) - def __setitem__(self, tag: int, value) -> None: + def __setitem__(self, tag: int, value: Any) -> None: for legacy_api in (False, True): self._setitem(tag, value, legacy_api) diff --git a/tox.ini b/tox.ini index 4b4059455..70b8bf145 100644 --- a/tox.ini +++ b/tox.ini @@ -36,4 +36,4 @@ deps = extras = typing commands = - mypy src Tests {posargs} + mypy docs src winbuild Tests {posargs} diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 7129699eb..1021e4f22 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,7 @@ import re import shutil import struct import subprocess +from typing import Any def cmd_cd(path: str) -> str: @@ -43,21 +44,19 @@ def cmd_nmake( target: str = "", params: list[str] | None = None, ) -> str: - params = "" if params is None else " ".join(params) - return " ".join( [ "{nmake}", "-nologo", f'-f "{makefile}"' if makefile is not None else "", - f"{params}", + f'{" ".join(params)}' if params is not None else "", f'"{target}"', ] ) def cmds_cmake( - target: str | tuple[str, ...] | list[str], *params, build_dir: str = "." + target: str | tuple[str, ...] | list[str], *params: str, build_dir: str = "." ) -> list[str]: if not isinstance(target, str): target = " ".join(target) @@ -129,7 +128,7 @@ V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") # dependencies, listed in order of compilation -DEPS = { +DEPS: dict[str, dict[str, Any]] = { "libjpeg": { "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz/download", @@ -538,7 +537,7 @@ def write_script( print(" " + line) -def get_footer(dep: dict) -> list[str]: +def get_footer(dep: dict[str, Any]) -> list[str]: lines = [] for out in dep.get("headers", []): lines.append(cmd_copy(out, "{inc_dir}")) @@ -583,6 +582,7 @@ def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str: license_text += f.read() if "license_pattern" in dep: match = re.search(dep["license_pattern"], license_text, re.DOTALL) + assert match is not None license_text = "\n".join(match.groups()) assert len(license_text) > 50 with open(os.path.join(license_dir, f"{directory}.txt"), "w") as f: From d4c72da6b2ae5aabc45f679f8feeaf2726976a2b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 14:10:31 +1000 Subject: [PATCH 079/652] Added type hints to example code --- docs/handbook/image-file-formats.rst | 12 +++++++----- docs/handbook/tutorial.rst | 6 +++--- docs/handbook/writing-your-own-image-plugin.rst | 6 +++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 861e09a43..ca0e05eb6 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1517,19 +1517,21 @@ To add other read or write support, use :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF handler. :: - from PIL import Image + from typing import IO + + from PIL import Image, ImageFile from PIL import WmfImagePlugin - class WmfHandler: - def open(self, im): + class WmfHandler(ImageFile.StubHandler): + def open(self, im: ImageFile.StubImageFile) -> None: ... - def load(self, im): + def load(self, im: ImageFile.StubImageFile) -> Image.Image: ... return image - def save(self, im, fp, filename): + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: ... diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index c36011362..3df8e0d20 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -186,7 +186,7 @@ Rolling an image :: - def roll(im, delta): + def roll(im: Image.Image, delta: int) -> Image.Image: """Roll an image sideways.""" xsize, ysize = im.size @@ -211,7 +211,7 @@ Merging images :: - def merge(im1, im2): + def merge(im1: Image.Image, im2: Image.Image) -> Image.Image: w = im1.size[0] + im2.size[0] h = max(im1.size[1], im2.size[1]) im = Image.new("RGBA", (w, h)) @@ -704,7 +704,7 @@ in the current directory can be saved as JPEGs at reduced quality. import glob from PIL import Image - def compress_image(source_path, dest_path): + def compress_image(source_path: str, dest_path: str) -> None: with Image.open(source_path) as img: if img.mode != "RGB": img = img.convert("RGB") diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 956d63aa7..2e853224d 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -53,7 +53,7 @@ true color. from PIL import Image, ImageFile - def _accept(prefix): + def _accept(prefix: bytes) -> bool: return prefix[:4] == b"SPAM" @@ -62,7 +62,7 @@ true color. format = "SPAM" format_description = "Spam raster image" - def _open(self): + def _open(self) -> None: header = self.fp.read(128).split() @@ -82,7 +82,7 @@ true color. raise SyntaxError(msg) # data descriptor - self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] Image.register_open(SpamImageFile.format, SpamImageFile, _accept) From d5e3f6b51673564c16a8973a72a2b68e8ba96f0c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 14:46:23 +1000 Subject: [PATCH 080/652] If left and right sides meet, do not draw rectangle to fill gap --- ...d_rectangle_joined_x_different_corners.png | Bin 0 -> 411 bytes Tests/test_imagedraw.py | 21 ++++++++++++++++++ src/PIL/ImageDraw.py | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png diff --git a/Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png b/Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png new file mode 100644 index 0000000000000000000000000000000000000000..b225afc2dc1449375e748d59e2336eedd9ddceaf GIT binary patch literal 411 zcmeAS@N?(olHy`uVBq!ia0vp^DIm None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.rounded_rectangle( + (20, 10, 80, 90), + 30, + fill="red", + outline="green", + width=5, + corners=(True, False, False, False), + ) + + # Assert + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png" + ) + + @pytest.mark.parametrize( "xy, radius, type", [ diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 6f56d0236..5609bf971 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -505,7 +505,7 @@ class ImageDraw: if full_x: self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) - else: + elif x1 - r - 1 > x0 + r + 1: self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) if not full_x and not full_y: left = [x0, y0, x0 + r, y1] From 0a03b77daf088e2aac0e32acbbc370704c749591 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 16 Aug 2024 06:36:31 +1000 Subject: [PATCH 081/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 25dcb1887..a7edc340c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304 + [radarhere] + - Remove WebP support without anim, mux/demux, and with buggy alpha #8213 [homm, radarhere] From 617699ffc76230d481d903c7a9b000d96b7e89e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 16 Aug 2024 12:03:02 +1000 Subject: [PATCH 082/652] Log value from tag_v2 --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2ce8a6c2a..7bbbc832e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1393,7 +1393,7 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("- photometric_interpretation: %s", photo) logger.debug("- planar_configuration: %s", self._planar_configuration) logger.debug("- fill_order: %s", fillorder) - logger.debug("- YCbCr subsampling: %s", self.tag.get(YCBCRSUBSAMPLING)) + logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) # size xsize = int(self.tag_v2.get(IMAGEWIDTH)) From 5cb79c6a69b7d9b56607b7b7499b03c1aa434a35 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 16 Aug 2024 18:31:07 +1000 Subject: [PATCH 083/652] Check object is bytes --- Tests/test_imagefont.py | 3 +++ src/_imagingft.c | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 340cc4742..953706010 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1113,6 +1113,9 @@ def test_bytes(font: ImageFont.FreeTypeFont) -> None: ) assert font.getmask2(b"test")[1] == font.getmask2("test")[1] + with pytest.raises(TypeError): + font.getlength((0, 0)) # type: ignore[arg-type] + @pytest.mark.parametrize( "test_file", diff --git a/src/_imagingft.c b/src/_imagingft.c index f8143e0cc..5c446e641 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -272,7 +272,7 @@ text_layout_raqm( } set_text = raqm_set_text(rq, text, size); PyMem_Free(text); - } else { + } else if (PyBytes_Check(string)) { char *buffer; PyBytes_AsStringAndSize(string, &buffer, &size); if (!buffer || !size) { @@ -281,6 +281,9 @@ text_layout_raqm( goto failed; } set_text = raqm_set_text_utf8(rq, buffer, size); + } else { + PyErr_SetString(PyExc_TypeError, "expected string or bytes"); + goto failed; } if (!set_text) { PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); @@ -425,8 +428,11 @@ text_layout_fallback( if (PyUnicode_Check(string)) { count = PyUnicode_GET_LENGTH(string); - } else { + } else if (PyBytes_Check(string)) { PyBytes_AsStringAndSize(string, &buffer, &count); + } else { + PyErr_SetString(PyExc_TypeError, "expected string or bytes"); + return 0; } if (count == 0) { return 0; From 6f506d2ae3879256e8a893233fc28ba6fb2518d8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:59:46 +0300 Subject: [PATCH 084/652] Run all flake8-pytest-style except rules some that fail --- .pre-commit-config.yaml | 6 +++--- pyproject.toml | 10 +++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74764752e..8ab478e84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.6 + rev: v0.6.0 hooks: - id: ruff args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black @@ -67,7 +67,7 @@ repos: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.18 + rev: v0.19 hooks: - id: validate-pyproject diff --git a/pyproject.toml b/pyproject.toml index 8bb21019c..d4b83ef7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,7 @@ lint.select = [ "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging "PGH", # pygrep-hooks - "PT006", # pytest-parametrize-names-wrong-type + "PT", # flake8-pytest-style "PYI", # flake8-pyi "RUF100", # unused noqa (yesqa) "UP", # pyupgrade @@ -121,6 +121,14 @@ lint.ignore = [ "E221", # Multiple spaces before operator "E226", # Missing whitespace around arithmetic operator "E241", # Multiple spaces after ',' + "PT001", # pytest-fixture-incorrect-parentheses-style + "PT007", # pytest-parametrize-values-wrong-type + "PT011", # pytest-raises-too-broad + "PT012", # pytest-raises-with-multiple-statements + "PT014", # pytest-duplicate-parametrize-test-cases + "PT016", # pytest-fail-without-message + "PT017", # pytest-assert-in-except + "PT018", # pytest-composite-assertion "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI034", # flake8-pyi: typing.Self added in Python 3.11 ] From 5c282d02991e6bca1993c974d85dd19bce734ddd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:47:11 +0300 Subject: [PATCH 085/652] Fix PT014: duplicate test cases in pytest.mark.parametrize --- Tests/test_file_jpeg2k.py | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a5cfa7c6c..5e11465ca 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -233,7 +233,7 @@ def test_layers() -> None: ("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"), ("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"), ("foo.jp2", {"no_jp2": False}, 4, b"jP"), - ("foo.jp2", {"no_jp2": False}, 4, b"jP"), + (None, {"no_jp2": False}, 4, b"jP"), ), ) def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: diff --git a/pyproject.toml b/pyproject.toml index d4b83ef7f..b42ad7424 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,7 +125,6 @@ lint.ignore = [ "PT007", # pytest-parametrize-values-wrong-type "PT011", # pytest-raises-too-broad "PT012", # pytest-raises-with-multiple-statements - "PT014", # pytest-duplicate-parametrize-test-cases "PT016", # pytest-fail-without-message "PT017", # pytest-assert-in-except "PT018", # pytest-composite-assertion From 5747267eb39961101d8ed6f18e77cefc1fc5c4b8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:52:56 +0300 Subject: [PATCH 086/652] Fix PT018: Assert only one thing --- Tests/test_file_jpeg.py | 9 ++++++--- Tests/test_imagechops.py | 3 ++- Tests/test_imagemorph.py | 3 ++- Tests/test_tiff_ifdrational.py | 4 ++-- pyproject.toml | 1 - 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index df589e24a..8e6221750 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1019,13 +1019,16 @@ class TestFileJpeg: # SOI, EOI for marker in b"\xff\xd8", b"\xff\xd9": - assert marker in data[1] and marker in data[2] + assert marker in data[1] + assert marker in data[2] # DHT, DQT for marker in b"\xff\xc4", b"\xff\xdb": - assert marker in data[1] and marker not in data[2] + assert marker in data[1] + assert marker not in data[2] # SOF0, SOS, APP0 (JFIF header) for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": - assert marker not in data[1] and marker in data[2] + assert marker not in data[1] + assert marker in data[2] with Image.open(BytesIO(data[0])) as interchange_im: with Image.open(BytesIO(data[1] + data[2])) as combined_im: diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 4fc28cdb9..4309214f5 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -398,7 +398,8 @@ def test_logical() -> None: for y in (a, b): imy = Image.new("1", (1, 1), y) value = op(imx, imy).getpixel((0, 0)) - assert not isinstance(value, tuple) and value is not None + assert not isinstance(value, tuple) + assert value is not None out.append(value) return out diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 4363f456e..80d8c3815 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -46,7 +46,8 @@ def img_to_string(im: Image.Image) -> str: line = "" for c in range(im.width): value = im.getpixel((c, r)) - assert not isinstance(value, tuple) and value is not None + assert not isinstance(value, tuple) + assert value is not None line += chars[value > 0] result.append(line) return "\n".join(result) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 9d06a9332..13f1f9c80 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -54,8 +54,8 @@ def test_nonetype() -> None: assert xres.denominator is not None assert yres._val is not None - assert xres and 1 - assert xres and yres + assert xres + assert yres @pytest.mark.parametrize( diff --git a/pyproject.toml b/pyproject.toml index b42ad7424..9e53d4bfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,6 @@ lint.ignore = [ "PT012", # pytest-raises-with-multiple-statements "PT016", # pytest-fail-without-message "PT017", # pytest-assert-in-except - "PT018", # pytest-composite-assertion "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI034", # flake8-pyi: typing.Self added in Python 3.11 ] From f26b47595b10eaf3d781a8ea3f357f78f0a2774c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2024 06:56:31 +1000 Subject: [PATCH 087/652] Get IPTC info from tag_v2 --- Tests/test_file_iptc.py | 10 ++++++++++ src/PIL/IptcImagePlugin.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index b0ea2bf42..8a7c59fb1 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -77,6 +77,16 @@ def test_getiptcinfo_zero_padding() -> None: assert len(iptc) == 3 +def test_getiptcinfo_tiff() -> None: + # Arrange + with Image.open("Tests/images/hopper.Lab.tif") as im: + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + assert iptc == {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"} + + def test_getiptcinfo_tiff_none() -> None: # Arrange with Image.open("Tests/images/hopper.tif") as im: diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 17243e705..153243519 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -213,7 +213,7 @@ def getiptcinfo( # get raw data from the IPTC/NAA tag (PhotoShop tags the data # as 4-byte integers, so we cannot use the get method...) try: - data = im.tag.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] + data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK] except (AttributeError, KeyError): pass From cd76b4853319619a0cb9726dcd7907551794f049 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Fri, 16 Aug 2024 09:08:18 -0500 Subject: [PATCH 088/652] move repeated code to private helper function --- src/PIL/TiffImagePlugin.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index deae199d5..7dda413e6 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2080,38 +2080,34 @@ class AppendingTiffWriter: (value,) = struct.unpack(self.longFmt, self.f.read(4)) return value + @staticmethod + def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: + if bytes_written is not None and bytes_written != expected: + msg = f"wrote only {bytes_written} bytes but wanted {expected}" + raise RuntimeError(msg) + def rewriteLastShortToLong(self, value: int) -> None: self.f.seek(-2, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 4) def rewriteLastShort(self, value: int) -> None: self.f.seek(-2, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - if bytes_written is not None and bytes_written != 2: - msg = f"wrote only {bytes_written} bytes but wanted 2" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 2) def rewriteLastLong(self, value: int) -> None: self.f.seek(-4, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 4) def writeShort(self, value: int) -> None: bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - if bytes_written is not None and bytes_written != 2: - msg = f"wrote only {bytes_written} bytes but wanted 2" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 2) def writeLong(self, value: int) -> None: bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 4) def close(self) -> None: self.finalize() From 2467369c134a882473e46928eac952d7fab5f7a4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2024 20:50:45 +1000 Subject: [PATCH 089/652] Uninstall gradle and maven on macOS 13 --- .github/workflows/macos-install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index d35cfcd31..25943776f 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,6 +2,9 @@ set -e +if [[ "$ImageOS" == "macos13" ]]; then + brew uninstall gradle maven +fi brew install \ freetype \ ghostscript \ From d1d567bb59886f15e91522d937e5399395158692 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2024 08:47:35 +1000 Subject: [PATCH 090/652] Do not use sys.stdout in PSDraw --- Tests/test_psdraw.py | 11 +++-------- src/PIL/PSDraw.py | 11 ++++------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index c3afa9089..a743d831f 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -5,8 +5,6 @@ import sys from io import BytesIO from pathlib import Path -import pytest - from PIL import Image, PSDraw @@ -49,15 +47,14 @@ def test_draw_postscript(tmp_path: Path) -> None: assert os.path.getsize(tempfile) > 0 -@pytest.mark.parametrize("buffer", (True, False)) -def test_stdout(buffer: bool) -> None: +def test_stdout() -> None: # Temporarily redirect stdout old_stdout = sys.stdout class MyStdOut: buffer = BytesIO() - mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() + mystdout = MyStdOut() sys.stdout = mystdout @@ -67,6 +64,4 @@ def test_stdout(buffer: bool) -> None: # Reset stdout sys.stdout = old_stdout - if isinstance(mystdout, MyStdOut): - mystdout = mystdout.buffer - assert mystdout.getvalue() != b"" + assert mystdout.buffer.getvalue() != b"" diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 673eae1d1..02939d26b 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -17,7 +17,7 @@ from __future__ import annotations import sys -from typing import TYPE_CHECKING +from typing import IO, TYPE_CHECKING from . import EpsImagePlugin @@ -28,15 +28,12 @@ from . import EpsImagePlugin class PSDraw: """ Sets up printing to the given file. If ``fp`` is omitted, - ``sys.stdout.buffer`` or ``sys.stdout`` is assumed. + ``sys.stdout.buffer`` is assumed. """ - def __init__(self, fp=None): + def __init__(self, fp: IO[bytes] | None = None) -> None: if not fp: - try: - fp = sys.stdout.buffer - except AttributeError: - fp = sys.stdout + fp = sys.stdout.buffer self.fp = fp def begin_document(self, id: str | None = None) -> None: From 2ed8502d12a0f0efd7d4cd2c65b4c6a7e59aaa2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2024 22:55:07 +1000 Subject: [PATCH 091/652] Use ipython PrettyPrinter, rather than custom class --- .appveyor.yml | 2 +- .ci/install.sh | 1 + .github/workflows/macos-install.sh | 1 + .github/workflows/test-cygwin.yml | 1 + Tests/test_image.py | 17 +++++++++++------ src/PIL/Image.py | 4 +++- 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index e12987a5f..f490561cd 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -51,7 +51,7 @@ build_script: test_script: - cd c:\pillow -- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma' +- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma' - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' diff --git a/.ci/install.sh b/.ci/install.sh index 8e65f64c4..b2d615866 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -30,6 +30,7 @@ python3 -m pip install --upgrade pip python3 -m pip install --upgrade wheel python3 -m pip install coverage python3 -m pip install defusedxml +python3 -m pip install ipython python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 25943776f..ddb421230 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -23,6 +23,7 @@ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" python3 -m pip install coverage python3 -m pip install defusedxml +python3 -m pip install ipython python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 8e2827099..0aa79e423 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -74,6 +74,7 @@ jobs: perl python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel + python3${{ matrix.python-minor-version }}-ipython python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter diff --git a/Tests/test_image.py b/Tests/test_image.py index 719732d12..2afcf2efd 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -42,6 +42,12 @@ try: except ImportError: ElementTree = None +PrettyPrinter: type | None +try: + from IPython.lib.pretty import PrettyPrinter +except ImportError: + PrettyPrinter = None + # Deprecation helper def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: @@ -91,16 +97,15 @@ class TestImage: # with pytest.raises(MemoryError): # Image.new("L", (1000000, 1000000)) + @pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed") def test_repr_pretty(self) -> None: - class Pretty: - def text(self, text: str) -> None: - self.pretty_output = text - im = Image.new("L", (100, 100)) - p = Pretty() + output = io.StringIO() + assert PrettyPrinter is not None + p = PrettyPrinter(output) im._repr_pretty_(p, False) - assert p.pretty_output == "" + assert output.getvalue() == "" def test_open_formats(self) -> None: PNGFILE = "Tests/images/hopper.png" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a6eefff56..705d0a144 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -221,6 +221,8 @@ if TYPE_CHECKING: import mmap from xml.etree.ElementTree import Element + from IPython.lib.pretty import PrettyPrinter + from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] @@ -677,7 +679,7 @@ class Image: id(self), ) - def _repr_pretty_(self, p, cycle: bool) -> None: + def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: """IPython plain text display support""" # Same as __repr__ but without unpredictable id(self), From e6e5ef5c5fbd83ac5dd63301e4d7d6860a7b2d09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Aug 2024 08:05:02 +1000 Subject: [PATCH 092/652] Added type hints --- Tests/test_file_tiff.py | 7 +++++ Tests/test_imagefile.py | 3 +- docs/reference/Image.rst | 1 + src/PIL/IcoImagePlugin.py | 4 +-- src/PIL/Image.py | 42 ++++++++++++++++++--------- src/PIL/ImageFile.py | 8 ++++-- src/PIL/IptcImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 59 ++++++++++++++++++++++---------------- 8 files changed, 80 insertions(+), 46 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 8cad25272..190f83f40 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -684,6 +684,13 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert_image_equal_tofile(reloaded, infile) + def test_invalid_tiled_dimensions(self) -> None: + with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp: + data = fp.read() + b = BytesIO(data[:144] + b"\x02" + data[145:]) + with pytest.raises(ValueError): + Image.open(b) + @pytest.mark.parametrize("mode", ("P", "PA")) def test_palette(self, mode: str, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 95e91db83..a8bd798c1 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -412,9 +412,8 @@ class TestPyEncoder(CodecsTest): with pytest.raises(NotImplementedError): encoder.encode_to_pyfd() - fh = BytesIO() with pytest.raises(NotImplementedError): - encoder.encode_to_file(fh, 0) + encoder.encode_to_file(0, 0) def test_zero_height(self) -> None: with pytest.raises(UnidentifiedImageError): diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 66c5e5422..02e714f20 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -363,6 +363,7 @@ Classes :show-inheritance: .. autoclass:: PIL.Image.ImagePointHandler .. autoclass:: PIL.Image.ImageTransformHandler +.. autoclass:: PIL.Image._E Protocols --------- diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 3bf5d9c04..926562497 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -319,11 +319,11 @@ class IcoImageFile(ImageFile.ImageFile): self.load() @property - def size(self): + def size(self) -> tuple[int, int]: return self._size @size.setter - def size(self, value): + def size(self, value: tuple[int, int]) -> None: if value not in self.info["sizes"]: msg = "This is not one of the allowed sizes of this image" raise ValueError(msg) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 705d0a144..ffc6bec34 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -504,7 +504,7 @@ class _E: return _E(self.scale / other, self.offset / other) -def _getscaleoffset(expr) -> tuple[float, float]: +def _getscaleoffset(expr: Callable[[_E], _E | float]) -> tuple[float, float]: a = expr(_E(1, 0)) return (a.scale, a.offset) if isinstance(a, _E) else (0, a) @@ -1894,7 +1894,13 @@ class Image: def point( self, - lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler, + lut: ( + Sequence[float] + | NumpyArray + | Callable[[int], float] + | Callable[[_E], _E | float] + | ImagePointHandler + ), mode: str | None = None, ) -> Image: """ @@ -1930,10 +1936,10 @@ class Image: # check if the function can be used with point_transform # UNDONE wiredfool -- I think this prevents us from ever doing # a gamma function point transform on > 8bit images. - scale, offset = _getscaleoffset(lut) + scale, offset = _getscaleoffset(lut) # type: ignore[arg-type] return self._new(self.im.point_transform(scale, offset)) # for other modes, convert the function to a table - flatLut = [lut(i) for i in range(256)] * self.im.bands + flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type] else: flatLut = lut @@ -2869,11 +2875,11 @@ class Image: self, box: tuple[int, int, int, int], image: Image, - method, - data, + method: Transform, + data: Sequence[float], resample: int = Resampling.NEAREST, fill: bool = True, - ): + ) -> None: w = box[2] - box[0] h = box[3] - box[1] @@ -4008,15 +4014,19 @@ class Exif(_ExifBase): ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) - def get_ifd(self, tag): + def get_ifd(self, tag: int) -> dict[int, Any]: if tag not in self._ifds: if tag == ExifTags.IFD.IFD1: if self._info is not None and self._info.next != 0: - self._ifds[tag] = self._get_ifd_dict(self._info.next) + ifd = self._get_ifd_dict(self._info.next) + if ifd is not None: + self._ifds[tag] = ifd elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: offset = self._hidden_data.get(tag, self.get(tag)) if offset is not None: - self._ifds[tag] = self._get_ifd_dict(offset, tag) + ifd = self._get_ifd_dict(offset, tag) + if ifd is not None: + self._ifds[tag] = ifd elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: if ExifTags.IFD.Exif not in self._ifds: self.get_ifd(ExifTags.IFD.Exif) @@ -4073,7 +4083,9 @@ class Exif(_ExifBase): (offset,) = struct.unpack(">L", data) self.fp.seek(offset) - camerainfo = {"ModelID": self.fp.read(4)} + camerainfo: dict[str, int | bytes] = { + "ModelID": self.fp.read(4) + } self.fp.read(4) # Seconds since 2000 @@ -4089,16 +4101,18 @@ class Exif(_ExifBase): ][1] camerainfo["Parallax"] = handler( ImageFileDirectory_v2(), parallax, False - ) + )[0] self.fp.read(4) camerainfo["Category"] = self.fp.read(2) - makernote = {0x1101: dict(self._fixup_dict(camerainfo))} + makernote = {0x1101: camerainfo} self._ifds[tag] = makernote else: # Interop - self._ifds[tag] = self._get_ifd_dict(tag_data, tag) + ifd = self._get_ifd_dict(tag_data, tag) + if ifd is not None: + self._ifds[tag] = ifd ifd = self._ifds.get(tag, {}) if tag == ExifTags.IFD.Exif and self._hidden_data: ifd = { diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 73554fa53..fdeb81d7a 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -31,6 +31,7 @@ from __future__ import annotations import abc import io import itertools +import os import struct import sys from typing import IO, Any, NamedTuple @@ -555,7 +556,7 @@ def _encode_tile( fp: IO[bytes], tile: list[_Tile], bufsize: int, - fh, + fh: int | None, exc: BaseException | None = None, ) -> None: for encoder_name, extents, offset, args in tile: @@ -577,6 +578,7 @@ def _encode_tile( break else: # slight speedup: compress to real file object + assert fh is not None errcode = encoder.encode_to_file(fh, bufsize) if errcode < 0: raise _get_oserror(errcode, encoder=True) from exc @@ -801,7 +803,7 @@ class PyEncoder(PyCodec): self.fd.write(data) return bytes_consumed, errcode - def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int: + def encode_to_file(self, fh: int, bufsize: int) -> int: """ :param fh: File handle. :param bufsize: Buffer size. @@ -814,5 +816,5 @@ class PyEncoder(PyCodec): while errcode == 0: status, errcode, buf = self.encode(bufsize) if status > 0: - fh.write(buf[status:]) + os.write(fh, buf[status:]) return errcode diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 153243519..6ccf28aa1 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -214,7 +214,7 @@ def getiptcinfo( # as 4-byte integers, so we cannot use the get method...) try: data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK] - except (AttributeError, KeyError): + except KeyError: pass if data is None: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 7bbbc832e..5c6996a65 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -624,12 +624,12 @@ class ImageFileDirectory_v2(_IFDv2Base): self._tagdata: dict[int, bytes] = {} self.tagtype = {} # added 2008-06-05 by Florian Hoech self._next = None - self._offset = None + self._offset: int | None = None def __str__(self) -> str: return str(dict(self)) - def named(self): + def named(self) -> dict[str, Any]: """ :returns: dict of name|key: value @@ -643,7 +643,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def __len__(self) -> int: return len(set(self._tagdata) | set(self._tags_v2)) - def __getitem__(self, tag): + def __getitem__(self, tag: int) -> Any: if tag not in self._tags_v2: # unpack on the fly data = self._tagdata[tag] typ = self.tagtype[tag] @@ -855,7 +855,7 @@ class ImageFileDirectory_v2(_IFDv2Base): raise OSError(msg) return ret - def load(self, fp): + def load(self, fp: IO[bytes]) -> None: self.reset() self._offset = fp.tell() @@ -1098,7 +1098,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): for legacy_api in (False, True): self._setitem(tag, value, legacy_api) - def __getitem__(self, tag): + def __getitem__(self, tag: int) -> Any: if tag not in self._tags_v1: # unpack on the fly data = self._tagdata[tag] typ = self.tagtype[tag] @@ -1124,11 +1124,15 @@ class TiffImageFile(ImageFile.ImageFile): format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False - def __init__(self, fp=None, filename=None): - self.tag_v2 = None + def __init__( + self, + fp: StrOrBytesPath | IO[bytes] | None = None, + filename: str | bytes | None = None, + ) -> None: + self.tag_v2: ImageFileDirectory_v2 """ Image file directory (tag dictionary) """ - self.tag = None + self.tag: ImageFileDirectory_v1 """ Legacy tag entries """ super().__init__(fp, filename) @@ -1143,9 +1147,6 @@ class TiffImageFile(ImageFile.ImageFile): self.tag_v2 = ImageFileDirectory_v2(ifh) - # legacy IFD entries will be filled in later - self.ifd: ImageFileDirectory_v1 | None = None - # setup frame pointers self.__first = self.__next = self.tag_v2.next self.__frame = -1 @@ -1396,8 +1397,11 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) # size - xsize = int(self.tag_v2.get(IMAGEWIDTH)) - ysize = int(self.tag_v2.get(IMAGELENGTH)) + xsize = self.tag_v2.get(IMAGEWIDTH) + ysize = self.tag_v2.get(IMAGELENGTH) + if not isinstance(xsize, int) or not isinstance(ysize, int): + msg = "Invalid dimensions" + raise ValueError(msg) self._size = xsize, ysize logger.debug("- size: %s", self.size) @@ -1545,8 +1549,12 @@ class TiffImageFile(ImageFile.ImageFile): else: # tiled image offsets = self.tag_v2[TILEOFFSETS] - w = self.tag_v2.get(TILEWIDTH) + tilewidth = self.tag_v2.get(TILEWIDTH) h = self.tag_v2.get(TILELENGTH) + if not isinstance(tilewidth, int) or not isinstance(h, int): + msg = "Invalid tile dimensions" + raise ValueError(msg) + w = tilewidth for offset in offsets: if x + w > xsize: @@ -1624,7 +1632,7 @@ SAVE_INFO = { } -def _save(im, fp, filename): +def _save(im: Image.Image, fp, filename: str | bytes) -> None: try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: @@ -1760,10 +1768,11 @@ def _save(im, fp, filename): if im.mode == "1": inverted_im = im.copy() px = inverted_im.load() - for y in range(inverted_im.height): - for x in range(inverted_im.width): - px[x, y] = 0 if px[x, y] == 255 else 255 - im = inverted_im + if px is not None: + for y in range(inverted_im.height): + for x in range(inverted_im.width): + px[x, y] = 0 if px[x, y] == 255 else 255 + im = inverted_im else: im = ImageOps.invert(im) @@ -1805,11 +1814,11 @@ def _save(im, fp, filename): ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) if im.mode == "YCbCr": - for tag, value in { + for tag, default_value in { YCBCRSUBSAMPLING: (1, 1), REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), }.items(): - ifd.setdefault(tag, value) + ifd.setdefault(tag, default_value) blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] if libtiff: @@ -1852,7 +1861,7 @@ def _save(im, fp, filename): ] # bits per sample is a single short in the tiff directory, not a list. - atts = {BITSPERSAMPLE: bits[0]} + atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]} # Merge the ones that we have with (optional) more bits from # the original file, e.g x,y resolution so that we can # save(load('')) == original file. @@ -1923,13 +1932,15 @@ def _save(im, fp, filename): offset = ifd.save(fp) ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] + im, + fp, + [ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))], ) # -- helper for multi-page save -- if "_debug_multipage" in encoderinfo: # just to access o32 and o16 (using correct byte order) - im._debug_multipage = ifd + setattr(im, "_debug_multipage", ifd) class AppendingTiffWriter: From 388fc78e36e60f45a9b5ff0cd3223e3b8bc2c307 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 15:32:06 -0400 Subject: [PATCH 093/652] Add/update project-makefile files --- Makefile | 4757 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 4644 insertions(+), 113 deletions(-) diff --git a/Makefile b/Makefile index 94f7565d8..4200f95da 100644 --- a/Makefile +++ b/Makefile @@ -1,125 +1,4656 @@ -.DEFAULT_GOAL := help +# Project Makefile +# +# A makefile to automate setup of a Wagtail CMS project and related tasks. +# +# https://github.com/aclark4life/project-makefile +# +# -------------------------------------------------------------------------------- +# Set the default goal to be `git commit -a -m $(GIT_MESSAGE)` and `git push` +# -------------------------------------------------------------------------------- -.PHONY: clean -clean: - rm src/PIL/*.so || true - rm -r build || true - find . -name __pycache__ | xargs rm -r || true +.DEFAULT_GOAL := git-commit -.PHONY: coverage -coverage: - python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq - rm -r htmlcov || true - python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage - python3 -m coverage report +# -------------------------------------------------------------------------------- +# Single line variables to be used by phony target rules +# -------------------------------------------------------------------------------- -.PHONY: doc -.PHONY: html -doc html: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . - $(MAKE) -C docs html +ADD_DIR := mkdir -pv +ADD_FILE := touch +AWS_OPTS := --no-cli-pager --output table +COPY_DIR := cp -rv +COPY_FILE := cp -v +DEL_DIR := rm -rv +DEL_FILE := rm -v +DJANGO_DB_COL = awk -F\= '{print $$2}' +DJANGO_DB_URL = eb ssh -c "source /opt/elasticbeanstalk/deployment/custom_env_var; env | grep DATABASE_URL" +DJANGO_DB_HOST = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["HOST"])') +DJANGO_DB_NAME = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["NAME"])') +DJANGO_DB_PASS = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["PASSWORD"])') +DJANGO_DB_USER = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["USER"])') +DJANGO_BACKEND_APPS_FILE := backend/apps.py +DJANGO_CUSTOM_ADMIN_FILE := backend/admin.py +DJANGO_FRONTEND_FILES = .babelrc .browserslistrc .eslintrc .nvmrc .stylelintrc.json frontend package-lock.json \ + package.json postcss.config.js +DJANGO_SETTINGS_DIR = backend/settings +DJANGO_SETTINGS_BASE_FILE = $(DJANGO_SETTINGS_DIR)/base.py +DJANGO_SETTINGS_DEV_FILE = $(DJANGO_SETTINGS_DIR)/dev.py +DJANGO_SETTINGS_PROD_FILE = $(DJANGO_SETTINGS_DIR)/production.py +DJANGO_SETTINGS_SECRET_KEY = $(shell openssl rand -base64 48) +DJANGO_URLS_FILE = backend/urls.py +EB_DIR_NAME := .elasticbeanstalk +EB_ENV_NAME ?= $(PROJECT_NAME)-$(GIT_BRANCH)-$(GIT_REV) +EB_PLATFORM ?= "Python 3.11 running on 64bit Amazon Linux 2023" +EC2_INSTANCE_MAX ?= 1 +EC2_INSTANCE_MIN ?= 1 +EC2_INSTANCE_PROFILE ?= aws-elasticbeanstalk-ec2-role +EC2_INSTANCE_TYPE ?= t4g.small +EC2_LB_TYPE ?= application +EDITOR_REVIEW = subl +GIT_ADD := git add +GIT_BRANCH = $(shell git branch --show-current) +GIT_BRANCHES = $(shell git branch -a) +GIT_CHECKOUT = git checkout +GIT_COMMIT_MSG = "Update $(PROJECT_NAME)" +GIT_COMMIT = git commit +GIT_PUSH = git push +GIT_PUSH_FORCE = git push --force-with-lease +GIT_REV = $(shell git rev-parse --short HEAD) +MAKEFILE_CUSTOM_FILE := project.mk +PACKAGE_NAME = $(shell echo $(PROJECT_NAME) | sed 's/-/_/g') +PAGER ?= less +PIP_ENSURE = python -m ensurepip +PIP_INSTALL_PLONE_CONSTRAINTS = https://dist.plone.org/release/6.0.11.1/constraints.txt +PROJECT_DIRS = backend contactpage home privacy siteuser +PROJECT_EMAIL := aclark@aclark.net +PROJECT_NAME = project-makefile +RANDIR := $(shell openssl rand -base64 12 | sed 's/\///g') +TMPDIR := $(shell mktemp -d) +UNAME := $(shell uname) +WAGTAIL_CLEAN_DIRS = backend contactpage dist frontend home logging_demo model_form_demo node_modules payments privacy search sitepage siteuser +WAGTAIL_CLEAN_FILES = .babelrc .browserslistrc .dockerignore .eslintrc .gitignore .nvmrc .stylelintrc.json Dockerfile db.sqlite3 docker-compose.yml manage.py package-lock.json package.json postcss.config.js requirements-test.txt requirements.txt -.PHONY: htmlview -htmlview: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . - $(MAKE) -C docs htmlview +# -------------------------------------------------------------------------------- +# Include $(MAKEFILE_CUSTOM_FILE) if it exists +# -------------------------------------------------------------------------------- -.PHONY: doccheck -doccheck: - $(MAKE) doc -# Don't make our tests rely on the links in the docs being up every single build. -# We don't control them. But do check, and update them to the target of their redirects. - $(MAKE) -C docs linkcheck || true +ifneq ($(wildcard $(MAKEFILE_CUSTOM_FILE)),) + include $(MAKEFILE_CUSTOM_FILE) +endif -.PHONY: docserve -docserve: - cd docs/_build/html && python3 -m http.server 2> /dev/null& +# -------------------------------------------------------------------------------- +# Multi-line variables to be used in phony target rules +# -------------------------------------------------------------------------------- -.PHONY: help -help: - @echo "Welcome to Pillow development. Please use \`make \` where is one of" - @echo " clean remove build products" - @echo " coverage run coverage test (in progress)" - @echo " doc make HTML docs" - @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 " install make and install" - @echo " install-coverage make and install with C coverage" - @echo " lint run the lint checks" - @echo " lint-fix run Ruff to (mostly) fix lint issues" - @echo " release-test run code and package tests before release" - @echo " test run tests on installed Pillow" +define DJANGO_ALLAUTH_BASE_TEMPLATE +{% extends 'base.html' %} +endef -.PHONY: install -install: - python3 -m pip -v install . - python3 selftest.py - -.PHONY: install-coverage -install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install . - python3 selftest.py - -.PHONY: debug -debug: -# make a debug version if we don't have a -dbg python. Leaves in symbols -# for our stuff, kills optimization, and redirects to dev null so we -# see any build failures. - make clean > /dev/null - CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null - -.PHONY: release-test -release-test: - python3 Tests/check_release_notes.py - python3 -m pip install -e .[tests] - python3 selftest.py - python3 -m pytest Tests - python3 -m pip install . - python3 -m pytest -qq - python3 -m check_manifest - python3 -m pyroma . - $(MAKE) readme - -.PHONY: sdist -sdist: - python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build - python3 -m build --sdist - python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine - python3 -m twine check --strict dist/* - -.PHONY: test -test: - python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq - -.PHONY: valgrind -valgrind: - python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ - --log-file=/tmp/valgrind-output \ - python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output - -.PHONY: readme -readme: - python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 - python3 -m markdown2 README.md > .long-description.html && open .long-description.html +define DJANGO_API_SERIALIZERS +from rest_framework import serializers +from siteuser.models import User -.PHONY: lint -lint: - python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox - python3 -m tox -e lint +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = ["url", "username", "email", "is_staff"] +endef -.PHONY: lint-fix -lint-fix: - python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black - python3 -m black . - python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff - python3 -m ruff --fix . +define DJANGO_API_VIEWS +from ninja import NinjaAPI +from rest_framework import viewsets +from siteuser.models import User +from .serializers import UserSerializer -.PHONY: mypy -mypy: - python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox - python3 -m tox -e mypy +api = NinjaAPI() + + +@api.get("/hello") +def hello(request): + return "Hello world" + + +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer +endef + +define DJANGO_APP_TESTS +from django.test import TestCase +from django.urls import reverse +from .models import YourModel +from .forms import YourForm + + +class YourModelTest(TestCase): + def setUp(self): + self.instance = YourModel.objects.create(field1="value1", field2="value2") + + def test_instance_creation(self): + self.assertIsInstance(self.instance, YourModel) + self.assertEqual(self.instance.field1, "value1") + self.assertEqual(self.instance.field2, "value2") + + def test_str_method(self): + self.assertEqual(str(self.instance), "Expected String Representation") + + +class YourViewTest(TestCase): + def setUp(self): + self.instance = YourModel.objects.create(field1="value1", field2="value2") + + def test_view_url_exists_at_desired_location(self): + response = self.client.get("/your-url/") + self.assertEqual(response.status_code, 200) + + def test_view_url_accessible_by_name(self): + response = self.client.get(reverse("your-view-name")) + self.assertEqual(response.status_code, 200) + + def test_view_uses_correct_template(self): + response = self.client.get(reverse("your-view-name")) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "your_template.html") + + def test_view_context(self): + response = self.client.get(reverse("your-view-name")) + self.assertEqual(response.status_code, 200) + self.assertIn("context_variable", response.context) + + +class YourFormTest(TestCase): + def test_form_valid_data(self): + form = YourForm(data={"field1": "value1", "field2": "value2"}) + self.assertTrue(form.is_valid()) + + def test_form_invalid_data(self): + form = YourForm(data={"field1": "", "field2": "value2"}) + self.assertFalse(form.is_valid()) + self.assertIn("field1", form.errors) + + def test_form_save(self): + form = YourForm(data={"field1": "value1", "field2": "value2"}) + if form.is_valid(): + instance = form.save() + self.assertEqual(instance.field1, "value1") + self.assertEqual(instance.field2, "value2") +endef + +define DJANGO_BACKEND_APPS +from django.contrib.admin.apps import AdminConfig + + +class CustomAdminConfig(AdminConfig): + default_site = "backend.admin.CustomAdminSite" +endef + +define DJANGO_BASE_TEMPLATE +{% load static webpack_loader %} + + + + + + {% block title %}{% endblock %} + {% block title_suffix %}{% endblock %} + + + {% stylesheet_pack 'app' %} + {% block extra_css %}{# Override this in templates to add extra stylesheets #}{% endblock %} + + {% include 'favicon.html' %} + {% csrf_token %} + + +
+
+ {% include 'header.html' %} + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} +
+ {% block content %}{% endblock %} +
+
+ {% include 'footer.html' %} + {% include 'offcanvas.html' %} + {% javascript_pack 'app' %} + {% block extra_js %}{# Override this in templates to add extra javascript #}{% endblock %} + + +endef + +define DJANGO_CUSTOM_ADMIN +from django.contrib.admin import AdminSite + + +class CustomAdminSite(AdminSite): + site_header = "Project Makefile" + site_title = "Project Makefile" + index_title = "Project Makefile" + + +custom_admin_site = CustomAdminSite(name="custom_admin") +endef + +define DJANGO_DOCKERCOMPOSE +version: '3' + +services: + db: + image: postgres:latest + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: project + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin + + web: + build: . + command: sh -c "python manage.py migrate && gunicorn project.wsgi:application -b 0.0.0.0:8000" + volumes: + - .:/app + ports: + - "8000:8000" + depends_on: + - db + environment: + DATABASE_URL: postgres://admin:admin@db:5432/project + +volumes: + postgres_data: +endef + +define DJANGO_DOCKERFILE +FROM amazonlinux:2023 +RUN dnf install -y shadow-utils python3.11 python3.11-pip make nodejs20-npm nodejs postgresql15 postgresql15-server +USER postgres +RUN initdb -D /var/lib/pgsql/data +USER root +RUN useradd wagtail +EXPOSE 8000 +ENV PYTHONUNBUFFERED=1 PORT=8000 +COPY requirements.txt / +RUN python3.11 -m pip install -r /requirements.txt +WORKDIR /app +RUN chown wagtail:wagtail /app +COPY --chown=wagtail:wagtail . . +USER wagtail +RUN npm-20 install; npm-20 run build +RUN python3.11 manage.py collectstatic --noinput --clear +CMD set -xe; pg_ctl -D /var/lib/pgsql/data -l /tmp/logfile start; python3.11 manage.py migrate --noinput; gunicorn backend.wsgi:application +endef + +define DJANGO_FAVICON_TEMPLATE +{% load static %} + +endef + +define DJANGO_FOOTER_TEMPLATE +
+

© {% now "Y" %} {{ current_site.site_name|default:"Project Makefile" }}

+
    +
  • + Home +
  • + {% for child in current_site.root_page.get_children %} +
  • + {{ child }} +
  • + {% endfor %} +
+
+endef + +define DJANGO_FRONTEND_APP +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import 'bootstrap'; +import '@fortawesome/fontawesome-free/js/fontawesome'; +import '@fortawesome/fontawesome-free/js/solid'; +import '@fortawesome/fontawesome-free/js/regular'; +import '@fortawesome/fontawesome-free/js/brands'; +import getDataComponents from '../dataComponents'; +import UserContextProvider from '../context'; +import * as components from '../components'; +import "../styles/index.scss"; +import "../styles/theme-blue.scss"; +import "./config"; + +const { ErrorBoundary } = components; +const dataComponents = getDataComponents(components); +const container = document.getElementById('app'); +const root = createRoot(container); +const App = () => ( + + + {dataComponents} + + +); +root.render(); +endef + +define DJANGO_FRONTEND_APP_CONFIG +import '../utils/themeToggler.js'; +// import '../utils/tinymce.js'; +endef + +define DJANGO_FRONTEND_BABELRC +{ + "presets": [ + [ + "@babel/preset-react", + ], + [ + "@babel/preset-env", + { + "useBuiltIns": "usage", + "corejs": "3.0.0" + } + ] + ], + "plugins": [ + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-transform-class-properties" + ] +} +endef + +define DJANGO_FRONTEND_COMPONENTS +export { default as ErrorBoundary } from './ErrorBoundary'; +export { default as UserMenu } from './UserMenu'; +endef + +define DJANGO_FRONTEND_COMPONENT_CLOCK +// Via ChatGPT +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import PropTypes from 'prop-types'; + +const Clock = ({ color = '#fff' }) => { + const [date, setDate] = useState(new Date()); + const [blink, setBlink] = useState(true); + const timerID = useRef(); + + const tick = useCallback(() => { + setDate(new Date()); + setBlink(prevBlink => !prevBlink); + }, []); + + useEffect(() => { + timerID.current = setInterval(() => tick(), 1000); + + // Return a cleanup function to be run on component unmount + return () => clearInterval(timerID.current); + }, [tick]); + + const formattedDate = date.toLocaleDateString(undefined, { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + }); + + const formattedTime = date.toLocaleTimeString(undefined, { + hour: 'numeric', + minute: 'numeric', + }); + + return ( + <> +
{formattedDate} {formattedTime}
+ + ); +}; + +Clock.propTypes = { + color: PropTypes.string, +}; + +export default Clock; +endef + +define DJANGO_FRONTEND_COMPONENT_ERROR +import { Component } from 'react'; +import PropTypes from 'prop-types'; + +class ErrorBoundary extends Component { + constructor (props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError () { + return { hasError: true }; + } + + componentDidCatch (error, info) { + const { onError } = this.props; + console.error(error); + onError && onError(error, info); + } + + render () { + const { children = null } = this.props; + const { hasError } = this.state; + + return hasError ? null : children; + } +} + +ErrorBoundary.propTypes = { + onError: PropTypes.func, + children: PropTypes.node, +}; + +export default ErrorBoundary; +endef + +define DJANGO_FRONTEND_COMPONENT_USER_MENU +// UserMenu.js +import React from 'react'; +import PropTypes from 'prop-types'; + +function handleLogout() { + window.location.href = '/accounts/logout'; +} + +const UserMenu = ({ isAuthenticated, isSuperuser, textColor }) => { + return ( +
+ {isAuthenticated ? ( +
  • + + +
  • + ) : ( +
  • + +
  • + )} +
    + ); +}; + +UserMenu.propTypes = { + isAuthenticated: PropTypes.bool.isRequired, + isSuperuser: PropTypes.bool.isRequired, + textColor: PropTypes.string, +}; + +export default UserMenu; +endef + +define DJANGO_FRONTEND_CONTEXT_INDEX +export { UserContextProvider as default } from './UserContextProvider'; +endef + +define DJANGO_FRONTEND_CONTEXT_USER_PROVIDER +// UserContextProvider.js +import React, { createContext, useContext, useState } from 'react'; +import PropTypes from 'prop-types'; + +const UserContext = createContext(); + +export const UserContextProvider = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + + const login = () => { + try { + // Add logic to handle login, set isAuthenticated to true + setIsAuthenticated(true); + } catch (error) { + console.error('Login error:', error); + // Handle error, e.g., show an error message to the user + } + }; + + const logout = () => { + try { + // Add logic to handle logout, set isAuthenticated to false + setIsAuthenticated(false); + } catch (error) { + console.error('Logout error:', error); + // Handle error, e.g., show an error message to the user + } + }; + + return ( + + {children} + + ); +}; + +UserContextProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +export const useUserContext = () => { + const context = useContext(UserContext); + + if (!context) { + throw new Error('useUserContext must be used within a UserContextProvider'); + } + + return context; +}; + +// Add PropTypes for the return value of useUserContext +useUserContext.propTypes = { + isAuthenticated: PropTypes.bool.isRequired, + login: PropTypes.func.isRequired, + logout: PropTypes.func.isRequired, +}; +endef + +define DJANGO_FRONTEND_ESLINTRC +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "overrides": [ + { + "env": { + "node": true + }, + "files": [ + ".eslintrc.{js,cjs}" + ], + "parserOptions": { + "sourceType": "script" + } + } + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "react" + ], + "rules": { + "no-unused-vars": "off" + }, + settings: { + react: { + version: 'detect', + }, + }, +} +endef + +define DJANGO_FRONTEND_OFFCANVAS_TEMPLATE +
    + +
    + +
    +
    +endef + +define DJANGO_FRONTEND_PORTAL +// Via pwellever +import React from 'react'; +import { createPortal } from 'react-dom'; + +const parseProps = data => Object.entries(data).reduce((result, [key, value]) => { + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } else if (value.toLowerCase() === 'null') { + value = null; + } else if (!isNaN(parseFloat(value)) && isFinite(value)) { + // Parse numeric value + value = parseFloat(value); + } else if ( + (value[0] === '[' && value.slice(-1) === ']') || (value[0] === '{' && value.slice(-1) === '}') + ) { + // Parse JSON strings + value = JSON.parse(value); + } + + result[key] = value; + return result; +}, {}); + +// This method of using portals instead of calling ReactDOM.render on individual components +// ensures that all components are mounted under a single React tree, and are therefore able +// to share context. + +export default function getPageComponents (components) { + const getPortalComponent = domEl => { + // The element's "data-component" attribute is used to determine which component to render. + // All other "data-*" attributes are passed as props. + const { component: componentName, ...rest } = domEl.dataset; + const Component = components[componentName]; + if (!Component) { + console.error(`Component "$${componentName}" not found.`); + return null; + } + const props = parseProps(rest); + domEl.innerHTML = ''; + + // eslint-disable-next-line no-unused-vars + const { ErrorBoundary } = components; + return createPortal( + + + , + domEl, + ); + }; + + return Array.from(document.querySelectorAll('[data-component]')).map(getPortalComponent); +} +endef + +define DJANGO_FRONTEND_STYLES +// If you comment out code below, bootstrap will use red as primary color +// and btn-primary will become red + +// $primary: red; + +@import "~bootstrap/scss/bootstrap.scss"; + +.jumbotron { + // should be relative path of the entry scss file + background-image: url("../../vendors/images/sample.jpg"); + background-size: cover; +} + +#theme-toggler-authenticated:hover { + cursor: pointer; /* Change cursor to pointer on hover */ + color: #007bff; /* Change color on hover */ +} + +#theme-toggler-anonymous:hover { + cursor: pointer; /* Change cursor to pointer on hover */ + color: #007bff; /* Change color on hover */ +} +endef + +define DJANGO_FRONTEND_THEME_BLUE +@import "~bootstrap/scss/bootstrap.scss"; + +[data-bs-theme="blue"] { + --bs-body-color: var(--bs-white); + --bs-body-color-rgb: #{to-rgb($$white)}; + --bs-body-bg: var(--bs-blue); + --bs-body-bg-rgb: #{to-rgb($$blue)}; + --bs-tertiary-bg: #{$$blue-600}; + + .dropdown-menu { + --bs-dropdown-bg: #{color-mix($$blue-500, $$blue-600)}; + --bs-dropdown-link-active-bg: #{$$blue-700}; + } + + .btn-secondary { + --bs-btn-bg: #{color-mix($gray-600, $blue-400, .5)}; + --bs-btn-border-color: #{rgba($$white, .25)}; + --bs-btn-hover-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 5%)}; + --bs-btn-hover-border-color: #{rgba($$white, .25)}; + --bs-btn-active-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 10%)}; + --bs-btn-active-border-color: #{rgba($$white, .5)}; + --bs-btn-focus-border-color: #{rgba($$white, .5)}; + + // --bs-btn-focus-box-shadow: 0 0 0 .25rem rgba(255, 255, 255, 20%); + } +} +endef + +define DJANGO_FRONTEND_THEME_TOGGLER +document.addEventListener('DOMContentLoaded', function () { + const rootElement = document.documentElement; + const anonThemeToggle = document.getElementById('theme-toggler-anonymous'); + const authThemeToggle = document.getElementById('theme-toggler-authenticated'); + if (authThemeToggle) { + localStorage.removeItem('data-bs-theme'); + } + const anonSavedTheme = localStorage.getItem('data-bs-theme'); + if (anonSavedTheme) { + rootElement.setAttribute('data-bs-theme', anonSavedTheme); + } + if (anonThemeToggle) { + anonThemeToggle.addEventListener('click', function () { + const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + rootElement.setAttribute('data-bs-theme', newTheme); + localStorage.setItem('data-bs-theme', newTheme); + }); + } + if (authThemeToggle) { + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; + authThemeToggle.addEventListener('click', function () { + const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + fetch('/user/update_theme_preference/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, // Include the CSRF token in the headers + }, + body: JSON.stringify({ theme: newTheme }), + }) + .then(response => response.json()) + .then(data => { + rootElement.setAttribute('data-bs-theme', newTheme); + }) + .catch(error => { + console.error('Error updating theme preference:', error); + }); + }); + } +}); +endef + +define DJANGO_HEADER_TEMPLATE +
    +
    + +
    +
    +endef + +define DJANGO_HOME_PAGE_ADMIN +from django.contrib import admin # noqa + +# Register your models here. +endef + +define DJANGO_HOME_PAGE_MODELS +from django.db import models # noqa + +# Create your models here. +endef + +define DJANGO_HOME_PAGE_TEMPLATE +{% extends "base.html" %} +{% block content %} +
    +
    +{% endblock %} +endef + +define DJANGO_HOME_PAGE_URLS +from django.urls import path +from .views import HomeView + +urlpatterns = [path("", HomeView.as_view(), name="home")] +endef + +define DJANGO_HOME_PAGE_VIEWS +from django.views.generic import TemplateView + + +class HomeView(TemplateView): + template_name = "home.html" +endef + +define DJANGO_LOGGING_DEMO_ADMIN +# Register your models here. +endef + +define DJANGO_LOGGING_DEMO_MODELS +from django.db import models # noqa + +# Create your models here. +endef + +define DJANGO_LOGGING_DEMO_SETTINGS +INSTALLED_APPS.append("logging_demo") # noqa +endef + +define DJANGO_LOGGING_DEMO_URLS +from django.urls import path +from .views import logging_demo + +urlpatterns = [ + path("", logging_demo, name="logging_demo"), +] +endef + +define DJANGO_LOGGING_DEMO_VIEWS +from django.http import HttpResponse +import logging + +logger = logging.getLogger(__name__) + + +def logging_demo(request): + logger.debug("Hello, world!") + return HttpResponse("Hello, world!") +endef + +define DJANGO_MANAGE_PY +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.dev") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() +endef + +define DJANGO_MODEL_FORM_DEMO_ADMIN +from django.contrib import admin +from .models import ModelFormDemo + + +@admin.register(ModelFormDemo) +class ModelFormDemoAdmin(admin.ModelAdmin): + pass +endef + +define DJANGO_MODEL_FORM_DEMO_FORMS +from django import forms +from .models import ModelFormDemo + + +class ModelFormDemoForm(forms.ModelForm): + class Meta: + model = ModelFormDemo + fields = ["name", "email", "age", "is_active"] +endef + +define DJANGO_MODEL_FORM_DEMO_MODEL +from django.db import models +from django.shortcuts import reverse + + +class ModelFormDemo(models.Model): + name = models.CharField(max_length=100, blank=True, null=True) + email = models.EmailField(blank=True, null=True) + age = models.IntegerField(blank=True, null=True) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name or f"test-model-{self.pk}" + + def get_absolute_url(self): + return reverse("model_form_demo_detail", kwargs={"pk": self.pk}) +endef + +define DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL +{% extends 'base.html' %} +{% block content %} +

    Test Model Detail: {{ model_form_demo.name }}

    +

    Name: {{ model_form_demo.name }}

    +

    Email: {{ model_form_demo.email }}

    +

    Age: {{ model_form_demo.age }}

    +

    Active: {{ model_form_demo.is_active }}

    +

    Created At: {{ model_form_demo.created_at }}

    + Edit Test Model +{% endblock %} +endef + +define DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM +{% extends 'base.html' %} +{% block content %} +

    + {% if form.instance.pk %} + Update Test Model + {% else %} + Create Test Model + {% endif %} +

    +
    + {% csrf_token %} + {{ form.as_p }} + +
    +{% endblock %} +endef + +define DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST +{% extends 'base.html' %} +{% block content %} +

    Test Models List

    + + Create New Test Model +{% endblock %} +endef + +define DJANGO_MODEL_FORM_DEMO_URLS +from django.urls import path +from .views import ( + ModelFormDemoListView, + ModelFormDemoCreateView, + ModelFormDemoUpdateView, + ModelFormDemoDetailView, +) + +urlpatterns = [ + path("", ModelFormDemoListView.as_view(), name="model_form_demo_list"), + path("create/", ModelFormDemoCreateView.as_view(), name="model_form_demo_create"), + path( + "/update/", + ModelFormDemoUpdateView.as_view(), + name="model_form_demo_update", + ), + path("/", ModelFormDemoDetailView.as_view(), name="model_form_demo_detail"), +] +endef + +define DJANGO_MODEL_FORM_DEMO_VIEWS +from django.views.generic import ListView, CreateView, UpdateView, DetailView +from .models import ModelFormDemo +from .forms import ModelFormDemoForm + + +class ModelFormDemoListView(ListView): + model = ModelFormDemo + template_name = "model_form_demo_list.html" + context_object_name = "model_form_demos" + + +class ModelFormDemoCreateView(CreateView): + model = ModelFormDemo + form_class = ModelFormDemoForm + template_name = "model_form_demo_form.html" + + def form_valid(self, form): + form.instance.created_by = self.request.user + return super().form_valid(form) + + +class ModelFormDemoUpdateView(UpdateView): + model = ModelFormDemo + form_class = ModelFormDemoForm + template_name = "model_form_demo_form.html" + + +class ModelFormDemoDetailView(DetailView): + model = ModelFormDemo + template_name = "model_form_demo_detail.html" + context_object_name = "model_form_demo" +endef + +define DJANGO_PAYMENTS_ADMIN +from django.contrib import admin +from .models import Product, Order + +admin.site.register(Product) +admin.site.register(Order) +endef + +define DJANGO_PAYMENTS_FORM +from django import forms + + +class PaymentsForm(forms.Form): + stripeToken = forms.CharField(widget=forms.HiddenInput()) + amount = forms.DecimalField( + max_digits=10, decimal_places=2, widget=forms.HiddenInput() + ) +endef + +define DJANGO_PAYMENTS_MIGRATION_0002 +from django.db import migrations +import os +import secrets +import logging + +logger = logging.getLogger(__name__) + + +def generate_default_key(): + return "sk_test_" + secrets.token_hex(24) + + +def set_stripe_api_keys(apps, schema_editor): + # Get the Stripe API Key model + APIKey = apps.get_model("djstripe", "APIKey") + + # Fetch the keys from environment variables or generate default keys + test_secret_key = os.environ.get("STRIPE_TEST_SECRET_KEY", generate_default_key()) + live_secret_key = os.environ.get("STRIPE_LIVE_SECRET_KEY", generate_default_key()) + + logger.info("STRIPE_TEST_SECRET_KEY: %s", test_secret_key) + logger.info("STRIPE_LIVE_SECRET_KEY: %s", live_secret_key) + + # Check if the keys are not already in the database + if not APIKey.objects.filter(secret=test_secret_key).exists(): + APIKey.objects.create(secret=test_secret_key, livemode=False) + logger.info("Added test secret key to the database.") + else: + logger.info("Test secret key already exists in the database.") + + if not APIKey.objects.filter(secret=live_secret_key).exists(): + APIKey.objects.create(secret=live_secret_key, livemode=True) + logger.info("Added live secret key to the database.") + else: + logger.info("Live secret key already exists in the database.") + + +class Migration(migrations.Migration): + + dependencies = [ + ("payments", "0001_initial"), + ] + + operations = [ + migrations.RunPython(set_stripe_api_keys), + ] +endef + +define DJANGO_PAYMENTS_MIGRATION_0003 +from django.db import migrations + + +def create_initial_products(apps, schema_editor): + Product = apps.get_model("payments", "Product") + Product.objects.create(name="T-shirt", description="A cool T-shirt", price=20.00) + Product.objects.create(name="Mug", description="A nice mug", price=10.00) + Product.objects.create(name="Hat", description="A stylish hat", price=15.00) + + +class Migration(migrations.Migration): + dependencies = [ + ( + "payments", + "0002_set_stripe_api_keys", + ), + ] + + operations = [ + migrations.RunPython(create_initial_products), + ] +endef + +define DJANGO_PAYMENTS_MODELS +from django.db import models + + +class Product(models.Model): + name = models.CharField(max_length=100) + description = models.TextField() + price = models.DecimalField(max_digits=10, decimal_places=2) + + def __str__(self): + return self.name + + +class Order(models.Model): + product = models.ForeignKey(Product, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + stripe_checkout_session_id = models.CharField(max_length=200) + + def __str__(self): + return f"Order {self.id} for {self.product.name}" +endef + +define DJANGO_PAYMENTS_TEMPLATE_CANCEL +{% extends "base.html" %} +{% block title %}Cancel{% endblock %} +{% block content %} +

    Payment Cancelled

    +

    Your payment was cancelled.

    +{% endblock %} +endef + +define DJANGO_PAYMENTS_TEMPLATE_CHECKOUT +{% extends "base.html" %} +{% block title %}Checkout{% endblock %} +{% block content %} +

    Checkout

    +
    + {% csrf_token %} + +
    +{% endblock %} +endef + +define DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL +{% extends "base.html" %} +{% block title %}{{ product.name }}{% endblock %} +{% block content %} +

    {{ product.name }}

    +

    {{ product.description }}

    +

    Price: ${{ product.price }}

    +
    + {% csrf_token %} + + +
    +{% endblock %} +endef + +define DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST +{% extends "base.html" %} +{% block title %}Products{% endblock %} +{% block content %} +

    Products

    + +{% endblock %} +endef + +define DJANGO_PAYMENTS_TEMPLATE_SUCCESS +{% extends "base.html" %} +{% block title %}Success{% endblock %} +{% block content %} +

    Payment Successful

    +

    Thank you for your purchase!

    +{% endblock %} +endef + +define DJANGO_PAYMENTS_URLS +from django.urls import path +from .views import ( + CheckoutView, + SuccessView, + CancelView, + ProductListView, + ProductDetailView, +) + +urlpatterns = [ + path("", ProductListView.as_view(), name="product_list"), + path("product//", ProductDetailView.as_view(), name="product_detail"), + path("checkout/", CheckoutView.as_view(), name="checkout"), + path("success/", SuccessView.as_view(), name="success"), + path("cancel/", CancelView.as_view(), name="cancel"), +] +endef + +define DJANGO_PAYMENTS_VIEW +from django.conf import settings +from django.shortcuts import render, redirect, get_object_or_404 +from django.views.generic import TemplateView, View, ListView, DetailView +import stripe +from .models import Product, Order + +stripe.api_key = settings.STRIPE_TEST_SECRET_KEY + + +class ProductListView(ListView): + model = Product + template_name = "payments/product_list.html" + context_object_name = "products" + + +class ProductDetailView(DetailView): + model = Product + template_name = "payments/product_detail.html" + context_object_name = "product" + + +class CheckoutView(View): + template_name = "payments/checkout.html" + + def get(self, request, *args, **kwargs): + products = Product.objects.all() + return render(request, self.template_name, {"products": products}) + + def post(self, request, *args, **kwargs): + product_id = request.POST.get("product_id") + product = get_object_or_404(Product, id=product_id) + + session = stripe.checkout.Session.create( + payment_method_types=["card"], + line_items=[ + { + "price_data": { + "currency": "usd", + "product_data": { + "name": product.name, + }, + "unit_amount": int(product.price * 100), + }, + "quantity": 1, + } + ], + mode="payment", + success_url="http://localhost:8000/payments/success/", + cancel_url="http://localhost:8000/payments/cancel/", + ) + + Order.objects.create(product=product, stripe_checkout_session_id=session.id) + return redirect(session.url, code=303) + + +class SuccessView(TemplateView): + + template_name = "payments/success.html" + + +class CancelView(TemplateView): + + template_name = "payments/cancel.html" +endef + +define DJANGO_SEARCH_FORMS +from django import forms + + +class SearchForm(forms.Form): + query = forms.CharField(max_length=100, required=True, label="Search") + +endef + +define DJANGO_SEARCH_SETTINGS +SEARCH_MODELS = [ + # Add search models here. +] +endef + +define DJANGO_SEARCH_TEMPLATE +{% extends "base.html" %} +{% block body_class %}template-searchresults{% endblock %} +{% block title %}Search{% endblock %} +{% block content %} +

    Search

    +
    + + +
    + {% if search_results %} +
      + {% for result in search_results %} +
    • +

      + {{ result }} +

      + {% if result.search_description %}{{ result.search_description }}{% endif %} +
    • + {% endfor %} +
    + {% if search_results.has_previous %} + Previous + {% endif %} + {% if search_results.has_next %} + Next + {% endif %} + {% elif search_query %} + No results found + {% else %} + No results found. Try a test query? + {% endif %} +{% endblock %} +endef + +define DJANGO_SEARCH_URLS +from django.urls import path +from .views import SearchView + +urlpatterns = [ + path("search/", SearchView.as_view(), name="search"), +] +endef + +define DJANGO_SEARCH_UTILS +from django.apps import apps +from django.conf import settings + +def get_search_models(): + models = [] + for model_path in settings.SEARCH_MODELS: + app_label, model_name = model_path.split(".") + model = apps.get_model(app_label, model_name) + models.append(model) + return models +endef + +define DJANGO_SEARCH_VIEWS +from django.views.generic import ListView +from django.db import models +from django.db.models import Q +from .forms import SearchForm +from .utils import get_search_models + + +class SearchView(ListView): + template_name = "your_app/search_results.html" + context_object_name = "results" + paginate_by = 10 + + def get_queryset(self): + form = SearchForm(self.request.GET) + query = None + results = [] + + if form.is_valid(): + query = form.cleaned_data["query"] + search_models = get_search_models() + + for model in search_models: + fields = [f.name for f in model._meta.fields if isinstance(f, (models.CharField, models.TextField))] + queries = [Q(**{f"{field}__icontains": query}) for field in fields] + model_results = model.objects.filter(queries.pop()) + + for item in queries: + model_results = model_results.filter(item) + + results.extend(model_results) + + return results + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form"] = SearchForm(self.request.GET) + context["query"] = self.request.GET.get("query", "") + return context +endef + +define DJANGO_SETTINGS_AUTHENTICATION_BACKENDS +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", +] +endef + +define DJANGO_SETTINGS_BASE +# $(PROJECT_NAME) +# +# Uncomment next two lines to enable custom admin +# INSTALLED_APPS = [app for app in INSTALLED_APPS if app != 'django.contrib.admin'] +# INSTALLED_APPS.append('backend.apps.CustomAdminConfig') +import os # noqa +import dj_database_url # noqa + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +EXPLORER_CONNECTIONS = {"Default": "default"} +EXPLORER_DEFAULT_CONNECTION = "default" +LOGIN_REDIRECT_URL = "/" +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SILENCED_SYSTEM_CHECKS = ["django_recaptcha.recaptcha_test_key_error"] +BASE_DIR = os.path.dirname(PROJECT_DIR) +STATICFILES_DIRS = [] +WEBPACK_LOADER = { + "MANIFEST_FILE": os.path.join(BASE_DIR, "frontend/build/manifest.json"), +} +STATICFILES_DIRS.append(os.path.join(BASE_DIR, "frontend/build")) +TEMPLATES[0]["DIRS"].append(os.path.join(PROJECT_DIR, "templates")) +endef + +define DJANGO_SETTINGS_BASE_MINIMAL +# $(PROJECT_NAME) +import os # noqa +import dj_database_url # noqa + +INSTALLED_APPS.append("debug_toolbar") +INSTALLED_APPS.append("webpack_boilerplate") +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = os.path.dirname(PROJECT_DIR) +STATICFILES_DIRS = [] +STATICFILES_DIRS.append(os.path.join(BASE_DIR, "frontend/build")) +TEMPLATES[0]["DIRS"].append(os.path.join(PROJECT_DIR, "templates")) +WEBPACK_LOADER = { + "MANIFEST_FILE": os.path.join(BASE_DIR, "frontend/build/manifest.json"), +} +endef + +define DJANGO_SETTINGS_CRISPY_FORMS +CRISPY_TEMPLATE_PACK = "bootstrap5" +CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" +endef + +define DJANGO_SETTINGS_DATABASE +DATABASE_URL = os.environ.get("DATABASE_URL", "postgres://:@:/$(PROJECT_NAME)") +DATABASES["default"] = dj_database_url.parse(DATABASE_URL) +endef + +define DJANGO_SETTINGS_DEV +from .base import * # noqa + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +# SECURITY WARNING: define the correct hosts in production! +ALLOWED_HOSTS = ["*"] + +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +try: + from .local import * # noqa +except ImportError: + pass + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {message}", + "style": "{", + }, + "simple": { + "format": "{levelname} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + }, + "loggers": { + "django": { + "handlers": ["console"], + "level": "DEBUG", + "propagate": True, + }, + }, +} + +INTERNAL_IPS = [ + "127.0.0.1", +] + +MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") # noqa +MIDDLEWARE.append("hijack.middleware.HijackUserMiddleware") # noqa +INSTALLED_APPS.append("django.contrib.admindocs") # noqa +SECRET_KEY = "$(DJANGO_SETTINGS_SECRET_KEY)" +endef + + +define DJANGO_SETTINGS_HOME_PAGE +INSTALLED_APPS.append("home") +endef + +define DJANGO_SETTINGS_INSTALLED_APPS +INSTALLED_APPS.append("allauth") +INSTALLED_APPS.append("allauth.account") +INSTALLED_APPS.append("allauth.socialaccount") +INSTALLED_APPS.append("crispy_bootstrap5") +INSTALLED_APPS.append("crispy_forms") +INSTALLED_APPS.append("debug_toolbar") +INSTALLED_APPS.append("django_extensions") +INSTALLED_APPS.append("django_recaptcha") +INSTALLED_APPS.append("rest_framework") +INSTALLED_APPS.append("rest_framework.authtoken") +INSTALLED_APPS.append("webpack_boilerplate") +INSTALLED_APPS.append("explorer") +endef + +define DJANGO_SETTINGS_MIDDLEWARE +MIDDLEWARE.append("allauth.account.middleware.AccountMiddleware") +endef + +define DJANGO_SETTINGS_MODEL_FORM_DEMO +INSTALLED_APPS.append("model_form_demo") # noqa +endef + +define DJANGO_SETTINGS_PAYMENTS +DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id" +DJSTRIPE_WEBHOOK_VALIDATION = "retrieve_event" +STRIPE_PUBLISHABLE_KEY = os.environ.get("STRIPE_PUBLISHABLE_KEY") +STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY") +STRIPE_TEST_SECRET_KEY = os.environ.get("STRIPE_TEST_SECRET_KEY") +INSTALLED_APPS.append("payments") # noqa +INSTALLED_APPS.append("djstripe") # noqa +endef + +define DJANGO_SETTINGS_REST_FRAMEWORK +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" + ] +} +endef + +define DJANGO_SETTINGS_SITEUSER +INSTALLED_APPS.append("siteuser") # noqa +AUTH_USER_MODEL = "siteuser.User" +endef + +define DJANGO_SETTINGS_PROD +from .base import * # noqa +from backend.utils import get_ec2_metadata + +DEBUG = False + +try: + from .local import * # noqa +except ImportError: + pass + +LOCAL_IPV4 = get_ec2_metadata() +ALLOWED_HOSTS.append(LOCAL_IPV4) # noqa +endef + +define DJANGO_SETTINGS_THEMES +THEMES = [ + ("light", "Light Theme"), + ("dark", "Dark Theme"), +] +endef + +define DJANGO_SITEUSER_ADMIN +from django.contrib.auth.admin import UserAdmin +from django.contrib import admin + +from .models import User + +admin.site.register(User, UserAdmin) +endef + +define DJANGO_SITEUSER_EDIT_TEMPLATE +{% extends 'base.html' %} +{% load crispy_forms_tags %} +{% block content %} +

    Edit User

    + {% crispy form %} +{% endblock %} +endef + +define DJANGO_SITEUSER_FORM +from django import forms +from django.contrib.auth.forms import UserChangeForm +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Fieldset, ButtonHolder, Submit +from .models import User + + +class SiteUserForm(UserChangeForm): + bio = forms.CharField(widget=forms.Textarea(attrs={"id": "editor"}), required=False) + + class Meta(UserChangeForm.Meta): + model = User + fields = ("username", "user_theme_preference", "bio", "rate") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_method = "post" + self.helper.layout = Layout( + Fieldset( + "Edit Your Profile", + "username", + "user_theme_preference", + "bio", + "rate", + ), + ButtonHolder(Submit("submit", "Save", css_class="btn btn-primary")), + ) +endef + +define DJANGO_SITEUSER_MODEL +from django.db import models +from django.contrib.auth.models import AbstractUser, Group, Permission +from django.conf import settings + + +class User(AbstractUser): + groups = models.ManyToManyField(Group, related_name="siteuser_set", blank=True) + user_permissions = models.ManyToManyField( + Permission, related_name="siteuser_set", blank=True + ) + + user_theme_preference = models.CharField( + max_length=10, choices=settings.THEMES, default="light" + ) + + bio = models.TextField(blank=True, null=True) + rate = models.FloatField(blank=True, null=True) +endef + +define DJANGO_SITEUSER_URLS +from django.urls import path +from .views import UserProfileView, UpdateThemePreferenceView, UserEditView + +urlpatterns = [ + path("profile/", UserProfileView.as_view(), name="user-profile"), + path( + "update_theme_preference/", + UpdateThemePreferenceView.as_view(), + name="update_theme_preference", + ), + path("/edit/", UserEditView.as_view(), name="user-edit"), +] +endef + +define DJANGO_SITEUSER_VIEW +import json + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import JsonResponse +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import DetailView +from django.views.generic.edit import UpdateView +from django.urls import reverse_lazy + +from .models import User +from .forms import SiteUserForm + + +class UserProfileView(LoginRequiredMixin, DetailView): + model = User + template_name = "profile.html" + + def get_object(self, queryset=None): + return self.request.user + + +@method_decorator(csrf_exempt, name="dispatch") +class UpdateThemePreferenceView(View): + def post(self, request, *args, **kwargs): + try: + data = json.loads(request.body.decode("utf-8")) + new_theme = data.get("theme") + user = request.user + user.user_theme_preference = new_theme + user.save() + response_data = {"theme": new_theme} + return JsonResponse(response_data) + except json.JSONDecodeError as e: + return JsonResponse({"error": e}, status=400) + + def http_method_not_allowed(self, request, *args, **kwargs): + return JsonResponse({"error": "Invalid request method"}, status=405) + + +class UserEditView(LoginRequiredMixin, UpdateView): + model = User + template_name = "user_edit.html" # Create this template in your templates folder + form_class = SiteUserForm + + def get_success_url(self): + # return reverse_lazy("user-profile", kwargs={"pk": self.object.pk}) + return reverse_lazy("user-profile") +endef + +define DJANGO_SITEUSER_VIEW_TEMPLATE +{% extends 'base.html' %} +{% block content %} +

    User Profile

    +
    + Edit +
    +

    Username: {{ user.username }}

    +

    Theme: {{ user.user_theme_preference }}

    +

    Bio: {{ user.bio|default:""|safe }}

    +

    Rate: {{ user.rate|default:"" }}

    +{% endblock %} +endef + +define DJANGO_URLS +from django.contrib import admin +from django.urls import path, include +from django.conf import settings + +urlpatterns = [ + path("django/", admin.site.urls), +] +endef + +define DJANGO_URLS_ALLAUTH +urlpatterns += [path("accounts/", include("allauth.urls"))] +endef + +define DJANGO_URLS_API +from rest_framework import routers # noqa +from .api import UserViewSet, api # noqa + +router = routers.DefaultRouter() +router.register(r"users", UserViewSet) +# urlpatterns += [path("api/", include(router.urls))] +urlpatterns += [path("api/", api.urls)] +endef + +define DJANGO_URLS_DEBUG_TOOLBAR +if settings.DEBUG: + urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))] +endef + +define DJANGO_URLS_HOME_PAGE +urlpatterns += [path("", include("home.urls"))] +endef + +define DJANGO_URLS_LOGGING_DEMO +urlpatterns += [path("logging-demo/", include("logging_demo.urls"))] +endef + +define DJANGO_URLS_MODEL_FORM_DEMO +urlpatterns += [path("model-form-demo/", include("model_form_demo.urls"))] +endef + +define DJANGO_URLS_PAYMENTS +urlpatterns += [path("payments/", include("payments.urls"))] +endef + +define DJANGO_URLS_SITEUSER +urlpatterns += [path("user/", include("siteuser.urls"))] +endef + +define DJANGO_UTILS +from django.urls import URLResolver +import requests + + +def get_ec2_metadata(): + try: + # Step 1: Get the token + token_url = "http://169.254.169.254/latest/api/token" + headers = {"X-aws-ec2-metadata-token-ttl-seconds": "21600"} + response = requests.put(token_url, headers=headers) + response.raise_for_status() # Raise an error for bad responses + + token = response.text + + # Step 2: Use the token to get the instance metadata + metadata_url = "http://169.254.169.254/latest/meta-data/local-ipv4" + headers = {"X-aws-ec2-metadata-token": token} + response = requests.get(metadata_url, headers=headers) + response.raise_for_status() # Raise an error for bad responses + + metadata = response.text + return metadata + except requests.RequestException as e: + print(f"Error retrieving EC2 metadata: {e}") + return None + + +# Function to remove a specific URL pattern based on its route (including catch-all) +def remove_urlpattern(urlpatterns, route_to_remove): + urlpatterns[:] = [ + urlpattern + for urlpattern in urlpatterns + if not ( + isinstance(urlpattern, URLResolver) + and urlpattern.pattern._route == route_to_remove + ) + ] +endef + +define EB_CUSTOM_ENV_EC2_USER +files: + "/home/ec2-user/.bashrc": + mode: "000644" + owner: ec2-user + group: ec2-user + content: | + # .bashrc + + # Source global definitions + if [ -f /etc/bashrc ]; then + . /etc/bashrc + fi + + # User specific aliases and functions + set -o vi + + source <(sed -E -n 's/[^#]+/export &/ p' /opt/elasticbeanstalk/deployment/custom_env_var) +endef + +define EB_CUSTOM_ENV_VAR_FILE +#!/bin/bash + +# Via https://aws.amazon.com/premiumsupport/knowledge-center/elastic-beanstalk-env-variables-linux2/ + +#Create a copy of the environment variable file. +cat /opt/elasticbeanstalk/deployment/env | perl -p -e 's/(.*)=(.*)/export $$1="$$2"/;' > /opt/elasticbeanstalk/deployment/custom_env_var + +#Set permissions to the custom_env_var file so this file can be accessed by any user on the instance. You can restrict permissions as per your requirements. +chmod 644 /opt/elasticbeanstalk/deployment/custom_env_var + +# add the virtual env path in. +VENV=/var/app/venv/`ls /var/app/venv` +cat <> /opt/elasticbeanstalk/deployment/custom_env_var +VENV=$$ENV +EOF + +#Remove duplicate files upon deployment. +rm -f /opt/elasticbeanstalk/deployment/*.bak +endef + +define GIT_IGNORE +__pycache__ +*.pyc +dist/ +node_modules/ +_build/ +.elasticbeanstalk/ +db.sqlite3 +static/ +backend/var +endef + +define JENKINS_FILE +pipeline { + agent any + stages { + stage('') { + steps { + echo '' + } + } + } +} +endef + +define MAKEFILE_CUSTOM +# Custom Makefile +# Add your custom makefile commands here +# +# PROJECT_NAME := my-new-project +endef + +define PIP_INSTALL_REQUIREMENTS_TEST +pytest +pytest-runner +coverage +pytest-mock +pytest-cov +hypothesis +selenium +pytest-django +factory-boy +flake8 +tox +endef + +define PROGRAMMING_INTERVIEW +from rich import print as rprint +from rich.console import Console +from rich.panel import Panel + +import argparse +import locale +import math +import time + +import code # noqa +import readline # noqa +import rlcompleter # noqa + + +locale.setlocale(locale.LC_ALL, "en_US.UTF-8") + + +class DataStructure: + # Data Structure: Binary Tree + class TreeNode: + def __init__(self, value=0, left=None, right=None): + self.value = value + self.left = left + self.right = right + + # Data Structure: Stack + class Stack: + def __init__(self): + self.items = [] + + def push(self, item): + self.items.append(item) + + def pop(self): + if not self.is_empty(): + return self.items.pop() + return None + + def peek(self): + if not self.is_empty(): + return self.items[-1] + return None + + def is_empty(self): + return len(self.items) == 0 + + def size(self): + return len(self.items) + + # Data Structure: Queue + class Queue: + def __init__(self): + self.items = [] + + def enqueue(self, item): + self.items.append(item) + + def dequeue(self): + if not self.is_empty(): + return self.items.pop(0) + return None + + def is_empty(self): + return len(self.items) == 0 + + def size(self): + return len(self.items) + + # Data Structure: Linked List + class ListNode: + def __init__(self, value=0, next=None): + self.value = value + self.next = next + + +class Interview(DataStructure): + + # Protected methods for factorial calculation + def _factorial_recursive(self, n): + if n == 0: + return 1 + return n * self._factorial_recursive(n - 1) + + def _factorial_divide_and_conquer(self, low, high): + if low > high: + return 1 + if low == high: + return low + mid = (low + high) // 2 + return self._factorial_divide_and_conquer( + low, mid + ) * self._factorial_divide_and_conquer(mid + 1, high) + + # Recursive Factorial with Timing + def factorial_recursive(self, n): + start_time = time.time() # Start timing + result = self._factorial_recursive(n) # Calculate factorial + end_time = time.time() # End timing + elapsed_time = end_time - start_time + return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" + + # Iterative Factorial with Timing + def factorial_iterative(self, n): + start_time = time.time() # Start timing + result = 1 + for i in range(1, n + 1): + result *= i + end_time = time.time() # End timing + elapsed_time = end_time - start_time + return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" + + # Divide and Conquer Factorial with Timing + def factorial_divide_and_conquer(self, n): + start_time = time.time() # Start timing + result = self._factorial_divide_and_conquer(1, n) # Calculate factorial + end_time = time.time() # End timing + elapsed_time = end_time - start_time + return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" + + # Built-in Factorial with Timing + def factorial_builtin(self, n): + start_time = time.time() # Start timing + result = math.factorial(n) # Calculate factorial using built-in + end_time = time.time() # End timing + + # Calculate elapsed time + elapsed_time = end_time - start_time + + # Print complexity and runtime + return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" + + # Recursion: Fibonacci + def fibonacci_recursive(self, n): + if n <= 1: + return n + return self.fibonacci_recursive(n - 1) + self.fibonacci_recursive(n - 2) + + # Iteration: Fibonacci + def fibonacci_iterative(self, n): + if n <= 1: + return n + a, b = 0, 1 + for _ in range(n - 1): + a, b = b, a + b + return b + + # Searching: Linear Search + def linear_search(self, arr, target): + for i, value in enumerate(arr): + if value == target: + return i + return -1 + + # Searching: Binary Search + def binary_search(self, arr, target): + left, right = 0, len(arr) - 1 + while left <= right: + mid = (left + right) // 2 + if arr[mid] == target: + return mid + elif arr[mid] < target: + left = mid + 1 + else: + right = mid - 1 + return -1 + + # Sorting: Bubble Sort + def bubble_sort(self, arr): + n = len(arr) + for i in range(n): + for j in range(0, n - i - 1): + if arr[j] > arr[j + 1]: + arr[j], arr[j + 1] = arr[j + 1], arr[j] + return arr + + # Sorting: Merge Sort + def merge_sort(self, arr): + if len(arr) > 1: + mid = len(arr) // 2 + left_half = arr[:mid] + right_half = arr[mid:] + + self.merge_sort(left_half) + self.merge_sort(right_half) + + i = j = k = 0 + + while i < len(left_half) and j < len(right_half): + if left_half[i] < right_half[j]: + arr[k] = left_half[i] + i += 1 + else: + arr[k] = right_half[j] + j += 1 + k += 1 + + while i < len(left_half): + arr[k] = left_half[i] + i += 1 + k += 1 + + while j < len(right_half): + arr[k] = right_half[j] + j += 1 + k += 1 + return arr + + def insert_linked_list(self, head, value): + new_node = self.ListNode(value) + if not head: + return new_node + current = head + while current.next: + current = current.next + current.next = new_node + return head + + def print_linked_list(self, head): + current = head + while current: + print(current.value, end=" -> ") + current = current.next + print("None") + + def inorder_traversal(self, root): + return ( + self.inorder_traversal(root.left) + + [root.value] + + self.inorder_traversal(root.right) + if root + else [] + ) + + def preorder_traversal(self, root): + return ( + [root.value] + + self.preorder_traversal(root.left) + + self.preorder_traversal(root.right) + if root + else [] + ) + + def postorder_traversal(self, root): + return ( + self.postorder_traversal(root.left) + + self.postorder_traversal(root.right) + + [root.value] + if root + else [] + ) + + # Graph Algorithms: Depth-First Search + def dfs(self, graph, start): + visited, stack = set(), [start] + while stack: + vertex = stack.pop() + if vertex not in visited: + visited.add(vertex) + stack.extend(set(graph[vertex]) - visited) + return visited + + # Graph Algorithms: Breadth-First Search + def bfs(self, graph, start): + visited, queue = set(), [start] + while queue: + vertex = queue.pop(0) + if vertex not in visited: + visited.add(vertex) + queue.extend(set(graph[vertex]) - visited) + return visited + + +def setup_readline(local): + + # Enable tab completion + readline.parse_and_bind("tab: complete") + # Optionally, you can set the completer function manually + readline.set_completer(rlcompleter.Completer(local).complete) + + +def main(): + + console = Console() + interview = Interview() + + parser = argparse.ArgumentParser(description="Programming Interview Questions") + + parser.add_argument( + "-f", "--factorial", type=int, help="Factorial algorithm examples" + ) + parser.add_argument("--fibonacci", type=int, help="Fibonacci algorithm examples") + parser.add_argument( + "--search", action="store_true", help="Search algorithm examples" + ) + parser.add_argument("--sort", action="store_true", help="Search algorithm examples") + parser.add_argument("--stack", action="store_true", help="Stack algorithm examples") + parser.add_argument("--queue", action="store_true", help="Queue algorithm examples") + parser.add_argument( + "--list", action="store_true", help="Linked List algorithm examples" + ) + parser.add_argument( + "--tree", action="store_true", help="Tree traversal algorithm examples" + ) + parser.add_argument("--graph", action="store_true", help="Graph algorithm examples") + parser.add_argument( + "-i", "--interactive", action="store_true", help="Interactive mode" + ) + + args = parser.parse_args() + + if args.factorial: + # Factorial examples + console.rule("Factorial Examples") + rprint( + Panel( + "[bold cyan]Recursive Factorial - Time Complexity: O(n)[/bold cyan]\n" + + str(interview.factorial_recursive(args.factorial)), + title="Factorial Recursive", + ) + ) + rprint( + Panel( + "[bold cyan]Iterative Factorial - Time Complexity: O(n)[/bold cyan]\n" + + str(interview.factorial_iterative(args.factorial)), + title="Factorial Iterative", + ) + ) + rprint( + Panel( + "[bold cyan]Built-in Factorial - Time Complexity: O(n)[/bold cyan]\n" + + str(interview.factorial_builtin(args.factorial)), + title="Factorial Built-in", + ) + ) + rprint( + Panel( + "[bold cyan]Divide and Conquer Factorial - Time Complexity: O(n log n)[/bold cyan]\n" + + str(interview.factorial_divide_and_conquer(args.factorial)), + title="Factorial Divide and Conquer", + ) + ) + exit() + + if args.fibonacci: + # Fibonacci examples + console.rule("Fibonacci Examples") + rprint( + Panel( + str(interview.fibonacci_recursive(args.fibonacci)), + title="Fibonacci Recursive", + ) + ) + rprint( + Panel( + str(interview.fibonacci_iterative(args.fibonacci)), + title="Fibonacci Iterative", + ) + ) + exit() + + if args.search: + # Searching examples + console.rule("Searching Examples") + array = [1, 3, 5, 7, 9] + rprint(Panel(str(interview.linear_search(array, 5)), title="Linear Search")) + rprint(Panel(str(interview.binary_search(array, 5)), title="Binary Search")) + exit() + + if args.sort: + # Sorting examples + console.rule("Sorting Examples") + unsorted_array = [64, 34, 25, 12, 22, 11, 90] + rprint( + Panel( + str(interview.bubble_sort(unsorted_array.copy())), title="Bubble Sort" + ) + ) + rprint( + Panel(str(interview.merge_sort(unsorted_array.copy())), title="Merge Sort") + ) + exit() + + if args.stack: + # Stack example + console.rule("Stack Example") + stack = interview.Stack() + stack.push(1) + stack.push(2) + stack.push(3) + rprint(Panel(str(stack.pop()), title="Stack Pop")) + rprint(Panel(str(stack.peek()), title="Stack Peek")) + rprint(Panel(str(stack.size()), title="Stack Size")) + + if args.queue: + # Queue example + console.rule("Queue Example") + queue = interview.Queue() + queue.enqueue(1) + queue.enqueue(2) + queue.enqueue(3) + rprint(Panel(str(queue.dequeue()), title="Queue Dequeue")) + rprint(Panel(str(queue.is_empty()), title="Queue Is Empty")) + rprint(Panel(str(queue.size()), title="Queue Size")) + + if args.list: + # Linked List example + console.rule("Linked List Example") + head = None + head = interview.insert_linked_list(head, 1) + head = interview.insert_linked_list(head, 2) + head = interview.insert_linked_list(head, 3) + interview.print_linked_list(head) # Output: 1 -> 2 -> 3 -> None + + if args.tree: + # Tree Traversal example + console.rule("Tree Traversal Example") + root = interview.TreeNode(1) + root.left = interview.TreeNode(2) + root.right = interview.TreeNode(3) + root.left.left = interview.TreeNode(4) + root.left.right = interview.TreeNode(5) + rprint(Panel(str(interview.inorder_traversal(root)), title="Inorder Traversal")) + rprint( + Panel(str(interview.preorder_traversal(root)), title="Preorder Traversal") + ) + rprint( + Panel(str(interview.postorder_traversal(root)), title="Postorder Traversal") + ) + + if args.graph: + # Graph Algorithms example + console.rule("Graph Algorithms Example") + graph = { + "A": ["B", "C"], + "B": ["A", "D", "E"], + "C": ["A", "F"], + "D": ["B"], + "E": ["B", "F"], + "F": ["C", "E"], + } + rprint(Panel(str(interview.dfs(graph, "A")), title="DFS")) + rprint(Panel(str(interview.bfs(graph, "A")), title="BFS")) + + if args.interactive: + # Starting interactive session with tab completion + setup_readline(locals()) + banner = "Interactive programming interview session started. Type 'exit()' or 'Ctrl-D' to exit." + code.interact( + banner=banner, + local=locals(), + exitmsg="Great interview!", + ) + + +if __name__ == "__main__": + main() + +endef + +define PYTHON_CI_YAML +name: Build Wheels +endef + +define PYTHON_LICENSE_TXT +MIT License + +Copyright (c) [YEAR] [OWNER NAME] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +endef + +define PYTHON_PROJECT_TOML +[build-system] +endef + +define SEPARATOR +.==========================================================================================================================================. +| | +| _|_|_| _| _| _| _| _| _|_| _| _| | +| _| _| _| _|_| _|_| _|_| _|_|_| _|_|_|_| _|_| _|_| _|_|_| _| _| _|_| _| _| _|_| | +| _|_|_| _|_| _| _| _| _|_|_|_| _| _| _| _| _| _| _| _|_| _|_|_|_| _|_|_|_| _| _| _|_|_|_| | +| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| | +| _| _| _|_| _| _|_|_| _|_|_| _|_| _| _| _|_|_| _| _| _|_|_| _| _| _| _|_|_| | +| _| | +| _| | +`==========================================================================================================================================' +endef + +define TINYMCE_JS +import tinymce from 'tinymce'; +import 'tinymce/icons/default'; +import 'tinymce/themes/silver'; +import 'tinymce/skins/ui/oxide/skin.css'; +import 'tinymce/plugins/advlist'; +import 'tinymce/plugins/code'; +import 'tinymce/plugins/emoticons'; +import 'tinymce/plugins/emoticons/js/emojis'; +import 'tinymce/plugins/link'; +import 'tinymce/plugins/lists'; +import 'tinymce/plugins/table'; +import 'tinymce/models/dom'; + +tinymce.init({ + selector: 'textarea#editor', + plugins: 'advlist code emoticons link lists table', + toolbar: 'bold italic | bullist numlist | link emoticons', + skin: false, + content_css: false, +}); +endef + +define WAGTAIL_BASE_TEMPLATE +{% load static wagtailcore_tags wagtailuserbar webpack_loader %} + + + + + + {% block title %} + {% if page.seo_title %} + {{ page.seo_title }} + {% else %} + {{ page.title }} + {% endif %} + {% endblock %} + {% block title_suffix %} + {% wagtail_site as current_site %} + {% if current_site and current_site.site_name %}- {{ current_site.site_name }}{% endif %} + {% endblock %} + + {% if page.search_description %}{% endif %} + + {# Force all links in the live preview panel to be opened in a new tab #} + {% if request.in_preview_panel %}{% endif %} + {% stylesheet_pack 'app' %} + {% block extra_css %}{# Override this in templates to add extra stylesheets #}{% endblock %} + + {% include 'favicon.html' %} + {% csrf_token %} + + +
    + {% wagtailuserbar %} +
    + {% include 'header.html' %} + {% if messages %} +
    + {% for message in messages %} + + {% endfor %} +
    + {% endif %} +
    + {% block content %}{% endblock %} +
    +
    + {% include 'footer.html' %} + {% include 'offcanvas.html' %} + {% javascript_pack 'app' %} + {% block extra_js %}{# Override this in templates to add extra javascript #}{% endblock %} + + +endef + +define WAGTAIL_BLOCK_CAROUSEL + +endef + +define WAGTAIL_BLOCK_MARKETING +{% load wagtailcore_tags %} +
    + {% if block.value.images.0 %} + {% include 'blocks/carousel_block.html' %} + {% else %} + {{ self.title }} + {{ self.content }} + {% endif %} +
    +endef + +define WAGTAIL_CONTACT_PAGE_LANDING +{% extends 'base.html' %} +{% block content %}

    Thank you!

    {% endblock %} +endef + +define WAGTAIL_CONTACT_PAGE_MODEL +from django.db import models +from modelcluster.fields import ParentalKey +from wagtail.admin.panels import ( + FieldPanel, FieldRowPanel, + InlinePanel, MultiFieldPanel +) +from wagtail.fields import RichTextField +from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField + + +class FormField(AbstractFormField): + page = ParentalKey('ContactPage', on_delete=models.CASCADE, related_name='form_fields') + + +class ContactPage(AbstractEmailForm): + intro = RichTextField(blank=True) + thank_you_text = RichTextField(blank=True) + + content_panels = AbstractEmailForm.content_panels + [ + FieldPanel('intro'), + InlinePanel('form_fields', label="Form fields"), + FieldPanel('thank_you_text'), + MultiFieldPanel([ + FieldRowPanel([ + FieldPanel('from_address', classname="col6"), + FieldPanel('to_address', classname="col6"), + ]), + FieldPanel('subject'), + ], "Email"), + ] + + class Meta: + verbose_name = "Contact Page" +endef + +define WAGTAIL_CONTACT_PAGE_TEMPLATE +{% extends 'base.html' %} +{% load crispy_forms_tags static wagtailcore_tags %} +{% block content %} +

    {{ page.title }}

    + {{ page.intro|richtext }} +
    + {% csrf_token %} + {{ form.as_p }} + +
    +{% endblock %} +endef + +define WAGTAIL_CONTACT_PAGE_TEST +from django.test import TestCase +from wagtail.test.utils import WagtailPageTestCase +from wagtail.models import Page + +from contactpage.models import ContactPage, FormField + +class ContactPageTest(TestCase, WagtailPageTestCase): + def test_contact_page_creation(self): + # Create a ContactPage instance + contact_page = ContactPage( + title='Contact', + intro='Welcome to our contact page!', + thank_you_text='Thank you for reaching out.' + ) + + # Save the ContactPage instance + self.assertEqual(contact_page.save_revision().publish().get_latest_revision_as_page(), contact_page) + + def test_form_field_creation(self): + # Create a ContactPage instance + contact_page = ContactPage( + title='Contact', + intro='Welcome to our contact page!', + thank_you_text='Thank you for reaching out.' + ) + # Save the ContactPage instance + contact_page_revision = contact_page.save_revision() + contact_page_revision.publish() + + # Create a FormField associated with the ContactPage + form_field = FormField( + page=contact_page, + label='Your Name', + field_type='singleline', + required=True + ) + form_field.save() + + # Retrieve the ContactPage from the database + contact_page_from_db = Page.objects.get(id=contact_page.id).specific + + # Check if the FormField is associated with the ContactPage + self.assertEqual(contact_page_from_db.form_fields.first(), form_field) + + def test_contact_page_form_submission(self): + # Create a ContactPage instance + contact_page = ContactPage( + title='Contact', + intro='Welcome to our contact page!', + thank_you_text='Thank you for reaching out.' + ) + # Save the ContactPage instance + contact_page_revision = contact_page.save_revision() + contact_page_revision.publish() + + # Simulate a form submission + form_data = { + 'your_name': 'John Doe', + # Add other form fields as needed + } + + response = self.client.post(contact_page.url, form_data) + + # Check if the form submission is successful (assuming a 302 redirect) + self.assertEqual(response.status_code, 302) + + # You may add more assertions based on your specific requirements +endef + +define WAGTAIL_HEADER_PREFIX +{% load wagtailcore_tags %} +{% wagtail_site as current_site %} +endef + +define WAGTAIL_HOME_PAGE_MODEL +from wagtail.models import Page +from wagtail.fields import StreamField +from wagtail import blocks +from wagtail.admin.panels import FieldPanel +from wagtail.images.blocks import ImageChooserBlock + + +class MarketingBlock(blocks.StructBlock): + title = blocks.CharBlock(required=False, help_text="Enter the block title") + content = blocks.RichTextBlock(required=False, help_text="Enter the block content") + images = blocks.ListBlock( + ImageChooserBlock(required=False), + help_text="Select one or two images for column display. Select three or more images for carousel display.", + ) + image = ImageChooserBlock( + required=False, help_text="Select one image for background display." + ) + block_class = blocks.CharBlock( + required=False, + help_text="Enter a CSS class for styling the marketing block", + classname="full title", + default="vh-100 bg-secondary", + ) + image_class = blocks.CharBlock( + required=False, + help_text="Enter a CSS class for styling the column display image(s)", + classname="full title", + default="img-thumbnail p-5", + ) + layout_class = blocks.CharBlock( + required=False, + help_text="Enter a CSS class for styling the layout.", + classname="full title", + default="d-flex flex-row", + ) + + class Meta: + icon = "placeholder" + template = "blocks/marketing_block.html" + + +class HomePage(Page): + template = "home/home_page.html" # Create a template for rendering the home page + + marketing_blocks = StreamField( + [ + ("marketing_block", MarketingBlock()), + ], + blank=True, + null=True, + use_json_field=True, + ) + content_panels = Page.content_panels + [ + FieldPanel("marketing_blocks"), + ] + + class Meta: + verbose_name = "Home Page" +endef + +define WAGTAIL_HOME_PAGE_TEMPLATE +{% extends "base.html" %} +{% load wagtailcore_tags %} +{% block content %} +
    + {% for block in page.marketing_blocks %} + {% include_block block %} + {% endfor %} +
    +{% endblock %} +endef + +define WAGTAIL_PRIVACY_PAGE_MODEL +from wagtail.models import Page +from wagtail.admin.panels import FieldPanel +from wagtailmarkdown.fields import MarkdownField + + +class PrivacyPage(Page): + """ + A Wagtail Page model for the Privacy Policy page. + """ + + template = "privacy_page.html" + + body = MarkdownField() + + content_panels = Page.content_panels + [ + FieldPanel("body", classname="full"), + ] + + class Meta: + verbose_name = "Privacy Page" +endef + +define WAGTAIL_PRIVACY_PAGE_TEMPLATE +{% extends 'base.html' %} +{% load wagtailmarkdown %} +{% block content %}
    {{ page.body|markdown }}
    {% endblock %} +endef + +define WAGTAIL_SEARCH_TEMPLATE +{% extends "base.html" %} +{% load static wagtailcore_tags %} +{% block body_class %}template-searchresults{% endblock %} +{% block title %}Search{% endblock %} +{% block content %} +

    Search

    +
    + + +
    + {% if search_results %} +
      + {% for result in search_results %} +
    • +

      + {{ result }} +

      + {% if result.search_description %}{{ result.search_description }}{% endif %} +
    • + {% endfor %} +
    + {% if search_results.has_previous %} + Previous + {% endif %} + {% if search_results.has_next %} + Next + {% endif %} + {% elif search_query %} + No results found + {% else %} + No results found. Try a test query? + {% endif %} +{% endblock %} +endef + +define WAGTAIL_SEARCH_URLS +from django.urls import path +from .views import search + +urlpatterns = [path("", search, name="search")] +endef + +define WAGTAIL_SETTINGS +INSTALLED_APPS.append("wagtail_color_panel") +INSTALLED_APPS.append("wagtail_modeladmin") +INSTALLED_APPS.append("wagtail.contrib.settings") +INSTALLED_APPS.append("wagtailmarkdown") +INSTALLED_APPS.append("wagtailmenus") +INSTALLED_APPS.append("wagtailseo") +TEMPLATES[0]["OPTIONS"]["context_processors"].append( + "wagtail.contrib.settings.context_processors.settings" +) +TEMPLATES[0]["OPTIONS"]["context_processors"].append( + "wagtailmenus.context_processors.wagtailmenus" +) +endef + +define WAGTAIL_SITEPAGE_MODEL +from wagtail.models import Page + + +class SitePage(Page): + template = "sitepage/site_page.html" + + class Meta: + verbose_name = "Site Page" +endef + +define WAGTAIL_SITEPAGE_TEMPLATE +{% extends 'base.html' %} +{% block content %} +

    {{ page.title }}

    +{% endblock %} +endef + +define WAGTAIL_URLS +from django.conf import settings +from django.urls import include, path +from django.contrib import admin + +from wagtail.admin import urls as wagtailadmin_urls +from wagtail.documents import urls as wagtaildocs_urls + +from search import views as search_views + +urlpatterns = [ + path("django/", admin.site.urls), + path("wagtail/", include(wagtailadmin_urls)), + path("documents/", include(wagtaildocs_urls)), + path("search/", search_views.search, name="search"), +] + +if settings.DEBUG: + from django.conf.urls.static import static + from django.contrib.staticfiles.urls import staticfiles_urlpatterns + + # Serve static and media files from development server + urlpatterns += staticfiles_urlpatterns() + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +endef + +define WAGTAIL_URLS_HOME +urlpatterns += [ + # For anything not caught by a more specific rule above, hand over to + # Wagtail's page serving mechanism. This should be the last pattern in + # the list: + path("", include("wagtail.urls")), + # Alternatively, if you want Wagtail pages to be served from a subpath + # of your site, rather than the site root: + # path("pages/", include("wagtail.urls"), +] +endef + +define WEBPACK_CONFIG_JS +const path = require('path'); + +module.exports = { + mode: 'development', + entry: './src/index.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, +}; +endef + +define WEBPACK_INDEX_HTML + + + + + + Hello, Webpack! + + + + + +endef + +define WEBPACK_INDEX_JS +const message = "Hello, World!"; +console.log(message); +endef + +define WEBPACK_REVEAL_CONFIG_JS +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +module.exports = { + mode: 'development', + entry: './src/index.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, + module: { + rules: [ + { + test: /\.css$$/, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + ], + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: 'bundle.css', + }), + ], +}; +endef + +define WEBPACK_REVEAL_INDEX_HTML + + + + + + Project Makefile + + +
    +
    +
    + Slide 1: Draw some circles +
    +
    + Slide 2: Draw the rest of the owl +
    +
    +
    + + +endef + +define WEBPACK_REVEAL_INDEX_JS +import 'reveal.js/dist/reveal.css'; +import 'reveal.js/dist/theme/black.css'; +import Reveal from 'reveal.js'; +import RevealNotes from 'reveal.js/plugin/notes/notes.js'; +Reveal.initialize({ slideNumber: true, plugins: [ RevealNotes ]}); +endef + +# ------------------------------------------------------------------------------ +# Export variables used by phony target rules +# ------------------------------------------------------------------------------ + +export DJANGO_ALLAUTH_BASE_TEMPLATE +export DJANGO_API_SERIALIZERS +export DJANGO_API_VIEWS +export DJANGO_APP_TESTS +export DJANGO_BACKEND_APPS +export DJANGO_BASE_TEMPLATE +export DJANGO_CUSTOM_ADMIN +export DJANGO_DOCKERCOMPOSE +export DJANGO_DOCKERFILE +export DJANGO_FAVICON_TEMPLATE +export DJANGO_FOOTER_TEMPLATE +export DJANGO_FRONTEND_APP +export DJANGO_FRONTEND_APP_CONFIG +export DJANGO_FRONTEND_BABELRC +export DJANGO_FRONTEND_COMPONENTS +export DJANGO_FRONTEND_COMPONENT_CLOCK +export DJANGO_FRONTEND_COMPONENT_ERROR +export DJANGO_FRONTEND_COMPONENT_USER_MENU +export DJANGO_FRONTEND_CONTEXT_INDEX +export DJANGO_FRONTEND_CONTEXT_USER_PROVIDER +export DJANGO_FRONTEND_ESLINTRC +export DJANGO_FRONTEND_OFFCANVAS_TEMPLATE +export DJANGO_FRONTEND_PORTAL +export DJANGO_FRONTEND_STYLES +export DJANGO_FRONTEND_THEME_BLUE +export DJANGO_FRONTEND_THEME_TOGGLER +export DJANGO_HEADER_TEMPLATE +export DJANGO_HOME_PAGE_ADMIN +export DJANGO_HOME_PAGE_MODELS +export DJANGO_HOME_PAGE_TEMPLATE +export DJANGO_HOME_PAGE_URLS +export DJANGO_HOME_PAGE_VIEWS +export DJANGO_LOGGING_DEMO_ADMIN +export DJANGO_LOGGING_DEMO_MODELS +export DJANGO_LOGGING_DEMO_SETTINGS +export DJANGO_LOGGING_DEMO_URLS +export DJANGO_LOGGING_DEMO_VIEWS +export DJANGO_MANAGE_PY +export DJANGO_MODEL_FORM_DEMO_ADMIN +export DJANGO_MODEL_FORM_DEMO_FORMS +export DJANGO_MODEL_FORM_DEMO_MODEL +export DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL +export DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM +export DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST +export DJANGO_MODEL_FORM_DEMO_URLS +export DJANGO_MODEL_FORM_DEMO_VIEWS +export DJANGO_PAYMENTS_ADMIN +export DJANGO_PAYMENTS_FORM +export DJANGO_PAYMENTS_MIGRATION_0002 +export DJANGO_PAYMENTS_MIGRATION_0003 +export DJANGO_PAYMENTS_MODELS +export DJANGO_PAYMENTS_TEMPLATE_CANCEL +export DJANGO_PAYMENTS_TEMPLATE_CHECKOUT +export DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL +export DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST +export DJANGO_PAYMENTS_TEMPLATE_SUCCESS +export DJANGO_PAYMENTS_URLS +export DJANGO_PAYMENTS_VIEW +export DJANGO_SEARCH_FORMS +export DJANGO_SEARCH_SETTINGS +export DJANGO_SEARCH_TEMPLATE +export DJANGO_SEARCH_URLS +export DJANGO_SEARCH_UTILS +export DJANGO_SEARCH_VIEWS +export DJANGO_SETTINGS_AUTHENTICATION_BACKENDS +export DJANGO_SETTINGS_BASE +export DJANGO_SETTINGS_BASE_MINIMAL +export DJANGO_SETTINGS_CRISPY_FORMS +export DJANGO_SETTINGS_DATABASE +export DJANGO_SETTINGS_DEV +export DJANGO_SETTINGS_HOME_PAGE +export DJANGO_SETTINGS_INSTALLED_APPS +export DJANGO_SETTINGS_MIDDLEWARE +export DJANGO_SETTINGS_MODEL_FORM_DEMO +export DJANGO_SETTINGS_PAYMENTS +export DJANGO_SETTINGS_PROD +export DJANGO_SETTINGS_REST_FRAMEWORK +export DJANGO_SETTINGS_SITEUSER +export DJANGO_SETTINGS_THEMES +export DJANGO_SITEUSER_ADMIN +export DJANGO_SITEUSER_EDIT_TEMPLATE +export DJANGO_SITEUSER_FORM +export DJANGO_SITEUSER_MODEL +export DJANGO_SITEUSER_URLS +export DJANGO_SITEUSER_VIEW +export DJANGO_SITEUSER_VIEW_TEMPLATE +export DJANGO_URLS +export DJANGO_URLS_ALLAUTH +export DJANGO_URLS_API +export DJANGO_URLS_DEBUG_TOOLBAR +export DJANGO_URLS_HOME_PAGE +export DJANGO_URLS_LOGGING_DEMO +export DJANGO_URLS_MODEL_FORM_DEMO +export DJANGO_URLS_SITEUSER +export DJANGO_UTILS +export EB_CUSTOM_ENV_EC2_USER +export EB_CUSTOM_ENV_VAR_FILE +export GIT_IGNORE +export JENKINS_FILE +export MAKEFILE_CUSTOM +export PIP_INSTALL_REQUIREMENTS_TEST +export PROGRAMMING_INTERVIEW +export PYTHON_CI_YAML +export PYTHON_LICENSE_TXT +export PYTHON_PROJECT_TOML +export SEPARATOR +export TINYMCE_JS +export WAGTAIL_BASE_TEMPLATE +export WAGTAIL_BLOCK_CAROUSEL +export WAGTAIL_BLOCK_MARKETING +export WAGTAIL_CONTACT_PAGE_LANDING +export WAGTAIL_CONTACT_PAGE_MODEL +export WAGTAIL_CONTACT_PAGE_TEMPLATE +export WAGTAIL_CONTACT_PAGE_TEST +export WAGTAIL_HOME_PAGE_MODEL +export WAGTAIL_HOME_PAGE_TEMPLATE +export WAGTAIL_HOME_PAGE_URLS +export WAGTAIL_HOME_PAGE_VIEWS +export WAGTAIL_PRIVACY_PAGE_MODEL +export WAGTAIL_PRIVACY_PAGE_MODEL +export WAGTAIL_PRIVACY_PAGE_TEMPLATE +export WAGTAIL_SEARCH_TEMPLATE +export WAGTAIL_SEARCH_URLS +export WAGTAIL_SETTINGS +export WAGTAIL_SITEPAGE_MODEL +export WAGTAIL_SITEPAGE_TEMPLATE +export WAGTAIL_URLS +export WAGTAIL_URLS_HOME +export WEBPACK_CONFIG_JS +export WEBPACK_INDEX_HTML +export WEBPACK_INDEX_JS +export WEBPACK_REVEAL_CONFIG_JS +export WEBPACK_REVEAL_INDEX_HTML +export WEBPACK_REVEAL_INDEX_JS + +# ------------------------------------------------------------------------------ +# Multi-line phony target rules +# ------------------------------------------------------------------------------ + +.PHONY: aws-check-env-profile-default +aws-check-env-profile-default: +ifndef AWS_PROFILE + $(error AWS_PROFILE is undefined) +endif + +.PHONY: aws-check-env-region-default +aws-check-env-region-default: +ifndef AWS_REGION + $(error AWS_REGION is undefined) +endif + +.PHONY: aws-secret-default +aws-secret-default: aws-check-env + @SECRET_KEY=$$(openssl rand -base64 48); aws ssm put-parameter --name "SECRET_KEY" --value "$$SECRET_KEY" --type String + +.PHONY: aws-sg-default +aws-sg-default: aws-check-env + aws ec2 describe-security-groups $(AWS_OPTS) + +.PHONY: aws-ssm-default +aws-ssm-default: aws-check-env + aws ssm describe-parameters $(AWS_OPTS) + @echo "Get parameter values with: aws ssm getparameter --name ." + +.PHONY: aws-subnet-default +aws-subnet-default: aws-check-env + aws ec2 describe-subnets $(AWS_OPTS) + +.PHONY: aws-vol-available-default +aws-vol-available-default: aws-check-env + aws ec2 describe-volumes --filters Name=status,Values=available --query "Volumes[*].{ID:VolumeId,Size:Size}" --output table + +.PHONY: aws-vol-default +aws-vol-default: aws-check-env + aws ec2 describe-volumes --output table + +.PHONY: aws-vpc-default +aws-vpc-default: aws-check-env + aws ec2 describe-vpcs $(AWS_OPTS) + +.PHONY: db-import-default +db-import-default: + @psql $(DJANGO_DB_NAME) < $(DJANGO_DB_NAME).sql + +.PHONY: db-init-default +db-init-default: + -dropdb $(PROJECT_NAME) + -createdb $(PROJECT_NAME) + +.PHONY: db-init-mysql-default +db-init-mysql-default: + -mysqladmin -u root drop $(PROJECT_NAME) + -mysqladmin -u root create $(PROJECT_NAME) + +.PHONY: db-init-test-default +db-init-test-default: + -dropdb test_$(PROJECT_NAME) + -createdb test_$(PROJECT_NAME) + +.PHONY: django-allauth-default +django-allauth-default: + $(ADD_DIR) backend/templates/allauth/layouts + @echo "$$DJANGO_ALLAUTH_BASE_TEMPLATE" > backend/templates/allauth/layouts/base.html + @echo "$$DJANGO_URLS_ALLAUTH" >> $(DJANGO_URLS_FILE) + -$(GIT_ADD) backend/templates/allauth/layouts/base.html + +.PHONY: django-app-tests-default +django-app-tests-default: + @echo "$$DJANGO_APP_TESTS" > $(APP_DIR)/tests.py + +.PHONY: django-base-template-default +django-base-template-default: + @$(ADD_DIR) backend/templates + @echo "$$DJANGO_BASE_TEMPLATE" > backend/templates/base.html + -$(GIT_ADD) backend/templates/base.html + +.PHONY: django-custom-admin-default +django-custom-admin-default: + @echo "$$DJANGO_CUSTOM_ADMIN" > $(DJANGO_CUSTOM_ADMIN_FILE) + @echo "$$DJANGO_BACKEND_APPS" > $(DJANGO_BACKEND_APPS_FILE) + -$(GIT_ADD) backend/*.py + +.PHONY: django-db-shell-default +django-db-shell-default: + python manage.py dbshell + +.PHONY: django-dockerfile-default +django-dockerfile-default: + @echo "$$DJANGO_DOCKERFILE" > Dockerfile + -$(GIT_ADD) Dockerfile + @echo "$$DJANGO_DOCKERCOMPOSE" > docker-compose.yml + -$(GIT_ADD) docker-compose.yml + +.PHONY: django-favicon-default +django-favicon-default: + @echo "$$DJANGO_FAVICON_TEMPLATE" > backend/templates/favicon.html + -$(GIT_ADD) backend/templates/favicon.html + +.PHONY: django-footer-template-default +django-footer-template-default: + @echo "$$DJANGO_FOOTER_TEMPLATE" > backend/templates/footer.html + -$(GIT_ADD) backend/templates/footer.html + +.PHONY: django-frontend-default +django-frontend-default: python-webpack-init + $(ADD_DIR) frontend/src/context + $(ADD_DIR) frontend/src/images + $(ADD_DIR) frontend/src/utils + @echo "$$DJANGO_FRONTEND_APP" > frontend/src/application/app.js + @echo "$$DJANGO_FRONTEND_APP_CONFIG" > frontend/src/application/config.js + @echo "$$DJANGO_FRONTEND_BABELRC" > frontend/.babelrc + @echo "$$DJANGO_FRONTEND_COMPONENT_CLOCK" > frontend/src/components/Clock.js + @echo "$$DJANGO_FRONTEND_COMPONENT_ERROR" > frontend/src/components/ErrorBoundary.js + @echo "$$DJANGO_FRONTEND_CONTEXT_INDEX" > frontend/src/context/index.js + @echo "$$DJANGO_FRONTEND_CONTEXT_USER_PROVIDER" > frontend/src/context/UserContextProvider.js + @echo "$$DJANGO_FRONTEND_COMPONENT_USER_MENU" > frontend/src/components/UserMenu.js + @echo "$$DJANGO_FRONTEND_COMPONENTS" > frontend/src/components/index.js + @echo "$$DJANGO_FRONTEND_ESLINTRC" > frontend/.eslintrc + @echo "$$DJANGO_FRONTEND_PORTAL" > frontend/src/dataComponents.js + @echo "$$DJANGO_FRONTEND_STYLES" > frontend/src/styles/index.scss + @echo "$$DJANGO_FRONTEND_THEME_BLUE" > frontend/src/styles/theme-blue.scss + @echo "$$DJANGO_FRONTEND_THEME_TOGGLER" > frontend/src/utils/themeToggler.js + # @echo "$$TINYMCE_JS" > frontend/src/utils/tinymce.js + @$(MAKE) npm-install-django + @$(MAKE) npm-install-django-dev + -$(GIT_ADD) $(DJANGO_FRONTEND_FILES) + +.PHONY: django-graph-default +django-graph-default: + python manage.py graph_models -a -o $(PROJECT_NAME).png + +.PHONY: django-header-template-default +django-header-template-default: + @echo "$$DJANGO_HEADER_TEMPLATE" > backend/templates/header.html + -$(GIT_ADD) backend/templates/header.html + +.PHONY: django-home-default +django-home-default: + python manage.py startapp home + $(ADD_DIR) home/templates + @echo "$$DJANGO_HOME_PAGE_ADMIN" > home/admin.py + @echo "$$DJANGO_HOME_PAGE_MODELS" > home/models.py + @echo "$$DJANGO_HOME_PAGE_TEMPLATE" > home/templates/home.html + @echo "$$DJANGO_HOME_PAGE_VIEWS" > home/views.py + @echo "$$DJANGO_HOME_PAGE_URLS" > home/urls.py + @echo "$$DJANGO_URLS_HOME_PAGE" >> $(DJANGO_URLS_FILE) + @echo "$$DJANGO_SETTINGS_HOME_PAGE" >> $(DJANGO_SETTINGS_BASE_FILE) + export APP_DIR="home"; $(MAKE) django-app-tests + -$(GIT_ADD) home/templates + -$(GIT_ADD) home/*.py + -$(GIT_ADD) home/migrations/*.py + +.PHONY: django-init-default +django-init-default: separator \ + db-init \ + django-install \ + django-project \ + django-utils \ + pip-freeze \ + pip-init-test \ + django-settings-directory \ + django-custom-admin \ + django-dockerfile \ + django-offcanvas-template \ + django-header-template \ + django-footer-template \ + django-base-template \ + django-manage-py \ + django-urls \ + django-urls-debug-toolbar \ + django-allauth \ + django-favicon \ + git-ignore \ + django-settings-base \ + django-settings-dev \ + django-settings-prod \ + django-siteuser \ + django-home \ + django-rest-serializers \ + django-rest-views \ + django-urls-api \ + django-frontend \ + django-migrate \ + django-su + +.PHONY: django-init-minimal-default +django-init-minimal-default: separator \ + db-init \ + django-install-minimal \ + django-project \ + django-settings-directory \ + django-settings-base-minimal \ + django-settings-dev \ + pip-freeze \ + pip-init-test \ + django-custom-admin \ + django-dockerfile \ + django-offcanvas-template \ + django-header-template \ + django-footer-template \ + django-base-template \ + django-manage-py \ + django-urls \ + django-urls-debug-toolbar \ + django-favicon \ + django-settings-prod \ + django-home \ + django-utils \ + django-frontend \ + django-migrate \ + git-ignore \ + django-su + +.PHONY: django-init-wagtail-default +django-init-wagtail-default: separator \ + db-init \ + django-install \ + wagtail-install \ + wagtail-project \ + django-utils \ + pip-freeze \ + pip-init-test \ + django-custom-admin \ + django-dockerfile \ + django-offcanvas-template \ + wagtail-header-prefix-template \ + django-header-template \ + wagtail-base-template \ + django-footer-template \ + django-manage-py \ + wagtail-home \ + wagtail-urls \ + django-urls-debug-toolbar \ + django-allauth \ + django-favicon \ + git-ignore \ + wagtail-search \ + django-settings-base \ + django-settings-dev \ + django-settings-prod \ + wagtail-settings \ + django-siteuser \ + django-model-form-demo \ + django-logging-demo \ + django-payments-demo-default \ + django-rest-serializers \ + django-rest-views \ + django-urls-api \ + wagtail-urls-home \ + django-frontend \ + django-migrate \ + django-su + +.PHONY: django-install-default +django-install-default: + $(PIP_ENSURE) + python -m pip install \ + Django \ + Faker \ + boto3 \ + crispy-bootstrap5 \ + djangorestframework \ + django-allauth \ + django-after-response \ + django-ckeditor \ + django-colorful \ + django-cors-headers \ + django-countries \ + django-crispy-forms \ + django-debug-toolbar \ + django-extensions \ + django-hijack \ + django-honeypot \ + django-imagekit \ + django-import-export \ + django-ipware \ + django-multiselectfield \ + django-ninja \ + django-phonenumber-field \ + django-recurrence \ + django-recaptcha \ + django-registration \ + django-richtextfield \ + django-sendgrid-v5 \ + django-social-share \ + django-sql-explorer \ + django-storages \ + django-tables2 \ + django-timezone-field \ + django-widget-tweaks \ + dj-database-url \ + dj-rest-auth \ + dj-stripe \ + docutils \ + enmerkar \ + gunicorn \ + html2docx \ + icalendar \ + mailchimp-marketing \ + mailchimp-transactional \ + phonenumbers \ + pipdeptree \ + psycopg2-binary \ + pydotplus \ + python-webpack-boilerplate \ + python-docx \ + reportlab \ + texttable + +.PHONY: django-install-minimal-default +django-install-minimal-default: + $(PIP_ENSURE) + python -m pip install \ + Django \ + dj-database-url \ + django-debug-toolbar \ + python-webpack-boilerplate + +.PHONY: django-lint-default +django-lint-default: + -ruff format -v + -djlint --reformat --format-css --format-js . + -ruff check -v --fix + +.PHONY: django-loaddata-default +django-loaddata-default: + python manage.py loaddata + +.PHONY: django-logging-demo-default +django-logging-demo-default: + python manage.py startapp logging_demo + @echo "$$DJANGO_LOGGING_DEMO_ADMIN" > logging_demo/admin.py + @echo "$$DJANGO_LOGGING_DEMO_MODELS" > logging_demo/models.py + @echo "$$DJANGO_LOGGING_DEMO_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_LOGGING_DEMO_URLS" > logging_demo/urls.py + @echo "$$DJANGO_LOGGING_DEMO_VIEWS" > logging_demo/views.py + @echo "$$DJANGO_URLS_LOGGING_DEMO" >> $(DJANGO_URLS_FILE) + export APP_DIR="logging_demo"; $(MAKE) django-app-tests + -$(GIT_ADD) logging_demo/*.py + -$(GIT_ADD) logging_demo/migrations/*.py + +.PHONY: django-manage-py-default +django-manage-py-default: + @echo "$$DJANGO_MANAGE_PY" > manage.py + -$(GIT_ADD) manage.py + +.PHONY: django-migrate-default +django-migrate-default: + python manage.py migrate + +.PHONY: django-migrations-make-default +django-migrations-make-default: + python manage.py makemigrations + +.PHONY: django-migrations-show-default +django-migrations-show-default: + python manage.py showmigrations + +.PHONY: django-model-form-demo-default +django-model-form-demo-default: + python manage.py startapp model_form_demo + @echo "$$DJANGO_MODEL_FORM_DEMO_ADMIN" > model_form_demo/admin.py + @echo "$$DJANGO_MODEL_FORM_DEMO_FORMS" > model_form_demo/forms.py + @echo "$$DJANGO_MODEL_FORM_DEMO_MODEL" > model_form_demo/models.py + @echo "$$DJANGO_MODEL_FORM_DEMO_URLS" > model_form_demo/urls.py + @echo "$$DJANGO_MODEL_FORM_DEMO_VIEWS" > model_form_demo/views.py + $(ADD_DIR) model_form_demo/templates + @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL" > model_form_demo/templates/model_form_demo_detail.html + @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM" > model_form_demo/templates/model_form_demo_form.html + @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST" > model_form_demo/templates/model_form_demo_list.html + @echo "$$DJANGO_SETTINGS_MODEL_FORM_DEMO" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_URLS_MODEL_FORM_DEMO" >> $(DJANGO_URLS_FILE) + export APP_DIR="model_form_demo"; $(MAKE) django-app-tests + python manage.py makemigrations + -$(GIT_ADD) model_form_demo/*.py + -$(GIT_ADD) model_form_demo/templates + -$(GIT_ADD) model_form_demo/migrations + +.PHONY: django-offcanvas-template-default +django-offcanvas-template-default: + -$(ADD_DIR) backend/templates + @echo "$$DJANGO_FRONTEND_OFFCANVAS_TEMPLATE" > backend/templates/offcanvas.html + -$(GIT_ADD) backend/templates/offcanvas.html + +.PHONY: django-open-default +django-open-default: +ifeq ($(UNAME), Linux) + @echo "Opening on Linux." + xdg-open http://0.0.0.0:8000 +else ifeq ($(UNAME), Darwin) + @echo "Opening on macOS (Darwin)." + open http://0.0.0.0:8000 +else + @echo "Unable to open on: $(UNAME)" +endif + +.PHONY: django-payments-demo-default +django-payments-demo-default: + python manage.py startapp payments + @echo "$$DJANGO_PAYMENTS_FORM" > payments/forms.py + @echo "$$DJANGO_PAYMENTS_MODELS" > payments/models.py + @echo "$$DJANGO_PAYMENTS_ADMIN" > payments/admin.py + @echo "$$DJANGO_PAYMENTS_VIEW" > payments/views.py + @echo "$$DJANGO_PAYMENTS_URLS" > payments/urls.py + $(ADD_DIR) payments/templates/payments + $(ADD_DIR) payments/management/commands + @echo "$$DJANGO_PAYMENTS_TEMPLATE_CANCEL" > payments/templates/payments/cancel.html + @echo "$$DJANGO_PAYMENTS_TEMPLATE_CHECKOUT" > payments/templates/payments/checkout.html + @echo "$$DJANGO_PAYMENTS_TEMPLATE_SUCCESS" > payments/templates/payments/success.html + @echo "$$DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST" > payments/templates/payments/product_list.html + @echo "$$DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL" > payments/templates/payments/product_detail.html + @echo "$$DJANGO_SETTINGS_PAYMENTS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_URLS_PAYMENTS" >> $(DJANGO_URLS_FILE) + export APP_DIR="payments"; $(MAKE) django-app-tests + python manage.py makemigrations payments + @echo "$$DJANGO_PAYMENTS_MIGRATION_0002" > payments/migrations/0002_set_stripe_api_keys.py + @echo "$$DJANGO_PAYMENTS_MIGRATION_0003" > payments/migrations/0003_create_initial_products.py + -$(GIT_ADD) payments/ + +.PHONY: django-project-default +django-project-default: + django-admin startproject backend . + -$(GIT_ADD) backend + +.PHONY: django-rest-serializers-default +django-rest-serializers-default: + @echo "$$DJANGO_API_SERIALIZERS" > backend/serializers.py + -$(GIT_ADD) backend/serializers.py + +.PHONY: django-rest-views-default +django-rest-views-default: + @echo "$$DJANGO_API_VIEWS" > backend/api.py + -$(GIT_ADD) backend/api.py + +.PHONY: django-search-default +django-search-default: + python manage.py startapp search + $(ADD_DIR) search/templates + @echo "$$DJANGO_SEARCH_TEMPLATE" > search/templates/search.html + @echo "$$DJANGO_SEARCH_FORMS" > search/forms.py + @echo "$$DJANGO_SEARCH_URLS" > search/urls.py + @echo "$$DJANGO_SEARCH_UTILS" > search/utils.py + @echo "$$DJANGO_SEARCH_VIEWS" > search/views.py + @echo "$$DJANGO_SEARCH_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "INSTALLED_APPS.append('search')" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "urlpatterns += [path('search/', include('search.urls'))]" >> $(DJANGO_URLS_FILE) + -$(GIT_ADD) search/templates + -$(GIT_ADD) search/*.py + +.PHONY: django-secret-key-default +django-secret-key-default: + @python -c "from secrets import token_urlsafe; print(token_urlsafe(50))" + +.PHONY: django-serve-default +django-serve-default: + npm run watch & + python manage.py runserver 0.0.0.0:8000 + +.PHONY: django-settings-base-default +django-settings-base-default: + @echo "$$DJANGO_SETTINGS_BASE" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_AUTHENTICATION_BACKENDS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_REST_FRAMEWORK" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_THEMES" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_DATABASE" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_INSTALLED_APPS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_MIDDLEWARE" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_CRISPY_FORMS" >> $(DJANGO_SETTINGS_BASE_FILE) + +.PHONY: django-settings-base-minimal-default +django-settings-base-minimal-default: + @echo "$$DJANGO_SETTINGS_BASE_MINIMAL" >> $(DJANGO_SETTINGS_BASE_FILE) + +.PHONY: django-settings-dev-default +django-settings-dev-default: + @echo "# $(PROJECT_NAME)" > $(DJANGO_SETTINGS_DEV_FILE) + @echo "$$DJANGO_SETTINGS_DEV" >> backend/settings/dev.py + -$(GIT_ADD) $(DJANGO_SETTINGS_DEV_FILE) + +.PHONY: django-settings-directory-default +django-settings-directory-default: + @$(ADD_DIR) $(DJANGO_SETTINGS_DIR) + @$(COPY_FILE) backend/settings.py backend/settings/base.py + @$(DEL_FILE) backend/settings.py + -$(GIT_ADD) backend/settings/*.py + +.PHONY: django-settings-prod-default +django-settings-prod-default: + @echo "$$DJANGO_SETTINGS_PROD" > $(DJANGO_SETTINGS_PROD_FILE) + -$(GIT_ADD) $(DJANGO_SETTINGS_PROD_FILE) + +.PHONY: django-shell-default +django-shell-default: + python manage.py shell + +.PHONY: django-siteuser-default +django-siteuser-default: + python manage.py startapp siteuser + $(ADD_DIR) siteuser/templates/ + @echo "$$DJANGO_SITEUSER_FORM" > siteuser/forms.py + @echo "$$DJANGO_SITEUSER_MODEL" > siteuser/models.py + @echo "$$DJANGO_SITEUSER_ADMIN" > siteuser/admin.py + @echo "$$DJANGO_SITEUSER_VIEW" > siteuser/views.py + @echo "$$DJANGO_SITEUSER_URLS" > siteuser/urls.py + @echo "$$DJANGO_SITEUSER_VIEW_TEMPLATE" > siteuser/templates/profile.html + @echo "$$DJANGO_SITEUSER_TEMPLATE" > siteuser/templates/user.html + @echo "$$DJANGO_SITEUSER_EDIT_TEMPLATE" > siteuser/templates/user_edit.html + @echo "$$DJANGO_URLS_SITEUSER" >> $(DJANGO_URLS_FILE) + @echo "$$DJANGO_SETTINGS_SITEUSER" >> $(DJANGO_SETTINGS_BASE_FILE) + export APP_DIR="siteuser"; $(MAKE) django-app-tests + -$(GIT_ADD) siteuser/templates + -$(GIT_ADD) siteuser/*.py + python manage.py makemigrations siteuser + -$(GIT_ADD) siteuser/migrations/*.py + +.PHONY: django-static-default +django-static-default: + python manage.py collectstatic --noinput + +.PHONY: django-su-default +django-su-default: + DJANGO_SUPERUSER_PASSWORD=admin python manage.py createsuperuser --noinput --username=admin --email=$(PROJECT_EMAIL) + +.PHONY: django-test-default +django-test-default: npm-install django-static + -$(MAKE) pip-install-test + python manage.py test + +.PHONY: django-urls-api-default +django-urls-api-default: + @echo "$$DJANGO_URLS_API" >> $(DJANGO_URLS_FILE) + -$(GIT_ADD) $(DJANGO_URLS_FILE) + +.PHONY: django-urls-debug-toolbar-default +django-urls-debug-toolbar-default: + @echo "$$DJANGO_URLS_DEBUG_TOOLBAR" >> $(DJANGO_URLS_FILE) + +.PHONY: django-urls-default +django-urls-default: + @echo "$$DJANGO_URLS" > $(DJANGO_URLS_FILE) + -$(GIT_ADD) $(DJANGO_URLS_FILE) + +.PHONY: django-urls-show-default +django-urls-show-default: + python manage.py show_urls + +.PHONY: django-user-default +django-user-default: + python manage.py shell -c "from django.contrib.auth.models import User; \ + User.objects.create_user('user', '', 'user')" + +.PHONY: django-utils-default +django-utils-default: + @echo "$$DJANGO_UTILS" > backend/utils.py + -$(GIT_ADD) backend/utils.py + +.PHONY: docker-build-default +docker-build-default: + podman build -t $(PROJECT_NAME) . + +.PHONY: docker-compose-default +docker-compose-default: + podman compose up + +.PHONY: docker-list-default +docker-list-default: + podman container list --all + podman images --all + +.PHONY: docker-run-default +docker-run-default: + podman run $(PROJECT_NAME) + +.PHONY: docker-serve-default +docker-serve-default: + podman run -p 8000:8000 $(PROJECT_NAME) + +.PHONY: docker-shell-default +docker-shell-default: + podman run -it $(PROJECT_NAME) /bin/bash + +.PHONY: eb-check-env-default +eb-check-env-default: # https://stackoverflow.com/a/4731504/185820 +ifndef EB_SSH_KEY + $(error EB_SSH_KEY is undefined) +endif +ifndef VPC_ID + $(error VPC_ID is undefined) +endif +ifndef VPC_SG + $(error VPC_SG is undefined) +endif +ifndef VPC_SUBNET_EC2 + $(error VPC_SUBNET_EC2 is undefined) +endif +ifndef VPC_SUBNET_ELB + $(error VPC_SUBNET_ELB is undefined) +endif + +.PHONY: eb-create-default +eb-create-default: aws-check-env eb-check-env + eb create $(EB_ENV_NAME) \ + -im $(EC2_INSTANCE_MIN) \ + -ix $(EC2_INSTANCE_MAX) \ + -ip $(EC2_INSTANCE_PROFILE) \ + -i $(EC2_INSTANCE_TYPE) \ + -k $(EB_SSH_KEY) \ + -p $(EB_PLATFORM) \ + --elb-type $(EC2_LB_TYPE) \ + --vpc \ + --vpc.id $(VPC_ID) \ + --vpc.elbpublic \ + --vpc.publicip \ + --vpc.ec2subnets $(VPC_SUBNET_EC2) \ + --vpc.elbsubnets $(VPC_SUBNET_ELB) \ + --vpc.securitygroups $(VPC_SG) + +.PHONY: eb-custom-env-default +eb-custom-env-default: + $(ADD_DIR) .ebextensions + @echo "$$EB_CUSTOM_ENV_EC2_USER" > .ebextensions/bash.config + -$(GIT_ADD) .ebextensions/bash.config + $(ADD_DIR) .platform/hooks/postdeploy + @echo "$$EB_CUSTOM_ENV_VAR_FILE" > .platform/hooks/postdeploy/setenv.sh + -$(GIT_ADD) .platform/hooks/postdeploy/setenv.sh + +.PHONY: eb-deploy-default +eb-deploy-default: + eb deploy + +.PHONY: eb-export-default +eb-export-default: + @if [ ! -d $(EB_DIR_NAME) ]; then \ + echo "Directory $(EB_DIR_NAME) does not exist"; \ + else \ + echo "Directory $(EB_DIR_NAME) does exist!"; \ + eb ssh --quiet -c "export PGPASSWORD=$(DJANGO_DB_PASS); pg_dump -U $(DJANGO_DB_USER) -h $(DJANGO_DB_HOST) $(DJANGO_DB_NAME)" > $(DJANGO_DB_NAME).sql; \ + echo "Wrote $(DJANGO_DB_NAME).sql"; \ + fi + +.PHONY: eb-restart-default +eb-restart-default: + eb ssh -c "systemctl restart web" + +.PHONY: eb-rebuild-default +eb-rebuild-default: + aws elasticbeanstalk rebuild-environment --environment-name $(ENV_NAME) + +.PHONY: eb-upgrade-default +eb-upgrade-default: + eb upgrade + +.PHONY: eb-init-default +eb-init-default: aws-check-env-profile + eb init --profile=$(AWS_PROFILE) + +.PHONY: eb-list-default +eb-list-platforms-default: + aws elasticbeanstalk list-platform-versions + +.PHONY: eb-list-databases-default +eb-list-databases-default: + @eb ssh --quiet -c "export PGPASSWORD=$(DJANGO_DB_PASS); psql -l -U $(DJANGO_DB_USER) -h $(DJANGO_DB_HOST) $(DJANGO_DB_NAME)" + +.PHONY: eb-logs-default +eb-logs-default: + eb logs + +.PHONY: eb-print-env-default +eb-print-env-default: + eb printenv + +.PHONY: favicon-default +favicon-init-default: + dd if=/dev/urandom bs=64 count=1 status=none | base64 | convert -size 16x16 -depth 8 -background none -fill white label:@- favicon.png + convert favicon.png favicon.ico + -$(GIT_ADD) favicon.ico + $(DEL_FILE) favicon.png + +.PHONY: git-ignore-default +git-ignore-default: + @echo "$$GIT_IGNORE" > .gitignore + -$(GIT_ADD) .gitignore + +.PHONY: git-branches-default +git-branches-default: + -for i in $(GIT_BRANCHES) ; do \ + -@$(GIT_CHECKOUT) -t $$i ; done + +.PHONY: git-commit-message-clean-default +git-commit-message-clean-default: + -@$(GIT_COMMIT) -a -m "Clean" + +.PHONY: git-commit-message-default +git-commit-message-default: + -@$(GIT_COMMIT) -a -m $(GIT_COMMIT_MSG) + +.PHONY: git-commit-message-empty-default +git-commit-message-empty-default: + -@$(GIT_COMMIT) --allow-empty -m "Empty-Commit" + +.PHONY: git-commit-message-init-default +git-commit-message-init-default: + -@$(GIT_COMMIT) -a -m "Init" + +.PHONY: git-commit-message-last-default +git-commit-message-last-default: + git log -1 --pretty=%B > $(TMPDIR)/commit.txt + -$(GIT_COMMIT) -a -F $(TMPDIR)/commit.txt + +.PHONY: git-commit-message-lint-default +git-commit-message-lint-default: + -@$(GIT_COMMIT) -a -m "Lint" + +.PHONY: git-commit-message-mk-default +git-commit-message-mk-default: + -@$(GIT_COMMIT) project.mk -m "Add/update $(MAKEFILE_CUSTOM_FILE)" + +.PHONY: git-commit-message-rename-default +git-commit-message-rename-default: + -@$(GIT_COMMIT) -a -m "Rename" + +.PHONY: git-commit-message-sort-default +git-commit-message-sort-default: + -@$(GIT_COMMIT) -a -m "Sort" + +.PHONY: git-push-default +git-push-default: + -@$(GIT_PUSH) + +.PHONY: git-push-force-default +git-push-force-default: + -@$(GIT_PUSH_FORCE) + +.PHONY: git-commit-edit-default +git-commit-edit-default: + -$(GIT_COMMIT) -a + +.PHONY: git-prune-default +git-prune-default: + git remote update origin --prune + +.PHONY: git-set-upstream-default +git-set-upstream-default: + git push --set-upstream origin main + +.PHONY: git-set-default-default +git-set-default-default: + gh repo set-default + +.PHONY: git-short-default +git-short-default: + @echo $(GIT_REV) + +.PHONY: help-default +help-default: + @echo "Project Makefile 🤷" + @echo "Usage: make [options] [target] ..." + @echo "Examples:" + @echo " make help Print this message" + @echo " make list-defines list all defines in the Makefile" + @echo " make list-commands list all targets in the Makefile" + +.PHONY: jenkins-init-default +jenkins-init-default: + @echo "$$JENKINS_FILE" > Jenkinsfile + +.PHONY: makefile-list-commands-default +makefile-list-commands-default: + @for makefile in $(MAKEFILE_LIST); do \ + echo "Commands from $$makefile:"; \ + $(MAKE) -pRrq -f $$makefile : 2>/dev/null | \ + awk -v RS= -F: '/^# File/,/^# Finished Make data base/ { \ + if ($$1 !~ "^[#.]") { sub(/-default$$/, "", $$1); print $$1 } }' | \ + egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | \ + tr ' ' '\n' | \ + sort | \ + awk '{print $$0}' ; \ + echo; \ + done | $(PAGER) + +.PHONY: makefile-list-defines-default +makefile-list-defines-default: + @grep '^define [A-Za-z_][A-Za-z0-9_]*' Makefile + +.PHONY: makefile-list-exports-default +makefile-list-exports-default: + @grep '^export [A-Z][A-Z_]*' Makefile + +.PHONY: makefile-list-targets-default +makefile-list-targets-default: + @perl -ne 'print if /^\s*\.PHONY:/ .. /^[a-zA-Z0-9_-]+:/;' Makefile | grep -v .PHONY + +.PHONY: make-default +make-default: + -$(GIT_ADD) Makefile + -$(GIT_COMMIT) Makefile -m "Add/update project-makefile files" + -git push + +.PHONY: npm-init-default +npm-init-default: + npm init -y + -$(GIT_ADD) package.json + -$(GIT_ADD) package-lock.json + +.PHONY: npm-build-default +npm-build-default: + npm run build + +.PHONY: npm-install-default +npm-install-default: + npm install + -$(GIT_ADD) package-lock.json + +.PHONY: npm-install-django-default +npm-install-django-default: + npm install \ + @fortawesome/fontawesome-free \ + @fortawesome/fontawesome-svg-core \ + @fortawesome/free-brands-svg-icons \ + @fortawesome/free-solid-svg-icons \ + @fortawesome/react-fontawesome \ + bootstrap \ + camelize \ + date-fns \ + history \ + mapbox-gl \ + query-string \ + react-animate-height \ + react-chartjs-2 \ + react-copy-to-clipboard \ + react-date-range \ + react-dom \ + react-dropzone \ + react-hook-form \ + react-image-crop \ + react-map-gl \ + react-modal \ + react-resize-detector \ + react-select \ + react-swipeable \ + snakeize \ + striptags \ + url-join \ + viewport-mercator-project + +.PHONY: npm-install-django-dev-default +npm-install-django-dev-default: + npm install \ + eslint-plugin-react \ + eslint-config-standard \ + eslint-config-standard-jsx \ + @babel/core \ + @babel/preset-env \ + @babel/preset-react \ + --save-dev + +.PHONY: npm-serve-default +npm-serve-default: + npm run start + +.PHONY: npm-test-default +npm-test-default: + npm run test + +.PHONY: pip-deps-default +pip-deps-default: + $(PIP_ENSURE) + python -m pip install pipdeptree + python -m pipdeptree + pipdeptree + +.PHONY: pip-freeze-default +pip-freeze-default: + $(PIP_ENSURE) + python -m pip freeze | sort > $(TMPDIR)/requirements.txt + mv -f $(TMPDIR)/requirements.txt . + -$(GIT_ADD) requirements.txt + +.PHONY: pip-init-default +pip-init-default: + touch requirements.txt + -$(GIT_ADD) requirements.txt + +.PHONY: pip-init-test-default +pip-init-test-default: + @echo "$$PIP_INSTALL_REQUIREMENTS_TEST" > requirements-test.txt + -$(GIT_ADD) requirements-test.txt + +.PHONY: pip-install-default +pip-install-default: + $(PIP_ENSURE) + $(MAKE) pip-upgrade + python -m pip install wheel + python -m pip install -r requirements.txt + +.PHONY: pip-install-dev-default +pip-install-dev-default: + $(PIP_ENSURE) + python -m pip install -r requirements-dev.txt + +.PHONY: pip-install-test-default +pip-install-test-default: + $(PIP_ENSURE) + python -m pip install -r requirements-test.txt + +.PHONY: pip-install-upgrade-default +pip-install-upgrade-default: + cat requirements.txt | awk -F\= '{print $$1}' > $(TMPDIR)/requirements.txt + mv -f $(TMPDIR)/requirements.txt . + $(PIP_ENSURE) + python -m pip install -U -r requirements.txt + python -m pip freeze | sort > $(TMPDIR)/requirements.txt + mv -f $(TMPDIR)/requirements.txt . + +.PHONY: pip-upgrade-default +pip-upgrade-default: + $(PIP_ENSURE) + python -m pip install -U pip + +.PHONY: pip-uninstall-default +pip-uninstall-default: + $(PIP_ENSURE) + python -m pip freeze | xargs python -m pip uninstall -y + +.PHONY: plone-clean-default +plone-clean-default: + $(DEL_DIR) $(PROJECT_NAME) + $(DEL_DIR) $(PACKAGE_NAME) + +.PHONY: plone-init-default +plone-init-default: git-ignore plone-install plone-instance plone-serve + +.PHONY: plone-install-default +plone-install-default: + $(PIP_ENSURE) + python -m pip install plone -c $(PIP_INSTALL_PLONE_CONSTRAINTS) + +.PHONY: plone-instance-default +plone-instance-default: + mkwsgiinstance -d backend -u admin:admin + cat backend/etc/zope.ini | sed -e 's/host = 127.0.0.1/host = 0.0.0.0/; s/port = 8080/port = 8000/' > $(TMPDIR)/zope.ini + mv -f $(TMPDIR)/zope.ini backend/etc/zope.ini + -$(GIT_ADD) backend/etc/site.zcml + -$(GIT_ADD) backend/etc/zope.conf + -$(GIT_ADD) backend/etc/zope.ini + +.PHONY: plone-serve-default +plone-serve-default: + runwsgi backend/etc/zope.ini + +.PHONY: plone-build-default +plone-build-default: + buildout + +.PHONY: programming-interview-default +programming-interview-default: + @echo "$$PROGRAMMING_INTERVIEW" > interview.py + @echo "Created interview.py!" + -@$(GIT_ADD) interview.py > /dev/null 2>&1 + +# .NOT_PHONY! +$(MAKEFILE_CUSTOM_FILE): + @echo "$$MAKEFILE_CUSTOM" > $(MAKEFILE_CUSTOM_FILE) + -$(GIT_ADD) $(MAKEFILE_CUSTOM_FILE) + +.PHONY: python-license-default +python-license-default: + @echo "$(PYTHON_LICENSE_TXT)" > LICENSE.txt + -$(GIT_ADD) LICENSE.txt + +.PHONY: python-project-default +python-project-default: + @echo "$(PYTHON_PROJECT_TOML)" > pyproject.toml + -$(GIT_ADD) pyproject.toml + +.PHONY: python-serve-default +python-serve-default: + @echo "\n\tServing HTTP on http://0.0.0.0:8000\n" + python3 -m http.server + +.PHONY: python-sdist-default +python-sdist-default: + $(PIP_ENSURE) + python setup.py sdist --format=zip + +.PHONY: python-webpack-init-default +python-webpack-init-default: + python manage.py webpack_init --no-input + +.PHONY: python-ci-default +python-ci-default: + $(ADD_DIR) .github/workflows + @echo "$(PYTHON_CI_YAML)" > .github/workflows/build_wheels.yml + -$(GIT_ADD) .github/workflows/build_wheels.yml + +.PHONY: rand-default +rand-default: + @openssl rand -base64 12 | sed 's/\///g' + +.PHONY: readme-init-default +readme-init-default: + @echo "# $(PROJECT_NAME)" > README.md + -$(GIT_ADD) README.md + +.PHONY: readme-edit-default +readme-edit-default: + $(EDITOR) README.md + +.PHONY: reveal-init-default +reveal-init-default: webpack-init-reveal + npm install \ + css-loader \ + mini-css-extract-plugin \ + reveal.js \ + style-loader + jq '.scripts += {"build": "webpack"}' package.json > \ + $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json + jq '.scripts += {"start": "webpack serve --mode development --port 8000 --static"}' package.json > \ + $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json + jq '.scripts += {"watch": "webpack watch --mode development"}' package.json > \ + $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json + +.PHONY: reveal-serve-default +reveal-serve-default: + npm run watch & + python -m http.server + +.PHONY: review-default +review-default: +ifeq ($(UNAME), Darwin) + $(EDITOR_REVIEW) `find backend/ -name \*.py` `find backend/ -name \*.html` `find frontend/ -name \*.js` `find frontend/ -name \*.js` +else + @echo "Unsupported" +endif + +.PHONY: separator-default +separator-default: + @echo "$$SEPARATOR" + +.PHONY: sphinx-init-default +sphinx-init-default: sphinx-install + sphinx-quickstart -q -p $(PROJECT_NAME) -a $(USER) -v 0.0.1 $(RANDIR) + $(COPY_DIR) $(RANDIR)/* . + $(DEL_DIR) $(RANDIR) + -$(GIT_ADD) index.rst + -$(GIT_ADD) conf.py + $(DEL_FILE) make.bat + -@$(GIT_CHECKOUT) Makefile + $(MAKE) git-ignore + +.PHONY: sphinx-theme-init-default +sphinx-theme-init-default: + export DJANGO_FRONTEND_THEME_NAME=$(PROJECT_NAME)_theme; \ + $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/__init__.py ; \ + -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/__init__.py ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/theme.conf ; \ + -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/theme.conf ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/layout.html ; \ + -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/layout.html ; \ + $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME/static/css ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/static/css/style.css ; \ + $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME/static/js ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/static/js/script.js ; \ + -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/static + +.PHONY: sphinx-install-default +sphinx-install-default: + echo "Sphinx\n" > requirements.txt + @$(MAKE) pip-install + @$(MAKE) pip-freeze + -$(GIT_ADD) requirements.txt + +.PHONY: sphinx-build-default +sphinx-build-default: + sphinx-build -b html -d _build/doctrees . _build/html + sphinx-build -b rinoh . _build/rinoh + +.PHONY: sphinx-serve-default +sphinx-serve-default: + cd _build/html;python3 -m http.server + +.PHONY: wagtail-base-template-default +wagtail-base-template-default: + @echo "$$WAGTAIL_BASE_TEMPLATE" > backend/templates/base.html + +.PHONY: wagtail-clean-default +wagtail-clean-default: + -@for dir in $(shell echo "$(WAGTAIL_CLEAN_DIRS)"); do \ + echo "Cleaning $$dir"; \ + $(DEL_DIR) $$dir >/dev/null 2>&1; \ + done + -@for file in $(shell echo "$(WAGTAIL_CLEAN_FILES)"); do \ + echo "Cleaning $$file"; \ + $(DEL_FILE) $$file >/dev/null 2>&1; \ + done + +.PHONY: wagtail-contactpage-default +wagtail-contactpage-default: + python manage.py startapp contactpage + @echo "$$WAGTAIL_CONTACT_PAGE_MODEL" > contactpage/models.py + @echo "$$WAGTAIL_CONTACT_PAGE_TEST" > contactpage/tests.py + $(ADD_DIR) contactpage/templates/contactpage/ + @echo "$$WAGTAIL_CONTACT_PAGE_TEMPLATE" > contactpage/templates/contactpage/contact_page.html + @echo "$$WAGTAIL_CONTACT_PAGE_LANDING" > contactpage/templates/contactpage/contact_page_landing.html + @echo "INSTALLED_APPS.append('contactpage')" >> $(DJANGO_SETTINGS_BASE_FILE) + python manage.py makemigrations contactpage + -$(GIT_ADD) contactpage/templates + -$(GIT_ADD) contactpage/*.py + -$(GIT_ADD) contactpage/migrations/*.py + +.PHONY: wagtail-header-prefix-template-default +wagtail-header-prefix-template-default: + @echo "$$WAGTAIL_HEADER_PREFIX" > backend/templates/header.html + +.PHONY: wagtail-home-default +wagtail-home-default: + @echo "$$WAGTAIL_HOME_PAGE_MODEL" > home/models.py + @echo "$$WAGTAIL_HOME_PAGE_TEMPLATE" > home/templates/home/home_page.html + $(ADD_DIR) home/templates/blocks + @echo "$$WAGTAIL_BLOCK_MARKETING" > home/templates/blocks/marketing_block.html + @echo "$$WAGTAIL_BLOCK_CAROUSEL" > home/templates/blocks/carousel_block.html + -$(GIT_ADD) home/templates + -$(GIT_ADD) home/*.py + python manage.py makemigrations home + -$(GIT_ADD) home/migrations/*.py + +.PHONY: wagtail-install-default +wagtail-install-default: + $(PIP_ENSURE) + python -m pip install \ + wagtail \ + wagtailmenus \ + wagtail-color-panel \ + wagtail-django-recaptcha \ + wagtail-markdown \ + wagtail-modeladmin \ + wagtail-seo \ + weasyprint \ + whitenoise \ + xhtml2pdf + +.PHONY: wagtail-private-default +wagtail-privacy-default: + python manage.py startapp privacy + @echo "$$WAGTAIL_PRIVACY_PAGE_MODEL" > privacy/models.py + $(ADD_DIR) privacy/templates + @echo "$$WAGTAIL_PRIVACY_PAGE_TEMPLATE" > privacy/templates/privacy_page.html + @echo "INSTALLED_APPS.append('privacy')" >> $(DJANGO_SETTINGS_BASE_FILE) + python manage.py makemigrations privacy + -$(GIT_ADD) privacy/templates + -$(GIT_ADD) privacy/*.py + -$(GIT_ADD) privacy/migrations/*.py + +.PHONY: wagtail-project-default +wagtail-project-default: + wagtail start backend . + $(DEL_FILE) home/templates/home/welcome_page.html + -$(GIT_ADD) backend/ + -$(GIT_ADD) .dockerignore + -$(GIT_ADD) Dockerfile + -$(GIT_ADD) manage.py + -$(GIT_ADD) requirements.txt + +.PHONY: wagtail-search-default +wagtail-search-default: + @echo "$$WAGTAIL_SEARCH_TEMPLATE" > search/templates/search/search.html + @echo "$$WAGTAIL_SEARCH_URLS" > search/urls.py + -$(GIT_ADD) search/templates + -$(GIT_ADD) search/*.py + +.PHONY: wagtail-settings-default +wagtail-settings-default: + @echo "$$WAGTAIL_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) + +.PHONY: wagtail-sitepage-default +wagtail-sitepage-default: + python manage.py startapp sitepage + @echo "$$WAGTAIL_SITEPAGE_MODEL" > sitepage/models.py + $(ADD_DIR) sitepage/templates/sitepage/ + @echo "$$WAGTAIL_SITEPAGE_TEMPLATE" > sitepage/templates/sitepage/site_page.html + @echo "INSTALLED_APPS.append('sitepage')" >> $(DJANGO_SETTINGS_BASE_FILE) + python manage.py makemigrations sitepage + -$(GIT_ADD) sitepage/templates + -$(GIT_ADD) sitepage/*.py + -$(GIT_ADD) sitepage/migrations/*.py + +.PHONY: wagtail-urls-default +wagtail-urls-default: + @echo "$$WAGTAIL_URLS" > $(DJANGO_URLS_FILE) + +.PHONY: wagtail-urls-home-default +wagtail-urls-home-default: + @echo "$$WAGTAIL_URLS_HOME" >> $(DJANGO_URLS_FILE) + +.PHONY: webpack-init-default +webpack-init-default: npm-init + @echo "$$WEBPACK_CONFIG_JS" > webpack.config.js + -$(GIT_ADD) webpack.config.js + npm install --save-dev webpack webpack-cli webpack-dev-server + $(ADD_DIR) src/ + @echo "$$WEBPACK_INDEX_JS" > src/index.js + -$(GIT_ADD) src/index.js + @echo "$$WEBPACK_INDEX_HTML" > index.html + -$(GIT_ADD) index.html + $(MAKE) git-ignore + +.PHONY: webpack-init-reveal-default +webpack-init-reveal-default: npm-init + @echo "$$WEBPACK_REVEAL_CONFIG_JS" > webpack.config.js + -$(GIT_ADD) webpack.config.js + npm install --save-dev webpack webpack-cli webpack-dev-server + $(ADD_DIR) src/ + @echo "$$WEBPACK_REVEAL_INDEX_JS" > src/index.js + -$(GIT_ADD) src/index.js + @echo "$$WEBPACK_REVEAL_INDEX_HTML" > index.html + -$(GIT_ADD) index.html + $(MAKE) git-ignore + +# -------------------------------------------------------------------------------- +# Single-line phony target rules +# -------------------------------------------------------------------------------- + +.PHONY: aws-check-env-default +aws-check-env-default: aws-check-env-profile aws-check-env-region + +.PHONY: ce-default +ce-default: git-commit-edit git-push + +.PHONY: clean-default +clean-default: wagtail-clean + +.PHONY: cp-default +cp-default: git-commit-message git-push + +.PHONY: db-dump-default +db-dump-default: eb-export + +.PHONY: dbshell-default +dbshell-default: django-db-shell + +.PHONY: deploy-default +deploy-default: eb-deploy + +.PHONY: d-default +d-default: eb-deploy + +.PHONY: deps-default +deps-default: pip-deps + +.PHONY: e-default +e-default: edit + +.PHONY: edit-default +edit-default: readme-edit + +.PHONY: empty-default +empty-default: git-commit-message-empty git-push + +.PHONY: fp-default +fp-default: git-push-force + +.PHONY: freeze-default +freeze-default: pip-freeze git-push + +.PHONY: git-commit-default +git-commit-default: git-commit-message git-push + +.PHONY: git-commit-clean-default +git-commit-clean-default: git-commit-message-clean git-push + +.PHONY: git-commit-init-default +git-commit-init-default: git-commit-message-init git-push + +.PHONY: git-commit-lint-default +git-commit-lint-default: git-commit-message-lint git-push + +.PHONY: gitignore-default +gitignore-default: git-ignore + +.PHONY: h-default +h-default: help + +.PHONY: init-default +init-default: django-init-wagtail django-serve + +.PHONY: init-wagtail-default +init-wagtail-default: django-init-wagtail + +.PHONY: install-default +install-default: pip-install + +.PHONY: l-default +l-default: makefile-list-commands + +.PHONY: last-default +last-default: git-commit-message-last git-push + +.PHONY: lint-default +lint-default: django-lint + +.PHONY: list-commands-default +list-commands-default: makefile-list-commands + +.PHONY: list-defines-default +list-defines-default: makefile-list-defines + +.PHONY: list-exports-default +list-exports-default: makefile-list-exports + +.PHONY: list-targets-default +list-targets-default: makefile-list-targets + +.PHONY: migrate-default +migrate-default: django-migrate + +.PHONY: migrations-default +migrations-default: django-migrations-make + +.PHONY: migrations-show-default +migrations-show-default: django-migrations-show + +.PHONY: mk-default +mk-default: project.mk git-commit-message-mk git-push + +.PHONY: open-default +open-default: django-open + +.PHONY: o-default +o-default: django-open + +.PHONY: readme-default +readme-default: readme-init + +.PHONY: rename-default +rename-default: git-commit-message-rename git-push + +.PHONY: s-default +s-default: django-serve + +.PHONY: shell-default +shell-default: django-shell + +.PHONY: serve-default +serve-default: django-serve + +.PHONY: static-default +static-default: django-static + +.PHONY: sort-default +sort-default: git-commit-message-sort git-push + +.PHONY: su-default +su-default: django-su + +.PHONY: test-default +test-default: django-test + +.PHONY: t-default +t-default: django-test + +.PHONY: u-default +u-default: help + +.PHONY: urls-default +urls-default: django-urls-show + +# -------------------------------------------------------------------------------- +# Allow customizing rules defined in this Makefile with rules defined in +# $(MAKEFILE_CUSTOM_FILE) +# -------------------------------------------------------------------------------- + +%: %-default # https://stackoverflow.com/a/49804748 + @ true From 776eb9e9b7b4406767a45523cc833f7fbc20100b Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 16:08:26 -0400 Subject: [PATCH 094/652] Add/update project-makefile files --- Makefile | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 4200f95da..b5fbff2c9 100644 --- a/Makefile +++ b/Makefile @@ -4521,21 +4521,21 @@ db-dump-default: eb-export .PHONY: dbshell-default dbshell-default: django-db-shell +.PHONY: d-default +d-default: deploy + .PHONY: deploy-default deploy-default: eb-deploy -.PHONY: d-default -d-default: eb-deploy - .PHONY: deps-default deps-default: pip-deps -.PHONY: e-default -e-default: edit - .PHONY: edit-default edit-default: readme-edit +.PHONY: e-default +e-default: edit + .PHONY: empty-default empty-default: git-commit-message-empty git-push @@ -4605,12 +4605,12 @@ migrations-show-default: django-migrations-show .PHONY: mk-default mk-default: project.mk git-commit-message-mk git-push -.PHONY: open-default -open-default: django-open - .PHONY: o-default o-default: django-open +.PHONY: open-default +open-default: open + .PHONY: readme-default readme-default: readme-init @@ -4618,14 +4618,14 @@ readme-default: readme-init rename-default: git-commit-message-rename git-push .PHONY: s-default -s-default: django-serve - -.PHONY: shell-default -shell-default: django-shell +s-default: serve .PHONY: serve-default serve-default: django-serve +.PHONY: shell-default +shell-default: django-shell + .PHONY: static-default static-default: django-static @@ -4639,7 +4639,7 @@ su-default: django-su test-default: django-test .PHONY: t-default -t-default: django-test +t-default: test .PHONY: u-default u-default: help From 837af6420d40fe772bcd910d9c531c2ca8fe980e Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 16:43:28 -0400 Subject: [PATCH 095/652] Add/update project-makefile files --- Makefile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Makefile b/Makefile index b5fbff2c9..d8e621c6c 100644 --- a/Makefile +++ b/Makefile @@ -3980,6 +3980,10 @@ git-commit-message-default: git-commit-message-empty-default: -@$(GIT_COMMIT) --allow-empty -m "Empty-Commit" +.PHONY: git-commit-message-ignore-default +git-commit-message-ignore-default: + -@$(GIT_COMMIT) -a -m "Ignore" + .PHONY: git-commit-message-init-default git-commit-message-init-default: -@$(GIT_COMMIT) -a -m "Init" @@ -4225,6 +4229,7 @@ plone-instance-default: -$(GIT_ADD) backend/etc/site.zcml -$(GIT_ADD) backend/etc/zope.conf -$(GIT_ADD) backend/etc/zope.ini + -$(GIT_ADD) backend/inituser .PHONY: plone-serve-default plone-serve-default: @@ -4530,6 +4535,9 @@ deploy-default: eb-deploy .PHONY: deps-default deps-default: pip-deps +.PHONY: dump-default +dump-default: db-dump + .PHONY: edit-default edit-default: readme-edit @@ -4551,6 +4559,9 @@ git-commit-default: git-commit-message git-push .PHONY: git-commit-clean-default git-commit-clean-default: git-commit-message-clean git-push +.PHONY: git-commit-ignore-default +git-commit-ignore-default: git-commit-message-ignore git-push + .PHONY: git-commit-init-default git-commit-init-default: git-commit-message-init git-push @@ -4563,6 +4574,9 @@ gitignore-default: git-ignore .PHONY: h-default h-default: help +.PHONY: ignore-default +ignore-default: git-commit-message-ignore git-push + .PHONY: init-default init-default: django-init-wagtail django-serve From 575fad24726fe25d06ea985ca59eb9126e26a00b Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 17:02:53 -0400 Subject: [PATCH 096/652] Add/update project-makefile files --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d8e621c6c..a16a18055 100644 --- a/Makefile +++ b/Makefile @@ -1996,6 +1996,7 @@ _build/ .elasticbeanstalk/ db.sqlite3 static/ +backend/inituser backend/var endef @@ -4229,7 +4230,6 @@ plone-instance-default: -$(GIT_ADD) backend/etc/site.zcml -$(GIT_ADD) backend/etc/zope.conf -$(GIT_ADD) backend/etc/zope.ini - -$(GIT_ADD) backend/inituser .PHONY: plone-serve-default plone-serve-default: From 132663a881bfe2bfc5265e7e05de7113f06501a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Aug 2024 07:36:52 +1000 Subject: [PATCH 097/652] Updated error message for invalid width or height --- Tests/test_file_webp.py | 8 ++++++++ src/_webp.c | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 6ccd489bb..ad08da364 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -157,6 +157,14 @@ class TestFileWebp: im.save(temp_file, method=0) assert str(e.value) == "encoding error 6" + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") + def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.webp") + im = Image.new("L", (16384, 16384)) + with pytest.raises(ValueError) as e: + im.save(temp_file) + assert str(e.value) == "encoding error 5: Image size exceeds WebP limit" + def test_WebPEncode_with_invalid_args(self) -> None: """ Calling encoder functions with no arguments should result in an error. diff --git a/src/_webp.c b/src/_webp.c index d1943b3e0..0d2d6f023 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -672,7 +672,12 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { WebPPictureFree(&pic); if (!ok) { - PyErr_Format(PyExc_ValueError, "encoding error %d", (&pic)->error_code); + int error_code = (&pic)->error_code; + const char *message = ""; + if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { + message = ": Image size exceeds WebP limit"; + } + PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); return NULL; } output = writer.mem; From 0f64d08e64e2a1d8bedf280626006a95cbb2b086 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 21:16:02 -0400 Subject: [PATCH 098/652] Revert "Add/update project-makefile files" This reverts commit 575fad24726fe25d06ea985ca59eb9126e26a00b. Sorry for the noise (1/4)! --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a16a18055..d8e621c6c 100644 --- a/Makefile +++ b/Makefile @@ -1996,7 +1996,6 @@ _build/ .elasticbeanstalk/ db.sqlite3 static/ -backend/inituser backend/var endef @@ -4230,6 +4229,7 @@ plone-instance-default: -$(GIT_ADD) backend/etc/site.zcml -$(GIT_ADD) backend/etc/zope.conf -$(GIT_ADD) backend/etc/zope.ini + -$(GIT_ADD) backend/inituser .PHONY: plone-serve-default plone-serve-default: From e19841afa486c33673d9ff6cb8330761414f91de Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 21:16:35 -0400 Subject: [PATCH 099/652] Revert "Add/update project-makefile files" This reverts commit 837af6420d40fe772bcd910d9c531c2ca8fe980e. Sorry for the noise (2/4)! --- Makefile | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Makefile b/Makefile index d8e621c6c..b5fbff2c9 100644 --- a/Makefile +++ b/Makefile @@ -3980,10 +3980,6 @@ git-commit-message-default: git-commit-message-empty-default: -@$(GIT_COMMIT) --allow-empty -m "Empty-Commit" -.PHONY: git-commit-message-ignore-default -git-commit-message-ignore-default: - -@$(GIT_COMMIT) -a -m "Ignore" - .PHONY: git-commit-message-init-default git-commit-message-init-default: -@$(GIT_COMMIT) -a -m "Init" @@ -4229,7 +4225,6 @@ plone-instance-default: -$(GIT_ADD) backend/etc/site.zcml -$(GIT_ADD) backend/etc/zope.conf -$(GIT_ADD) backend/etc/zope.ini - -$(GIT_ADD) backend/inituser .PHONY: plone-serve-default plone-serve-default: @@ -4535,9 +4530,6 @@ deploy-default: eb-deploy .PHONY: deps-default deps-default: pip-deps -.PHONY: dump-default -dump-default: db-dump - .PHONY: edit-default edit-default: readme-edit @@ -4559,9 +4551,6 @@ git-commit-default: git-commit-message git-push .PHONY: git-commit-clean-default git-commit-clean-default: git-commit-message-clean git-push -.PHONY: git-commit-ignore-default -git-commit-ignore-default: git-commit-message-ignore git-push - .PHONY: git-commit-init-default git-commit-init-default: git-commit-message-init git-push @@ -4574,9 +4563,6 @@ gitignore-default: git-ignore .PHONY: h-default h-default: help -.PHONY: ignore-default -ignore-default: git-commit-message-ignore git-push - .PHONY: init-default init-default: django-init-wagtail django-serve From 1143bffe1a0a5fe6b7d58dd2c3660f91299cff8d Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 21:17:06 -0400 Subject: [PATCH 100/652] Revert "Add/update project-makefile files" This reverts commit 776eb9e9b7b4406767a45523cc833f7fbc20100b. Sorry for the noise (3/4)! --- Makefile | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index b5fbff2c9..4200f95da 100644 --- a/Makefile +++ b/Makefile @@ -4521,21 +4521,21 @@ db-dump-default: eb-export .PHONY: dbshell-default dbshell-default: django-db-shell -.PHONY: d-default -d-default: deploy - .PHONY: deploy-default deploy-default: eb-deploy +.PHONY: d-default +d-default: eb-deploy + .PHONY: deps-default deps-default: pip-deps -.PHONY: edit-default -edit-default: readme-edit - .PHONY: e-default e-default: edit +.PHONY: edit-default +edit-default: readme-edit + .PHONY: empty-default empty-default: git-commit-message-empty git-push @@ -4605,12 +4605,12 @@ migrations-show-default: django-migrations-show .PHONY: mk-default mk-default: project.mk git-commit-message-mk git-push +.PHONY: open-default +open-default: django-open + .PHONY: o-default o-default: django-open -.PHONY: open-default -open-default: open - .PHONY: readme-default readme-default: readme-init @@ -4618,14 +4618,14 @@ readme-default: readme-init rename-default: git-commit-message-rename git-push .PHONY: s-default -s-default: serve - -.PHONY: serve-default -serve-default: django-serve +s-default: django-serve .PHONY: shell-default shell-default: django-shell +.PHONY: serve-default +serve-default: django-serve + .PHONY: static-default static-default: django-static @@ -4639,7 +4639,7 @@ su-default: django-su test-default: django-test .PHONY: t-default -t-default: test +t-default: django-test .PHONY: u-default u-default: help From d6cfebd016db25549d05a9c5caa2ef3b53cff5c5 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 21:17:29 -0400 Subject: [PATCH 101/652] Revert "Add/update project-makefile files" This reverts commit 388fc78e36e60f45a9b5ff0cd3223e3b8bc2c307. Sorry for the noise (4/4)! --- Makefile | 4781 ++---------------------------------------------------- 1 file changed, 125 insertions(+), 4656 deletions(-) diff --git a/Makefile b/Makefile index 4200f95da..94f7565d8 100644 --- a/Makefile +++ b/Makefile @@ -1,4656 +1,125 @@ -# Project Makefile -# -# A makefile to automate setup of a Wagtail CMS project and related tasks. -# -# https://github.com/aclark4life/project-makefile -# -# -------------------------------------------------------------------------------- -# Set the default goal to be `git commit -a -m $(GIT_MESSAGE)` and `git push` -# -------------------------------------------------------------------------------- - -.DEFAULT_GOAL := git-commit - -# -------------------------------------------------------------------------------- -# Single line variables to be used by phony target rules -# -------------------------------------------------------------------------------- - -ADD_DIR := mkdir -pv -ADD_FILE := touch -AWS_OPTS := --no-cli-pager --output table -COPY_DIR := cp -rv -COPY_FILE := cp -v -DEL_DIR := rm -rv -DEL_FILE := rm -v -DJANGO_DB_COL = awk -F\= '{print $$2}' -DJANGO_DB_URL = eb ssh -c "source /opt/elasticbeanstalk/deployment/custom_env_var; env | grep DATABASE_URL" -DJANGO_DB_HOST = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ - python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["HOST"])') -DJANGO_DB_NAME = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ - python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["NAME"])') -DJANGO_DB_PASS = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ - python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["PASSWORD"])') -DJANGO_DB_USER = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ - python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["USER"])') -DJANGO_BACKEND_APPS_FILE := backend/apps.py -DJANGO_CUSTOM_ADMIN_FILE := backend/admin.py -DJANGO_FRONTEND_FILES = .babelrc .browserslistrc .eslintrc .nvmrc .stylelintrc.json frontend package-lock.json \ - package.json postcss.config.js -DJANGO_SETTINGS_DIR = backend/settings -DJANGO_SETTINGS_BASE_FILE = $(DJANGO_SETTINGS_DIR)/base.py -DJANGO_SETTINGS_DEV_FILE = $(DJANGO_SETTINGS_DIR)/dev.py -DJANGO_SETTINGS_PROD_FILE = $(DJANGO_SETTINGS_DIR)/production.py -DJANGO_SETTINGS_SECRET_KEY = $(shell openssl rand -base64 48) -DJANGO_URLS_FILE = backend/urls.py -EB_DIR_NAME := .elasticbeanstalk -EB_ENV_NAME ?= $(PROJECT_NAME)-$(GIT_BRANCH)-$(GIT_REV) -EB_PLATFORM ?= "Python 3.11 running on 64bit Amazon Linux 2023" -EC2_INSTANCE_MAX ?= 1 -EC2_INSTANCE_MIN ?= 1 -EC2_INSTANCE_PROFILE ?= aws-elasticbeanstalk-ec2-role -EC2_INSTANCE_TYPE ?= t4g.small -EC2_LB_TYPE ?= application -EDITOR_REVIEW = subl -GIT_ADD := git add -GIT_BRANCH = $(shell git branch --show-current) -GIT_BRANCHES = $(shell git branch -a) -GIT_CHECKOUT = git checkout -GIT_COMMIT_MSG = "Update $(PROJECT_NAME)" -GIT_COMMIT = git commit -GIT_PUSH = git push -GIT_PUSH_FORCE = git push --force-with-lease -GIT_REV = $(shell git rev-parse --short HEAD) -MAKEFILE_CUSTOM_FILE := project.mk -PACKAGE_NAME = $(shell echo $(PROJECT_NAME) | sed 's/-/_/g') -PAGER ?= less -PIP_ENSURE = python -m ensurepip -PIP_INSTALL_PLONE_CONSTRAINTS = https://dist.plone.org/release/6.0.11.1/constraints.txt -PROJECT_DIRS = backend contactpage home privacy siteuser -PROJECT_EMAIL := aclark@aclark.net -PROJECT_NAME = project-makefile -RANDIR := $(shell openssl rand -base64 12 | sed 's/\///g') -TMPDIR := $(shell mktemp -d) -UNAME := $(shell uname) -WAGTAIL_CLEAN_DIRS = backend contactpage dist frontend home logging_demo model_form_demo node_modules payments privacy search sitepage siteuser -WAGTAIL_CLEAN_FILES = .babelrc .browserslistrc .dockerignore .eslintrc .gitignore .nvmrc .stylelintrc.json Dockerfile db.sqlite3 docker-compose.yml manage.py package-lock.json package.json postcss.config.js requirements-test.txt requirements.txt - -# -------------------------------------------------------------------------------- -# Include $(MAKEFILE_CUSTOM_FILE) if it exists -# -------------------------------------------------------------------------------- - -ifneq ($(wildcard $(MAKEFILE_CUSTOM_FILE)),) - include $(MAKEFILE_CUSTOM_FILE) -endif - -# -------------------------------------------------------------------------------- -# Multi-line variables to be used in phony target rules -# -------------------------------------------------------------------------------- - -define DJANGO_ALLAUTH_BASE_TEMPLATE -{% extends 'base.html' %} -endef - -define DJANGO_API_SERIALIZERS -from rest_framework import serializers -from siteuser.models import User - - -class UserSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = User - fields = ["url", "username", "email", "is_staff"] -endef - -define DJANGO_API_VIEWS -from ninja import NinjaAPI -from rest_framework import viewsets -from siteuser.models import User -from .serializers import UserSerializer - -api = NinjaAPI() - - -@api.get("/hello") -def hello(request): - return "Hello world" - - -class UserViewSet(viewsets.ModelViewSet): - queryset = User.objects.all() - serializer_class = UserSerializer -endef - -define DJANGO_APP_TESTS -from django.test import TestCase -from django.urls import reverse -from .models import YourModel -from .forms import YourForm - - -class YourModelTest(TestCase): - def setUp(self): - self.instance = YourModel.objects.create(field1="value1", field2="value2") - - def test_instance_creation(self): - self.assertIsInstance(self.instance, YourModel) - self.assertEqual(self.instance.field1, "value1") - self.assertEqual(self.instance.field2, "value2") - - def test_str_method(self): - self.assertEqual(str(self.instance), "Expected String Representation") - - -class YourViewTest(TestCase): - def setUp(self): - self.instance = YourModel.objects.create(field1="value1", field2="value2") - - def test_view_url_exists_at_desired_location(self): - response = self.client.get("/your-url/") - self.assertEqual(response.status_code, 200) - - def test_view_url_accessible_by_name(self): - response = self.client.get(reverse("your-view-name")) - self.assertEqual(response.status_code, 200) - - def test_view_uses_correct_template(self): - response = self.client.get(reverse("your-view-name")) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, "your_template.html") - - def test_view_context(self): - response = self.client.get(reverse("your-view-name")) - self.assertEqual(response.status_code, 200) - self.assertIn("context_variable", response.context) - - -class YourFormTest(TestCase): - def test_form_valid_data(self): - form = YourForm(data={"field1": "value1", "field2": "value2"}) - self.assertTrue(form.is_valid()) - - def test_form_invalid_data(self): - form = YourForm(data={"field1": "", "field2": "value2"}) - self.assertFalse(form.is_valid()) - self.assertIn("field1", form.errors) - - def test_form_save(self): - form = YourForm(data={"field1": "value1", "field2": "value2"}) - if form.is_valid(): - instance = form.save() - self.assertEqual(instance.field1, "value1") - self.assertEqual(instance.field2, "value2") -endef - -define DJANGO_BACKEND_APPS -from django.contrib.admin.apps import AdminConfig - - -class CustomAdminConfig(AdminConfig): - default_site = "backend.admin.CustomAdminSite" -endef - -define DJANGO_BASE_TEMPLATE -{% load static webpack_loader %} - - - - - - {% block title %}{% endblock %} - {% block title_suffix %}{% endblock %} - - - {% stylesheet_pack 'app' %} - {% block extra_css %}{# Override this in templates to add extra stylesheets #}{% endblock %} - - {% include 'favicon.html' %} - {% csrf_token %} - - -
    -
    - {% include 'header.html' %} - {% if messages %} -
    - {% for message in messages %} - - {% endfor %} -
    - {% endif %} -
    - {% block content %}{% endblock %} -
    -
    - {% include 'footer.html' %} - {% include 'offcanvas.html' %} - {% javascript_pack 'app' %} - {% block extra_js %}{# Override this in templates to add extra javascript #}{% endblock %} - - -endef - -define DJANGO_CUSTOM_ADMIN -from django.contrib.admin import AdminSite - - -class CustomAdminSite(AdminSite): - site_header = "Project Makefile" - site_title = "Project Makefile" - index_title = "Project Makefile" - - -custom_admin_site = CustomAdminSite(name="custom_admin") -endef - -define DJANGO_DOCKERCOMPOSE -version: '3' - -services: - db: - image: postgres:latest - volumes: - - postgres_data:/var/lib/postgresql/data - environment: - POSTGRES_DB: project - POSTGRES_USER: admin - POSTGRES_PASSWORD: admin - - web: - build: . - command: sh -c "python manage.py migrate && gunicorn project.wsgi:application -b 0.0.0.0:8000" - volumes: - - .:/app - ports: - - "8000:8000" - depends_on: - - db - environment: - DATABASE_URL: postgres://admin:admin@db:5432/project - -volumes: - postgres_data: -endef - -define DJANGO_DOCKERFILE -FROM amazonlinux:2023 -RUN dnf install -y shadow-utils python3.11 python3.11-pip make nodejs20-npm nodejs postgresql15 postgresql15-server -USER postgres -RUN initdb -D /var/lib/pgsql/data -USER root -RUN useradd wagtail -EXPOSE 8000 -ENV PYTHONUNBUFFERED=1 PORT=8000 -COPY requirements.txt / -RUN python3.11 -m pip install -r /requirements.txt -WORKDIR /app -RUN chown wagtail:wagtail /app -COPY --chown=wagtail:wagtail . . -USER wagtail -RUN npm-20 install; npm-20 run build -RUN python3.11 manage.py collectstatic --noinput --clear -CMD set -xe; pg_ctl -D /var/lib/pgsql/data -l /tmp/logfile start; python3.11 manage.py migrate --noinput; gunicorn backend.wsgi:application -endef - -define DJANGO_FAVICON_TEMPLATE -{% load static %} - -endef - -define DJANGO_FOOTER_TEMPLATE -
    -

    © {% now "Y" %} {{ current_site.site_name|default:"Project Makefile" }}

    -
      -
    • - Home -
    • - {% for child in current_site.root_page.get_children %} -
    • - {{ child }} -
    • - {% endfor %} -
    -
    -endef - -define DJANGO_FRONTEND_APP -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import 'bootstrap'; -import '@fortawesome/fontawesome-free/js/fontawesome'; -import '@fortawesome/fontawesome-free/js/solid'; -import '@fortawesome/fontawesome-free/js/regular'; -import '@fortawesome/fontawesome-free/js/brands'; -import getDataComponents from '../dataComponents'; -import UserContextProvider from '../context'; -import * as components from '../components'; -import "../styles/index.scss"; -import "../styles/theme-blue.scss"; -import "./config"; - -const { ErrorBoundary } = components; -const dataComponents = getDataComponents(components); -const container = document.getElementById('app'); -const root = createRoot(container); -const App = () => ( - - - {dataComponents} - - -); -root.render(); -endef - -define DJANGO_FRONTEND_APP_CONFIG -import '../utils/themeToggler.js'; -// import '../utils/tinymce.js'; -endef - -define DJANGO_FRONTEND_BABELRC -{ - "presets": [ - [ - "@babel/preset-react", - ], - [ - "@babel/preset-env", - { - "useBuiltIns": "usage", - "corejs": "3.0.0" - } - ] - ], - "plugins": [ - "@babel/plugin-syntax-dynamic-import", - "@babel/plugin-transform-class-properties" - ] -} -endef - -define DJANGO_FRONTEND_COMPONENTS -export { default as ErrorBoundary } from './ErrorBoundary'; -export { default as UserMenu } from './UserMenu'; -endef - -define DJANGO_FRONTEND_COMPONENT_CLOCK -// Via ChatGPT -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import PropTypes from 'prop-types'; - -const Clock = ({ color = '#fff' }) => { - const [date, setDate] = useState(new Date()); - const [blink, setBlink] = useState(true); - const timerID = useRef(); - - const tick = useCallback(() => { - setDate(new Date()); - setBlink(prevBlink => !prevBlink); - }, []); - - useEffect(() => { - timerID.current = setInterval(() => tick(), 1000); - - // Return a cleanup function to be run on component unmount - return () => clearInterval(timerID.current); - }, [tick]); - - const formattedDate = date.toLocaleDateString(undefined, { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - }); - - const formattedTime = date.toLocaleTimeString(undefined, { - hour: 'numeric', - minute: 'numeric', - }); - - return ( - <> -
    {formattedDate} {formattedTime}
    - - ); -}; - -Clock.propTypes = { - color: PropTypes.string, -}; - -export default Clock; -endef - -define DJANGO_FRONTEND_COMPONENT_ERROR -import { Component } from 'react'; -import PropTypes from 'prop-types'; - -class ErrorBoundary extends Component { - constructor (props) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError () { - return { hasError: true }; - } - - componentDidCatch (error, info) { - const { onError } = this.props; - console.error(error); - onError && onError(error, info); - } - - render () { - const { children = null } = this.props; - const { hasError } = this.state; - - return hasError ? null : children; - } -} - -ErrorBoundary.propTypes = { - onError: PropTypes.func, - children: PropTypes.node, -}; - -export default ErrorBoundary; -endef - -define DJANGO_FRONTEND_COMPONENT_USER_MENU -// UserMenu.js -import React from 'react'; -import PropTypes from 'prop-types'; - -function handleLogout() { - window.location.href = '/accounts/logout'; -} - -const UserMenu = ({ isAuthenticated, isSuperuser, textColor }) => { - return ( -
    - {isAuthenticated ? ( -
  • - - -
  • - ) : ( -
  • - -
  • - )} -
    - ); -}; - -UserMenu.propTypes = { - isAuthenticated: PropTypes.bool.isRequired, - isSuperuser: PropTypes.bool.isRequired, - textColor: PropTypes.string, -}; - -export default UserMenu; -endef - -define DJANGO_FRONTEND_CONTEXT_INDEX -export { UserContextProvider as default } from './UserContextProvider'; -endef - -define DJANGO_FRONTEND_CONTEXT_USER_PROVIDER -// UserContextProvider.js -import React, { createContext, useContext, useState } from 'react'; -import PropTypes from 'prop-types'; - -const UserContext = createContext(); - -export const UserContextProvider = ({ children }) => { - const [isAuthenticated, setIsAuthenticated] = useState(false); - - const login = () => { - try { - // Add logic to handle login, set isAuthenticated to true - setIsAuthenticated(true); - } catch (error) { - console.error('Login error:', error); - // Handle error, e.g., show an error message to the user - } - }; - - const logout = () => { - try { - // Add logic to handle logout, set isAuthenticated to false - setIsAuthenticated(false); - } catch (error) { - console.error('Logout error:', error); - // Handle error, e.g., show an error message to the user - } - }; - - return ( - - {children} - - ); -}; - -UserContextProvider.propTypes = { - children: PropTypes.node.isRequired, -}; - -export const useUserContext = () => { - const context = useContext(UserContext); - - if (!context) { - throw new Error('useUserContext must be used within a UserContextProvider'); - } - - return context; -}; - -// Add PropTypes for the return value of useUserContext -useUserContext.propTypes = { - isAuthenticated: PropTypes.bool.isRequired, - login: PropTypes.func.isRequired, - logout: PropTypes.func.isRequired, -}; -endef - -define DJANGO_FRONTEND_ESLINTRC -{ - "env": { - "browser": true, - "es2021": true, - "node": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended" - ], - "overrides": [ - { - "env": { - "node": true - }, - "files": [ - ".eslintrc.{js,cjs}" - ], - "parserOptions": { - "sourceType": "script" - } - } - ], - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - "react" - ], - "rules": { - "no-unused-vars": "off" - }, - settings: { - react: { - version: 'detect', - }, - }, -} -endef - -define DJANGO_FRONTEND_OFFCANVAS_TEMPLATE -
    - -
    - -
    -
    -endef - -define DJANGO_FRONTEND_PORTAL -// Via pwellever -import React from 'react'; -import { createPortal } from 'react-dom'; - -const parseProps = data => Object.entries(data).reduce((result, [key, value]) => { - if (value.toLowerCase() === 'true') { - value = true; - } else if (value.toLowerCase() === 'false') { - value = false; - } else if (value.toLowerCase() === 'null') { - value = null; - } else if (!isNaN(parseFloat(value)) && isFinite(value)) { - // Parse numeric value - value = parseFloat(value); - } else if ( - (value[0] === '[' && value.slice(-1) === ']') || (value[0] === '{' && value.slice(-1) === '}') - ) { - // Parse JSON strings - value = JSON.parse(value); - } - - result[key] = value; - return result; -}, {}); - -// This method of using portals instead of calling ReactDOM.render on individual components -// ensures that all components are mounted under a single React tree, and are therefore able -// to share context. - -export default function getPageComponents (components) { - const getPortalComponent = domEl => { - // The element's "data-component" attribute is used to determine which component to render. - // All other "data-*" attributes are passed as props. - const { component: componentName, ...rest } = domEl.dataset; - const Component = components[componentName]; - if (!Component) { - console.error(`Component "$${componentName}" not found.`); - return null; - } - const props = parseProps(rest); - domEl.innerHTML = ''; - - // eslint-disable-next-line no-unused-vars - const { ErrorBoundary } = components; - return createPortal( - - - , - domEl, - ); - }; - - return Array.from(document.querySelectorAll('[data-component]')).map(getPortalComponent); -} -endef - -define DJANGO_FRONTEND_STYLES -// If you comment out code below, bootstrap will use red as primary color -// and btn-primary will become red - -// $primary: red; - -@import "~bootstrap/scss/bootstrap.scss"; - -.jumbotron { - // should be relative path of the entry scss file - background-image: url("../../vendors/images/sample.jpg"); - background-size: cover; -} - -#theme-toggler-authenticated:hover { - cursor: pointer; /* Change cursor to pointer on hover */ - color: #007bff; /* Change color on hover */ -} - -#theme-toggler-anonymous:hover { - cursor: pointer; /* Change cursor to pointer on hover */ - color: #007bff; /* Change color on hover */ -} -endef - -define DJANGO_FRONTEND_THEME_BLUE -@import "~bootstrap/scss/bootstrap.scss"; - -[data-bs-theme="blue"] { - --bs-body-color: var(--bs-white); - --bs-body-color-rgb: #{to-rgb($$white)}; - --bs-body-bg: var(--bs-blue); - --bs-body-bg-rgb: #{to-rgb($$blue)}; - --bs-tertiary-bg: #{$$blue-600}; - - .dropdown-menu { - --bs-dropdown-bg: #{color-mix($$blue-500, $$blue-600)}; - --bs-dropdown-link-active-bg: #{$$blue-700}; - } - - .btn-secondary { - --bs-btn-bg: #{color-mix($gray-600, $blue-400, .5)}; - --bs-btn-border-color: #{rgba($$white, .25)}; - --bs-btn-hover-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 5%)}; - --bs-btn-hover-border-color: #{rgba($$white, .25)}; - --bs-btn-active-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 10%)}; - --bs-btn-active-border-color: #{rgba($$white, .5)}; - --bs-btn-focus-border-color: #{rgba($$white, .5)}; - - // --bs-btn-focus-box-shadow: 0 0 0 .25rem rgba(255, 255, 255, 20%); - } -} -endef - -define DJANGO_FRONTEND_THEME_TOGGLER -document.addEventListener('DOMContentLoaded', function () { - const rootElement = document.documentElement; - const anonThemeToggle = document.getElementById('theme-toggler-anonymous'); - const authThemeToggle = document.getElementById('theme-toggler-authenticated'); - if (authThemeToggle) { - localStorage.removeItem('data-bs-theme'); - } - const anonSavedTheme = localStorage.getItem('data-bs-theme'); - if (anonSavedTheme) { - rootElement.setAttribute('data-bs-theme', anonSavedTheme); - } - if (anonThemeToggle) { - anonThemeToggle.addEventListener('click', function () { - const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; - const newTheme = currentTheme === 'light' ? 'dark' : 'light'; - rootElement.setAttribute('data-bs-theme', newTheme); - localStorage.setItem('data-bs-theme', newTheme); - }); - } - if (authThemeToggle) { - const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; - authThemeToggle.addEventListener('click', function () { - const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; - const newTheme = currentTheme === 'light' ? 'dark' : 'light'; - fetch('/user/update_theme_preference/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrfToken, // Include the CSRF token in the headers - }, - body: JSON.stringify({ theme: newTheme }), - }) - .then(response => response.json()) - .then(data => { - rootElement.setAttribute('data-bs-theme', newTheme); - }) - .catch(error => { - console.error('Error updating theme preference:', error); - }); - }); - } -}); -endef - -define DJANGO_HEADER_TEMPLATE -
    -
    - -
    -
    -endef - -define DJANGO_HOME_PAGE_ADMIN -from django.contrib import admin # noqa - -# Register your models here. -endef - -define DJANGO_HOME_PAGE_MODELS -from django.db import models # noqa - -# Create your models here. -endef - -define DJANGO_HOME_PAGE_TEMPLATE -{% extends "base.html" %} -{% block content %} -
    -
    -{% endblock %} -endef - -define DJANGO_HOME_PAGE_URLS -from django.urls import path -from .views import HomeView - -urlpatterns = [path("", HomeView.as_view(), name="home")] -endef - -define DJANGO_HOME_PAGE_VIEWS -from django.views.generic import TemplateView - - -class HomeView(TemplateView): - template_name = "home.html" -endef - -define DJANGO_LOGGING_DEMO_ADMIN -# Register your models here. -endef - -define DJANGO_LOGGING_DEMO_MODELS -from django.db import models # noqa - -# Create your models here. -endef - -define DJANGO_LOGGING_DEMO_SETTINGS -INSTALLED_APPS.append("logging_demo") # noqa -endef - -define DJANGO_LOGGING_DEMO_URLS -from django.urls import path -from .views import logging_demo - -urlpatterns = [ - path("", logging_demo, name="logging_demo"), -] -endef - -define DJANGO_LOGGING_DEMO_VIEWS -from django.http import HttpResponse -import logging - -logger = logging.getLogger(__name__) - - -def logging_demo(request): - logger.debug("Hello, world!") - return HttpResponse("Hello, world!") -endef - -define DJANGO_MANAGE_PY -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" - -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.dev") - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == "__main__": - main() -endef - -define DJANGO_MODEL_FORM_DEMO_ADMIN -from django.contrib import admin -from .models import ModelFormDemo - - -@admin.register(ModelFormDemo) -class ModelFormDemoAdmin(admin.ModelAdmin): - pass -endef - -define DJANGO_MODEL_FORM_DEMO_FORMS -from django import forms -from .models import ModelFormDemo - - -class ModelFormDemoForm(forms.ModelForm): - class Meta: - model = ModelFormDemo - fields = ["name", "email", "age", "is_active"] -endef - -define DJANGO_MODEL_FORM_DEMO_MODEL -from django.db import models -from django.shortcuts import reverse - - -class ModelFormDemo(models.Model): - name = models.CharField(max_length=100, blank=True, null=True) - email = models.EmailField(blank=True, null=True) - age = models.IntegerField(blank=True, null=True) - is_active = models.BooleanField(default=True) - created_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return self.name or f"test-model-{self.pk}" - - def get_absolute_url(self): - return reverse("model_form_demo_detail", kwargs={"pk": self.pk}) -endef - -define DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL -{% extends 'base.html' %} -{% block content %} -

    Test Model Detail: {{ model_form_demo.name }}

    -

    Name: {{ model_form_demo.name }}

    -

    Email: {{ model_form_demo.email }}

    -

    Age: {{ model_form_demo.age }}

    -

    Active: {{ model_form_demo.is_active }}

    -

    Created At: {{ model_form_demo.created_at }}

    - Edit Test Model -{% endblock %} -endef - -define DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM -{% extends 'base.html' %} -{% block content %} -

    - {% if form.instance.pk %} - Update Test Model - {% else %} - Create Test Model - {% endif %} -

    -
    - {% csrf_token %} - {{ form.as_p }} - -
    -{% endblock %} -endef - -define DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST -{% extends 'base.html' %} -{% block content %} -

    Test Models List

    - - Create New Test Model -{% endblock %} -endef - -define DJANGO_MODEL_FORM_DEMO_URLS -from django.urls import path -from .views import ( - ModelFormDemoListView, - ModelFormDemoCreateView, - ModelFormDemoUpdateView, - ModelFormDemoDetailView, -) - -urlpatterns = [ - path("", ModelFormDemoListView.as_view(), name="model_form_demo_list"), - path("create/", ModelFormDemoCreateView.as_view(), name="model_form_demo_create"), - path( - "/update/", - ModelFormDemoUpdateView.as_view(), - name="model_form_demo_update", - ), - path("/", ModelFormDemoDetailView.as_view(), name="model_form_demo_detail"), -] -endef - -define DJANGO_MODEL_FORM_DEMO_VIEWS -from django.views.generic import ListView, CreateView, UpdateView, DetailView -from .models import ModelFormDemo -from .forms import ModelFormDemoForm - - -class ModelFormDemoListView(ListView): - model = ModelFormDemo - template_name = "model_form_demo_list.html" - context_object_name = "model_form_demos" - - -class ModelFormDemoCreateView(CreateView): - model = ModelFormDemo - form_class = ModelFormDemoForm - template_name = "model_form_demo_form.html" - - def form_valid(self, form): - form.instance.created_by = self.request.user - return super().form_valid(form) - - -class ModelFormDemoUpdateView(UpdateView): - model = ModelFormDemo - form_class = ModelFormDemoForm - template_name = "model_form_demo_form.html" - - -class ModelFormDemoDetailView(DetailView): - model = ModelFormDemo - template_name = "model_form_demo_detail.html" - context_object_name = "model_form_demo" -endef - -define DJANGO_PAYMENTS_ADMIN -from django.contrib import admin -from .models import Product, Order - -admin.site.register(Product) -admin.site.register(Order) -endef - -define DJANGO_PAYMENTS_FORM -from django import forms - - -class PaymentsForm(forms.Form): - stripeToken = forms.CharField(widget=forms.HiddenInput()) - amount = forms.DecimalField( - max_digits=10, decimal_places=2, widget=forms.HiddenInput() - ) -endef - -define DJANGO_PAYMENTS_MIGRATION_0002 -from django.db import migrations -import os -import secrets -import logging - -logger = logging.getLogger(__name__) - - -def generate_default_key(): - return "sk_test_" + secrets.token_hex(24) - - -def set_stripe_api_keys(apps, schema_editor): - # Get the Stripe API Key model - APIKey = apps.get_model("djstripe", "APIKey") - - # Fetch the keys from environment variables or generate default keys - test_secret_key = os.environ.get("STRIPE_TEST_SECRET_KEY", generate_default_key()) - live_secret_key = os.environ.get("STRIPE_LIVE_SECRET_KEY", generate_default_key()) - - logger.info("STRIPE_TEST_SECRET_KEY: %s", test_secret_key) - logger.info("STRIPE_LIVE_SECRET_KEY: %s", live_secret_key) - - # Check if the keys are not already in the database - if not APIKey.objects.filter(secret=test_secret_key).exists(): - APIKey.objects.create(secret=test_secret_key, livemode=False) - logger.info("Added test secret key to the database.") - else: - logger.info("Test secret key already exists in the database.") - - if not APIKey.objects.filter(secret=live_secret_key).exists(): - APIKey.objects.create(secret=live_secret_key, livemode=True) - logger.info("Added live secret key to the database.") - else: - logger.info("Live secret key already exists in the database.") - - -class Migration(migrations.Migration): - - dependencies = [ - ("payments", "0001_initial"), - ] - - operations = [ - migrations.RunPython(set_stripe_api_keys), - ] -endef - -define DJANGO_PAYMENTS_MIGRATION_0003 -from django.db import migrations - - -def create_initial_products(apps, schema_editor): - Product = apps.get_model("payments", "Product") - Product.objects.create(name="T-shirt", description="A cool T-shirt", price=20.00) - Product.objects.create(name="Mug", description="A nice mug", price=10.00) - Product.objects.create(name="Hat", description="A stylish hat", price=15.00) - - -class Migration(migrations.Migration): - dependencies = [ - ( - "payments", - "0002_set_stripe_api_keys", - ), - ] - - operations = [ - migrations.RunPython(create_initial_products), - ] -endef - -define DJANGO_PAYMENTS_MODELS -from django.db import models - - -class Product(models.Model): - name = models.CharField(max_length=100) - description = models.TextField() - price = models.DecimalField(max_digits=10, decimal_places=2) - - def __str__(self): - return self.name - - -class Order(models.Model): - product = models.ForeignKey(Product, on_delete=models.CASCADE) - created_at = models.DateTimeField(auto_now_add=True) - stripe_checkout_session_id = models.CharField(max_length=200) - - def __str__(self): - return f"Order {self.id} for {self.product.name}" -endef - -define DJANGO_PAYMENTS_TEMPLATE_CANCEL -{% extends "base.html" %} -{% block title %}Cancel{% endblock %} -{% block content %} -

    Payment Cancelled

    -

    Your payment was cancelled.

    -{% endblock %} -endef - -define DJANGO_PAYMENTS_TEMPLATE_CHECKOUT -{% extends "base.html" %} -{% block title %}Checkout{% endblock %} -{% block content %} -

    Checkout

    -
    - {% csrf_token %} - -
    -{% endblock %} -endef - -define DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL -{% extends "base.html" %} -{% block title %}{{ product.name }}{% endblock %} -{% block content %} -

    {{ product.name }}

    -

    {{ product.description }}

    -

    Price: ${{ product.price }}

    -
    - {% csrf_token %} - - -
    -{% endblock %} -endef - -define DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST -{% extends "base.html" %} -{% block title %}Products{% endblock %} -{% block content %} -

    Products

    - -{% endblock %} -endef - -define DJANGO_PAYMENTS_TEMPLATE_SUCCESS -{% extends "base.html" %} -{% block title %}Success{% endblock %} -{% block content %} -

    Payment Successful

    -

    Thank you for your purchase!

    -{% endblock %} -endef - -define DJANGO_PAYMENTS_URLS -from django.urls import path -from .views import ( - CheckoutView, - SuccessView, - CancelView, - ProductListView, - ProductDetailView, -) - -urlpatterns = [ - path("", ProductListView.as_view(), name="product_list"), - path("product//", ProductDetailView.as_view(), name="product_detail"), - path("checkout/", CheckoutView.as_view(), name="checkout"), - path("success/", SuccessView.as_view(), name="success"), - path("cancel/", CancelView.as_view(), name="cancel"), -] -endef - -define DJANGO_PAYMENTS_VIEW -from django.conf import settings -from django.shortcuts import render, redirect, get_object_or_404 -from django.views.generic import TemplateView, View, ListView, DetailView -import stripe -from .models import Product, Order - -stripe.api_key = settings.STRIPE_TEST_SECRET_KEY - - -class ProductListView(ListView): - model = Product - template_name = "payments/product_list.html" - context_object_name = "products" - - -class ProductDetailView(DetailView): - model = Product - template_name = "payments/product_detail.html" - context_object_name = "product" - - -class CheckoutView(View): - template_name = "payments/checkout.html" - - def get(self, request, *args, **kwargs): - products = Product.objects.all() - return render(request, self.template_name, {"products": products}) - - def post(self, request, *args, **kwargs): - product_id = request.POST.get("product_id") - product = get_object_or_404(Product, id=product_id) - - session = stripe.checkout.Session.create( - payment_method_types=["card"], - line_items=[ - { - "price_data": { - "currency": "usd", - "product_data": { - "name": product.name, - }, - "unit_amount": int(product.price * 100), - }, - "quantity": 1, - } - ], - mode="payment", - success_url="http://localhost:8000/payments/success/", - cancel_url="http://localhost:8000/payments/cancel/", - ) - - Order.objects.create(product=product, stripe_checkout_session_id=session.id) - return redirect(session.url, code=303) - - -class SuccessView(TemplateView): - - template_name = "payments/success.html" - - -class CancelView(TemplateView): - - template_name = "payments/cancel.html" -endef - -define DJANGO_SEARCH_FORMS -from django import forms - - -class SearchForm(forms.Form): - query = forms.CharField(max_length=100, required=True, label="Search") - -endef - -define DJANGO_SEARCH_SETTINGS -SEARCH_MODELS = [ - # Add search models here. -] -endef - -define DJANGO_SEARCH_TEMPLATE -{% extends "base.html" %} -{% block body_class %}template-searchresults{% endblock %} -{% block title %}Search{% endblock %} -{% block content %} -

    Search

    -
    - - -
    - {% if search_results %} -
      - {% for result in search_results %} -
    • -

      - {{ result }} -

      - {% if result.search_description %}{{ result.search_description }}{% endif %} -
    • - {% endfor %} -
    - {% if search_results.has_previous %} - Previous - {% endif %} - {% if search_results.has_next %} - Next - {% endif %} - {% elif search_query %} - No results found - {% else %} - No results found. Try a test query? - {% endif %} -{% endblock %} -endef - -define DJANGO_SEARCH_URLS -from django.urls import path -from .views import SearchView - -urlpatterns = [ - path("search/", SearchView.as_view(), name="search"), -] -endef - -define DJANGO_SEARCH_UTILS -from django.apps import apps -from django.conf import settings - -def get_search_models(): - models = [] - for model_path in settings.SEARCH_MODELS: - app_label, model_name = model_path.split(".") - model = apps.get_model(app_label, model_name) - models.append(model) - return models -endef - -define DJANGO_SEARCH_VIEWS -from django.views.generic import ListView -from django.db import models -from django.db.models import Q -from .forms import SearchForm -from .utils import get_search_models - - -class SearchView(ListView): - template_name = "your_app/search_results.html" - context_object_name = "results" - paginate_by = 10 - - def get_queryset(self): - form = SearchForm(self.request.GET) - query = None - results = [] - - if form.is_valid(): - query = form.cleaned_data["query"] - search_models = get_search_models() - - for model in search_models: - fields = [f.name for f in model._meta.fields if isinstance(f, (models.CharField, models.TextField))] - queries = [Q(**{f"{field}__icontains": query}) for field in fields] - model_results = model.objects.filter(queries.pop()) - - for item in queries: - model_results = model_results.filter(item) - - results.extend(model_results) - - return results - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["form"] = SearchForm(self.request.GET) - context["query"] = self.request.GET.get("query", "") - return context -endef - -define DJANGO_SETTINGS_AUTHENTICATION_BACKENDS -AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", - "allauth.account.auth_backends.AuthenticationBackend", -] -endef - -define DJANGO_SETTINGS_BASE -# $(PROJECT_NAME) -# -# Uncomment next two lines to enable custom admin -# INSTALLED_APPS = [app for app in INSTALLED_APPS if app != 'django.contrib.admin'] -# INSTALLED_APPS.append('backend.apps.CustomAdminConfig') -import os # noqa -import dj_database_url # noqa - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -EXPLORER_CONNECTIONS = {"Default": "default"} -EXPLORER_DEFAULT_CONNECTION = "default" -LOGIN_REDIRECT_URL = "/" -PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -SILENCED_SYSTEM_CHECKS = ["django_recaptcha.recaptcha_test_key_error"] -BASE_DIR = os.path.dirname(PROJECT_DIR) -STATICFILES_DIRS = [] -WEBPACK_LOADER = { - "MANIFEST_FILE": os.path.join(BASE_DIR, "frontend/build/manifest.json"), -} -STATICFILES_DIRS.append(os.path.join(BASE_DIR, "frontend/build")) -TEMPLATES[0]["DIRS"].append(os.path.join(PROJECT_DIR, "templates")) -endef - -define DJANGO_SETTINGS_BASE_MINIMAL -# $(PROJECT_NAME) -import os # noqa -import dj_database_url # noqa - -INSTALLED_APPS.append("debug_toolbar") -INSTALLED_APPS.append("webpack_boilerplate") -PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -BASE_DIR = os.path.dirname(PROJECT_DIR) -STATICFILES_DIRS = [] -STATICFILES_DIRS.append(os.path.join(BASE_DIR, "frontend/build")) -TEMPLATES[0]["DIRS"].append(os.path.join(PROJECT_DIR, "templates")) -WEBPACK_LOADER = { - "MANIFEST_FILE": os.path.join(BASE_DIR, "frontend/build/manifest.json"), -} -endef - -define DJANGO_SETTINGS_CRISPY_FORMS -CRISPY_TEMPLATE_PACK = "bootstrap5" -CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" -endef - -define DJANGO_SETTINGS_DATABASE -DATABASE_URL = os.environ.get("DATABASE_URL", "postgres://:@:/$(PROJECT_NAME)") -DATABASES["default"] = dj_database_url.parse(DATABASE_URL) -endef - -define DJANGO_SETTINGS_DEV -from .base import * # noqa - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -# SECURITY WARNING: define the correct hosts in production! -ALLOWED_HOSTS = ["*"] - -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - -try: - from .local import * # noqa -except ImportError: - pass - -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "verbose": { - "format": "{levelname} {asctime} {module} {message}", - "style": "{", - }, - "simple": { - "format": "{levelname} {message}", - "style": "{", - }, - }, - "handlers": { - "console": { - "level": "DEBUG", - "class": "logging.StreamHandler", - "formatter": "verbose", - }, - }, - "loggers": { - "django": { - "handlers": ["console"], - "level": "DEBUG", - "propagate": True, - }, - }, -} - -INTERNAL_IPS = [ - "127.0.0.1", -] - -MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") # noqa -MIDDLEWARE.append("hijack.middleware.HijackUserMiddleware") # noqa -INSTALLED_APPS.append("django.contrib.admindocs") # noqa -SECRET_KEY = "$(DJANGO_SETTINGS_SECRET_KEY)" -endef - - -define DJANGO_SETTINGS_HOME_PAGE -INSTALLED_APPS.append("home") -endef - -define DJANGO_SETTINGS_INSTALLED_APPS -INSTALLED_APPS.append("allauth") -INSTALLED_APPS.append("allauth.account") -INSTALLED_APPS.append("allauth.socialaccount") -INSTALLED_APPS.append("crispy_bootstrap5") -INSTALLED_APPS.append("crispy_forms") -INSTALLED_APPS.append("debug_toolbar") -INSTALLED_APPS.append("django_extensions") -INSTALLED_APPS.append("django_recaptcha") -INSTALLED_APPS.append("rest_framework") -INSTALLED_APPS.append("rest_framework.authtoken") -INSTALLED_APPS.append("webpack_boilerplate") -INSTALLED_APPS.append("explorer") -endef - -define DJANGO_SETTINGS_MIDDLEWARE -MIDDLEWARE.append("allauth.account.middleware.AccountMiddleware") -endef - -define DJANGO_SETTINGS_MODEL_FORM_DEMO -INSTALLED_APPS.append("model_form_demo") # noqa -endef - -define DJANGO_SETTINGS_PAYMENTS -DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id" -DJSTRIPE_WEBHOOK_VALIDATION = "retrieve_event" -STRIPE_PUBLISHABLE_KEY = os.environ.get("STRIPE_PUBLISHABLE_KEY") -STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY") -STRIPE_TEST_SECRET_KEY = os.environ.get("STRIPE_TEST_SECRET_KEY") -INSTALLED_APPS.append("payments") # noqa -INSTALLED_APPS.append("djstripe") # noqa -endef - -define DJANGO_SETTINGS_REST_FRAMEWORK -REST_FRAMEWORK = { - # Use Django's standard `django.contrib.auth` permissions, - # or allow read-only access for unauthenticated users. - "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" - ] -} -endef - -define DJANGO_SETTINGS_SITEUSER -INSTALLED_APPS.append("siteuser") # noqa -AUTH_USER_MODEL = "siteuser.User" -endef - -define DJANGO_SETTINGS_PROD -from .base import * # noqa -from backend.utils import get_ec2_metadata - -DEBUG = False - -try: - from .local import * # noqa -except ImportError: - pass - -LOCAL_IPV4 = get_ec2_metadata() -ALLOWED_HOSTS.append(LOCAL_IPV4) # noqa -endef - -define DJANGO_SETTINGS_THEMES -THEMES = [ - ("light", "Light Theme"), - ("dark", "Dark Theme"), -] -endef - -define DJANGO_SITEUSER_ADMIN -from django.contrib.auth.admin import UserAdmin -from django.contrib import admin - -from .models import User - -admin.site.register(User, UserAdmin) -endef - -define DJANGO_SITEUSER_EDIT_TEMPLATE -{% extends 'base.html' %} -{% load crispy_forms_tags %} -{% block content %} -

    Edit User

    - {% crispy form %} -{% endblock %} -endef - -define DJANGO_SITEUSER_FORM -from django import forms -from django.contrib.auth.forms import UserChangeForm -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Fieldset, ButtonHolder, Submit -from .models import User - - -class SiteUserForm(UserChangeForm): - bio = forms.CharField(widget=forms.Textarea(attrs={"id": "editor"}), required=False) - - class Meta(UserChangeForm.Meta): - model = User - fields = ("username", "user_theme_preference", "bio", "rate") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_method = "post" - self.helper.layout = Layout( - Fieldset( - "Edit Your Profile", - "username", - "user_theme_preference", - "bio", - "rate", - ), - ButtonHolder(Submit("submit", "Save", css_class="btn btn-primary")), - ) -endef - -define DJANGO_SITEUSER_MODEL -from django.db import models -from django.contrib.auth.models import AbstractUser, Group, Permission -from django.conf import settings - - -class User(AbstractUser): - groups = models.ManyToManyField(Group, related_name="siteuser_set", blank=True) - user_permissions = models.ManyToManyField( - Permission, related_name="siteuser_set", blank=True - ) - - user_theme_preference = models.CharField( - max_length=10, choices=settings.THEMES, default="light" - ) - - bio = models.TextField(blank=True, null=True) - rate = models.FloatField(blank=True, null=True) -endef - -define DJANGO_SITEUSER_URLS -from django.urls import path -from .views import UserProfileView, UpdateThemePreferenceView, UserEditView - -urlpatterns = [ - path("profile/", UserProfileView.as_view(), name="user-profile"), - path( - "update_theme_preference/", - UpdateThemePreferenceView.as_view(), - name="update_theme_preference", - ), - path("/edit/", UserEditView.as_view(), name="user-edit"), -] -endef - -define DJANGO_SITEUSER_VIEW -import json - -from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import JsonResponse -from django.utils.decorators import method_decorator -from django.views import View -from django.views.decorators.csrf import csrf_exempt -from django.views.generic import DetailView -from django.views.generic.edit import UpdateView -from django.urls import reverse_lazy - -from .models import User -from .forms import SiteUserForm - - -class UserProfileView(LoginRequiredMixin, DetailView): - model = User - template_name = "profile.html" - - def get_object(self, queryset=None): - return self.request.user - - -@method_decorator(csrf_exempt, name="dispatch") -class UpdateThemePreferenceView(View): - def post(self, request, *args, **kwargs): - try: - data = json.loads(request.body.decode("utf-8")) - new_theme = data.get("theme") - user = request.user - user.user_theme_preference = new_theme - user.save() - response_data = {"theme": new_theme} - return JsonResponse(response_data) - except json.JSONDecodeError as e: - return JsonResponse({"error": e}, status=400) - - def http_method_not_allowed(self, request, *args, **kwargs): - return JsonResponse({"error": "Invalid request method"}, status=405) - - -class UserEditView(LoginRequiredMixin, UpdateView): - model = User - template_name = "user_edit.html" # Create this template in your templates folder - form_class = SiteUserForm - - def get_success_url(self): - # return reverse_lazy("user-profile", kwargs={"pk": self.object.pk}) - return reverse_lazy("user-profile") -endef - -define DJANGO_SITEUSER_VIEW_TEMPLATE -{% extends 'base.html' %} -{% block content %} -

    User Profile

    -
    - Edit -
    -

    Username: {{ user.username }}

    -

    Theme: {{ user.user_theme_preference }}

    -

    Bio: {{ user.bio|default:""|safe }}

    -

    Rate: {{ user.rate|default:"" }}

    -{% endblock %} -endef - -define DJANGO_URLS -from django.contrib import admin -from django.urls import path, include -from django.conf import settings - -urlpatterns = [ - path("django/", admin.site.urls), -] -endef - -define DJANGO_URLS_ALLAUTH -urlpatterns += [path("accounts/", include("allauth.urls"))] -endef - -define DJANGO_URLS_API -from rest_framework import routers # noqa -from .api import UserViewSet, api # noqa - -router = routers.DefaultRouter() -router.register(r"users", UserViewSet) -# urlpatterns += [path("api/", include(router.urls))] -urlpatterns += [path("api/", api.urls)] -endef - -define DJANGO_URLS_DEBUG_TOOLBAR -if settings.DEBUG: - urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))] -endef - -define DJANGO_URLS_HOME_PAGE -urlpatterns += [path("", include("home.urls"))] -endef - -define DJANGO_URLS_LOGGING_DEMO -urlpatterns += [path("logging-demo/", include("logging_demo.urls"))] -endef - -define DJANGO_URLS_MODEL_FORM_DEMO -urlpatterns += [path("model-form-demo/", include("model_form_demo.urls"))] -endef - -define DJANGO_URLS_PAYMENTS -urlpatterns += [path("payments/", include("payments.urls"))] -endef - -define DJANGO_URLS_SITEUSER -urlpatterns += [path("user/", include("siteuser.urls"))] -endef - -define DJANGO_UTILS -from django.urls import URLResolver -import requests - - -def get_ec2_metadata(): - try: - # Step 1: Get the token - token_url = "http://169.254.169.254/latest/api/token" - headers = {"X-aws-ec2-metadata-token-ttl-seconds": "21600"} - response = requests.put(token_url, headers=headers) - response.raise_for_status() # Raise an error for bad responses - - token = response.text - - # Step 2: Use the token to get the instance metadata - metadata_url = "http://169.254.169.254/latest/meta-data/local-ipv4" - headers = {"X-aws-ec2-metadata-token": token} - response = requests.get(metadata_url, headers=headers) - response.raise_for_status() # Raise an error for bad responses - - metadata = response.text - return metadata - except requests.RequestException as e: - print(f"Error retrieving EC2 metadata: {e}") - return None - - -# Function to remove a specific URL pattern based on its route (including catch-all) -def remove_urlpattern(urlpatterns, route_to_remove): - urlpatterns[:] = [ - urlpattern - for urlpattern in urlpatterns - if not ( - isinstance(urlpattern, URLResolver) - and urlpattern.pattern._route == route_to_remove - ) - ] -endef - -define EB_CUSTOM_ENV_EC2_USER -files: - "/home/ec2-user/.bashrc": - mode: "000644" - owner: ec2-user - group: ec2-user - content: | - # .bashrc - - # Source global definitions - if [ -f /etc/bashrc ]; then - . /etc/bashrc - fi - - # User specific aliases and functions - set -o vi - - source <(sed -E -n 's/[^#]+/export &/ p' /opt/elasticbeanstalk/deployment/custom_env_var) -endef - -define EB_CUSTOM_ENV_VAR_FILE -#!/bin/bash - -# Via https://aws.amazon.com/premiumsupport/knowledge-center/elastic-beanstalk-env-variables-linux2/ - -#Create a copy of the environment variable file. -cat /opt/elasticbeanstalk/deployment/env | perl -p -e 's/(.*)=(.*)/export $$1="$$2"/;' > /opt/elasticbeanstalk/deployment/custom_env_var - -#Set permissions to the custom_env_var file so this file can be accessed by any user on the instance. You can restrict permissions as per your requirements. -chmod 644 /opt/elasticbeanstalk/deployment/custom_env_var - -# add the virtual env path in. -VENV=/var/app/venv/`ls /var/app/venv` -cat <> /opt/elasticbeanstalk/deployment/custom_env_var -VENV=$$ENV -EOF - -#Remove duplicate files upon deployment. -rm -f /opt/elasticbeanstalk/deployment/*.bak -endef - -define GIT_IGNORE -__pycache__ -*.pyc -dist/ -node_modules/ -_build/ -.elasticbeanstalk/ -db.sqlite3 -static/ -backend/var -endef - -define JENKINS_FILE -pipeline { - agent any - stages { - stage('') { - steps { - echo '' - } - } - } -} -endef - -define MAKEFILE_CUSTOM -# Custom Makefile -# Add your custom makefile commands here -# -# PROJECT_NAME := my-new-project -endef - -define PIP_INSTALL_REQUIREMENTS_TEST -pytest -pytest-runner -coverage -pytest-mock -pytest-cov -hypothesis -selenium -pytest-django -factory-boy -flake8 -tox -endef - -define PROGRAMMING_INTERVIEW -from rich import print as rprint -from rich.console import Console -from rich.panel import Panel - -import argparse -import locale -import math -import time - -import code # noqa -import readline # noqa -import rlcompleter # noqa - - -locale.setlocale(locale.LC_ALL, "en_US.UTF-8") - - -class DataStructure: - # Data Structure: Binary Tree - class TreeNode: - def __init__(self, value=0, left=None, right=None): - self.value = value - self.left = left - self.right = right - - # Data Structure: Stack - class Stack: - def __init__(self): - self.items = [] - - def push(self, item): - self.items.append(item) - - def pop(self): - if not self.is_empty(): - return self.items.pop() - return None - - def peek(self): - if not self.is_empty(): - return self.items[-1] - return None - - def is_empty(self): - return len(self.items) == 0 - - def size(self): - return len(self.items) - - # Data Structure: Queue - class Queue: - def __init__(self): - self.items = [] - - def enqueue(self, item): - self.items.append(item) - - def dequeue(self): - if not self.is_empty(): - return self.items.pop(0) - return None - - def is_empty(self): - return len(self.items) == 0 - - def size(self): - return len(self.items) - - # Data Structure: Linked List - class ListNode: - def __init__(self, value=0, next=None): - self.value = value - self.next = next - - -class Interview(DataStructure): - - # Protected methods for factorial calculation - def _factorial_recursive(self, n): - if n == 0: - return 1 - return n * self._factorial_recursive(n - 1) - - def _factorial_divide_and_conquer(self, low, high): - if low > high: - return 1 - if low == high: - return low - mid = (low + high) // 2 - return self._factorial_divide_and_conquer( - low, mid - ) * self._factorial_divide_and_conquer(mid + 1, high) - - # Recursive Factorial with Timing - def factorial_recursive(self, n): - start_time = time.time() # Start timing - result = self._factorial_recursive(n) # Calculate factorial - end_time = time.time() # End timing - elapsed_time = end_time - start_time - return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" - - # Iterative Factorial with Timing - def factorial_iterative(self, n): - start_time = time.time() # Start timing - result = 1 - for i in range(1, n + 1): - result *= i - end_time = time.time() # End timing - elapsed_time = end_time - start_time - return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" - - # Divide and Conquer Factorial with Timing - def factorial_divide_and_conquer(self, n): - start_time = time.time() # Start timing - result = self._factorial_divide_and_conquer(1, n) # Calculate factorial - end_time = time.time() # End timing - elapsed_time = end_time - start_time - return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" - - # Built-in Factorial with Timing - def factorial_builtin(self, n): - start_time = time.time() # Start timing - result = math.factorial(n) # Calculate factorial using built-in - end_time = time.time() # End timing - - # Calculate elapsed time - elapsed_time = end_time - start_time - - # Print complexity and runtime - return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" - - # Recursion: Fibonacci - def fibonacci_recursive(self, n): - if n <= 1: - return n - return self.fibonacci_recursive(n - 1) + self.fibonacci_recursive(n - 2) - - # Iteration: Fibonacci - def fibonacci_iterative(self, n): - if n <= 1: - return n - a, b = 0, 1 - for _ in range(n - 1): - a, b = b, a + b - return b - - # Searching: Linear Search - def linear_search(self, arr, target): - for i, value in enumerate(arr): - if value == target: - return i - return -1 - - # Searching: Binary Search - def binary_search(self, arr, target): - left, right = 0, len(arr) - 1 - while left <= right: - mid = (left + right) // 2 - if arr[mid] == target: - return mid - elif arr[mid] < target: - left = mid + 1 - else: - right = mid - 1 - return -1 - - # Sorting: Bubble Sort - def bubble_sort(self, arr): - n = len(arr) - for i in range(n): - for j in range(0, n - i - 1): - if arr[j] > arr[j + 1]: - arr[j], arr[j + 1] = arr[j + 1], arr[j] - return arr - - # Sorting: Merge Sort - def merge_sort(self, arr): - if len(arr) > 1: - mid = len(arr) // 2 - left_half = arr[:mid] - right_half = arr[mid:] - - self.merge_sort(left_half) - self.merge_sort(right_half) - - i = j = k = 0 - - while i < len(left_half) and j < len(right_half): - if left_half[i] < right_half[j]: - arr[k] = left_half[i] - i += 1 - else: - arr[k] = right_half[j] - j += 1 - k += 1 - - while i < len(left_half): - arr[k] = left_half[i] - i += 1 - k += 1 - - while j < len(right_half): - arr[k] = right_half[j] - j += 1 - k += 1 - return arr - - def insert_linked_list(self, head, value): - new_node = self.ListNode(value) - if not head: - return new_node - current = head - while current.next: - current = current.next - current.next = new_node - return head - - def print_linked_list(self, head): - current = head - while current: - print(current.value, end=" -> ") - current = current.next - print("None") - - def inorder_traversal(self, root): - return ( - self.inorder_traversal(root.left) - + [root.value] - + self.inorder_traversal(root.right) - if root - else [] - ) - - def preorder_traversal(self, root): - return ( - [root.value] - + self.preorder_traversal(root.left) - + self.preorder_traversal(root.right) - if root - else [] - ) - - def postorder_traversal(self, root): - return ( - self.postorder_traversal(root.left) - + self.postorder_traversal(root.right) - + [root.value] - if root - else [] - ) - - # Graph Algorithms: Depth-First Search - def dfs(self, graph, start): - visited, stack = set(), [start] - while stack: - vertex = stack.pop() - if vertex not in visited: - visited.add(vertex) - stack.extend(set(graph[vertex]) - visited) - return visited - - # Graph Algorithms: Breadth-First Search - def bfs(self, graph, start): - visited, queue = set(), [start] - while queue: - vertex = queue.pop(0) - if vertex not in visited: - visited.add(vertex) - queue.extend(set(graph[vertex]) - visited) - return visited - - -def setup_readline(local): - - # Enable tab completion - readline.parse_and_bind("tab: complete") - # Optionally, you can set the completer function manually - readline.set_completer(rlcompleter.Completer(local).complete) - - -def main(): - - console = Console() - interview = Interview() - - parser = argparse.ArgumentParser(description="Programming Interview Questions") - - parser.add_argument( - "-f", "--factorial", type=int, help="Factorial algorithm examples" - ) - parser.add_argument("--fibonacci", type=int, help="Fibonacci algorithm examples") - parser.add_argument( - "--search", action="store_true", help="Search algorithm examples" - ) - parser.add_argument("--sort", action="store_true", help="Search algorithm examples") - parser.add_argument("--stack", action="store_true", help="Stack algorithm examples") - parser.add_argument("--queue", action="store_true", help="Queue algorithm examples") - parser.add_argument( - "--list", action="store_true", help="Linked List algorithm examples" - ) - parser.add_argument( - "--tree", action="store_true", help="Tree traversal algorithm examples" - ) - parser.add_argument("--graph", action="store_true", help="Graph algorithm examples") - parser.add_argument( - "-i", "--interactive", action="store_true", help="Interactive mode" - ) - - args = parser.parse_args() - - if args.factorial: - # Factorial examples - console.rule("Factorial Examples") - rprint( - Panel( - "[bold cyan]Recursive Factorial - Time Complexity: O(n)[/bold cyan]\n" - + str(interview.factorial_recursive(args.factorial)), - title="Factorial Recursive", - ) - ) - rprint( - Panel( - "[bold cyan]Iterative Factorial - Time Complexity: O(n)[/bold cyan]\n" - + str(interview.factorial_iterative(args.factorial)), - title="Factorial Iterative", - ) - ) - rprint( - Panel( - "[bold cyan]Built-in Factorial - Time Complexity: O(n)[/bold cyan]\n" - + str(interview.factorial_builtin(args.factorial)), - title="Factorial Built-in", - ) - ) - rprint( - Panel( - "[bold cyan]Divide and Conquer Factorial - Time Complexity: O(n log n)[/bold cyan]\n" - + str(interview.factorial_divide_and_conquer(args.factorial)), - title="Factorial Divide and Conquer", - ) - ) - exit() - - if args.fibonacci: - # Fibonacci examples - console.rule("Fibonacci Examples") - rprint( - Panel( - str(interview.fibonacci_recursive(args.fibonacci)), - title="Fibonacci Recursive", - ) - ) - rprint( - Panel( - str(interview.fibonacci_iterative(args.fibonacci)), - title="Fibonacci Iterative", - ) - ) - exit() - - if args.search: - # Searching examples - console.rule("Searching Examples") - array = [1, 3, 5, 7, 9] - rprint(Panel(str(interview.linear_search(array, 5)), title="Linear Search")) - rprint(Panel(str(interview.binary_search(array, 5)), title="Binary Search")) - exit() - - if args.sort: - # Sorting examples - console.rule("Sorting Examples") - unsorted_array = [64, 34, 25, 12, 22, 11, 90] - rprint( - Panel( - str(interview.bubble_sort(unsorted_array.copy())), title="Bubble Sort" - ) - ) - rprint( - Panel(str(interview.merge_sort(unsorted_array.copy())), title="Merge Sort") - ) - exit() - - if args.stack: - # Stack example - console.rule("Stack Example") - stack = interview.Stack() - stack.push(1) - stack.push(2) - stack.push(3) - rprint(Panel(str(stack.pop()), title="Stack Pop")) - rprint(Panel(str(stack.peek()), title="Stack Peek")) - rprint(Panel(str(stack.size()), title="Stack Size")) - - if args.queue: - # Queue example - console.rule("Queue Example") - queue = interview.Queue() - queue.enqueue(1) - queue.enqueue(2) - queue.enqueue(3) - rprint(Panel(str(queue.dequeue()), title="Queue Dequeue")) - rprint(Panel(str(queue.is_empty()), title="Queue Is Empty")) - rprint(Panel(str(queue.size()), title="Queue Size")) - - if args.list: - # Linked List example - console.rule("Linked List Example") - head = None - head = interview.insert_linked_list(head, 1) - head = interview.insert_linked_list(head, 2) - head = interview.insert_linked_list(head, 3) - interview.print_linked_list(head) # Output: 1 -> 2 -> 3 -> None - - if args.tree: - # Tree Traversal example - console.rule("Tree Traversal Example") - root = interview.TreeNode(1) - root.left = interview.TreeNode(2) - root.right = interview.TreeNode(3) - root.left.left = interview.TreeNode(4) - root.left.right = interview.TreeNode(5) - rprint(Panel(str(interview.inorder_traversal(root)), title="Inorder Traversal")) - rprint( - Panel(str(interview.preorder_traversal(root)), title="Preorder Traversal") - ) - rprint( - Panel(str(interview.postorder_traversal(root)), title="Postorder Traversal") - ) - - if args.graph: - # Graph Algorithms example - console.rule("Graph Algorithms Example") - graph = { - "A": ["B", "C"], - "B": ["A", "D", "E"], - "C": ["A", "F"], - "D": ["B"], - "E": ["B", "F"], - "F": ["C", "E"], - } - rprint(Panel(str(interview.dfs(graph, "A")), title="DFS")) - rprint(Panel(str(interview.bfs(graph, "A")), title="BFS")) - - if args.interactive: - # Starting interactive session with tab completion - setup_readline(locals()) - banner = "Interactive programming interview session started. Type 'exit()' or 'Ctrl-D' to exit." - code.interact( - banner=banner, - local=locals(), - exitmsg="Great interview!", - ) - - -if __name__ == "__main__": - main() - -endef - -define PYTHON_CI_YAML -name: Build Wheels -endef - -define PYTHON_LICENSE_TXT -MIT License - -Copyright (c) [YEAR] [OWNER NAME] - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -endef - -define PYTHON_PROJECT_TOML -[build-system] -endef - -define SEPARATOR -.==========================================================================================================================================. -| | -| _|_|_| _| _| _| _| _| _|_| _| _| | -| _| _| _| _|_| _|_| _|_| _|_|_| _|_|_|_| _|_| _|_| _|_|_| _| _| _|_| _| _| _|_| | -| _|_|_| _|_| _| _| _| _|_|_|_| _| _| _| _| _| _| _| _|_| _|_|_|_| _|_|_|_| _| _| _|_|_|_| | -| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| | -| _| _| _|_| _| _|_|_| _|_|_| _|_| _| _| _|_|_| _| _| _|_|_| _| _| _| _|_|_| | -| _| | -| _| | -`==========================================================================================================================================' -endef - -define TINYMCE_JS -import tinymce from 'tinymce'; -import 'tinymce/icons/default'; -import 'tinymce/themes/silver'; -import 'tinymce/skins/ui/oxide/skin.css'; -import 'tinymce/plugins/advlist'; -import 'tinymce/plugins/code'; -import 'tinymce/plugins/emoticons'; -import 'tinymce/plugins/emoticons/js/emojis'; -import 'tinymce/plugins/link'; -import 'tinymce/plugins/lists'; -import 'tinymce/plugins/table'; -import 'tinymce/models/dom'; - -tinymce.init({ - selector: 'textarea#editor', - plugins: 'advlist code emoticons link lists table', - toolbar: 'bold italic | bullist numlist | link emoticons', - skin: false, - content_css: false, -}); -endef - -define WAGTAIL_BASE_TEMPLATE -{% load static wagtailcore_tags wagtailuserbar webpack_loader %} - - - - - - {% block title %} - {% if page.seo_title %} - {{ page.seo_title }} - {% else %} - {{ page.title }} - {% endif %} - {% endblock %} - {% block title_suffix %} - {% wagtail_site as current_site %} - {% if current_site and current_site.site_name %}- {{ current_site.site_name }}{% endif %} - {% endblock %} - - {% if page.search_description %}{% endif %} - - {# Force all links in the live preview panel to be opened in a new tab #} - {% if request.in_preview_panel %}{% endif %} - {% stylesheet_pack 'app' %} - {% block extra_css %}{# Override this in templates to add extra stylesheets #}{% endblock %} - - {% include 'favicon.html' %} - {% csrf_token %} - - -
    - {% wagtailuserbar %} -
    - {% include 'header.html' %} - {% if messages %} -
    - {% for message in messages %} - - {% endfor %} -
    - {% endif %} -
    - {% block content %}{% endblock %} -
    -
    - {% include 'footer.html' %} - {% include 'offcanvas.html' %} - {% javascript_pack 'app' %} - {% block extra_js %}{# Override this in templates to add extra javascript #}{% endblock %} - - -endef - -define WAGTAIL_BLOCK_CAROUSEL - -endef - -define WAGTAIL_BLOCK_MARKETING -{% load wagtailcore_tags %} -
    - {% if block.value.images.0 %} - {% include 'blocks/carousel_block.html' %} - {% else %} - {{ self.title }} - {{ self.content }} - {% endif %} -
    -endef - -define WAGTAIL_CONTACT_PAGE_LANDING -{% extends 'base.html' %} -{% block content %}

    Thank you!

    {% endblock %} -endef - -define WAGTAIL_CONTACT_PAGE_MODEL -from django.db import models -from modelcluster.fields import ParentalKey -from wagtail.admin.panels import ( - FieldPanel, FieldRowPanel, - InlinePanel, MultiFieldPanel -) -from wagtail.fields import RichTextField -from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField - - -class FormField(AbstractFormField): - page = ParentalKey('ContactPage', on_delete=models.CASCADE, related_name='form_fields') - - -class ContactPage(AbstractEmailForm): - intro = RichTextField(blank=True) - thank_you_text = RichTextField(blank=True) - - content_panels = AbstractEmailForm.content_panels + [ - FieldPanel('intro'), - InlinePanel('form_fields', label="Form fields"), - FieldPanel('thank_you_text'), - MultiFieldPanel([ - FieldRowPanel([ - FieldPanel('from_address', classname="col6"), - FieldPanel('to_address', classname="col6"), - ]), - FieldPanel('subject'), - ], "Email"), - ] - - class Meta: - verbose_name = "Contact Page" -endef - -define WAGTAIL_CONTACT_PAGE_TEMPLATE -{% extends 'base.html' %} -{% load crispy_forms_tags static wagtailcore_tags %} -{% block content %} -

    {{ page.title }}

    - {{ page.intro|richtext }} -
    - {% csrf_token %} - {{ form.as_p }} - -
    -{% endblock %} -endef - -define WAGTAIL_CONTACT_PAGE_TEST -from django.test import TestCase -from wagtail.test.utils import WagtailPageTestCase -from wagtail.models import Page - -from contactpage.models import ContactPage, FormField - -class ContactPageTest(TestCase, WagtailPageTestCase): - def test_contact_page_creation(self): - # Create a ContactPage instance - contact_page = ContactPage( - title='Contact', - intro='Welcome to our contact page!', - thank_you_text='Thank you for reaching out.' - ) - - # Save the ContactPage instance - self.assertEqual(contact_page.save_revision().publish().get_latest_revision_as_page(), contact_page) - - def test_form_field_creation(self): - # Create a ContactPage instance - contact_page = ContactPage( - title='Contact', - intro='Welcome to our contact page!', - thank_you_text='Thank you for reaching out.' - ) - # Save the ContactPage instance - contact_page_revision = contact_page.save_revision() - contact_page_revision.publish() - - # Create a FormField associated with the ContactPage - form_field = FormField( - page=contact_page, - label='Your Name', - field_type='singleline', - required=True - ) - form_field.save() - - # Retrieve the ContactPage from the database - contact_page_from_db = Page.objects.get(id=contact_page.id).specific - - # Check if the FormField is associated with the ContactPage - self.assertEqual(contact_page_from_db.form_fields.first(), form_field) - - def test_contact_page_form_submission(self): - # Create a ContactPage instance - contact_page = ContactPage( - title='Contact', - intro='Welcome to our contact page!', - thank_you_text='Thank you for reaching out.' - ) - # Save the ContactPage instance - contact_page_revision = contact_page.save_revision() - contact_page_revision.publish() - - # Simulate a form submission - form_data = { - 'your_name': 'John Doe', - # Add other form fields as needed - } - - response = self.client.post(contact_page.url, form_data) - - # Check if the form submission is successful (assuming a 302 redirect) - self.assertEqual(response.status_code, 302) - - # You may add more assertions based on your specific requirements -endef - -define WAGTAIL_HEADER_PREFIX -{% load wagtailcore_tags %} -{% wagtail_site as current_site %} -endef - -define WAGTAIL_HOME_PAGE_MODEL -from wagtail.models import Page -from wagtail.fields import StreamField -from wagtail import blocks -from wagtail.admin.panels import FieldPanel -from wagtail.images.blocks import ImageChooserBlock - - -class MarketingBlock(blocks.StructBlock): - title = blocks.CharBlock(required=False, help_text="Enter the block title") - content = blocks.RichTextBlock(required=False, help_text="Enter the block content") - images = blocks.ListBlock( - ImageChooserBlock(required=False), - help_text="Select one or two images for column display. Select three or more images for carousel display.", - ) - image = ImageChooserBlock( - required=False, help_text="Select one image for background display." - ) - block_class = blocks.CharBlock( - required=False, - help_text="Enter a CSS class for styling the marketing block", - classname="full title", - default="vh-100 bg-secondary", - ) - image_class = blocks.CharBlock( - required=False, - help_text="Enter a CSS class for styling the column display image(s)", - classname="full title", - default="img-thumbnail p-5", - ) - layout_class = blocks.CharBlock( - required=False, - help_text="Enter a CSS class for styling the layout.", - classname="full title", - default="d-flex flex-row", - ) - - class Meta: - icon = "placeholder" - template = "blocks/marketing_block.html" - - -class HomePage(Page): - template = "home/home_page.html" # Create a template for rendering the home page - - marketing_blocks = StreamField( - [ - ("marketing_block", MarketingBlock()), - ], - blank=True, - null=True, - use_json_field=True, - ) - content_panels = Page.content_panels + [ - FieldPanel("marketing_blocks"), - ] - - class Meta: - verbose_name = "Home Page" -endef - -define WAGTAIL_HOME_PAGE_TEMPLATE -{% extends "base.html" %} -{% load wagtailcore_tags %} -{% block content %} -
    - {% for block in page.marketing_blocks %} - {% include_block block %} - {% endfor %} -
    -{% endblock %} -endef - -define WAGTAIL_PRIVACY_PAGE_MODEL -from wagtail.models import Page -from wagtail.admin.panels import FieldPanel -from wagtailmarkdown.fields import MarkdownField - - -class PrivacyPage(Page): - """ - A Wagtail Page model for the Privacy Policy page. - """ - - template = "privacy_page.html" - - body = MarkdownField() - - content_panels = Page.content_panels + [ - FieldPanel("body", classname="full"), - ] - - class Meta: - verbose_name = "Privacy Page" -endef - -define WAGTAIL_PRIVACY_PAGE_TEMPLATE -{% extends 'base.html' %} -{% load wagtailmarkdown %} -{% block content %}
    {{ page.body|markdown }}
    {% endblock %} -endef - -define WAGTAIL_SEARCH_TEMPLATE -{% extends "base.html" %} -{% load static wagtailcore_tags %} -{% block body_class %}template-searchresults{% endblock %} -{% block title %}Search{% endblock %} -{% block content %} -

    Search

    -
    - - -
    - {% if search_results %} -
      - {% for result in search_results %} -
    • -

      - {{ result }} -

      - {% if result.search_description %}{{ result.search_description }}{% endif %} -
    • - {% endfor %} -
    - {% if search_results.has_previous %} - Previous - {% endif %} - {% if search_results.has_next %} - Next - {% endif %} - {% elif search_query %} - No results found - {% else %} - No results found. Try a test query? - {% endif %} -{% endblock %} -endef - -define WAGTAIL_SEARCH_URLS -from django.urls import path -from .views import search - -urlpatterns = [path("", search, name="search")] -endef - -define WAGTAIL_SETTINGS -INSTALLED_APPS.append("wagtail_color_panel") -INSTALLED_APPS.append("wagtail_modeladmin") -INSTALLED_APPS.append("wagtail.contrib.settings") -INSTALLED_APPS.append("wagtailmarkdown") -INSTALLED_APPS.append("wagtailmenus") -INSTALLED_APPS.append("wagtailseo") -TEMPLATES[0]["OPTIONS"]["context_processors"].append( - "wagtail.contrib.settings.context_processors.settings" -) -TEMPLATES[0]["OPTIONS"]["context_processors"].append( - "wagtailmenus.context_processors.wagtailmenus" -) -endef - -define WAGTAIL_SITEPAGE_MODEL -from wagtail.models import Page - - -class SitePage(Page): - template = "sitepage/site_page.html" - - class Meta: - verbose_name = "Site Page" -endef - -define WAGTAIL_SITEPAGE_TEMPLATE -{% extends 'base.html' %} -{% block content %} -

    {{ page.title }}

    -{% endblock %} -endef - -define WAGTAIL_URLS -from django.conf import settings -from django.urls import include, path -from django.contrib import admin - -from wagtail.admin import urls as wagtailadmin_urls -from wagtail.documents import urls as wagtaildocs_urls - -from search import views as search_views - -urlpatterns = [ - path("django/", admin.site.urls), - path("wagtail/", include(wagtailadmin_urls)), - path("documents/", include(wagtaildocs_urls)), - path("search/", search_views.search, name="search"), -] - -if settings.DEBUG: - from django.conf.urls.static import static - from django.contrib.staticfiles.urls import staticfiles_urlpatterns - - # Serve static and media files from development server - urlpatterns += staticfiles_urlpatterns() - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -endef - -define WAGTAIL_URLS_HOME -urlpatterns += [ - # For anything not caught by a more specific rule above, hand over to - # Wagtail's page serving mechanism. This should be the last pattern in - # the list: - path("", include("wagtail.urls")), - # Alternatively, if you want Wagtail pages to be served from a subpath - # of your site, rather than the site root: - # path("pages/", include("wagtail.urls"), -] -endef - -define WEBPACK_CONFIG_JS -const path = require('path'); - -module.exports = { - mode: 'development', - entry: './src/index.js', - output: { - filename: 'bundle.js', - path: path.resolve(__dirname, 'dist'), - }, -}; -endef - -define WEBPACK_INDEX_HTML - - - - - - Hello, Webpack! - - - - - -endef - -define WEBPACK_INDEX_JS -const message = "Hello, World!"; -console.log(message); -endef - -define WEBPACK_REVEAL_CONFIG_JS -const path = require('path'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); - -module.exports = { - mode: 'development', - entry: './src/index.js', - output: { - filename: 'bundle.js', - path: path.resolve(__dirname, 'dist'), - }, - module: { - rules: [ - { - test: /\.css$$/, - use: [MiniCssExtractPlugin.loader, 'css-loader'], - }, - ], - }, - plugins: [ - new MiniCssExtractPlugin({ - filename: 'bundle.css', - }), - ], -}; -endef - -define WEBPACK_REVEAL_INDEX_HTML - - - - - - Project Makefile - - -
    -
    -
    - Slide 1: Draw some circles -
    -
    - Slide 2: Draw the rest of the owl -
    -
    -
    - - -endef - -define WEBPACK_REVEAL_INDEX_JS -import 'reveal.js/dist/reveal.css'; -import 'reveal.js/dist/theme/black.css'; -import Reveal from 'reveal.js'; -import RevealNotes from 'reveal.js/plugin/notes/notes.js'; -Reveal.initialize({ slideNumber: true, plugins: [ RevealNotes ]}); -endef - -# ------------------------------------------------------------------------------ -# Export variables used by phony target rules -# ------------------------------------------------------------------------------ - -export DJANGO_ALLAUTH_BASE_TEMPLATE -export DJANGO_API_SERIALIZERS -export DJANGO_API_VIEWS -export DJANGO_APP_TESTS -export DJANGO_BACKEND_APPS -export DJANGO_BASE_TEMPLATE -export DJANGO_CUSTOM_ADMIN -export DJANGO_DOCKERCOMPOSE -export DJANGO_DOCKERFILE -export DJANGO_FAVICON_TEMPLATE -export DJANGO_FOOTER_TEMPLATE -export DJANGO_FRONTEND_APP -export DJANGO_FRONTEND_APP_CONFIG -export DJANGO_FRONTEND_BABELRC -export DJANGO_FRONTEND_COMPONENTS -export DJANGO_FRONTEND_COMPONENT_CLOCK -export DJANGO_FRONTEND_COMPONENT_ERROR -export DJANGO_FRONTEND_COMPONENT_USER_MENU -export DJANGO_FRONTEND_CONTEXT_INDEX -export DJANGO_FRONTEND_CONTEXT_USER_PROVIDER -export DJANGO_FRONTEND_ESLINTRC -export DJANGO_FRONTEND_OFFCANVAS_TEMPLATE -export DJANGO_FRONTEND_PORTAL -export DJANGO_FRONTEND_STYLES -export DJANGO_FRONTEND_THEME_BLUE -export DJANGO_FRONTEND_THEME_TOGGLER -export DJANGO_HEADER_TEMPLATE -export DJANGO_HOME_PAGE_ADMIN -export DJANGO_HOME_PAGE_MODELS -export DJANGO_HOME_PAGE_TEMPLATE -export DJANGO_HOME_PAGE_URLS -export DJANGO_HOME_PAGE_VIEWS -export DJANGO_LOGGING_DEMO_ADMIN -export DJANGO_LOGGING_DEMO_MODELS -export DJANGO_LOGGING_DEMO_SETTINGS -export DJANGO_LOGGING_DEMO_URLS -export DJANGO_LOGGING_DEMO_VIEWS -export DJANGO_MANAGE_PY -export DJANGO_MODEL_FORM_DEMO_ADMIN -export DJANGO_MODEL_FORM_DEMO_FORMS -export DJANGO_MODEL_FORM_DEMO_MODEL -export DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL -export DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM -export DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST -export DJANGO_MODEL_FORM_DEMO_URLS -export DJANGO_MODEL_FORM_DEMO_VIEWS -export DJANGO_PAYMENTS_ADMIN -export DJANGO_PAYMENTS_FORM -export DJANGO_PAYMENTS_MIGRATION_0002 -export DJANGO_PAYMENTS_MIGRATION_0003 -export DJANGO_PAYMENTS_MODELS -export DJANGO_PAYMENTS_TEMPLATE_CANCEL -export DJANGO_PAYMENTS_TEMPLATE_CHECKOUT -export DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL -export DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST -export DJANGO_PAYMENTS_TEMPLATE_SUCCESS -export DJANGO_PAYMENTS_URLS -export DJANGO_PAYMENTS_VIEW -export DJANGO_SEARCH_FORMS -export DJANGO_SEARCH_SETTINGS -export DJANGO_SEARCH_TEMPLATE -export DJANGO_SEARCH_URLS -export DJANGO_SEARCH_UTILS -export DJANGO_SEARCH_VIEWS -export DJANGO_SETTINGS_AUTHENTICATION_BACKENDS -export DJANGO_SETTINGS_BASE -export DJANGO_SETTINGS_BASE_MINIMAL -export DJANGO_SETTINGS_CRISPY_FORMS -export DJANGO_SETTINGS_DATABASE -export DJANGO_SETTINGS_DEV -export DJANGO_SETTINGS_HOME_PAGE -export DJANGO_SETTINGS_INSTALLED_APPS -export DJANGO_SETTINGS_MIDDLEWARE -export DJANGO_SETTINGS_MODEL_FORM_DEMO -export DJANGO_SETTINGS_PAYMENTS -export DJANGO_SETTINGS_PROD -export DJANGO_SETTINGS_REST_FRAMEWORK -export DJANGO_SETTINGS_SITEUSER -export DJANGO_SETTINGS_THEMES -export DJANGO_SITEUSER_ADMIN -export DJANGO_SITEUSER_EDIT_TEMPLATE -export DJANGO_SITEUSER_FORM -export DJANGO_SITEUSER_MODEL -export DJANGO_SITEUSER_URLS -export DJANGO_SITEUSER_VIEW -export DJANGO_SITEUSER_VIEW_TEMPLATE -export DJANGO_URLS -export DJANGO_URLS_ALLAUTH -export DJANGO_URLS_API -export DJANGO_URLS_DEBUG_TOOLBAR -export DJANGO_URLS_HOME_PAGE -export DJANGO_URLS_LOGGING_DEMO -export DJANGO_URLS_MODEL_FORM_DEMO -export DJANGO_URLS_SITEUSER -export DJANGO_UTILS -export EB_CUSTOM_ENV_EC2_USER -export EB_CUSTOM_ENV_VAR_FILE -export GIT_IGNORE -export JENKINS_FILE -export MAKEFILE_CUSTOM -export PIP_INSTALL_REQUIREMENTS_TEST -export PROGRAMMING_INTERVIEW -export PYTHON_CI_YAML -export PYTHON_LICENSE_TXT -export PYTHON_PROJECT_TOML -export SEPARATOR -export TINYMCE_JS -export WAGTAIL_BASE_TEMPLATE -export WAGTAIL_BLOCK_CAROUSEL -export WAGTAIL_BLOCK_MARKETING -export WAGTAIL_CONTACT_PAGE_LANDING -export WAGTAIL_CONTACT_PAGE_MODEL -export WAGTAIL_CONTACT_PAGE_TEMPLATE -export WAGTAIL_CONTACT_PAGE_TEST -export WAGTAIL_HOME_PAGE_MODEL -export WAGTAIL_HOME_PAGE_TEMPLATE -export WAGTAIL_HOME_PAGE_URLS -export WAGTAIL_HOME_PAGE_VIEWS -export WAGTAIL_PRIVACY_PAGE_MODEL -export WAGTAIL_PRIVACY_PAGE_MODEL -export WAGTAIL_PRIVACY_PAGE_TEMPLATE -export WAGTAIL_SEARCH_TEMPLATE -export WAGTAIL_SEARCH_URLS -export WAGTAIL_SETTINGS -export WAGTAIL_SITEPAGE_MODEL -export WAGTAIL_SITEPAGE_TEMPLATE -export WAGTAIL_URLS -export WAGTAIL_URLS_HOME -export WEBPACK_CONFIG_JS -export WEBPACK_INDEX_HTML -export WEBPACK_INDEX_JS -export WEBPACK_REVEAL_CONFIG_JS -export WEBPACK_REVEAL_INDEX_HTML -export WEBPACK_REVEAL_INDEX_JS - -# ------------------------------------------------------------------------------ -# Multi-line phony target rules -# ------------------------------------------------------------------------------ - -.PHONY: aws-check-env-profile-default -aws-check-env-profile-default: -ifndef AWS_PROFILE - $(error AWS_PROFILE is undefined) -endif - -.PHONY: aws-check-env-region-default -aws-check-env-region-default: -ifndef AWS_REGION - $(error AWS_REGION is undefined) -endif - -.PHONY: aws-secret-default -aws-secret-default: aws-check-env - @SECRET_KEY=$$(openssl rand -base64 48); aws ssm put-parameter --name "SECRET_KEY" --value "$$SECRET_KEY" --type String - -.PHONY: aws-sg-default -aws-sg-default: aws-check-env - aws ec2 describe-security-groups $(AWS_OPTS) - -.PHONY: aws-ssm-default -aws-ssm-default: aws-check-env - aws ssm describe-parameters $(AWS_OPTS) - @echo "Get parameter values with: aws ssm getparameter --name ." - -.PHONY: aws-subnet-default -aws-subnet-default: aws-check-env - aws ec2 describe-subnets $(AWS_OPTS) - -.PHONY: aws-vol-available-default -aws-vol-available-default: aws-check-env - aws ec2 describe-volumes --filters Name=status,Values=available --query "Volumes[*].{ID:VolumeId,Size:Size}" --output table - -.PHONY: aws-vol-default -aws-vol-default: aws-check-env - aws ec2 describe-volumes --output table - -.PHONY: aws-vpc-default -aws-vpc-default: aws-check-env - aws ec2 describe-vpcs $(AWS_OPTS) - -.PHONY: db-import-default -db-import-default: - @psql $(DJANGO_DB_NAME) < $(DJANGO_DB_NAME).sql - -.PHONY: db-init-default -db-init-default: - -dropdb $(PROJECT_NAME) - -createdb $(PROJECT_NAME) - -.PHONY: db-init-mysql-default -db-init-mysql-default: - -mysqladmin -u root drop $(PROJECT_NAME) - -mysqladmin -u root create $(PROJECT_NAME) - -.PHONY: db-init-test-default -db-init-test-default: - -dropdb test_$(PROJECT_NAME) - -createdb test_$(PROJECT_NAME) - -.PHONY: django-allauth-default -django-allauth-default: - $(ADD_DIR) backend/templates/allauth/layouts - @echo "$$DJANGO_ALLAUTH_BASE_TEMPLATE" > backend/templates/allauth/layouts/base.html - @echo "$$DJANGO_URLS_ALLAUTH" >> $(DJANGO_URLS_FILE) - -$(GIT_ADD) backend/templates/allauth/layouts/base.html - -.PHONY: django-app-tests-default -django-app-tests-default: - @echo "$$DJANGO_APP_TESTS" > $(APP_DIR)/tests.py - -.PHONY: django-base-template-default -django-base-template-default: - @$(ADD_DIR) backend/templates - @echo "$$DJANGO_BASE_TEMPLATE" > backend/templates/base.html - -$(GIT_ADD) backend/templates/base.html - -.PHONY: django-custom-admin-default -django-custom-admin-default: - @echo "$$DJANGO_CUSTOM_ADMIN" > $(DJANGO_CUSTOM_ADMIN_FILE) - @echo "$$DJANGO_BACKEND_APPS" > $(DJANGO_BACKEND_APPS_FILE) - -$(GIT_ADD) backend/*.py - -.PHONY: django-db-shell-default -django-db-shell-default: - python manage.py dbshell - -.PHONY: django-dockerfile-default -django-dockerfile-default: - @echo "$$DJANGO_DOCKERFILE" > Dockerfile - -$(GIT_ADD) Dockerfile - @echo "$$DJANGO_DOCKERCOMPOSE" > docker-compose.yml - -$(GIT_ADD) docker-compose.yml - -.PHONY: django-favicon-default -django-favicon-default: - @echo "$$DJANGO_FAVICON_TEMPLATE" > backend/templates/favicon.html - -$(GIT_ADD) backend/templates/favicon.html - -.PHONY: django-footer-template-default -django-footer-template-default: - @echo "$$DJANGO_FOOTER_TEMPLATE" > backend/templates/footer.html - -$(GIT_ADD) backend/templates/footer.html - -.PHONY: django-frontend-default -django-frontend-default: python-webpack-init - $(ADD_DIR) frontend/src/context - $(ADD_DIR) frontend/src/images - $(ADD_DIR) frontend/src/utils - @echo "$$DJANGO_FRONTEND_APP" > frontend/src/application/app.js - @echo "$$DJANGO_FRONTEND_APP_CONFIG" > frontend/src/application/config.js - @echo "$$DJANGO_FRONTEND_BABELRC" > frontend/.babelrc - @echo "$$DJANGO_FRONTEND_COMPONENT_CLOCK" > frontend/src/components/Clock.js - @echo "$$DJANGO_FRONTEND_COMPONENT_ERROR" > frontend/src/components/ErrorBoundary.js - @echo "$$DJANGO_FRONTEND_CONTEXT_INDEX" > frontend/src/context/index.js - @echo "$$DJANGO_FRONTEND_CONTEXT_USER_PROVIDER" > frontend/src/context/UserContextProvider.js - @echo "$$DJANGO_FRONTEND_COMPONENT_USER_MENU" > frontend/src/components/UserMenu.js - @echo "$$DJANGO_FRONTEND_COMPONENTS" > frontend/src/components/index.js - @echo "$$DJANGO_FRONTEND_ESLINTRC" > frontend/.eslintrc - @echo "$$DJANGO_FRONTEND_PORTAL" > frontend/src/dataComponents.js - @echo "$$DJANGO_FRONTEND_STYLES" > frontend/src/styles/index.scss - @echo "$$DJANGO_FRONTEND_THEME_BLUE" > frontend/src/styles/theme-blue.scss - @echo "$$DJANGO_FRONTEND_THEME_TOGGLER" > frontend/src/utils/themeToggler.js - # @echo "$$TINYMCE_JS" > frontend/src/utils/tinymce.js - @$(MAKE) npm-install-django - @$(MAKE) npm-install-django-dev - -$(GIT_ADD) $(DJANGO_FRONTEND_FILES) - -.PHONY: django-graph-default -django-graph-default: - python manage.py graph_models -a -o $(PROJECT_NAME).png - -.PHONY: django-header-template-default -django-header-template-default: - @echo "$$DJANGO_HEADER_TEMPLATE" > backend/templates/header.html - -$(GIT_ADD) backend/templates/header.html - -.PHONY: django-home-default -django-home-default: - python manage.py startapp home - $(ADD_DIR) home/templates - @echo "$$DJANGO_HOME_PAGE_ADMIN" > home/admin.py - @echo "$$DJANGO_HOME_PAGE_MODELS" > home/models.py - @echo "$$DJANGO_HOME_PAGE_TEMPLATE" > home/templates/home.html - @echo "$$DJANGO_HOME_PAGE_VIEWS" > home/views.py - @echo "$$DJANGO_HOME_PAGE_URLS" > home/urls.py - @echo "$$DJANGO_URLS_HOME_PAGE" >> $(DJANGO_URLS_FILE) - @echo "$$DJANGO_SETTINGS_HOME_PAGE" >> $(DJANGO_SETTINGS_BASE_FILE) - export APP_DIR="home"; $(MAKE) django-app-tests - -$(GIT_ADD) home/templates - -$(GIT_ADD) home/*.py - -$(GIT_ADD) home/migrations/*.py - -.PHONY: django-init-default -django-init-default: separator \ - db-init \ - django-install \ - django-project \ - django-utils \ - pip-freeze \ - pip-init-test \ - django-settings-directory \ - django-custom-admin \ - django-dockerfile \ - django-offcanvas-template \ - django-header-template \ - django-footer-template \ - django-base-template \ - django-manage-py \ - django-urls \ - django-urls-debug-toolbar \ - django-allauth \ - django-favicon \ - git-ignore \ - django-settings-base \ - django-settings-dev \ - django-settings-prod \ - django-siteuser \ - django-home \ - django-rest-serializers \ - django-rest-views \ - django-urls-api \ - django-frontend \ - django-migrate \ - django-su - -.PHONY: django-init-minimal-default -django-init-minimal-default: separator \ - db-init \ - django-install-minimal \ - django-project \ - django-settings-directory \ - django-settings-base-minimal \ - django-settings-dev \ - pip-freeze \ - pip-init-test \ - django-custom-admin \ - django-dockerfile \ - django-offcanvas-template \ - django-header-template \ - django-footer-template \ - django-base-template \ - django-manage-py \ - django-urls \ - django-urls-debug-toolbar \ - django-favicon \ - django-settings-prod \ - django-home \ - django-utils \ - django-frontend \ - django-migrate \ - git-ignore \ - django-su - -.PHONY: django-init-wagtail-default -django-init-wagtail-default: separator \ - db-init \ - django-install \ - wagtail-install \ - wagtail-project \ - django-utils \ - pip-freeze \ - pip-init-test \ - django-custom-admin \ - django-dockerfile \ - django-offcanvas-template \ - wagtail-header-prefix-template \ - django-header-template \ - wagtail-base-template \ - django-footer-template \ - django-manage-py \ - wagtail-home \ - wagtail-urls \ - django-urls-debug-toolbar \ - django-allauth \ - django-favicon \ - git-ignore \ - wagtail-search \ - django-settings-base \ - django-settings-dev \ - django-settings-prod \ - wagtail-settings \ - django-siteuser \ - django-model-form-demo \ - django-logging-demo \ - django-payments-demo-default \ - django-rest-serializers \ - django-rest-views \ - django-urls-api \ - wagtail-urls-home \ - django-frontend \ - django-migrate \ - django-su - -.PHONY: django-install-default -django-install-default: - $(PIP_ENSURE) - python -m pip install \ - Django \ - Faker \ - boto3 \ - crispy-bootstrap5 \ - djangorestframework \ - django-allauth \ - django-after-response \ - django-ckeditor \ - django-colorful \ - django-cors-headers \ - django-countries \ - django-crispy-forms \ - django-debug-toolbar \ - django-extensions \ - django-hijack \ - django-honeypot \ - django-imagekit \ - django-import-export \ - django-ipware \ - django-multiselectfield \ - django-ninja \ - django-phonenumber-field \ - django-recurrence \ - django-recaptcha \ - django-registration \ - django-richtextfield \ - django-sendgrid-v5 \ - django-social-share \ - django-sql-explorer \ - django-storages \ - django-tables2 \ - django-timezone-field \ - django-widget-tweaks \ - dj-database-url \ - dj-rest-auth \ - dj-stripe \ - docutils \ - enmerkar \ - gunicorn \ - html2docx \ - icalendar \ - mailchimp-marketing \ - mailchimp-transactional \ - phonenumbers \ - pipdeptree \ - psycopg2-binary \ - pydotplus \ - python-webpack-boilerplate \ - python-docx \ - reportlab \ - texttable - -.PHONY: django-install-minimal-default -django-install-minimal-default: - $(PIP_ENSURE) - python -m pip install \ - Django \ - dj-database-url \ - django-debug-toolbar \ - python-webpack-boilerplate - -.PHONY: django-lint-default -django-lint-default: - -ruff format -v - -djlint --reformat --format-css --format-js . - -ruff check -v --fix - -.PHONY: django-loaddata-default -django-loaddata-default: - python manage.py loaddata - -.PHONY: django-logging-demo-default -django-logging-demo-default: - python manage.py startapp logging_demo - @echo "$$DJANGO_LOGGING_DEMO_ADMIN" > logging_demo/admin.py - @echo "$$DJANGO_LOGGING_DEMO_MODELS" > logging_demo/models.py - @echo "$$DJANGO_LOGGING_DEMO_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_LOGGING_DEMO_URLS" > logging_demo/urls.py - @echo "$$DJANGO_LOGGING_DEMO_VIEWS" > logging_demo/views.py - @echo "$$DJANGO_URLS_LOGGING_DEMO" >> $(DJANGO_URLS_FILE) - export APP_DIR="logging_demo"; $(MAKE) django-app-tests - -$(GIT_ADD) logging_demo/*.py - -$(GIT_ADD) logging_demo/migrations/*.py - -.PHONY: django-manage-py-default -django-manage-py-default: - @echo "$$DJANGO_MANAGE_PY" > manage.py - -$(GIT_ADD) manage.py - -.PHONY: django-migrate-default -django-migrate-default: - python manage.py migrate - -.PHONY: django-migrations-make-default -django-migrations-make-default: - python manage.py makemigrations - -.PHONY: django-migrations-show-default -django-migrations-show-default: - python manage.py showmigrations - -.PHONY: django-model-form-demo-default -django-model-form-demo-default: - python manage.py startapp model_form_demo - @echo "$$DJANGO_MODEL_FORM_DEMO_ADMIN" > model_form_demo/admin.py - @echo "$$DJANGO_MODEL_FORM_DEMO_FORMS" > model_form_demo/forms.py - @echo "$$DJANGO_MODEL_FORM_DEMO_MODEL" > model_form_demo/models.py - @echo "$$DJANGO_MODEL_FORM_DEMO_URLS" > model_form_demo/urls.py - @echo "$$DJANGO_MODEL_FORM_DEMO_VIEWS" > model_form_demo/views.py - $(ADD_DIR) model_form_demo/templates - @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL" > model_form_demo/templates/model_form_demo_detail.html - @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM" > model_form_demo/templates/model_form_demo_form.html - @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST" > model_form_demo/templates/model_form_demo_list.html - @echo "$$DJANGO_SETTINGS_MODEL_FORM_DEMO" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_URLS_MODEL_FORM_DEMO" >> $(DJANGO_URLS_FILE) - export APP_DIR="model_form_demo"; $(MAKE) django-app-tests - python manage.py makemigrations - -$(GIT_ADD) model_form_demo/*.py - -$(GIT_ADD) model_form_demo/templates - -$(GIT_ADD) model_form_demo/migrations - -.PHONY: django-offcanvas-template-default -django-offcanvas-template-default: - -$(ADD_DIR) backend/templates - @echo "$$DJANGO_FRONTEND_OFFCANVAS_TEMPLATE" > backend/templates/offcanvas.html - -$(GIT_ADD) backend/templates/offcanvas.html - -.PHONY: django-open-default -django-open-default: -ifeq ($(UNAME), Linux) - @echo "Opening on Linux." - xdg-open http://0.0.0.0:8000 -else ifeq ($(UNAME), Darwin) - @echo "Opening on macOS (Darwin)." - open http://0.0.0.0:8000 -else - @echo "Unable to open on: $(UNAME)" -endif - -.PHONY: django-payments-demo-default -django-payments-demo-default: - python manage.py startapp payments - @echo "$$DJANGO_PAYMENTS_FORM" > payments/forms.py - @echo "$$DJANGO_PAYMENTS_MODELS" > payments/models.py - @echo "$$DJANGO_PAYMENTS_ADMIN" > payments/admin.py - @echo "$$DJANGO_PAYMENTS_VIEW" > payments/views.py - @echo "$$DJANGO_PAYMENTS_URLS" > payments/urls.py - $(ADD_DIR) payments/templates/payments - $(ADD_DIR) payments/management/commands - @echo "$$DJANGO_PAYMENTS_TEMPLATE_CANCEL" > payments/templates/payments/cancel.html - @echo "$$DJANGO_PAYMENTS_TEMPLATE_CHECKOUT" > payments/templates/payments/checkout.html - @echo "$$DJANGO_PAYMENTS_TEMPLATE_SUCCESS" > payments/templates/payments/success.html - @echo "$$DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST" > payments/templates/payments/product_list.html - @echo "$$DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL" > payments/templates/payments/product_detail.html - @echo "$$DJANGO_SETTINGS_PAYMENTS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_URLS_PAYMENTS" >> $(DJANGO_URLS_FILE) - export APP_DIR="payments"; $(MAKE) django-app-tests - python manage.py makemigrations payments - @echo "$$DJANGO_PAYMENTS_MIGRATION_0002" > payments/migrations/0002_set_stripe_api_keys.py - @echo "$$DJANGO_PAYMENTS_MIGRATION_0003" > payments/migrations/0003_create_initial_products.py - -$(GIT_ADD) payments/ - -.PHONY: django-project-default -django-project-default: - django-admin startproject backend . - -$(GIT_ADD) backend - -.PHONY: django-rest-serializers-default -django-rest-serializers-default: - @echo "$$DJANGO_API_SERIALIZERS" > backend/serializers.py - -$(GIT_ADD) backend/serializers.py - -.PHONY: django-rest-views-default -django-rest-views-default: - @echo "$$DJANGO_API_VIEWS" > backend/api.py - -$(GIT_ADD) backend/api.py - -.PHONY: django-search-default -django-search-default: - python manage.py startapp search - $(ADD_DIR) search/templates - @echo "$$DJANGO_SEARCH_TEMPLATE" > search/templates/search.html - @echo "$$DJANGO_SEARCH_FORMS" > search/forms.py - @echo "$$DJANGO_SEARCH_URLS" > search/urls.py - @echo "$$DJANGO_SEARCH_UTILS" > search/utils.py - @echo "$$DJANGO_SEARCH_VIEWS" > search/views.py - @echo "$$DJANGO_SEARCH_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "INSTALLED_APPS.append('search')" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "urlpatterns += [path('search/', include('search.urls'))]" >> $(DJANGO_URLS_FILE) - -$(GIT_ADD) search/templates - -$(GIT_ADD) search/*.py - -.PHONY: django-secret-key-default -django-secret-key-default: - @python -c "from secrets import token_urlsafe; print(token_urlsafe(50))" - -.PHONY: django-serve-default -django-serve-default: - npm run watch & - python manage.py runserver 0.0.0.0:8000 - -.PHONY: django-settings-base-default -django-settings-base-default: - @echo "$$DJANGO_SETTINGS_BASE" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_AUTHENTICATION_BACKENDS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_REST_FRAMEWORK" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_THEMES" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_DATABASE" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_INSTALLED_APPS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_MIDDLEWARE" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_CRISPY_FORMS" >> $(DJANGO_SETTINGS_BASE_FILE) - -.PHONY: django-settings-base-minimal-default -django-settings-base-minimal-default: - @echo "$$DJANGO_SETTINGS_BASE_MINIMAL" >> $(DJANGO_SETTINGS_BASE_FILE) - -.PHONY: django-settings-dev-default -django-settings-dev-default: - @echo "# $(PROJECT_NAME)" > $(DJANGO_SETTINGS_DEV_FILE) - @echo "$$DJANGO_SETTINGS_DEV" >> backend/settings/dev.py - -$(GIT_ADD) $(DJANGO_SETTINGS_DEV_FILE) - -.PHONY: django-settings-directory-default -django-settings-directory-default: - @$(ADD_DIR) $(DJANGO_SETTINGS_DIR) - @$(COPY_FILE) backend/settings.py backend/settings/base.py - @$(DEL_FILE) backend/settings.py - -$(GIT_ADD) backend/settings/*.py - -.PHONY: django-settings-prod-default -django-settings-prod-default: - @echo "$$DJANGO_SETTINGS_PROD" > $(DJANGO_SETTINGS_PROD_FILE) - -$(GIT_ADD) $(DJANGO_SETTINGS_PROD_FILE) - -.PHONY: django-shell-default -django-shell-default: - python manage.py shell - -.PHONY: django-siteuser-default -django-siteuser-default: - python manage.py startapp siteuser - $(ADD_DIR) siteuser/templates/ - @echo "$$DJANGO_SITEUSER_FORM" > siteuser/forms.py - @echo "$$DJANGO_SITEUSER_MODEL" > siteuser/models.py - @echo "$$DJANGO_SITEUSER_ADMIN" > siteuser/admin.py - @echo "$$DJANGO_SITEUSER_VIEW" > siteuser/views.py - @echo "$$DJANGO_SITEUSER_URLS" > siteuser/urls.py - @echo "$$DJANGO_SITEUSER_VIEW_TEMPLATE" > siteuser/templates/profile.html - @echo "$$DJANGO_SITEUSER_TEMPLATE" > siteuser/templates/user.html - @echo "$$DJANGO_SITEUSER_EDIT_TEMPLATE" > siteuser/templates/user_edit.html - @echo "$$DJANGO_URLS_SITEUSER" >> $(DJANGO_URLS_FILE) - @echo "$$DJANGO_SETTINGS_SITEUSER" >> $(DJANGO_SETTINGS_BASE_FILE) - export APP_DIR="siteuser"; $(MAKE) django-app-tests - -$(GIT_ADD) siteuser/templates - -$(GIT_ADD) siteuser/*.py - python manage.py makemigrations siteuser - -$(GIT_ADD) siteuser/migrations/*.py - -.PHONY: django-static-default -django-static-default: - python manage.py collectstatic --noinput - -.PHONY: django-su-default -django-su-default: - DJANGO_SUPERUSER_PASSWORD=admin python manage.py createsuperuser --noinput --username=admin --email=$(PROJECT_EMAIL) - -.PHONY: django-test-default -django-test-default: npm-install django-static - -$(MAKE) pip-install-test - python manage.py test - -.PHONY: django-urls-api-default -django-urls-api-default: - @echo "$$DJANGO_URLS_API" >> $(DJANGO_URLS_FILE) - -$(GIT_ADD) $(DJANGO_URLS_FILE) - -.PHONY: django-urls-debug-toolbar-default -django-urls-debug-toolbar-default: - @echo "$$DJANGO_URLS_DEBUG_TOOLBAR" >> $(DJANGO_URLS_FILE) - -.PHONY: django-urls-default -django-urls-default: - @echo "$$DJANGO_URLS" > $(DJANGO_URLS_FILE) - -$(GIT_ADD) $(DJANGO_URLS_FILE) - -.PHONY: django-urls-show-default -django-urls-show-default: - python manage.py show_urls - -.PHONY: django-user-default -django-user-default: - python manage.py shell -c "from django.contrib.auth.models import User; \ - User.objects.create_user('user', '', 'user')" - -.PHONY: django-utils-default -django-utils-default: - @echo "$$DJANGO_UTILS" > backend/utils.py - -$(GIT_ADD) backend/utils.py - -.PHONY: docker-build-default -docker-build-default: - podman build -t $(PROJECT_NAME) . - -.PHONY: docker-compose-default -docker-compose-default: - podman compose up - -.PHONY: docker-list-default -docker-list-default: - podman container list --all - podman images --all - -.PHONY: docker-run-default -docker-run-default: - podman run $(PROJECT_NAME) - -.PHONY: docker-serve-default -docker-serve-default: - podman run -p 8000:8000 $(PROJECT_NAME) - -.PHONY: docker-shell-default -docker-shell-default: - podman run -it $(PROJECT_NAME) /bin/bash - -.PHONY: eb-check-env-default -eb-check-env-default: # https://stackoverflow.com/a/4731504/185820 -ifndef EB_SSH_KEY - $(error EB_SSH_KEY is undefined) -endif -ifndef VPC_ID - $(error VPC_ID is undefined) -endif -ifndef VPC_SG - $(error VPC_SG is undefined) -endif -ifndef VPC_SUBNET_EC2 - $(error VPC_SUBNET_EC2 is undefined) -endif -ifndef VPC_SUBNET_ELB - $(error VPC_SUBNET_ELB is undefined) -endif - -.PHONY: eb-create-default -eb-create-default: aws-check-env eb-check-env - eb create $(EB_ENV_NAME) \ - -im $(EC2_INSTANCE_MIN) \ - -ix $(EC2_INSTANCE_MAX) \ - -ip $(EC2_INSTANCE_PROFILE) \ - -i $(EC2_INSTANCE_TYPE) \ - -k $(EB_SSH_KEY) \ - -p $(EB_PLATFORM) \ - --elb-type $(EC2_LB_TYPE) \ - --vpc \ - --vpc.id $(VPC_ID) \ - --vpc.elbpublic \ - --vpc.publicip \ - --vpc.ec2subnets $(VPC_SUBNET_EC2) \ - --vpc.elbsubnets $(VPC_SUBNET_ELB) \ - --vpc.securitygroups $(VPC_SG) - -.PHONY: eb-custom-env-default -eb-custom-env-default: - $(ADD_DIR) .ebextensions - @echo "$$EB_CUSTOM_ENV_EC2_USER" > .ebextensions/bash.config - -$(GIT_ADD) .ebextensions/bash.config - $(ADD_DIR) .platform/hooks/postdeploy - @echo "$$EB_CUSTOM_ENV_VAR_FILE" > .platform/hooks/postdeploy/setenv.sh - -$(GIT_ADD) .platform/hooks/postdeploy/setenv.sh - -.PHONY: eb-deploy-default -eb-deploy-default: - eb deploy - -.PHONY: eb-export-default -eb-export-default: - @if [ ! -d $(EB_DIR_NAME) ]; then \ - echo "Directory $(EB_DIR_NAME) does not exist"; \ - else \ - echo "Directory $(EB_DIR_NAME) does exist!"; \ - eb ssh --quiet -c "export PGPASSWORD=$(DJANGO_DB_PASS); pg_dump -U $(DJANGO_DB_USER) -h $(DJANGO_DB_HOST) $(DJANGO_DB_NAME)" > $(DJANGO_DB_NAME).sql; \ - echo "Wrote $(DJANGO_DB_NAME).sql"; \ - fi - -.PHONY: eb-restart-default -eb-restart-default: - eb ssh -c "systemctl restart web" - -.PHONY: eb-rebuild-default -eb-rebuild-default: - aws elasticbeanstalk rebuild-environment --environment-name $(ENV_NAME) - -.PHONY: eb-upgrade-default -eb-upgrade-default: - eb upgrade - -.PHONY: eb-init-default -eb-init-default: aws-check-env-profile - eb init --profile=$(AWS_PROFILE) - -.PHONY: eb-list-default -eb-list-platforms-default: - aws elasticbeanstalk list-platform-versions - -.PHONY: eb-list-databases-default -eb-list-databases-default: - @eb ssh --quiet -c "export PGPASSWORD=$(DJANGO_DB_PASS); psql -l -U $(DJANGO_DB_USER) -h $(DJANGO_DB_HOST) $(DJANGO_DB_NAME)" - -.PHONY: eb-logs-default -eb-logs-default: - eb logs - -.PHONY: eb-print-env-default -eb-print-env-default: - eb printenv - -.PHONY: favicon-default -favicon-init-default: - dd if=/dev/urandom bs=64 count=1 status=none | base64 | convert -size 16x16 -depth 8 -background none -fill white label:@- favicon.png - convert favicon.png favicon.ico - -$(GIT_ADD) favicon.ico - $(DEL_FILE) favicon.png - -.PHONY: git-ignore-default -git-ignore-default: - @echo "$$GIT_IGNORE" > .gitignore - -$(GIT_ADD) .gitignore - -.PHONY: git-branches-default -git-branches-default: - -for i in $(GIT_BRANCHES) ; do \ - -@$(GIT_CHECKOUT) -t $$i ; done - -.PHONY: git-commit-message-clean-default -git-commit-message-clean-default: - -@$(GIT_COMMIT) -a -m "Clean" - -.PHONY: git-commit-message-default -git-commit-message-default: - -@$(GIT_COMMIT) -a -m $(GIT_COMMIT_MSG) - -.PHONY: git-commit-message-empty-default -git-commit-message-empty-default: - -@$(GIT_COMMIT) --allow-empty -m "Empty-Commit" - -.PHONY: git-commit-message-init-default -git-commit-message-init-default: - -@$(GIT_COMMIT) -a -m "Init" - -.PHONY: git-commit-message-last-default -git-commit-message-last-default: - git log -1 --pretty=%B > $(TMPDIR)/commit.txt - -$(GIT_COMMIT) -a -F $(TMPDIR)/commit.txt - -.PHONY: git-commit-message-lint-default -git-commit-message-lint-default: - -@$(GIT_COMMIT) -a -m "Lint" - -.PHONY: git-commit-message-mk-default -git-commit-message-mk-default: - -@$(GIT_COMMIT) project.mk -m "Add/update $(MAKEFILE_CUSTOM_FILE)" - -.PHONY: git-commit-message-rename-default -git-commit-message-rename-default: - -@$(GIT_COMMIT) -a -m "Rename" - -.PHONY: git-commit-message-sort-default -git-commit-message-sort-default: - -@$(GIT_COMMIT) -a -m "Sort" - -.PHONY: git-push-default -git-push-default: - -@$(GIT_PUSH) - -.PHONY: git-push-force-default -git-push-force-default: - -@$(GIT_PUSH_FORCE) - -.PHONY: git-commit-edit-default -git-commit-edit-default: - -$(GIT_COMMIT) -a - -.PHONY: git-prune-default -git-prune-default: - git remote update origin --prune - -.PHONY: git-set-upstream-default -git-set-upstream-default: - git push --set-upstream origin main - -.PHONY: git-set-default-default -git-set-default-default: - gh repo set-default - -.PHONY: git-short-default -git-short-default: - @echo $(GIT_REV) - -.PHONY: help-default -help-default: - @echo "Project Makefile 🤷" - @echo "Usage: make [options] [target] ..." - @echo "Examples:" - @echo " make help Print this message" - @echo " make list-defines list all defines in the Makefile" - @echo " make list-commands list all targets in the Makefile" - -.PHONY: jenkins-init-default -jenkins-init-default: - @echo "$$JENKINS_FILE" > Jenkinsfile - -.PHONY: makefile-list-commands-default -makefile-list-commands-default: - @for makefile in $(MAKEFILE_LIST); do \ - echo "Commands from $$makefile:"; \ - $(MAKE) -pRrq -f $$makefile : 2>/dev/null | \ - awk -v RS= -F: '/^# File/,/^# Finished Make data base/ { \ - if ($$1 !~ "^[#.]") { sub(/-default$$/, "", $$1); print $$1 } }' | \ - egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | \ - tr ' ' '\n' | \ - sort | \ - awk '{print $$0}' ; \ - echo; \ - done | $(PAGER) - -.PHONY: makefile-list-defines-default -makefile-list-defines-default: - @grep '^define [A-Za-z_][A-Za-z0-9_]*' Makefile - -.PHONY: makefile-list-exports-default -makefile-list-exports-default: - @grep '^export [A-Z][A-Z_]*' Makefile - -.PHONY: makefile-list-targets-default -makefile-list-targets-default: - @perl -ne 'print if /^\s*\.PHONY:/ .. /^[a-zA-Z0-9_-]+:/;' Makefile | grep -v .PHONY - -.PHONY: make-default -make-default: - -$(GIT_ADD) Makefile - -$(GIT_COMMIT) Makefile -m "Add/update project-makefile files" - -git push - -.PHONY: npm-init-default -npm-init-default: - npm init -y - -$(GIT_ADD) package.json - -$(GIT_ADD) package-lock.json - -.PHONY: npm-build-default -npm-build-default: - npm run build - -.PHONY: npm-install-default -npm-install-default: - npm install - -$(GIT_ADD) package-lock.json - -.PHONY: npm-install-django-default -npm-install-django-default: - npm install \ - @fortawesome/fontawesome-free \ - @fortawesome/fontawesome-svg-core \ - @fortawesome/free-brands-svg-icons \ - @fortawesome/free-solid-svg-icons \ - @fortawesome/react-fontawesome \ - bootstrap \ - camelize \ - date-fns \ - history \ - mapbox-gl \ - query-string \ - react-animate-height \ - react-chartjs-2 \ - react-copy-to-clipboard \ - react-date-range \ - react-dom \ - react-dropzone \ - react-hook-form \ - react-image-crop \ - react-map-gl \ - react-modal \ - react-resize-detector \ - react-select \ - react-swipeable \ - snakeize \ - striptags \ - url-join \ - viewport-mercator-project - -.PHONY: npm-install-django-dev-default -npm-install-django-dev-default: - npm install \ - eslint-plugin-react \ - eslint-config-standard \ - eslint-config-standard-jsx \ - @babel/core \ - @babel/preset-env \ - @babel/preset-react \ - --save-dev - -.PHONY: npm-serve-default -npm-serve-default: - npm run start - -.PHONY: npm-test-default -npm-test-default: - npm run test - -.PHONY: pip-deps-default -pip-deps-default: - $(PIP_ENSURE) - python -m pip install pipdeptree - python -m pipdeptree - pipdeptree - -.PHONY: pip-freeze-default -pip-freeze-default: - $(PIP_ENSURE) - python -m pip freeze | sort > $(TMPDIR)/requirements.txt - mv -f $(TMPDIR)/requirements.txt . - -$(GIT_ADD) requirements.txt - -.PHONY: pip-init-default -pip-init-default: - touch requirements.txt - -$(GIT_ADD) requirements.txt - -.PHONY: pip-init-test-default -pip-init-test-default: - @echo "$$PIP_INSTALL_REQUIREMENTS_TEST" > requirements-test.txt - -$(GIT_ADD) requirements-test.txt - -.PHONY: pip-install-default -pip-install-default: - $(PIP_ENSURE) - $(MAKE) pip-upgrade - python -m pip install wheel - python -m pip install -r requirements.txt - -.PHONY: pip-install-dev-default -pip-install-dev-default: - $(PIP_ENSURE) - python -m pip install -r requirements-dev.txt - -.PHONY: pip-install-test-default -pip-install-test-default: - $(PIP_ENSURE) - python -m pip install -r requirements-test.txt - -.PHONY: pip-install-upgrade-default -pip-install-upgrade-default: - cat requirements.txt | awk -F\= '{print $$1}' > $(TMPDIR)/requirements.txt - mv -f $(TMPDIR)/requirements.txt . - $(PIP_ENSURE) - python -m pip install -U -r requirements.txt - python -m pip freeze | sort > $(TMPDIR)/requirements.txt - mv -f $(TMPDIR)/requirements.txt . - -.PHONY: pip-upgrade-default -pip-upgrade-default: - $(PIP_ENSURE) - python -m pip install -U pip - -.PHONY: pip-uninstall-default -pip-uninstall-default: - $(PIP_ENSURE) - python -m pip freeze | xargs python -m pip uninstall -y - -.PHONY: plone-clean-default -plone-clean-default: - $(DEL_DIR) $(PROJECT_NAME) - $(DEL_DIR) $(PACKAGE_NAME) - -.PHONY: plone-init-default -plone-init-default: git-ignore plone-install plone-instance plone-serve - -.PHONY: plone-install-default -plone-install-default: - $(PIP_ENSURE) - python -m pip install plone -c $(PIP_INSTALL_PLONE_CONSTRAINTS) - -.PHONY: plone-instance-default -plone-instance-default: - mkwsgiinstance -d backend -u admin:admin - cat backend/etc/zope.ini | sed -e 's/host = 127.0.0.1/host = 0.0.0.0/; s/port = 8080/port = 8000/' > $(TMPDIR)/zope.ini - mv -f $(TMPDIR)/zope.ini backend/etc/zope.ini - -$(GIT_ADD) backend/etc/site.zcml - -$(GIT_ADD) backend/etc/zope.conf - -$(GIT_ADD) backend/etc/zope.ini - -.PHONY: plone-serve-default -plone-serve-default: - runwsgi backend/etc/zope.ini - -.PHONY: plone-build-default -plone-build-default: - buildout - -.PHONY: programming-interview-default -programming-interview-default: - @echo "$$PROGRAMMING_INTERVIEW" > interview.py - @echo "Created interview.py!" - -@$(GIT_ADD) interview.py > /dev/null 2>&1 - -# .NOT_PHONY! -$(MAKEFILE_CUSTOM_FILE): - @echo "$$MAKEFILE_CUSTOM" > $(MAKEFILE_CUSTOM_FILE) - -$(GIT_ADD) $(MAKEFILE_CUSTOM_FILE) - -.PHONY: python-license-default -python-license-default: - @echo "$(PYTHON_LICENSE_TXT)" > LICENSE.txt - -$(GIT_ADD) LICENSE.txt - -.PHONY: python-project-default -python-project-default: - @echo "$(PYTHON_PROJECT_TOML)" > pyproject.toml - -$(GIT_ADD) pyproject.toml - -.PHONY: python-serve-default -python-serve-default: - @echo "\n\tServing HTTP on http://0.0.0.0:8000\n" - python3 -m http.server - -.PHONY: python-sdist-default -python-sdist-default: - $(PIP_ENSURE) - python setup.py sdist --format=zip - -.PHONY: python-webpack-init-default -python-webpack-init-default: - python manage.py webpack_init --no-input - -.PHONY: python-ci-default -python-ci-default: - $(ADD_DIR) .github/workflows - @echo "$(PYTHON_CI_YAML)" > .github/workflows/build_wheels.yml - -$(GIT_ADD) .github/workflows/build_wheels.yml - -.PHONY: rand-default -rand-default: - @openssl rand -base64 12 | sed 's/\///g' - -.PHONY: readme-init-default -readme-init-default: - @echo "# $(PROJECT_NAME)" > README.md - -$(GIT_ADD) README.md - -.PHONY: readme-edit-default -readme-edit-default: - $(EDITOR) README.md - -.PHONY: reveal-init-default -reveal-init-default: webpack-init-reveal - npm install \ - css-loader \ - mini-css-extract-plugin \ - reveal.js \ - style-loader - jq '.scripts += {"build": "webpack"}' package.json > \ - $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json - jq '.scripts += {"start": "webpack serve --mode development --port 8000 --static"}' package.json > \ - $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json - jq '.scripts += {"watch": "webpack watch --mode development"}' package.json > \ - $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json - -.PHONY: reveal-serve-default -reveal-serve-default: - npm run watch & - python -m http.server - -.PHONY: review-default -review-default: -ifeq ($(UNAME), Darwin) - $(EDITOR_REVIEW) `find backend/ -name \*.py` `find backend/ -name \*.html` `find frontend/ -name \*.js` `find frontend/ -name \*.js` -else - @echo "Unsupported" -endif - -.PHONY: separator-default -separator-default: - @echo "$$SEPARATOR" - -.PHONY: sphinx-init-default -sphinx-init-default: sphinx-install - sphinx-quickstart -q -p $(PROJECT_NAME) -a $(USER) -v 0.0.1 $(RANDIR) - $(COPY_DIR) $(RANDIR)/* . - $(DEL_DIR) $(RANDIR) - -$(GIT_ADD) index.rst - -$(GIT_ADD) conf.py - $(DEL_FILE) make.bat - -@$(GIT_CHECKOUT) Makefile - $(MAKE) git-ignore - -.PHONY: sphinx-theme-init-default -sphinx-theme-init-default: - export DJANGO_FRONTEND_THEME_NAME=$(PROJECT_NAME)_theme; \ - $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/__init__.py ; \ - -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/__init__.py ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/theme.conf ; \ - -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/theme.conf ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/layout.html ; \ - -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/layout.html ; \ - $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME/static/css ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/static/css/style.css ; \ - $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME/static/js ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/static/js/script.js ; \ - -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/static - -.PHONY: sphinx-install-default -sphinx-install-default: - echo "Sphinx\n" > requirements.txt - @$(MAKE) pip-install - @$(MAKE) pip-freeze - -$(GIT_ADD) requirements.txt - -.PHONY: sphinx-build-default -sphinx-build-default: - sphinx-build -b html -d _build/doctrees . _build/html - sphinx-build -b rinoh . _build/rinoh - -.PHONY: sphinx-serve-default -sphinx-serve-default: - cd _build/html;python3 -m http.server - -.PHONY: wagtail-base-template-default -wagtail-base-template-default: - @echo "$$WAGTAIL_BASE_TEMPLATE" > backend/templates/base.html - -.PHONY: wagtail-clean-default -wagtail-clean-default: - -@for dir in $(shell echo "$(WAGTAIL_CLEAN_DIRS)"); do \ - echo "Cleaning $$dir"; \ - $(DEL_DIR) $$dir >/dev/null 2>&1; \ - done - -@for file in $(shell echo "$(WAGTAIL_CLEAN_FILES)"); do \ - echo "Cleaning $$file"; \ - $(DEL_FILE) $$file >/dev/null 2>&1; \ - done - -.PHONY: wagtail-contactpage-default -wagtail-contactpage-default: - python manage.py startapp contactpage - @echo "$$WAGTAIL_CONTACT_PAGE_MODEL" > contactpage/models.py - @echo "$$WAGTAIL_CONTACT_PAGE_TEST" > contactpage/tests.py - $(ADD_DIR) contactpage/templates/contactpage/ - @echo "$$WAGTAIL_CONTACT_PAGE_TEMPLATE" > contactpage/templates/contactpage/contact_page.html - @echo "$$WAGTAIL_CONTACT_PAGE_LANDING" > contactpage/templates/contactpage/contact_page_landing.html - @echo "INSTALLED_APPS.append('contactpage')" >> $(DJANGO_SETTINGS_BASE_FILE) - python manage.py makemigrations contactpage - -$(GIT_ADD) contactpage/templates - -$(GIT_ADD) contactpage/*.py - -$(GIT_ADD) contactpage/migrations/*.py - -.PHONY: wagtail-header-prefix-template-default -wagtail-header-prefix-template-default: - @echo "$$WAGTAIL_HEADER_PREFIX" > backend/templates/header.html - -.PHONY: wagtail-home-default -wagtail-home-default: - @echo "$$WAGTAIL_HOME_PAGE_MODEL" > home/models.py - @echo "$$WAGTAIL_HOME_PAGE_TEMPLATE" > home/templates/home/home_page.html - $(ADD_DIR) home/templates/blocks - @echo "$$WAGTAIL_BLOCK_MARKETING" > home/templates/blocks/marketing_block.html - @echo "$$WAGTAIL_BLOCK_CAROUSEL" > home/templates/blocks/carousel_block.html - -$(GIT_ADD) home/templates - -$(GIT_ADD) home/*.py - python manage.py makemigrations home - -$(GIT_ADD) home/migrations/*.py - -.PHONY: wagtail-install-default -wagtail-install-default: - $(PIP_ENSURE) - python -m pip install \ - wagtail \ - wagtailmenus \ - wagtail-color-panel \ - wagtail-django-recaptcha \ - wagtail-markdown \ - wagtail-modeladmin \ - wagtail-seo \ - weasyprint \ - whitenoise \ - xhtml2pdf - -.PHONY: wagtail-private-default -wagtail-privacy-default: - python manage.py startapp privacy - @echo "$$WAGTAIL_PRIVACY_PAGE_MODEL" > privacy/models.py - $(ADD_DIR) privacy/templates - @echo "$$WAGTAIL_PRIVACY_PAGE_TEMPLATE" > privacy/templates/privacy_page.html - @echo "INSTALLED_APPS.append('privacy')" >> $(DJANGO_SETTINGS_BASE_FILE) - python manage.py makemigrations privacy - -$(GIT_ADD) privacy/templates - -$(GIT_ADD) privacy/*.py - -$(GIT_ADD) privacy/migrations/*.py - -.PHONY: wagtail-project-default -wagtail-project-default: - wagtail start backend . - $(DEL_FILE) home/templates/home/welcome_page.html - -$(GIT_ADD) backend/ - -$(GIT_ADD) .dockerignore - -$(GIT_ADD) Dockerfile - -$(GIT_ADD) manage.py - -$(GIT_ADD) requirements.txt - -.PHONY: wagtail-search-default -wagtail-search-default: - @echo "$$WAGTAIL_SEARCH_TEMPLATE" > search/templates/search/search.html - @echo "$$WAGTAIL_SEARCH_URLS" > search/urls.py - -$(GIT_ADD) search/templates - -$(GIT_ADD) search/*.py - -.PHONY: wagtail-settings-default -wagtail-settings-default: - @echo "$$WAGTAIL_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) - -.PHONY: wagtail-sitepage-default -wagtail-sitepage-default: - python manage.py startapp sitepage - @echo "$$WAGTAIL_SITEPAGE_MODEL" > sitepage/models.py - $(ADD_DIR) sitepage/templates/sitepage/ - @echo "$$WAGTAIL_SITEPAGE_TEMPLATE" > sitepage/templates/sitepage/site_page.html - @echo "INSTALLED_APPS.append('sitepage')" >> $(DJANGO_SETTINGS_BASE_FILE) - python manage.py makemigrations sitepage - -$(GIT_ADD) sitepage/templates - -$(GIT_ADD) sitepage/*.py - -$(GIT_ADD) sitepage/migrations/*.py - -.PHONY: wagtail-urls-default -wagtail-urls-default: - @echo "$$WAGTAIL_URLS" > $(DJANGO_URLS_FILE) - -.PHONY: wagtail-urls-home-default -wagtail-urls-home-default: - @echo "$$WAGTAIL_URLS_HOME" >> $(DJANGO_URLS_FILE) - -.PHONY: webpack-init-default -webpack-init-default: npm-init - @echo "$$WEBPACK_CONFIG_JS" > webpack.config.js - -$(GIT_ADD) webpack.config.js - npm install --save-dev webpack webpack-cli webpack-dev-server - $(ADD_DIR) src/ - @echo "$$WEBPACK_INDEX_JS" > src/index.js - -$(GIT_ADD) src/index.js - @echo "$$WEBPACK_INDEX_HTML" > index.html - -$(GIT_ADD) index.html - $(MAKE) git-ignore - -.PHONY: webpack-init-reveal-default -webpack-init-reveal-default: npm-init - @echo "$$WEBPACK_REVEAL_CONFIG_JS" > webpack.config.js - -$(GIT_ADD) webpack.config.js - npm install --save-dev webpack webpack-cli webpack-dev-server - $(ADD_DIR) src/ - @echo "$$WEBPACK_REVEAL_INDEX_JS" > src/index.js - -$(GIT_ADD) src/index.js - @echo "$$WEBPACK_REVEAL_INDEX_HTML" > index.html - -$(GIT_ADD) index.html - $(MAKE) git-ignore - -# -------------------------------------------------------------------------------- -# Single-line phony target rules -# -------------------------------------------------------------------------------- - -.PHONY: aws-check-env-default -aws-check-env-default: aws-check-env-profile aws-check-env-region - -.PHONY: ce-default -ce-default: git-commit-edit git-push - -.PHONY: clean-default -clean-default: wagtail-clean - -.PHONY: cp-default -cp-default: git-commit-message git-push - -.PHONY: db-dump-default -db-dump-default: eb-export - -.PHONY: dbshell-default -dbshell-default: django-db-shell - -.PHONY: deploy-default -deploy-default: eb-deploy - -.PHONY: d-default -d-default: eb-deploy - -.PHONY: deps-default -deps-default: pip-deps - -.PHONY: e-default -e-default: edit - -.PHONY: edit-default -edit-default: readme-edit - -.PHONY: empty-default -empty-default: git-commit-message-empty git-push - -.PHONY: fp-default -fp-default: git-push-force - -.PHONY: freeze-default -freeze-default: pip-freeze git-push - -.PHONY: git-commit-default -git-commit-default: git-commit-message git-push - -.PHONY: git-commit-clean-default -git-commit-clean-default: git-commit-message-clean git-push - -.PHONY: git-commit-init-default -git-commit-init-default: git-commit-message-init git-push - -.PHONY: git-commit-lint-default -git-commit-lint-default: git-commit-message-lint git-push - -.PHONY: gitignore-default -gitignore-default: git-ignore - -.PHONY: h-default -h-default: help - -.PHONY: init-default -init-default: django-init-wagtail django-serve - -.PHONY: init-wagtail-default -init-wagtail-default: django-init-wagtail - -.PHONY: install-default -install-default: pip-install - -.PHONY: l-default -l-default: makefile-list-commands - -.PHONY: last-default -last-default: git-commit-message-last git-push - -.PHONY: lint-default -lint-default: django-lint - -.PHONY: list-commands-default -list-commands-default: makefile-list-commands - -.PHONY: list-defines-default -list-defines-default: makefile-list-defines - -.PHONY: list-exports-default -list-exports-default: makefile-list-exports - -.PHONY: list-targets-default -list-targets-default: makefile-list-targets - -.PHONY: migrate-default -migrate-default: django-migrate - -.PHONY: migrations-default -migrations-default: django-migrations-make - -.PHONY: migrations-show-default -migrations-show-default: django-migrations-show - -.PHONY: mk-default -mk-default: project.mk git-commit-message-mk git-push - -.PHONY: open-default -open-default: django-open - -.PHONY: o-default -o-default: django-open - -.PHONY: readme-default -readme-default: readme-init - -.PHONY: rename-default -rename-default: git-commit-message-rename git-push - -.PHONY: s-default -s-default: django-serve - -.PHONY: shell-default -shell-default: django-shell - -.PHONY: serve-default -serve-default: django-serve - -.PHONY: static-default -static-default: django-static - -.PHONY: sort-default -sort-default: git-commit-message-sort git-push - -.PHONY: su-default -su-default: django-su - -.PHONY: test-default -test-default: django-test - -.PHONY: t-default -t-default: django-test - -.PHONY: u-default -u-default: help - -.PHONY: urls-default -urls-default: django-urls-show - -# -------------------------------------------------------------------------------- -# Allow customizing rules defined in this Makefile with rules defined in -# $(MAKEFILE_CUSTOM_FILE) -# -------------------------------------------------------------------------------- - -%: %-default # https://stackoverflow.com/a/49804748 - @ true +.DEFAULT_GOAL := help + +.PHONY: clean +clean: + rm src/PIL/*.so || true + rm -r build || true + find . -name __pycache__ | xargs rm -r || true + +.PHONY: coverage +coverage: + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq + rm -r htmlcov || true + python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage + python3 -m coverage report + +.PHONY: doc +.PHONY: html +doc html: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + $(MAKE) -C docs html + +.PHONY: htmlview +htmlview: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + $(MAKE) -C docs htmlview + +.PHONY: doccheck +doccheck: + $(MAKE) doc +# Don't make our tests rely on the links in the docs being up every single build. +# We don't control them. But do check, and update them to the target of their redirects. + $(MAKE) -C docs linkcheck || true + +.PHONY: docserve +docserve: + cd docs/_build/html && python3 -m http.server 2> /dev/null& + +.PHONY: help +help: + @echo "Welcome to Pillow development. Please use \`make \` where is one of" + @echo " clean remove build products" + @echo " coverage run coverage test (in progress)" + @echo " doc make HTML docs" + @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 " install make and install" + @echo " install-coverage make and install with C coverage" + @echo " lint run the lint checks" + @echo " lint-fix run Ruff to (mostly) fix lint issues" + @echo " release-test run code and package tests before release" + @echo " test run tests on installed Pillow" + +.PHONY: install +install: + python3 -m pip -v install . + python3 selftest.py + +.PHONY: install-coverage +install-coverage: + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install . + python3 selftest.py + +.PHONY: debug +debug: +# make a debug version if we don't have a -dbg python. Leaves in symbols +# for our stuff, kills optimization, and redirects to dev null so we +# see any build failures. + make clean > /dev/null + CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null + +.PHONY: release-test +release-test: + python3 Tests/check_release_notes.py + python3 -m pip install -e .[tests] + python3 selftest.py + python3 -m pytest Tests + python3 -m pip install . + python3 -m pytest -qq + python3 -m check_manifest + python3 -m pyroma . + $(MAKE) readme + +.PHONY: sdist +sdist: + python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build + python3 -m build --sdist + python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine + python3 -m twine check --strict dist/* + +.PHONY: test +test: + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq + +.PHONY: valgrind +valgrind: + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output + +.PHONY: readme +readme: + python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 + python3 -m markdown2 README.md > .long-description.html && open .long-description.html + + +.PHONY: lint +lint: + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox + python3 -m tox -e lint + +.PHONY: lint-fix +lint-fix: + python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black + python3 -m black . + python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff + python3 -m ruff --fix . + +.PHONY: mypy +mypy: + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox + python3 -m tox -e mypy From d49ea378115f4ae32e12f1f0c3e6146ebdc68fe0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Aug 2024 17:50:42 +1000 Subject: [PATCH 102/652] Include limit in error message --- Tests/test_file_webp.py | 4 +++- src/_webp.c | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index ad08da364..a86757e64 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -163,7 +163,9 @@ class TestFileWebp: im = Image.new("L", (16384, 16384)) with pytest.raises(ValueError) as e: im.save(temp_file) - assert str(e.value) == "encoding error 5: Image size exceeds WebP limit" + assert ( + str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383" + ) def test_WebPEncode_with_invalid_args(self) -> None: """ diff --git a/src/_webp.c b/src/_webp.c index 0d2d6f023..c0e1a6f63 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -673,9 +673,11 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { WebPPictureFree(&pic); if (!ok) { int error_code = (&pic)->error_code; - const char *message = ""; + char message[50] = ""; if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { - message = ": Image size exceeds WebP limit"; + sprintf( + message, ": Image size exceeds WebP limit of %d", WEBP_MAX_DIMENSION + ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); return NULL; From fed916825290cb1f45610017993c82ccca2b7843 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Aug 2024 18:48:28 +1000 Subject: [PATCH 103/652] Catch defusedxml warnings --- Tests/test_file_webp_metadata.py | 10 +++++++++- Tests/test_image.py | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 4ef3d95f2..d9a834c75 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -116,7 +116,15 @@ def test_read_no_exif() -> None: def test_getxmp() -> None: with Image.open("Tests/images/flower.webp") as im: assert "xmp" not in im.info - assert im.getxmp() == {} + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + xmp = im.getxmp() + else: + xmp = im.getxmp() + assert xmp == {} with Image.open("Tests/images/flower2.webp") as im: if ElementTree is None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 719732d12..b1ecc6184 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -938,7 +938,15 @@ class TestImage: def test_empty_xmp(self) -> None: with Image.open("Tests/images/hopper.gif") as im: - assert im.getxmp() == {} + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + xmp = im.getxmp() + else: + xmp = im.getxmp() + assert xmp == {} def test_getxmp_padded(self) -> None: im = Image.new("RGB", (1, 1)) From e382ebed3ad757567ce8522c41a6c704c68c16a1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Aug 2024 19:38:07 +1000 Subject: [PATCH 104/652] Remove warning if NumPy failed to raise an error during conversion --- Tests/test_image_array.py | 2 +- src/PIL/Image.py | 24 ++++++------------------ 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index bb6064882..38425a515 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -47,7 +47,7 @@ def test_toarray() -> None: with pytest.raises(OSError): numpy.array(im_truncated) else: - with pytest.warns(UserWarning): + with pytest.warns(DeprecationWarning): numpy.array(im_truncated) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a6eefff56..2f1a878c1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -724,24 +724,12 @@ class Image: def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]: # numpy array interface support new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3} - try: - if self.mode == "1": - # Binary images need to be extended from bits to bytes - # See: https://github.com/python-pillow/Pillow/issues/350 - new["data"] = self.tobytes("raw", "L") - else: - new["data"] = self.tobytes() - except Exception as e: - if not isinstance(e, (MemoryError, RecursionError)): - try: - import numpy - from packaging.version import parse as parse_version - except ImportError: - pass - else: - if parse_version(numpy.__version__) < parse_version("1.23"): - warnings.warn(str(e)) - raise + if self.mode == "1": + # Binary images need to be extended from bits to bytes + # See: https://github.com/python-pillow/Pillow/issues/350 + new["data"] = self.tobytes("raw", "L") + else: + new["data"] = self.tobytes() new["shape"], new["typestr"] = _conv_type_shape(self) return new From 6e9518b88de595c56a955a20a7359ff93a7e9668 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Aug 2024 19:18:25 +1000 Subject: [PATCH 105/652] Added type hint to example code --- 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 ffc6bec34..37d8cb335 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1917,7 +1917,7 @@ class Image: object:: class Example(Image.ImagePointHandler): - def point(self, data): + def point(self, im: Image) -> Image: # Return result :param mode: Output mode (default is same as input). This can only be used if the source image has mode "L" or "P", and the output has mode "1" or the From 8aa58e320fd2dde581325c63d1673d6a2e3648f4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Aug 2024 21:25:41 +1000 Subject: [PATCH 106/652] Rename _E to ImagePointTransform --- docs/reference/Image.rst | 2 +- src/PIL/Image.py | 48 ++++++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 02e714f20..bc3758218 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -362,8 +362,8 @@ Classes :undoc-members: :show-inheritance: .. autoclass:: PIL.Image.ImagePointHandler +.. autoclass:: PIL.Image.ImagePointTransform .. autoclass:: PIL.Image.ImageTransformHandler -.. autoclass:: PIL.Image._E Protocols --------- diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 37d8cb335..5aa49b619 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -470,43 +470,53 @@ def _getencoder( # Simple expression analyzer -class _E: +class ImagePointTransform: + """ + Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than + 8 bits, this represents an affine transformation, where the value is multiplied by + ``scale`` and ``offset`` is added. + """ + def __init__(self, scale: float, offset: float) -> None: self.scale = scale self.offset = offset - def __neg__(self) -> _E: - return _E(-self.scale, -self.offset) + def __neg__(self) -> ImagePointTransform: + return ImagePointTransform(-self.scale, -self.offset) - def __add__(self, other: _E | float) -> _E: - if isinstance(other, _E): - return _E(self.scale + other.scale, self.offset + other.offset) - return _E(self.scale, self.offset + other) + def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): + return ImagePointTransform( + self.scale + other.scale, self.offset + other.offset + ) + return ImagePointTransform(self.scale, self.offset + other) __radd__ = __add__ - def __sub__(self, other: _E | float) -> _E: + def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform: return self + -other - def __rsub__(self, other: _E | float) -> _E: + def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform: return other + -self - def __mul__(self, other: _E | float) -> _E: - if isinstance(other, _E): + def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): return NotImplemented - return _E(self.scale * other, self.offset * other) + return ImagePointTransform(self.scale * other, self.offset * other) __rmul__ = __mul__ - def __truediv__(self, other: _E | float) -> _E: - if isinstance(other, _E): + def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): return NotImplemented - return _E(self.scale / other, self.offset / other) + return ImagePointTransform(self.scale / other, self.offset / other) -def _getscaleoffset(expr: Callable[[_E], _E | float]) -> tuple[float, float]: - a = expr(_E(1, 0)) - return (a.scale, a.offset) if isinstance(a, _E) else (0, a) +def _getscaleoffset( + expr: Callable[[ImagePointTransform], ImagePointTransform | float] +) -> tuple[float, float]: + a = expr(ImagePointTransform(1, 0)) + return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) # -------------------------------------------------------------------- @@ -1898,7 +1908,7 @@ class Image: Sequence[float] | NumpyArray | Callable[[int], float] - | Callable[[_E], _E | float] + | Callable[[ImagePointTransform], ImagePointTransform | float] | ImagePointHandler ), mode: str | None = None, From 658b60e3a33d6bf4e3ff3a2053cb73faa466772d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 24 Aug 2024 21:26:56 +1000 Subject: [PATCH 107/652] Include units Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_file_webp.py | 2 +- src/_webp.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index a86757e64..4a048f2c2 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -164,7 +164,7 @@ class TestFileWebp: with pytest.raises(ValueError) as e: im.save(temp_file) assert ( - str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383" + str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" ) def test_WebPEncode_with_invalid_args(self) -> None: diff --git a/src/_webp.c b/src/_webp.c index c0e1a6f63..b4cf9c329 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -676,7 +676,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { char message[50] = ""; if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { sprintf( - message, ": Image size exceeds WebP limit of %d", WEBP_MAX_DIMENSION + message, ": Image size exceeds WebP limit of %d pixels", WEBP_MAX_DIMENSION ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); From a3e3639a59ffaf33430b08b089da1eedf04286ab Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:27:40 +0000 Subject: [PATCH 108/652] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_webp.py | 3 ++- src/_webp.c | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 4a048f2c2..247fc6021 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -164,7 +164,8 @@ class TestFileWebp: with pytest.raises(ValueError) as e: im.save(temp_file) assert ( - str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" + str(e.value) + == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" ) def test_WebPEncode_with_invalid_args(self) -> None: diff --git a/src/_webp.c b/src/_webp.c index b4cf9c329..f59ad3036 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -676,7 +676,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { char message[50] = ""; if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { sprintf( - message, ": Image size exceeds WebP limit of %d pixels", WEBP_MAX_DIMENSION + message, + ": Image size exceeds WebP limit of %d pixels", + WEBP_MAX_DIMENSION ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); From cfb093af39cf01fc631845d941e0bfda0bb4a52f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Aug 2024 23:15:35 +1000 Subject: [PATCH 109/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index a7edc340c..9da76d82a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Updated error message when saving WebP with invalid width or height #8322 + [radarhere, hugovk] + +- Remove warning if NumPy failed to raise an error during conversion #8326 + [radarhere] + - If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304 [radarhere] From 53bbfea7637e5cc16d074a205784d0408f13de6e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Aug 2024 23:49:03 +1000 Subject: [PATCH 110/652] Added type hints --- Tests/test_qt_image_qapplication.py | 42 +++++++++++++++------- Tests/test_qt_image_toqimage.py | 2 +- pyproject.toml | 1 - src/PIL/EpsImagePlugin.py | 2 +- src/PIL/Image.py | 10 +++--- src/PIL/ImageQt.py | 54 +++++++++++++++++------------ 6 files changed, 67 insertions(+), 44 deletions(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 28f66891c..0ed9fbfa5 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from typing import TYPE_CHECKING, Union import pytest @@ -8,6 +9,20 @@ from PIL import Image, ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper +if TYPE_CHECKING: + import PyQt6 + import PySide6 + + QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication] + QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout] + QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] + QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel] + QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter] + QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] + QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint] + QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion] + QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget] + if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap @@ -20,7 +35,7 @@ if ImageQt.qt_is_installed: from PySide6.QtGui import QImage, QPainter, QRegion from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - class Example(QWidget): + class Example(QWidget): # type: ignore[misc] def __init__(self) -> None: super().__init__() @@ -28,11 +43,12 @@ if ImageQt.qt_is_installed: qimage = ImageQt.ImageQt(img) - pixmap1 = ImageQt.QPixmap.fromImage(qimage) + pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage) - QHBoxLayout(self) # hbox + # hbox + QHBoxLayout(self) # type: ignore[operator] - lbl = QLabel(self) + lbl = QLabel(self) # type: ignore[operator] # Segfault in the problem lbl.setPixmap(pixmap1.copy()) @@ -46,7 +62,7 @@ def roundtrip(expected: Image.Image) -> None: @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") def test_sanity(tmp_path: Path) -> None: # Segfault test - app: QApplication | None = QApplication([]) + app: QApplication | None = QApplication([]) # type: ignore[operator] ex = Example() assert app # Silence warning assert ex # Silence warning @@ -56,7 +72,7 @@ def test_sanity(tmp_path: Path) -> None: im = hopper(mode) data = ImageQt.toqpixmap(im) - assert isinstance(data, QPixmap) + assert data.__class__.__name__ == "QPixmap" assert not data.isNull() # Test saving the file @@ -64,14 +80,14 @@ def test_sanity(tmp_path: Path) -> None: data.save(tempfile) # Render the image - qimage = ImageQt.ImageQt(im) - data = QPixmap.fromImage(qimage) - qt_format = QImage.Format if ImageQt.qt_version == "6" else QImage - qimage = QImage(128, 128, qt_format.Format_ARGB32) - painter = QPainter(qimage) - image_label = QLabel() + imageqt = ImageQt.ImageQt(im) + data = getattr(QPixmap, "fromImage")(imageqt) + qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage + qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator] + painter = QPainter(qimage) # type: ignore[operator] + image_label = QLabel() # type: ignore[operator] image_label.setPixmap(data) - image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) + image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator] painter.end() rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png") qimage.save(rendered_tempfile) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 6110be707..8cb7ffb9b 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -21,7 +21,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) - assert isinstance(data, QImage) + assert isinstance(data, QImage) # type: ignore[arg-type, misc] assert not data.isNull() # reload directly from the qimage diff --git a/pyproject.toml b/pyproject.toml index 9e53d4bfe..e4ae73acf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -166,5 +166,4 @@ warn_unused_ignores = true exclude = [ '^Tests/oss-fuzz/fuzz_font.py$', '^Tests/oss-fuzz/fuzz_pillow.py$', - '^Tests/test_qt_image_qapplication.py$', ] diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 4da0cd230..d830ad53f 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -71,7 +71,7 @@ def Ghostscript( fp: IO[bytes], scale: int = 1, transparency: bool = False, -) -> Image.Image: +) -> Image.core.ImagingCore: """Render an image using Ghostscript""" global gs_binary if not has_ghostscript(): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index fbeecef0e..0877f7792 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -223,7 +223,7 @@ if TYPE_CHECKING: from IPython.lib.pretty import PrettyPrinter - from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin + from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] OPEN: dict[ @@ -2978,7 +2978,7 @@ class Image: self.load() return self._new(self.im.effect_spread(distance)) - def toqimage(self): + def toqimage(self) -> ImageQt.ImageQt: """Returns a QImage copy of this image""" from . import ImageQt @@ -2987,7 +2987,7 @@ class Image: raise ImportError(msg) return ImageQt.toqimage(self) - def toqpixmap(self): + def toqpixmap(self) -> ImageQt.QPixmap: """Returns a QPixmap copy of this image""" from . import ImageQt @@ -3314,7 +3314,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) -def fromqimage(im) -> ImageFile.ImageFile: +def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile: """Creates an image instance from a QImage image""" from . import ImageQt @@ -3324,7 +3324,7 @@ def fromqimage(im) -> ImageFile.ImageFile: return ImageQt.fromqimage(im) -def fromqpixmap(im) -> ImageFile.ImageFile: +def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile: """Creates an image instance from a QPixmap image""" from . import ImageQt diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index a2d946714..a3d647138 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,14 +19,23 @@ from __future__ import annotations import sys from io import BytesIO -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Any, Callable, Union from . import Image from ._util import is_path if TYPE_CHECKING: + import PyQt6 + import PySide6 + from . import ImageFile + QBuffer: type + QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray] + QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice] + QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] + QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] + qt_version: str | None qt_versions = [ ["6", "PyQt6"], @@ -37,10 +46,6 @@ qt_versions = [ qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) for version, qt_module in qt_versions: try: - QBuffer: type - QIODevice: type - QImage: type - QPixmap: type qRgba: Callable[[int, int, int, int], int] if qt_module == "PyQt6": from PyQt6.QtCore import QBuffer, QIODevice @@ -65,19 +70,20 @@ def rgb(r: int, g: int, b: int, a: int = 255) -> int: return qRgba(r, g, b, a) & 0xFFFFFFFF -def fromqimage(im): +def fromqimage(im: QImage | QPixmap) -> ImageFile.ImageFile: """ :param im: QImage or PIL ImageQt object """ buffer = QBuffer() + qt_openmode: object if qt_version == "6": try: - qt_openmode = QIODevice.OpenModeFlag + qt_openmode = getattr(QIODevice, "OpenModeFlag") except AttributeError: - qt_openmode = QIODevice.OpenMode + qt_openmode = getattr(QIODevice, "OpenMode") else: qt_openmode = QIODevice - buffer.open(qt_openmode.ReadWrite) + buffer.open(getattr(qt_openmode, "ReadWrite")) # preserve alpha channel with png # otherwise ppm is more friendly with Image.open if im.hasAlphaChannel(): @@ -93,7 +99,7 @@ def fromqimage(im): return Image.open(b) -def fromqpixmap(im) -> ImageFile.ImageFile: +def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile: return fromqimage(im) @@ -123,7 +129,7 @@ def align8to32(bytes: bytes, width: int, mode: str) -> bytes: return b"".join(new_data) -def _toqclass_helper(im): +def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]: data = None colortable = None exclusive_fp = False @@ -135,30 +141,32 @@ def _toqclass_helper(im): if is_path(im): im = Image.open(im) exclusive_fp = True + assert isinstance(im, Image.Image) - qt_format = QImage.Format if qt_version == "6" else QImage + qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage if im.mode == "1": - format = qt_format.Format_Mono + format = getattr(qt_format, "Format_Mono") elif im.mode == "L": - format = qt_format.Format_Indexed8 + format = getattr(qt_format, "Format_Indexed8") colortable = [rgb(i, i, i) for i in range(256)] elif im.mode == "P": - format = qt_format.Format_Indexed8 + format = getattr(qt_format, "Format_Indexed8") palette = im.getpalette() + assert palette is not None colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)] elif im.mode == "RGB": # Populate the 4th channel with 255 im = im.convert("RGBA") data = im.tobytes("raw", "BGRA") - format = qt_format.Format_RGB32 + format = getattr(qt_format, "Format_RGB32") elif im.mode == "RGBA": data = im.tobytes("raw", "BGRA") - format = qt_format.Format_ARGB32 + format = getattr(qt_format, "Format_ARGB32") elif im.mode == "I;16": im = im.point(lambda i: i * 256) - format = qt_format.Format_Grayscale16 + format = getattr(qt_format, "Format_Grayscale16") else: if exclusive_fp: im.close() @@ -174,8 +182,8 @@ def _toqclass_helper(im): if qt_is_installed: - class ImageQt(QImage): - def __init__(self, im) -> None: + class ImageQt(QImage): # type: ignore[misc] + def __init__(self, im: Image.Image | str | QByteArray) -> None: """ An PIL image wrapper for Qt. This is a subclass of PyQt's QImage class. @@ -199,10 +207,10 @@ if qt_is_installed: self.setColorTable(im_data["colortable"]) -def toqimage(im) -> ImageQt: +def toqimage(im: Image.Image | str | QByteArray) -> ImageQt: return ImageQt(im) -def toqpixmap(im): +def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap: qimage = toqimage(im) - return QPixmap.fromImage(qimage) + return getattr(QPixmap, "fromImage")(qimage) From 65172db28db0cc965c026512546857540b0c6e20 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 Aug 2024 19:04:46 +1000 Subject: [PATCH 111/652] Corrected Ghostscript path --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 5b34d6703..2943bbe31 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -87,7 +87,7 @@ jobs: echo "C:\Program Files\NASM" >> $env:GITHUB_PATH choco install ghostscript --version=10.3.1 --no-progress - echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH + echo "C:\Program Files\gs\gs10.03.1\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images From ae59ea4899e05146188230ddcbd11ca2e51511d5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 00:52:05 +0000 Subject: [PATCH 112/652] Update dependency mypy to v1.11.2 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 47fc64399..5b09ef64c 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.11.1 +mypy==1.11.2 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From bdc53f6c4d499c918cc8d050ed163b8b9a178fe3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Aug 2024 18:33:43 +1000 Subject: [PATCH 113/652] Expand C image to match GIF frame image size --- Tests/images/test_extents_transparency.gif | Bin 0 -> 415 bytes Tests/test_file_gif.py | 37 ++++++++++++++++----- src/PIL/GifImagePlugin.py | 18 ++++++++++ 3 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 Tests/images/test_extents_transparency.gif diff --git a/Tests/images/test_extents_transparency.gif b/Tests/images/test_extents_transparency.gif new file mode 100644 index 0000000000000000000000000000000000000000..739c82ad47c73560495e23c4b44bab5e1a88184f GIT binary patch literal 415 zcmZ?wbhEHbOkqf2Xk=jc&wv1mKUo+V7#JCJKtdpS29AXcj2to^8x|aF<`CA3Ik92k z;dTLKuQ?tY7ai@EFwVMjV&mfD{R+-qGM<~3oSdu?yej78rlqH+8zi5aWIx*cTa6yeSLkx;VxP4 zZEJ3B&bYiP_Vl*3x3?EOJ~h{S`?|ZkD?Y!vdwTo&`}-T1x#fIzY None: im.load() -def test_extents() -> None: - with Image.open("Tests/images/test_extents.gif") as im: - assert im.size == (100, 100) +@pytest.mark.parametrize( + "test_file, loading_strategy", + ( + ("test_extents.gif", GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST), + ( + "test_extents.gif", + GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY, + ), + ( + "test_extents_transparency.gif", + GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST, + ), + ), +) +def test_extents(test_file, loading_strategy) -> None: + GifImagePlugin.LOADING_STRATEGY = loading_strategy + try: + with Image.open("Tests/images/" + test_file) as im: + assert im.size == (100, 100) - # Check that n_frames does not change the size - assert im.n_frames == 2 - assert im.size == (100, 100) + # Check that n_frames does not change the size + assert im.n_frames == 2 + assert im.size == (100, 100) - im.seek(1) - assert im.size == (150, 150) + im.seek(1) + assert im.size == (150, 150) + + im.load() + assert im.im.size == (150, 150) + finally: + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST def test_missing_background() -> None: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index fa8137fb9..82f58905d 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -438,6 +438,13 @@ class GifImageFile(ImageFile.ImageFile): self.im.putpalette("RGB", *self._frame_palette.getdata()) else: self._im = None + if not self._prev_im and self._im is not None and self.size != self.im.size: + expanded_im = Image.core.fill(self.im.mode, self.size) + if self._frame_palette: + expanded_im.putpalette("RGB", *self._frame_palette.getdata()) + expanded_im.paste(self.im, (0, 0) + self.im.size) + + self.im = expanded_im self._mode = temp_mode self._frame_palette = None @@ -455,6 +462,17 @@ class GifImageFile(ImageFile.ImageFile): return if not self._prev_im: return + if self.size != self._prev_im.size: + if self._frame_transparency is not None: + expanded_im = Image.core.fill("RGBA", self.size) + else: + expanded_im = Image.core.fill("P", self.size) + expanded_im.putpalette("RGB", "RGB", self.im.getpalette()) + expanded_im = expanded_im.convert("RGB") + expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size) + + self._prev_im = expanded_im + assert self._prev_im is not None if self._frame_transparency is not None: self.im.putpalettealpha(self._frame_transparency, 0) frame_im = self.im.convert("RGBA") From 0c335d239bb6f73a45795a8d814670f9ddc8aa45 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Apr 2024 12:38:28 -0500 Subject: [PATCH 114/652] use (void) for empty function parameters --- src/libImaging/JpegDecode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index 30c64f235..2970f56d1 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -48,7 +48,7 @@ char *libjpeg_turbo_version = NULL; #endif int -ImagingJpegUseJCSExtensions() { +ImagingJpegUseJCSExtensions(void) { int use_jcs_extensions = 0; #ifdef JCS_EXTENSIONS #if defined(LIBJPEG_TURBO_VERSION_NUMBER) From d92603a0c6ea70d285847ece6fc800048d60646d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:58:53 +0300 Subject: [PATCH 115/652] Drop support for EOL PyPy3.9 --- .ci/install.sh | 5 +---- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 1 - .github/workflows/wheels.yml | 2 -- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index b2d615866..ee92d447a 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -53,10 +53,7 @@ if [[ $(uname) != CYGWIN* ]]; then fi # Pyroma uses non-isolated build and fails with old setuptools - if [[ - $GHA_PYTHON_VERSION == pypy3.9 - || $GHA_PYTHON_VERSION == 3.9 - ]]; then + if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then # To match pyproject.toml python3 -m pip install "setuptools>=67.8" fi diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 2943bbe31..f58e8dae3 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "pypy3.9", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"] timeout-minutes: 30 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e63333b0..1b169db91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,6 @@ jobs: ] python-version: [ "pypy3.10", - "pypy3.9", "3.13", "3.12", "3.11", diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3ed1b5926..f3ed8399b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -48,7 +48,6 @@ jobs: fail-fast: false matrix: python-version: - - pp39 - pp310 - cp3{9,10,11} - cp3{12,13} @@ -57,7 +56,6 @@ jobs: - manylinux_2_28 - musllinux exclude: - - { python-version: pp39, spec: musllinux } - { python-version: pp310, spec: musllinux } steps: From d00f3656a684825569152ea1d6e74526ffa5d14b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Aug 2024 22:51:15 +1000 Subject: [PATCH 116/652] Changed im.tile to never be None --- Tests/test_imagefile.py | 24 +++++++++++++++++------- docs/example/DdsImagePlugin.py | 4 +++- src/PIL/BlpImagePlugin.py | 7 +++++-- src/PIL/BmpImagePlugin.py | 2 +- src/PIL/CurImagePlugin.py | 4 ++-- src/PIL/DdsImagePlugin.py | 2 +- src/PIL/FitsImagePlugin.py | 2 +- src/PIL/FliImagePlugin.py | 2 +- src/PIL/FpxImagePlugin.py | 6 +++--- src/PIL/FtexImagePlugin.py | 4 ++-- src/PIL/GdImageFile.py | 2 +- src/PIL/GifImagePlugin.py | 2 +- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/ImImagePlugin.py | 22 ++++++++++++++++------ src/PIL/Image.py | 5 ++++- src/PIL/ImageFile.py | 27 ++++++++++++++++++--------- src/PIL/ImtImagePlugin.py | 2 +- src/PIL/IptcImagePlugin.py | 4 +++- src/PIL/Jpeg2KImagePlugin.py | 5 +++-- src/PIL/JpegImagePlugin.py | 8 ++++++-- src/PIL/McIdasImagePlugin.py | 4 +++- src/PIL/MpoImagePlugin.py | 5 ++++- src/PIL/MspImagePlugin.py | 4 ++-- src/PIL/PcdImagePlugin.py | 2 +- src/PIL/PcxImagePlugin.py | 4 +++- src/PIL/PixarImagePlugin.py | 4 +++- src/PIL/PpmImagePlugin.py | 4 +++- src/PIL/PsdImagePlugin.py | 6 ++---- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/SgiImagePlugin.py | 15 ++++++++++++--- src/PIL/SpiderImagePlugin.py | 4 +++- src/PIL/SunImagePlugin.py | 8 ++++++-- src/PIL/TgaImagePlugin.py | 4 ++-- src/PIL/TiffImagePlugin.py | 15 ++++++++------- src/PIL/WebPImagePlugin.py | 2 +- src/PIL/XVThumbImagePlugin.py | 6 +++++- src/PIL/XbmImagePlugin.py | 2 +- src/PIL/XpmImagePlugin.py | 4 +++- 38 files changed, 153 insertions(+), 78 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index a8bd798c1..0985dce9c 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -238,7 +238,9 @@ class MockImageFile(ImageFile.ImageFile): self.rawmode = "RGBA" self._mode = "RGBA" self._size = (200, 200) - self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] + self.tile = [ + ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None) + ] class CodecsTest: @@ -268,7 +270,7 @@ class TestPyDecoder(CodecsTest): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [("MOCK", None, 32, None)] + im.tile = [ImageFile._Tile("MOCK", None, 32, None)] im.load() @@ -281,12 +283,12 @@ class TestPyDecoder(CodecsTest): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] + im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] with pytest.raises(ValueError): im.load() - im.tile = [("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)] + im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)] with pytest.raises(ValueError): im.load() @@ -294,12 +296,20 @@ class TestPyDecoder(CodecsTest): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] + im.tile = [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None + ) + ] with pytest.raises(ValueError): im.load() - im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)] + im.tile = [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None + ) + ] with pytest.raises(ValueError): im.load() @@ -336,7 +346,7 @@ class TestPyEncoder(CodecsTest): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [("MOCK", None, 32, None)] + im.tile = [ImageFile._Tile("MOCK", None, 32, None)] fp = BytesIO() ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")]) diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index caa852b1f..d8f536076 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -246,7 +246,9 @@ class DdsImageFile(ImageFile.ImageFile): msg = f"Unimplemented pixel format {repr(fourcc)}" raise NotImplementedError(msg) - self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] + self.tile = [ + ImageFile._Tile(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1)) + ] def load_seek(self, pos: int) -> None: pass diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 1112276dc..8e29898df 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -273,7 +273,7 @@ class BlpImageFile(ImageFile.ImageFile): raise BLPFormatError(msg) self._mode = "RGBA" if self._blp_alpha_depth else "RGB" - self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] + self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] class _BLPBaseDecoder(ImageFile.PyDecoder): @@ -372,7 +372,10 @@ class BLP1Decoder(_BLPBaseDecoder): Image._decompression_bomb_check(image.size) if image.mode == "CMYK": decoder_name, extents, offset, args = image.tile[0] - image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))] + assert isinstance(args, tuple) + image.tile = [ + ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK")) + ] r, g, b = image.convert("RGB").split() reversed_image = Image.merge("RGB", (b, g, r)) self.set_as_raw(reversed_image.tobytes()) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index de441c6b5..e70856ffb 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -296,7 +296,7 @@ class BmpImageFile(ImageFile.ImageFile): args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) args.append(file_info["direction"]) self.tile = [ - ( + ImageFile._Tile( decoder_name, (0, 0, file_info["width"], file_info["height"]), offset or self.fp.tell(), diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index 85e2145e7..c4be0ceca 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -17,7 +17,7 @@ # from __future__ import annotations -from . import BmpImagePlugin, Image +from . import BmpImagePlugin, Image, ImageFile from ._binary import i16le as i16 from ._binary import i32le as i32 @@ -64,7 +64,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): # patch up the bitmap height self._size = self.size[0], self.size[1] // 2 d, e, o, a = self.tile[0] - self.tile[0] = d, (0, 0) + self.size, o, a + self.tile[0] = ImageFile._Tile(d, (0, 0) + self.size, o, a) # diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index a57e4aea2..631532470 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -367,7 +367,7 @@ class DdsImageFile(ImageFile.ImageFile): mask_count = 3 masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4)) - self.tile = [("dds_rgb", extents, 0, (bitcount, masks))] + self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))] return elif pfflags & DDPF.LUMINANCE: if bitcount == 8: diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 4846054b1..abacc5e67 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -67,7 +67,7 @@ class FitsImageFile(ImageFile.ImageFile): raise ValueError(msg) offset += self.fp.tell() - 80 - self.tile = [(decoder_name, (0, 0) + self.size, offset, args)] + self.tile = [ImageFile._Tile(decoder_name, (0, 0) + self.size, offset, args)] def _get_size( self, headers: dict[bytes, bytes], prefix: bytes diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 52d1fce31..666390be9 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -159,7 +159,7 @@ class FliImageFile(ImageFile.ImageFile): framesize = i32(s) self.decodermaxblock = framesize - self.tile = [("fli", (0, 0) + self.size, self.__offset, None)] + self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset, None)] self.__offset += framesize diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 64d48635e..8fef51076 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -166,7 +166,7 @@ class FpxImageFile(ImageFile.ImageFile): if compression == 0: self.tile.append( - ( + ImageFile._Tile( "raw", (x, y, x1, y1), i32(s, i) + 28, @@ -177,7 +177,7 @@ class FpxImageFile(ImageFile.ImageFile): elif compression == 1: # FIXME: the fill decoder is not implemented self.tile.append( - ( + ImageFile._Tile( "fill", (x, y, x1, y1), i32(s, i) + 28, @@ -205,7 +205,7 @@ class FpxImageFile(ImageFile.ImageFile): jpegmode = rawmode self.tile.append( - ( + ImageFile._Tile( "jpeg", (x, y, x1, y1), i32(s, i) + 28, diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 5acbb4912..ddb469bc3 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -93,9 +93,9 @@ class FtexImageFile(ImageFile.ImageFile): if format == Format.DXT1: self._mode = "RGBA" - self.tile = [("bcn", (0, 0) + self.size, 0, 1)] + self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] elif format == Format.UNCOMPRESSED: - self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] else: msg = f"Invalid texture compression format: {repr(format)}" raise ValueError(msg) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 88b87a22c..f1b4969f2 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -72,7 +72,7 @@ class GdImageFile(ImageFile.ImageFile): ) self.tile = [ - ( + ImageFile._Tile( "raw", (0, 0) + self.size, 7 + true_color_offset + 4 + 256 * 4, diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index fa8137fb9..22539925f 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -407,7 +407,7 @@ class GifImageFile(ImageFile.ImageFile): elif self.mode not in ("RGB", "RGBA"): transparency = frame_transparency self.tile = [ - ( + ImageFile._Tile( "gif", (x0, y0, x1, y1), self.__offset, diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index d0605b75a..144fe0b63 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -228,7 +228,7 @@ class IcoFile: # change tile dimension to only encompass XOR image im._size = (im.size[0], int(im.size[1] / 2)) d, e, o, a = im.tile[0] - im.tile[0] = d, (0, 0) + im.size, o, a + im.tile[0] = ImageFile._Tile(d, (0, 0) + im.size, o, a) # figure out where AND mask image starts if header.bpp == 32: diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index b94165089..f9f47348c 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -253,7 +253,11 @@ class ImImageFile(ImageFile.ImageFile): # use bit decoder (if necessary) bits = int(self.rawmode[2:]) if bits not in [8, 16, 32]: - self.tile = [("bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1))] + self.tile = [ + ImageFile._Tile( + "bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1) + ) + ] return except ValueError: pass @@ -263,13 +267,17 @@ class ImImageFile(ImageFile.ImageFile): # ever stumbled upon such a file ;-) size = self.size[0] * self.size[1] self.tile = [ - ("raw", (0, 0) + self.size, offs, ("G", 0, -1)), - ("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)), - ("raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)), + ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)), + ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)), + ImageFile._Tile( + "raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1) + ), ] else: # LabEye/IFUNC files - self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1)) + ] @property def n_frames(self) -> int: @@ -295,7 +303,9 @@ class ImImageFile(ImageFile.ImageFile): self.fp = self._fp - self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1)) + ] def tell(self) -> int: return self.frame diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 95b4c64ee..04f73870a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3641,7 +3641,10 @@ def merge(mode: str, bands: Sequence[Image]) -> Image: def register_open( id: str, - factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + factory: ( + Callable[[IO[bytes], str | bytes], ImageFile.ImageFile] + | type[ImageFile.ImageFile] + ), accept: Callable[[bytes], bool | str] | None = None, ) -> None: """ diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index a1c33f219..fce1591ef 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -34,12 +34,15 @@ import itertools import os import struct import sys -from typing import IO, Any, NamedTuple +from typing import IO, TYPE_CHECKING, Any, NamedTuple from . import Image from ._deprecate import deprecate from ._util import is_path +if TYPE_CHECKING: + from ._typing import StrOrBytesPath + MAXBLOCK = 65536 SAFEBLOCK = 1024 * 1024 @@ -107,32 +110,34 @@ class _Tile(NamedTuple): class ImageFile(Image.Image): """Base class for image file format handlers.""" - def __init__(self, fp=None, filename=None): + def __init__( + self, fp: StrOrBytesPath | IO[bytes] | None = None, filename: str | bytes | None = None + ) -> None: super().__init__() self._min_frame = 0 - self.custom_mimetype = None + self.custom_mimetype: str | None = None - self.tile = None + self.tile: list[_Tile] = [] """ A list of tile descriptors, or ``None`` """ self.readonly = 1 # until we know better - self.decoderconfig = () + self.decoderconfig: tuple[Any, ...] = () self.decodermaxblock = MAXBLOCK if is_path(fp): # filename self.fp = open(fp, "rb") - self.filename = fp + self.filename = os.path.realpath(os.fspath(fp)) self._exclusive_fp = True else: # stream self.fp = fp - self.filename = filename + self.filename = filename if filename is not None else "" # can be overridden - self._exclusive_fp = None + self._exclusive_fp = False try: try: @@ -155,6 +160,9 @@ class ImageFile(Image.Image): self.fp.close() raise + def _open(self) -> None: + pass + def get_format_mimetype(self) -> str | None: if self.custom_mimetype: return self.custom_mimetype @@ -178,7 +186,7 @@ class ImageFile(Image.Image): def load(self) -> Image.core.PixelAccess | None: """Load image data based on tile list""" - if self.tile is None: + if not self.tile and self._im is None: msg = "cannot load this image" raise OSError(msg) @@ -214,6 +222,7 @@ class ImageFile(Image.Image): args = (args, 0, 1) if ( decoder_name == "raw" + and isinstance(args, tuple) and len(args) >= 3 and args[0] == self.mode and args[0] in Image._MAPMODES diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index abb3fb762..594c56513 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -58,7 +58,7 @@ class ImtImageFile(ImageFile.ImageFile): if s == b"\x0C": # image data begins self.tile = [ - ( + ImageFile._Tile( "raw", (0, 0) + self.size, self.fp.tell() - len(buffer), diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index a8cd38218..60ab7c83f 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -147,7 +147,9 @@ class IptcImageFile(ImageFile.ImageFile): # tile if tag == (8, 10): - self.tile = [("iptc", (0, 0) + self.size, offset, compression)] + self.tile = [ + ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression) + ] def load(self) -> Image.core.PixelAccess | None: if len(self.tile) != 1 or self.tile[0][0] != "iptc": diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 02c2e48cf..cfbf78460 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -286,7 +286,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): length = -1 self.tile = [ - ( + ImageFile._Tile( "jpeg2k", (0, 0) + self.size, 0, @@ -338,8 +338,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile): # Update the reduce and layers settings t = self.tile[0] + assert isinstance(t[3], tuple) t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4]) - self.tile = [(t[0], (0, 0) + self.size, t[2], t3)] + self.tile = [ImageFile._Tile(t[0], (0, 0) + self.size, t[2], t3)] return ImageFile.ImageFile.load(self) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index bd4539be4..1f54b7e92 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -372,7 +372,9 @@ class JpegImageFile(ImageFile.ImageFile): rawmode = self.mode if self.mode == "CMYK": rawmode = "CMYK;I" # assume adobe conventions - self.tile = [("jpeg", (0, 0) + self.size, 0, (rawmode, ""))] + self.tile = [ + ImageFile._Tile("jpeg", (0, 0) + self.size, 0, (rawmode, "")) + ] # self.__offset = self.fp.tell() break s = self.fp.read(1) @@ -423,6 +425,7 @@ class JpegImageFile(ImageFile.ImageFile): scale = 1 original_size = self.size + assert isinstance(a, tuple) if a[0] == "RGB" and mode in ["L", "YCbCr"]: self._mode = mode a = mode, "" @@ -432,6 +435,7 @@ class JpegImageFile(ImageFile.ImageFile): for s in [8, 4, 2, 1]: if scale >= s: break + assert e is not None e = ( e[0], e[1], @@ -441,7 +445,7 @@ class JpegImageFile(ImageFile.ImageFile): self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s) scale = s - self.tile = [(d, e, o, a)] + self.tile = [ImageFile._Tile(d, e, o, a)] self.decoderconfig = (scale, 0) box = (0, 0, original_size[0] / scale, original_size[1] / scale) diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 27972236c..5dd031be3 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -67,7 +67,9 @@ class McIdasImageFile(ImageFile.ImageFile): offset = w[34] + w[15] stride = w[15] + w[10] * w[11] * w[14] - self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))] + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride, 1)) + ] # -------------------------------------------------------------------- diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 5ed9f56a1..71f89a09a 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -26,6 +26,7 @@ from typing import IO, Any, cast from . import ( Image, + ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin, @@ -145,7 +146,9 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): if self.info.get("exif") != original_exif: self._reload_exif() - self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])] + self.tile = [ + ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1]) + ] self.__frame = frame def tell(self) -> int: diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 40e5fa435..c92603254 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -70,9 +70,9 @@ class MspImageFile(ImageFile.ImageFile): self._size = i16(s, 4), i16(s, 6) if s[:4] == b"DanM": - self.tile = [("raw", (0, 0) + self.size, 32, ("1", 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))] else: - self.tile = [("MSP", (0, 0) + self.size, 32, None)] + self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32, None)] class MspDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index d9970405c..e8ea800a4 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -47,7 +47,7 @@ class PcdImageFile(ImageFile.ImageFile): self._mode = "RGB" self._size = 768, 512 # FIXME: not correct for rotated images! - self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] + self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048, None)] def load_end(self) -> None: if self.tile_post_rotate: diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index c3c9dd3f1..8445d5cc7 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -128,7 +128,9 @@ class PcxImageFile(ImageFile.ImageFile): bbox = (0, 0) + self.size logger.debug("size: %sx%s", *self.size) - self.tile = [("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))] + self.tile = [ + ImageFile._Tile("pcx", bbox, self.fp.tell(), (rawmode, planes * stride)) + ] # -------------------------------------------------------------------- diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 887b6568b..36f565f1c 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -61,7 +61,9 @@ class PixarImageFile(ImageFile.ImageFile): # FIXME: to be continued... # create tile descriptor (assuming "dumped") - self.tile = [("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))] + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1)) + ] # diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 7bdaa9fe7..e4d745e67 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -151,7 +151,9 @@ class PpmImageFile(ImageFile.ImageFile): decoder_name = "ppm" args = rawmode if decoder_name == "raw" else (rawmode, maxval) - self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)] + self.tile = [ + ImageFile._Tile(decoder_name, (0, 0) + self.size, self.fp.tell(), args) + ] # diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index cc99cd6d8..8ff5e3908 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -277,8 +277,8 @@ def _layerinfo( def _maketile( file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int -) -> list[ImageFile._Tile] | None: - tiles = None +) -> list[ImageFile._Tile]: + tiles = [] read = file.read compression = i16(read(2)) @@ -291,7 +291,6 @@ def _maketile( if compression == 0: # # raw compression - tiles = [] for channel in range(channels): layer = mode[channel] if mode == "CMYK": @@ -303,7 +302,6 @@ def _maketile( # # packbits compression i = 0 - tiles = [] bytecount = read(channels * ysize * 2) offset = file.tell() for channel in range(channels): diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index c81d381fb..eaf031402 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -32,7 +32,7 @@ class QoiImageFile(ImageFile.ImageFile): self._mode = "RGB" if channels == 3 else "RGBA" self.fp.seek(1, os.SEEK_CUR) # colorspace - self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)] + self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell(), None)] class QoiDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 50d979109..2c205a926 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -109,19 +109,28 @@ class SgiImageFile(ImageFile.ImageFile): pagesize = xsize * ysize * bpc if bpc == 2: self.tile = [ - ("SGI16", (0, 0) + self.size, headlen, (self.mode, 0, orientation)) + ImageFile._Tile( + "SGI16", + (0, 0) + self.size, + headlen, + (self.mode, 0, orientation), + ) ] else: self.tile = [] offset = headlen for layer in self.mode: self.tile.append( - ("raw", (0, 0) + self.size, offset, (layer, 0, orientation)) + ImageFile._Tile( + "raw", (0, 0) + self.size, offset, (layer, 0, orientation) + ) ) offset += pagesize elif compression == 1: self.tile = [ - ("sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc)) + ImageFile._Tile( + "sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc) + ) ] diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 7045ab566..075073f9f 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -154,7 +154,9 @@ class SpiderImageFile(ImageFile.ImageFile): self.rawmode = "F;32F" self._mode = "F" - self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))] + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1)) + ] self._fp = self.fp # FIXME: hack @property diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index 4e098474a..8912379ea 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -124,9 +124,13 @@ class SunImageFile(ImageFile.ImageFile): # (https://www.fileformat.info/format/sunraster/egff.htm) if file_type in (0, 1, 3, 4, 5): - self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride))] + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride)) + ] elif file_type == 2: - self.tile = [("sun_rle", (0, 0) + self.size, offset, rawmode)] + self.tile = [ + ImageFile._Tile("sun_rle", (0, 0) + self.size, offset, rawmode) + ] else: msg = "Unsupported Sun Raster file type" raise SyntaxError(msg) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 5aea363c0..90d5b5cf4 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -137,7 +137,7 @@ class TgaImageFile(ImageFile.ImageFile): if imagetype & 8: # compressed self.tile = [ - ( + ImageFile._Tile( "tga_rle", (0, 0) + self.size, self.fp.tell(), @@ -146,7 +146,7 @@ class TgaImageFile(ImageFile.ImageFile): ] else: self.tile = [ - ( + ImageFile._Tile( "raw", (0, 0) + self.size, self.fp.tell(), diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ab23356d4..d05c08489 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1298,7 +1298,7 @@ class TiffImageFile(ImageFile.ImageFile): # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) extents = self.tile[0][1] - args = list(self.tile[0][3]) + args = self.tile[0][3] # To be nice on memory footprint, if there's a # file descriptor, use that instead of reading @@ -1316,11 +1316,12 @@ class TiffImageFile(ImageFile.ImageFile): fp = False if fp: - args[2] = fp + assert isinstance(args, tuple) + args_list = list(args) + args_list[2] = fp + args = tuple(args_list) - decoder = Image._getdecoder( - self.mode, "libtiff", tuple(args), self.decoderconfig - ) + decoder = Image._getdecoder(self.mode, "libtiff", args, self.decoderconfig) try: decoder.setimage(self.im, extents) except ValueError as e: @@ -1538,7 +1539,7 @@ class TiffImageFile(ImageFile.ImageFile): # Offset in the tile tuple is 0, we go from 0,0 to # w,h, and we only do this once -- eds a = (rawmode, self._compression, False, self.tag_v2.offset) - self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a)) + self.tile.append(ImageFile._Tile("libtiff", (0, 0, xsize, ysize), 0, a)) elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: # striped image @@ -1571,7 +1572,7 @@ class TiffImageFile(ImageFile.ImageFile): args = (tile_rawmode, int(stride), 1) self.tile.append( - ( + ImageFile._Tile( self._compression, (x, y, min(x + w, xsize), min(y + h, ysize)), offset, diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index fc8f4fc2e..f8d6168ba 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -142,7 +142,7 @@ class WebPImageFile(ImageFile.ImageFile): if self.fp and self._exclusive_fp: self.fp.close() self.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.rawmode)] return super().load() diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index c84adaca2..5d1f201a4 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -73,7 +73,11 @@ class XVThumbImageFile(ImageFile.ImageFile): self.palette = ImagePalette.raw("RGB", PALETTE) - self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1))] + self.tile = [ + ImageFile._Tile( + "raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1) + ) + ] # -------------------------------------------------------------------- diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 6c2e32804..f3d490a84 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -67,7 +67,7 @@ class XbmImageFile(ImageFile.ImageFile): self._mode = "1" self._size = xsize, ysize - self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] + self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end(), None)] def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 8d56331e6..1fc6c0c39 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -101,7 +101,9 @@ class XpmImageFile(ImageFile.ImageFile): self._mode = "P" self.palette = ImagePalette.raw("RGB", b"".join(palette)) - self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1)) + ] def load_read(self, read_bytes: int) -> bytes: # From 8699f13a74f162552fa70644bfdbe9705f87f080 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Aug 2024 22:15:43 +1000 Subject: [PATCH 117/652] Require fp parameter --- src/PIL/ImageFile.py | 6 +++--- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index fce1591ef..69fbafd39 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -34,7 +34,7 @@ import itertools import os import struct import sys -from typing import IO, TYPE_CHECKING, Any, NamedTuple +from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast from . import Image from ._deprecate import deprecate @@ -111,7 +111,7 @@ class ImageFile(Image.Image): """Base class for image file format handlers.""" def __init__( - self, fp: StrOrBytesPath | IO[bytes] | None = None, filename: str | bytes | None = None + self, fp: StrOrBytesPath | IO[bytes], filename: str | bytes | None = None ) -> None: super().__init__() @@ -134,7 +134,7 @@ class ImageFile(Image.Image): self._exclusive_fp = True else: # stream - self.fp = fp + self.fp = cast(IO[bytes], fp) self.filename = filename if filename is not None else "" # can be overridden self._exclusive_fp = False diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 1f54b7e92..e3372a1c2 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -848,7 +848,7 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ## # Factory for making JPEG and MPO instances def jpeg_factory( - fp: IO[bytes] | None = None, filename: str | bytes | None = None + fp: IO[bytes], filename: str | bytes | None = None ) -> JpegImageFile | MpoImageFile: im = JpegImageFile(fp, filename) try: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index d05c08489..ad898dc8e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1126,7 +1126,7 @@ class TiffImageFile(ImageFile.ImageFile): def __init__( self, - fp: StrOrBytesPath | IO[bytes] | None = None, + fp: StrOrBytesPath | IO[bytes], filename: str | bytes | None = None, ) -> None: self.tag_v2: ImageFileDirectory_v2 From cdadf931e36cb3816096197ce91abfbe6b11df92 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Fri, 30 Aug 2024 20:36:22 +0000 Subject: [PATCH 118/652] Improve error messages --- Tests/test_imagefont.py | 27 +++++++++++++++++++++++++++ src/PIL/ImageFont.py | 19 +++++++++++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 340cc4742..e2a7b759a 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -460,6 +460,17 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: assert mask.size == (108, 13) +def test_load_raises_if_image_not_found(tmp_path) -> None: + font_path = tmp_path / "file.font" + font_path.write_bytes(b"") + with pytest.raises(OSError) as excinfo: + ImageFont.load(font_path) + + pre = tmp_path / "file" + msg = f"cannot find glyph data file {pre}.{{png|gif|pbm}}" + assert msg in str(excinfo.value) + + def test_load_path_not_found() -> None: # Arrange filename = "somefilenamethatdoesntexist.ttf" @@ -471,6 +482,22 @@ def test_load_path_not_found() -> None: ImageFont.truetype(filename) +def test_load_path_exisitng_path(tmp_path) -> None: + # First, the file doens't exist, so we don't suggest `load` + some_path = tmp_path / "file.ttf" + with pytest.raises(OSError) as excinfo: + ImageFont.load_path(str(some_path)) + assert str(some_path) in str(excinfo.value) + assert "did you mean" not in str(excinfo.value) + + # The file exists, so the error message suggests to use `load` instead + some_path.write_bytes(b"") + with pytest.raises(OSError) as excinfo: + ImageFont.load_path(str(some_path)) + assert str(some_path) in str(excinfo.value) + assert " did you mean" in str(excinfo.value) + + def test_load_non_font_bytes() -> None: with open("Tests/images/hopper.jpg", "rb") as f: with pytest.raises(OSError): diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 2ab65bfef..072acc31f 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -98,11 +98,13 @@ class ImageFont: def _load_pilfont(self, filename: str) -> None: with open(filename, "rb") as fp: image: ImageFile.ImageFile | None = None + filename_body = os.path.splitext(filename)[0] + for ext in (".png", ".gif", ".pbm"): if image: image.close() try: - fullname = os.path.splitext(filename)[0] + ext + fullname = filename_body + ext image = Image.open(fullname) except Exception: pass @@ -112,7 +114,9 @@ class ImageFont: else: if image: image.close() - msg = "cannot find glyph data file" + + pre = filename_body + msg = f"cannot find glyph data file {pre}.{{png|gif|pbm}}" raise OSError(msg) self.file = fullname @@ -224,7 +228,7 @@ class FreeTypeFont: raise core.ex if size <= 0: - msg = "font size must be greater than 0" + msg = f"font size must be greater than 0, not {size}" raise ValueError(msg) self.path = font @@ -774,6 +778,8 @@ def load(filename: str) -> ImageFont: :param filename: Name of font file. :return: A font object. :exception OSError: If the file could not be read. + + .. seealso:: :py:func:`PIL.ImageFont.truetype` """ f = ImageFont() f._load_pilfont(filename) @@ -850,6 +856,8 @@ def truetype( :return: A font object. :exception OSError: If the file could not be read. :exception ValueError: If the font size is not greater than zero. + + .. seealso:: :py:func:`PIL.ImageFont.load` """ def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: @@ -927,7 +935,10 @@ def load_path(filename: str | bytes) -> ImageFont: return load(os.path.join(directory, filename)) except OSError: pass - msg = "cannot find font file" + msg = f"cannot find font file '{filename}' in `sys.path`" + if os.path.exists(filename): + msg += f" did you mean `ImageFont.load({filename})` instead?" + raise OSError(msg) From e47b1810841be44ca90327259a01ce9d9a71e5ad Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Aug 2024 18:48:16 +1000 Subject: [PATCH 119/652] Added type hints --- .ci/requirements-mypy.txt | 1 + Tests/oss-fuzz/fuzz_font.py | 3 +- Tests/oss-fuzz/fuzz_pillow.py | 3 +- docs/reference/internal_modules.rst | 4 +++ pyproject.toml | 4 --- src/PIL/BmpImagePlugin.py | 2 +- src/PIL/Image.py | 7 ++++- src/PIL/ImageFile.py | 7 +++-- src/PIL/ImageFont.py | 6 ++-- src/PIL/Jpeg2KImagePlugin.py | 10 ++++-- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 4 ++- src/PIL/TiffImagePlugin.py | 48 +++++++++++++++++++---------- src/PIL/_typing.py | 4 ++- 14 files changed, 70 insertions(+), 35 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 5b09ef64c..dcb3996e2 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -6,6 +6,7 @@ numpy packaging pytest sphinx +types-atheris types-defusedxml types-olefile types-setuptools diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index 8788d7021..f4e40ea36 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -16,8 +16,9 @@ import atheris +from atheris.import_hook import instrument_imports -with atheris.instrument_imports(): +with instrument_imports(): import sys import fuzzers diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index 9137391b6..d58a6f015 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -14,8 +14,9 @@ import atheris +from atheris.import_hook import instrument_imports -with atheris.instrument_imports(): +with instrument_imports(): import sys import fuzzers diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 2fb4ff8c0..31d60cd83 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -33,6 +33,10 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. +.. py:class:: IntegralLike + + Typing alias. + .. py:class:: NumpyArray Typing alias. diff --git a/pyproject.toml b/pyproject.toml index e4ae73acf..4042bd9ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -163,7 +163,3 @@ follow_imports = "silent" warn_redundant_casts = true warn_unreachable = true warn_unused_ignores = true -exclude = [ - '^Tests/oss-fuzz/fuzz_font.py$', - '^Tests/oss-fuzz/fuzz_pillow.py$', -] diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index de441c6b5..707fb2192 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -387,7 +387,7 @@ class BmpRleDecoder(ImageFile.PyDecoder): if self.fd.tell() % 2 != 0: self.fd.seek(1, os.SEEK_CUR) rawmode = "L" if self.mode == "L" else "P" - self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1])) + self.set_as_raw(bytes(data), rawmode, (0, self.args[-1])) return -1, 0 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 95b4c64ee..d27328a83 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -225,6 +225,11 @@ if TYPE_CHECKING: from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard + + if sys.version_info >= (3, 13): + from types import CapsuleType + else: + CapsuleType = object ID: list[str] = [] OPEN: dict[ str, @@ -1598,7 +1603,7 @@ class Image: self.fp.seek(offset) return child_images - def getim(self): + def getim(self) -> CapsuleType: """ Returns a capsule that points to the internal image memory. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index a1c33f219..0ecc9053c 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -736,19 +736,22 @@ class PyDecoder(PyCodec): msg = "unavailable in base decoder" raise NotImplementedError(msg) - def set_as_raw(self, data: bytes, rawmode=None) -> None: + def set_as_raw( + self, data: bytes, rawmode: str | None = None, extra: tuple[Any, ...] = () + ) -> None: """ Convenience method to set the internal image from a stream of raw data :param data: Bytes to be set :param rawmode: The rawmode to be used for the decoder. If not specified, it will default to the mode of the image + :param extra: Extra arguments for the decoder. :returns: None """ if not rawmode: rawmode = self.mode - d = Image._getdecoder(self.mode, "raw", rawmode) + d = Image._getdecoder(self.mode, "raw", rawmode, extra) assert self.im is not None d.setimage(self.im, self.state.extents()) s = d.decode(data) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 2ab65bfef..acc7ca1d2 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,7 +34,7 @@ import warnings from enum import IntEnum from io import BytesIO from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict +from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast from . import Image from ._typing import StrOrBytesPath @@ -245,7 +245,7 @@ class FreeTypeFont: self.layout_engine = layout_engine - def load_from_bytes(f) -> None: + def load_from_bytes(f: IO[bytes]) -> None: self.font_bytes = f.read() self.font = core.getfont( "", size, index, encoding, self.font_bytes, layout_engine @@ -267,7 +267,7 @@ class FreeTypeFont: font, size, index, encoding, layout_engine=layout_engine ) else: - load_from_bytes(font) + load_from_bytes(cast(IO[bytes], font)) def __getstate__(self) -> list[Any]: return [self.path, self.size, self.index, self.encoding, self.layout_engine] diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 02c2e48cf..5c88ef0bb 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -18,6 +18,7 @@ from __future__ import annotations import io import os import struct +from collections.abc import Callable from typing import IO, cast from . import Image, ImageFile, ImagePalette, _binary @@ -316,8 +317,13 @@ class Jpeg2KImageFile(ImageFile.ImageFile): else: self.fp.seek(length - 2, os.SEEK_CUR) - @property - def reduce(self): + @property # type: ignore[override] + def reduce( + self, + ) -> ( + Callable[[int | tuple[int, int], tuple[int, int, int, int] | None], Image.Image] + | int + ): # https://github.com/python-pillow/Pillow/issues/4343 found that the # new Image 'reduce' method was shadowed by this plugin's 'reduce' # property. This attempts to allow for both scenarios diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 40e5fa435..987d20c13 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -152,7 +152,7 @@ class MspDecoder(ImageFile.PyDecoder): msg = f"Corrupted MSP file in row {x}" raise OSError(msg) from e - self.set_as_raw(img.getvalue(), ("1", 0, 1)) + self.set_as_raw(img.getvalue(), "1") return -1, 0 diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index dc7face2f..a87e1a625 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -258,7 +258,9 @@ class iTXt(str): tkey: str | bytes | None @staticmethod - def __new__(cls, text, lang=None, tkey=None): + def __new__( + cls, text: str, lang: str | None = None, tkey: str | None = None + ) -> iTXt: """ :param cls: the class to use when creating the instance :param text: value for this key diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ab23356d4..08d598f77 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -61,6 +61,9 @@ from ._typing import StrOrBytesPath from ._util import is_path from .TiffTags import TYPES +if TYPE_CHECKING: + from ._typing import IntegralLike + logger = logging.getLogger(__name__) # Set these to true to force use of libtiff for reading or writing. @@ -291,22 +294,24 @@ def _accept(prefix: bytes) -> bool: def _limit_rational( val: float | Fraction | IFDRational, max_val: int -) -> tuple[float, float]: +) -> tuple[IntegralLike, IntegralLike]: inv = abs(float(val)) > 1 n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) return n_d[::-1] if inv else n_d -def _limit_signed_rational(val, max_val, min_val): +def _limit_signed_rational( + val: IFDRational, max_val: int, min_val: int +) -> tuple[IntegralLike, IntegralLike]: frac = Fraction(val) - n_d = frac.numerator, frac.denominator + n_d: tuple[IntegralLike, IntegralLike] = frac.numerator, frac.denominator - if min(n_d) < min_val: + if min(float(i) for i in n_d) < min_val: n_d = _limit_rational(val, abs(min_val)) - if max(n_d) > max_val: - val = Fraction(*n_d) - n_d = _limit_rational(val, max_val) + n_d_float = tuple(float(i) for i in n_d) + if max(n_d_float) > max_val: + n_d = _limit_rational(n_d_float[0] / n_d_float[1], max_val) return n_d @@ -318,8 +323,10 @@ _load_dispatch = {} _write_dispatch = {} -def _delegate(op: str): - def delegate(self, *args): +def _delegate(op: str) -> Any: + def delegate( + self: IFDRational, *args: tuple[float, ...] + ) -> bool | float | Fraction: return getattr(self._val, op)(*args) return delegate @@ -358,7 +365,10 @@ class IFDRational(Rational): self._numerator = value.numerator self._denominator = value.denominator else: - self._numerator = value + if TYPE_CHECKING: + self._numerator = cast(IntegralLike, value) + else: + self._numerator = value self._denominator = denominator if denominator == 0: @@ -371,14 +381,14 @@ class IFDRational(Rational): self._val = Fraction(value / denominator) @property - def numerator(self): + def numerator(self) -> IntegralLike: return self._numerator @property def denominator(self) -> int: return self._denominator - def limit_rational(self, max_denominator: int) -> tuple[float, int]: + def limit_rational(self, max_denominator: int) -> tuple[IntegralLike, int]: """ :param max_denominator: Integer, the maximum denominator value @@ -406,14 +416,18 @@ class IFDRational(Rational): val = float(val) return val == other - def __getstate__(self) -> list[float | Fraction]: + def __getstate__(self) -> list[float | Fraction | IntegralLike]: return [self._val, self._numerator, self._denominator] - def __setstate__(self, state: list[float | Fraction]) -> None: + def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None: IFDRational.__init__(self, 0) _val, _numerator, _denominator = state + assert isinstance(_val, (float, Fraction)) self._val = _val - self._numerator = _numerator + if TYPE_CHECKING: + self._numerator = cast(IntegralLike, _numerator) + else: + self._numerator = _numerator assert isinstance(_denominator, int) self._denominator = _denominator @@ -471,8 +485,8 @@ def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc return decorator -def _register_writer(idx: int): - def decorator(func): +def _register_writer(idx: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: _write_dispatch[idx] = func # noqa: F821 return func diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index b6bb8d89a..093778464 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -6,6 +6,8 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union if TYPE_CHECKING: + from numbers import _IntegralLike as IntegralLike + try: import numpy.typing as npt @@ -38,4 +40,4 @@ class SupportsRead(Protocol[_T_co]): StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] -__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] +__all__ = ["IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] From 95194a2050081169f0b7db02040da91b1994da5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Aug 2024 20:51:26 +1000 Subject: [PATCH 120/652] Use tempfile.NamedTemporaryFile --- Tests/test_imagefont.py | 43 ++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index e2a7b759a..80cd0d578 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -5,6 +5,7 @@ import os import re import shutil import sys +import tempfile from io import BytesIO from pathlib import Path from typing import Any, BinaryIO @@ -460,15 +461,15 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: assert mask.size == (108, 13) -def test_load_raises_if_image_not_found(tmp_path) -> None: - font_path = tmp_path / "file.font" - font_path.write_bytes(b"") - with pytest.raises(OSError) as excinfo: - ImageFont.load(font_path) +def test_load_when_image_not_found(tmp_path: Path) -> None: + tmpfile = tmp_path / "file.font" + tmpfile.write_bytes(b"") + tempfile = str(tmpfile) + with pytest.raises(OSError) as e: + ImageFont.load(tempfile) - pre = tmp_path / "file" - msg = f"cannot find glyph data file {pre}.{{png|gif|pbm}}" - assert msg in str(excinfo.value) + root = os.path.splitext(tempfile)[0] + assert str(e.value) == f"cannot find glyph data file {root}.{{png|gif|pbm}}" def test_load_path_not_found() -> None: @@ -476,26 +477,24 @@ def test_load_path_not_found() -> None: filename = "somefilenamethatdoesntexist.ttf" # Act/Assert - with pytest.raises(OSError): + with pytest.raises(OSError) as e: ImageFont.load_path(filename) + + # The file doesn't exist, so don't suggest `load` + assert filename in str(e.value) + assert "did you mean" not in str(e.value) with pytest.raises(OSError): ImageFont.truetype(filename) -def test_load_path_exisitng_path(tmp_path) -> None: - # First, the file doens't exist, so we don't suggest `load` - some_path = tmp_path / "file.ttf" - with pytest.raises(OSError) as excinfo: - ImageFont.load_path(str(some_path)) - assert str(some_path) in str(excinfo.value) - assert "did you mean" not in str(excinfo.value) +def test_load_path_existing_path() -> None: + with tempfile.NamedTemporaryFile() as tmp: + with pytest.raises(OSError) as e: + ImageFont.load_path(tmp.name) - # The file exists, so the error message suggests to use `load` instead - some_path.write_bytes(b"") - with pytest.raises(OSError) as excinfo: - ImageFont.load_path(str(some_path)) - assert str(some_path) in str(excinfo.value) - assert " did you mean" in str(excinfo.value) + # The file exists, so the error message suggests to use `load` instead + assert tmp.name in str(e.value) + assert " did you mean" in str(e.value) def test_load_non_font_bytes() -> None: From e0a75b6d695b7be82c090adb1896022e42d344ff Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Aug 2024 19:43:35 +1000 Subject: [PATCH 121/652] Renamed variable for first part of splitext to root --- src/PIL/ImageFont.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 072acc31f..43e86a7ed 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -98,13 +98,13 @@ class ImageFont: def _load_pilfont(self, filename: str) -> None: with open(filename, "rb") as fp: image: ImageFile.ImageFile | None = None - filename_body = os.path.splitext(filename)[0] + root = os.path.splitext(filename)[0] for ext in (".png", ".gif", ".pbm"): if image: image.close() try: - fullname = filename_body + ext + fullname = root + ext image = Image.open(fullname) except Exception: pass @@ -115,8 +115,7 @@ class ImageFont: if image: image.close() - pre = filename_body - msg = f"cannot find glyph data file {pre}.{{png|gif|pbm}}" + msg = f"cannot find glyph data file {root}.{{png|gif|pbm}}" raise OSError(msg) self.file = fullname @@ -937,7 +936,7 @@ def load_path(filename: str | bytes) -> ImageFont: pass msg = f"cannot find font file '{filename}' in `sys.path`" if os.path.exists(filename): - msg += f" did you mean `ImageFont.load({filename})` instead?" + msg += f", did you mean `ImageFont.load({filename})` instead?" raise OSError(msg) From fcca8a3059f94640dee493164b54ec466c13ff74 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Sat, 31 Aug 2024 19:03:11 +0000 Subject: [PATCH 122/652] Fix accidental indent --- Tests/test_imagefont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index e2a7b759a..80ec7bd6b 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -487,8 +487,8 @@ def test_load_path_exisitng_path(tmp_path) -> None: some_path = tmp_path / "file.ttf" with pytest.raises(OSError) as excinfo: ImageFont.load_path(str(some_path)) - assert str(some_path) in str(excinfo.value) - assert "did you mean" not in str(excinfo.value) + assert str(some_path) in str(excinfo.value) + assert "did you mean" not in str(excinfo.value) # The file exists, so the error message suggests to use `load` instead some_path.write_bytes(b"") From ef51e7a1c70ae684615724750c32ba3ca6923123 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Sun, 1 Sep 2024 19:49:44 +0000 Subject: [PATCH 123/652] Fix wrong indentation for assert --- Tests/test_imagefont.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 80cd0d578..66e6947c2 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -480,9 +480,9 @@ def test_load_path_not_found() -> None: with pytest.raises(OSError) as e: ImageFont.load_path(filename) - # The file doesn't exist, so don't suggest `load` - assert filename in str(e.value) - assert "did you mean" not in str(e.value) + # The file doesn't exist, so don't suggest `load` + assert filename in str(e.value) + assert "did you mean" not in str(e.value) with pytest.raises(OSError): ImageFont.truetype(filename) From 2303c063cb9660c8fdd4a5d757e1d971cd47dcf8 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 01:24:51 +0400 Subject: [PATCH 124/652] Use PyCapsule in _imagingmath --- src/PIL/ImageMath.py | 4 +- src/_imagingmath.c | 145 +++++++++++++++++++++++++++---------------- 2 files changed, 94 insertions(+), 55 deletions(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 191cc2a5f..b32742657 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -65,7 +65,7 @@ class _Operand: except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.unop(op, out.im.id, im_1.im.id) + _imagingmath.unop(op, out.im.ptr, im_1.im.ptr) else: # binary operation im_2 = self.__fixup(im2) @@ -93,7 +93,7 @@ class _Operand: except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) + _imagingmath.binop(op, out.im.ptr, im_1.im.ptr, im_2.im.ptr) return _Operand(out) # unary operators diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 550a10903..eafcf1444 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -23,6 +23,9 @@ #define MAX_INT32 2147483647.0 #define MIN_INT32 -2147483648.0 +#define MATH_FUNC_UNOP_MAGIC "Pillow Math unary func" +#define MATH_FUNC_BINOP_MAGIC "Pillow Math binary func" + #define UNOP(name, op, type) \ void name(Imaging out, Imaging im1) { \ int x, y; \ @@ -168,15 +171,28 @@ _unop(PyObject *self, PyObject *args) { Imaging im1; void (*unop)(Imaging, Imaging); - Py_ssize_t op, i0, i1; - if (!PyArg_ParseTuple(args, "nnn", &op, &i0, &i1)) { + PyObject *op, *i0, *i1; + if (!PyArg_ParseTuple(args, "OOO", &op, &i0, &i1)) { return NULL; } - out = (Imaging)i0; - im1 = (Imaging)i1; + if (!PyCapsule_IsValid(op, MATH_FUNC_UNOP_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", MATH_FUNC_UNOP_MAGIC + ); + return NULL; + } + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } - unop = (void *)op; + unop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_UNOP_MAGIC); + out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); unop(out, im1); @@ -191,16 +207,30 @@ _binop(PyObject *self, PyObject *args) { Imaging im2; void (*binop)(Imaging, Imaging, Imaging); - Py_ssize_t op, i0, i1, i2; - if (!PyArg_ParseTuple(args, "nnnn", &op, &i0, &i1, &i2)) { + PyObject *op, *i0, *i1, *i2; + if (!PyArg_ParseTuple(args, "OOOO", &op, &i0, &i1, &i2)) { return NULL; } - out = (Imaging)i0; - im1 = (Imaging)i1; - im2 = (Imaging)i2; + if (!PyCapsule_IsValid(op, MATH_FUNC_BINOP_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", MATH_FUNC_BINOP_MAGIC + ); + return NULL; + } + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC) || + !PyCapsule_IsValid(i2, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } - binop = (void *)op; + binop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_BINOP_MAGIC); + out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + im2 = (Imaging)PyCapsule_GetPointer(i2, IMAGING_MAGIC); binop(out, im1, im2); @@ -213,8 +243,17 @@ static PyMethodDef _functions[] = { }; static void -install(PyObject *d, char *name, void *value) { - PyObject *v = PyLong_FromSsize_t((Py_ssize_t)value); +install_unary(PyObject *d, char *name, void *func) { + PyObject *v = PyCapsule_New(func, MATH_FUNC_UNOP_MAGIC, NULL); + if (!v || PyDict_SetItemString(d, name, v)) { + PyErr_Clear(); + } + Py_XDECREF(v); +} + +static void +install_binary(PyObject *d, char *name, void *func) { + PyObject *v = PyCapsule_New(func, MATH_FUNC_BINOP_MAGIC, NULL); if (!v || PyDict_SetItemString(d, name, v)) { PyErr_Clear(); } @@ -225,50 +264,50 @@ static int setup_module(PyObject *m) { PyObject *d = PyModule_GetDict(m); - install(d, "abs_I", abs_I); - install(d, "neg_I", neg_I); - install(d, "add_I", add_I); - install(d, "sub_I", sub_I); - install(d, "diff_I", diff_I); - install(d, "mul_I", mul_I); - install(d, "div_I", div_I); - install(d, "mod_I", mod_I); - install(d, "min_I", min_I); - install(d, "max_I", max_I); - install(d, "pow_I", pow_I); + install_unary(d, "abs_I", abs_I); + install_unary(d, "neg_I", neg_I); + install_binary(d, "add_I", add_I); + install_binary(d, "sub_I", sub_I); + install_binary(d, "diff_I", diff_I); + install_binary(d, "mul_I", mul_I); + install_binary(d, "div_I", div_I); + install_binary(d, "mod_I", mod_I); + install_binary(d, "min_I", min_I); + install_binary(d, "max_I", max_I); + install_binary(d, "pow_I", pow_I); - install(d, "invert_I", invert_I); - install(d, "and_I", and_I); - install(d, "or_I", or_I); - install(d, "xor_I", xor_I); - install(d, "lshift_I", lshift_I); - install(d, "rshift_I", rshift_I); + install_unary(d, "invert_I", invert_I); + install_binary(d, "and_I", and_I); + install_binary(d, "or_I", or_I); + install_binary(d, "xor_I", xor_I); + install_binary(d, "lshift_I", lshift_I); + install_binary(d, "rshift_I", rshift_I); - install(d, "eq_I", eq_I); - install(d, "ne_I", ne_I); - install(d, "lt_I", lt_I); - install(d, "le_I", le_I); - install(d, "gt_I", gt_I); - install(d, "ge_I", ge_I); + install_binary(d, "eq_I", eq_I); + install_binary(d, "ne_I", ne_I); + install_binary(d, "lt_I", lt_I); + install_binary(d, "le_I", le_I); + install_binary(d, "gt_I", gt_I); + install_binary(d, "ge_I", ge_I); - install(d, "abs_F", abs_F); - install(d, "neg_F", neg_F); - install(d, "add_F", add_F); - install(d, "sub_F", sub_F); - install(d, "diff_F", diff_F); - install(d, "mul_F", mul_F); - install(d, "div_F", div_F); - install(d, "mod_F", mod_F); - install(d, "min_F", min_F); - install(d, "max_F", max_F); - install(d, "pow_F", pow_F); + install_unary(d, "abs_F", abs_F); + install_unary(d, "neg_F", neg_F); + install_binary(d, "add_F", add_F); + install_binary(d, "sub_F", sub_F); + install_binary(d, "diff_F", diff_F); + install_binary(d, "mul_F", mul_F); + install_binary(d, "div_F", div_F); + install_binary(d, "mod_F", mod_F); + install_binary(d, "min_F", min_F); + install_binary(d, "max_F", max_F); + install_binary(d, "pow_F", pow_F); - install(d, "eq_F", eq_F); - install(d, "ne_F", ne_F); - install(d, "lt_F", lt_F); - install(d, "le_F", le_F); - install(d, "gt_F", gt_F); - install(d, "ge_F", ge_F); + install_binary(d, "eq_F", eq_F); + install_binary(d, "ne_F", ne_F); + install_binary(d, "lt_F", lt_F); + install_binary(d, "le_F", le_F); + install_binary(d, "gt_F", gt_F); + install_binary(d, "ge_F", ge_F); return 0; } From a9798e78af90674d2983c749c2bb63af9a9e5a6f Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 01:32:28 +0400 Subject: [PATCH 125/652] Use PyCapsule in _imagingcms --- src/PIL/ImageCms.py | 4 ++-- src/_imagingcms.c | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index ec10230f1..5e256b0ab 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -352,7 +352,7 @@ class ImageCmsTransform(Image.ImagePointHandler): im.load() if imOut is None: imOut = Image.new(self.output_mode, im.size, None) - self.transform.apply(im.im.id, imOut.im.id) + self.transform.apply(im.im.ptr, imOut.im.ptr) imOut.info["icc_profile"] = self.output_profile.tobytes() return imOut @@ -361,7 +361,7 @@ class ImageCmsTransform(Image.ImagePointHandler): if im.mode != self.output_mode: msg = "mode mismatch" raise ValueError(msg) # wrong output mode - self.transform.apply(im.im.id, im.im.id) + self.transform.apply(im.im.ptr, im.im.ptr) im.info["icc_profile"] = self.output_profile.tobytes() return im diff --git a/src/_imagingcms.c b/src/_imagingcms.c index bafe787a7..444b6a602 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -531,23 +531,26 @@ buildProofTransform(PyObject *self, PyObject *args) { static PyObject * cms_transform_apply(CmsTransformObject *self, PyObject *args) { - Py_ssize_t idIn; - Py_ssize_t idOut; + PyObject *i0, *i1; Imaging im; Imaging imOut; - int result; - - if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) { + if (!PyArg_ParseTuple(args, "OO:apply", &i0, &i1)) { return NULL; } - im = (Imaging)idIn; - imOut = (Imaging)idOut; + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } - result = pyCMSdoTransform(im, imOut, self->transform); + im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + imOut = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); - return Py_BuildValue("i", result); + return Py_BuildValue("i", pyCMSdoTransform(im, imOut, self->transform)); } /* -------------------------------------------------------------------- */ From 2fcab263d3e3aae6156a18a0180e00954f032153 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 01:43:38 +0400 Subject: [PATCH 126/652] Use PyCapsule in _imagingmorph --- Tests/test_imagemorph.py | 8 +++---- src/PIL/ImageMorph.py | 6 ++--- src/_imagingmorph.c | 48 ++++++++++++++++++++++++++++++---------- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 80d8c3815..04860fd61 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -328,13 +328,13 @@ def test_wrong_mode() -> None: iml = Image.new("L", (10, 10)) with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) + _imagingmorph.apply(bytes(lut), imrgb.im.ptr, iml.im.ptr) with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) + _imagingmorph.apply(bytes(lut), iml.im.ptr, imrgb.im.ptr) with pytest.raises(RuntimeError): - _imagingmorph.match(bytes(lut), imrgb.im.id) + _imagingmorph.match(bytes(lut), imrgb.im.ptr) # Should not raise - _imagingmorph.match(bytes(lut), iml.im.id) + _imagingmorph.match(bytes(lut), iml.im.ptr) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 6a43983d3..bc7ed66d3 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -213,7 +213,7 @@ class MorphOp: msg = "Image mode must be L" raise ValueError(msg) outimage = Image.new(image.mode, image.size, None) - count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) + count = _imagingmorph.apply(bytes(self.lut), image.im.ptr, outimage.im.ptr) return count, outimage def match(self, image: Image.Image) -> list[tuple[int, int]]: @@ -229,7 +229,7 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) - return _imagingmorph.match(bytes(self.lut), image.im.id) + return _imagingmorph.match(bytes(self.lut), image.im.ptr) def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: """Get a list of all turned on pixels in a binary image @@ -240,7 +240,7 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) - return _imagingmorph.get_on_pixels(image.im.id) + return _imagingmorph.get_on_pixels(image.im.ptr) def load_lut(self, filename: str) -> None: """Load an operator from an mrl file""" diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 614dfbe7f..c7933cc7d 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -30,15 +30,15 @@ static PyObject * apply(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut; - Py_ssize_t lut_len, i0, i1; + PyObject *py_lut, *i0, *i1; + Py_ssize_t lut_len; Imaging imgin, imgout; int width, height; int row_idx, col_idx; UINT8 **inrows, **outrows; int num_changed_pixels = 0; - if (!PyArg_ParseTuple(args, "Onn", &py_lut, &i0, &i1)) { + if (!PyArg_ParseTuple(args, "OOO", &py_lut, &i0, &i1)) { PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } @@ -57,8 +57,16 @@ apply(PyObject *self, PyObject *args) { lut = PyBytes_AsString(py_lut); - imgin = (Imaging)i0; - imgout = (Imaging)i1; + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } + + imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + imgout = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); width = imgin->xsize; height = imgin->ysize; @@ -129,8 +137,8 @@ apply(PyObject *self, PyObject *args) { static PyObject * match(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut; - Py_ssize_t lut_len, i0; + PyObject *py_lut, *i0; + Py_ssize_t lut_len; Imaging imgin; int width, height; int row_idx, col_idx; @@ -140,7 +148,7 @@ match(PyObject *self, PyObject *args) { return NULL; } - if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) { + if (!PyArg_ParseTuple(args, "OO", &py_lut, &i0)) { Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; @@ -161,7 +169,15 @@ match(PyObject *self, PyObject *args) { } lut = PyBytes_AsString(py_lut); - imgin = (Imaging)i0; + + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } + + imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { Py_DECREF(ret); @@ -215,7 +231,7 @@ match(PyObject *self, PyObject *args) { */ static PyObject * get_on_pixels(PyObject *self, PyObject *args) { - Py_ssize_t i0; + PyObject *i0; Imaging img; UINT8 **rows; int row_idx, col_idx; @@ -225,12 +241,20 @@ get_on_pixels(PyObject *self, PyObject *args) { return NULL; } - if (!PyArg_ParseTuple(args, "n", &i0)) { + if (!PyArg_ParseTuple(args, "O", &i0)) { Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } - img = (Imaging)i0; + + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } + + img = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); rows = img->image8; width = img->xsize; height = img->ysize; From f246be7b13867d6c85f01e38b5d14b093ace66c7 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 02:00:07 +0400 Subject: [PATCH 127/652] Use s# in PyArg_ParseTuple --- src/_imagingmorph.c | 55 ++++++++++--------------------------- src/libImaging/ImPlatform.h | 1 + 2 files changed, 16 insertions(+), 40 deletions(-) diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index c7933cc7d..715cccf51 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -11,7 +11,6 @@ * See the README file for information on usage and redistribution. */ -#include "Python.h" #include "libImaging/Imaging.h" #define LUT_SIZE (1 << 9) @@ -30,7 +29,7 @@ static PyObject * apply(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut, *i0, *i1; + PyObject *i0, *i1; Py_ssize_t lut_len; Imaging imgin, imgout; int width, height; @@ -38,25 +37,15 @@ apply(PyObject *self, PyObject *args) { UINT8 **inrows, **outrows; int num_changed_pixels = 0; - if (!PyArg_ParseTuple(args, "OOO", &py_lut, &i0, &i1)) { - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); + if (!PyArg_ParseTuple(args, "s#OO", &lut, &lut_len, &i0, &i1)) { return NULL; } - if (!PyBytes_Check(py_lut)) { - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); - return NULL; - } - - lut_len = PyBytes_Size(py_lut); - if (lut_len < LUT_SIZE) { PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } - lut = PyBytes_AsString(py_lut); - if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { PyErr_Format( @@ -137,39 +126,22 @@ apply(PyObject *self, PyObject *args) { static PyObject * match(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut, *i0; + PyObject *i0; Py_ssize_t lut_len; Imaging imgin; int width, height; int row_idx, col_idx; UINT8 **inrows; - PyObject *ret = PyList_New(0); - if (ret == NULL) { + + if (!PyArg_ParseTuple(args, "s#O", &lut, &lut_len, &i0)) { return NULL; } - if (!PyArg_ParseTuple(args, "OO", &py_lut, &i0)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); - return NULL; - } - - if (!PyBytes_Check(py_lut)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); - return NULL; - } - - lut_len = PyBytes_Size(py_lut); - if (lut_len < LUT_SIZE) { - Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } - lut = PyBytes_AsString(py_lut); - if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { PyErr_Format( PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC @@ -180,11 +152,15 @@ match(PyObject *self, PyObject *args) { imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { - Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } + PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } + inrows = imgin->image8; width = imgin->xsize; height = imgin->ysize; @@ -236,14 +212,8 @@ get_on_pixels(PyObject *self, PyObject *args) { UINT8 **rows; int row_idx, col_idx; int width, height; - PyObject *ret = PyList_New(0); - if (ret == NULL) { - return NULL; - } if (!PyArg_ParseTuple(args, "O", &i0)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } @@ -259,6 +229,11 @@ get_on_pixels(PyObject *self, PyObject *args) { width = img->xsize; height = img->ysize; + PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } + for (row_idx = 0; row_idx < height; row_idx++) { UINT8 *row = rows[row_idx]; for (col_idx = 0; col_idx < width; col_idx++) { diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index f6e7fb6b9..c9b7e43b4 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -7,6 +7,7 @@ * Copyright (c) Fredrik Lundh 1995-2003. */ +#define PY_SSIZE_T_CLEAN #include "Python.h" /* Check that we have an ANSI compliant compiler */ From 7435a06cbbd0dd98319f8c55752794eb6f11aa17 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 02:19:44 +0400 Subject: [PATCH 128/652] Deprecate ImageCore.id and ImageCore.unsafe_ptrs --- Tests/test_image_getim.py | 12 ++++++++++-- docs/deprecations.rst | 10 ++++++++++ src/_imaging.c | 14 ++++++++++++++ src/libImaging/Imaging.h | 2 +- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 9afa02b0a..75d97cd44 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,11 +1,19 @@ from __future__ import annotations +import pytest + from .helper import hopper def test_sanity() -> None: im = hopper() - type_repr = repr(type(im.getim())) + type_repr = repr(type(im.getim())) assert "PyCapsule" in type_repr - assert isinstance(im.im.id, int) + + with pytest.warns(DeprecationWarning): + assert isinstance(im.im.id, int) + + with pytest.warns(DeprecationWarning): + ptrs = dict(im.im.unsafe_ptrs) + assert all(k in ptrs for k in ["image8", "image32", "image"]) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a9498d5ed..5bb0ebf58 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -136,6 +136,16 @@ Specific WebP Feature Checks ``True`` if the WebP module is installed, until they are removed in Pillow 12.0.0 (2025-10-15). +Get Internal Pointers to Objects +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``Image.core.ImageCore.id`` and ``Image.core.ImageCore.unsafe_ptrs`` +have been deprecated and will be removed in Pillow 12 (2025-10-15). +They were used for obtaining raw pointers to ``ImageCore`` internals. To interact with +C code, you can use ``Image.core.ImageCore.ptr``, which returns a ``PyCapsule`` object. + Removed features ---------------- diff --git a/src/_imaging.c b/src/_imaging.c index 07d9a64cc..a455c873b 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3707,6 +3707,13 @@ _getattr_bands(ImagingObject *self, void *closure) { static PyObject * _getattr_id(ImagingObject *self, void *closure) { + if (PyErr_WarnEx( + PyExc_DeprecationWarning, + "id property is deprecated and will be removed in Pillow 12.0", + 1 + ) < 0) { + return NULL; + } return PyLong_FromSsize_t((Py_ssize_t)self->image); } @@ -3717,6 +3724,13 @@ _getattr_ptr(ImagingObject *self, void *closure) { static PyObject * _getattr_unsafe_ptrs(ImagingObject *self, void *closure) { + if (PyErr_WarnEx( + PyExc_DeprecationWarning, + "unsafe_ptrs property is deprecated and will be removed in Pillow 12.0", + 1 + ) < 0) { + return NULL; + } return Py_BuildValue( "(sn)(sn)(sn)", "image8", diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 321dce988..870f5ed97 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -61,7 +61,7 @@ typedef struct ImagingOutlineInstance *ImagingOutline; typedef struct ImagingPaletteInstance *ImagingPalette; /* handle magics (used with PyCObject). */ -#define IMAGING_MAGIC "PIL Imaging" +#define IMAGING_MAGIC "Pillow Imaging" /* pixel types */ #define IMAGING_TYPE_UINT8 0 From c69ad03af0216f1cce2d9f1c1653bec6c295e6a6 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 03:45:18 +0400 Subject: [PATCH 129/652] Remove legacy 1-bit api, fix AttributeError PytestUnraisableExceptionWarning: Exception ignored in: AttributeError: 'PhotoImage' object has no attribute '_PhotoImage__photo' --- src/PIL/ImageTk.py | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index a788c9d1f..d546989ee 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -35,20 +35,6 @@ from . import Image, ImageFile # -------------------------------------------------------------------- # Check for Tkinter interface hooks -_pilbitmap_ok = None - - -def _pilbitmap_check() -> int: - global _pilbitmap_ok - if _pilbitmap_ok is None: - try: - im = Image.new("1", (1, 1)) - tkinter.BitmapImage(data=f"PIL:{im.im.id}") - _pilbitmap_ok = 1 - except tkinter.TclError: - _pilbitmap_ok = 0 - return _pilbitmap_ok - def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: source = None @@ -142,6 +128,8 @@ class PhotoImage: self.paste(image) def __del__(self) -> None: + if not hasattr(self, "__photo"): + return name = self.__photo.name self.__photo.name = None try: @@ -225,17 +213,11 @@ class BitmapImage: self.__mode = image.mode self.__size = image.size - if _pilbitmap_check(): - # fast way (requires the pilbitmap booster patch) - image.load() - kw["data"] = f"PIL:{image.im.id}" - self.__im = image # must keep a reference - else: - # slow but safe way - kw["data"] = image.tobitmap() - self.__photo = tkinter.BitmapImage(**kw) + self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw) def __del__(self) -> None: + if not hasattr(self, "__photo"): + return name = self.__photo.name self.__photo.name = None try: From 920c4ac4479ee12636f90e281e14e8bfc89b0580 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 04:29:34 +0400 Subject: [PATCH 130/652] Use PyCapsule in _imagingtk --- src/PIL/ImageTk.py | 11 +++++------ src/Tk/tkImaging.c | 35 ++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index d546989ee..279a21716 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -48,18 +48,18 @@ def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: def _pyimagingtkcall( - command: str, photo: PhotoImage | tkinter.PhotoImage, id: int + command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: object ) -> None: tk = photo.tk try: - tk.call(command, photo, id) + tk.call(command, photo, repr(ptr)) except tkinter.TclError: # activate Tkinter hook # may raise an error if it cannot attach to Tkinter from . import _imagingtk _imagingtk.tkinit(tk.interpaddr()) - tk.call(command, photo, id) + tk.call(command, photo, repr(ptr)) # -------------------------------------------------------------------- @@ -181,7 +181,7 @@ class PhotoImage: block = image.new_block(self.__mode, im.size) image.convert2(block, image) # convert directly between buffers - _pyimagingtkcall("PyImagingPhoto", self.__photo, block.id) + _pyimagingtkcall("PyImagingPhoto", self.__photo, block.ptr) # -------------------------------------------------------------------- @@ -255,9 +255,8 @@ class BitmapImage: def getimage(photo: PhotoImage) -> Image.Image: """Copies the contents of a PhotoImage to a PIL image memory.""" im = Image.new("RGBA", (photo.width(), photo.height())) - block = im.im - _pyimagingtkcall("PyImagingPhotoGet", photo, block.id) + _pyimagingtkcall("PyImagingPhotoGet", photo, im.im.ptr) return im diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 727ee6bed..b33a47d30 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -56,19 +56,36 @@ static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; static Imaging ImagingFind(const char *name) { - Py_ssize_t id; + PyObject *capsule; + int direct_pointer = 0; + const char *expected = "capsule object \"" IMAGING_MAGIC "\" at 0x"; - /* FIXME: use CObject instead? */ -#if defined(_WIN64) - id = _atoi64(name); -#else - id = atol(name); -#endif - if (!id) { + if (name[0] == '<') { + name++; + } else { + // Special case for PyPy, where the string representation of a Capsule + // refers directly to the pointer itself, not to the PyCapsule object. + direct_pointer = 1; + } + + if (strncmp(name, expected, strlen(expected))) { return NULL; } - return (Imaging)id; + capsule = (PyObject *)strtoull(name + strlen(expected), NULL, 16); + + if (direct_pointer) { + return (Imaging)capsule; + } + + if (!PyCapsule_IsValid(capsule, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } + + return (Imaging)PyCapsule_GetPointer(capsule, IMAGING_MAGIC); } static int From 147f75ee9376688560942f814a66ea0c2ad03361 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 13:04:57 +0400 Subject: [PATCH 131/652] Use PyCapsule in _imagingft --- src/_imagingft.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index f8143e0cc..af5d80f87 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -824,7 +824,6 @@ font_render(FontObject *self, PyObject *args) { 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? */ int color = 0; /* is FT_LOAD_COLOR enabled? */ int stroke_width = 0; @@ -923,10 +922,9 @@ font_render(FontObject *self, PyObject *args) { PyMem_Del(glyph_info); return NULL; } - PyObject *imageId = PyObject_GetAttrString(image, "id"); - id = PyLong_AsSsize_t(imageId); - Py_XDECREF(imageId); - im = (Imaging)id; + PyObject *imagePtr = PyObject_GetAttrString(image, "ptr"); + im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC); + Py_XDECREF(imagePtr); x_offset -= stroke_width; y_offset -= stroke_width; From 6b59e3822f202f475b8b5440949a5b673019286b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 17:26:20 +0000 Subject: [PATCH 132/652] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.0 → v0.6.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.0...v0.6.3) - [github.com/python-jsonschema/check-jsonschema: 0.29.1 → 0.29.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.29.1...0.29.2) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ab478e84..14d75c689 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.0 + rev: v0.6.3 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -50,7 +50,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.1 + rev: 0.29.2 hooks: - id: check-github-workflows - id: check-readthedocs From 55f49393af490fcfc0ef40f4790b53c54395a99f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Sep 2024 09:55:06 +1000 Subject: [PATCH 133/652] Treat unknown colorspace as unspecified --- Tests/test_file_jpeg2k.py | 9 +++++++++ src/libImaging/Jpeg2KDecode.c | 5 ++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 5e11465ca..53a105ca5 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -182,6 +182,15 @@ def test_restricted_icc_profile() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = False +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_unknown_colorspace() -> None: + with Image.open(f"{EXTRA_DIR}/file8.jp2") as im: + im.load() + assert im.mode == "L" + + def test_header_errors() -> None: for path in ( "Tests/images/invalid_header_length.jp2", diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index 5b3d7ffc4..a81a14a09 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -698,8 +698,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { } /* Check that this image is something we can handle */ - if (image->numcomps < 1 || image->numcomps > 4 || - image->color_space == OPJ_CLRSPC_UNKNOWN) { + if (image->numcomps < 1 || image->numcomps > 4) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; @@ -744,7 +743,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { /* Find the correct unpacker */ color_space = image->color_space; - if (color_space == OPJ_CLRSPC_UNSPECIFIED) { + if (color_space == OPJ_CLRSPC_UNKNOWN || color_space == OPJ_CLRSPC_UNSPECIFIED) { switch (image->numcomps) { case 1: case 2: From 61dade88e8ad55745c06e72561302bb8c21b490f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Sep 2024 17:00:59 +1000 Subject: [PATCH 134/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9da76d82a..4958ed701 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Treat unknown JPEG2000 colorspace as unspecified #8343 + [radarhere] + - Updated error message when saving WebP with invalid width or height #8322 [radarhere, hugovk] From bf11639626373bd27a80f790d56101d4b4e2f1c6 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 3 Sep 2024 11:54:24 +0400 Subject: [PATCH 135/652] rename PyCapsule -> Capsule --- docs/deprecations.rst | 2 +- src/Tk/tkImaging.c | 4 +--- src/_imagingcms.c | 4 +--- src/_imagingmath.c | 16 ++++------------ src/_imagingmorph.c | 12 +++--------- 5 files changed, 10 insertions(+), 28 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 5bb0ebf58..93bd51ef1 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -144,7 +144,7 @@ Get Internal Pointers to Objects ``Image.core.ImageCore.id`` and ``Image.core.ImageCore.unsafe_ptrs`` have been deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining raw pointers to ``ImageCore`` internals. To interact with -C code, you can use ``Image.core.ImageCore.ptr``, which returns a ``PyCapsule`` object. +C code, you can use ``Image.core.ImageCore.ptr``, which returns a ``Capsule`` object. Removed features ---------------- diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index b33a47d30..a36c3e0bd 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -79,9 +79,7 @@ ImagingFind(const char *name) { } if (!PyCapsule_IsValid(capsule, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 444b6a602..1823bcf03 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -541,9 +541,7 @@ cms_transform_apply(CmsTransformObject *self, PyObject *args) { if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } diff --git a/src/_imagingmath.c b/src/_imagingmath.c index eafcf1444..dbe636707 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -177,16 +177,12 @@ _unop(PyObject *self, PyObject *args) { } if (!PyCapsule_IsValid(op, MATH_FUNC_UNOP_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", MATH_FUNC_UNOP_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", MATH_FUNC_UNOP_MAGIC); return NULL; } if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } @@ -213,17 +209,13 @@ _binop(PyObject *self, PyObject *args) { } if (!PyCapsule_IsValid(op, MATH_FUNC_BINOP_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", MATH_FUNC_BINOP_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", MATH_FUNC_BINOP_MAGIC); return NULL; } if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC) || !PyCapsule_IsValid(i2, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 715cccf51..637295be7 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -48,9 +48,7 @@ apply(PyObject *self, PyObject *args) { if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } @@ -143,9 +141,7 @@ match(PyObject *self, PyObject *args) { } if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } @@ -218,9 +214,7 @@ get_on_pixels(PyObject *self, PyObject *args) { } if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } From 63b11966228717460e1e4170a009ef958e1704f2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Sep 2024 16:13:19 +1000 Subject: [PATCH 136/652] Updated harfbuzz to 9.0.0 --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1021e4f22..9216119e8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -112,7 +112,7 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.2", "FRIBIDI": "1.0.15", - "HARFBUZZ": "8.5.0", + "HARFBUZZ": "9.0.0", "JPEGTURBO": "3.0.3", "LCMS2": "2.16", "LIBPNG": "1.6.43", From be34a7da4bb25a660d83abee3495626f221e949d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Sep 2024 19:19:39 +1000 Subject: [PATCH 137/652] Save xmp from info --- Tests/test_file_jpeg.py | 6 ++++-- src/PIL/JpegImagePlugin.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index f9c5fbc0b..c8981ce85 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -995,11 +995,13 @@ class TestFileJpeg: f = str(tmp_path / "temp.jpg") im = hopper() im.save(f, xmp=b"XMP test") - with Image.open(f) as reloaded: assert reloaded.info["xmp"] == b"XMP test" - im.save(f, xmp=b"1" * 65504) + im.info["xmp"] = b"1" * 65504 + im.save(f) + with Image.open(f) as reloaded: + assert reloaded.info["xmp"] == b"1" * 65504 with pytest.raises(ValueError): im.save(f, xmp=b"1" * 65505) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 0cb50e0bf..723165ec4 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -747,7 +747,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: extra = info.get("extra", b"") MAX_BYTES_IN_MARKER = 65533 - xmp = info.get("xmp") + xmp = info.get("xmp", im.info.get("xmp")) if xmp: overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len From 8833548e55977493fcd21b8270ba515e0503a3df Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Thu, 5 Sep 2024 01:04:35 +0400 Subject: [PATCH 138/652] Move new_block to module --- src/PIL/ImageTk.py | 2 +- src/_imaging.c | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 279a21716..5bad9b546 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -178,7 +178,7 @@ class PhotoImage: if image.isblock() and im.mode == self.__mode: block = image else: - block = image.new_block(self.__mode, im.size) + block = Image.core.new_block(self.__mode, im.size) image.convert2(block, image) # convert directly between buffers _pyimagingtkcall("PyImagingPhoto", self.__photo, block.ptr) diff --git a/src/_imaging.c b/src/_imaging.c index a455c873b..e227d8efc 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3674,15 +3674,12 @@ static struct PyMethodDef methods[] = { /* Unsharpmask extension */ {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, - {"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, /* Special effects */ {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, /* Misc. */ - {"new_block", (PyCFunction)_new_block, METH_VARARGS}, - {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, {NULL, NULL} /* sentinel */ @@ -4212,6 +4209,7 @@ static PyMethodDef functions[] = { {"blend", (PyCFunction)_blend, METH_VARARGS}, {"fill", (PyCFunction)_fill, METH_VARARGS}, {"new", (PyCFunction)_new, METH_VARARGS}, + {"new_block", (PyCFunction)_new_block, METH_VARARGS}, {"merge", (PyCFunction)_merge, METH_VARARGS}, /* Functions */ From 12700436827a06194e4b63a47c5e60585e52be59 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 5 Sep 2024 15:46:54 +1000 Subject: [PATCH 139/652] Return early if either width or height is zero --- Tests/test_box_blur.py | 5 +++++ src/libImaging/BoxBlur.c | 3 +++ 2 files changed, 8 insertions(+) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 1f6ed6127..cb267b204 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -71,6 +71,11 @@ def test_color_modes() -> None: box_blur(sample.convert("YCbCr")) +@pytest.mark.parametrize("size", ((0, 1), (1, 0))) +def test_zero_dimension(size: tuple[int, int]) -> None: + assert box_blur(Image.new("L", size)).size == size + + def test_radius_0() -> None: assert_blur( sample, diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 51cb7e102..ed91541fe 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -238,6 +238,9 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) int i; Imaging imTransposed; + if (imOut->xsize == 0 || imOut->ysize == 0) { + return imOut; + } if (n < 1) { return ImagingError_ValueError("number of passes must be greater than zero"); } From a56ac4faa878fef90a15d716bb57ea2917c9e4a5 Mon Sep 17 00:00:00 2001 From: Aryaz Eghbali Date: Thu, 5 Sep 2024 13:29:19 +0200 Subject: [PATCH 140/652] Canonical type checking --- Tests/test_image_array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index bb6064882..c1b09c659 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -24,7 +24,7 @@ def test_toarray() -> None: def test_with_dtype(dtype: npt.DTypeLike) -> None: ai = numpy.array(im, dtype=dtype) - assert ai.dtype == dtype + assert ai.dtype.type is dtype # assert test("1") == ((100, 128), '|b1', 1600)) assert test("L") == ((100, 128), "|u1", 12800) From f7f3af799be98e453d6faafc6bd9b08f3bb18e79 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 5 Sep 2024 22:53:48 +1000 Subject: [PATCH 141/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4958ed701..b43466833 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,30 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Return early from BoxBlur if either width or height is zero #8347 + [radarhere] + +- Check text is either string or bytes #8308 + [radarhere] + +- Added writing XMP bytes to JPEG #8286 + [radarhere] + +- Support JPEG2000 RGBA palettes #8256 + [radarhere] + +- Expand C image to match GIF frame image size #8237 + [radarhere] + +- Allow saving I;16 images as PPM #8231 + [radarhere] + +- When IFD is missing, connect get_ifd() dictionary to Exif #8230 + [radarhere] + +- Skip truncated ICO mask if LOAD_TRUNCATED_IMAGES is enabled #8180 + [radarhere] + - Treat unknown JPEG2000 colorspace as unspecified #8343 [radarhere] From 31638ee29aae89452adf30acd41620d8c8d91373 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 5 Sep 2024 23:07:40 +1000 Subject: [PATCH 142/652] Added type hints --- Tests/test_file_gif.py | 4 +++- src/PIL/PngImagePlugin.py | 46 ++++++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 7c332149d..571fe1b9a 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1392,7 +1392,9 @@ def test_lzw_bits() -> None: ), ), ) -def test_extents(test_file, loading_strategy) -> None: +def test_extents( + test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy +) -> None: GifImagePlugin.LOADING_STRATEGY = loading_strategy try: with Image.open("Tests/images/" + test_file) as im: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index a87e1a625..c268d7b1a 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -370,21 +370,27 @@ class PngInfo: # PNG image stream (IHDR/IEND) +class _RewindState(NamedTuple): + info: dict[str | tuple[int, int], Any] + tile: list[ImageFile._Tile] + seq_num: int | None + + class PngStream(ChunkStream): - def __init__(self, fp): + def __init__(self, fp: IO[bytes]) -> None: super().__init__(fp) # local copies of Image attributes - self.im_info = {} - self.im_text = {} + self.im_info: dict[str | tuple[int, int], Any] = {} + self.im_text: dict[str, str | iTXt] = {} self.im_size = (0, 0) - self.im_mode = None - self.im_tile = None - self.im_palette = None - self.im_custom_mimetype = None - self.im_n_frames = None - self._seq_num = None - self.rewind_state = None + self.im_mode = "" + self.im_tile: list[ImageFile._Tile] = [] + self.im_palette: tuple[str, bytes] | None = None + self.im_custom_mimetype: str | None = None + self.im_n_frames: int | None = None + self._seq_num: int | None = None + self.rewind_state = _RewindState({}, [], None) self.text_memory = 0 @@ -398,16 +404,16 @@ class PngStream(ChunkStream): raise ValueError(msg) def save_rewind(self) -> None: - self.rewind_state = { - "info": self.im_info.copy(), - "tile": self.im_tile, - "seq_num": self._seq_num, - } + self.rewind_state = _RewindState( + self.im_info.copy(), + self.im_tile, + self._seq_num, + ) def rewind(self) -> None: - self.im_info = self.rewind_state["info"].copy() - self.im_tile = self.rewind_state["tile"] - self._seq_num = self.rewind_state["seq_num"] + self.im_info = self.rewind_state.info.copy() + self.im_tile = self.rewind_state.tile + self._seq_num = self.rewind_state.seq_num def chunk_iCCP(self, pos: int, length: int) -> bytes: # ICC profile @@ -461,11 +467,11 @@ class PngStream(ChunkStream): def chunk_IDAT(self, pos: int, length: int) -> NoReturn: # image data if "bbox" in self.im_info: - tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] + tile = [ImageFile._Tile("zip", self.im_info["bbox"], pos, self.im_rawmode)] else: if self.im_n_frames is not None: self.im_info["default_image"] = True - tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] + tile = [ImageFile._Tile("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] self.im_tile = tile self.im_idat = length msg = "image data found" From e625f7311965abd0a88992ca215d8ad61a27715e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2024 11:37:12 +1000 Subject: [PATCH 143/652] Added scale argument to IcnsImageFile load() --- Tests/test_file_icns.py | 20 +++++++----- docs/handbook/image-file-formats.rst | 12 +++++++ src/PIL/IcnsImagePlugin.py | 47 ++++++++++++++-------------- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 488984aef..f37a6996f 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -63,8 +63,8 @@ def test_save_append_images(tmp_path: Path) -> None: assert_image_similar_tofile(im, temp_file, 1) with Image.open(temp_file) as reread: - reread.size = (16, 16, 2) - reread.load() + reread.size = (16, 16) + reread.load(2) assert_image_equal(reread, provided_im) @@ -92,9 +92,15 @@ def test_sizes() -> None: assert im.mode == "RGBA" assert im.size == (wr, hr) + # Test using load() with scale + im.size = (w, h) + im.load(scale=r) + assert im.mode == "RGBA" + assert im.size == (wr, hr) + # Check that we cannot load an incorrect size with pytest.raises(ValueError): - im.size = (1, 1) + im.size = (1, 2) def test_older_icon() -> None: @@ -105,8 +111,8 @@ def test_older_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow2.icns") as im2: - im2.size = (w, h, r) - im2.load() + im2.size = (w, h) + im2.load(r) assert im2.mode == "RGBA" assert im2.size == (wr, hr) @@ -122,8 +128,8 @@ def test_jp2_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow3.icns") as im2: - im2.size = (w, h, r) - im2.load() + im2.size = (w, h) + im2.load(r) assert im2.mode == "RGBA" assert im2.size == (wr, hr) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index ca0e05eb6..a5fbc8053 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -331,6 +331,18 @@ sets the following :py:attr:`~PIL.Image.Image.info` property: ask for ``(512, 512, 2)``, the final value of :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). +.. _icns-loading: + +Loading +~~~~~~~ + +You can call the :py:meth:`~PIL.Image.Image.load` method with the following parameter. + +**scale** + Affects the scale of the resultant image. If the size is set to ``(512, 512)``, + after loading at scale 2, the final value of :py:attr:`~PIL.Image.Image.size` will + be ``(1024, 1024)``. + .. _icns-saving: Saving diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index ca66aa0fd..e4f486a4f 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -281,31 +281,30 @@ class IcnsImageFile(ImageFile.ImageFile): @size.setter def size(self, value) -> None: - info_size = value - if info_size not in self.info["sizes"] and len(info_size) == 2: - info_size = (info_size[0], info_size[1], 1) - if ( - info_size not in self.info["sizes"] - and len(info_size) == 3 - and info_size[2] == 1 - ): - simple_sizes = [ - (size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"] - ] - if value in simple_sizes: - info_size = self.info["sizes"][simple_sizes.index(value)] - if info_size not in self.info["sizes"]: - msg = "This is not one of the allowed sizes of this image" - raise ValueError(msg) - self._size = value + if len(value) == 3: + if value in self.info["sizes"]: + self._size = value + return + else: + # Check that a matching size exists, + # or that there is a scale that would create a size that matches + for size in self.info["sizes"]: + simple_size = size[0] * size[2], size[1] * size[2] + scale = simple_size[0] // value[0] + if simple_size[1] / value[1] == scale: + self._size = value + return + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) - def load(self) -> Image.core.PixelAccess | None: - if len(self.size) == 3: - self.best_size = self.size - self.size = ( - self.best_size[0] * self.best_size[2], - self.best_size[1] * self.best_size[2], - ) + def load(self, scale: int | None = None) -> Image.core.PixelAccess | None: + if scale is not None or len(self.size) == 3: + if scale is None and len(self.size) == 3: + scale = self.size[2] + assert scale is not None + width, height = self.size[:2] + self.size = width * scale, height * scale + self.best_size = width, height, scale px = Image.Image.load(self) if self._im is not None and self.im.size == self.size: From a6f5f4dd43c95ded41da5d56c7e9782f54e8dea2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2024 11:42:56 +1000 Subject: [PATCH 144/652] Deprecate setting size to (width, height, scale) --- Tests/test_file_icns.py | 3 ++- docs/deprecations.rst | 8 ++++++++ docs/handbook/image-file-formats.rst | 7 +------ docs/releasenotes/11.0.0.rst | 13 +++++++++++++ src/PIL/IcnsImagePlugin.py | 10 ++++++---- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index f37a6996f..16f6b3651 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -87,7 +87,8 @@ def test_sizes() -> None: for w, h, r in im.info["sizes"]: wr = w * r hr = h * r - im.size = (w, h, r) + with pytest.warns(DeprecationWarning): + im.size = (w, h, r) im.load() assert im.mode == "RGBA" assert im.size == (wr, hr) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a9498d5ed..061f579de 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -109,6 +109,14 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. +ICNS (width, height, scale) sizes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +Setting an ICNS image size to ``(width, height, scale)`` before loading has been +deprecated. Instead, ``load(scale)`` can be used. + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a5fbc8053..8183473e4 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -324,12 +324,7 @@ sets the following :py:attr:`~PIL.Image.Image.info` property: **sizes** A list of supported sizes found in this icon file; these are a 3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina - icon and 1 for a standard icon. You *are* permitted to use this 3-tuple - format for the :py:attr:`~PIL.Image.Image.size` property if you set it - before calling :py:meth:`~PIL.Image.Image.load`; after loading, the size - will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you - ask for ``(512, 512, 2)``, the final value of - :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). + icon and 1 for a standard icon. .. _icns-loading: diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index ac9237acf..27c1bd308 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -43,9 +43,20 @@ similarly removed. Deprecations ============ + +ICNS (width, height, scale) sizes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +Setting an ICNS image size to ``(width, height, scale)`` before loading has been +deprecated. Instead, ``load(scale)`` can be used. + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. deprecated:: 11.0.0 + The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword arguments can be used instead. @@ -61,6 +72,8 @@ have been deprecated, and will be removed in Pillow 12 (2025-10-15). Specific WebP Feature Checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. deprecated:: 11.0.0 + ``features.check("transp_webp")``, ``features.check("webp_mux")`` and ``features.check("webp_anim")`` are now deprecated. They will always return ``True`` if the WebP module is installed, until they are removed in Pillow diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index e4f486a4f..9757b2b14 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -25,6 +25,7 @@ import sys from typing import IO from . import Image, ImageFile, PngImagePlugin, features +from ._deprecate import deprecate enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: @@ -275,15 +276,16 @@ class IcnsImageFile(ImageFile.ImageFile): self.best_size[1] * self.best_size[2], ) - @property - def size(self): + @property # type: ignore[override] + def size(self) -> tuple[int, int] | tuple[int, int, int]: return self._size @size.setter - def size(self, value) -> None: + def size(self, value: tuple[int, int] | tuple[int, int, int]) -> None: if len(value) == 3: + deprecate("Setting size to (width, height, scale)", 12, "load(scale)") if value in self.info["sizes"]: - self._size = value + self._size = value # type: ignore[assignment] return else: # Check that a matching size exists, From 23c461e431e84fbfb81fca10c5a81c047884559b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2024 11:56:06 +1000 Subject: [PATCH 145/652] Handle duplicate exif header Co-authored-by: Jamil Zakirov --- Tests/test_image.py | 8 ++++++++ src/PIL/Image.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index b0d719667..97e97acaa 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -775,6 +775,14 @@ class TestImage: exif.load(b"Exif\x00\x00") assert not dict(exif) + def test_duplicate_exif_header(self) -> None: + with Image.open("Tests/images/exif.png") as im: + im.load() + im.info["exif"] = b"Exif\x00\x00" + im.info["exif"] + + exif = im.getexif() + assert exif[274] == 1 + def test_empty_get_ifd(self) -> None: exif = Image.Exif() ifd = exif.get_ifd(0x8769) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1c0cf2936..2fb677ead 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3968,7 +3968,7 @@ class Exif(_ExifBase): self._data.clear() self._hidden_data.clear() self._ifds.clear() - if data and data.startswith(b"Exif\x00\x00"): + while data and data.startswith(b"Exif\x00\x00"): data = data[6:] if not data: self._info = None From 06c331af5d85825b7c2595ca3861730d52b22e00 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2024 15:16:59 +1000 Subject: [PATCH 146/652] Added type hints to frombuffer --- Tests/test_file_jpeg.py | 4 +++- Tests/test_imagefile.py | 2 +- docs/example/DdsImagePlugin.py | 4 ++-- src/PIL/BlpImagePlugin.py | 2 +- src/PIL/BmpImagePlugin.py | 2 +- src/PIL/DdsImagePlugin.py | 2 +- src/PIL/FitsImagePlugin.py | 2 +- src/PIL/Image.py | 13 ++++++++++--- src/PIL/ImageFile.py | 2 +- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PpmImagePlugin.py | 4 ++-- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/SgiImagePlugin.py | 2 +- 13 files changed, 26 insertions(+), 17 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index affc18625..cde951395 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1011,7 +1011,9 @@ class TestFileJpeg: # Even though this decoder never says that it is finished # the image should still end when there is no new data class InfiniteMockPyDecoder(ImageFile.PyDecoder): - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode( + self, buffer: bytes | Image.SupportsArrayInterface + ) -> tuple[int, int]: return 0, 0 Image.register_decoder("INFINITE", InfiniteMockPyDecoder) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 0985dce9c..1ee684926 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -210,7 +210,7 @@ class MockPyDecoder(ImageFile.PyDecoder): super().__init__(mode, *args) - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: # eof return -1, 0 diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index d8f536076..10828bff0 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -257,7 +257,7 @@ class DdsImageFile(ImageFile.ImageFile): class DXT1Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None try: self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) @@ -270,7 +270,7 @@ class DXT1Decoder(ImageFile.PyDecoder): class DXT5Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None try: self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 8e29898df..e5605635e 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -279,7 +279,7 @@ class BlpImageFile(ImageFile.ImageFile): class _BLPBaseDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: try: self._read_blp_header() self._load() diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 16c8cf6ff..bf8f29577 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -321,7 +321,7 @@ class BmpImageFile(ImageFile.ImageFile): class BmpRleDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None rle4 = self.args[1] data = bytearray() diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 631532470..1b6408237 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -481,7 +481,7 @@ class DdsImageFile(ImageFile.ImageFile): class DdsRgbDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None bitcount, masks = self.args diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index abacc5e67..6bbd2641a 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -126,7 +126,7 @@ class FitsImageFile(ImageFile.ImageFile): class FitsGzipDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None value = gzip.decompress(self.fd.read()) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1c0cf2936..90d028b9b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -856,7 +856,10 @@ class Image: ) def frombytes( - self, data: bytes | bytearray, decoder_name: str = "raw", *args: Any + self, + data: bytes | bytearray | SupportsArrayInterface, + decoder_name: str = "raw", + *args: Any, ) -> None: """ Loads this image with pixel data from a bytes object. @@ -3145,7 +3148,7 @@ def new( def frombytes( mode: str, size: tuple[int, int], - data: bytes | bytearray, + data: bytes | bytearray | SupportsArrayInterface, decoder_name: str = "raw", *args: Any, ) -> Image: @@ -3189,7 +3192,11 @@ def frombytes( def frombuffer( - mode: str, size: tuple[int, int], data, decoder_name: str = "raw", *args: Any + mode: str, + size: tuple[int, int], + data: bytes | SupportsArrayInterface, + decoder_name: str = "raw", + *args: Any, ) -> Image: """ Creates an image memory referencing pixel data in a byte buffer. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 615961ce5..b8bf0b7fe 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -733,7 +733,7 @@ class PyDecoder(PyCodec): def pulls_fd(self) -> bool: return self._pulls_fd - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: """ Override to perform the decoding process. diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 1363ddd1f..f3460a787 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -112,7 +112,7 @@ class MspDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None img = io.BytesIO() diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 3bda2bf28..4e779df17 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -284,7 +284,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): break return data - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: self._comment_spans = False if self.mode == "1": data = self._decode_bitonal() @@ -300,7 +300,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): class PpmDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None data = bytearray() diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index eaf031402..010d3f941 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -47,7 +47,7 @@ class QoiDecoder(ImageFile.PyDecoder): hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 self._previously_seen_pixels[hash_value] = value - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None self._previously_seen_pixels = {} diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 2c205a926..44254b7a4 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -214,7 +214,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: class SGI16Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None assert self.im is not None From 94315f87c32d67c097ae8fb6eb8052bba9384af4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2024 19:13:35 +1000 Subject: [PATCH 147/652] Removed unused IFD_LEGACY_API --- docs/deprecations.rst | 7 +++++++ docs/releasenotes/11.0.0.rst | 5 +++++ src/PIL/TiffImagePlugin.py | 3 +-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a9498d5ed..cafda3803 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -142,6 +142,13 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +TiffImagePlugin IFD_LEGACY_API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 11.0.0 + +``TiffImagePlugin.IFD_LEGACY_API`` was removed, as it was an unused setting. + PSFile ~~~~~~ diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index ac9237acf..f0d864f6a 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -40,6 +40,11 @@ removed. Pillow's C API will now be used on PyPy instead. ``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was similarly removed. +TiffImagePlugin IFD_LEGACY_API +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An unused setting, ``TiffImagePlugin.IFD_LEGACY_API``, has been removed. + Deprecations ============ diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88f75ce8e..87d9677e0 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -69,7 +69,6 @@ logger = logging.getLogger(__name__) # Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False WRITE_LIBTIFF = False -IFD_LEGACY_API = True STRIP_SIZE = 65536 II = b"II" # little-endian (Intel style) @@ -1125,7 +1124,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): return val -# undone -- switch this pointer when IFD_LEGACY_API == False +# undone -- switch this pointer ImageFileDirectory = ImageFileDirectory_v1 From 93a0bf02760e6b9e1764eb5f15c69a2c12d96e00 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2024 21:33:30 +1000 Subject: [PATCH 148/652] Added type hints to TiffImagePlugin _save --- docs/conf.py | 2 +- src/PIL/TiffImagePlugin.py | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 41f9c0e38..a0f5867d7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -121,7 +121,7 @@ nitpicky = True # generating warnings in “nitpicky mode”. Note that type should include the domain name # if present. Example entries would be ('py:func', 'int') or # ('envvar', 'LD_LIBRARY_PATH'). -# nitpick_ignore = [] +nitpick_ignore = [("py:class", "_io.BytesIO")] # -- Options for HTML output ---------------------------------------------- diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88f75ce8e..53f6ae621 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -46,6 +46,7 @@ import logging import math import os import struct +import sys import warnings from collections.abc import Iterator, MutableMapping from fractions import Fraction @@ -1647,7 +1648,7 @@ SAVE_INFO = { } -def _save(im: Image.Image, fp, filename: str | bytes) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: @@ -1958,7 +1959,7 @@ def _save(im: Image.Image, fp, filename: str | bytes) -> None: setattr(im, "_debug_multipage", ifd) -class AppendingTiffWriter: +class AppendingTiffWriter(io.BytesIO): fieldSizes = [ 0, # None 1, # byte @@ -2068,6 +2069,12 @@ class AppendingTiffWriter: return self.f.tell() - self.offsetOfNewPage def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: + """ + :param offset: Distance to seek. + :param whence: Whether the distance is relative to the start, + end or current position. + :returns: The resulting position, relative to the start. + """ if whence == os.SEEK_SET: offset += self.offsetOfNewPage @@ -2101,8 +2108,16 @@ class AppendingTiffWriter: num_tags = self.readShort() self.f.seek(num_tags * 12, os.SEEK_CUR) - def write(self, data: bytes) -> int | None: - return self.f.write(data) + if sys.version_info >= (3, 12): + from collections.abc import Buffer + + def write(self, data: Buffer, /) -> int: + return self.f.write(data) + + else: + + def write(self, data: Any, /) -> int: + return self.f.write(data) def readShort(self) -> int: (value,) = struct.unpack(self.shortFmt, self.f.read(2)) @@ -2143,7 +2158,8 @@ class AppendingTiffWriter: def close(self) -> None: self.finalize() - self.f.close() + if self.close_fp: + self.f.close() def fixIFD(self) -> None: num_tags = self.readShort() From d3c1d99d00b3d76d9c3dbdfc629b6278a0e38613 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:22:39 +0300 Subject: [PATCH 149/652] Deprecate support for FreeType 2.9.0 --- Tests/test_imagefont.py | 12 ++++++++++++ docs/deprecations.rst | 13 +++++++++++++ docs/releasenotes/11.0.0.rst | 13 +++++++++++++ src/PIL/ImageFont.py | 17 ++++++++++++++++- 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 953706010..cfadc7312 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1150,3 +1150,15 @@ def test_invalid_truetype_sizes_raise_valueerror( ) -> None: with pytest.raises(ValueError): ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) + + +def test_freetype_deprecation(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange: mock features.version_module to return fake FreeType version + def fake_version_module(module): + return "2.9.0" + + monkeypatch.setattr(features, "version_module", fake_version_module) + + # Act / Assert + with pytest.warns(DeprecationWarning): + ImageFont.truetype(FONT_PATH, FONT_SIZE) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a9498d5ed..9a2f16cfb 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,6 +12,19 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a :py:exc:`DeprecationWarning` is issued. +FreeType 2.9.0 +~~~~~~~~~~~~~~ + +.. deprecated:: 11.0.0 + +Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 +(2025-10-15), when FreeType 2.9.1 will be the minimum supported. + +We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + ImageFile.raise_oserror ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index ac9237acf..99e0a2927 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -43,6 +43,19 @@ similarly removed. Deprecations ============ +FreeType 2.9.0 +^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 +(2025-10-15), when FreeType 2.9.1 will be the minimum supported. + +We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a82c36ba6..50796e132 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -36,7 +36,7 @@ from io import BytesIO from types import ModuleType from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast -from . import Image +from . import Image, features from ._typing import StrOrBytesPath from ._util import DeferredError, is_path @@ -232,6 +232,21 @@ class FreeTypeFont: self.index = index self.encoding = encoding + try: + from packaging.version import parse as parse_version + except ImportError: + pass + else: + if freetype_version := features.version_module("freetype2"): + if parse_version(freetype_version) < parse_version("2.9.1"): + warnings.warn( + "Support for FreeType 2.9.0 is deprecated and will be removed " + "in Pillow 12 (2025-10-15). Please upgrade to FreeType 2.9.1 " + "or newer, preferably FreeType 2.10.4 which fixes " + "CVE-2020-15999.", + DeprecationWarning, + ) + if layout_engine not in (Layout.BASIC, Layout.RAQM): layout_engine = Layout.BASIC if core.HAVE_RAQM: From de3c6fa2957cd237cea024ae558034379129362e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 12:14:30 +1000 Subject: [PATCH 150/652] Support converting more modes to LAB by converting to RGBA first --- Tests/test_imagecms.py | 6 ++++++ src/PIL/Image.py | 14 ++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 5ee5fcedf..f062651f0 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -696,6 +696,12 @@ def test_rgb_lab(mode: str) -> None: assert value[:3] == (0, 255, 255) +def test_cmyk_lab() -> None: + im = Image.new("CMYK", (1, 1)) + converted_im = im.convert("LAB") + assert converted_im.getpixel((0, 0)) == (255, 128, 128) + + def test_deprecation() -> None: with pytest.warns(DeprecationWarning): assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1c0cf2936..d706d2d33 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1127,17 +1127,23 @@ class Image: return new_im if "LAB" in (self.mode, mode): - other_mode = mode if self.mode == "LAB" else self.mode + im = self + if mode == "LAB": + if im.mode not in ("RGB", "RGBA", "RGBX"): + im = im.convert("RGBA") + other_mode = im.mode + else: + other_mode = mode if other_mode in ("RGB", "RGBA", "RGBX"): from . import ImageCms srgb = ImageCms.createProfile("sRGB") lab = ImageCms.createProfile("LAB") - profiles = [lab, srgb] if self.mode == "LAB" else [srgb, lab] + profiles = [lab, srgb] if im.mode == "LAB" else [srgb, lab] transform = ImageCms.buildTransform( - profiles[0], profiles[1], self.mode, mode + profiles[0], profiles[1], im.mode, mode ) - return transform.apply(self) + return transform.apply(im) # colorspace conversion if dither is None: From 6f9128bfaf4c91a32e075847fa2c629d826c7eaf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 14:28:36 +1000 Subject: [PATCH 151/652] Updated type hint --- src/PIL/Image.py | 7 +------ src/PIL/ImageTk.py | 5 ++++- src/PIL/_typing.py | 5 +++++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1c0cf2936..871d38a83 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -224,12 +224,7 @@ if TYPE_CHECKING: from IPython.lib.pretty import PrettyPrinter from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin - from ._typing import NumpyArray, StrOrBytesPath, TypeGuard - - if sys.version_info >= (3, 13): - from types import CapsuleType - else: - CapsuleType = object + from ._typing import CapsuleType, NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] OPEN: dict[ str, diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 5bad9b546..d01af364b 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -32,6 +32,9 @@ from typing import TYPE_CHECKING, Any, cast from . import Image, ImageFile +if TYPE_CHECKING: + from ._typing import CapsuleType + # -------------------------------------------------------------------- # Check for Tkinter interface hooks @@ -48,7 +51,7 @@ def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: def _pyimagingtkcall( - command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: object + command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType ) -> None: tk = photo.tk try: diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 093778464..dd08cdd50 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -15,6 +15,11 @@ if TYPE_CHECKING: except (ImportError, AttributeError): pass +if sys.version_info >= (3, 13): + from types import CapsuleType +else: + CapsuleType = object + if sys.version_info >= (3, 10): from typing import TypeGuard else: From a6ca45aea661bd037ad0c46771fd047575ab138a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 14:55:45 +1000 Subject: [PATCH 152/652] Use (void) to indicate no parameters --- src/libImaging/Access.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index bf7db281e..1c1937105 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -185,7 +185,7 @@ put_pixel_32(Imaging im, int x, int y, const void *color) { } void -ImagingAccessInit() { +ImagingAccessInit(void) { #define ADD(mode_, get_pixel_, put_pixel_) \ { \ ImagingAccess access = add_item(mode_); \ From 1105256f2b5e6817a23b6215a32bcf1d26687d60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 18:25:44 +1000 Subject: [PATCH 153/652] Added type hints to setup.py --- setup.py | 257 +++++++++++++++++++++++++++++++------------------------ tox.ini | 2 +- 2 files changed, 146 insertions(+), 113 deletions(-) diff --git a/setup.py b/setup.py index 863d102cf..60707083f 100644 --- a/setup.py +++ b/setup.py @@ -15,18 +15,20 @@ import struct import subprocess import sys import warnings +from collections.abc import Iterator +from typing import Any from setuptools import Extension, setup from setuptools.command.build_ext import build_ext -def get_version(): +def get_version() -> str: version_file = "src/PIL/_version.py" with open(version_file, encoding="utf-8") as f: return f.read().split('"')[1] -configuration = {} +configuration: dict[str, list[str]] = {} PILLOW_VERSION = get_version() @@ -143,7 +145,7 @@ class RequiredDependencyException(Exception): PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version -def _dbg(s, tp=None): +def _dbg(s: str, tp: Any = None) -> None: if DEBUG: if tp: print(s % tp) @@ -151,10 +153,13 @@ def _dbg(s, tp=None): print(s) -def _find_library_dirs_ldconfig(): +def _find_library_dirs_ldconfig() -> list[str]: # Based on ctypes.util from Python 2 ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" + args: list[str] + env: dict[str, str] + expr: str if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): if struct.calcsize("l") == 4: machine = os.uname()[4] + "-32" @@ -184,13 +189,11 @@ def _find_library_dirs_ldconfig(): try: p = subprocess.Popen( - args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env + args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env, text=True ) except OSError: # E.g. command not found return [] - [data, _] = p.communicate() - if isinstance(data, bytes): - data = data.decode("latin1") + data = p.communicate()[0] dirs = [] for dll in re.findall(expr, data): @@ -200,7 +203,9 @@ def _find_library_dirs_ldconfig(): return dirs -def _add_directory(path, subdir, where=None): +def _add_directory( + path: list[str], subdir: str | None, where: int | None = None +) -> None: if subdir is None: return subdir = os.path.realpath(subdir) @@ -216,7 +221,7 @@ def _add_directory(path, subdir, where=None): path.insert(where, subdir) -def _find_include_file(self, include): +def _find_include_file(self: pil_build_ext, include: str) -> int: for directory in self.compiler.include_dirs: _dbg("Checking for include file %s in %s", (include, directory)) if os.path.isfile(os.path.join(directory, include)): @@ -225,7 +230,7 @@ def _find_include_file(self, include): return 0 -def _find_library_file(self, library): +def _find_library_file(self: pil_build_ext, library: str) -> str | None: ret = self.compiler.find_library_file(self.compiler.library_dirs, library) if ret: _dbg("Found library %s at %s", (library, ret)) @@ -234,7 +239,7 @@ def _find_library_file(self, library): return ret -def _find_include_dir(self, dirname, include): +def _find_include_dir(self: pil_build_ext, dirname: str, include: str) -> bool | str: for directory in self.compiler.include_dirs: _dbg("Checking for include file %s in %s", (include, directory)) if os.path.isfile(os.path.join(directory, include)): @@ -245,6 +250,7 @@ def _find_include_dir(self, dirname, include): if os.path.isfile(os.path.join(subdir, include)): _dbg("Found %s in %s", (include, subdir)) return subdir + return False def _cmd_exists(cmd: str) -> bool: @@ -256,7 +262,7 @@ def _cmd_exists(cmd: str) -> bool: ) -def _pkg_config(name): +def _pkg_config(name: str) -> tuple[list[str], list[str]] | None: command = os.environ.get("PKG_CONFIG", "pkg-config") for keep_system in (True, False): try: @@ -283,10 +289,11 @@ def _pkg_config(name): return libs, cflags except Exception: pass + return None class pil_build_ext(build_ext): - class feature: + class ext_feature: features = [ "zlib", "jpeg", @@ -301,25 +308,32 @@ class pil_build_ext(build_ext): ] required = {"jpeg", "zlib"} - vendor = set() + vendor: set[str] = set() - def __init__(self): + def __init__(self) -> None: + self._settings: dict[str, str | bool | None] = {} for f in self.features: - setattr(self, f, None) + self.set(f, None) - def require(self, feat): + def require(self, feat: str) -> bool: return feat in self.required - def want(self, feat): - return getattr(self, feat) is None + def get(self, feat: str) -> str | bool | None: + return self._settings[feat] - def want_vendor(self, feat): + def set(self, feat: str, value: str | bool | None) -> None: + self._settings[feat] = value + + def want(self, feat: str) -> bool: + return self._settings[feat] is None + + def want_vendor(self, feat: str) -> bool: return feat in self.vendor - def __iter__(self): + def __iter__(self) -> Iterator[str]: yield from self.features - feature = feature() + feature = ext_feature() user_options = ( build_ext.user_options @@ -337,10 +351,10 @@ class pil_build_ext(build_ext): ) @staticmethod - def check_configuration(option, value): + def check_configuration(option: str, value: str) -> bool | None: return True if value in configuration.get(option, []) else None - def initialize_options(self): + def initialize_options(self) -> None: self.disable_platform_guessing = self.check_configuration( "platform-guessing", "disable" ) @@ -355,7 +369,7 @@ class pil_build_ext(build_ext): self.debug = True self.parallel = configuration.get("parallel", [None])[-1] - def finalize_options(self): + def finalize_options(self) -> None: build_ext.finalize_options(self) if self.debug: global DEBUG @@ -363,12 +377,16 @@ class pil_build_ext(build_ext): if not self.parallel: # If --parallel (or -j) wasn't specified, we want to reproduce the same # behavior as before, that is, auto-detect the number of jobs. - try: - self.parallel = int( - os.environ.get("MAX_CONCURRENCY", min(4, os.cpu_count())) - ) - except TypeError: - self.parallel = None + self.parallel = None + + cpu_count = os.cpu_count() + if cpu_count is not None: + try: + self.parallel = int( + os.environ.get("MAX_CONCURRENCY", min(4, cpu_count)) + ) + except TypeError: + pass for x in self.feature: if getattr(self, f"disable_{x}"): setattr(self.feature, x, False) @@ -402,7 +420,13 @@ class pil_build_ext(build_ext): _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) - def _update_extension(self, name, libraries, define_macros=None, sources=None): + def _update_extension( + self, + name: str, + libraries: list[str] | list[str | bool | None], + define_macros: list[tuple[str, str | None]] | None = None, + sources: list[str] | None = None, + ) -> None: for extension in self.extensions: if extension.name == name: extension.libraries += libraries @@ -415,13 +439,13 @@ class pil_build_ext(build_ext): extension.extra_link_args = ["--stdlib=libc++"] break - def _remove_extension(self, name): + def _remove_extension(self, name: str) -> None: for extension in self.extensions: if extension.name == name: self.extensions.remove(extension) break - def get_macos_sdk_path(self): + def get_macos_sdk_path(self) -> str | None: try: sdk_path = ( subprocess.check_output(["xcrun", "--show-sdk-path"]) @@ -442,9 +466,9 @@ class pil_build_ext(build_ext): sdk_path = commandlinetools_sdk_path return sdk_path - def build_extensions(self): - library_dirs = [] - include_dirs = [] + def build_extensions(self) -> None: + library_dirs: list[str] = [] + include_dirs: list[str] = [] pkg_config = None if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): @@ -468,19 +492,22 @@ class pil_build_ext(build_ext): root = globals()[root_name] if root is None and root_name in os.environ: - prefix = os.environ[root_name] - root = (os.path.join(prefix, "lib"), os.path.join(prefix, "include")) + root_prefix = os.environ[root_name] + root = ( + os.path.join(root_prefix, "lib"), + os.path.join(root_prefix, "include"), + ) if root is None and pkg_config: - if isinstance(lib_name, tuple): + if isinstance(lib_name, str): + _dbg(f"Looking for `{lib_name}` using pkg-config.") + root = pkg_config(lib_name) + else: for lib_name2 in lib_name: _dbg(f"Looking for `{lib_name2}` using pkg-config.") root = pkg_config(lib_name2) if root: break - else: - _dbg(f"Looking for `{lib_name}` using pkg-config.") - root = pkg_config(lib_name) if isinstance(root, tuple): lib_root, include_root = root @@ -660,22 +687,22 @@ class pil_build_ext(build_ext): _dbg("Looking for zlib") if _find_include_file(self, "zlib.h"): if _find_library_file(self, "z"): - feature.zlib = "z" + feature.set("zlib", "z") elif sys.platform == "win32" and _find_library_file(self, "zlib"): - feature.zlib = "zlib" # alternative name + feature.set("zlib", "zlib") # alternative name if feature.want("jpeg"): _dbg("Looking for jpeg") if _find_include_file(self, "jpeglib.h"): if _find_library_file(self, "jpeg"): - feature.jpeg = "jpeg" + feature.set("jpeg", "jpeg") elif sys.platform == "win32" and _find_library_file(self, "libjpeg"): - feature.jpeg = "libjpeg" # alternative name + feature.set("jpeg", "libjpeg") # alternative name - feature.openjpeg_version = None + feature.set("openjpeg_version", None) if feature.want("jpeg2000"): _dbg("Looking for jpeg2000") - best_version = None + best_version: tuple[int, ...] | None = None best_path = None # Find the best version @@ -705,26 +732,26 @@ class pil_build_ext(build_ext): # rather than having to cope with the versioned # include path _add_directory(self.compiler.include_dirs, best_path, 0) - feature.jpeg2000 = "openjp2" - feature.openjpeg_version = ".".join(str(x) for x in best_version) + feature.set("jpeg2000", "openjp2") + feature.set("openjpeg_version", ".".join(str(x) for x in best_version)) if feature.want("imagequant"): _dbg("Looking for imagequant") if _find_include_file(self, "libimagequant.h"): if _find_library_file(self, "imagequant"): - feature.imagequant = "imagequant" + feature.set("imagequant", "imagequant") elif _find_library_file(self, "libimagequant"): - feature.imagequant = "libimagequant" + feature.set("imagequant", "libimagequant") if feature.want("tiff"): _dbg("Looking for tiff") if _find_include_file(self, "tiff.h"): if _find_library_file(self, "tiff"): - feature.tiff = "tiff" + feature.set("tiff", "tiff") if sys.platform in ["win32", "darwin"] and _find_library_file( self, "libtiff" ): - feature.tiff = "libtiff" + feature.set("tiff", "libtiff") if feature.want("freetype"): _dbg("Looking for freetype") @@ -745,31 +772,31 @@ class pil_build_ext(build_ext): freetype_version = 21 break if freetype_version: - feature.freetype = "freetype" + feature.set("freetype", "freetype") if subdir: _add_directory(self.compiler.include_dirs, subdir, 0) - if feature.freetype and feature.want("raqm"): + if feature.get("freetype") and feature.want("raqm"): if not feature.want_vendor("raqm"): # want system Raqm _dbg("Looking for Raqm") if _find_include_file(self, "raqm.h"): if _find_library_file(self, "raqm"): - feature.raqm = "raqm" + feature.set("raqm", "raqm") elif _find_library_file(self, "libraqm"): - feature.raqm = "libraqm" + feature.set("raqm", "libraqm") else: # want to build Raqm from src/thirdparty _dbg("Looking for HarfBuzz") - feature.harfbuzz = None + feature.set("harfbuzz", None) hb_dir = _find_include_dir(self, "harfbuzz", "hb.h") if hb_dir: if isinstance(hb_dir, str): _add_directory(self.compiler.include_dirs, hb_dir, 0) if _find_library_file(self, "harfbuzz"): - feature.harfbuzz = "harfbuzz" - if feature.harfbuzz: + feature.set("harfbuzz", "harfbuzz") + if feature.get("harfbuzz"): if not feature.want_vendor("fribidi"): # want system FriBiDi _dbg("Looking for FriBiDi") - feature.fribidi = None + feature.set("fribidi", None) fribidi_dir = _find_include_dir(self, "fribidi", "fribidi.h") if fribidi_dir: if isinstance(fribidi_dir, str): @@ -777,19 +804,19 @@ class pil_build_ext(build_ext): self.compiler.include_dirs, fribidi_dir, 0 ) if _find_library_file(self, "fribidi"): - feature.fribidi = "fribidi" - feature.raqm = True + feature.set("fribidi", "fribidi") + feature.set("raqm", True) else: # want to build FriBiDi shim from src/thirdparty - feature.raqm = True + feature.set("raqm", True) if feature.want("lcms"): _dbg("Looking for lcms") if _find_include_file(self, "lcms2.h"): if _find_library_file(self, "lcms2"): - feature.lcms = "lcms2" + feature.set("lcms", "lcms2") elif _find_library_file(self, "lcms2_static"): # alternate Windows name. - feature.lcms = "lcms2_static" + feature.set("lcms", "lcms2_static") if feature.want("webp"): _dbg("Looking for webp") @@ -803,17 +830,17 @@ class pil_build_ext(build_ext): _find_library_file(self, prefix + library) for library in ("webp", "webpmux", "webpdemux") ): - feature.webp = prefix + "webp" + feature.set("webp", prefix + "webp") break if feature.want("xcb"): _dbg("Looking for xcb") if _find_include_file(self, "xcb/xcb.h"): if _find_library_file(self, "xcb"): - feature.xcb = "xcb" + feature.set("xcb", "xcb") for f in feature: - if not getattr(feature, f) and feature.require(f): + if not feature.get(f) and feature.require(f): if f in ("jpeg", "zlib"): raise RequiredDependencyException(f) raise DependencyException(f) @@ -821,10 +848,11 @@ class pil_build_ext(build_ext): # # core library - libs = self.add_imaging_libs.split() - defs = [] - if feature.tiff: - libs.append(feature.tiff) + libs: list[str | bool | None] = [] + libs.extend(self.add_imaging_libs.split()) + defs: list[tuple[str, str | None]] = [] + if feature.get("tiff"): + libs.append(feature.get("tiff")) defs.append(("HAVE_LIBTIFF", None)) if sys.platform == "win32": # This define needs to be defined if-and-only-if it was defined @@ -832,22 +860,22 @@ class pil_build_ext(build_ext): # so we have to guess; by default it is defined in all Windows builds. # See #4237, #5243, #5359 for more information. defs.append(("USE_WIN32_FILEIO", None)) - if feature.jpeg: - libs.append(feature.jpeg) + if feature.get("jpeg"): + libs.append(feature.get("jpeg")) defs.append(("HAVE_LIBJPEG", None)) - if feature.jpeg2000: - libs.append(feature.jpeg2000) + if feature.get("jpeg2000"): + libs.append(feature.get("jpeg2000")) defs.append(("HAVE_OPENJPEG", None)) if sys.platform == "win32" and not PLATFORM_MINGW: defs.append(("OPJ_STATIC", None)) - if feature.zlib: - libs.append(feature.zlib) + if feature.get("zlib"): + libs.append(feature.get("zlib")) defs.append(("HAVE_LIBZ", None)) - if feature.imagequant: - libs.append(feature.imagequant) + if feature.get("imagequant"): + libs.append(feature.get("imagequant")) defs.append(("HAVE_LIBIMAGEQUANT", None)) - if feature.xcb: - libs.append(feature.xcb) + if feature.get("xcb"): + libs.append(feature.get("xcb")) defs.append(("HAVE_XCB", None)) if sys.platform == "win32": libs.extend(["kernel32", "user32", "gdi32"]) @@ -861,22 +889,22 @@ class pil_build_ext(build_ext): # # additional libraries - if feature.freetype: + if feature.get("freetype"): srcs = [] libs = ["freetype"] defs = [] - if feature.raqm: + if feature.get("raqm"): if not feature.want_vendor("raqm"): # using system Raqm defs.append(("HAVE_RAQM", None)) defs.append(("HAVE_RAQM_SYSTEM", None)) - libs.append(feature.raqm) + libs.append(feature.get("raqm")) else: # building Raqm from src/thirdparty defs.append(("HAVE_RAQM", None)) srcs.append("src/thirdparty/raqm/raqm.c") - libs.append(feature.harfbuzz) + libs.append(feature.get("harfbuzz")) if not feature.want_vendor("fribidi"): # using system FriBiDi defs.append(("HAVE_FRIBIDI_SYSTEM", None)) - libs.append(feature.fribidi) + libs.append(feature.get("fribidi")) else: # building FriBiDi shim from src/thirdparty srcs.append("src/thirdparty/fribidi-shim/fribidi.c") self._update_extension("PIL._imagingft", libs, defs, srcs) @@ -884,16 +912,17 @@ class pil_build_ext(build_ext): else: self._remove_extension("PIL._imagingft") - if feature.lcms: - extra = [] + if feature.get("lcms"): + libs = [feature.get("lcms")] if sys.platform == "win32": - extra.extend(["user32", "gdi32"]) - self._update_extension("PIL._imagingcms", [feature.lcms] + extra) + libs.extend(["user32", "gdi32"]) + self._update_extension("PIL._imagingcms", libs) else: self._remove_extension("PIL._imagingcms") - if feature.webp: - libs = [feature.webp, feature.webp + "mux", feature.webp + "demux"] + webp = feature.get("webp") + if isinstance(webp, str): + libs = [webp, webp + "mux", webp + "demux"] self._update_extension("PIL._webp", libs) else: self._remove_extension("PIL._webp") @@ -908,14 +937,14 @@ class pil_build_ext(build_ext): self.summary_report(feature) - def summary_report(self, feature): + def summary_report(self, feature: ext_feature) -> None: print("-" * 68) print("PIL SETUP SUMMARY") print("-" * 68) print(f"version Pillow {PILLOW_VERSION}") - v = sys.version.split("[") - print(f"platform {sys.platform} {v[0].strip()}") - for v in v[1:]: + version = sys.version.split("[") + print(f"platform {sys.platform} {version[0].strip()}") + for v in version[1:]: print(f" [{v.strip()}") print("-" * 68) @@ -926,16 +955,20 @@ class pil_build_ext(build_ext): raqm_extra_info += ", FriBiDi shim" options = [ - (feature.jpeg, "JPEG"), - (feature.jpeg2000, "OPENJPEG (JPEG2000)", feature.openjpeg_version), - (feature.zlib, "ZLIB (PNG/ZIP)"), - (feature.imagequant, "LIBIMAGEQUANT"), - (feature.tiff, "LIBTIFF"), - (feature.freetype, "FREETYPE2"), - (feature.raqm, "RAQM (Text shaping)", raqm_extra_info), - (feature.lcms, "LITTLECMS2"), - (feature.webp, "WEBP"), - (feature.xcb, "XCB (X protocol)"), + (feature.get("jpeg"), "JPEG"), + ( + feature.get("jpeg2000"), + "OPENJPEG (JPEG2000)", + feature.get("openjpeg_version"), + ), + (feature.get("zlib"), "ZLIB (PNG/ZIP)"), + (feature.get("imagequant"), "LIBIMAGEQUANT"), + (feature.get("tiff"), "LIBTIFF"), + (feature.get("freetype"), "FREETYPE2"), + (feature.get("raqm"), "RAQM (Text shaping)", raqm_extra_info), + (feature.get("lcms"), "LITTLECMS2"), + (feature.get("webp"), "WEBP"), + (feature.get("xcb"), "XCB (X protocol)"), ] all = 1 @@ -964,7 +997,7 @@ class pil_build_ext(build_ext): print("") -def debug_build(): +def debug_build() -> bool: return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD diff --git a/tox.ini b/tox.ini index 70b8bf145..e79d88500 100644 --- a/tox.ini +++ b/tox.ini @@ -36,4 +36,4 @@ deps = extras = typing commands = - mypy docs src winbuild Tests {posargs} + mypy conftest.py selftest.py setup.py docs src winbuild Tests {posargs} From e14072e9738275ffe2898f32d36cbd2f3c687cf3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 19:08:07 +1000 Subject: [PATCH 154/652] Added further detail --- src/PIL/ImageFont.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 43e86a7ed..abfa4aa80 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -771,14 +771,13 @@ class TransposedFont: def load(filename: str) -> ImageFont: """ - Load a font file. This function loads a font object from the given - bitmap font file, and returns the corresponding font object. + Load a font file. This function loads a font object from the given + bitmap font file, and returns the corresponding font object. For loading TrueType + or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`. :param filename: Name of font file. :return: A font object. :exception OSError: If the file could not be read. - - .. seealso:: :py:func:`PIL.ImageFont.truetype` """ f = ImageFont() f._load_pilfont(filename) @@ -794,7 +793,8 @@ def truetype( ) -> FreeTypeFont: """ Load a TrueType or OpenType font from a file or file-like object, - and create a font object. + and create a font object. For loading bitmap fonts instead, + see :py:func:`~PIL.ImageFont.load` and :py:func:`~PIL.ImageFont.load_path`. This function loads a font object from the given file or file-like object, and creates a font object for a font of the given size. @@ -855,8 +855,6 @@ def truetype( :return: A font object. :exception OSError: If the file could not be read. :exception ValueError: If the font size is not greater than zero. - - .. seealso:: :py:func:`PIL.ImageFont.load` """ def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: From dbe979d032afed04b93f9ce1aaa9c7f8e23542b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 19:09:01 +1000 Subject: [PATCH 155/652] Sort extensions alphabetically in error message --- Tests/test_imagefont.py | 2 +- src/PIL/ImageFont.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 66e6947c2..32cd1db8a 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -469,7 +469,7 @@ def test_load_when_image_not_found(tmp_path: Path) -> None: ImageFont.load(tempfile) root = os.path.splitext(tempfile)[0] - assert str(e.value) == f"cannot find glyph data file {root}.{{png|gif|pbm}}" + assert str(e.value) == f"cannot find glyph data file {root}.{{gif|pbm|png}}" def test_load_path_not_found() -> None: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index abfa4aa80..b6d69019d 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -115,7 +115,7 @@ class ImageFont: if image: image.close() - msg = f"cannot find glyph data file {root}.{{png|gif|pbm}}" + msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}" raise OSError(msg) self.file = fullname From 2c02146cf4c745df531be49e6900005d7df486b7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 7 Sep 2024 12:32:30 +0300 Subject: [PATCH 156/652] Use type hints and fix CVE role Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_imagefont.py | 2 +- docs/deprecations.rst | 2 +- docs/releasenotes/11.0.0.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index cfadc7312..3c916db18 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1154,7 +1154,7 @@ def test_invalid_truetype_sizes_raise_valueerror( def test_freetype_deprecation(monkeypatch: pytest.MonkeyPatch) -> None: # Arrange: mock features.version_module to return fake FreeType version - def fake_version_module(module): + def fake_version_module(module: str) -> str: return "2.9.0" monkeypatch.setattr(features, "version_module", fake_version_module) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 9a2f16cfb..8d3032827 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -21,7 +21,7 @@ Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 (2025-10-15), when FreeType 2.9.1 will be the minimum supported. We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 99e0a2927..0c637cc65 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -52,7 +52,7 @@ Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 (2025-10-15), when FreeType 2.9.1 will be the minimum supported. We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ From a3d23165b9414b17aee42239bead5034d6fa97c5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 19:40:29 +1000 Subject: [PATCH 157/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b43466833..c5beebeba 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Handle duplicate EXIF header #8350 + [zakajd, radarhere] + - Return early from BoxBlur if either width or height is zero #8347 [radarhere] From 547832fd592ee1392533be5bf935c19c690a665f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 19:49:21 +1000 Subject: [PATCH 158/652] Use tempfile.NamedTemporaryFile --- Tests/test_imagefont.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 32cd1db8a..e3d847567 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -461,14 +461,15 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: assert mask.size == (108, 13) -def test_load_when_image_not_found(tmp_path: Path) -> None: - tmpfile = tmp_path / "file.font" - tmpfile.write_bytes(b"") - tempfile = str(tmpfile) +def test_load_when_image_not_found() -> None: + with tempfile.NamedTemporaryFile(delete=False) as tmp: + pass with pytest.raises(OSError) as e: - ImageFont.load(tempfile) + ImageFont.load(tmp.name) - root = os.path.splitext(tempfile)[0] + os.unlink(tmp.name) + + root = os.path.splitext(tmp.name)[0] assert str(e.value) == f"cannot find glyph data file {root}.{{gif|pbm|png}}" @@ -492,8 +493,8 @@ def test_load_path_existing_path() -> None: with pytest.raises(OSError) as e: ImageFont.load_path(tmp.name) - # The file exists, so the error message suggests to use `load` instead - assert tmp.name in str(e.value) + # The file exists, so the error message suggests to use `load` instead + assert tmp.name in str(e.value) assert " did you mean" in str(e.value) From 8b3ef3784bc655e346028a72844d5c6e04083f47 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 8 Sep 2024 08:30:30 +1000 Subject: [PATCH 159/652] Moved Buffer into _typing --- docs/reference/internal_modules.rst | 4 ++++ src/PIL/GifImagePlugin.py | 17 ++++------------- src/PIL/TiffImagePlugin.py | 15 +++------------ src/PIL/_typing.py | 7 ++++++- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 31d60cd83..93fd82cf9 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -33,6 +33,10 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. +.. py:class:: Buffer + + Typing alias. + .. py:class:: IntegralLike Typing alias. diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index a27f7aae9..f206fbb9c 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -29,7 +29,6 @@ import itertools import math import os import subprocess -import sys from enum import IntEnum from functools import cached_property from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union @@ -49,6 +48,7 @@ from ._binary import o16le as o16 if TYPE_CHECKING: from . import _imaging + from ._typing import Buffer class LoadingStrategy(IntEnum): @@ -1157,18 +1157,9 @@ def getdata( class Collector(BytesIO): data = [] - if sys.version_info >= (3, 12): - from collections.abc import Buffer - - def write(self, data: Buffer) -> int: - self.data.append(data) - return len(data) - - else: - - def write(self, data: Any) -> int: - self.data.append(data) - return len(data) + def write(self, data: Buffer) -> int: + self.data.append(data) + return len(data) im.load() # make sure raster data is available diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 53f6ae621..a6b03e171 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -46,7 +46,6 @@ import logging import math import os import struct -import sys import warnings from collections.abc import Iterator, MutableMapping from fractions import Fraction @@ -63,7 +62,7 @@ from ._util import is_path from .TiffTags import TYPES if TYPE_CHECKING: - from ._typing import IntegralLike + from ._typing import Buffer, IntegralLike logger = logging.getLogger(__name__) @@ -2108,16 +2107,8 @@ class AppendingTiffWriter(io.BytesIO): num_tags = self.readShort() self.f.seek(num_tags * 12, os.SEEK_CUR) - if sys.version_info >= (3, 12): - from collections.abc import Buffer - - def write(self, data: Buffer, /) -> int: - return self.f.write(data) - - else: - - def write(self, data: Any, /) -> int: - return self.f.write(data) + def write(self, data: Buffer, /) -> int: + return self.f.write(data) def readShort(self) -> int: (value,) = struct.unpack(self.shortFmt, self.f.read(2)) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 093778464..da49f7c5d 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -15,6 +15,11 @@ if TYPE_CHECKING: except (ImportError, AttributeError): pass +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + Buffer = Any + if sys.version_info >= (3, 10): from typing import TypeGuard else: @@ -40,4 +45,4 @@ class SupportsRead(Protocol[_T_co]): StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] -__all__ = ["IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] +__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] From 89c790f2b03aa44980eaabb6e9b5d312e5002fba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 8 Sep 2024 21:38:21 +1000 Subject: [PATCH 160/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c5beebeba..be94594f8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Removed unused TiffImagePlugin IFD_LEGACY_API #8355 + [radarhere] + - Handle duplicate EXIF header #8350 [zakajd, radarhere] From 6231453895ff9098fde1de8e4be2ddcf0574b1a8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 8 Sep 2024 21:53:08 +1000 Subject: [PATCH 161/652] Group 11.0.0 deprecations --- docs/deprecations.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 8d3032827..bf290a9cd 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,19 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a :py:exc:`DeprecationWarning` is issued. -FreeType 2.9.0 -~~~~~~~~~~~~~~ - -.. deprecated:: 11.0.0 - -Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 -(2025-10-15), when FreeType 2.9.1 will be the minimum supported. - -We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). - -.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ - ImageFile.raise_oserror ~~~~~~~~~~~~~~~~~~~~~~~ @@ -122,6 +109,19 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. +FreeType 2.9.0 +~~~~~~~~~~~~~~ + +.. deprecated:: 11.0.0 + +Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 +(2025-10-15), when FreeType 2.9.1 will be the minimum supported. + +We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 8c738f6427c1be395921f5ef3f1e6e4aa86f4eb7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Sep 2024 13:00:15 +0000 Subject: [PATCH 162/652] Update deadsnakes/action action to v3.2.0 --- .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 1b169db91..e0903f886 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: "pyproject.toml" - name: Set up Python ${{ matrix.python-version }} (free-threaded) - uses: deadsnakes/action@v3.1.0 + uses: deadsnakes/action@v3.2.0 if: "${{ matrix.disable-gil }}" with: python-version: ${{ matrix.python-version }} From 2f13c4588d6287140a7e94bc8bc6c732610bd31e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 8 Sep 2024 16:17:13 +0300 Subject: [PATCH 163/652] Fix underline Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/deprecations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index bf290a9cd..1d2da2381 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -110,7 +110,7 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. FreeType 2.9.0 -~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^ .. deprecated:: 11.0.0 From fe002a72606ff33279ce798f77154f5bf8dcef17 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 01:24:51 +0400 Subject: [PATCH 164/652] Use PyCapsule in _imagingmath --- src/PIL/ImageMath.py | 4 +- src/_imagingmath.c | 145 +++++++++++++++++++++++++++---------------- 2 files changed, 94 insertions(+), 55 deletions(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 191cc2a5f..b32742657 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -65,7 +65,7 @@ class _Operand: except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.unop(op, out.im.id, im_1.im.id) + _imagingmath.unop(op, out.im.ptr, im_1.im.ptr) else: # binary operation im_2 = self.__fixup(im2) @@ -93,7 +93,7 @@ class _Operand: except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) + _imagingmath.binop(op, out.im.ptr, im_1.im.ptr, im_2.im.ptr) return _Operand(out) # unary operators diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 550a10903..eafcf1444 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -23,6 +23,9 @@ #define MAX_INT32 2147483647.0 #define MIN_INT32 -2147483648.0 +#define MATH_FUNC_UNOP_MAGIC "Pillow Math unary func" +#define MATH_FUNC_BINOP_MAGIC "Pillow Math binary func" + #define UNOP(name, op, type) \ void name(Imaging out, Imaging im1) { \ int x, y; \ @@ -168,15 +171,28 @@ _unop(PyObject *self, PyObject *args) { Imaging im1; void (*unop)(Imaging, Imaging); - Py_ssize_t op, i0, i1; - if (!PyArg_ParseTuple(args, "nnn", &op, &i0, &i1)) { + PyObject *op, *i0, *i1; + if (!PyArg_ParseTuple(args, "OOO", &op, &i0, &i1)) { return NULL; } - out = (Imaging)i0; - im1 = (Imaging)i1; + if (!PyCapsule_IsValid(op, MATH_FUNC_UNOP_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", MATH_FUNC_UNOP_MAGIC + ); + return NULL; + } + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } - unop = (void *)op; + unop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_UNOP_MAGIC); + out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); unop(out, im1); @@ -191,16 +207,30 @@ _binop(PyObject *self, PyObject *args) { Imaging im2; void (*binop)(Imaging, Imaging, Imaging); - Py_ssize_t op, i0, i1, i2; - if (!PyArg_ParseTuple(args, "nnnn", &op, &i0, &i1, &i2)) { + PyObject *op, *i0, *i1, *i2; + if (!PyArg_ParseTuple(args, "OOOO", &op, &i0, &i1, &i2)) { return NULL; } - out = (Imaging)i0; - im1 = (Imaging)i1; - im2 = (Imaging)i2; + if (!PyCapsule_IsValid(op, MATH_FUNC_BINOP_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", MATH_FUNC_BINOP_MAGIC + ); + return NULL; + } + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC) || + !PyCapsule_IsValid(i2, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } - binop = (void *)op; + binop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_BINOP_MAGIC); + out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + im2 = (Imaging)PyCapsule_GetPointer(i2, IMAGING_MAGIC); binop(out, im1, im2); @@ -213,8 +243,17 @@ static PyMethodDef _functions[] = { }; static void -install(PyObject *d, char *name, void *value) { - PyObject *v = PyLong_FromSsize_t((Py_ssize_t)value); +install_unary(PyObject *d, char *name, void *func) { + PyObject *v = PyCapsule_New(func, MATH_FUNC_UNOP_MAGIC, NULL); + if (!v || PyDict_SetItemString(d, name, v)) { + PyErr_Clear(); + } + Py_XDECREF(v); +} + +static void +install_binary(PyObject *d, char *name, void *func) { + PyObject *v = PyCapsule_New(func, MATH_FUNC_BINOP_MAGIC, NULL); if (!v || PyDict_SetItemString(d, name, v)) { PyErr_Clear(); } @@ -225,50 +264,50 @@ static int setup_module(PyObject *m) { PyObject *d = PyModule_GetDict(m); - install(d, "abs_I", abs_I); - install(d, "neg_I", neg_I); - install(d, "add_I", add_I); - install(d, "sub_I", sub_I); - install(d, "diff_I", diff_I); - install(d, "mul_I", mul_I); - install(d, "div_I", div_I); - install(d, "mod_I", mod_I); - install(d, "min_I", min_I); - install(d, "max_I", max_I); - install(d, "pow_I", pow_I); + install_unary(d, "abs_I", abs_I); + install_unary(d, "neg_I", neg_I); + install_binary(d, "add_I", add_I); + install_binary(d, "sub_I", sub_I); + install_binary(d, "diff_I", diff_I); + install_binary(d, "mul_I", mul_I); + install_binary(d, "div_I", div_I); + install_binary(d, "mod_I", mod_I); + install_binary(d, "min_I", min_I); + install_binary(d, "max_I", max_I); + install_binary(d, "pow_I", pow_I); - install(d, "invert_I", invert_I); - install(d, "and_I", and_I); - install(d, "or_I", or_I); - install(d, "xor_I", xor_I); - install(d, "lshift_I", lshift_I); - install(d, "rshift_I", rshift_I); + install_unary(d, "invert_I", invert_I); + install_binary(d, "and_I", and_I); + install_binary(d, "or_I", or_I); + install_binary(d, "xor_I", xor_I); + install_binary(d, "lshift_I", lshift_I); + install_binary(d, "rshift_I", rshift_I); - install(d, "eq_I", eq_I); - install(d, "ne_I", ne_I); - install(d, "lt_I", lt_I); - install(d, "le_I", le_I); - install(d, "gt_I", gt_I); - install(d, "ge_I", ge_I); + install_binary(d, "eq_I", eq_I); + install_binary(d, "ne_I", ne_I); + install_binary(d, "lt_I", lt_I); + install_binary(d, "le_I", le_I); + install_binary(d, "gt_I", gt_I); + install_binary(d, "ge_I", ge_I); - install(d, "abs_F", abs_F); - install(d, "neg_F", neg_F); - install(d, "add_F", add_F); - install(d, "sub_F", sub_F); - install(d, "diff_F", diff_F); - install(d, "mul_F", mul_F); - install(d, "div_F", div_F); - install(d, "mod_F", mod_F); - install(d, "min_F", min_F); - install(d, "max_F", max_F); - install(d, "pow_F", pow_F); + install_unary(d, "abs_F", abs_F); + install_unary(d, "neg_F", neg_F); + install_binary(d, "add_F", add_F); + install_binary(d, "sub_F", sub_F); + install_binary(d, "diff_F", diff_F); + install_binary(d, "mul_F", mul_F); + install_binary(d, "div_F", div_F); + install_binary(d, "mod_F", mod_F); + install_binary(d, "min_F", min_F); + install_binary(d, "max_F", max_F); + install_binary(d, "pow_F", pow_F); - install(d, "eq_F", eq_F); - install(d, "ne_F", ne_F); - install(d, "lt_F", lt_F); - install(d, "le_F", le_F); - install(d, "gt_F", gt_F); - install(d, "ge_F", ge_F); + install_binary(d, "eq_F", eq_F); + install_binary(d, "ne_F", ne_F); + install_binary(d, "lt_F", lt_F); + install_binary(d, "le_F", le_F); + install_binary(d, "gt_F", gt_F); + install_binary(d, "ge_F", ge_F); return 0; } From 56bc6a1a71da0d51666689c9d89a21615edfc757 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 01:32:28 +0400 Subject: [PATCH 165/652] Use PyCapsule in _imagingcms --- src/PIL/ImageCms.py | 4 ++-- src/_imagingcms.c | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index ec10230f1..5e256b0ab 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -352,7 +352,7 @@ class ImageCmsTransform(Image.ImagePointHandler): im.load() if imOut is None: imOut = Image.new(self.output_mode, im.size, None) - self.transform.apply(im.im.id, imOut.im.id) + self.transform.apply(im.im.ptr, imOut.im.ptr) imOut.info["icc_profile"] = self.output_profile.tobytes() return imOut @@ -361,7 +361,7 @@ class ImageCmsTransform(Image.ImagePointHandler): if im.mode != self.output_mode: msg = "mode mismatch" raise ValueError(msg) # wrong output mode - self.transform.apply(im.im.id, im.im.id) + self.transform.apply(im.im.ptr, im.im.ptr) im.info["icc_profile"] = self.output_profile.tobytes() return im diff --git a/src/_imagingcms.c b/src/_imagingcms.c index bafe787a7..444b6a602 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -531,23 +531,26 @@ buildProofTransform(PyObject *self, PyObject *args) { static PyObject * cms_transform_apply(CmsTransformObject *self, PyObject *args) { - Py_ssize_t idIn; - Py_ssize_t idOut; + PyObject *i0, *i1; Imaging im; Imaging imOut; - int result; - - if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) { + if (!PyArg_ParseTuple(args, "OO:apply", &i0, &i1)) { return NULL; } - im = (Imaging)idIn; - imOut = (Imaging)idOut; + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } - result = pyCMSdoTransform(im, imOut, self->transform); + im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + imOut = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); - return Py_BuildValue("i", result); + return Py_BuildValue("i", pyCMSdoTransform(im, imOut, self->transform)); } /* -------------------------------------------------------------------- */ From f916b5dc87664b89603f413c179f1ccc6aeab95a Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 01:43:38 +0400 Subject: [PATCH 166/652] Use PyCapsule in _imagingmorph --- Tests/test_imagemorph.py | 8 +++---- src/PIL/ImageMorph.py | 6 ++--- src/_imagingmorph.c | 48 ++++++++++++++++++++++++++++++---------- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 80d8c3815..04860fd61 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -328,13 +328,13 @@ def test_wrong_mode() -> None: iml = Image.new("L", (10, 10)) with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) + _imagingmorph.apply(bytes(lut), imrgb.im.ptr, iml.im.ptr) with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) + _imagingmorph.apply(bytes(lut), iml.im.ptr, imrgb.im.ptr) with pytest.raises(RuntimeError): - _imagingmorph.match(bytes(lut), imrgb.im.id) + _imagingmorph.match(bytes(lut), imrgb.im.ptr) # Should not raise - _imagingmorph.match(bytes(lut), iml.im.id) + _imagingmorph.match(bytes(lut), iml.im.ptr) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 6a43983d3..bc7ed66d3 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -213,7 +213,7 @@ class MorphOp: msg = "Image mode must be L" raise ValueError(msg) outimage = Image.new(image.mode, image.size, None) - count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) + count = _imagingmorph.apply(bytes(self.lut), image.im.ptr, outimage.im.ptr) return count, outimage def match(self, image: Image.Image) -> list[tuple[int, int]]: @@ -229,7 +229,7 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) - return _imagingmorph.match(bytes(self.lut), image.im.id) + return _imagingmorph.match(bytes(self.lut), image.im.ptr) def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: """Get a list of all turned on pixels in a binary image @@ -240,7 +240,7 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) - return _imagingmorph.get_on_pixels(image.im.id) + return _imagingmorph.get_on_pixels(image.im.ptr) def load_lut(self, filename: str) -> None: """Load an operator from an mrl file""" diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 614dfbe7f..c7933cc7d 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -30,15 +30,15 @@ static PyObject * apply(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut; - Py_ssize_t lut_len, i0, i1; + PyObject *py_lut, *i0, *i1; + Py_ssize_t lut_len; Imaging imgin, imgout; int width, height; int row_idx, col_idx; UINT8 **inrows, **outrows; int num_changed_pixels = 0; - if (!PyArg_ParseTuple(args, "Onn", &py_lut, &i0, &i1)) { + if (!PyArg_ParseTuple(args, "OOO", &py_lut, &i0, &i1)) { PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } @@ -57,8 +57,16 @@ apply(PyObject *self, PyObject *args) { lut = PyBytes_AsString(py_lut); - imgin = (Imaging)i0; - imgout = (Imaging)i1; + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } + + imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + imgout = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); width = imgin->xsize; height = imgin->ysize; @@ -129,8 +137,8 @@ apply(PyObject *self, PyObject *args) { static PyObject * match(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut; - Py_ssize_t lut_len, i0; + PyObject *py_lut, *i0; + Py_ssize_t lut_len; Imaging imgin; int width, height; int row_idx, col_idx; @@ -140,7 +148,7 @@ match(PyObject *self, PyObject *args) { return NULL; } - if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) { + if (!PyArg_ParseTuple(args, "OO", &py_lut, &i0)) { Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; @@ -161,7 +169,15 @@ match(PyObject *self, PyObject *args) { } lut = PyBytes_AsString(py_lut); - imgin = (Imaging)i0; + + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } + + imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { Py_DECREF(ret); @@ -215,7 +231,7 @@ match(PyObject *self, PyObject *args) { */ static PyObject * get_on_pixels(PyObject *self, PyObject *args) { - Py_ssize_t i0; + PyObject *i0; Imaging img; UINT8 **rows; int row_idx, col_idx; @@ -225,12 +241,20 @@ get_on_pixels(PyObject *self, PyObject *args) { return NULL; } - if (!PyArg_ParseTuple(args, "n", &i0)) { + if (!PyArg_ParseTuple(args, "O", &i0)) { Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } - img = (Imaging)i0; + + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } + + img = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); rows = img->image8; width = img->xsize; height = img->ysize; From 7f48567002a74a9f102dc3b183f872846e2925b3 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 02:00:07 +0400 Subject: [PATCH 167/652] Use s# in PyArg_ParseTuple --- src/_imagingmorph.c | 55 ++++++++++--------------------------- src/libImaging/ImPlatform.h | 1 + 2 files changed, 16 insertions(+), 40 deletions(-) diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index c7933cc7d..715cccf51 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -11,7 +11,6 @@ * See the README file for information on usage and redistribution. */ -#include "Python.h" #include "libImaging/Imaging.h" #define LUT_SIZE (1 << 9) @@ -30,7 +29,7 @@ static PyObject * apply(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut, *i0, *i1; + PyObject *i0, *i1; Py_ssize_t lut_len; Imaging imgin, imgout; int width, height; @@ -38,25 +37,15 @@ apply(PyObject *self, PyObject *args) { UINT8 **inrows, **outrows; int num_changed_pixels = 0; - if (!PyArg_ParseTuple(args, "OOO", &py_lut, &i0, &i1)) { - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); + if (!PyArg_ParseTuple(args, "s#OO", &lut, &lut_len, &i0, &i1)) { return NULL; } - if (!PyBytes_Check(py_lut)) { - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); - return NULL; - } - - lut_len = PyBytes_Size(py_lut); - if (lut_len < LUT_SIZE) { PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } - lut = PyBytes_AsString(py_lut); - if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { PyErr_Format( @@ -137,39 +126,22 @@ apply(PyObject *self, PyObject *args) { static PyObject * match(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut, *i0; + PyObject *i0; Py_ssize_t lut_len; Imaging imgin; int width, height; int row_idx, col_idx; UINT8 **inrows; - PyObject *ret = PyList_New(0); - if (ret == NULL) { + + if (!PyArg_ParseTuple(args, "s#O", &lut, &lut_len, &i0)) { return NULL; } - if (!PyArg_ParseTuple(args, "OO", &py_lut, &i0)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); - return NULL; - } - - if (!PyBytes_Check(py_lut)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); - return NULL; - } - - lut_len = PyBytes_Size(py_lut); - if (lut_len < LUT_SIZE) { - Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } - lut = PyBytes_AsString(py_lut); - if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { PyErr_Format( PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC @@ -180,11 +152,15 @@ match(PyObject *self, PyObject *args) { imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { - Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } + PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } + inrows = imgin->image8; width = imgin->xsize; height = imgin->ysize; @@ -236,14 +212,8 @@ get_on_pixels(PyObject *self, PyObject *args) { UINT8 **rows; int row_idx, col_idx; int width, height; - PyObject *ret = PyList_New(0); - if (ret == NULL) { - return NULL; - } if (!PyArg_ParseTuple(args, "O", &i0)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } @@ -259,6 +229,11 @@ get_on_pixels(PyObject *self, PyObject *args) { width = img->xsize; height = img->ysize; + PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } + for (row_idx = 0; row_idx < height; row_idx++) { UINT8 *row = rows[row_idx]; for (col_idx = 0; col_idx < width; col_idx++) { diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index f6e7fb6b9..c9b7e43b4 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -7,6 +7,7 @@ * Copyright (c) Fredrik Lundh 1995-2003. */ +#define PY_SSIZE_T_CLEAN #include "Python.h" /* Check that we have an ANSI compliant compiler */ From 5428e3568e09d69241b03827eecb08d7b5dc9ea9 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 02:19:44 +0400 Subject: [PATCH 168/652] Deprecate ImageCore.id and ImageCore.unsafe_ptrs --- Tests/test_image_getim.py | 12 ++++++++++-- docs/deprecations.rst | 10 ++++++++++ src/_imaging.c | 14 ++++++++++++++ src/libImaging/Imaging.h | 2 +- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 9afa02b0a..75d97cd44 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,11 +1,19 @@ from __future__ import annotations +import pytest + from .helper import hopper def test_sanity() -> None: im = hopper() - type_repr = repr(type(im.getim())) + type_repr = repr(type(im.getim())) assert "PyCapsule" in type_repr - assert isinstance(im.im.id, int) + + with pytest.warns(DeprecationWarning): + assert isinstance(im.im.id, int) + + with pytest.warns(DeprecationWarning): + ptrs = dict(im.im.unsafe_ptrs) + assert all(k in ptrs for k in ["image8", "image32", "image"]) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index cafda3803..abb902c88 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -136,6 +136,16 @@ Specific WebP Feature Checks ``True`` if the WebP module is installed, until they are removed in Pillow 12.0.0 (2025-10-15). +Get Internal Pointers to Objects +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``Image.core.ImageCore.id`` and ``Image.core.ImageCore.unsafe_ptrs`` +have been deprecated and will be removed in Pillow 12 (2025-10-15). +They were used for obtaining raw pointers to ``ImageCore`` internals. To interact with +C code, you can use ``Image.core.ImageCore.ptr``, which returns a ``PyCapsule`` object. + Removed features ---------------- diff --git a/src/_imaging.c b/src/_imaging.c index 07d9a64cc..a455c873b 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3707,6 +3707,13 @@ _getattr_bands(ImagingObject *self, void *closure) { static PyObject * _getattr_id(ImagingObject *self, void *closure) { + if (PyErr_WarnEx( + PyExc_DeprecationWarning, + "id property is deprecated and will be removed in Pillow 12.0", + 1 + ) < 0) { + return NULL; + } return PyLong_FromSsize_t((Py_ssize_t)self->image); } @@ -3717,6 +3724,13 @@ _getattr_ptr(ImagingObject *self, void *closure) { static PyObject * _getattr_unsafe_ptrs(ImagingObject *self, void *closure) { + if (PyErr_WarnEx( + PyExc_DeprecationWarning, + "unsafe_ptrs property is deprecated and will be removed in Pillow 12.0", + 1 + ) < 0) { + return NULL; + } return Py_BuildValue( "(sn)(sn)(sn)", "image8", diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 321dce988..870f5ed97 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -61,7 +61,7 @@ typedef struct ImagingOutlineInstance *ImagingOutline; typedef struct ImagingPaletteInstance *ImagingPalette; /* handle magics (used with PyCObject). */ -#define IMAGING_MAGIC "PIL Imaging" +#define IMAGING_MAGIC "Pillow Imaging" /* pixel types */ #define IMAGING_TYPE_UINT8 0 From cb3a4e6a00fd41e42afa78b4289e5e1056fc0e81 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 03:45:18 +0400 Subject: [PATCH 169/652] Remove legacy 1-bit api, fix AttributeError PytestUnraisableExceptionWarning: Exception ignored in: AttributeError: 'PhotoImage' object has no attribute '_PhotoImage__photo' --- src/PIL/ImageTk.py | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index a788c9d1f..d546989ee 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -35,20 +35,6 @@ from . import Image, ImageFile # -------------------------------------------------------------------- # Check for Tkinter interface hooks -_pilbitmap_ok = None - - -def _pilbitmap_check() -> int: - global _pilbitmap_ok - if _pilbitmap_ok is None: - try: - im = Image.new("1", (1, 1)) - tkinter.BitmapImage(data=f"PIL:{im.im.id}") - _pilbitmap_ok = 1 - except tkinter.TclError: - _pilbitmap_ok = 0 - return _pilbitmap_ok - def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: source = None @@ -142,6 +128,8 @@ class PhotoImage: self.paste(image) def __del__(self) -> None: + if not hasattr(self, "__photo"): + return name = self.__photo.name self.__photo.name = None try: @@ -225,17 +213,11 @@ class BitmapImage: self.__mode = image.mode self.__size = image.size - if _pilbitmap_check(): - # fast way (requires the pilbitmap booster patch) - image.load() - kw["data"] = f"PIL:{image.im.id}" - self.__im = image # must keep a reference - else: - # slow but safe way - kw["data"] = image.tobitmap() - self.__photo = tkinter.BitmapImage(**kw) + self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw) def __del__(self) -> None: + if not hasattr(self, "__photo"): + return name = self.__photo.name self.__photo.name = None try: From ee65b305b1fd041b679382003b4ce727bb03392d Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 04:29:34 +0400 Subject: [PATCH 170/652] Use PyCapsule in _imagingtk --- src/PIL/ImageTk.py | 11 +++++------ src/Tk/tkImaging.c | 35 ++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index d546989ee..279a21716 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -48,18 +48,18 @@ def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: def _pyimagingtkcall( - command: str, photo: PhotoImage | tkinter.PhotoImage, id: int + command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: object ) -> None: tk = photo.tk try: - tk.call(command, photo, id) + tk.call(command, photo, repr(ptr)) except tkinter.TclError: # activate Tkinter hook # may raise an error if it cannot attach to Tkinter from . import _imagingtk _imagingtk.tkinit(tk.interpaddr()) - tk.call(command, photo, id) + tk.call(command, photo, repr(ptr)) # -------------------------------------------------------------------- @@ -181,7 +181,7 @@ class PhotoImage: block = image.new_block(self.__mode, im.size) image.convert2(block, image) # convert directly between buffers - _pyimagingtkcall("PyImagingPhoto", self.__photo, block.id) + _pyimagingtkcall("PyImagingPhoto", self.__photo, block.ptr) # -------------------------------------------------------------------- @@ -255,9 +255,8 @@ class BitmapImage: def getimage(photo: PhotoImage) -> Image.Image: """Copies the contents of a PhotoImage to a PIL image memory.""" im = Image.new("RGBA", (photo.width(), photo.height())) - block = im.im - _pyimagingtkcall("PyImagingPhotoGet", photo, block.id) + _pyimagingtkcall("PyImagingPhotoGet", photo, im.im.ptr) return im diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 727ee6bed..b33a47d30 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -56,19 +56,36 @@ static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; static Imaging ImagingFind(const char *name) { - Py_ssize_t id; + PyObject *capsule; + int direct_pointer = 0; + const char *expected = "capsule object \"" IMAGING_MAGIC "\" at 0x"; - /* FIXME: use CObject instead? */ -#if defined(_WIN64) - id = _atoi64(name); -#else - id = atol(name); -#endif - if (!id) { + if (name[0] == '<') { + name++; + } else { + // Special case for PyPy, where the string representation of a Capsule + // refers directly to the pointer itself, not to the PyCapsule object. + direct_pointer = 1; + } + + if (strncmp(name, expected, strlen(expected))) { return NULL; } - return (Imaging)id; + capsule = (PyObject *)strtoull(name + strlen(expected), NULL, 16); + + if (direct_pointer) { + return (Imaging)capsule; + } + + if (!PyCapsule_IsValid(capsule, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } + + return (Imaging)PyCapsule_GetPointer(capsule, IMAGING_MAGIC); } static int From 882ac783eb67a13c1ff8afba9369d1603328718e Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 13:04:57 +0400 Subject: [PATCH 171/652] Use PyCapsule in _imagingft --- src/_imagingft.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 5c446e641..c539ec267 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -830,7 +830,6 @@ font_render(FontObject *self, PyObject *args) { 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? */ int color = 0; /* is FT_LOAD_COLOR enabled? */ int stroke_width = 0; @@ -929,10 +928,9 @@ font_render(FontObject *self, PyObject *args) { PyMem_Del(glyph_info); return NULL; } - PyObject *imageId = PyObject_GetAttrString(image, "id"); - id = PyLong_AsSsize_t(imageId); - Py_XDECREF(imageId); - im = (Imaging)id; + PyObject *imagePtr = PyObject_GetAttrString(image, "ptr"); + im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC); + Py_XDECREF(imagePtr); x_offset -= stroke_width; y_offset -= stroke_width; From 934ae12ed5e98b7d2ec14d1946fad3bce6a42924 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 3 Sep 2024 11:54:24 +0400 Subject: [PATCH 172/652] rename PyCapsule -> Capsule --- docs/deprecations.rst | 2 +- src/Tk/tkImaging.c | 4 +--- src/_imagingcms.c | 4 +--- src/_imagingmath.c | 16 ++++------------ src/_imagingmorph.c | 12 +++--------- 5 files changed, 10 insertions(+), 28 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index abb902c88..dd271a97f 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -144,7 +144,7 @@ Get Internal Pointers to Objects ``Image.core.ImageCore.id`` and ``Image.core.ImageCore.unsafe_ptrs`` have been deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining raw pointers to ``ImageCore`` internals. To interact with -C code, you can use ``Image.core.ImageCore.ptr``, which returns a ``PyCapsule`` object. +C code, you can use ``Image.core.ImageCore.ptr``, which returns a ``Capsule`` object. Removed features ---------------- diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index b33a47d30..a36c3e0bd 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -79,9 +79,7 @@ ImagingFind(const char *name) { } if (!PyCapsule_IsValid(capsule, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 444b6a602..1823bcf03 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -541,9 +541,7 @@ cms_transform_apply(CmsTransformObject *self, PyObject *args) { if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } diff --git a/src/_imagingmath.c b/src/_imagingmath.c index eafcf1444..dbe636707 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -177,16 +177,12 @@ _unop(PyObject *self, PyObject *args) { } if (!PyCapsule_IsValid(op, MATH_FUNC_UNOP_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", MATH_FUNC_UNOP_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", MATH_FUNC_UNOP_MAGIC); return NULL; } if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } @@ -213,17 +209,13 @@ _binop(PyObject *self, PyObject *args) { } if (!PyCapsule_IsValid(op, MATH_FUNC_BINOP_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", MATH_FUNC_BINOP_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", MATH_FUNC_BINOP_MAGIC); return NULL; } if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC) || !PyCapsule_IsValid(i2, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 715cccf51..637295be7 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -48,9 +48,7 @@ apply(PyObject *self, PyObject *args) { if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } @@ -143,9 +141,7 @@ match(PyObject *self, PyObject *args) { } if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } @@ -218,9 +214,7 @@ get_on_pixels(PyObject *self, PyObject *args) { } if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } From d29fa73ea608449f86082dc312eb132a10901fd0 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Thu, 5 Sep 2024 01:04:35 +0400 Subject: [PATCH 173/652] Move new_block to module --- src/PIL/ImageTk.py | 2 +- src/_imaging.c | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 279a21716..5bad9b546 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -178,7 +178,7 @@ class PhotoImage: if image.isblock() and im.mode == self.__mode: block = image else: - block = image.new_block(self.__mode, im.size) + block = Image.core.new_block(self.__mode, im.size) image.convert2(block, image) # convert directly between buffers _pyimagingtkcall("PyImagingPhoto", self.__photo, block.ptr) diff --git a/src/_imaging.c b/src/_imaging.c index a455c873b..e227d8efc 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3674,15 +3674,12 @@ static struct PyMethodDef methods[] = { /* Unsharpmask extension */ {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, - {"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, /* Special effects */ {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, /* Misc. */ - {"new_block", (PyCFunction)_new_block, METH_VARARGS}, - {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, {NULL, NULL} /* sentinel */ @@ -4212,6 +4209,7 @@ static PyMethodDef functions[] = { {"blend", (PyCFunction)_blend, METH_VARARGS}, {"fill", (PyCFunction)_fill, METH_VARARGS}, {"new", (PyCFunction)_new, METH_VARARGS}, + {"new_block", (PyCFunction)_new_block, METH_VARARGS}, {"merge", (PyCFunction)_merge, METH_VARARGS}, /* Functions */ From 0a8e6dbedb84b50ca2ca8762ae1279a66c385bad Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sat, 31 Aug 2024 20:41:37 +0400 Subject: [PATCH 174/652] Use im.has_transparency_data for webp._save_all Also: remove _VALID_WEBP_MODES and _VALID_WEBP_LEGACY_MODES consts RGBX is not faster RGB since demands more bandwidth Do not convert to str paths in tests --- Tests/test_file_webp.py | 27 ++++++++++++--------------- src/PIL/WebPImagePlugin.py | 25 +++++-------------------- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index e75e3ddd2..719831db9 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -72,7 +72,7 @@ class TestFileWebp: def _roundtrip( self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {} ) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" hopper(mode).save(temp_file, **args) with Image.open(temp_file) as image: @@ -116,7 +116,7 @@ class TestFileWebp: assert buffer_no_args.getbuffer() != buffer_method.getbuffer() def test_save_all(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" im = Image.new("RGB", (1, 1)) im2 = Image.new("RGB", (1, 1), "#f00") im.save(temp_file, save_all=True, append_images=[im2]) @@ -151,18 +151,16 @@ class TestFileWebp: @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) with pytest.raises(ValueError) as e: - im.save(temp_file, method=0) + im.save(tmp_path / "temp.webp", method=0) assert str(e.value) == "encoding error 6" @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") im = Image.new("L", (16384, 16384)) with pytest.raises(ValueError) as e: - im.save(temp_file) + im.save(tmp_path / "temp.webp") assert ( str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" @@ -187,9 +185,8 @@ class TestFileWebp: def test_no_resource_warning(self, tmp_path: Path) -> None: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: - temp_file = str(tmp_path / "temp.webp") with warnings.catch_warnings(): - image.save(temp_file) + image.save(tmp_path / "temp.webp") def test_file_pointer_could_be_reused(self) -> None: file_path = "Tests/images/hopper.webp" @@ -204,15 +201,16 @@ class TestFileWebp: def test_invalid_background( self, background: int | tuple[int, ...], tmp_path: Path ) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" im = hopper() with pytest.raises(OSError): im.save(temp_file, save_all=True, append_images=[im], background=background) def test_background_from_gif(self, tmp_path: Path) -> None: + out_webp = tmp_path / "temp.webp" + # Save L mode GIF with background with Image.open("Tests/images/no_palette_with_background.gif") as im: - out_webp = str(tmp_path / "temp.webp") im.save(out_webp, save_all=True) # Save P mode GIF with background @@ -220,11 +218,10 @@ class TestFileWebp: original_value = im.convert("RGB").getpixel((1, 1)) # Save as WEBP - out_webp = str(tmp_path / "temp.webp") im.save(out_webp, save_all=True) # Save as GIF - out_gif = str(tmp_path / "temp.gif") + out_gif = tmp_path / "temp.gif" with Image.open(out_webp) as im: im.save(out_gif) @@ -234,10 +231,10 @@ class TestFileWebp: assert difference < 5 def test_duration(self, tmp_path: Path) -> None: + out_webp = tmp_path / "temp.webp" + with Image.open("Tests/images/dispose_bgnd.gif") as im: assert im.info["duration"] == 1000 - - out_webp = str(tmp_path / "temp.webp") im.save(out_webp, save_all=True) with Image.open(out_webp) as reloaded: @@ -245,7 +242,7 @@ class TestFileWebp: assert reloaded.info["duration"] == 1000 def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" im = Image.new("RGBA", (1, 1)).convert("P") assert im.mode == "P" assert im.palette is not None diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index f8d6168ba..3754d784a 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -13,10 +13,6 @@ except ImportError: SUPPORTED = False -_VALID_WEBP_MODES = {"RGBX": True, "RGBA": True, "RGB": True} - -_VALID_WEBP_LEGACY_MODES = {"RGB": True, "RGBA": True} - _VP8_MODES_BY_IDENTIFIER = { b"VP8 ": "RGB", b"VP8X": "RGBA", @@ -247,27 +243,16 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Make sure image mode is supported frame = ims - rawmode = ims.mode - if ims.mode not in _VALID_WEBP_MODES: - alpha = ( - "A" in ims.mode - or "a" in ims.mode - or (ims.mode == "P" and "A" in ims.im.getpalettemode()) - ) - rawmode = "RGBA" if alpha else "RGB" - frame = ims.convert(rawmode) - - if rawmode == "RGB": - # For faster conversion, use RGBX - rawmode = "RGBX" + if frame.mode not in ("RGBX", "RGBA", "RGB"): + frame = frame.convert("RGBA" if im.has_transparency_data else "RGB") # Append the frame to the animation encoder enc.add( - frame.tobytes("raw", rawmode), + frame.tobytes(), round(timestamp), frame.size[0], frame.size[1], - rawmode, + frame.mode, lossless, quality, alpha_quality, @@ -310,7 +295,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: method = im.encoderinfo.get("method", 4) exact = 1 if im.encoderinfo.get("exact") else 0 - if im.mode not in _VALID_WEBP_LEGACY_MODES: + if im.mode not in ("RGB", "RGBA"): im = im.convert("RGBA" if im.has_transparency_data else "RGB") data = _webp.WebPEncode( From 8bb3134b1d2fa2a13111a6555e09bbc26b96ff7a Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 1 Sep 2024 19:41:45 +0400 Subject: [PATCH 175/652] call _webp.WebPEncode with ptr --- src/PIL/WebPImagePlugin.py | 7 +--- src/_webp.c | 75 ++++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 3754d784a..3eeba9400 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -295,17 +295,14 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: method = im.encoderinfo.get("method", 4) exact = 1 if im.encoderinfo.get("exact") else 0 - if im.mode not in ("RGB", "RGBA"): + if im.mode not in ("RGBX", "RGBA", "RGB"): im = im.convert("RGBA" if im.has_transparency_data else "RGB") data = _webp.WebPEncode( - im.tobytes(), - im.size[0], - im.size[1], + im.im.ptr, lossless, float(quality), float(alpha_quality), - im.mode, icc_profile, method, exact, diff --git a/src/_webp.c b/src/_webp.c index f59ad3036..bfd9de5c0 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -566,32 +566,65 @@ static PyTypeObject WebPAnimDecoder_Type = { 0, /*tp_getset*/ }; +/* -------------------------------------------------------------------- */ +/* Frame import */ +/* -------------------------------------------------------------------- */ + +static int +import_frame_libwebp(WebPPicture *frame, Imaging im) { + UINT32 mask = 0; + + if (strcmp(im->mode, "RGBA") && strcmp(im->mode, "RGB") && strcmp(im->mode, "RGBX")) { + PyErr_SetString(PyExc_ValueError, "unsupported image mode"); + return -1; + } + + if (strcmp(im->mode, "RGBA")) { + mask = MASK_UINT32_CHANNEL_3; + } + + frame->width = im->xsize; + frame->height = im->ysize; + frame->use_argb = 1; // Don't convert RGB pixels to YUV + + if (!WebPPictureAlloc(frame)) { + PyErr_SetString(PyExc_MemoryError, "can't allocate picture frame"); + return -2; + } + + for (int y = 0; y < im->ysize; ++y) { + UINT8 *src = (UINT8 *)im->image32[y]; + UINT32 *dst = frame->argb + frame->argb_stride * y; + for (int x = 0; x < im->xsize; ++x) { + UINT32 pix = MAKE_UINT32(src[x * 4 + 2], src[x * 4 + 1], src[x * 4], src[x * 4 + 3]); + dst[x] = pix | mask; + } + } + + return 0; +} + /* -------------------------------------------------------------------- */ /* Legacy WebP Support */ /* -------------------------------------------------------------------- */ PyObject * WebPEncode_wrapper(PyObject *self, PyObject *args) { - int width; - int height; int lossless; float quality_factor; float alpha_quality_factor; int method; int exact; - uint8_t *rgb; + Imaging im; + PyObject *i0; uint8_t *icc_bytes; uint8_t *exif_bytes; uint8_t *xmp_bytes; uint8_t *output; - char *mode; - Py_ssize_t size; Py_ssize_t icc_size; Py_ssize_t exif_size; Py_ssize_t xmp_size; size_t ret_size; - int rgba_mode; - int channels; int ok; ImagingSectionCookie cookie; WebPConfig config; @@ -600,15 +633,11 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "y#iiiffss#iis#s#", - (char **)&rgb, - &size, - &width, - &height, + "Oiffs#iis#s#", + &i0, &lossless, &quality_factor, &alpha_quality_factor, - &mode, &icc_bytes, &icc_size, &method, @@ -621,15 +650,12 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { return NULL; } - rgba_mode = strcmp(mode, "RGBA") == 0; - if (!rgba_mode && strcmp(mode, "RGB") != 0) { - Py_RETURN_NONE; + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); + return NULL; } - channels = rgba_mode ? 4 : 3; - if (size < width * height * channels) { - Py_RETURN_NONE; - } + im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); // Setup config for this frame if (!WebPConfigInit(&config)) { @@ -652,14 +678,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { PyErr_SetString(PyExc_ValueError, "could not initialise picture"); return NULL; } - pic.width = width; - pic.height = height; - pic.use_argb = 1; // Don't convert RGB pixels to YUV - if (rgba_mode) { - WebPPictureImportRGBA(&pic, rgb, channels * width); - } else { - WebPPictureImportRGB(&pic, rgb, channels * width); + if (import_frame_libwebp(&pic, im)) { + return NULL; } WebPMemoryWriterInit(&writer); From 0962b468b71ea9cdb99417d091c75e86c29f768a Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 3 Sep 2024 13:47:31 +0400 Subject: [PATCH 176/652] ImagingSectionEnter for WebPAnimEncoder --- src/_webp.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index bfd9de5c0..ede261df6 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -190,6 +190,7 @@ _anim_encoder_add(PyObject *self, PyObject *args) { float quality_factor; float alpha_quality_factor; int method; + ImagingSectionCookie cookie; WebPConfig config; WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; WebPAnimEncoder *enc = encp->enc; @@ -246,8 +247,11 @@ _anim_encoder_add(PyObject *self, PyObject *args) { WebPPictureImportRGB(frame, rgb, 3 * width); } - // Add the frame to the encoder - if (!WebPAnimEncoderAdd(enc, frame, timestamp, &config)) { + ImagingSectionEnter(&cookie); + int ok = WebPAnimEncoderAdd(enc, frame, timestamp, &config); + ImagingSectionLeave(&cookie); + + if (!ok) { PyErr_SetString(PyExc_RuntimeError, WebPAnimEncoderGetError(enc)); return NULL; } From 4d271c8ec87355962e0345a00d9e4032007a13c5 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 3 Sep 2024 13:58:40 +0400 Subject: [PATCH 177/652] import_frame for anim_encoder_add --- src/PIL/WebPImagePlugin.py | 7 +-- src/_webp.c | 114 ++++++++++++++++++------------------- 2 files changed, 56 insertions(+), 65 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 3eeba9400..8251316d3 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -248,11 +248,8 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Append the frame to the animation encoder enc.add( - frame.tobytes(), + frame.im.ptr, round(timestamp), - frame.size[0], - frame.size[1], - frame.mode, lossless, quality, alpha_quality, @@ -270,7 +267,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: im.seek(cur_idx) # Force encoder to flush frames - enc.add(None, round(timestamp), 0, 0, "", lossless, quality, alpha_quality, 0) + enc.add(None, round(timestamp), lossless, quality, alpha_quality, 0) # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) diff --git a/src/_webp.c b/src/_webp.c index ede261df6..9717b9bc0 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -83,6 +83,46 @@ HandleMuxError(WebPMuxError err, char *chunk) { return NULL; } +/* -------------------------------------------------------------------- */ +/* Frame import */ +/* -------------------------------------------------------------------- */ + +static int +import_frame_libwebp(WebPPicture *frame, Imaging im) { + UINT32 mask = 0; + + if (strcmp(im->mode, "RGBA") && strcmp(im->mode, "RGB") && + strcmp(im->mode, "RGBX")) { + PyErr_SetString(PyExc_ValueError, "unsupported image mode"); + return -1; + } + + if (strcmp(im->mode, "RGBA")) { + mask = MASK_UINT32_CHANNEL_3; + } + + frame->width = im->xsize; + frame->height = im->ysize; + frame->use_argb = 1; // Don't convert RGB pixels to YUV + + if (!WebPPictureAlloc(frame)) { + PyErr_SetString(PyExc_MemoryError, "can't allocate picture frame"); + return -2; + } + + for (int y = 0; y < im->ysize; ++y) { + UINT8 *src = (UINT8 *)im->image32[y]; + UINT32 *dst = frame->argb + frame->argb_stride * y; + for (int x = 0; x < im->xsize; ++x) { + UINT32 pix = + MAKE_UINT32(src[x * 4 + 2], src[x * 4 + 1], src[x * 4], src[x * 4 + 3]); + dst[x] = pix | mask; + } + } + + return 0; +} + /* -------------------------------------------------------------------- */ /* WebP Animation Support */ /* -------------------------------------------------------------------- */ @@ -180,12 +220,9 @@ _anim_encoder_dealloc(PyObject *self) { PyObject * _anim_encoder_add(PyObject *self, PyObject *args) { - uint8_t *rgb; - Py_ssize_t size; + PyObject *i0; + Imaging im; int timestamp; - int width; - int height; - char *mode; int lossless; float quality_factor; float alpha_quality_factor; @@ -198,13 +235,9 @@ _anim_encoder_add(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "z#iiisiffi", - (char **)&rgb, - &size, + "Oiiffi", + &i0, ×tamp, - &width, - &height, - &mode, &lossless, &quality_factor, &alpha_quality_factor, @@ -214,11 +247,18 @@ _anim_encoder_add(PyObject *self, PyObject *args) { } // Check for NULL frame, which sets duration of final frame - if (!rgb) { + if (i0 == Py_None) { WebPAnimEncoderAdd(enc, NULL, timestamp, NULL); Py_RETURN_NONE; } + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); + return NULL; + } + + im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + // Setup config for this frame if (!WebPConfigInit(&config)) { PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); @@ -235,16 +275,8 @@ _anim_encoder_add(PyObject *self, PyObject *args) { return NULL; } - // Populate the frame with raw bytes passed to us - frame->width = width; - frame->height = height; - frame->use_argb = 1; // Don't convert RGB pixels to YUV - if (strcmp(mode, "RGBA") == 0) { - WebPPictureImportRGBA(frame, rgb, 4 * width); - } else if (strcmp(mode, "RGBX") == 0) { - WebPPictureImportRGBX(frame, rgb, 4 * width); - } else { - WebPPictureImportRGB(frame, rgb, 3 * width); + if (import_frame_libwebp(frame, im)) { + return NULL; } ImagingSectionEnter(&cookie); @@ -570,44 +602,6 @@ static PyTypeObject WebPAnimDecoder_Type = { 0, /*tp_getset*/ }; -/* -------------------------------------------------------------------- */ -/* Frame import */ -/* -------------------------------------------------------------------- */ - -static int -import_frame_libwebp(WebPPicture *frame, Imaging im) { - UINT32 mask = 0; - - if (strcmp(im->mode, "RGBA") && strcmp(im->mode, "RGB") && strcmp(im->mode, "RGBX")) { - PyErr_SetString(PyExc_ValueError, "unsupported image mode"); - return -1; - } - - if (strcmp(im->mode, "RGBA")) { - mask = MASK_UINT32_CHANNEL_3; - } - - frame->width = im->xsize; - frame->height = im->ysize; - frame->use_argb = 1; // Don't convert RGB pixels to YUV - - if (!WebPPictureAlloc(frame)) { - PyErr_SetString(PyExc_MemoryError, "can't allocate picture frame"); - return -2; - } - - for (int y = 0; y < im->ysize; ++y) { - UINT8 *src = (UINT8 *)im->image32[y]; - UINT32 *dst = frame->argb + frame->argb_stride * y; - for (int x = 0; x < im->xsize; ++x) { - UINT32 pix = MAKE_UINT32(src[x * 4 + 2], src[x * 4 + 1], src[x * 4], src[x * 4 + 3]); - dst[x] = pix | mask; - } - } - - return 0; -} - /* -------------------------------------------------------------------- */ /* Legacy WebP Support */ /* -------------------------------------------------------------------- */ From 608a5e83c067baa91e555b24b3cc577c3704153c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 9 Sep 2024 12:56:28 +1000 Subject: [PATCH 178/652] Updated harfbuzz to 9.0.0, except on manylinux2014 --- .github/workflows/wheels-dependencies.sh | 47 +++++++++++++++++------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97f70ed84..c25e1a27d 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -16,7 +16,11 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -HARFBUZZ_VERSION=8.5.0 +if [[ "$MB_ML_VER" != 2014 ]]; then + HARFBUZZ_VERSION=9.0.0 +else + HARFBUZZ_VERSION=8.5.0 +fi LIBPNG_VERSION=1.6.43 JPEGTURBO_VERSION=3.0.3 OPENJPEG_VERSION=2.5.2 @@ -40,7 +44,7 @@ BROTLI_VERSION=1.1.0 if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then function build_openjpeg { - local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz) + local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v$OPENJPEG_VERSION.tar.gz openjpeg-$OPENJPEG_VERSION.tar.gz) (cd $out_dir \ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) @@ -50,7 +54,7 @@ fi function build_brotli { local cmake=$(get_modern_cmake) - local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz) + local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) @@ -60,6 +64,25 @@ function build_brotli { fi } +function build_harfbuzz { + if [[ "$HARFBUZZ_VERSION" == 8.5.0 ]]; then + export FREETYPE_LIBS=-lfreetype + export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ + build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no + export FREETYPE_LIBS="" + export FREETYPE_CFLAGS="" + else + local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) + (cd $out_dir \ + && meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled) + (cd $out_dir/build \ + && meson install) + if [[ "$MB_ML_LIBC" == "manylinux" ]]; then + cp /usr/local/lib64/libharfbuzz* /usr/local/lib + fi + fi +} + function build { if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then sudo chown -R runner /usr/local @@ -109,15 +132,7 @@ function build { build_freetype fi - if [ -z "$IS_MACOS" ]; then - export FREETYPE_LIBS=-lfreetype - export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ - fi - build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no - if [ -z "$IS_MACOS" ]; then - export FREETYPE_LIBS="" - export FREETYPE_CFLAGS="" - fi + build_harfbuzz } # Any stuff that you need to do before you start building the wheels @@ -140,7 +155,13 @@ if [[ -n "$IS_MACOS" ]]; then brew remove --ignore-dependencies webp fi - brew install pkg-config + brew install meson pkg-config +elif [[ "$MB_ML_LIBC" == "manylinux" ]]; then + if [[ "$HARFBUZZ_VERSION" != 8.5.0 ]]; then + yum install -y meson + fi +else + apk add meson fi wrap_wheel_builder build From c9dc34ae8d90e632d90a685857c80c516b1668f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 9 Sep 2024 13:19:37 +1000 Subject: [PATCH 179/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index be94594f8..15da95e09 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Deprecate support for FreeType 2.9.0 #8356 + [hugovk, radarhere] + - Removed unused TiffImagePlugin IFD_LEGACY_API #8355 [radarhere] From be01d536c652f2224351ad0803c92067394db1bb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 9 Sep 2024 15:22:27 +1000 Subject: [PATCH 180/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 15da95e09..e217c9b82 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Support converting more modes to LAB by converting to RGBA first #8358 + [radarhere] + - Deprecate support for FreeType 2.9.0 #8356 [hugovk, radarhere] From d25d12f792c88b3bae881c9090b13d2a9f0e3d84 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 11:29:52 +1000 Subject: [PATCH 181/652] Removed custom build_openjpeg --- .github/workflows/wheels-dependencies.sh | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97f70ed84..9e92e416e 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -38,16 +38,6 @@ BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 -if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then - function build_openjpeg { - local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz) - (cd $out_dir \ - && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ - && make install) - touch openjpeg-stamp - } -fi - function build_brotli { local cmake=$(get_modern_cmake) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz) From e0845f06d8d063fbf78708dd20a692041b60596c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 16:20:52 +1000 Subject: [PATCH 182/652] Deprecate isImageType --- Tests/test_image.py | 4 ++++ docs/deprecations.rst | 8 ++++++++ docs/releasenotes/11.0.0.rst | 8 ++++++++ src/PIL/Image.py | 17 +++++++---------- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 97e97acaa..cf5b5a710 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1117,6 +1117,10 @@ class TestImage: assert len(caplog.records) == 0 assert im.fp is None + def test_deprecation(self) -> None: + with pytest.warns(DeprecationWarning): + assert not Image.isImageType(None) + class TestImageBytes: @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 8d1537a5d..57f7f5b5c 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -122,6 +122,14 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ +Image isImageType() +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)`` +instead. + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index dd19c4b7f..e0be11690 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -61,6 +61,14 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ +Image isImageType() +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)`` +instead. + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e6d9047f5..8f16eef94 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -133,6 +133,7 @@ def isImageType(t: Any) -> TypeGuard[Image]: :param t: object to check if it's an image :returns: True if the object is an image """ + deprecate("Image.isImageType(im)", 12, "isinstance(im, Image.Image)") return hasattr(t, "im") @@ -1823,23 +1824,22 @@ class Image: :param mask: An optional mask image. """ - if isImageType(box): + if isinstance(box, Image): if mask is not None: msg = "If using second argument as mask, third argument must be None" raise ValueError(msg) # abbreviated paste(im, mask) syntax mask = box box = None - assert not isinstance(box, Image) if box is None: box = (0, 0) if len(box) == 2: # upper left corner given; get size from image or mask - if isImageType(im): + if isinstance(im, Image): size = im.size - elif isImageType(mask): + elif isinstance(mask, Image): size = mask.size else: # FIXME: use self.size here? @@ -1852,17 +1852,15 @@ class Image: from . import ImageColor source = ImageColor.getcolor(im, self.mode) - elif isImageType(im): + elif isinstance(im, Image): im.load() if self.mode != im.mode: if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): # should use an adapter for this! im = im.convert(self.mode) source = im.im - elif isinstance(im, tuple): - source = im else: - source = cast(float, im) + source = im self._ensure_mutable() @@ -2023,7 +2021,7 @@ class Image: else: band = 3 - if isImageType(alpha): + if isinstance(alpha, Image): # alpha layer if alpha.mode not in ("1", "L"): msg = "illegal image mode" @@ -2033,7 +2031,6 @@ class Image: alpha = alpha.convert("L") else: # constant alpha - alpha = cast(int, alpha) # see python/typing#1013 try: self.im.fillband(band, alpha) except (AttributeError, ValueError): From adb691821476eed08bbefe2478794c5dbd94b527 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 11:38:56 +1000 Subject: [PATCH 183/652] Use isinstance to detect Image instances --- src/PIL/ImageMath.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 191cc2a5f..191ca8524 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -268,7 +268,7 @@ def lambda_eval( args.update(options) args.update(kw) for k, v in args.items(): - if hasattr(v, "im"): + if isinstance(v, Image.Image): args[k] = _Operand(v) out = expression(args) @@ -319,7 +319,7 @@ def unsafe_eval( args.update(options) args.update(kw) for k, v in args.items(): - if hasattr(v, "im"): + if isinstance(v, Image.Image): args[k] = _Operand(v) compiled_code = compile(expression, "", "eval") From d522e0a5c03f87b653ab25a23116d9f685aa0c1e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 18:50:06 +1000 Subject: [PATCH 184/652] Improved handling of RGBA palettes when saving GIF images --- Tests/test_file_gif.py | 18 ++++++++++++++++++ src/PIL/GifImagePlugin.py | 17 ++++++++++++++--- src/PIL/Image.py | 5 ++++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 571fe1b9a..16c8466f3 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1429,3 +1429,21 @@ def test_saving_rgba(tmp_path: Path) -> None: with Image.open(out) as reloaded: reloaded_rgba = reloaded.convert("RGBA") assert reloaded_rgba.load()[0, 0][3] == 0 + + +def test_optimizing_p_rgba(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im1 = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im1) + d.ellipse([(40, 40), (60, 60)], fill=1) + data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254 + im1.putpalette(data, "RGBA") + + im2 = Image.new("P", (100, 100)) + im2.putpalette(data, "RGBA") + + im1.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reloaded: + assert reloaded.n_frames == 2 diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index f206fbb9c..57c291792 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -553,7 +553,9 @@ def _normalize_palette( if im.mode == "P": if not source_palette: - source_palette = im.im.getpalette("RGB")[:768] + im_palette = im.getpalette(None) + assert im_palette is not None + source_palette = bytearray(im_palette) else: # L-mode if not source_palette: source_palette = bytearray(i // 3 for i in range(768)) @@ -629,7 +631,10 @@ def _write_single_frame( def _getbbox( base_im: Image.Image, im_frame: Image.Image ) -> tuple[Image.Image, tuple[int, int, int, int] | None]: - if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): + palette_bytes = [ + bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame) + ] + if palette_bytes[0] != palette_bytes[1]: im_frame = im_frame.convert("RGBA") base_im = base_im.convert("RGBA") delta = ImageChops.subtract_modulo(im_frame, base_im) @@ -984,7 +989,13 @@ def _get_palette_bytes(im: Image.Image) -> bytes: :param im: Image object :returns: Bytes, len<=768 suitable for inclusion in gif header """ - return bytes(im.palette.palette) if im.palette else b"" + if not im.palette: + return b"" + + palette = bytes(im.palette.palette) + if im.palette.mode == "RGBA": + palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3)) + return palette def _get_background( diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e6d9047f5..a7a158d35 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1066,7 +1066,7 @@ class Image: trns_im = new(self.mode, (1, 1)) if self.mode == "P": assert self.palette is not None - trns_im.putpalette(self.palette) + trns_im.putpalette(self.palette, self.palette.mode) if isinstance(t, tuple): err = "Couldn't allocate a palette color for transparency" assert trns_im.palette is not None @@ -2182,6 +2182,9 @@ class Image: source_palette = self.im.getpalette(palette_mode, palette_mode) else: # L-mode source_palette = bytearray(i // 3 for i in range(768)) + elif len(source_palette) > 768: + bands = 4 + palette_mode = "RGBA" palette_bytes = b"" new_positions = [0] * 256 From 8d508406ad8917919c5da084bf0e543146643522 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 20:55:34 +1000 Subject: [PATCH 185/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e217c9b82..3e4a99be7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Deprecate isImageType #8364 + [radarhere] + - Support converting more modes to LAB by converting to RGBA first #8358 [radarhere] From 54c5b532c54759d9a66541e8bb6ba0893b3c8cf6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 21:32:20 +1000 Subject: [PATCH 186/652] Added release notes for #8286 --- docs/releasenotes/11.0.0.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index e0be11690..22dd5313b 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -103,10 +103,18 @@ TODO API Additions ============= -TODO -^^^^ +Writing XMP bytes to JPEG and MPO +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +XMP data can now be saved to JPEG files using an ``xmp`` argument:: + + im.save("out.jpg", xmp=b"test") + +The data can also be set through :py:attr:`~PIL.Image.Image.info`, for use when saving +either JPEG or MPO images:: + + im.info["xmp"] = b"test" + im.save("out.jpg") Other Changes ============= From fea3e0dd5facd07c9133b1b5dbb7b97452d62411 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 21:22:03 +1000 Subject: [PATCH 187/652] Added release notes for #8213 --- docs/releasenotes/11.0.0.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 22dd5313b..a83e3f2e9 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -45,6 +45,11 @@ TiffImagePlugin IFD_LEGACY_API An unused setting, ``TiffImagePlugin.IFD_LEGACY_API``, has been removed. +WebP 0.4 +^^^^^^^^ + +Support for WebP 0.4 and earlier has been removed; WebP 0.5 is the minimum supported. + Deprecations ============ From 5c67178c952560c8992c2eaac4d62a5ca278f83d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 21:30:48 +1000 Subject: [PATCH 188/652] Added release notes for #8199 --- docs/releasenotes/11.0.0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index a83e3f2e9..376130499 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -133,6 +133,8 @@ of 3.13.0 final (2024-10-01, :pep:`719`). Pillow 11.0.0 now officially supports Python 3.13. +Support has also been added for the experimental free-threaded mode of :pep:`703`. + C-level Flags ^^^^^^^^^^^^^ From bc4c57e9ea3089d168ac85b0f8c4e92db48e669d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 22:39:47 +1000 Subject: [PATCH 189/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3e4a99be7..af292f7a1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Deprecate ICNS (width, height, scale) sizes in favour of load(scale) #8352 + [radarhere] + +- Improved handling of RGBA palettes when saving GIF images #8366 + [radarhere] + - Deprecate isImageType #8364 [radarhere] From 6ade6873dec23cb6c411f1b1e8a405ada8b85a35 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 11 Sep 2024 08:13:41 +1000 Subject: [PATCH 190/652] Removed libffi-dev --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index ee92d447a..7e3a691e5 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -21,7 +21,7 @@ set -e if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ - ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ + ghostscript libjpeg-turbo-progs libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ sway wl-clipboard libopenblas-dev fi From d2efd7dd5fcd19b7cb7768fc90ffa51676ff5e7a Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Wed, 11 Sep 2024 10:32:42 +0200 Subject: [PATCH 191/652] Update src/PIL/ImageFont.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b6d69019d..7ee3aaa55 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -934,7 +934,7 @@ def load_path(filename: str | bytes) -> ImageFont: pass msg = f"cannot find font file '{filename}' in `sys.path`" if os.path.exists(filename): - msg += f", did you mean `ImageFont.load({filename})` instead?" + msg += f", did you mean `ImageFont.load(\"{filename}\")` instead?" raise OSError(msg) From e4f13020e16925f1dcfe738218e4d03a3c3992bb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 08:33:09 +0000 Subject: [PATCH 192/652] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 7ee3aaa55..55ab61951 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -934,7 +934,7 @@ def load_path(filename: str | bytes) -> ImageFont: pass msg = f"cannot find font file '{filename}' in `sys.path`" if os.path.exists(filename): - msg += f", did you mean `ImageFont.load(\"{filename}\")` instead?" + msg += f', did you mean `ImageFont.load("{filename}")` instead?' raise OSError(msg) From 01ba1e2252bb6fab3bdee4ee0b2952746d919270 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 11 Sep 2024 19:53:52 +1000 Subject: [PATCH 193/652] Accept float stroke widths --- Tests/images/imagedraw_stroke_float.png | Bin 0 -> 1509 bytes Tests/test_imagedraw.py | 14 ++++++++++++++ src/_imagingft.c | 18 +++++++++--------- 3 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 Tests/images/imagedraw_stroke_float.png diff --git a/Tests/images/imagedraw_stroke_float.png b/Tests/images/imagedraw_stroke_float.png new file mode 100644 index 0000000000000000000000000000000000000000..a590f8af0b616852c8d54e591d04265dea90447b GIT binary patch literal 1509 zcma)+dpi>f0Eb7mn8{^mF&s3vgt;u2C}ZZ7!A38(GopOnZ9cH6qE+N-r71fmL zkz`~jvY;v-<^-QkdSNMhcV&r?TbN@@SobhJcmaC8Hn=PkN zzerpf&46-HeRZoeb0Q=&%}6(H{ShMi)jeAJ;W`1r96?c`b&i?GQPuT2bk8-7i&xbL zc})(n%PNfw6nfGS&YMc4t4WM1c9{>1{;7m?1pz)45y~$DhnIXbnBNd<_i9^12X8ud zS6^`63@I9FhTqem}l${p+<T znpcwuxCqOwtUi>ATq$DB$4p9Q8&iKO-@|Eg#F1KVFdG7t;4y-aQ+H zI$Rg}OIEQfKCv4f3OXWfhy$F@K5M2!9OWIJ7-t)mQp0RxWAF!CVbi)LEQ!UNKSD1< zx(bXNje3ECbh|viOa1E(SJns_nsoi%f(@$L^PWLW<;VzAhLh>4BHb661WKGTu@PBC z58Z%D+D2}$e;*MBHq6a^+hP}5@4O9yRxI~9b<4q?P1r*SVfI|KW3Gh`Bf#{#cFrvx zz4}4+@7l$AbdO~CTgt4Z@(a1k1I^+B?e$UbzHg44f>Q@jURX_tZIJCCR96r{yH(we?{wxN&{QF6E{s|{Y$s)+q+%7VHj%gK|aq?UPp_x~WsLT&lk%aE6{kzXUYDZ(SkTJG1H zm@fW8XV#|U-jshaiQ67&sXc~G@n#oJOCn?j^ms0mlYQ?M&$(G3U>gP8G= zy;$!r None: ) +@skip_unless_feature("freetype2") +def test_stroke_float() -> None: + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.text((12, 12), "A", "#f00", font, stroke_width=0.5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_float.png", 3.1) + + @skip_unless_feature("freetype2") def test_stroke_descender() -> None: # Arrange diff --git a/src/_imagingft.c b/src/_imagingft.c index 5c446e641..8bef45baf 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -833,7 +833,7 @@ font_render(FontObject *self, PyObject *args) { Py_ssize_t id; int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */ - int stroke_width = 0; + float stroke_width = 0; PY_LONG_LONG foreground_ink_long = 0; unsigned int foreground_ink; const char *mode = NULL; @@ -853,7 +853,7 @@ font_render(FontObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "OO|zzOzizLffO:render", + "OO|zzOzfzLffO:render", &string, &fill, &mode, @@ -919,8 +919,8 @@ font_render(FontObject *self, PyObject *args) { return NULL; } - width += stroke_width * 2 + ceil(x_start); - height += stroke_width * 2 + ceil(y_start); + width += ceil(stroke_width * 2 + x_start); + height += ceil(stroke_width * 2 + y_start); image = PyObject_CallFunction(fill, "ii", width, height); if (image == Py_None) { PyMem_Del(glyph_info); @@ -934,8 +934,8 @@ font_render(FontObject *self, PyObject *args) { Py_XDECREF(imageId); im = (Imaging)id; - x_offset -= stroke_width; - y_offset -= stroke_width; + x_offset = round(x_offset - stroke_width); + y_offset = round(y_offset - stroke_width); if (count == 0 || width == 0 || height == 0) { PyMem_Del(glyph_info); return Py_BuildValue("N(ii)", image, x_offset, y_offset); @@ -950,7 +950,7 @@ font_render(FontObject *self, PyObject *args) { FT_Stroker_Set( stroker, - (FT_Fixed)stroke_width * 64, + (FT_Fixed)round(stroke_width * 64), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0 @@ -988,8 +988,8 @@ font_render(FontObject *self, PyObject *args) { } /* set pen position to text origin */ - x = (-x_min + stroke_width + x_start) * 64; - y = (-y_max + (-stroke_width) - y_start) * 64; + x = round((-x_min + stroke_width + x_start) * 64); + y = round((-y_max + (-stroke_width) - y_start) * 64); if (stroker == NULL) { load_flags |= FT_LOAD_RENDER; From 4a284a78c58e6952f4adc7230a4349defe38dcae Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 1 Sep 2024 20:40:04 +0400 Subject: [PATCH 194/652] Remove weird comments --- src/encode.c | 7 ------- src/libImaging/Jpeg2K.h | 7 ------- src/libImaging/Jpeg2KDecode.c | 7 ------- src/libImaging/Jpeg2KEncode.c | 7 ------- 4 files changed, 28 deletions(-) diff --git a/src/encode.c b/src/encode.c index 529982dad..1a4cd489d 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1411,10 +1411,3 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } #endif - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index e8d92f7b6..6fbbf7311 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -104,10 +104,3 @@ typedef struct { int plt; } JPEG2KENCODESTATE; - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index a81a14a09..fc927d2f0 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -979,10 +979,3 @@ ImagingJpeg2KVersion(void) { } #endif /* HAVE_OPENJPEG */ - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index cb21a186c..d30ccde60 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -652,10 +652,3 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { } #endif /* HAVE_OPENJPEG */ - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ From 77503156b17a8314652dcd69941cbdb222dd7bcf Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Wed, 11 Sep 2024 14:22:45 +0200 Subject: [PATCH 195/652] Update src/PIL/ImageFont.py Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/ImageFont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 55ab61951..0fccee32f 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -932,9 +932,9 @@ def load_path(filename: str | bytes) -> ImageFont: return load(os.path.join(directory, filename)) except OSError: pass - msg = f"cannot find font file '{filename}' in `sys.path`" + msg = f"cannot find font file '{filename}' in sys.path" if os.path.exists(filename): - msg += f', did you mean `ImageFont.load("{filename}")` instead?' + msg += f', did you mean ImageFont.load("{filename}") instead?' raise OSError(msg) From 32c514d24ccac400b025c981bb27b48d6e78b2b6 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Wed, 11 Sep 2024 14:54:53 +0200 Subject: [PATCH 196/652] Update src/PIL/ImageFont.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 0fccee32f..89efe5bd8 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -932,7 +932,7 @@ def load_path(filename: str | bytes) -> ImageFont: return load(os.path.join(directory, filename)) except OSError: pass - msg = f"cannot find font file '{filename}' in sys.path" + msg = f'cannot find font file "{filename}" in sys.path' if os.path.exists(filename): msg += f', did you mean ImageFont.load("{filename}") instead?' From a2988da0d2e0057726687c8030b5f2ea2b007f8f Mon Sep 17 00:00:00 2001 From: Alexander Karpinsky Date: Wed, 11 Sep 2024 19:55:07 +0400 Subject: [PATCH 197/652] =?UTF-8?q?ImageCore=20=E2=86=92=20ImagingCore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/deprecations.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index dd271a97f..af6de2f92 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -141,10 +141,10 @@ Get Internal Pointers to Objects .. deprecated:: 11.0.0 -``Image.core.ImageCore.id`` and ``Image.core.ImageCore.unsafe_ptrs`` -have been deprecated and will be removed in Pillow 12 (2025-10-15). -They were used for obtaining raw pointers to ``ImageCore`` internals. To interact with -C code, you can use ``Image.core.ImageCore.ptr``, which returns a ``Capsule`` object. +``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been +deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining +raw pointers to ``ImagingCore`` internals. To interact with C code, you can use +``Image.core.ImagingCore.ptr``, which returns a ``Capsule`` object. Removed features ---------------- From aa7dfe3a56380a4ba98a7b439fbc3085603beae4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Sep 2024 16:16:21 +1000 Subject: [PATCH 198/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index af292f7a1..f23ec609f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Accept float stroke widths #8369 + [radarhere] + - Deprecate ICNS (width, height, scale) sizes in favour of load(scale) #8352 [radarhere] From fab19b0af822128f305f8ffedd0645f33160f5d8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Sep 2024 16:57:28 +1000 Subject: [PATCH 199/652] Windows wheels are now grouped with the others --- wheels/README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/wheels/README.md b/wheels/README.md index 8b412b7fe..1175616e1 100644 --- a/wheels/README.md +++ b/wheels/README.md @@ -1,11 +1,11 @@ README ------ -[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build macOS and Linux -wheels for tagged versions of Pillow. +[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build wheels for tagged +versions of Pillow. This directory contains [multibuild](https://github.com/multi-build/multibuild) to -build dependencies for the wheels, and dependency licenses to be included. +build dependencies for macOS and Linux wheels, and dependency licenses to be included. Archives -------- @@ -30,6 +30,3 @@ Wheels Wheels are [GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml). - -Windows wheels are created separately. They are -[GitHub Actions artifacts created on each run of the Windows workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml?query=branch%3Amain). From 8336852b610f3a2b83c1d0706c0d76998526c709 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Sep 2024 18:52:07 +1000 Subject: [PATCH 200/652] Install NumPy from PyPI for free threaded job --- .ci/install.sh | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 7e3a691e5..e85e6bdc5 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -38,12 +38,7 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma if [[ $(uname) != CYGWIN* ]]; then - # TODO Update condition when NumPy supports free-threading - if [[ "$PYTHON_GIL" == "0" ]]; then - python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple - else - python3 -m pip install numpy - fi + python3 -m pip install numpy # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then From c9f88890a56882ca43a5e00fb6ed1892f706eaa0 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 12 Sep 2024 14:52:24 +0300 Subject: [PATCH 201/652] Install numpy from PyPI when testing wheels --- .github/workflows/wheels-test.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 023a33824..eebfd5c55 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -15,11 +15,7 @@ fi if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then # TODO Update condition when NumPy supports free-threading - if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then - python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple - else - python3 -m pip install numpy - fi + python3 -m pip install numpy fi if [ ! -d "test-images-main" ]; then From 1ead7791c697d5ea76c299156680a3cdd2fa7f14 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 12 Sep 2024 16:34:29 +0300 Subject: [PATCH 202/652] Remove comment --- .github/workflows/wheels-test.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index eebfd5c55..bdf0caffa 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -14,7 +14,6 @@ else fi if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then - # TODO Update condition when NumPy supports free-threading python3 -m pip install numpy fi From fea929bf10dbf0c77cffd1375a5542d40d24b2f5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Sep 2024 07:34:56 +1000 Subject: [PATCH 203/652] Test NumPy with musllinux wheels --- .github/workflows/wheels-test.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index bdf0caffa..b30b1725f 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -13,9 +13,7 @@ else yum install -y fribidi fi -if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then - python3 -m pip install numpy -fi +python3 -m pip install numpy if [ ! -d "test-images-main" ]; then curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip From 5d8e891387cfd2313d021578b9a049a057980726 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Sep 2024 10:52:36 +1000 Subject: [PATCH 204/652] Updated libpng to 1.6.44 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index c25e1a27d..f87ace4c6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -21,7 +21,7 @@ if [[ "$MB_ML_VER" != 2014 ]]; then else HARFBUZZ_VERSION=8.5.0 fi -LIBPNG_VERSION=1.6.43 +LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.0.3 OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.4.5 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 423aaa966..6d7d5dcdf 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -115,7 +115,7 @@ V = { "HARFBUZZ": "9.0.0", "JPEGTURBO": "3.0.3", "LCMS2": "2.16", - "LIBPNG": "1.6.43", + "LIBPNG": "1.6.44", "LIBWEBP": "1.4.0", "OPENJPEG": "2.5.2", "TIFF": "4.6.0", From 0f67dfc380a460eb2b4d2bcd43e54b59240006b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Sep 2024 15:31:37 +1000 Subject: [PATCH 205/652] Change macos-14 to macos-latest --- .github/workflows/test.yml | 4 ++-- .github/workflows/wheels.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0903f886..6576292b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: fail-fast: false matrix: os: [ - "macos-14", + "macos-latest", "ubuntu-latest", ] python-version: [ @@ -56,7 +56,7 @@ jobs: # M1 only available for 3.10+ - { os: "macos-13", python-version: "3.9" } exclude: - - { os: "macos-14", python-version: "3.9" } + - { os: "macos-latest", python-version: "3.9" } runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index f3ed8399b..e208417e1 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -107,7 +107,7 @@ jobs: cibw_arch: x86_64 macosx_deployment_target: "10.10" - name: "macOS arm64" - os: macos-14 + os: macos-latest cibw_arch: arm64 macosx_deployment_target: "11.0" - name: "manylinux2014 and musllinux x86_64" From 1557f7e36f134979ee607e7583185f8b3adc2212 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Sep 2024 23:41:03 +1000 Subject: [PATCH 206/652] Removed unused variable --- src/path.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/path.c b/src/path.c index b96e8b78a..f4f4287f3 100644 --- a/src/path.c +++ b/src/path.c @@ -44,7 +44,6 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view); typedef struct { PyObject_HEAD Py_ssize_t count; double *xy; - int index; /* temporary use, e.g. in decimate */ } PyPathObject; static PyTypeObject PyPathType; From 6921f83629242d199eaab87af55da6279588a8e0 Mon Sep 17 00:00:00 2001 From: Alexander Karpinsky Date: Fri, 13 Sep 2024 22:54:16 +0200 Subject: [PATCH 207/652] Update docs/deprecations.rst Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/deprecations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index af6de2f92..bfe9849da 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -144,7 +144,7 @@ Get Internal Pointers to Objects ``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining raw pointers to ``ImagingCore`` internals. To interact with C code, you can use -``Image.core.ImagingCore.ptr``, which returns a ``Capsule`` object. +``Image.Image.getim()``, which returns a ``Capsule`` object. Removed features ---------------- From 57131599682b1c6a54748006ccda4abfd0f2615e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 23:52:11 +0000 Subject: [PATCH 208/652] Update dependency cibuildwheel to v2.21.0 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index b8e6c3947..210f29d3a 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.20.0 +cibuildwheel==2.21.0 From a3d9529f045af093eb964bf264064b261714b127 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Sep 2024 11:23:09 +1000 Subject: [PATCH 209/652] Updated macOS deployment target for Python >= 3.12 on Intel to 10.13 --- .github/workflows/wheels.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e208417e1..11564c142 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -102,10 +102,16 @@ jobs: fail-fast: false matrix: include: - - name: "macOS x86_64" + - name: "macOS 10.10 x86_64" os: macos-13 cibw_arch: x86_64 + build: "pp310* cp3{9,10,11}*" macosx_deployment_target: "10.10" + - name: "macOS 10.13 x86_64" + os: macos-13 + cibw_arch: x86_64 + build: "cp3{12,13}*" + macosx_deployment_target: "10.13" - name: "macOS arm64" os: macos-latest cibw_arch: arm64 @@ -145,7 +151,7 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} + name: dist-${{ matrix.os }}${{ matrix.macosx_deployment_target && format('-{0}', matrix.macosx_deployment_target) }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} path: ./wheelhouse/*.whl windows: From 10c118bb5c2dca0343d1e6e733de51e519f0374a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Sep 2024 19:58:57 +1000 Subject: [PATCH 210/652] Added release notes for #8379 --- docs/releasenotes/11.0.0.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 5f90348ad..1b8407636 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -23,6 +23,13 @@ Python 3.8 Pillow has dropped support for Python 3.8, which reached end-of-life in October 2024. +Python 3.12 on macOS < 10.13 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The latest version of Python 3.12 no longer supports macOS versions earlier than 10.13, +and so Pillow has also updated the deployment target for its prebuilt Python 3.12 +wheels. + PSFile ^^^^^^ @@ -147,6 +154,8 @@ Pillow 11.0.0 now officially supports Python 3.13. Support has also been added for the experimental free-threaded mode of :pep:`703`. +Python 3.13 does not support macOS versions earlier than 10.13. + C-level Flags ^^^^^^^^^^^^^ From ad0091096f4c653c88a98a03e1323d855152bb81 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 14 Sep 2024 20:52:49 +1000 Subject: [PATCH 211/652] Describe supported OS versions instead of unsupported Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/releasenotes/11.0.0.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 1b8407636..36334e39f 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -23,10 +23,10 @@ Python 3.8 Pillow has dropped support for Python 3.8, which reached end-of-life in October 2024. -Python 3.12 on macOS < 10.13 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Python 3.12 on macOS <= 10.12 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The latest version of Python 3.12 no longer supports macOS versions earlier than 10.13, +The latest version of Python 3.12 only supports macOS versions 10.13 and later, and so Pillow has also updated the deployment target for its prebuilt Python 3.12 wheels. @@ -154,7 +154,7 @@ Pillow 11.0.0 now officially supports Python 3.13. Support has also been added for the experimental free-threaded mode of :pep:`703`. -Python 3.13 does not support macOS versions earlier than 10.13. +Python 3.13 only supports macOS versions 10.13 and later. C-level Flags ^^^^^^^^^^^^^ From 88b3265a9cb86ffd8a7c43c1afa8dae1b78cf035 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Sep 2024 20:59:02 +1000 Subject: [PATCH 212/652] Lint: Run PT016 --- Tests/test_image.py | 15 +++++---------- Tests/test_imageshow.py | 2 +- pyproject.toml | 1 - 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index cf5b5a710..9b65041f4 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1086,22 +1086,17 @@ class TestImage: valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c """ with Image.open(os.path.join("Tests/images", path)) as im: - try: + with pytest.raises(OSError) as e: im.load() - pytest.fail() - except OSError as e: - buffer_overrun = str(e) == "buffer overrun when reading image file" - truncated = "image file is truncated" in str(e) + buffer_overrun = str(e.value) == "buffer overrun when reading image file" + truncated = "image file is truncated" in str(e.value) - assert buffer_overrun or truncated + assert buffer_overrun or truncated def test_fli_overrun2(self) -> None: with Image.open("Tests/images/fli_overrun2.bin") as im: - try: + with pytest.raises(OSError, match="buffer overrun when reading image file"): im.seek(1) - pytest.fail() - except OSError as e: - assert str(e) == "buffer overrun when reading image file" def test_exit_fp(self) -> None: with Image.new("L", (1, 1)) as im: diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index a4f7e5cc5..67c32783f 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -115,7 +115,7 @@ def test_ipythonviewer() -> None: test_viewer = viewer break else: - pytest.fail() + pytest.fail("IPythonViewer not found") im = hopper() assert test_viewer.show(im) == 1 diff --git a/pyproject.toml b/pyproject.toml index 4042bd9ee..228c344e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,7 +125,6 @@ lint.ignore = [ "PT007", # pytest-parametrize-values-wrong-type "PT011", # pytest-raises-too-broad "PT012", # pytest-raises-with-multiple-statements - "PT016", # pytest-fail-without-message "PT017", # pytest-assert-in-except "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI034", # flake8-pyi: typing.Self added in Python 3.11 From c01d2d05772dd52e316ccd0b868a1c7b560cc391 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Sep 2024 21:57:45 +1000 Subject: [PATCH 213/652] Updated libjpeg-turbo to 3.0.4 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index d1e87899f..4d40f7fab 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -22,7 +22,7 @@ else HARFBUZZ_VERSION=8.5.0 fi LIBPNG_VERSION=1.6.44 -JPEGTURBO_VERSION=3.0.3 +JPEGTURBO_VERSION=3.0.4 OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.6.2 TIFF_VERSION=4.6.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b478c1c6b..e2022d283 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ V = { "FREETYPE": "2.13.3", "FRIBIDI": "1.0.15", "HARFBUZZ": "9.0.0", - "JPEGTURBO": "3.0.3", + "JPEGTURBO": "3.0.4", "LCMS2": "2.16", "LIBPNG": "1.6.44", "LIBWEBP": "1.4.0", From 8a086edbe965bff4d89fc3b189e021f0b1a2b322 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Sep 2024 08:22:58 +1000 Subject: [PATCH 214/652] Cast handle to int --- Tests/test_imagewin.py | 12 ++++++++++++ src/PIL/ImageWin.py | 19 +++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index a836bb90b..e7af160dd 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -60,6 +60,18 @@ class TestImageWinDib: with pytest.raises(ValueError): ImageWin.Dib(mode) + def test_dib_hwnd(self) -> None: + mode = "RGBA" + size = (128, 128) + wnd = 0 + + dib = ImageWin.Dib(mode, size) + hwnd = ImageWin.HWND(wnd) + + dib.expose(hwnd) + dib.draw(hwnd, (0, 0) + size) + assert isinstance(dib.query_palette(hwnd), int) + def test_dib_paste(self) -> None: # Arrange im = hopper() diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 6fc7cfaf5..98c28f29f 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -98,14 +98,15 @@ class Dib: HDC or HWND instance. In PythonWin, you can use ``CDC.GetHandleAttrib()`` to get a suitable handle. """ + handle_int = int(handle) if isinstance(handle, HWND): - dc = self.image.getdc(handle) + dc = self.image.getdc(handle_int) try: self.image.expose(dc) finally: - self.image.releasedc(handle, dc) + self.image.releasedc(handle_int, dc) else: - self.image.expose(handle) + self.image.expose(handle_int) def draw( self, @@ -124,14 +125,15 @@ class Dib: """ if src is None: src = (0, 0) + self.size + handle_int = int(handle) if isinstance(handle, HWND): - dc = self.image.getdc(handle) + dc = self.image.getdc(handle_int) try: self.image.draw(dc, dst, src) finally: - self.image.releasedc(handle, dc) + self.image.releasedc(handle_int, dc) else: - self.image.draw(handle, dst, src) + self.image.draw(handle_int, dst, src) def query_palette(self, handle: int | HDC | HWND) -> int: """ @@ -148,14 +150,15 @@ class Dib: :return: The number of entries that were changed (if one or more entries, this indicates that the image should be redrawn). """ + handle_int = int(handle) if isinstance(handle, HWND): - handle = self.image.getdc(handle) + handle = self.image.getdc(handle_int) try: result = self.image.query_palette(handle) finally: self.image.releasedc(handle, handle) else: - result = self.image.query_palette(handle) + result = self.image.query_palette(handle_int) return result def paste( From 1f3fe6f73350aa9630b236a9f16370aa0c71258d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Sep 2024 10:21:17 +1000 Subject: [PATCH 215/652] Use getim() --- Tests/test_imagemorph.py | 8 ++++---- src/PIL/ImageCms.py | 4 ++-- src/PIL/ImageMath.py | 5 ++--- src/PIL/ImageMorph.py | 6 +++--- src/PIL/ImageTk.py | 2 +- src/PIL/_imagingcms.pyi | 4 +++- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 04860fd61..9da32aa01 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -328,13 +328,13 @@ def test_wrong_mode() -> None: iml = Image.new("L", (10, 10)) with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), imrgb.im.ptr, iml.im.ptr) + _imagingmorph.apply(bytes(lut), imrgb.getim(), iml.getim()) with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), iml.im.ptr, imrgb.im.ptr) + _imagingmorph.apply(bytes(lut), iml.getim(), imrgb.getim()) with pytest.raises(RuntimeError): - _imagingmorph.match(bytes(lut), imrgb.im.ptr) + _imagingmorph.match(bytes(lut), imrgb.getim()) # Should not raise - _imagingmorph.match(bytes(lut), iml.im.ptr) + _imagingmorph.match(bytes(lut), iml.getim()) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 5e256b0ab..f028f1775 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -352,7 +352,7 @@ class ImageCmsTransform(Image.ImagePointHandler): im.load() if imOut is None: imOut = Image.new(self.output_mode, im.size, None) - self.transform.apply(im.im.ptr, imOut.im.ptr) + self.transform.apply(im.getim(), imOut.getim()) imOut.info["icc_profile"] = self.output_profile.tobytes() return imOut @@ -361,7 +361,7 @@ class ImageCmsTransform(Image.ImagePointHandler): if im.mode != self.output_mode: msg = "mode mismatch" raise ValueError(msg) # wrong output mode - self.transform.apply(im.im.ptr, im.im.ptr) + self.transform.apply(im.getim(), im.getim()) im.info["icc_profile"] = self.output_profile.tobytes() return im diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index b32742657..ff3014a8d 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -65,7 +65,7 @@ class _Operand: except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.unop(op, out.im.ptr, im_1.im.ptr) + _imagingmath.unop(op, out.getim(), im_1.getim()) else: # binary operation im_2 = self.__fixup(im2) @@ -87,13 +87,12 @@ class _Operand: im_2 = im_2.crop((0, 0) + size) out = Image.new(mode or im_1.mode, im_1.size, None) im_1.load() - im_2.load() try: op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.binop(op, out.im.ptr, im_1.im.ptr, im_2.im.ptr) + _imagingmath.binop(op, out.getim(), im_1.getim(), im_2.getim()) return _Operand(out) # unary operators diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index bc7ed66d3..f0a066b5b 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -213,7 +213,7 @@ class MorphOp: msg = "Image mode must be L" raise ValueError(msg) outimage = Image.new(image.mode, image.size, None) - count = _imagingmorph.apply(bytes(self.lut), image.im.ptr, outimage.im.ptr) + count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim()) return count, outimage def match(self, image: Image.Image) -> list[tuple[int, int]]: @@ -229,7 +229,7 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) - return _imagingmorph.match(bytes(self.lut), image.im.ptr) + return _imagingmorph.match(bytes(self.lut), image.getim()) def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: """Get a list of all turned on pixels in a binary image @@ -240,7 +240,7 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) - return _imagingmorph.get_on_pixels(image.im.ptr) + return _imagingmorph.get_on_pixels(image.getim()) def load_lut(self, filename: str) -> None: """Load an operator from an mrl file""" diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index d01af364b..c36cf9b84 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -259,7 +259,7 @@ def getimage(photo: PhotoImage) -> Image.Image: """Copies the contents of a PhotoImage to a PIL image memory.""" im = Image.new("RGBA", (photo.width(), photo.height())) - _pyimagingtkcall("PyImagingPhotoGet", photo, im.im.ptr) + _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim()) return im diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi index 2abd6d0f7..ddcf93ab1 100644 --- a/src/PIL/_imagingcms.pyi +++ b/src/PIL/_imagingcms.pyi @@ -2,6 +2,8 @@ import datetime import sys from typing import Literal, SupportsFloat, TypedDict +from ._typing import CapsuleType + littlecms_version: str | None _Tuple3f = tuple[float, float, float] @@ -108,7 +110,7 @@ class CmsProfile: def is_intent_supported(self, intent: int, direction: int, /) -> int: ... class CmsTransform: - def apply(self, id_in: int, id_out: int) -> int: ... + def apply(self, id_in: CapsuleType, id_out: CapsuleType) -> int: ... def profile_open(profile: str, /) -> CmsProfile: ... def profile_frombytes(profile: bytes, /) -> CmsProfile: ... From d8ef314205ff65e6aeeefe3092414cc45e7349eb Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 16 Sep 2024 10:39:14 +0200 Subject: [PATCH 216/652] Remove extra load() calls --- src/PIL/ImageCms.py | 2 -- src/PIL/ImageMath.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index f028f1775..b6c5de5b3 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -349,7 +349,6 @@ class ImageCmsTransform(Image.ImagePointHandler): return self.apply(im) def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image: - im.load() if imOut is None: imOut = Image.new(self.output_mode, im.size, None) self.transform.apply(im.getim(), imOut.getim()) @@ -357,7 +356,6 @@ class ImageCmsTransform(Image.ImagePointHandler): return imOut def apply_in_place(self, im: Image.Image) -> Image.Image: - im.load() if im.mode != self.output_mode: msg = "mode mismatch" raise ValueError(msg) # wrong output mode diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index ff3014a8d..773147b53 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -59,7 +59,6 @@ class _Operand: if im2 is None: # unary operation out = Image.new(mode or im_1.mode, im_1.size, None) - im_1.load() try: op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: @@ -86,7 +85,6 @@ class _Operand: if im_2.size != size: im_2 = im_2.crop((0, 0) + size) out = Image.new(mode or im_1.mode, im_1.size, None) - im_1.load() try: op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: From d1f40a94ffe783aceb7050d85fa5c17ecc4961a7 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 16 Sep 2024 10:52:06 +0200 Subject: [PATCH 217/652] Use Image.getim() instead of ImagingCore.ptr --- src/PIL/WebPImagePlugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 8251316d3..ab545563f 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -239,7 +239,6 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: for idx in range(nfr): ims.seek(idx) - ims.load() # Make sure image mode is supported frame = ims @@ -248,7 +247,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Append the frame to the animation encoder enc.add( - frame.im.ptr, + frame.getim(), round(timestamp), lossless, quality, @@ -296,7 +295,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: im = im.convert("RGBA" if im.has_transparency_data else "RGB") data = _webp.WebPEncode( - im.im.ptr, + im.getim(), lossless, float(quality), float(alpha_quality), From 31d36e6b70e1e835bcc6d70f363cda49d3bc9e98 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 16 Sep 2024 11:04:00 +0200 Subject: [PATCH 218/652] Use current frame for transparency detection --- Tests/test_file_webp.py | 2 +- src/PIL/WebPImagePlugin.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 719831db9..e7c887279 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -208,7 +208,7 @@ class TestFileWebp: def test_background_from_gif(self, tmp_path: Path) -> None: out_webp = tmp_path / "temp.webp" - + # Save L mode GIF with background with Image.open("Tests/images/no_palette_with_background.gif") as im: im.save(out_webp, save_all=True) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index ab545563f..1a714d7ea 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -243,7 +243,9 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Make sure image mode is supported frame = ims if frame.mode not in ("RGBX", "RGBA", "RGB"): - frame = frame.convert("RGBA" if im.has_transparency_data else "RGB") + frame = frame.convert( + "RGBA" if frame.has_transparency_data else "RGB" + ) # Append the frame to the animation encoder enc.add( From bc973690397a075b94f694ee1cb3f3392fff9cdc Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 16 Sep 2024 15:45:34 +0200 Subject: [PATCH 219/652] Increase reference to the image while capsule is alive --- src/_imaging.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index e227d8efc..9422c42bf 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3714,9 +3714,18 @@ _getattr_id(ImagingObject *self, void *closure) { return PyLong_FromSsize_t((Py_ssize_t)self->image); } +static void +_ptr_destructor(PyObject *capsule) { + PyObject *self = (PyObject *)PyCapsule_GetContext(capsule); + Py_DECREF(self); +} + static PyObject * _getattr_ptr(ImagingObject *self, void *closure) { - return PyCapsule_New(self->image, IMAGING_MAGIC, NULL); + PyObject *capsule = PyCapsule_New(self->image, IMAGING_MAGIC, _ptr_destructor); + Py_INCREF(self); + PyCapsule_SetContext(capsule, self); + return capsule; } static PyObject * From 1d5b330758c1e9210a8c00c457b03a1f19903939 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 16 Sep 2024 15:37:57 +0200 Subject: [PATCH 220/652] Move common conversion in _convert_frame --- src/PIL/WebPImagePlugin.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 1a714d7ea..64188f28c 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -149,6 +149,13 @@ class WebPImageFile(ImageFile.ImageFile): return self.__logical_frame +def _convert_frame(im: Image.Image) -> Image.Image: + # Make sure image mode is supported + if im.mode not in ("RGBX", "RGBA", "RGB"): + im = im.convert("RGBA" if im.has_transparency_data else "RGB") + return im + + def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: encoderinfo = im.encoderinfo.copy() append_images = list(encoderinfo.get("append_images", [])) @@ -240,12 +247,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: for idx in range(nfr): ims.seek(idx) - # Make sure image mode is supported - frame = ims - if frame.mode not in ("RGBX", "RGBA", "RGB"): - frame = frame.convert( - "RGBA" if frame.has_transparency_data else "RGB" - ) + frame = _convert_frame(ims) # Append the frame to the animation encoder enc.add( @@ -293,8 +295,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: method = im.encoderinfo.get("method", 4) exact = 1 if im.encoderinfo.get("exact") else 0 - if im.mode not in ("RGBX", "RGBA", "RGB"): - im = im.convert("RGBA" if im.has_transparency_data else "RGB") + im = _convert_frame(im) data = _webp.WebPEncode( im.getim(), From a988750595af555e07af0e937ab6f6697a5bb1e1 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 16 Sep 2024 16:37:39 +0200 Subject: [PATCH 221/652] Try fix bigendian --- src/_webp.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index 9717b9bc0..92d5c20fe 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -89,7 +89,7 @@ HandleMuxError(WebPMuxError err, char *chunk) { static int import_frame_libwebp(WebPPicture *frame, Imaging im) { - UINT32 mask = 0; + int drop_alpha = strcmp(im->mode, "RGBA"); if (strcmp(im->mode, "RGBA") && strcmp(im->mode, "RGB") && strcmp(im->mode, "RGBX")) { @@ -97,10 +97,6 @@ import_frame_libwebp(WebPPicture *frame, Imaging im) { return -1; } - if (strcmp(im->mode, "RGBA")) { - mask = MASK_UINT32_CHANNEL_3; - } - frame->width = im->xsize; frame->height = im->ysize; frame->use_argb = 1; // Don't convert RGB pixels to YUV @@ -113,10 +109,18 @@ import_frame_libwebp(WebPPicture *frame, Imaging im) { for (int y = 0; y < im->ysize; ++y) { UINT8 *src = (UINT8 *)im->image32[y]; UINT32 *dst = frame->argb + frame->argb_stride * y; - for (int x = 0; x < im->xsize; ++x) { - UINT32 pix = - MAKE_UINT32(src[x * 4 + 2], src[x * 4 + 1], src[x * 4], src[x * 4 + 3]); - dst[x] = pix | mask; + if (drop_alpha) { + for (int x = 0; x < im->xsize; ++x) { + dst[x] = + ((UINT32)(src[x * 4 + 2]) | ((UINT32)(src[x * 4 + 1]) << 8) | + ((UINT32)(src[x * 4]) << 16) | (0xff << 24)); + } + } else { + for (int x = 0; x < im->xsize; ++x) { + dst[x] = + ((UINT32)(src[x * 4 + 2]) | ((UINT32)(src[x * 4 + 1]) << 8) | + ((UINT32)(src[x * 4]) << 16) | ((UINT32)(src[x * 4 + 3]) << 24)); + } } } From 75e4d5a10d171b9f101f09a671826f3f302139cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 06:12:03 +0000 Subject: [PATCH 222/652] Update dependency cibuildwheel to v2.21.1 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 210f29d3a..7dc3a53fe 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.21.0 +cibuildwheel==2.21.1 From f1e86965f6f791bf1af57f4d202a0c342221190b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 20:26:06 +1000 Subject: [PATCH 223/652] Use transposed size after opening for TIFF images --- Tests/test_image_copy.py | 2 ++ Tests/test_image_resize.py | 4 +--- Tests/test_image_thumbnail.py | 8 +++----- Tests/test_numpy.py | 4 +++- src/PIL/Image.py | 22 ++++++---------------- src/PIL/ImageFile.py | 2 +- src/PIL/TiffImagePlugin.py | 16 +++++++++++++++- 7 files changed, 31 insertions(+), 27 deletions(-) diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 027e5338b..a0b829cc5 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -49,5 +49,7 @@ def test_copy_zero() -> None: @skip_unless_feature("libtiff") def test_deepcopy() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: + assert im.size == (590, 88) + out = copy.deepcopy(im) assert out.size == (590, 88) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index c9e304512..d9ddf5009 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -300,9 +300,7 @@ class TestImageResize: im.resize((10, 10), "unknown") @skip_unless_feature("libtiff") - def test_load_first(self) -> None: - # load() may change the size of the image - # Test that resize() is calling it before getting the size + def test_transposed(self) -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: im = im.resize((64, 64)) assert im.size == (64, 64) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index bdbf09c40..01bd4b1d7 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -92,15 +92,13 @@ def test_no_resize() -> None: @skip_unless_feature("libtiff") -def test_load_first() -> None: - # load() may change the size of the image - # Test that thumbnail() is calling it before performing size calculations +def test_transposed() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: + assert im.size == (590, 88) + im.thumbnail((64, 64)) assert im.size == (64, 10) - # Test thumbnail(), without draft(), - # on an image that is large enough once load() has changed the size with Image.open("Tests/images/g4_orientation_5.tif") as im: im.thumbnail((590, 88), reducing_gap=None) assert im.size == (590, 88) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 312e32e0c..040472d69 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -238,8 +238,10 @@ def test_zero_size() -> None: @skip_unless_feature("libtiff") -def test_load_first() -> None: +def test_transposed() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: + assert im.size == (590, 88) + a = numpy.array(im) assert a.shape == (88, 590) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f88cdf6e2..3f94cef38 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2332,7 +2332,6 @@ class Image: msg = "reducing_gap must be 1.0 or greater" raise ValueError(msg) - self.load() if box is None: box = (0, 0) + self.size @@ -2781,27 +2780,18 @@ class Image: ) return x, y - box = None - final_size: tuple[int, int] - if reducing_gap is not None: - preserved_size = preserve_aspect_ratio() - if preserved_size is None: - return - final_size = preserved_size + preserved_size = preserve_aspect_ratio() + if preserved_size is None: + return + final_size = preserved_size + box = None + if reducing_gap is not None: res = self.draft( None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) ) if res is not None: box = res[1] - if box is None: - self.load() - - # load() may have changed the size of the image - preserved_size = preserve_aspect_ratio() - if preserved_size is None: - return - final_size = preserved_size if self.size != final_size: im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index b8bf0b7fe..721319fd7 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -322,7 +322,7 @@ class ImageFile(Image.Image): def load_prepare(self) -> None: # create image memory if necessary - if self._im is None or self.im.mode != self.mode or self.im.size != self.size: + if self._im is None or self.im.mode != self.mode: self.im = Image.core.new(self.mode, self.size) # create palette (optional) if self.mode == "P": diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index cc16cbfb0..28e504744 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1275,6 +1275,11 @@ class TiffImageFile(ImageFile.ImageFile): return self._load_libtiff() return super().load() + def load_prepare(self) -> None: + if self._im is None and self._will_be_transposed: + self.im = Image.core.new(self.mode, self.size[::-1]) + ImageFile.ImageFile.load_prepare(self) + def load_end(self) -> None: # allow closing if we're on the first frame, there's no next # This is the ImageFile.load path only, libtiff specific below. @@ -1416,7 +1421,16 @@ class TiffImageFile(ImageFile.ImageFile): if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) - self._size = xsize, ysize + self._will_be_transposed = self.tag_v2.get(ExifTags.Base.Orientation) in ( + 5, + 6, + 7, + 8, + ) + if self._will_be_transposed: + self._size = ysize, xsize + else: + self._size = xsize, ysize logger.debug("- size: %s", self.size) From 84e275d90629bfdf508f81a1142eddd5d5ea46e4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 20:27:35 +1000 Subject: [PATCH 224/652] Loading does not change mode --- src/PIL/ImageFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 721319fd7..d69d84568 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -322,7 +322,7 @@ class ImageFile(Image.Image): def load_prepare(self) -> None: # create image memory if necessary - if self._im is None or self.im.mode != self.mode: + if self._im is None: self.im = Image.core.new(self.mode, self.size) # create palette (optional) if self.mode == "P": From 9135fd0fb24607510f38b5fa4b1b4d279974363a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 20:56:55 +1000 Subject: [PATCH 225/652] Mention limit in error message --- src/PIL/PngImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index c268d7b1a..28ade293e 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -140,7 +140,7 @@ def _safe_zlib_decompress(s: bytes) -> bytes: dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) if dobj.unconsumed_tail: - msg = "Decompressed Data Too Large" + msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK" raise ValueError(msg) return plaintext From a859695d9a0bf9d9dc20ef7c053909658a12df6a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 18 Sep 2024 21:17:49 +1000 Subject: [PATCH 226/652] Rearranged code Co-authored-by: Alexander Karpinsky --- src/PIL/TiffImagePlugin.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 28e504744..437f7d98b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1421,12 +1421,8 @@ class TiffImageFile(ImageFile.ImageFile): if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) - self._will_be_transposed = self.tag_v2.get(ExifTags.Base.Orientation) in ( - 5, - 6, - 7, - 8, - ) + orientation = self.tag_v2.get(ExifTags.Base.Orientation) + self._will_be_transposed = orientation in (5, 6, 7, 8) if self._will_be_transposed: self._size = ysize, xsize else: From a92dca66bdeb26106447079fcf2d36bc397298c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 21:22:59 +1000 Subject: [PATCH 227/652] Use raw size for striped tiles --- src/PIL/TiffImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 437f7d98b..c024e57fc 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1569,7 +1569,7 @@ class TiffImageFile(ImageFile.ImageFile): if STRIPOFFSETS in self.tag_v2: offsets = self.tag_v2[STRIPOFFSETS] h = self.tag_v2.get(ROWSPERSTRIP, ysize) - w = self.size[0] + w = xsize else: # tiled image offsets = self.tag_v2[TILEOFFSETS] @@ -1603,9 +1603,9 @@ class TiffImageFile(ImageFile.ImageFile): ) ) x = x + w - if x >= self.size[0]: + if x >= xsize: x, y = 0, y + h - if y >= self.size[1]: + if y >= ysize: x = y = 0 layer += 1 else: From 629f5be52eb9043d70b5d5467f10a07ad3a68cdf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 22:56:29 +1000 Subject: [PATCH 228/652] In seek(), create core image at size needed for loading --- src/PIL/TiffImagePlugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index c024e57fc..14e6ea278 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1194,8 +1194,8 @@ class TiffImageFile(ImageFile.ImageFile): # Create a new core image object on second and # subsequent frames in the image. Image may be # different size/mode. - Image._decompression_bomb_check(self.size) - self.im = Image.core.new(self.mode, self.size) + Image._decompression_bomb_check(self._tile_size) + self.im = Image.core.new(self.mode, self._tile_size) def _seek(self, frame: int) -> None: self.fp = self._fp @@ -1276,8 +1276,8 @@ class TiffImageFile(ImageFile.ImageFile): return super().load() def load_prepare(self) -> None: - if self._im is None and self._will_be_transposed: - self.im = Image.core.new(self.mode, self.size[::-1]) + if self._im is None: + self.im = Image.core.new(self.mode, self._tile_size) ImageFile.ImageFile.load_prepare(self) def load_end(self) -> None: @@ -1421,9 +1421,9 @@ class TiffImageFile(ImageFile.ImageFile): if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) + self._tile_size = xsize, ysize orientation = self.tag_v2.get(ExifTags.Base.Orientation) - self._will_be_transposed = orientation in (5, 6, 7, 8) - if self._will_be_transposed: + if orientation in (5, 6, 7, 8): self._size = ysize, xsize else: self._size = xsize, ysize From 9adb476f37f28cd4a27bb66c6242e4d11925b53b Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 18 Sep 2024 23:58:23 +1000 Subject: [PATCH 229/652] Rearranged text --- src/PIL/ImageFont.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 70cd57de7..b694b817e 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -808,10 +808,10 @@ def truetype( ) -> FreeTypeFont: """ Load a TrueType or OpenType font from a file or file-like object, - and create a font object. For loading bitmap fonts instead, - see :py:func:`~PIL.ImageFont.load` and :py:func:`~PIL.ImageFont.load_path`. - This function loads a font object from the given file or file-like - object, and creates a font object for a font of the given size. + and create a font object. This function loads a font object from the given + file or file-like object, and creates a font object for a font of the given + size. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load` + and :py:func:`~PIL.ImageFont.load_path`. Pillow uses FreeType to open font files. On Windows, be aware that FreeType will keep the file open as long as the FreeTypeFont object exists. Windows From e33d8bb32bd04c5b29afa3dca52bce9cb2bbffbd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:19:53 +0300 Subject: [PATCH 230/652] Generate and upload attestations to PyPI --- .github/workflows/wheels.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 11564c142..35ea0496d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -296,3 +296,5 @@ jobs: merge-multiple: true - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + with: + attestations: true From 362ffaf9b7f063609bcd02ef30b1288e16e99cd1 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 18 Aug 2024 20:39:14 -0500 Subject: [PATCH 231/652] implement tiff exif multistrip support --- Tests/test_file_tiff.py | 3 ++- Tests/test_file_tiff_metadata.py | 22 ++++++++++++++++++++++ src/PIL/TiffImagePlugin.py | 8 +++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 190f83f40..44da25295 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -108,7 +108,8 @@ class TestFileTiff: assert_image_equal_tofile(im, "Tests/images/hopper.tif") with Image.open("Tests/images/hopper_bigtiff.tif") as im: - # multistrip support not yet implemented + # The data type of this file's StripOffsets tag is LONG8, + # which is not yet supported for offset data when saving multiple frames. del im.tag_v2[273] outfile = str(tmp_path / "temp.tif") diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 1e0310001..00fe745cf 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -181,6 +181,28 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG +def test_save_multiple_stripoffsets() -> None: + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[TiffImagePlugin.STRIPOFFSETS] = (123, 456) + assert ifd.tagtype[TiffImagePlugin.STRIPOFFSETS] == TiffTags.LONG + + # all values are in little-endian + assert ifd.tobytes() == ( + # number of tags == 1 + b"\x01\x00" + # tag id (2 bytes), type (2 bytes), count (4 bytes), value (4 bytes) + # == 273, 4, 2, 18 + # == TiffImagePlugin.STRIPOFFSETS, TiffTags.LONG, 2, 18 + # the value is the index of the tag data + b"\x11\x01\x04\x00\x02\x00\x00\x00\x12\x00\x00\x00" + # end of tags marker + b"\x00\x00\x00\x00" + # tag data == (149, 482) == (123 + 26, 456 + 26) + # 26 is the number of bytes before this data + b"\x95\x00\x00\x00\xe2\x01\x00\x00" + ) + + def test_no_duplicate_50741_tag() -> None: assert TAG_IDS["MakerNoteSafety"] == 50741 assert TAG_IDS["BestQualityScale"] == 50780 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 14e6ea278..d4c46a797 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -991,9 +991,11 @@ class ImageFileDirectory_v2(_IFDv2Base): if stripoffsets is not None: tag, typ, count, value, data = entries[stripoffsets] if data: - msg = "multistrip support not yet implemented" - raise NotImplementedError(msg) - value = self._pack("L", self._unpack("L", value)[0] + offset) + size, handler = self._load_dispatch[typ] + values = [val + offset for val in handler(self, data, self.legacy_api)] + data = self._write_dispatch[typ](self, *values) + else: + value = self._pack("L", self._unpack("L", value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data # pass 2: write entries to file From 1392eab89b9c7e6bbc1fa5a1d3af2434bcb04e5d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 Sep 2024 09:21:23 +1000 Subject: [PATCH 232/652] Updated Ghostscript to 10.4.0 --- .appveyor.yml | 4 ++-- .github/workflows/test-windows.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index f490561cd..de5e52742 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -34,8 +34,8 @@ install: - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip - 7z x nasm-win64.zip -oc:\ -- choco install ghostscript --version=10.3.1 -- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH% +- choco install ghostscript --version=10.4.0 +- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.04.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f58e8dae3..13147d86b 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -86,8 +86,8 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.3.1 --no-progress - echo "C:\Program Files\gs\gs10.03.1\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.4.0 --no-progress + echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images From 0f47ecd4325268156c27fa86799d31829f31fcea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 Sep 2024 15:07:06 +1000 Subject: [PATCH 233/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f23ec609f..4ab9eaf2b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,18 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Use transposed size after opening for TIFF images #8390 + [radarhere, homm] + +- Improve ImageFont error messages #8338 + [yngvem, radarhere, hugovk] + +- Mention MAX_TEXT_CHUNK limit in PNG error message #8391 + [radarhere] + +- Cast Dib handle to int #8385 + [radarhere] + - Accept float stroke widths #8369 [radarhere] From 46f2fa17be9768ec2647988b0446dd848b833a5b Mon Sep 17 00:00:00 2001 From: PavlNekrasov <95914807+PavlNekrasov@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:53:45 +0300 Subject: [PATCH 234/652] fix dereference before checking for NULL in the `ImagingTransformAffine` function The `imIn` pointer is checked for `NULL`, but it seems to be dereferenced before this check. You must first make sure that the pointer is not `NULL` before using it. --- src/libImaging/Geometry.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 2bfeed7b6..264c7d169 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -1035,6 +1035,10 @@ ImagingTransformAffine( double xx, yy; double xo, yo; + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (filterid || imIn->type == IMAGING_TYPE_SPECIAL) { return ImagingGenericTransform( imOut, imIn, x0, y0, x1, y1, affine_transform, a, filterid, fill @@ -1046,10 +1050,6 @@ ImagingTransformAffine( return ImagingScaleAffine(imOut, imIn, x0, y0, x1, y1, a, fill); } - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - if (x0 < 0) { x0 = 0; } From 9424b1a8920201b52086c0b06b5148db33255668 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:59:08 +0000 Subject: [PATCH 235/652] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/libImaging/Geometry.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 264c7d169..84aa442f0 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -1037,7 +1037,7 @@ ImagingTransformAffine( if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging)ImagingError_ModeError(); - } + } if (filterid || imIn->type == IMAGING_TYPE_SPECIAL) { return ImagingGenericTransform( From 693a68b2bb7fdf01bbafb7070320c16ec57ca78d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 20 Sep 2024 18:46:47 +1000 Subject: [PATCH 236/652] Updated link to OSS Fuzz issues --- README.md | 2 +- docs/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b4c6d2987..5bbebaccb 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ As of 2019, Pillow development is Code coverage - Fuzzing Status diff --git a/docs/index.rst b/docs/index.rst index 3a12953f0..18f5c3d13 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -54,7 +54,7 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Fri, 20 Sep 2024 19:20:53 +1000 Subject: [PATCH 237/652] Check image value before use --- src/libImaging/Geometry.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 84aa442f0..1e2abd7e7 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -791,15 +791,15 @@ ImagingGenericTransform( char *out; double xx, yy; + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + ImagingTransformFilter filter = getfilter(imIn, filterid); if (!filter) { return (Imaging)ImagingError_ValueError("bad filter number"); } - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - ImagingCopyPalette(imOut, imIn); ImagingSectionEnter(&cookie); From 8b6fa92cc885cbd1d7b0beb60aaad9ab47298cbf Mon Sep 17 00:00:00 2001 From: Yay295 Date: Fri, 20 Sep 2024 08:39:11 -0500 Subject: [PATCH 238/652] rewrite some comments Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_tiff_metadata.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 00fe745cf..36aabf4f8 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -191,14 +191,15 @@ def test_save_multiple_stripoffsets() -> None: # number of tags == 1 b"\x01\x00" # tag id (2 bytes), type (2 bytes), count (4 bytes), value (4 bytes) - # == 273, 4, 2, 18 - # == TiffImagePlugin.STRIPOFFSETS, TiffTags.LONG, 2, 18 - # the value is the index of the tag data + # TiffImagePlugin.STRIPOFFSETS, TiffTags.LONG, 2, 18 + # where STRIPOFFSETS is 273, LONG is 4 + # and 18 is the offset of the tag data b"\x11\x01\x04\x00\x02\x00\x00\x00\x12\x00\x00\x00" - # end of tags marker + # end of entries b"\x00\x00\x00\x00" - # tag data == (149, 482) == (123 + 26, 456 + 26) - # 26 is the number of bytes before this data + # 26 is the total number of bytes output, + # the offset for any auxiliary strip data that will then be appended + # (123 + 26, 456 + 26) == (149, 482) b"\x95\x00\x00\x00\xe2\x01\x00\x00" ) From 83c7043471df575e446c44a4d384df56cd76538b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Sep 2024 15:54:27 +1000 Subject: [PATCH 239/652] Rename variable, since alpha channel is not dropped --- src/_webp.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index 92d5c20fe..dfda7048d 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -89,8 +89,6 @@ HandleMuxError(WebPMuxError err, char *chunk) { static int import_frame_libwebp(WebPPicture *frame, Imaging im) { - int drop_alpha = strcmp(im->mode, "RGBA"); - if (strcmp(im->mode, "RGBA") && strcmp(im->mode, "RGB") && strcmp(im->mode, "RGBX")) { PyErr_SetString(PyExc_ValueError, "unsupported image mode"); @@ -106,10 +104,11 @@ import_frame_libwebp(WebPPicture *frame, Imaging im) { return -2; } + int ignore_fourth_channel = strcmp(im->mode, "RGBA"); for (int y = 0; y < im->ysize; ++y) { UINT8 *src = (UINT8 *)im->image32[y]; UINT32 *dst = frame->argb + frame->argb_stride * y; - if (drop_alpha) { + if (ignore_fourth_channel) { for (int x = 0; x < im->xsize; ++x) { dst[x] = ((UINT32)(src[x * 4 + 2]) | ((UINT32)(src[x * 4 + 1]) << 8) | From 75cb1c1b87ae4be028f3e15141c14c28dd0d04a0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Sep 2024 16:02:23 +1000 Subject: [PATCH 240/652] Test unsupported image mode --- Tests/test_file_webp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index e7c887279..79f6bb4e0 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -127,6 +127,11 @@ class TestFileWebp: reloaded.seek(1) assert_image_similar(im2, reloaded, 1) + def test_unsupported_image_mode(self) -> None: + im = Image.new("1", (1, 1)) + with pytest.raises(ValueError): + _webp.WebPEncode(im.getim(), False, 0, 0, "", 4, 0, b"", "") + def test_icc_profile(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) self._roundtrip( From aa22b241690e0c0084523bc7e4ea137d3596ee93 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Sep 2024 16:22:50 +1000 Subject: [PATCH 241/652] Load before trying to catch exceptions --- Tests/test_imagemorph.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 9da32aa01..6180a7b5d 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -324,17 +324,17 @@ def test_set_lut() -> None: def test_wrong_mode() -> None: lut = ImageMorph.LutBuilder(op_name="corner").build_lut() - imrgb = Image.new("RGB", (10, 10)) - iml = Image.new("L", (10, 10)) + imrgb_ptr = Image.new("RGB", (10, 10)).getim() + iml_ptr = Image.new("L", (10, 10)).getim() with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), imrgb.getim(), iml.getim()) + _imagingmorph.apply(bytes(lut), imrgb_ptr, iml_ptr) with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), iml.getim(), imrgb.getim()) + _imagingmorph.apply(bytes(lut), iml_ptr, imrgb_ptr) with pytest.raises(RuntimeError): - _imagingmorph.match(bytes(lut), imrgb.getim()) + _imagingmorph.match(bytes(lut), imrgb_ptr) # Should not raise - _imagingmorph.match(bytes(lut), iml.getim()) + _imagingmorph.match(bytes(lut), iml_ptr) From 5d430eab25a7695cdba79ddf250c40e1e10832da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Sep 2024 16:24:54 +1000 Subject: [PATCH 242/652] Added release notes --- docs/releasenotes/11.0.0.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 36334e39f..3194f1a52 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -73,6 +73,16 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ +Get Internal Pointers to Objects +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been +deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining +raw pointers to ``ImagingCore`` internals. To interact with C code, you can use +``Image.Image.getim()``, which returns a ``Capsule`` object. + ICNS (width, height, scale) sizes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 9f409e823b0914b18b0a5447e93bb5f40c0e0c4c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Sep 2024 16:44:39 +1000 Subject: [PATCH 243/652] Use getim() --- src/PIL/ImageTk.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index c36cf9b84..72fa798eb 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -176,15 +176,14 @@ class PhotoImage: the bitmap image. """ # convert to blittable - im.load() + ptr = im.getim() image = im.im - if image.isblock() and im.mode == self.__mode: - block = image - else: + if not image.isblock() or im.mode != self.__mode: block = Image.core.new_block(self.__mode, im.size) image.convert2(block, image) # convert directly between buffers + ptr = block.ptr - _pyimagingtkcall("PyImagingPhoto", self.__photo, block.ptr) + _pyimagingtkcall("PyImagingPhoto", self.__photo, ptr) # -------------------------------------------------------------------- From 11bcd5aaa9c35d7f8557b0f5abf899b0adf0cecf Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 22 Sep 2024 13:39:22 +0400 Subject: [PATCH 244/652] Fix hasattr for ImageTk.PhotoImage.__del__ --- src/PIL/ImageTk.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 72fa798eb..c817ab09c 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -131,9 +131,10 @@ class PhotoImage: self.paste(image) def __del__(self) -> None: - if not hasattr(self, "__photo"): + try: + name = self.__photo.name + except AttributeError: return - name = self.__photo.name self.__photo.name = None try: self.__photo.tk.call("image", "delete", name) From 8fa5ba844361536135d3d6cb67e3a3f4a1a651f4 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 19 Sep 2024 11:05:52 -0500 Subject: [PATCH 245/652] add common windows ci test script --- .appveyor.yml | 5 ++--- .ci/test.cmd | 3 +++ .github/workflows/test-windows.yml | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 .ci/test.cmd diff --git a/.appveyor.yml b/.appveyor.yml index f490561cd..7d45e11ee 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -53,9 +53,8 @@ test_script: - cd c:\pillow - '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma' - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% -- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' -- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' -#- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? +- path %PYTHON%;%PATH% +- .ci\test.cmd after_test: - curl -Os https://uploader.codecov.io/latest/windows/codecov.exe diff --git a/.ci/test.cmd b/.ci/test.cmd new file mode 100644 index 000000000..aafc9290c --- /dev/null +++ b/.ci/test.cmd @@ -0,0 +1,3 @@ +python.exe -c "from PIL import Image" +IF ERRORLEVEL 1 EXIT /B +python.exe -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f58e8dae3..e36dc05fd 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -190,8 +190,8 @@ jobs: - name: Test Pillow run: | - path %GITHUB_WORKSPACE%\\winbuild\\build\\bin;%PATH% - python.exe -m pytest -vx -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests + path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH% + .ci\test.cmd shell: cmd - name: Prepare to upload errors From be3192ecd41581761a78913950ec3fb4be4dd981 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 19 Sep 2024 12:08:34 -0500 Subject: [PATCH 246/652] use .ci/test.sh in test-mingw.yml --- .ci/test.sh | 2 +- .github/workflows/test-mingw.yml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.ci/test.sh b/.ci/test.sh index 8ff7c5f64..3f0ddc350 100755 --- a/.ci/test.sh +++ b/.ci/test.sh @@ -4,4 +4,4 @@ set -e python3 -c "from PIL import Image" -python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests $REVERSE +python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index e5e1ec32e..c7a73439c 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -80,8 +80,7 @@ jobs: - name: Test Pillow run: | python3 selftest.py --installed - python3 -c "from PIL import Image" - python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests + .ci/test.sh - name: Upload coverage uses: codecov/codecov-action@v4 From 6081640aa5b68a177ed54cfd03e4ae6a748e8be8 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sun, 22 Sep 2024 22:46:35 +0200 Subject: [PATCH 247/652] winbuild: Build freetype.vcxproj to fix passing in custom parameters. --- winbuild/build_prepare.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e2022d283..d350e2575 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -292,8 +292,12 @@ DEPS: dict[str, dict[str, Any]] = { }, "build": [ cmd_rmdir("objs"), - cmd_msbuild("MSBuild.sln", "Release Static", "Clean"), - cmd_msbuild("MSBuild.sln", "Release Static", "Build"), + cmd_msbuild( + r"builds\windows\vc2010\freetype.vcxproj", "Release Static", "Clean" + ), + cmd_msbuild( + r"builds\windows\vc2010\freetype.vcxproj", "Release Static", "Build" + ), cmd_xcopy("include", "{inc_dir}"), ], "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], From 8adf15a6ee1bab667a0e7b76d19ea0f26b29ce85 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Sep 2024 09:29:16 +1000 Subject: [PATCH 248/652] Updated raqm to 0.10.2 --- depends/install_raqm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 070ba23a1..5d862403e 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,7 +2,7 @@ # install raqm -archive=libraqm-0.10.1 +archive=libraqm-0.10.2 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz From 9fe014876ab3dbc2a765ae1175659cc0d7bc7e42 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 Sep 2024 22:42:39 +1000 Subject: [PATCH 249/652] Shared libraries may be located within usr/lib64 --- depends/install_imagequant.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 867b31e4d..060d9840e 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -23,7 +23,12 @@ else cargo cinstall --prefix=/usr --destdir=. # Copy into place - sudo cp usr/lib/libimagequant.so* /usr/lib/ + if [ -d "usr/lib64" ]; then + lib="lib64" + else + lib="lib" + fi + sudo cp usr/$lib/libimagequant.so* /usr/lib/ sudo cp usr/include/libimagequant.h /usr/include/ if [ -n "$GITHUB_ACTIONS" ]; then From 3f8b496431c3eb2f0cedad35669e725fd3be45e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Sep 2024 09:54:26 +1000 Subject: [PATCH 250/652] Updated harfbuzz to 10.0.0 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4d40f7fab..92e9ad5af 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -17,7 +17,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 if [[ "$MB_ML_VER" != 2014 ]]; then - HARFBUZZ_VERSION=9.0.0 + HARFBUZZ_VERSION=10.0.0 else HARFBUZZ_VERSION=8.5.0 fi diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d350e2575..db592d23c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -112,7 +112,7 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.15", - "HARFBUZZ": "9.0.0", + "HARFBUZZ": "10.0.0", "JPEGTURBO": "3.0.4", "LCMS2": "2.16", "LIBPNG": "1.6.44", From 61c83a882cc0a1b78f2e8f1e3ce38616f95c6f3e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Sep 2024 00:00:22 +1000 Subject: [PATCH 251/652] Updated macOS version to 10.15 for PyPy x86-64 --- .github/workflows/wheels.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 11564c142..d3c2ac44c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -105,13 +105,18 @@ jobs: - name: "macOS 10.10 x86_64" os: macos-13 cibw_arch: x86_64 - build: "pp310* cp3{9,10,11}*" + build: "cp3{9,10,11}*" macosx_deployment_target: "10.10" - name: "macOS 10.13 x86_64" os: macos-13 cibw_arch: x86_64 build: "cp3{12,13}*" macosx_deployment_target: "10.13" + - name: "macOS 10.15 x86_64" + os: macos-13 + cibw_arch: x86_64 + build: "pp310*" + macosx_deployment_target: "10.15" - name: "macOS arm64" os: macos-latest cibw_arch: arm64 From 72144ec5300d92bfffd029ccfb52e4d087a0bd95 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Sep 2024 08:33:18 +1000 Subject: [PATCH 252/652] Updated harfbuzz to 10.0.1 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 92e9ad5af..b5fbdc421 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -17,7 +17,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 if [[ "$MB_ML_VER" != 2014 ]]; then - HARFBUZZ_VERSION=10.0.0 + HARFBUZZ_VERSION=10.0.1 else HARFBUZZ_VERSION=8.5.0 fi diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index db592d23c..84ad6417e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -112,7 +112,7 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.15", - "HARFBUZZ": "10.0.0", + "HARFBUZZ": "10.0.1", "JPEGTURBO": "3.0.4", "LCMS2": "2.16", "LIBPNG": "1.6.44", From 0a99d63028ebcc92d950cd63cd805eb558a5131c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 20 Sep 2024 22:31:00 +1000 Subject: [PATCH 253/652] Use field size in AppendingTiffWriter --- src/PIL/TiffImagePlugin.py | 69 ++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index d4c46a797..0fe55e426 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2121,13 +2121,24 @@ class AppendingTiffWriter(io.BytesIO): def write(self, data: Buffer, /) -> int: return self.f.write(data) - def readShort(self) -> int: - (value,) = struct.unpack(self.shortFmt, self.f.read(2)) + def _fmt(self, field_size: int) -> str: + try: + return {2: "H", 4: "L"}[field_size] + except KeyError: + msg = "offset is not supported" + raise RuntimeError(msg) + + def _read(self, field_size: int) -> int: + (value,) = struct.unpack( + self.endian + self._fmt(field_size), self.f.read(field_size) + ) return value + def readShort(self) -> int: + return self._read(2) + def readLong(self) -> int: - (value,) = struct.unpack(self.longFmt, self.f.read(4)) - return value + return self._read(4) @staticmethod def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: @@ -2140,15 +2151,18 @@ class AppendingTiffWriter(io.BytesIO): bytes_written = self.f.write(struct.pack(self.longFmt, value)) self._verify_bytes_written(bytes_written, 4) + def _rewriteLast(self, value: int, field_size: int) -> None: + self.f.seek(-field_size, os.SEEK_CUR) + bytes_written = self.f.write( + struct.pack(self.endian + self._fmt(field_size), value) + ) + self._verify_bytes_written(bytes_written, field_size) + def rewriteLastShort(self, value: int) -> None: - self.f.seek(-2, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - self._verify_bytes_written(bytes_written, 2) + return self._rewriteLast(value, 2) def rewriteLastLong(self, value: int) -> None: - self.f.seek(-4, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - self._verify_bytes_written(bytes_written, 4) + return self._rewriteLast(value, 4) def writeShort(self, value: int) -> None: bytes_written = self.f.write(struct.pack(self.shortFmt, value)) @@ -2180,32 +2194,22 @@ class AppendingTiffWriter(io.BytesIO): cur_pos = self.f.tell() if is_local: - self.fixOffsets( - count, isShort=(field_size == 2), isLong=(field_size == 4) - ) + self._fixOffsets(count, field_size) self.f.seek(cur_pos + 4) else: self.f.seek(offset) - self.fixOffsets( - count, isShort=(field_size == 2), isLong=(field_size == 4) - ) + self._fixOffsets(count, field_size) self.f.seek(cur_pos) elif is_local: # skip the locally stored value that is not an offset self.f.seek(4, os.SEEK_CUR) - def fixOffsets( - self, count: int, isShort: bool = False, isLong: bool = False - ) -> None: - if not isShort and not isLong: - msg = "offset is neither short nor long" - raise RuntimeError(msg) - + def _fixOffsets(self, count: int, field_size: int) -> None: for i in range(count): - offset = self.readShort() if isShort else self.readLong() + offset = self._read(field_size) offset += self.offsetOfNewPage - if isShort and offset >= 65536: + if field_size == 2 and offset >= 65536: # offset is now too large - we must convert shorts to longs if count != 1: msg = "not implemented" @@ -2217,10 +2221,19 @@ class AppendingTiffWriter(io.BytesIO): self.f.seek(-10, os.SEEK_CUR) self.writeShort(TiffTags.LONG) # rewrite the type to LONG self.f.seek(8, os.SEEK_CUR) - elif isShort: - self.rewriteLastShort(offset) else: - self.rewriteLastLong(offset) + self._rewriteLast(offset, field_size) + + def fixOffsets( + self, count: int, isShort: bool = False, isLong: bool = False + ) -> None: + if isShort: + field_size = 2 + elif isLong: + field_size = 4 + else: + field_size = 0 + return self._fixOffsets(count, field_size) def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: From 24e9961c4f68bdc676a5e7a2dde02460f8bd62c1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Sep 2024 18:45:16 +1000 Subject: [PATCH 254/652] Support writing LONG8 offsets --- Tests/test_file_tiff.py | 4 ---- src/PIL/TiffImagePlugin.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 44da25295..6d76ccca6 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -108,10 +108,6 @@ class TestFileTiff: assert_image_equal_tofile(im, "Tests/images/hopper.tif") with Image.open("Tests/images/hopper_bigtiff.tif") as im: - # The data type of this file's StripOffsets tag is LONG8, - # which is not yet supported for offset data when saving multiple frames. - del im.tag_v2[273] - outfile = str(tmp_path / "temp.tif") im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0fe55e426..a655bb741 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2123,7 +2123,7 @@ class AppendingTiffWriter(io.BytesIO): def _fmt(self, field_size: int) -> str: try: - return {2: "H", 4: "L"}[field_size] + return {2: "H", 4: "L", 8: "Q"}[field_size] except KeyError: msg = "offset is not supported" raise RuntimeError(msg) From 5d16eb73eb658daceb7b0af9f8d29cca96888a48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Sep 2024 19:07:32 +1000 Subject: [PATCH 255/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4ab9eaf2b..ae7370a79 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Fixed writing multiple StripOffsets to TIFF #8317 + [Yay295, radarhere] + +- Shared imagequant libraries may be located within usr/lib64 #8407 + [radarhere] + +- Fix dereference before checking for NULL in ImagingTransformAffine #8398 + [PavlNekrasov] + - Use transposed size after opening for TIFF images #8390 [radarhere, homm] From 7cf351cfc50dcbda82270c80e44e32643688a7a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Sep 2024 20:37:41 +1000 Subject: [PATCH 256/652] Added additional test --- Tests/test_file_tiff.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 6d76ccca6..af766022b 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -728,6 +728,20 @@ class TestFileTiff: with Image.open(mp) as reread: assert reread.n_frames == 3 + def test_fixoffsets(self) -> None: + b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") + with TiffImagePlugin.AppendingTiffWriter(b) as a: + b.seek(0) + a.fixOffsets(1, isShort=True) + + b.seek(0) + a.fixOffsets(1, isLong=True) + + # Neither short nor long + b.seek(0) + with pytest.raises(RuntimeError): + a.fixOffsets(1) + def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs From d8e3572caad2f3dce65db6951909f161a4021687 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Sep 2024 08:52:29 +1000 Subject: [PATCH 257/652] Updated fribidi to 1.0.16 --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 84ad6417e..026d9d306 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -111,7 +111,7 @@ ARCHITECTURES = { V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", - "FRIBIDI": "1.0.15", + "FRIBIDI": "1.0.16", "HARFBUZZ": "10.0.1", "JPEGTURBO": "3.0.4", "LCMS2": "2.16", From b9d176856135b581bd503e04ab2407318cbe7689 Mon Sep 17 00:00:00 2001 From: Alexander Karpinsky Date: Thu, 26 Sep 2024 16:27:55 +0400 Subject: [PATCH 258/652] Catch AttributeError for BitmapImage.__photo Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageTk.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index c817ab09c..bf29fdba5 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -219,9 +219,10 @@ class BitmapImage: self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw) def __del__(self) -> None: - if not hasattr(self, "__photo"): + try: + name = self.__photo.name + except AttributeError: return - name = self.__photo.name self.__photo.name = None try: self.__photo.tk.call("image", "delete", name) From 4ca2b92503ed0250d7e48bf4c187c4fd1dbbdc54 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Sep 2024 14:59:27 +1000 Subject: [PATCH 259/652] Use $IS_ALPINE instead of $MB_ML_LIBC --- .github/workflows/wheels-dependencies.sh | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index b5fbdc421..5175a2a92 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -156,12 +156,10 @@ if [[ -n "$IS_MACOS" ]]; then fi brew install meson pkg-config -elif [[ "$MB_ML_LIBC" == "manylinux" ]]; then - if [[ "$HARFBUZZ_VERSION" != 8.5.0 ]]; then - yum install -y meson - fi -else +elif [[ -n "$IS_ALPINE" ]]; then apk add meson +elif [[ "$HARFBUZZ_VERSION" != 8.5.0 ]]; then + yum install -y meson fi wrap_wheel_builder build From 71da6d8952c174fa0a465fa6caf98989d6eff9c9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Sep 2024 16:37:10 +1000 Subject: [PATCH 260/652] Downgrade harfbuzz on OSS Fuzz --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 5175a2a92..4289afdf3 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -16,7 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -if [[ "$MB_ML_VER" != 2014 ]]; then +if [[ "$MB_ML_VER" != 2014 ]] && [[ -z "$SANITIZER" ]]; then HARFBUZZ_VERSION=10.0.1 else HARFBUZZ_VERSION=8.5.0 From fb8db83122a2f942958fc93103ad7b043cca3a3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Sep 2024 17:35:06 +1000 Subject: [PATCH 261/652] Updated harfbuzz to 10.0.1 on macOS --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4289afdf3..bb0f8a307 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -16,7 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -if [[ "$MB_ML_VER" != 2014 ]] && [[ -z "$SANITIZER" ]]; then +if [[ -n "$IS_MACOS" ]] || ([[ "$MB_ML_VER" != 2014 ]] && [[ -z "$SANITIZER" ]]); then HARFBUZZ_VERSION=10.0.1 else HARFBUZZ_VERSION=8.5.0 From 485a062010d0942f88686dbf7ece6a765d329e53 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Sep 2024 23:13:03 +1000 Subject: [PATCH 262/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ae7370a79..6ff35ea4f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Use Capsule for WebP saving #8386 + [homm, radarhere] + - Fixed writing multiple StripOffsets to TIFF #8317 [Yay295, radarhere] From bda62c1ac52f6a843263d2e195025d204a094f78 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Sep 2024 22:11:03 +1000 Subject: [PATCH 263/652] Revert "Temporarily disable cifuzz" This reverts commit 31469407166026ec6d74d2df07196ef2d4e32ab4. --- .github/workflows/cifuzz.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 033ff98ce..eb73fc6a7 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -24,8 +24,6 @@ concurrency: jobs: Fuzzing: - # Disabled until google/oss-fuzz#11419 upgrades Python to 3.9+ - if: false runs-on: ubuntu-latest steps: - name: Build Fuzzers From 04a00d273c6fe72f8a2f4099950831deb8ad5407 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Sep 2024 14:27:40 +1000 Subject: [PATCH 264/652] Support all resampling filters when resizing I;16* images --- Tests/test_image_resize.py | 16 ++++++-- src/_imaging.c | 10 ++--- src/libImaging/Resample.c | 84 +++++++++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 11 deletions(-) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index d9ddf5009..1cd9c3800 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -44,9 +44,19 @@ class TestImagingCoreResize: self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR) with pytest.raises(ValueError): self.resize(hopper("P"), (15, 12), Image.Resampling.BILINEAR) - with pytest.raises(ValueError): - self.resize(hopper("I;16"), (15, 12), Image.Resampling.BILINEAR) - for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]: + for mode in [ + "L", + "I", + "I;16", + "I;16L", + "I;16B", + "I;16N", + "F", + "RGB", + "RGBA", + "CMYK", + "YCbCr", + ]: im = hopper(mode) r = self.resize(im, (15, 12), Image.Resampling.BILINEAR) assert r.mode == mode diff --git a/src/_imaging.c b/src/_imaging.c index 07d9a64cc..426036335 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1579,16 +1579,12 @@ _putdata(ImagingObject *self, PyObject *args) { int bigendian = 0; if (image->type == IMAGING_TYPE_SPECIAL) { // I;16* - if (strcmp(image->mode, "I;16N") == 0) { + if (strcmp(image->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN - bigendian = 1; -#else - bigendian = 0; + || strcmp(image->mode, "I;16N") == 0 #endif - } else if (strcmp(image->mode, "I;16B") == 0) { + ) { bigendian = 1; - } else { - bigendian = 0; } } for (i = x = y = 0; i < n; i++) { diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 222d6bca4..f5e386dc2 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -460,6 +460,83 @@ ImagingResampleVertical_8bpc( ImagingSectionLeave(&cookie); } +void +ImagingResampleHorizontal_16bpc( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk +) { + ImagingSectionCookie cookie; + double ss; + int xx, yy, x, xmin, xmax, ss_int; + double *k; + + int bigendian = 0; + if (strcmp(imIn->mode, "I;16N") == 0 +#ifdef WORDS_BIGENDIAN + || strcmp(imIn->mode, "I;16B") == 0 +#endif + ) { + bigendian = 1; + } + + ImagingSectionEnter(&cookie); + for (yy = 0; yy < imOut->ysize; yy++) { + for (xx = 0; xx < imOut->xsize; xx++) { + xmin = bounds[xx * 2 + 0]; + xmax = bounds[xx * 2 + 1]; + k = &kk[xx * ksize]; + ss = 0.0; + for (x = 0; x < xmax; x++) { + ss += (imIn->image8[yy + offset][(x + xmin) * 2 + (bigendian ? 1 : 0)] + + (imIn->image8[yy + offset][(x + xmin) * 2 + (bigendian ? 0 : 1)] + << 8)) * + k[x]; + } + ss_int = ROUND_UP(ss); + imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256); + imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8); + } + } + ImagingSectionLeave(&cookie); +} + +void +ImagingResampleVertical_16bpc( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk +) { + ImagingSectionCookie cookie; + double ss; + int xx, yy, y, ymin, ymax, ss_int; + double *k; + + int bigendian = 0; + if (strcmp(imIn->mode, "I;16N") == 0 +#ifdef WORDS_BIGENDIAN + || strcmp(imIn->mode, "I;16B") == 0 +#endif + ) { + bigendian = 1; + } + + ImagingSectionEnter(&cookie); + for (yy = 0; yy < imOut->ysize; yy++) { + ymin = bounds[yy * 2 + 0]; + ymax = bounds[yy * 2 + 1]; + k = &kk[yy * ksize]; + for (xx = 0; xx < imOut->xsize; xx++) { + ss = 0.0; + for (y = 0; y < ymax; y++) { + ss += (imIn->image8[y + ymin][xx * 2 + (bigendian ? 1 : 0)] + + (imIn->image8[y + ymin][xx * 2 + (bigendian ? 0 : 1)] << 8)) * + k[y]; + } + ss_int = ROUND_UP(ss); + imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256); + imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8); + } + } + ImagingSectionLeave(&cookie); +} + void ImagingResampleHorizontal_32bpc( Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk @@ -574,7 +651,12 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) { } if (imIn->type == IMAGING_TYPE_SPECIAL) { - return (Imaging)ImagingError_ModeError(); + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ResampleHorizontal = ImagingResampleHorizontal_16bpc; + ResampleVertical = ImagingResampleVertical_16bpc; + } else { + return (Imaging)ImagingError_ModeError(); + } } else if (imIn->image8) { ResampleHorizontal = ImagingResampleHorizontal_8bpc; ResampleVertical = ImagingResampleVertical_8bpc; From e306546bf18b01c9bfa68ea397f2f5d5690113e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Sep 2024 15:10:16 +1000 Subject: [PATCH 265/652] Use ruff check --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 94f7565d8..ec4159627 100644 --- a/Makefile +++ b/Makefile @@ -117,7 +117,7 @@ lint-fix: python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black python3 -m black . python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff - python3 -m ruff --fix . + python3 -m ruff check --fix . .PHONY: mypy mypy: From 2e73ffe7034457461b13da71b05645999dd3c0ef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Sep 2024 15:29:09 +1000 Subject: [PATCH 266/652] Exclude multibuild from black and ruff --- pyproject.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 228c344e8..0d0a6f170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,9 +97,13 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" -[tool.ruff] -fix = true +[tool.black] +exclude = "wheels/multibuild" +[tool.ruff] +exclude = [ "wheels/multibuild" ] + +fix = true lint.select = [ "C4", # flake8-comprehensions "E", # pycodestyle errors From d33270ab51f6b62710b227394f2dadf913370d96 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Sep 2024 15:35:42 +1000 Subject: [PATCH 267/652] Set default resize sampling for I;16* images to BICUBIC --- Tests/test_image_resize.py | 8 ++++---- docs/releasenotes/11.0.0.rst | 7 ++++--- src/PIL/Image.py | 8 ++++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 1cd9c3800..8548fb5da 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -315,14 +315,14 @@ class TestImageResize: im = im.resize((64, 64)) assert im.size == (64, 64) - @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) + @pytest.mark.parametrize( + "mode", ("L", "RGB", "I", "I;16", "I;16L", "I;16B", "I;16N", "F") + ) def test_default_filter_bicubic(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) - @pytest.mark.parametrize( - "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16") - ) + @pytest.mark.parametrize("mode", ("1", "P", "BGR;15", "BGR;16")) def test_default_filter_nearest(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 36334e39f..1218c5014 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -119,10 +119,11 @@ Specific WebP Feature Checks API Changes =========== -TODO -^^^^ +Default resampling filter for I;16* image modes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +The default resampling filter for I;16, I;16L, I;16B and I;16N has been changed from +``Image.NEAREST`` to ``Image.BICUBIC``, to match the majority of modes. API Additions ============= diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3f94cef38..bf9079f8b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2278,8 +2278,8 @@ class Image: :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. If the image has mode "1" or "P", it is always set to - :py:data:`Resampling.NEAREST`. If the image mode specifies a number - of bits, such as "I;16", then the default filter is + :py:data:`Resampling.NEAREST`. If the image mode is "BGR;15", + "BGR;16" or "BGR;24", then the default filter is :py:data:`Resampling.NEAREST`. Otherwise, the default filter is :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. :param box: An optional 4-tuple of floats providing @@ -2302,8 +2302,8 @@ class Image: """ if resample is None: - type_special = ";" in self.mode - resample = Resampling.NEAREST if type_special else Resampling.BICUBIC + bgr = self.mode.startswith("BGR;") + resample = Resampling.NEAREST if bgr else Resampling.BICUBIC elif resample not in ( Resampling.NEAREST, Resampling.BILINEAR, From 30fca7a1d62a8b41f3536adb35355f5aa81c6a2f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Sep 2024 18:38:45 +1000 Subject: [PATCH 268/652] Install meson through pip --- .github/workflows/wheels-dependencies.sh | 36 ++++++++---------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index bb0f8a307..b3996d5a1 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -16,11 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -if [[ -n "$IS_MACOS" ]] || ([[ "$MB_ML_VER" != 2014 ]] && [[ -z "$SANITIZER" ]]); then - HARFBUZZ_VERSION=10.0.1 -else - HARFBUZZ_VERSION=8.5.0 -fi +HARFBUZZ_VERSION=10.0.1 LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.0.4 OPENJPEG_VERSION=2.5.2 @@ -65,21 +61,15 @@ function build_brotli { } function build_harfbuzz { - if [[ "$HARFBUZZ_VERSION" == 8.5.0 ]]; then - export FREETYPE_LIBS=-lfreetype - export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ - build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no - export FREETYPE_LIBS="" - export FREETYPE_CFLAGS="" - else - local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) - (cd $out_dir \ - && meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled) - (cd $out_dir/build \ - && meson install) - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libharfbuzz* /usr/local/lib - fi + python3 -m pip install meson ninja + + local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) + (cd $out_dir \ + && meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled) + (cd $out_dir/build \ + && meson install) + if [[ "$MB_ML_LIBC" == "manylinux" ]]; then + cp /usr/local/lib64/libharfbuzz* /usr/local/lib fi } @@ -155,11 +145,7 @@ if [[ -n "$IS_MACOS" ]]; then brew remove --ignore-dependencies webp fi - brew install meson pkg-config -elif [[ -n "$IS_ALPINE" ]]; then - apk add meson -elif [[ "$HARFBUZZ_VERSION" != 8.5.0 ]]; then - yum install -y meson + brew install pkg-config fi wrap_wheel_builder build From e976096c2eda0a41c49e0f06e6f84f6b3d0c99b0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Sep 2024 20:39:04 +1000 Subject: [PATCH 269/652] Allow libimagequant shared library path to change --- depends/install_imagequant.sh | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 060d9840e..8d62d5ac7 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -23,19 +23,14 @@ else cargo cinstall --prefix=/usr --destdir=. # Copy into place - if [ -d "usr/lib64" ]; then - lib="lib64" - else - lib="lib" - fi - sudo cp usr/$lib/libimagequant.so* /usr/lib/ + sudo find usr -name libimagequant.so* -exec cp {} /usr/lib/ \; sudo cp usr/include/libimagequant.h /usr/include/ if [ -n "$GITHUB_ACTIONS" ]; then # Copy to cache rm -rf ~/cache-$archive_name mkdir ~/cache-$archive_name - cp usr/lib/libimagequant.so* ~/cache-$archive_name/ + find usr -name libimagequant.so* -exec cp {} ~/cache-$archive_name/ \; cp usr/include/libimagequant.h ~/cache-$archive_name/ fi From ed143f5fec816c7164ff2d8a76deef6a8abfaf77 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Sep 2024 06:40:50 +1000 Subject: [PATCH 270/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6ff35ea4f..b4644c541 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,15 +5,15 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Improved copying imagequant libraries #8420 + [radarhere] + - Use Capsule for WebP saving #8386 [homm, radarhere] - Fixed writing multiple StripOffsets to TIFF #8317 [Yay295, radarhere] -- Shared imagequant libraries may be located within usr/lib64 #8407 - [radarhere] - - Fix dereference before checking for NULL in ImagingTransformAffine #8398 [PavlNekrasov] From 3a734b5d4b2ee9d602e4d7d3e89530498bac5e68 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Sep 2024 05:19:39 +0000 Subject: [PATCH 271/652] Update scientific-python/upload-nightly-action action to v0.6.0 --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d3c2ac44c..ee0c33166 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -278,7 +278,7 @@ jobs: path: dist merge-multiple: true - name: Upload wheels to scientific-python-nightly-wheels - uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 + uses: scientific-python/upload-nightly-action@ccf29c805b5d0c1dc31fa97fcdb962be074cade3 # 0.6.0 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} From 21ab53e0c0f81427c45e4b4205d5d6d07008ee81 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Sep 2024 12:38:13 +1000 Subject: [PATCH 272/652] Updated test environment documentation --- docs/installation/platform-support.rst | 2 +- winbuild/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index f2ef9cacb..9216248a0 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -48,7 +48,7 @@ These platforms are built and tested for every change. | Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, | | | | s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2016 | 3.9 | x86-64 | +| Windows Server 2019 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 | | | 3.12, 3.13, PyPy3 | | diff --git a/winbuild/README.md b/winbuild/README.md index c8048bcc9..f6111c79b 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -11,7 +11,7 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. * Requires CMake 3.15 or newer (available as Visual Studio component). -* Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor). +* Tested on Windows Server 2019 with Visual Studio 2019 Community and Visual Studio 2022 Community (AppVeyor). * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). The following is a simplified version of the script used on AppVeyor: From 7a62c788edb77e6c0c11f31a180018411d3955f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Sep 2024 12:42:39 +1000 Subject: [PATCH 273/652] Updated tested Python versions --- docs/installation/platform-support.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 9216248a0..4cd3f2814 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -17,13 +17,13 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Operating system | Tested Python versions | Tested architecture | +==================================+============================+=====================+ -| Alpine | 3.9 | x86-64 | +| Alpine | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Amazon Linux 2 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Amazon Linux 2023 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Arch | 3.9 | x86-64 | +| Arch | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ @@ -33,7 +33,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 40 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Gentoo | 3.9 | x86-64 | +| Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 13 Ventura | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ From b94c5c5e3474f915e817d77cd984f62f8fd4399b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Sep 2024 19:32:21 +1000 Subject: [PATCH 274/652] Updated macOS tested Pillow versions --- docs/installation/platform-support.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index f2ef9cacb..28f749867 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -75,6 +75,8 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+============================+==================+==============+ +| macOS 15 Sequoia | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | ++----------------------------------+----------------------------+------------------+--------------+ | macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | +----------------------------------+----------------------------+------------------+--------------+ | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | From cc0b6b9de94e7c632efb0a10cf985dd7aab74e2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Sep 2024 07:56:45 +1000 Subject: [PATCH 275/652] Cast int before potentially exceeding INT_MAX --- src/libImaging/SgiRleDecode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index a8db11740..a4ee2e10d 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -183,7 +183,7 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t each with 4 bytes per element of tablen Check here before we allocate any memory */ - if (c->bufsize < 8 * c->tablen) { + if (c->bufsize < 8 * (int64_t)c->tablen) { state->errcode = IMAGING_CODEC_OVERRUN; return -1; } From fc65e437cfc6ccc381792ca1f35c6afb44c66ec3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Sep 2024 23:25:42 +1000 Subject: [PATCH 276/652] Prevent division by zero --- src/libImaging/FliDecode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index 6b2518d35..130ecb7f7 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -224,7 +224,7 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt break; case 16: /* COPY chunk */ - if (INT32_MAX / state->xsize < state->ysize) { + if (INT32_MAX < (uint64_t)state->xsize * state->ysize) { /* Integer overflow, bail */ state->errcode = IMAGING_CODEC_OVERRUN; return -1; From 851449edfc1746a3ad63260db825ab3a2b23d331 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Sep 2024 19:14:26 +1000 Subject: [PATCH 277/652] Free memory on early return --- src/libImaging/SgiRleDecode.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index a8db11740..c285637dd 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -195,6 +195,7 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t } _imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET); if (_imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { + free(ptr); state->errcode = IMAGING_CODEC_UNKNOWN; return -1; } From a99361a38f4c0830205ccbe5d58f1f0ba6c7312c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Sep 2024 23:21:35 +1000 Subject: [PATCH 278/652] Raise an error if path is compacted during mapping --- Tests/test_imagepath.py | 11 +++++++++++ src/path.c | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index cda2584e7..76bdf1e5f 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -204,6 +204,17 @@ def test_overflow_segfault() -> None: x[i] = b"0" * 16 +def test_compact_within_map() -> None: + p = ImagePath.Path([0, 1]) + + def map_func(x: float, y: float) -> tuple[float, float]: + p.compact() + return 0, 0 + + with pytest.raises(ValueError): + p.map(map_func) + + class Evil: def __init__(self) -> None: self.corrupt = Image.core.path(0x4000000000000000) diff --git a/src/path.c b/src/path.c index f4f4287f3..067f42f62 100644 --- a/src/path.c +++ b/src/path.c @@ -44,6 +44,7 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view); typedef struct { PyObject_HEAD Py_ssize_t count; double *xy; + int mapping; } PyPathObject; static PyTypeObject PyPathType; @@ -91,6 +92,7 @@ path_new(Py_ssize_t count, double *xy, int duplicate) { path->count = count; path->xy = xy; + path->mapping = 0; return path; } @@ -276,6 +278,10 @@ path_compact(PyPathObject *self, PyObject *args) { double cityblock = 2.0; + if (self->mapping) { + PyErr_SetString(PyExc_ValueError, "Path compacted during mapping"); + return NULL; + } if (!PyArg_ParseTuple(args, "|d:compact", &cityblock)) { return NULL; } @@ -393,11 +399,13 @@ path_map(PyPathObject *self, PyObject *args) { xy = self->xy; /* apply function to coordinate set */ + self->mapping = 1; for (i = 0; i < self->count; i++) { double x = xy[i + i]; double y = xy[i + i + 1]; PyObject *item = PyObject_CallFunction(function, "dd", x, y); if (!item || !PyArg_ParseTuple(item, "dd", &x, &y)) { + self->mapping = 0; Py_XDECREF(item); return NULL; } @@ -405,6 +413,7 @@ path_map(PyPathObject *self, PyObject *args) { xy[i + i + 1] = y; Py_DECREF(item); } + self->mapping = 0; Py_INCREF(Py_None); return Py_None; From c6b08ef32c39dc6517e9140c92dde7e10999da5e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Sep 2024 22:13:24 +1000 Subject: [PATCH 279/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b4644c541..08c2f26bc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Check image value before use #8400 + [radarhere] + - Improved copying imagequant libraries #8420 [radarhere] From 6fe4375f28015f0c1a7c3cb6fbc9ea3c4f405679 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 3 Aug 2024 11:13:38 -0500 Subject: [PATCH 280/652] move eps test images to their own folder Co-authored-by: Andrew Murray --- Tests/images/{ => eps}/1.eps | Bin Tests/images/{ => eps}/binary_preview_map.eps | Bin Tests/images/{ => eps}/create_eps.gnuplot | 0 Tests/images/{ => eps}/illu10_no_preview.eps | Bin Tests/images/{ => eps}/illu10_preview.eps | Bin Tests/images/{ => eps}/illuCS6_no_preview.eps | Bin Tests/images/{ => eps}/illuCS6_preview.eps | Bin Tests/images/{ => eps}/non_zero_bb.eps | Bin Tests/images/{ => eps}/non_zero_bb.png | Bin Tests/images/{ => eps}/non_zero_bb_scale2.png | Bin Tests/images/{ => eps}/pil_sample_cmyk.eps | Bin Tests/images/{ => eps}/reqd_showpage.eps | Bin Tests/images/{ => eps}/reqd_showpage.png | Bin .../{ => eps}/reqd_showpage_transparency.png | Bin ...75703545fee17acab56e5fec644c19979175de.eps | Bin Tests/images/{ => eps}/zero_bb.eps | Bin Tests/images/{ => eps}/zero_bb.png | Bin Tests/images/{ => eps}/zero_bb_emptyline.eps | Bin .../zero_bb_eof_before_boundingbox.eps | Bin Tests/images/{ => eps}/zero_bb_scale2.png | Bin Tests/images/{ => eps}/zero_bb_trailer.eps | Bin Tests/test_file_eps.py | 48 +++++++++--------- Tests/test_pickle.py | 8 +-- 23 files changed, 29 insertions(+), 27 deletions(-) rename Tests/images/{ => eps}/1.eps (100%) rename Tests/images/{ => eps}/binary_preview_map.eps (100%) rename Tests/images/{ => eps}/create_eps.gnuplot (100%) rename Tests/images/{ => eps}/illu10_no_preview.eps (100%) rename Tests/images/{ => eps}/illu10_preview.eps (100%) rename Tests/images/{ => eps}/illuCS6_no_preview.eps (100%) rename Tests/images/{ => eps}/illuCS6_preview.eps (100%) rename Tests/images/{ => eps}/non_zero_bb.eps (100%) rename Tests/images/{ => eps}/non_zero_bb.png (100%) rename Tests/images/{ => eps}/non_zero_bb_scale2.png (100%) rename Tests/images/{ => eps}/pil_sample_cmyk.eps (100%) rename Tests/images/{ => eps}/reqd_showpage.eps (100%) rename Tests/images/{ => eps}/reqd_showpage.png (100%) rename Tests/images/{ => eps}/reqd_showpage_transparency.png (100%) rename Tests/images/{ => eps}/timeout-d675703545fee17acab56e5fec644c19979175de.eps (100%) rename Tests/images/{ => eps}/zero_bb.eps (100%) rename Tests/images/{ => eps}/zero_bb.png (100%) rename Tests/images/{ => eps}/zero_bb_emptyline.eps (100%) rename Tests/images/{ => eps}/zero_bb_eof_before_boundingbox.eps (100%) rename Tests/images/{ => eps}/zero_bb_scale2.png (100%) rename Tests/images/{ => eps}/zero_bb_trailer.eps (100%) diff --git a/Tests/images/1.eps b/Tests/images/eps/1.eps similarity index 100% rename from Tests/images/1.eps rename to Tests/images/eps/1.eps diff --git a/Tests/images/binary_preview_map.eps b/Tests/images/eps/binary_preview_map.eps similarity index 100% rename from Tests/images/binary_preview_map.eps rename to Tests/images/eps/binary_preview_map.eps diff --git a/Tests/images/create_eps.gnuplot b/Tests/images/eps/create_eps.gnuplot similarity index 100% rename from Tests/images/create_eps.gnuplot rename to Tests/images/eps/create_eps.gnuplot diff --git a/Tests/images/illu10_no_preview.eps b/Tests/images/eps/illu10_no_preview.eps similarity index 100% rename from Tests/images/illu10_no_preview.eps rename to Tests/images/eps/illu10_no_preview.eps diff --git a/Tests/images/illu10_preview.eps b/Tests/images/eps/illu10_preview.eps similarity index 100% rename from Tests/images/illu10_preview.eps rename to Tests/images/eps/illu10_preview.eps diff --git a/Tests/images/illuCS6_no_preview.eps b/Tests/images/eps/illuCS6_no_preview.eps similarity index 100% rename from Tests/images/illuCS6_no_preview.eps rename to Tests/images/eps/illuCS6_no_preview.eps diff --git a/Tests/images/illuCS6_preview.eps b/Tests/images/eps/illuCS6_preview.eps similarity index 100% rename from Tests/images/illuCS6_preview.eps rename to Tests/images/eps/illuCS6_preview.eps diff --git a/Tests/images/non_zero_bb.eps b/Tests/images/eps/non_zero_bb.eps similarity index 100% rename from Tests/images/non_zero_bb.eps rename to Tests/images/eps/non_zero_bb.eps diff --git a/Tests/images/non_zero_bb.png b/Tests/images/eps/non_zero_bb.png similarity index 100% rename from Tests/images/non_zero_bb.png rename to Tests/images/eps/non_zero_bb.png diff --git a/Tests/images/non_zero_bb_scale2.png b/Tests/images/eps/non_zero_bb_scale2.png similarity index 100% rename from Tests/images/non_zero_bb_scale2.png rename to Tests/images/eps/non_zero_bb_scale2.png diff --git a/Tests/images/pil_sample_cmyk.eps b/Tests/images/eps/pil_sample_cmyk.eps similarity index 100% rename from Tests/images/pil_sample_cmyk.eps rename to Tests/images/eps/pil_sample_cmyk.eps diff --git a/Tests/images/reqd_showpage.eps b/Tests/images/eps/reqd_showpage.eps similarity index 100% rename from Tests/images/reqd_showpage.eps rename to Tests/images/eps/reqd_showpage.eps diff --git a/Tests/images/reqd_showpage.png b/Tests/images/eps/reqd_showpage.png similarity index 100% rename from Tests/images/reqd_showpage.png rename to Tests/images/eps/reqd_showpage.png diff --git a/Tests/images/reqd_showpage_transparency.png b/Tests/images/eps/reqd_showpage_transparency.png similarity index 100% rename from Tests/images/reqd_showpage_transparency.png rename to Tests/images/eps/reqd_showpage_transparency.png diff --git a/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps b/Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps similarity index 100% rename from Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps rename to Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps diff --git a/Tests/images/zero_bb.eps b/Tests/images/eps/zero_bb.eps similarity index 100% rename from Tests/images/zero_bb.eps rename to Tests/images/eps/zero_bb.eps diff --git a/Tests/images/zero_bb.png b/Tests/images/eps/zero_bb.png similarity index 100% rename from Tests/images/zero_bb.png rename to Tests/images/eps/zero_bb.png diff --git a/Tests/images/zero_bb_emptyline.eps b/Tests/images/eps/zero_bb_emptyline.eps similarity index 100% rename from Tests/images/zero_bb_emptyline.eps rename to Tests/images/eps/zero_bb_emptyline.eps diff --git a/Tests/images/zero_bb_eof_before_boundingbox.eps b/Tests/images/eps/zero_bb_eof_before_boundingbox.eps similarity index 100% rename from Tests/images/zero_bb_eof_before_boundingbox.eps rename to Tests/images/eps/zero_bb_eof_before_boundingbox.eps diff --git a/Tests/images/zero_bb_scale2.png b/Tests/images/eps/zero_bb_scale2.png similarity index 100% rename from Tests/images/zero_bb_scale2.png rename to Tests/images/eps/zero_bb_scale2.png diff --git a/Tests/images/zero_bb_trailer.eps b/Tests/images/eps/zero_bb_trailer.eps similarity index 100% rename from Tests/images/zero_bb_trailer.eps rename to Tests/images/eps/zero_bb_trailer.eps diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index d54deb515..89471fb5a 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -19,18 +19,18 @@ from .helper import ( HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() # Our two EPS test files (they are identical except for their bounding boxes) -FILE1 = "Tests/images/zero_bb.eps" -FILE2 = "Tests/images/non_zero_bb.eps" +FILE1 = "Tests/images/eps/zero_bb.eps" +FILE2 = "Tests/images/eps/non_zero_bb.eps" # Due to palletization, we'll need to convert these to RGB after load -FILE1_COMPARE = "Tests/images/zero_bb.png" -FILE1_COMPARE_SCALE2 = "Tests/images/zero_bb_scale2.png" +FILE1_COMPARE = "Tests/images/eps/zero_bb.png" +FILE1_COMPARE_SCALE2 = "Tests/images/eps/zero_bb_scale2.png" -FILE2_COMPARE = "Tests/images/non_zero_bb.png" -FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png" +FILE2_COMPARE = "Tests/images/eps/non_zero_bb.png" +FILE2_COMPARE_SCALE2 = "Tests/images/eps/non_zero_bb_scale2.png" # EPS test files with binary preview -FILE3 = "Tests/images/binary_preview_map.eps" +FILE3 = "Tests/images/eps/binary_preview_map.eps" # Three unsigned 32bit little-endian values: # 0xC6D3D0C5 magic number @@ -187,7 +187,7 @@ def test_load_long_binary_data(prefix: bytes) -> None: ) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_cmyk() -> None: - with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: + with Image.open("Tests/images/eps/pil_sample_cmyk.eps") as cmyk_image: assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) assert cmyk_image.format == "EPS" @@ -204,8 +204,8 @@ def test_cmyk() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_showpage() -> None: # See https://github.com/python-pillow/Pillow/issues/2615 - with Image.open("Tests/images/reqd_showpage.eps") as plot_image: - with Image.open("Tests/images/reqd_showpage.png") as target: + with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image: + with Image.open("Tests/images/eps/reqd_showpage.png") as target: # should not crash/hang plot_image.load() # fonts could be slightly different @@ -214,11 +214,11 @@ def test_showpage() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_transparency() -> None: - with Image.open("Tests/images/reqd_showpage.eps") as plot_image: + with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image: plot_image.load(transparency=True) assert plot_image.mode == "RGBA" - with Image.open("Tests/images/reqd_showpage_transparency.png") as target: + with Image.open("Tests/images/eps/reqd_showpage_transparency.png") as target: # fonts could be slightly different assert_image_similar(plot_image, target, 6) @@ -246,7 +246,7 @@ def test_bytesio_object() -> None: def test_1_mode() -> None: - with Image.open("Tests/images/1.eps") as im: + with Image.open("Tests/images/eps/1.eps") as im: assert im.mode == "1" @@ -302,7 +302,9 @@ def test_render_scale2() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) +@pytest.mark.parametrize( + "filename", (FILE1, FILE2, "Tests/images/eps/illu10_preview.eps") +) def test_resize(filename: str) -> None: with Image.open(filename) as im: new_size = (100, 100) @@ -344,10 +346,10 @@ def test_readline(prefix: bytes, line_ending: bytes) -> None: @pytest.mark.parametrize( "filename", ( - "Tests/images/illu10_no_preview.eps", - "Tests/images/illu10_preview.eps", - "Tests/images/illuCS6_no_preview.eps", - "Tests/images/illuCS6_preview.eps", + "Tests/images/eps/illu10_no_preview.eps", + "Tests/images/eps/illu10_preview.eps", + "Tests/images/eps/illuCS6_no_preview.eps", + "Tests/images/eps/illuCS6_preview.eps", ), ) def test_open_eps(filename: str) -> None: @@ -359,7 +361,7 @@ def test_open_eps(filename: str) -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_emptyline() -> None: # Test file includes an empty line in the header data - emptyline_file = "Tests/images/zero_bb_emptyline.eps" + emptyline_file = "Tests/images/eps/zero_bb_emptyline.eps" with Image.open(emptyline_file) as image: image.load() @@ -371,7 +373,7 @@ def test_emptyline() -> None: @pytest.mark.timeout(timeout=5) @pytest.mark.parametrize( "test_file", - ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], + ["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ) def test_timeout(test_file: str) -> None: with open(test_file, "rb") as f: @@ -384,7 +386,7 @@ def test_bounding_box_in_trailer() -> None: # Check bounding boxes are parsed in the same way # when specified in the header and the trailer with ( - Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, + Image.open("Tests/images/eps/zero_bb_trailer.eps") as trailer_image, Image.open(FILE1) as header_image, ): assert trailer_image.size == header_image.size @@ -392,12 +394,12 @@ def test_bounding_box_in_trailer() -> None: def test_eof_before_bounding_box() -> None: with pytest.raises(OSError): - with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): + with Image.open("Tests/images/eps/zero_bb_eof_before_boundingbox.eps"): pass def test_invalid_data_after_eof() -> None: - with open("Tests/images/illuCS6_preview.eps", "rb") as f: + with open("Tests/images/eps/illuCS6_preview.eps", "rb") as f: img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255)) with Image.open(img_bytes) as img: diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index be143e9c6..d250ba369 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -56,10 +56,10 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non ), ("Tests/images/hopper.tif", None), ("Tests/images/test-card.png", None), - ("Tests/images/zero_bb.png", None), - ("Tests/images/zero_bb_scale2.png", None), - ("Tests/images/non_zero_bb.png", None), - ("Tests/images/non_zero_bb_scale2.png", None), + ("Tests/images/eps/zero_bb.png", None), + ("Tests/images/eps/zero_bb_scale2.png", None), + ("Tests/images/eps/non_zero_bb.png", None), + ("Tests/images/eps/non_zero_bb_scale2.png", None), ("Tests/images/p_trns_single.png", None), ("Tests/images/pil123p.png", None), ("Tests/images/itxt_chunks.png", None), From 283b41afa033c64ce5baf9643356599240ff6367 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 3 Aug 2024 11:47:34 -0500 Subject: [PATCH 281/652] test 1.eps size and data --- Tests/images/eps/1.bmp | Bin 0 -> 1202 bytes Tests/test_file_eps.py | 6 ++++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 Tests/images/eps/1.bmp diff --git a/Tests/images/eps/1.bmp b/Tests/images/eps/1.bmp new file mode 100644 index 0000000000000000000000000000000000000000..194c85784c51ac3e30ea62381102eeca429fe63e GIT binary patch literal 1202 zcmZ|PZERCj7zgnGIlJA?O3~Y{V_O;Vu5@!VzEn35gb%0fWo&GkZt!8mC3a&%L`cAw z1RcgpH?TFE>7*Jn&33my7z%Mgqr{h_4ieBQ8H?zQi7CbpMh&1W;e#yoIc59k={>jS z_dL(ZxjpBm;m6Nt$hSD%_fYSl9-+?Yqgrq=uf}|_1G%M3g>&J}?a!;hVv|b)`u@Lh zz9g5+AzgDm72)2kpOgz`P;_7dVXhsT9<6leYq;=)k=*&2tGlEmND;rgC2p1%Ze9D* zRH-TmYa(_)9A05`1VXA5z^^6B(}_LI(p%m zVoCNl6^sjC`zujrc9qK8kPfYR;$)yQtB4Fu=`8rwbK`hwmn1tehLNB%J+=N#wH+w7 zMTy?A{~9Z<>etENz$n8bT6*l(I>RP2Lqy82W$O9#Ss3IM7CX&fJ$i{HSBHr3OIWe% zK41G;bD6RWiDZq-SND41zGKQCjk&uj%zrM*i9~}kEWNAh#d|LItgL-sQB={bbM5ua zTW^S{vd+ZkBJxFlKtv@OhHhMCqq!a2UOLJCHul72(i!K6KPQHs%PIr9Bf>{-ub1y;2om(3L|qHsOC@$nWg&jA+QaVd?4R8m z%^Tb4AfE31VECZQOK5V9zIs>oPxRVkhPr_r=6!$s_)~~alEHY&m(Wk$FhhO8yftC| z((=TW-%c`18C-taQuJ?8Vrax73A#K7!w&^MKKGy?1Of`JKlZN2vRbA$`Tu<0-`3Evk+i91Sz` zEkRiIMCf)wlbvC$63naCqw{gKkF9Z9IA+;oX&_W~YFRbqYKe$LIDGFF_{q9*Y@i!; z&fzXX-jYGzkKu%bNaq6(JORy1rFlB&kKw7{Dvj(=^Mlmm=$Fz$g>n|bBNpv4H9bEe z8L|Z4F-2Ij3<_rG5d}w;VX3nK+73sxIiP{!w3R@Y1-%1(l~Jmb None: assert_image_similar(img, image1_scale1_compare, 5) -def test_1_mode() -> None: +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_1() -> None: with Image.open("Tests/images/eps/1.eps") as im: - assert im.mode == "1" + assert_image_equal_tofile(im, "Tests/images/eps/1.bmp") def test_image_mode_not_supported(tmp_path: Path) -> None: From 3ccecd91cea6625612ef7e05d485058a7783bf07 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 3 Aug 2024 18:04:05 -0500 Subject: [PATCH 282/652] convert eps using pnmraw instead of ppmraw This lets Ghostscript choose the best device to use (pbmraw, pgmraw, ppmraw) based on the image data. --- Tests/test_file_eps.py | 2 +- src/PIL/EpsImagePlugin.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index f0f235f81..bca516a78 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -178,7 +178,7 @@ def test_load_long_binary_data(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) with Image.open(data) as img: img.load() - assert img.mode == "RGB" + assert img.mode == "1" assert img.size == (100, 100) assert img.format == "EPS" diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index dd6ae4a77..f6c1ea2a5 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -121,7 +121,13 @@ def Ghostscript( lengthfile -= len(s) f.write(s) - device = "pngalpha" if transparency else "ppmraw" + if transparency: + # "RGBA" + device = "pngalpha" + else: + # "pnmraw" automatically chooses between + # PBM ("1"), PGM ("L"), and PPM ("RGB"). + device = "pnmraw" # Build Ghostscript command command = [ From 00bbd4a5b99447aad9def172a05ab768fe428dd6 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 4 Aug 2024 00:48:12 -0500 Subject: [PATCH 283/652] use "with Image" instead of closing manually --- src/PIL/EpsImagePlugin.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index f6c1ea2a5..cbf48de18 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -157,8 +157,9 @@ def Ghostscript( startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW subprocess.check_call(command, startupinfo=startupinfo) - out_im = Image.open(outfile) - out_im.load() + with Image.open(outfile) as out_im: + out_im.load() + return out_im.im.copy() finally: try: os.unlink(outfile) @@ -167,10 +168,6 @@ def Ghostscript( except OSError: pass - im = out_im.im.copy() - out_im.close() - return im - def _accept(prefix: bytes) -> bool: return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) From 6b168a3e2bafe1c957de8e9705ec9a17dd62fd84 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 6 Aug 2024 21:02:46 -0500 Subject: [PATCH 284/652] add EPS test for image with ImageData and BoundingBox (atend) --- Tests/images/eps/1_atend.eps | Bin 0 -> 45867 bytes Tests/test_file_eps.py | 40 +++++++++++++-------- src/PIL/EpsImagePlugin.py | 65 ++++++++++++++++++++++------------- 3 files changed, 66 insertions(+), 39 deletions(-) create mode 100644 Tests/images/eps/1_atend.eps diff --git a/Tests/images/eps/1_atend.eps b/Tests/images/eps/1_atend.eps new file mode 100644 index 0000000000000000000000000000000000000000..08f8c4681a90908a2ca3d7695d19f150a5787816 GIT binary patch literal 45867 zcmeHw349b)x^Dvl8d=;$MePK`o^;pN)fGrWS64S{7Dz(E8ahd5F`e$v-GKlD+&lMP zy^hXs@j5!V3@)Q6q6mm6jBs@nHXX<1yleZMn=F>q_gLZ=9xW=qVzKe#hg&Yj(-oHUmWwRi%o^Q# zu=F@QcQev6VlfhX@YsJQU5sry}nyd*6#u2 z-(aMVAwATn-yLbLTX#z`?oIgcNMD8f0JI%{TQ`gI28(4R(lxj1I9x*m#r4nUk}Q`E zX}!BDLS=qyesw4sid2W{t-O%VTZiWO%4>p@GYpXJJfC5AW^jTP-=dPiJ1tIie9lD| zF@PllyG^(BxFL0Hr|&=={|9!@w_G$KZ(Q!c?y}`#-p)DgBF_!%KHAcY<9S|ESRbjZ zkIM!=RMF$XJAU*u1o8~Ks>J@THg@GV^7M^Z@ZVSo-dVYSmJfYjm%EO`h;%Yx8 z7d%-LtTM^hj|!#$Cp8p}hUx;LU=?+XtB+YiC?w?fHS?#`R8=PyTxu2ow|IS$jvhxD zz{@NZxOaoV8FwZ*gIQTW_RYSs|H-2BTVM7_l6@1ad0pG*9^bR)OV~HQpl8t65aLa6 zx*--|Sk{)5X`FAnk3lv*MnAeXVj6_iy`D;Xc<|MWQpQEIL3m$m@mTUL1(s4vjwR2M zi;pRSMn&UhupfJ(c5F5it0Y!1^IHYW5^M;N#FK9ARTgRpRz%wIb$OIS8AF?xAx?2* zwwp1F+B&lgjVY}YN2v9R1Zv9tk*R@6#CqM@^wX1-A^Qq^db5Y|T0&pFtHPm%`Zig; zLgAXKnz&Z+6cF@0mIoMpeGSpjSbxwT_C@{FyQOeJy{QJ1^vaY&CF6Bv_>UfckA`rd zjWT7#Z<~`7scNfmd-(#eDNh0_qpVfK9^sbbm`?+_wUoE|J4Joyz=TRuN-iBA9fix{h|GLx4xHN+P7cd z%lh@Z?5ckK`dvj|{jSoRT;3S~vH!AMeld_A*P~mCrTgXGdR*Qu_8DZgXY93Z*}$@A zH+BJGy5QAyzo=)g-WOkzbZNKth0vEh%)-ko-7f0Zqx(hOd-m>ianBxo1QcH07F zUVSX;xb}mhFa3Yt@UMT`)$rHvy+iK#!@oWM+t)t)+lj|FzP|gblT$0_-FEL2FKqwl zK$ASSxN83G_pRFW#-4+xF1K{=4s0)CIQ8z;Q(`D2b5~ph4Cba>*^~e0#l2pva%_N=-UjSUTh z|8_F4^pnpD_|<=pS;l{`{`^~Kg>J(RY_7_?Zo$dumZN9o-1S@5Zs50-6xVVyR#Y9$ z^L~_h=H{w$e#Gk4->rLjYEe_(Zocl#H>WSCTH|}}%Z6*V2qTgnIO;s}o2Q2^Uj6Cq zv+i5|FNZdyohdneA{w)lW-s1d_WsIWuguHebmG*B&mWurc(gX-z%|}FW%tQGM}Bep zgLiFxxMch0`@ea$rS9I714q6SUS|(KFl)u^7mB79e^7fi`{TK5zL|Y@TFa;#daRj# zB=XHQ{xAGRziTS3oZEL3e{RQp4Ff+}mr-fUfBc@(rgMSj?5e3dW-Qqq+Si=xzhl@} z3!h#wJ2y9LV!*LyT2N`MuGkv0yihzZb-=t0SN6W*+{BgshCd&B{PyOmeZOuVy*kX* za4#NjSXMi5`05pr8za~M{qG;`+Pc1Z-{B2M=PlgtU%ht5k*zz6Pt2=K`+V)m^3~g( zsw_M_^07hvu5TV`Ym_$aI&^x-Wn(d2k?|pLOb$1khJmRf)H=MX{&x+r? z{q1LW&d#~#{_h9w`P)*i+TXwC@n_E#Y+7bVf$6oGp|GI(uC+{16HnnNt{xwVPzN>UY>ZrYo$F4d#d-;-_ zji24QVesmkO1A%P-Nup|zP^9PlGy?m3A}Nr$q}>cJw4#`y#>3Tv(Me;FUx9vc1vE# zmM8z*T)J%K@?~d*-G{F`wKrJwk@UolMd7WlEGZf=Ei-%7fv;vY-8|8MdfHV>c5N7P z;=13x;6A*ybn=Zujuba%m-T(~t=AvjHLEU?*=J4e;}0#*k>3BX`L@i)Q`3HZxcp=< zf6@B=&wY6^Z|_S3e%D-gxUt_muk3i{qs*4EyWiUI`Aj?kPF<$J?iVa%|)8Hp)%Gd5h+L`^>vVxLn z`d#DAd4Biu(95YxX6W&XIa{9ja`}J$^R9)zw{L#%nzFKjUDFoSMZerLKmX0Q-hJh@ zH=la9CA|Io&JBYGubj~@W+`ZDDE|AbInn;pSAS=FW7Var0~-W>b?p}afltowoqf}Y z&x)rUnl<8*WgAZi&Zawp^G__w+H=R(vzF~Xnf=U}GY^%^1HK3zy6#@NY4PdQC*B#k zdSCt0Y9V#gq-s3^Jp!u6$-}hVZ+0x~8 zYqsyWYHIk~&r2&Edk%~m(K};*>7b_4@k<8h7A-j4cp|Xj(LJ}%_CJ-gdUM0zcNSLV zZb?7(*hh1PXV%~O?2}(Ucpvw!(sIkm-lK*LUemPhbmQL7A3Ehf@ta2$@839lTaV1t z_r5AWnf-LjpeGJ~e4u%r-0$3^MLCae+CAs6>@R8+U9E~Z$G*9 zmE|iR@=h3^7kmG}++H`8dJo_Hw{yRkG;rZ}$9HbMeOg_^4BzH8DWB~B@YtK%(q25% z|7`lcVcS<_eDmDq_xi6oKl#7jzBc>mlYzSyUAL^R_-~6Jedq9JC2McE{Jzbfd>h!j zW$o*SPX6WCiSN$8vH0_Q2jAH6<<_^Cytiuia~tYvpFOf+}=byp4F zoN@aj?r+!B?)_j!)68Ei`)J|pvHOnhc>QeNX9!B{&mN>bsv25=7uNtMP6_GFso_f>5X3=+c4y&Uu}IqXX_j5?|Sy>`CqSYkv=TG z>z<euu{nXkumHvjE zPoF+}{`KZUFy}tYQrd7o!7u;2INZIS~{dMoWf}3vi7A<>ymF?!o zD}@_Iq@Dgu-4S@zjyL0o)d#1nk+MBPw@z@CY{lhJ8+u%>W{p?pa z|HrG%0}g)G@Zi$*({e-khZmH5{?OH*?Fzj1`r6?S2DWZ_Ag^TmN$!K?(X8cntoV>W ze(1NG@A~GU70W8qy_wR=f_bT*FPm^^!F4Z2KR9=5@ZD9y6`w!;;ZtXOWk3Gm&=VOC z*pE!RXYp@WEI6_M*YgVxM>l@?%OkTN4pvSbyL!hj0=L$GH+S>ef|i%=SR|}%NzIgd zyubE}Kklnt_2;#>)y*rP*06lVo=ivi$)|6=_nViKo_>AnVQ}1g@4Rt%-pPUUcF$UT z^Uk6}4+Z*Eep%)`aIod^Gu`$$j*Oi6Z0-H$K7XwI`)|M7 zy00q#v*q6Gr*}11j(z6v7cJWc?RtH}+?`WiJDZ;N;$8Eyi(a4q$+zpDdEtQvZ@BAh z=Jo4C8&8})|5k3M=z~v?y|-lBoIk}ZwpoX63I2BX+6}8JXMFQ%c4fhx)pai^>5HCz z;_Ydkt9RdZZuHU(?|-;IZ*R;}^2U}Y$_^fwHumVi&EFk+ef+T4UVXik%$5tq`0hpv zr2ZBcLJ}cMnZ<9hA}CRfbQGTmOVy*sRXa;;qi}_Tx<=Nf~2MAdu9Y0OEe?O zTaT!o8FhZRKbCUH?}57K@i8OK6s_ow=r09ME&_^|Ax(jUB&0ivI#YWyx;zkd%;@qh zNOu%nrYN!*UCu#z6yWy+3?I^2h)h%el}KkJO*qU$`tt5Q8J0!X!L&} zKlR`B$n;DAtH*vY*Enn z*C4`p<30v>QA{M+xLyDiu2bR+uS ztq0$>BftCIIu7qqeIx%PqAV(-76t&|3mk95RyZh!kN7=Nd6B!89lE;T15Wk$QO~8kiHaY zFA?H#9iJ(b#)Y9<{@hy+!X+V1TND{~F@xu<9G`(b5lQJV4;u!0>xT&fef00OxYOL~ za!pnb4Rjm>cp*>e^V!JM>}x@#$xkZ&irag3le8z z-#@kcXzcLW&tp>;#h!@84g==0w-+yMSrlt(IXnC4{{7jp*wQtx4L;klDi+%t`QjG` zv-4u-XYTu8wRHZ~SZwx?2QPi&{5!E@r<#ZVq3q!IvE#NoV--t2Z;GAY?`V4N`|K-Y zvDX*G*7uN-VzJoyGd<@0Vsk8Z{M7HqD8GL{w(|J;<~hA%N-XyFOXvD%OP`J%|J9z@ z2T3oTi+y%_ckIt8FF&(1cJS_(#zgl5)#glTz=^uhd$FU>yxJ?aZ*45L>yLl8Z~o2} zi*4ETY5!+3Z$7@XsrNBY^Qpzhj>pbNttZxfdd2b2?yF4y{?yvzEgM4%5B)xO?Z()) z1=qhfdEcJqmd{q$_U#!Gt2%q=+{;CO8M5Wz+4FZb7w&yIwlubX44;?%eD?R&K-j>Bp zv+rGd@Z}Y=PMuMlJFjH5?1EiP52p*fWEGXNO1rAXla z3L5I`!~RG_3q{BJYJ%=iUA;dT@zI7O6>FjLhB|*Rimgj*WJFkiJCb2l1HSTFl)JRN zQgt;MjUqEWzRp+W zr%k;KE7XP6AzPuDF$N4?wx*O3$Qr7Wf_8cb<}P=J)uA|?$hHbjkrx$Fvy1o_za%H; zwsVpT83L#Bk|_VsGr{<&IzLj*QeEL3J9Fw#J7kv z$4cBNaUhpxW8sl62}sLI1M^(1VJm~3>MbS$zWCCobZ=*IF5~U@(EXblHICzH_|3G7-hFxUs z9TNU|bQlh;eObb@=8ze|D9hX0KOifzQx+W(!5~;7FvGLip9vxXXNM&7qQ}8GG>0UK zq9i#b18;)SiebB+Xv{PQ2l}M(O0r$_q92z-FglSOPM*=!p*jSoi`OJoNuVhx-T@4w zZ7W@Phnvwuba-eyvJ|I_+ri^Wakw!)P!F^h*&hrAEjEmM>`4(bO!oi-Lnj!JDP=0=vXJ zWLXfk3rqB|pp6Hu+s`XOBtGn@1U-RPMhP1WNsn=X17ysiqXUSbxWg@8$U{tSb-wLr z1mHXYGcUK}iUg`c9vDqU89K=18uLyR5TVZP4$((b@zG0! z4`wT|KH>3W6ocl#=-ad*85R;pZjMFAm4mWJ4fG_*4kwL^NxiN+@$Hb~lrRV9aWL68 zTJbvVz?)uhJ8&l6CdqZgCDv%@j`zgc5FU^#P{skO{6HJ6Z5vRJ>;f>=TQGWD*b}s4 zTkLdNUf2ieErP-9wcv@{2i-2gvYAAJ{|~d+=DAhp@s74MwRa>`cERgp&|2Hs6Z*50 ze`Gbf^R(9C5;@lBpKC1_ym9G9Tk_5f7&;{O4GuQp@w5tRy~uX7$-pzChglyUjE*#G z6R)Z#pg-7U-tl0zV|a*MA{|WH#p~EbuGMx4qG{0`I#9!hF(267l&APS05@t!O(oDl z^5PjZ73q$?b>G1N5l6la$O|7XsMktUvtJXB3meCC+CgcB5w8(X#H+>=RK)DE8~%)b zf&1bxm_>p9_Bce?v$h^W8yJukXf?si!wke>*iG$np-ku<{;*uO!hk;SK;B!agZ|@^ z+NuwTig6eY($rLjc|2BeeCR+v+S+Qrg#v)59Q@sDUd$|MvAvO5*N- z-6!9H?7vL)2u7>--lktzLCg~*6tyK2=`Vp7*?ooFTSpLA8yM$?*3eT7(DjVS@jlkmrM5M7@`v>OsakVp zm~HJYO(Lt7A6{3u3bFBu*zUce{VT66BW+Ishr3o^n_xGcVjIk z2!bf{PF~~mwK^6xlEOoP$%`cz)+kWP3W6~z6o>}J zLNL7KU_s?zr4JLJ$Szrx-LfWoWUq{wB5GlehtUL4vN+X=Hj;q3GUkQmR^Y+f@L-^e zi6$~;9`Y&)ypvO)98H?IISEwo+67R@!FxD8hT`NEtoJ2I2&0AQa1eK+V}a0BCJtcytK zRDshR8o>au;kRmDRpofidE9P?>hi#PIYiCvbt`Vg1vBPR zG{LD745t@!>rxROvwL9%Q~_l!kIN-FWv5qB5SQWIn$sH>UG<7EIQJXBF| zY0!}HZB>T~U8%^D+k;0p}5_kyn|OA9@PU>VQB?S5jqr^yl^g$Q*{9qhbr@`$K}PSRYgFARg*wYj^@&3 zS6qnFdA%z9F$p$D#loD|08aPEG^grBJyAuz)8i6Q4kkg*fajJxD(7`;5(i#@Focre&0R>G&9vfr_U18oqJf88SqCwlySiBy$0veF5P&^O| zh_sgjPkG@9sj3Hp1ZniBysEmW8n1fc(aH`89K-{$6oTQ=pzxs8Aa)7|4s)O$kBLHW z(8b^^GV~773lw+<7|-pLTr%3a+zt-h$MBG$wlF8?!0uAeuK-EI{6Hnknv1wqmNcNn zIR#Kl;L#H?KbAxyjYnc}e!(kam=18h;&no_F0mU^#>a!sl>%!MIpskTy|eaFoNVsa_YUA*X_Qlw{@yxiCYL2j&Q3EyC~!U_l8y zMiK*a1g0k10e=}`I(SKJS}`;ZScs~^Xo^A!&W(qZAB0)fR44R_;C4Y}3Y!MHSkK1P^lxR~o&5=OxSxm_h@Az;hm_uGisAlTL8bFyLjlapN%r=p0xEgs;f& zaAOaRBOU-q8WprBq@LHn>`bq#G6#*KC@OTU=m0VvH*6&bGX#qTHgY+=P`waE=we<| zoEnrFEl-L4c#pz#A@ifWnjL-+6ey25^Em84)uVCnJRDB11hJc+`_()vT(v#z%)a=A!P6i zFc|Q@sDQB@WN6uv3U*9`R64w*pTJVku}rUH`k=WW*w9r_IWCU|CFp@wRG{bq$puSm zhnXUCh0$>ud;>3bbjdzLu=Kq(Xbq=Zg1!asfLgK!P68i8n-EuNzzkE3Qilsd6o^x5|vC#%vg< z4QB{e2BUYvg@DP`V;AsFp$kapnWF0BR9*#+1+=-$Fog># zqIc$x@d}t2?grTa!Vi8Lkb#EA>_KrUoFEifi<79rLU~C8NlrKti1P`UL=r|OFXUIaV25~cwCsS5;-R!* zWxcQ>*eh2R6+9xtgefAt1E_s8g?oTW0`IvPk73%t2oA_K=;gwKnY<}4bT8xq%LJ@R z;Tw|K_da4xv$4|y1psXbNl+kqaLfTj1l488GdwZ486XOjEkq8o zz>{`_NWckDyo^7vy9+Y}-~`!&F=DCbVLT32#*VF91t+TtTxi%orx%lral#q*(CDB9 zh#rCre-mAUkFh?2!$DdSTnB^iwX0sY3tE~u4Auvp17wgkuQ7!xmWFu4f#Csf7fvG- zsS3P#g~N*vm`>RR|I7~Vz)}=z6I2JoVz$7^B4!_JKR6)7;X1E#FkVhn#*D~3Rv%F9 z7#d_9E*Th@Xo{69j6Y{S}@y8pJF2)GXyq;_><2E#|n}M6$(y)WA5QR zuvu`c;e29s0QQ9_fK{-^3GVR-dYsN9lRaj}+Npqt;7Swk2xMfzM|g8X5U}h9IXqBH zm?NT~tickKK4AJ3_8JzSOfc*ebeRg52A&{I9n=yu131SeXdHx{@e1?Rz||0Z*mqf< zTMtl$e+|VeQ|BJ|A0Qs=6`_n7(8$ue-3S2U6%5#MFsK8X4)z<18lK^SEmyK6kRw`l z!0clwFG4B7%cT_^3A6z1Lfj7z%7theJP%k4gpO4iJ`eE+d|=Q9evkk`!V(Gt;~@vI zk(g;Wxjb%olaL~EIi}g$0Nfx1 z=;#6m!(L#`4F!oIlK6n%y#geN_ydT!c!~i+;57jYF%Sh_KRkE}7{hHwr~#oQ8S@RV zzzP3FMCbyAueCHUjV)+8Ua^0jxZmz_&^b8ir5U6*klah{&=t+g1d*6 zC4`*q6~NTOiG@Hy=&&21`7`|vY-bRx>&zJ z8^H*}R-i+NB4NSkVXJ=lSy<%2=)oL&n7nuu5BMH9!a}L=AfPX?FM-Go_`u@=g&w$1@nvaHLM)*kHd?lEKrY|3w$fpA0@!VIMiHvqgj+;1_B< z)_d@xVfjHUxFO6Bg2I9VA`OWp0+axGE-H91w-a?{H%UfkhGHQwm`c(>bA|6xbP9 zO{fq%ya`fGh-XT$=2-YbCu1>&Il+vpZ~#@H0>0B>Xs}S=N>BuI0^We+GYt+^s=<;& zx5GL?zk*>ED0B=JL&u8;P~GT*@dsiT2p*C-MlgVf5W{xDq~PsFa@DZbK#&cJ9kC!% z=ZFe{2gqR~-x{lag2BR2SVw@wa0kh5Lb7GzK*%7xZ)Rz{2s!BMJMb378CINp4B|F| z0e1}23@3*L2JeN;KAO>58}12o4*6^x{45;?{0oRTJV~f;&;c$CmVYAA8KECg z2LwSQM59!QoEriM1)<;uD+ce9U|>x{u}DO!h)AofTGtgmn*k$Nncyb!Jkqn9p4gxFyW&_>}d>@P!kw{``xF0Zc z2rR%6Bs}aep9oBlv1B?EyoUu6@B-DK#)zNcz;TGZ!ivEFK=IH50P6$zu^<=0ASV+m zGw6DF8Za8b>>s9*f(#%Rk5~m3op_fKp*wU5e4tqg2BJ@}MD%VITnG*` z07fNEhPW>lTM!s18`v!j0^6`yE7MAlLVGX*uw_ntEk?sbBp(I_Tm%;t0I__4yN`7N z#2C5=yS2~_Xa@-a9|0V8fQ>i_qba5dV|0*z3=Iqa91Mo|J2_wAEod7M735N(QNb(F zMk=BXEV9YNMRGBH3ZIGvfH;c!LRSgIx9A?)o^%L{5TbxQDq=TyKj3Lvg+m>{hBG^d zQ6f47i;uU0TKD*PwgH3KIxf7(pkK5Hq+h^TMB04s!hvV3%h-M@y)Z#rs1EEoQ9iwk z$L`oCisu<%Xt$Bz(7X_={~b8gA7c0n25m)Anm+6T?1V9yxj@kEfuE!Bw0mG|B@!-- zPifam-_v3sWV`X6D2+ZLA5AejT(ld*I%ZEgkD2@Aj<#)3SHHZ-r?mNI676&0!WYXY zJmPPjxQsHhA68SZ%-YcYTmoJ!jRFRP+R|hgZQy9>{m|}QLOx!%V)@uDZUc+a!@wDP zYYaD&Gk60QXwCA8esp2ymcfF}ijRf$g))c1L4pTm)V~26@p8Ql(S`D`o% zjml`OxLigV!mhM`7$0LCrh+&h;lv>8^uTVYK28an(FtXeTgMQLPCe*6L41J^ZS}D& zD9kD0(fW*yHN*`NzP2NV0cRgedqju@$Pr%C=ZQOjGLxOeXGT+iZr#tOVL`U>t|LC! zBcQhqFeL0yV%>!wTJTQnU67vuWvbU97MCP2E&)7$27sJDxcSB2EXNA- zLOfJ`_Dnp?*XPY=>Xe7fDA-hEGnFv6PLDnoTIeU1DGoiKrxy?s`NZHOUe@#3ycqCI z90hy3Hl{2PtOWis>oXX5?EpK~#02bO^8<&N2v4Ik9FL#fvb8}Mj8VE<~@X{KEq@Y%rCLpvCJ%%%)YdPY3_Mx%B&0so> zenCgC=TK#n2Tgu$|4en*h1wU;!{AwZMHTSuh>GF;0W24=&k6s7j49slVejJsIsG5Q zgLZnU1>P|t*qrIhLVV(v0h{4v*hAKz?s_^$svej2q*7?G<*7?=`u;03<9%p$*!wvq$)6y1<5#K`HIZmdAm9(z`@+7uNMgf|YoMt+6!vRqXa2;u${)pVoA683 zD!=u@o_xUyYcLeGE^3JQ!xc5>-D0aPic>$WHvQ3R%Tp(` zLa^l+wF{y+zqZPX*r%XaQI#GDaH`ec=mVYsf3PZAZ4FggP52mmp}(%)Y=|MZHe-Yd zM)1>N6TWp(-z0;Gl{EpsHK8f6tO(#(Q!C97K5Z+S0E~X9ve8ex`eJKY4G0T#`zGPn z$cf#y;R_J3)*rN!@%I+-cQw`@zX)>LGs6iYewoO-vvx@O$Mu)kno zZejVt+H$2LB|9r=bYn(iT|E)O3SJ9FG8!{ee008c2GW#mOR=&l(b~)u{fFTAWwe#} z#44wAY30NmvXWkAuXL9A75hl~JvV3LcpE3B@mvNkWpI+!xKgr`kQyDXsLYt6d5sS7 zl$lZ;jn-$_Z1d;OPoFQOhr(4h1dkM(owxCP8fv6P7T{;ujcLKiARUMaNApL@!!_(g zX)D3SX~_-I%#;-BD1I3utE-Q96pW;^QKpxN>THd^dK|@Iw{_I0qCDQF9>>2j)GEqt z{(ztT5FG8e4%_(}KuBX$Z94Ne*3iKZb{lGR*fP%g9k-7c{~*ek&zzi&8=E7otLw1q zNYpbA#Onxcvt@)1inZZq_@Rbyx!*GndS*~Zbc;l%_*>_-4Fx&|h3c(Si_+>9n(bh)qYnHA z49=Nm7s0!Q=8-9S?+JC zweZjikZQ2pwryGO7j`rnGJsyfHNHS@LtU9aoJAoa^B|ScBqD8#TRUP+C*f>|@xjVa zmSneauv7T5+i)a=4HHgnrt)|K!iDqGCh>c#nY)2bRc>mHaWBZ9-6LZj>fm2O{enI07yI@C! zK!Ny48MUI;nw`*!Dc}tAtPwB@!u{}}@yeY8ZzG5l&Bq>^1Fy^A%^Ylgfo2ypxd7bw zpgz3-hsx62Yw9XL_(EO5;0d5?Cnq61|4SG zMn4R^yxJG6@>gWp%yF3+?VEJ0@Bbf}GjRJqa?WVl6Z&+vT^E{5*bCQ@ z6#vV~$KOQ;UBcnS>@C zLDc_P+Rt31>ySHW+S-Fj-2@_J*vGN?qhKDbcBW z7r8n`CbLo(xlBrQ>fS}JPLav1)I~0n5}mqtk*iZ=GAnhF%cMl7?p@^S6q(FQUF0$; z(W!eExjIEAvr-qiOiFa>-bJoXk;$yoMJ|&Pow|3Ct5ak$D|L~}q(rCgUF7N%naoOE zIz=Y4QWv>QN_6VpMXpYf$*j~xE|U_Sx_6PQQ)DtLb&<=YM5pdu`LTEGK@l6An2-UshXwQDN)(NI%+}d5np1G*SDet%oxO{f$v} zFa-`{${pVdJw7=7#HuR#0S!)E^5hp-S-NkMXipdsPE{&pKT5Mk7DS@{I{g4CJ%`Sn zVh14EN+bSgRoJ(nO(iogp_(!93#umOCsYT7XxJBwRQkj1A;e+q8|WdwWmwgKy>ks)cB#k zs;HHIa+uL+jq$7QY-_xGeI^3_dHw+3hFTb3RfDezry-%pS{n?_4+4j_xjfDeYkzc@ z(G_bZ{A`HA4?;9qj&Y}T91hpQ^cuWp3y0=MtPa`eyF3(#SaD8@{wVWS(V5PR)=0T8 zfHTyLvEr03o93TaQ|=!N7VMh@u0gv{m>phbHD*=k+kErHGOZyL4F&z|WRLvZ!Hb@E2D%`DNRsA8Ro{CNOv4}R}%#F z=J^6vy?fTw3MA;Hw?yQOllySMo*{@P4VW(EaabJ_yhS!$H?q_87;P6VVy2;QQgKZM zPO}^5uc@kzTGg6pB;OyV(|o{(!KjrDIL8O(X(WCIR}`?*yYzGUbfB;mtXC5URAjJ? zE)5JI4=e+Jvrn%LX=f)9rsI@7Lr>%UC2L)1o*zFswp%$X9fcPSk+L(>NZ5^R;;;ao zjJAD~s0CB90UQy8G?UVbnt4bv@uj38qP8#{6=7Y(s^FMLSeAL#x`u#Bym9^jkidZw z=pBgcU;Iq`d0BQBB&6wCMjMWsiBtd4gqw*&jrDTmF_=IsKNN`;u%qm#AGGJF zj?id42v%P2pC_2SG!b9&JiRr;hbT*)hMf#Qx=1f0Kw#l_&Jnq^?{*jZE>l_1h%Rv z#?-Cn9&(AU55~h-m^na9tsqBdd7VG#q(qy+{W_bl9M(wZd+Q8VlX#O=fN3+5vkZN6 zfz+5C8M<%=SktVVAVP~w^=uByV829d57~m}m#X@!YJzG_&=+2iVdyaYeQ*3Ji&jDF zo0MHsWKSRB8J9e6(B!p*v&tAT2j}@YM0O$;r2l z$W2a8zGZlFafz#9!pO=guB2pJNpfJ&}gfb;-#!bH){<4T(h1VDcELuqw4KX=ve$hFkNTA*U_1 zGBAz1C0txS^_DR;Bl9O@Ps}Y&Nu4!(P_j05&a~vL99zi4>1mYN@~Ds^Vd<5H{m+6>3c`YAJ&N_8pzisa<6`JTb{+M2O}oQ4c1P)JEl8Zp5+d+fM5xrL>L!)De*Y76sx z?$r7czoMmu^3n&7(<;+aZB=#ULq>7oD4I-mIC6`FC4zg%&>6|e#p9ewLzC-9)mG)Z z8^pSrnn9&p>7q@e*OYnjn4F&Em^*pQ^k80Av3q#KIQOW*6I?kB;SskCy=COooV@bnVIzjl z6orvmupm@6!3_qRF)4XM<*2E{s;35~CEub|%}uHq6)MObQ&}@=L~2#>?D2#Alg8L* z6{O}V8UDH1+2|x?C|}|qJ1)1x77a{DpPVLoi)W>{hRvQkD>A)Qj%Fnd8<9CULn>D% z%uTHxJT{awWKQv<^elho#G={3X>xM<%n4(Msl#&SIBvZ)XUv3=4P{d%BslH@VI=bc#JY&pB$Sn#~9E%f?spBPIrhdj=IuDa-Q|d1@RZ%8Kee z$-Ld|!(S>JJWH79^A^mWC5}l+%9=bU(ty*v#OWCY^32I8MXB1{;_NvST*)a#PEU%> zF*s|~q?|DmYX;?J25%9{q7_Q&^eJKA&}lVA6GqLA&PvK0H+iHl+!`aESM` z%;a05O6?rUGc0!u{vuSym@@AyUqy0CzLY)3R+m{hBzKgzc+lkOQ None: EpsImagePlugin.EpsImageFile(data) +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_simple_eps_file(prefix: bytes) -> None: + data = io.BytesIO(prefix + b"\n".join(simple_eps_file)) + with Image.open(data) as img: + assert img.mode == "RGB" + assert img.size == (100, 100) + assert img.format == "EPS" + + @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) def test_missing_version_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) @@ -142,23 +151,19 @@ def test_missing_boundingbox_comment(prefix: bytes) -> None: @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment(prefix: bytes) -> None: - data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) +@pytest.mark.parametrize( + "file_lines", + ( + simple_eps_file_with_invalid_boundingbox, + simple_eps_file_with_invalid_boundingbox_valid_imagedata, + ), +) +def test_invalid_boundingbox_comment(prefix: bytes, file_lines: list[bytes]) -> None: + data = io.BytesIO(prefix + b"\n".join(file_lines)) with pytest.raises(OSError, match="cannot determine EPS bounding box"): EpsImagePlugin.EpsImageFile(data) -@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None: - data = io.BytesIO( - prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) - ) - with Image.open(data) as img: - assert img.mode == "RGB" - assert img.size == (100, 100) - assert img.format == "EPS" - - @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) def test_ascii_comment_too_long(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) @@ -247,8 +252,13 @@ def test_bytesio_object() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_1() -> None: - with Image.open("Tests/images/eps/1.eps") as im: +@pytest.mark.parametrize( + # These images have an "ImageData" descriptor. + "filename", + ("Tests/images/eps/1.eps", "Tests/images/eps/1_atend.eps"), +) +def test_1(filename: str) -> None: + with Image.open(filename) as im: assert_image_equal_tofile(im, "Tests/images/eps/1.bmp") diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index cbf48de18..ce8e54908 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -194,6 +194,11 @@ class EpsImageFile(ImageFile.ImageFile): self._mode = "RGB" + # When reading header comments, the first comment is used. + # When reading trailer comments, the last comment is used. + bounding_box: list[int] | None = None + imagedata_size: tuple[int, int] | None = None + byte_arr = bytearray(255) bytes_mv = memoryview(byte_arr) bytes_read = 0 @@ -214,8 +219,8 @@ class EpsImageFile(ImageFile.ImageFile): msg = 'EPS header missing "%%BoundingBox" comment' raise SyntaxError(msg) - def _read_comment(s: str) -> bool: - nonlocal reading_trailer_comments + def read_comment(s: str) -> bool: + nonlocal bounding_box, reading_trailer_comments try: m = split.match(s) except re.error as e: @@ -230,18 +235,12 @@ class EpsImageFile(ImageFile.ImageFile): if k == "BoundingBox": if v == "(atend)": reading_trailer_comments = True - elif not self.tile or (trailer_reached and reading_trailer_comments): + elif not bounding_box or (trailer_reached and reading_trailer_comments): try: # Note: The DSC spec says that BoundingBox # fields should be integers, but some drivers # put floating point values there anyway. - box = [int(float(i)) for i in v.split()] - self._size = box[2] - box[0], box[3] - box[1] - self.tile = [ - ImageFile._Tile( - "eps", (0, 0) + self.size, offset, (length, box) - ) - ] + bounding_box = [int(float(i)) for i in v.split()] except Exception: pass return True @@ -292,7 +291,7 @@ class EpsImageFile(ImageFile.ImageFile): continue s = str(bytes_mv[:bytes_read], "latin-1") - if not _read_comment(s): + if not read_comment(s): m = field.match(s) if m: k = m.group(1) @@ -326,32 +325,50 @@ class EpsImageFile(ImageFile.ImageFile): int(value) for value in image_data_values[:4] ) - if bit_depth == 1: - self._mode = "1" - elif bit_depth == 8: - try: - self._mode = self.mode_map[mode_id] - except ValueError: - break - else: - break + if not imagedata_size: + imagedata_size = columns, rows - self._size = columns, rows - return + if bit_depth == 1: + self._mode = "1" + elif bit_depth == 8: + try: + self._mode = self.mode_map[mode_id] + except ValueError: + pass elif bytes_mv[:5] == b"%%EOF": break elif trailer_reached and reading_trailer_comments: # Load EPS trailer s = str(bytes_mv[:bytes_read], "latin-1") - _read_comment(s) + read_comment(s) elif bytes_mv[:9] == b"%%Trailer": trailer_reached = True bytes_read = 0 - if not self.tile: + # A "BoundingBox" is always required, + # even if an "ImageData" descriptor size exists. + if not bounding_box: msg = "cannot determine EPS bounding box" raise OSError(msg) + # An "ImageData" size takes precedence over the "BoundingBox". + if imagedata_size: + self._size = imagedata_size + else: + self._size = ( + bounding_box[2] - bounding_box[0], + bounding_box[3] - bounding_box[1], + ) + + self.tile = [ + ImageFile._Tile( + codec_name="eps", + extents=(0, 0) + self._size, + offset=offset, + args=(length, bounding_box), + ) + ] + def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]: s = fp.read(4) From 56e4ad0dea6bcfcfa5ce8e0af07a512fd7f5a392 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 19 Aug 2024 19:24:30 -0500 Subject: [PATCH 285/652] don't name positional arguments --- src/PIL/EpsImagePlugin.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index ce8e54908..44391fcb1 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -361,12 +361,7 @@ class EpsImageFile(ImageFile.ImageFile): ) self.tile = [ - ImageFile._Tile( - codec_name="eps", - extents=(0, 0) + self._size, - offset=offset, - args=(length, bounding_box), - ) + ImageFile._Tile("eps", (0, 0) + self._size, offset, (length, bounding_box)) ] def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]: From f3fe22d2f2345ed0e3eed9dc89a0cd1df728af9e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Sep 2024 23:22:35 +1000 Subject: [PATCH 286/652] Break if the bit depth or mode id are unknown --- src/PIL/EpsImagePlugin.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 44391fcb1..c7b1f2164 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -320,21 +320,26 @@ class EpsImageFile(ImageFile.ImageFile): # binary/ascii (1: binary, 2: ascii) # data start identifier (the image data follows after a single line # consisting only of this quoted value) + if imagedata_size: + bytes_read = 0 + continue + image_data_values = byte_arr[11:bytes_read].split(None, 7) columns, rows, bit_depth, mode_id = ( int(value) for value in image_data_values[:4] ) - if not imagedata_size: - imagedata_size = columns, rows + if bit_depth == 1: + self._mode = "1" + elif bit_depth == 8: + try: + self._mode = self.mode_map[mode_id] + except ValueError: + break + else: + break - if bit_depth == 1: - self._mode = "1" - elif bit_depth == 8: - try: - self._mode = self.mode_map[mode_id] - except ValueError: - pass + imagedata_size = columns, rows elif bytes_mv[:5] == b"%%EOF": break elif trailer_reached and reading_trailer_comments: From 75286a4e408f363ee8befc36d0ba082fa25fbd32 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 3 Sep 2024 09:03:37 -0500 Subject: [PATCH 287/652] add some comments --- src/PIL/EpsImagePlugin.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index c7b1f2164..da0bf3153 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -310,6 +310,12 @@ class EpsImageFile(ImageFile.ImageFile): # Check for an "ImageData" descriptor # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 + # If we've already read an "ImageData" descriptor, + # don't read another one. + if imagedata_size: + bytes_read = 0 + continue + # Values: # columns # rows @@ -320,10 +326,6 @@ class EpsImageFile(ImageFile.ImageFile): # binary/ascii (1: binary, 2: ascii) # data start identifier (the image data follows after a single line # consisting only of this quoted value) - if imagedata_size: - bytes_read = 0 - continue - image_data_values = byte_arr[11:bytes_read].split(None, 7) columns, rows, bit_depth, mode_id = ( int(value) for value in image_data_values[:4] @@ -339,6 +341,8 @@ class EpsImageFile(ImageFile.ImageFile): else: break + # Read the columns and rows after checking the bit depth and mode + # in case the bit depth and/or mode are invalid. imagedata_size = columns, rows elif bytes_mv[:5] == b"%%EOF": break From 782f0e8a5a8e61073ea2686e722e0575bbe09b6b Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 10 Sep 2024 08:31:01 -0500 Subject: [PATCH 288/652] change "Read" to "Parse" in comment Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/EpsImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index da0bf3153..1c1b6e0b5 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -341,7 +341,7 @@ class EpsImageFile(ImageFile.ImageFile): else: break - # Read the columns and rows after checking the bit depth and mode + # Parse the columns and rows after checking the bit depth and mode # in case the bit depth and/or mode are invalid. imagedata_size = columns, rows elif bytes_mv[:5] == b"%%EOF": From 4bfc77a1b12198d1f8b688fc2037e26059f80ab3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 20:19:02 +0000 Subject: [PATCH 289/652] Update scientific-python/upload-nightly-action action to v0.6.1 --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ee0c33166..1f5265bf5 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -278,7 +278,7 @@ jobs: path: dist merge-multiple: true - name: Upload wheels to scientific-python-nightly-wheels - uses: scientific-python/upload-nightly-action@ccf29c805b5d0c1dc31fa97fcdb962be074cade3 # 0.6.0 + uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} From 749bec097ca48bb70f12bf3b5a78cd4d64e91fc9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Oct 2024 07:11:24 +1000 Subject: [PATCH 290/652] Do not convert image unnecessarily --- src/PIL/ImageEnhance.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageEnhance.py b/src/PIL/ImageEnhance.py index d7e99a968..0e7e6dd8a 100644 --- a/src/PIL/ImageEnhance.py +++ b/src/PIL/ImageEnhance.py @@ -55,7 +55,9 @@ class Color(_Enhance): if "A" in image.getbands(): self.intermediate_mode = "LA" - self.degenerate = image.convert(self.intermediate_mode).convert(image.mode) + if self.intermediate_mode != image.mode: + image = image.convert(self.intermediate_mode).convert(image.mode) + self.degenerate = image class Contrast(_Enhance): @@ -68,11 +70,15 @@ class Contrast(_Enhance): def __init__(self, image: Image.Image) -> None: self.image = image - mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5) - self.degenerate = Image.new("L", image.size, mean).convert(image.mode) + if image.mode != "L": + image = image.convert("L") + mean = int(ImageStat.Stat(image).mean[0] + 0.5) + self.degenerate = Image.new("L", image.size, mean) + if self.degenerate.mode != self.image.mode: + self.degenerate = self.degenerate.convert(self.image.mode) - if "A" in image.getbands(): - self.degenerate.putalpha(image.getchannel("A")) + if "A" in self.image.getbands(): + self.degenerate.putalpha(self.image.getchannel("A")) class Brightness(_Enhance): From 84b167dfd53fe726a61953040ddbfa0e4e3e3359 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Oct 2024 21:11:56 +1000 Subject: [PATCH 291/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 08c2f26bc..36266421a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Support all resampling filters when resizing I;16* images #8422 + [radarhere] + +- Free memory on early return #8413 + [radarhere] + +- Cast int before potentially exceeding INT_MAX #8402 + [radarhere] + - Check image value before use #8400 [radarhere] From c0d04e8b34f759797e45bccac74f49bddfaa7a41 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 1 Oct 2024 09:33:33 -0500 Subject: [PATCH 292/652] use .size instead of ._size Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/EpsImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 1c1b6e0b5..27582f2a8 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -370,7 +370,7 @@ class EpsImageFile(ImageFile.ImageFile): ) self.tile = [ - ImageFile._Tile("eps", (0, 0) + self._size, offset, (length, bounding_box)) + ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box)) ] def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]: From f9c69deaae2e029601dddba48268fadf630a18eb Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 1 Oct 2024 09:35:22 -0500 Subject: [PATCH 293/652] simplify setting self._size Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/EpsImagePlugin.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 27582f2a8..fb1e301c0 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -361,13 +361,10 @@ class EpsImageFile(ImageFile.ImageFile): raise OSError(msg) # An "ImageData" size takes precedence over the "BoundingBox". - if imagedata_size: - self._size = imagedata_size - else: - self._size = ( - bounding_box[2] - bounding_box[0], - bounding_box[3] - bounding_box[1], - ) + self._size = imagedata_size or ( + bounding_box[2] - bounding_box[0], + bounding_box[3] - bounding_box[1], + ) self.tile = [ ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box)) From baf2d8160a5c1ec0c1ce1c64ea70ded2e5581495 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Oct 2024 09:48:09 +1000 Subject: [PATCH 294/652] Updated xz to 5.6.3 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index b3996d5a1..19eb91cbb 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -20,7 +20,7 @@ HARFBUZZ_VERSION=10.0.1 LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.0.4 OPENJPEG_VERSION=2.5.2 -XZ_VERSION=5.6.2 +XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 if [[ -n "$IS_MACOS" ]]; then diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 026d9d306..a21fbef91 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -119,7 +119,7 @@ V = { "LIBWEBP": "1.4.0", "OPENJPEG": "2.5.2", "TIFF": "4.6.0", - "XZ": "5.6.2", + "XZ": "5.6.3", "ZLIB": "1.3.1", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") @@ -185,7 +185,7 @@ DEPS: dict[str, dict[str, Any]] = { cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), ], "headers": [r"src\liblzma\api\lzma.h"], - "libs": [r"liblzma.lib"], + "libs": [r"lzma.lib"], }, "libwebp": { "url": f"http://downloads.webmproject.org/releases/webp/libwebp-{V['LIBWEBP']}.tar.gz", @@ -216,8 +216,8 @@ DEPS: dict[str, dict[str, Any]] = { "license": "LICENSE.md", "patch": { r"libtiff\tif_lzma.c": { - # link against liblzma.lib - "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 + # link against lzma.lib + "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "lzma.lib")', # noqa: E501 }, r"libtiff\tif_webp.c": { # link against libwebp.lib From 547e7dcc5df0e9bbd05119c9accc9295bfaa0cec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Oct 2024 19:17:13 +1000 Subject: [PATCH 295/652] Test cifuzz when wheel dependencies change --- .github/workflows/cifuzz.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index eb73fc6a7..0456bbaba 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -6,11 +6,13 @@ on: - "**" paths: - ".github/workflows/cifuzz.yml" + - ".github/workflows/wheels-dependencies.sh" - "**.c" - "**.h" pull_request: paths: - ".github/workflows/cifuzz.yml" + - ".github/workflows/wheels-dependencies.sh" - "**.c" - "**.h" workflow_dispatch: From 33f065eb5e458724cdd76dc6074445b4fad9fd7f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 00:41:31 +0000 Subject: [PATCH 296/652] Update dependency cibuildwheel to v2.21.2 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 7dc3a53fe..a1424ecc0 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.21.1 +cibuildwheel==2.21.2 From 07be6fca17fc31b3df87bc8b577822ed5b1de51c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Oct 2024 23:26:54 +1000 Subject: [PATCH 297/652] Corrected check for BuiltinFilter --- Tests/test_image_filter.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 412ab44c3..5a22738a9 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -36,9 +36,14 @@ from .helper import assert_image_equal, hopper ), ) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: +def test_sanity( + filter_to_apply: ImageFilter.Filter | type[ImageFilter.Filter], mode: str +) -> None: im = hopper(mode) - if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): + if mode != "I" or ( + callable(filter_to_apply) + and issubclass(filter_to_apply, ImageFilter.BuiltinFilter) + ): out = im.filter(filter_to_apply) assert out.mode == im.mode assert out.size == im.size From 418ae7caa28d3ecb88f6cfaf1fb1a27b544794a8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Oct 2024 01:12:51 +1000 Subject: [PATCH 298/652] Support BuiltinFilter for I;16* images --- Tests/test_image_filter.py | 18 +++++-- src/libImaging/Filter.c | 106 ++++++++++++++++++++++++++++++++----- 2 files changed, 105 insertions(+), 19 deletions(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 5a22738a9..e566cd055 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -35,12 +35,14 @@ from .helper import assert_image_equal, hopper ImageFilter.UnsharpMask(10), ), ) -@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) +@pytest.mark.parametrize( + "mode", ("L", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK") +) def test_sanity( filter_to_apply: ImageFilter.Filter | type[ImageFilter.Filter], mode: str ) -> None: im = hopper(mode) - if mode != "I" or ( + if mode[0] != "I" or ( callable(filter_to_apply) and issubclass(filter_to_apply, ImageFilter.BuiltinFilter) ): @@ -49,7 +51,9 @@ def test_sanity( assert out.size == im.size -@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) +@pytest.mark.parametrize( + "mode", ("L", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK") +) def test_sanity_error(mode: str) -> None: im = hopper(mode) with pytest.raises(TypeError): @@ -150,7 +154,9 @@ def test_kernel_not_enough_coefficients() -> None: ImageFilter.Kernel((3, 3), (0, 0)) -@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) +@pytest.mark.parametrize( + "mode", ("L", "LA", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK") +) def test_consistency_3x3(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper_emboss.bmp") as reference: @@ -166,7 +172,9 @@ def test_consistency_3x3(mode: str) -> None: assert_image_equal(source.filter(kernel), reference) -@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) +@pytest.mark.parametrize( + "mode", ("L", "LA", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK") +) def test_consistency_5x5(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index fbd6b425f..7b7b2e429 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -26,6 +26,8 @@ #include "Imaging.h" +#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) + static inline UINT8 clip8(float in) { if (in <= 0.0) { @@ -105,6 +107,22 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin) { return imOut; } +float +kernel_i16(int size, UINT8 *in0, int x, const float *kernel, int bigendian) { + int i; + float result = 0; + int half_size = (size - 1) / 2; + for (i = 0; i < size; i++) { + int x1 = x + i - half_size; + result += _i2f( + in0[x1 * 2 + (bigendian ? 1 : 0)] + + (in0[x1 * 2 + (bigendian ? 0 : 1)] >> 8) + ) * + kernel[i]; + } + return result; +} + void ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x3(in0, x, kernel, d) \ @@ -135,6 +153,16 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { out[x] = in0[x]; } } else { + int bigendian = 0; + if (im->type == IMAGING_TYPE_SPECIAL) { + if (strcmp(im->mode, "I;16B") == 0 +#ifdef WORDS_BIGENDIAN + || strcmp(im->mode, "I;16N") == 0 +#endif + ) { + bigendian = 1; + } + } for (y = 1; y < im->ysize - 1; y++) { UINT8 *in_1 = (UINT8 *)im->image[y - 1]; UINT8 *in0 = (UINT8 *)im->image[y]; @@ -142,14 +170,31 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { UINT8 *out = (UINT8 *)imOut->image[y]; out[0] = in0[0]; + if (im->type == IMAGING_TYPE_SPECIAL) { + out[1] = in0[1]; + } for (x = 1; x < im->xsize - 1; x++) { float ss = offset; - ss += KERNEL1x3(in1, x, &kernel[0], 1); - ss += KERNEL1x3(in0, x, &kernel[3], 1); - ss += KERNEL1x3(in_1, x, &kernel[6], 1); - out[x] = clip8(ss); + if (im->type == IMAGING_TYPE_SPECIAL) { + ss += kernel_i16(3, in1, x, &kernel[0], bigendian); + ss += kernel_i16(3, in0, x, &kernel[3], bigendian); + ss += kernel_i16(3, in_1, x, &kernel[6], bigendian); + int ss_int = ROUND_UP(ss); + out[x * 2 + (bigendian ? 1 : 0)] = clip8(ss_int % 256); + out[x * 2 + (bigendian ? 0 : 1)] = clip8(ss_int >> 8); + } else { + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip8(ss); + } + } + if (im->type == IMAGING_TYPE_SPECIAL) { + out[x * 2] = in0[x * 2]; + out[x * 2 + 1] = in0[x * 2 + 1]; + } else { + out[x] = in0[x]; } - out[x] = in0[x]; } } } else { @@ -261,6 +306,16 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { out[x + 1] = in0[x + 1]; } } else { + int bigendian = 0; + if (im->type == IMAGING_TYPE_SPECIAL) { + if (strcmp(im->mode, "I;16B") == 0 +#ifdef WORDS_BIGENDIAN + || strcmp(im->mode, "I;16N") == 0 +#endif + ) { + bigendian = 1; + } + } for (y = 2; y < im->ysize - 2; y++) { UINT8 *in_2 = (UINT8 *)im->image[y - 2]; UINT8 *in_1 = (UINT8 *)im->image[y - 1]; @@ -271,17 +326,39 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { out[0] = in0[0]; out[1] = in0[1]; + if (im->type == IMAGING_TYPE_SPECIAL) { + out[2] = in0[2]; + out[3] = in0[3]; + } for (x = 2; x < im->xsize - 2; x++) { float ss = offset; - ss += KERNEL1x5(in2, x, &kernel[0], 1); - ss += KERNEL1x5(in1, x, &kernel[5], 1); - ss += KERNEL1x5(in0, x, &kernel[10], 1); - ss += KERNEL1x5(in_1, x, &kernel[15], 1); - ss += KERNEL1x5(in_2, x, &kernel[20], 1); - out[x] = clip8(ss); + if (im->type == IMAGING_TYPE_SPECIAL) { + ss += kernel_i16(5, in2, x, &kernel[0], bigendian); + ss += kernel_i16(5, in1, x, &kernel[5], bigendian); + ss += kernel_i16(5, in0, x, &kernel[10], bigendian); + ss += kernel_i16(5, in_1, x, &kernel[15], bigendian); + ss += kernel_i16(5, in_2, x, &kernel[20], bigendian); + int ss_int = ROUND_UP(ss); + out[x * 2 + (bigendian ? 1 : 0)] = clip8(ss_int % 256); + out[x * 2 + (bigendian ? 0 : 1)] = clip8(ss_int >> 8); + } else { + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip8(ss); + } + } + if (im->type == IMAGING_TYPE_SPECIAL) { + out[x * 2 + 0] = in0[x * 2 + 0]; + out[x * 2 + 1] = in0[x * 2 + 1]; + out[x * 2 + 2] = in0[x * 2 + 2]; + out[x * 2 + 3] = in0[x * 2 + 3]; + } else { + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; } - out[x + 0] = in0[x + 0]; - out[x + 1] = in0[x + 1]; } } } else { @@ -383,7 +460,8 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o Imaging imOut; ImagingSectionCookie cookie; - if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) { + if (im->type == IMAGING_TYPE_FLOAT32 || + (im->type == IMAGING_TYPE_SPECIAL && im->bands != 1)) { return (Imaging)ImagingError_ModeError(); } From 8e6d518ea89035077fb4a855f61f3b5bab26a9c4 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 5 Oct 2024 08:05:00 -0500 Subject: [PATCH 299/652] change parameter type from list to tuple Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_eps.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 94ab5d327..7ae0900fd 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -158,7 +158,9 @@ def test_missing_boundingbox_comment(prefix: bytes) -> None: simple_eps_file_with_invalid_boundingbox_valid_imagedata, ), ) -def test_invalid_boundingbox_comment(prefix: bytes, file_lines: list[bytes]) -> None: +def test_invalid_boundingbox_comment( + prefix: bytes, file_lines: tuple[bytes, ...] +) -> None: data = io.BytesIO(prefix + b"\n".join(file_lines)) with pytest.raises(OSError, match="cannot determine EPS bounding box"): EpsImagePlugin.EpsImageFile(data) From d4fedc852c2a78b9dc93b67f93d6211f7337ca43 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Oct 2024 15:21:42 +1000 Subject: [PATCH 300/652] Rename test image --- ..._atend.eps => 1_boundingbox_after_imagedata.eps} | Bin Tests/test_file_eps.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename Tests/images/eps/{1_atend.eps => 1_boundingbox_after_imagedata.eps} (100%) diff --git a/Tests/images/eps/1_atend.eps b/Tests/images/eps/1_boundingbox_after_imagedata.eps similarity index 100% rename from Tests/images/eps/1_atend.eps rename to Tests/images/eps/1_boundingbox_after_imagedata.eps diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 7ae0900fd..0a69ee6e5 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -257,7 +257,7 @@ def test_bytesio_object() -> None: @pytest.mark.parametrize( # These images have an "ImageData" descriptor. "filename", - ("Tests/images/eps/1.eps", "Tests/images/eps/1_atend.eps"), + ("Tests/images/eps/1.eps", "Tests/images/eps/1_boundingbox_after_imagedata.eps"), ) def test_1(filename: str) -> None: with Image.open(filename) as im: From a9cbf6d5a71013124305f05d09b344e3698f2c0c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:41:01 +0300 Subject: [PATCH 301/652] Test Python 3.13 on AppVeyor --- .appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 41a5725b2..781ad4a4b 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -18,7 +18,7 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/Python312 + - PYTHON: C:/Python313 ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python39-x64 From 1b57b32caf99f7fb5b9c6b83e439832f5a55dc83 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 6 Oct 2024 08:02:17 +1100 Subject: [PATCH 302/652] Test ignoring second ImageData --- Tests/images/eps/1_second_imagedata.eps | Bin 0 -> 45834 bytes Tests/test_file_eps.py | 6 +++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 Tests/images/eps/1_second_imagedata.eps diff --git a/Tests/images/eps/1_second_imagedata.eps b/Tests/images/eps/1_second_imagedata.eps new file mode 100644 index 0000000000000000000000000000000000000000..e6309a3b4ede316d83a2bb84388f1d29140b433f GIT binary patch literal 45834 zcmeHw349b)x^Dvl8d=;$MePK`o^)4LS65dc30+-XtXUul32W#ioyByzLw5%P3~=w< zd-Xax!^P|9;4-+3qKG0OqA0Cbc6tb(C`19s_Jw?)cEe3_j~vK zQqWze&hnk_eCONFcdAcb|MRZxb8fO&R^MZZUwE`A_=?5Gj~{Ni7*AJN&RZ_BbTeyo z>%r3F@Z8Nv+mY^Wq^Vwygmf<>P0cUwro*Wi2&f5lu1-i_W26b@wMPBt0QCB9Jz2j8 zkbi@bK8EyAqkea!xo+Jp$+$P+!y|na?gP+v{B7MVt{W_tkx19vuH$eG4HVZupG&e_ zHl+3Lt_YR+t@+iVXed%0s<-k&I&U4C<14QTQqC|yw)1?3-Ic)!R(w0;4BlmNDdTf4 zx`+WR8Q5*QrN<4aV>^8Z>i9pfd%oqO33=mk2X>b%7xQ+`Wq0u0!0w|hy*QrdMVa-H z%KEr$;6oKX9=zjM4;PR9WAW*0ljgFr3|6)W-hsEOEk|L`>g3w;U!~2spBh_|>f_~jVhxo*G-%+^r>vnS5UmL^ASl0t z)%N5}o2b)x6oKfThOgE!UXd`yy!vQOC>TNcwN%qn9}SvlTRefXaQtCPRU{|=;0Xsk ztq;Ly>%*k7KqOw0TNRxbfAH1?)c6A^*9uxGudOnMuG7UbWvt=>Ha!TXDHT?0MW~@{ zOz1E4$gZ*B;01X~;KF)JxT3Hq7}W+%3D9iDFC|c6?I?drBoJl!`Hg`w)8f^W=K0H` zp|I+U`e+0U$^5GPh{*?(rpmYz+LlTFN*zqQZVMqF9d%n!9?q25<(#%zpt4;rKYMnvEWj(0Jz2LlXUbr z$^c$wsldG(1kSiK$r;Sb`mt~JmHkf^o!|PhN0RKDSk3F&KKJ;ZJzv7U@dZ7DzJ?HQ zg3}GL0K>Ajq)g*{+kFhO@iF?*wGq=GtnT$x(!-0dUX(H}nhnDHVvE<3Zz-^pT5>FT zmRx*H5i}|qH-r7y6SZTrnOG&Uf|=hcSe9TzfFz!DYp=3UL$D&!j<3t39LgBl#0+tY zBeUI%S(Mh9WoS%koj5|RS0qqV?vG3jOd{6n)~27HtPI&#;M1Euj8_x->RlBMHPpAs z>JE_*vxKI`I72Tc%eH` zQ)N=BzdrbJW;O`WyTV`TYhViJl6n4cw1etX&Fr?-FD^VSNi(}op`s#O;-sJMm0EqpU|qvOX6bfOw;tUu>fW<=uZw&3=p&%;@*Wo@ zU%~ZsPxf7zGWVbPURN!DWSuhTmaC_{QYJXUzkJ`*JN52@Z@>7iJR)B6==y&dto~_YMF0r(F$y{azb#&maEn`QN_w;onX?zVY?lU!9yubv`9A(^}4B498#<;tG?KQF(EFnDFk z``-$;lzkEY<=viwtIH#f?^1ga0=)-|uYLd|9=o=}TjY%Tzl@{s!x#){MIrWyrB4sm z=jE1c%U?F!SXWe5zJJRVpVYkRdu+sj+xQ;eeZHgi*?E;YJKuV1)sREG?p(HO;l`Kx zZ$B#?dUkb5FuiOi@i9pCVtl=<;j8+R|eW5I@C=3RgCU%hz6$uHkOG^_d6 zriBAmR$a+o|LL9l;mC7G%U_LmNRjQb`Yj+R9~&0AMlxwo?Kg5x*+W?a8jtM-;x|LO4jT}?Nwe!Fhzp{(79 zDrW6Y-+t(&o9^2ioIc>ChsKo`{M(G$LorMF+T@+Dt~MO1bN|tlhwGD=Dt!W~`_> znx}n~dFJM-a(=|>)!(gqd1_Hp-fq6`%{Qkns9NKD?#qU2wg@AV9ysbc^P8uKE?)iV z?X&J%{x63%q@5`_eIgpOlx8p9UH1OUU$4x|-*n>CiO(OK|9G@EJFR8Z4L#OO zKN9)o8vhsmqTe-@R?h9ai9fgFzJ`IHtjnmhTf|Ni%nc5PkXyzlUaqw^N-_pe?%;JU(gEcAppMUYi;)zQ>`=`UR?kgIw14 zn$mvvS@wt9cRrSW&)o&FKYyRQX3**b{fm!YzP$QmOKItcr?#BfR=YRvlb7>WH7%R4 zcE*{VIYWfuzqx;Z@s+`>x?k@2#Ty^F=Rb0IS6$;v{hBhKIdk(f+{!O*eIsVkRz)%z zUO5w3_u{W3ch30b&MEt@Kc3gPZrR3-yPAf4wdD2_EAFfO>#mq3ve-8D^x{YVxODfw zHJ&LdUVb>^=-Qh$OgrLV^4QCL?q4@>|Kxqc&!#pl+`neY-FKC4NFB9z@z_--XD?rp zv+=V#Hw<2VQ_1$ft=m{~!`JuESTbASB7rv!H92FJy{8A9zPDi4bN0F0{AF3q&u+;p z+4AI{n@g9iT)ynAu>0_Jr}hSmJ`$hUu_(Otl_f<3re$WYI`Gx3rkf}FPfxpQ$*v7U zPF(l97d(fzmQKEL$dTga?6SUZzV-T}yJpozGW)E_ef**2IpX^tHs6-ncxu|O50{_p zqf`A-}YbE^3a~_ zK`EaO?6>l)r|O(f+msEvS{^8Ub_ovSOjl~r4Izx`i-I-5P|(LYWY9kcwQrS$9h zl^cdXc;v3iFOO$e^{;+=%kT#V94tRE>D=o|9oQi7t82IT4}5Zd@9djK zd{#W=(5w-cEZcZGa5mi;oPT0b)}A}Qp0#ZE$?Rv&oO!5R8t_H%&~^7pO^Z*bKJm`T z)%)s~Rtu?{CKt7M23@=A;FibUU%Dn{*>vLke>M#tx%j4+_Z|;Q1dg&<-&t6d zyCwbDV;{{Go>_nAvrm5c;CG)ehhMw{yRkG;rZ}$9HbMeOg_^4BzH8DWB~B@YtK%(q25% z|7`lcVcS<_eDmDq_xi6oKl#7jzBc>mlYzSyUAL^R_-~6Jedq9JC2McE{Jzbfd>h!j zW$o*SPX6WCiSN$8vH0_Q2jAH6<<_^Cytiuia~tYvpFOZuyO zIpg+6Jm0RV-TT3erkTH3_R+%GWA`21@%q`k$#1;5{gG6$IW5!q#lOtRS^C6tMIY{6 zeqWRG#f3ZG?)&DVO=8Q4BPGAzxna}MdroG5Fe7GJP_p-iuO6EC^yFR5d6VDX`Pj9q zOFkVX2>llS+lspE#~&MU&B5Ybwin)8^_wHXl})wV3jcI+Tg~LA5FKoJY z)FWRM{OgW$>puAA%?(fPi@e_WVOG<|(;L4$wqeLkzuNkK&ek{9-}UU%^S@r*B7RtW z*F8rYdilyLM_sXU!~TaJT9&u3zV^`z&8m`etbfA6_3A@ZM|NMmS&o-Kb$ty}D*z{`r;j3cjDQ`>C~SD*X*R zpFVx|{OirzK7Q(~_jL5H%`cvVdVTlpO{zjyL0o)d#1nk+MBPw@z@CI{lhID+u%>W{p?pa z|HrG%0}g)G@Zi$*({e-khZmH5{?OH*?Fzj1`r6?S2DWZ_Ag^TmN$!K?(X8cntoV>W ze(1NG@A~GU70W8qwM=nk!MxPZmrXdd;JO#1ADp{2`0gs_?{Ev-r0w7M$4s>-mL;qZ_~c<&oJB2P>zJUA^NMfm`dpo4a{!LCZ^bEE3kXq-IJz z-d}sgANSR+`t#b`>gJVCYgoQwPo}f{nzbtbdIN0*|nQnWWM@CM3w)XyWpFdXq{kPw3 z-B*?W*>WxW>0Qm0W1l(vMa#B9yI!9#cjuJX&Zeimc-Oq_qSxnt^6mO(UU=Za8}2%r zdHwp(#uI1Hzm=Qm@WCg@-dnP5&Yxly+pI&k1b@4G?S@sAGrsvWyRzWU>bjTY^hHlU z@%A+D)w}OHH+t!Y_dnd9w>M@fd1K2HWd{#T8+&x%=I@TZK7LqiufASNX3K?Qe0QS- zQh$pZA&HQs%;L9L5tOJ#I*Lz(rRq`Rs-LJ~weVP_@c2hdF17|sLDJImJu?E0C7KcC ztw&VPj5L8p3VH2S}g zpZf25WO^om)#Gz9`{(fmLqQAswtDjltP^V@5o?KT8Kd-PaclGblX2^g!qyaSw#exF zYY<_)aUTP`C?=9@+^;t7V~sn=&dOCAp9f$X*zLEbC(mjp*&yntZw~i0Z<0VF#;$TTeodc+I+ugk_7h|l)kp7Rm zyR-S^9Tv;-g!J7BY0JX~&Ya!e?O`2$ECzfYHg-V(zlV|bAl>g#9nYqVz`sWNJkpmv zs<$PYbo+yzM$*zfA>AV(-76t&|3mk95RyZh!kN7=Nd6B!89lE;+JXGbk?%lSMEX*s zH6q00IzCe=JlLc8LhKn$$Ay)Iz7Je)npky{^t$Y2(se z)`G$u$P_M~s}wo3z6P~v?)19d7!N>PjH_3C=Z<9C)QMX$|E5mdj&!0F)Y}(b(a$pU0*yiaimF9R|#0Z!ccjvMAQna(4F7{rj_Hv88KX8+^89RV=nQ^2IL> zX6MDu&)oOHYVrK5vDoY(4_^Al`FCQ+PBjnzL)pRaW5;cG#wwP4-V{5(-`Vus_t{s* zVy`cVt?wZw#bUAZXL`*0#pYP-_^IEIk$?YwY~}Iw&2xIkF5LTaY-w!&%G*8-Z%fTSbtbl} z_T0``?DLoRKfO-ty?g1ZMfbmR@W9c~sh9WP+_K{wi0;omf9`>44;?%eD?R&K-j>Bp zv+rGd@Z}Y=PMukx3DG}@MloP(ww~9ymWtkBwiWYdf2c+DH$@G-DSi6D{a401%>t|u%V=c8Z)eR z{F8XA!$l?IYNq%j9T%tb4ysYmP+uSRM(4673GmDi%&t>%?_Lcu6nMoMz>GpspCX*J>N2cQ0#>JFg}%iQF?`9IQbr(as7eajwXf^?k-9oE z=010Z)hRoPVphTB;2p9ikGKPhBz?EQMZp zk+K{Pw?lKd9A3&1B;>dRI0FJFxbW>jnqws%lsJ*gv$62V7X`U>Ogb;t?gy#YuLz#I>b|K%+M)n*!H@{B zoxY9QY)F(A?UEolM8U~BHT(mOjT&|bYwr~C&#S|5s_n}Xo>iyB2u4|2YyW^OOD@Ub z6bS~wasV?toBf#}5^#2k67TRjIj8CrMTbKaU7~?E!Dz*>T~9P-8iNyk(s)J5?$FSW z+bI~Gh)x&JXzEm)g3HaTq9P~I6cq0OhS9c_F1*vj=;3gBX*`k`r;EqQ<4JaUFg{QZ zv^6j?V92fT9Rk5{sG{spz|wJ^7w}|UJa5oD-gP{IdFKfhqp2~^t#j-k`V$5kurtOE zeljixm=~p9a|Req%|g3FaMCDvvou~{7kQ^72@dtb5`8Ra<3;QC^GXnj4?8MBPoR}i z!p1_3A5_W2^G+0S zK>6F94yT5B<{fsY<~7kxxH}{o3(=z!bwPoSVYZzY0`H)CaflEIMo}kJ3(?P%=>!b) zq(kQq4`JQeHX@Jt0L<3z$f3r3F%dxCasi=9r(3;Q6wMKGAX7QAu$pxY%_Hj_wL z0l+M_d2ZEtyt6G$?Hvh~UC>+%T5CIdLVtGhrL0DG-qt!?BF7s2bFJlq7ME_cC11{f zp+jQd;9wITPwSx8i)=@m3_LS>nDybo=t#3R@v3?P`h#8O9S>$ZhKGYoq=QMjcpbZg zYqed1!?fs59jM{Mm=El3%2RwEfE%@=rXuJdYIp`s9dt+Ey6<3sh$G(y*iG$np-ku<{;*uO!hk;SK;B!agZ|@^+NuwTig6fD($ARTX3cW2Rm<;K(c>Gw@6OY&cPbO*ZSd_PO=P4?~q1p=Q9x=Q0lEcCe$@X zqQa)YqjPF-2WZ~lA2KT=njEFm1B(K9Sg6sq2X`WLvyK6oyj;U`ZKWw`?Eg>rLjc|2 zBeeCR+v+S+Qrg#v)59Q@Lj_lW{_X3Tl*HW$yHCCY*?)=Z5sX&vy-mNcf|w^rC~Awe zLU72CMtJ%T6|!S`k&wMO97e#i94|Q$2_}jCL*B_R_8g1(bjafik{DKB^mT;Li7WOl4QwavDn9o?#$72vC9J^iR;+j%yvvn9M%i~=}kq1PjH|g=EX{t-pn2+QEzmVpKA17)Ktf};JFPj&ny{+ZsH5ssU z7+4>=2u}|@3;HMHwyFM0rB0A&FN*sy30mq-fD3lP9|0=kGCkAQJdOHPM@_DCY$1SbSj z>f{h?f@6v)#&VN$$ef!~I0ui=6N*LNgSD6-2o8yN@hYdU)v>4%WnRRvy`09u%MkXo4tNoNh!LQNUao^Fnhg@L+9tFwn(B6A3d9c@+iT#mP{PCQUq?2r6iH0n~Bw zUQUmpxOf@seGwAEXyMQ}#OYwfbc(PgAZVdtbxV|S3mj|$^evng=uOatjfGJrkt#or zCIo}%0<}OPxeKTQt|fop13DPoz_=OfA`&`9;8dqdFhFeht*WLd9Iv{a2rW7}x4v6c9K*-|}(V^;A z1ceh+yQHbIhH2n584jx(@Es~I0WjgAI0Ux}4GG^?aVpT2vLt%EcvMxF7yPA)F3lxN zvVs_s0E9fST%0HY0}ZpHGXC(&qN=%_Zp|sVIhTwZhso#Ms>h`WJdj7o&5L!iqJnh+ z#_I*0FpX|_C>*>)!ULf+x8_iInLJq${Hmd`0!9@jFOATxiEfG=aBc;}5;YfA@iOXx ziO{u&#S6*xl~oV?a|B9(H)IJdouXTHsUA!`4?JX#2b6d6iqorjfhsJmfGI+U4kj<0 z+v`%?K*gy@yyA6h7_}k`h_I?6$jQ-My6v(XQ94ai;E##0ISLl$yb5r-Kc>1A7wS0_ zHH{1i1g4f~e5Is~(y~2TgFifY@u1d03 z;{Xjh7uDRD0O0NN2zZcGL34889S>$-(!4UTVSb@kMl4HJMc(UidSyjMm(=i(J)x&u_ZV_?-7Ik56uz-RlB99HS zgRU^|ARfygj{<8`2i$p(v(l7QGN9$q7wIyI5= z@Zb}$9#pHu%Md_W2FsA_!x2__C)l502=H|z&h7CkqE|*78%zx{*aa{c^nfIQ%fOpn zjvC5L6ks3VNhp%QYaT;hBv6F(1o#h2b|C3SupB}I;d6R4FfP^@q)ikF9OcwhMRSuH za>2_dy%uL%hc37UUBY@Q~(KL0S?> zm^^4UNIzIaRJ@?L2Mk9GT#Qx4TxsB2!3}-lhJ+DKp<~^=2bx@f(ROP{^P1#>VuXZ2 zK3rJrKmn_o7bYLN*5ihfmoN>`u%biZm`--dm?amlkOEc&Ql}7IaGA%g*`ZP~TMkur z$ZkylAN5fS2LHjmHq6b6^<|z9PTFgFQ5kcmN=2RM4J~dR_&yGrg`z95jk7E6}kH zCy?=aU@JM8Ay_Q1k=vy~^+FV(i+NRcsZeILJSFzyy)x5<%#YGkJNzIhP#$yUb=rZd zSLNV&I9-|uv8OLcp`yTwfKNbV&`Lp;o5`GpY7WK;nv8+K0FtV5!RXSUIkIkECngTo z1OKsQ$U%~!RADPr3EGn8#qNbS1w}y`2nEmpkS{mp1ypi4p@;{1{Q$f5rFs6$Gf5=SsNiV=Xg<+s*mIodT=mWEeg&d>> z19ZvQDTG|Yp^`Yv3fNZgYM4Gnf!<_$(a`xW%q#>8LjqyJbI{P>HaEztTPv6wK_nXw z*A*t&1N?|zMVZJ%^Wv7gFlz8YoE#<}Rv%@Y1MDFHe^3?d=Te*w@U22D%fX^aUfKgD z4aeIVpF${0cYh5D$))oUlE-MLLRV8z?u}kA(?#*0tDH?jJstF5)6xRdkve7ogOFvXhTSX z4AFyQ4j>LtU4lHr6N8%pqCnX~~cs$X<*QOFb{+aj-IW zY&{A%S&`vF!~VH6Og6>|XWUDpgAyQm2on5FbPYbn`UnmOX-RM$48CSpG>;ovnm7#B z2c83DkT$O|g$kC2c*B9=0dE&hBNV9uym^_!iw~Gi$qoO^4(`BG6l)Vy2g72vz{w8G zKGuG4K#0S2Ugu!ET&Rp0k$9{=pxQAs$U0myFfh>+D_0nQ$Ow;F#cnqAE%UWtwwXS~ zMm}Z;YzpxwpAU`|Bo8VSoC3$(%Xwk5;8w%=#OeU-3sC^8V2=~r;}!HcomV1z%#5{5 z1`olNCf*Uq$bygX=7u0(*$r}dp_VX5L_tY~B_@5q^eOB$EIyfF*eU2T1uhLdL7FUd?}W*Kj&MU6 zLk)`XaCCnR%N5877FwV##0yFcYhR2N?B#)ShtfiH0E-jM6&`_&SA`qTV@d3GyA%ck zD@wO2(HNl(p|ip7UW9dkG9myT5CU{`gM(o&u;zw>#1Kh*!0(y>2_pUgVs4&dfDm|9 zz(Nc}f!7ZYo&v^jn-OY2C`rP6!z*yXe{mpmfkN1KOg`gNa3|;vIfE|%UlfgiD;!6d z4_bVn2sA}(21{%*hH!tpSP#M7!^#pu&h`pmYT?8}AR%;k-2)u~g4<3DC@k6067mkC zg&~q&Ayo<{fM5|0q+4h#5HD~ndGc#2Nt?mzd#$o2*XyOL#Hfa!RTeHe)w5f|K2%^IIjplC2tB|rRC%oT;6=mogII7wm>&d%1qDPJ5=#Uq0rFg6R^hQA zqKHWX1H)f~E>I-6ig0AfBXOXyM|Qwe>Gqa(hXiOQ#HDD#4JC?+hw_2LfPkI?n+Yk0 zV!`?s+8W9RtO1n@szEUH^(}aS$Fd9-jT{yzDy(N56uJPbljDy7rB_rTCW1!>+2F+i zQF#1L4^|LZ6fr)f5GFC53pz@Moq^Sa3bDhRAk~C;rU+|}g)ek67Gszb%(wyvPys67 zI~|4!3k9wOMKCAe4M;xI;83M1EID*LtP}Jr7*>Wt$51hJym$cBjXoHEAa;S^A(>+Y z19%8AY$r?#-fkpU4QmYq*`U}F3nF!ns1SI795(W;vFaxnEDVKp1V{{bknAQTTOtmG z48r?nmPSL!L0{j2w;;~2;^boxw-F4uV~}PzIn22OqZ044@Q;e+4h)Nf7H;IX02lBc z7E7Qj=~FNnY&^k0j2I@+g#`kcHZMpi!&L&w6>?sn*)U=7UdZgD8LhS9o@;KE?}=O8*G^aJXEAZUbWlme0SK;WPtWZYoI;9U|7tZ66~iAWXk zsS8{JR~;@UrV-2!g$)&g@H5;4gdE|oV(ksVS7gKxA<1stwQ|@iQDa z4zX8QF&F?S9$ElkeE>fe35hr0Z#WZ1zPV$eT zVd0;H!4Q8Z=L@_AZ3Ci$TnaQQcm>)>LDYdoHhH*6Zl+J+Q?URLM^RtsDuMVG-9y`x z4q*{O6p%+n>;~@#JWZ=`r~}w=X6GB3(z+g~Ynhc{2 z94);c+MP?t$Lm%sAG^hEU@>|aIAd>(;bw9MZ@>brSw7K^F6`VgSg={~v9P{S<}^4+ z@S=?RH((=PuD2n&Fy|<$<2(bV;!fX0G zaR*RlvXl7CXbRA+``I)s$Tr?}#0Pr>^wt4}gdIw(yLfE(xv+OZegc%KUWZ`dy={_M zdfy&yzTn2)g>Tx(0XbOOfi)-vM`;wsoBC)09Z5qZ7+%^BCR~iX_%mhVGT$XK=iGe8 zK03P?9_G9oTEeVjNJkuo$?Il0R+ty!q3E+`;$gl%Z$49}JY+`3rW%{6gt>Kj^|8=G zKe0@9>iImqfRM;11|RW~p3mmRfM?<;*xR)+Wr1KN@Q+!a!N6+=*r_HaU>BPoIK)JF z8l5R8vVkuVKQW(RC^|=@vW(xEwZbBX(#Ih4cG6u2z8+(YcI?^G*b~QsRhB+dsJp~A z2omV|*!51Nmul2!FeH>wEu$Y+k47c8%{Pm&OVb7x!9d`M^@E*mo{fcc3@)!ZUv}gZ zU3F}Tn;GTcOR#)~NBc4wX9sPd724At>OnB*fwF|LV3BL+Z{&9{m)MzUlTUcW-$TT< zLn6&!#`W3860A+WSI_6{$VcC7EGDrGUb1s;+Q)CDF%gF@zT3h`2F& z8GSPtdM^l)8D-|1!+4K?iV!2BcQWF948GO#p-KoA!C>zrP+hZ3G+-IO*uZj`wZXiI zEM@dB>QvI{L7mZMlmQKr?j6@{UylI> zu5o%W95C+&F7bN;esK8MWy(C%ExQ;Dvz?yxv%lkb{Op#k4Z2{o(t=X|6X=1L)+i(e zwaPRBp^fM3eSzJMME&(bTZfM-Wk z4DSzMxqy96_#b3U@qQ0`9}mds{}>*$(@QPzjuFA;OkWn_6Tb}D3@^hTLU%B2egPiT zp5Eh0)P)92yw%^vUsb%iULhFRLvK?CTgY@ydta96KSGRbXo9{c7_gDLo+5fDl*Pwk zB;v0fnlv-k4s0w&UVOcQRdhmo!|Ju>pbdVC8b8z+kL-7&rHyrg((+J+zi*PQzM`_u z7p}F=ul9%i)3szWzp{R9HL&P7hs40(H<5=3t z1Abo^CyElvfsn7lit~*L$G%B3Y;-Phq~2HV&u@rSTaAHHL20P$Rvc!D(V0JcEv@rM zeHFf_uQU?HFOusPTWwLC`)Rf5k5*frGNBcMEyt)`5XA|$RaV441=)(K^gw`9t^P(I z@C^8aRncl|sM2b}$KVV7b@gUL47s%#BTO)YpBbC*t&9358APnC3HYrEO@U=a00)~| zX@>A=ThRny^h1@6e&W>^Tgz%dSfJZC3BO!U?6wVGfPl6BD1LNIgY26$I=fNNu`<@l zOgX+~TZ*-&A~U5}%(3Uxd;HZk;}(Yf1q*Wv%NN#`%M~fvSxKWCGaBpai3nEkS}>B) zn3>|E6Sgyurfge^l~sw>W~S&r1;?+bt;8o*DV<9zC+3jk^fG&;tIRLkN7ApqIUC2@ zI5CaqGI%kA6RpOTl9hzi=x{}4#uQaEI>b|EN_8|^pJB7jpFcl+zK|XYSJ@Cel5KY0 z#`9^Ykrr8ipKv#(1tWuWASN8uA1M#lursBt1Q+KeH$*d2QmCW&WsIz@KHgCi;M?ctY zsL^4|IO})ZK3@ETC}Tczayo8oj!>%Jy?>rE%Beczy5jrZ?hM(+*8p7p%?>y+4 zK^@U85}o32o!2%L=ol2Lw@xi)ZZw9GF$KT!4_0N_N81vfTBnxP$q7}|R4!0`QGXU# zk599^(m0{eJotTtUCyu@?GtLYgTW3v@Eb5VSDM`c-Yql_P0@Q#sMD^sDbt<8!B9}F zZaX-}kqd>PP#`OQ>Vk2~y{EvLc*uPMN;tij8YIA~D9@-2h3kCLEOzdI4Tm&Tj<&Vt zwyjzY9fX4(5&#hONtO!ybKB;c1%a9@I+$p*jcVbc6(H4Mxoz9B-Y@KEG-Lq1gll|( z+=jX`e>jUmLgrB_qe(>C7Pof9nohzw5aWZDp)ApE<6x)oWw+sU2^%Jy+Q!Q}XqVOg z9G2E53FB$)M$N)|)6UjP4q*_&7(rrIyV`D?iV*ABkpRWr&Sl zylh7sOnCp&E@AtLUK4ZDo`FkAgnmKe4!dB-ia>$*Ng1`G)|#EriYed>^IQ@z3c~&H zq4CO{6K^9pD4LHwG$&q{!J9eQ`~uA`XmSC#@j-oh0S=YZYicSo{ACsP3ZKiB=5shJ z(;N;*d750#`O;jz3SRKz%{seJGC?OazW}s;>bfuRzkzWFO)mgH4vZNJFGvs3zBm=-5){q@@V_h@FgGT_+3XU&TE=J_i|!$E{GGxJ)JinlWw z;aC_ZHVJe^j1@nV!Bzz#j!yg-$i-t%2WOgK$A(@3Qcc(iO@A0Z-1Q(axlD$Mw-<8u z3_E-x!8A-dnyl6j1Brwxqa+v@r z*tt%&dBF<9L67TNlRFcD{9k0y3V10$N3A4a8K;x=f7+n``YTqY$tb?+iqr^sYh>LQm(iB8?S$kiz_nU%W8Wm2M3 z_bzgEicDsuE^?Wa=+wQ7T%975S*eR$CM7y`?;=;H$YfUPB9}>tPTjl6)hRNWmAc4f zQleA$E^>8>OlGAna+#Fq)V+&bog$N2sf%1DB|3HQB3Gx#WLD}Tmr03E-Mh%uDKeRr zy2xcxqEq)Sa&?MKW~DB2nUv_%y^CC(B9mFEi(DopI(6?NSEtBiR_Y>`Nr_I~yU5ik zGMSaS$YoNZQ}-@%b&5=8r7m)rl<3sGi(H)|lUb>YTqY$tb^lRvC7j3O58@D>`8aI{ z$M+!34t3Ix>cN>uKX%j+Z)JxnvGaVaHu@pFHGaYoJI=^f&`_oyhg6a?iRHvkcETY? z=*w!$FDh(3AL&PXGmkMbjwWi~wDoYNpuaK74yM3iOu6G*p~nZOpIB8zKcK;hOWynf zD@*rHa@Z3_gj1DD*^kn!kp+>czfM0uO3$Hlr`Q2Vw$g|{S{3#!Xj93|OQ>cH{DP{9 z`3cnlAsY4tBbEMed&u$pgz5&Acu^w8?aNx5l?8mkTC3e!+P;dv(O=#-iR-w)>YrzFTwCD#wS?%efH4t6=BQ<`guPSP#pB!d%T4VgGJKGxXUZ078f1W?U zx1kosSJmLF!f8k-vepJe^Mk;lZ7z?q!`dGmW^~1x2|pX6@PiOdmSfy$9f!lUFuexv z*}|du5vx-&`YsOzB37J}qCZN!)!|C#9o9&>FMu=DjIrXBFPrM0S5xjE3l{8~1g=55 zP?#NFW;JG2=i7Yq#4@!Z6b%La>|~GvI^@jS5Tv8Wezdkb5WS#*ep;HXzNQ=}gmG35 zgo+(j_F4jgz~h%(a*1`3wGL;dh>h%tKL0pY6TK>(pw^O#>ss+V9yXl zlLkx|@;I!H3Em=`t{d5DdW^P<7BSP%H>tR$0;k!H^Vd{WN3BXtG?MQR(`i27!(i0P z2Atyq^E47agDVQy>0SD{d^%9r3f8NM11d7uMwbQ#kO!85zuBkPhP1Pj2-9)Oo}s64 z{*tvWG|!Kp9NVp&m5#!ThDh0&X(a4MHgQ-0Pe$9mNz{TV*#M3RLYhfwMa?`UnfOxD z5K&v0j*74@VpVWVBP`22Yh6RYB;GiG07&3K33L*ys0@J+IEhYg#6Ykt^i21@NoEnw zXfZ13gXo(y)DQ~mOq?$^6Mr%ncVR4j&BWgjru!6paWg1YjW2#C{=6)^3nJ3=ETIj@ z&BUqyXu{3Jp~iYS@)%4YmLG~l3)oS1)DPNoR7Yqu9t10|_sp0&(0t5YBTkHWwb2)g66L&+2^~A7a1nGfF(l5VW9KvKXGjiRk_A{e!4dMwijC6= zjcSzD$|;M%NPu8+MEo4ew)()(w6?fZV**>%6l3bva}T*h*9YTaEX*7rrdE)nv%Jn9 zbW)2=A; zHFL%lqz#Eg&|vZyv9Kz&E@^1tjD}nDTp^b&wK6b`yCqy)KJ}I{H6!yUWKYa3PD!0L zd{DAFcFwfqtQ=d(2wBZbsYq8MWy$H&ZNcK4Dlv~_` zC3Q@`G;7x2@;Uj*%8=4fdh(3Q;?(4nTT5qo?AgfgdVNKtKRd5|wnP8PlO^6;{(~!W92TKIckfAe@lZ(f>l7=SNjjFB6_cS=_YH9|Ra;1}IPIWla?A20uf;%mF zcG$4n3A zWfgmdH;nU)8a%cZ0JUPQZH#-}hqzvUt{A0)Eme`_! z3F(v599r?L6!);%b7w`SmrBvBq+ug62WN=o%7nS8)q}@|a)!()o|K;D&zx8^J2*{B zPM@a0m&K&2hx8{tQFtVX+%7kQB`m~%OV>2Sj*`?E5NyAb`+UF+Mxra`%XXm*_ z4OO!FV1C*7YJSATz;N%Nf+=Nr-Xd>}b3|EDy*HV+dwlpyWrJr46Mb62>{*U6DM?wA z=R_KCnwMjGMu9YQa!OIEI=47`&IEUIN|DQ(Vsj488Z{|r%*2{OxtYORgtBOboH~6< z*f(@qP0@r=bEC78GRI9G=}Wm)bGcKiQnO~eM*Gt8C*w~EPfr<_Uz=p7F6o|c(>OH{6%BYKDBj=^7q${16o&GJ None: @pytest.mark.parametrize( # These images have an "ImageData" descriptor. "filename", - ("Tests/images/eps/1.eps", "Tests/images/eps/1_boundingbox_after_imagedata.eps"), + ( + "Tests/images/eps/1.eps", + "Tests/images/eps/1_boundingbox_after_imagedata.eps", + "Tests/images/eps/1_second_imagedata.eps", + ), ) def test_1(filename: str) -> None: with Image.open(filename) as im: From e2f996e2bd9b933a7a88b9ed47e6065a6d5a28b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 6 Oct 2024 08:55:15 +1100 Subject: [PATCH 303/652] Updated CI target --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 0e46ef0c7..00dec41d1 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -53,7 +53,7 @@ These platforms are built and tested for every change. | Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 | | | 3.12, 3.13, PyPy3 | | | +----------------------------+---------------------+ -| | 3.12 | x86 | +| | 3.13 | x86 | | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86-64 | | +----------------------------+---------------------+ From b77cd009e210cefae9424206f80ed5dc65b53fdf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 6 Oct 2024 11:30:27 +1100 Subject: [PATCH 304/652] Use transparency when combining P frames --- Tests/test_file_apng.py | 4 ++-- src/PIL/PngImagePlugin.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index e95850212..ee6c867c3 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -258,8 +258,8 @@ def test_apng_mode() -> None: assert im.mode == "P" im.seek(im.n_frames - 1) im = im.convert("RGBA") - assert im.getpixel((0, 0)) == (255, 0, 0, 0) - assert im.getpixel((64, 32)) == (255, 0, 0, 0) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: assert im.mode == "P" diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 28ade293e..4e1227204 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1063,6 +1063,12 @@ class PngImageFile(ImageFile.ImageFile): "RGBA", self.info["transparency"] ) else: + if self.im.mode == "P" and "transparency" in self.info: + t = self.info["transparency"] + if isinstance(t, bytes): + updated.putpalettealphas(t) + elif isinstance(t, int): + updated.putpalettealpha(t) mask = updated.convert("RGBA") self._prev_im.paste(updated, self.dispose_extent, mask) self.im = self._prev_im From 27c1bb265432cb5a1138e39165f0610e2d8a4e94 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Oct 2024 07:48:32 +1100 Subject: [PATCH 305/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 36266421a..cc891ed1d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Updated EPS mode when opening images without transparency #8281 + [Yay295, radarhere] + +- Use transparency when combining P frames from APNGs #8443 + [radarhere] + - Support all resampling filters when resizing I;16* images #8422 [radarhere] From feeceb9ae629199b69361d698154076734e3902a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Oct 2024 15:34:28 +1100 Subject: [PATCH 306/652] Simplified code --- src/_imagingmorph.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 614dfbe7f..fc35107b5 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -62,11 +62,8 @@ apply(PyObject *self, PyObject *args) { width = imgin->xsize; height = imgin->ysize; - if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { - PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); - return NULL; - } - if (imgout->type != IMAGING_TYPE_UINT8 || imgout->bands != 1) { + if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1 || + imgout->type != IMAGING_TYPE_UINT8 || imgout->bands != 1) { PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } From 76dd669e6bbe186f7362917b004ad2560d8b5368 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Oct 2024 16:50:05 +1100 Subject: [PATCH 307/652] Fixed unclosed file warning --- Tests/test_imageshow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 67c32783f..7a2f58767 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -117,5 +117,5 @@ def test_ipythonviewer() -> None: else: pytest.fail("IPythonViewer not found") - im = hopper() - assert test_viewer.show(im) == 1 + with hopper() as im: + assert test_viewer.show(im) == 1 From 8de66597f967baab1d4e4eaf487b0fdb4949abbb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Oct 2024 19:17:59 +1100 Subject: [PATCH 308/652] Removed unused code --- src/_imagingft.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 8bef45baf..3b30e268b 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -922,10 +922,7 @@ font_render(FontObject *self, PyObject *args) { width += ceil(stroke_width * 2 + x_start); height += ceil(stroke_width * 2 + y_start); image = PyObject_CallFunction(fill, "ii", width, height); - if (image == Py_None) { - PyMem_Del(glyph_info); - return Py_BuildValue("N(ii)", image, 0, 0); - } else if (image == NULL) { + if (image == NULL) { PyMem_Del(glyph_info); return NULL; } From 8e332eb3b06812adaa497463f0de97f9ec84d1cb Mon Sep 17 00:00:00 2001 From: Alexander Karpinsky Date: Mon, 7 Oct 2024 12:51:44 +0400 Subject: [PATCH 309/652] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_image_getim.py | 2 +- src/_imaging.c | 5 +++-- src/_imagingmorph.c | 4 ++-- src/libImaging/Imaging.h | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 75d97cd44..fa58492fc 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -16,4 +16,4 @@ def test_sanity() -> None: with pytest.warns(DeprecationWarning): ptrs = dict(im.im.unsafe_ptrs) - assert all(k in ptrs for k in ["image8", "image32", "image"]) + assert ptrs.keys() == {"image8", "image32", "image"} diff --git a/src/_imaging.c b/src/_imaging.c index 9422c42bf..d5eb69314 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3706,7 +3706,7 @@ static PyObject * _getattr_id(ImagingObject *self, void *closure) { if (PyErr_WarnEx( PyExc_DeprecationWarning, - "id property is deprecated and will be removed in Pillow 12.0", + "id property is deprecated and will be removed in Pillow 12 (2025-10-15)", 1 ) < 0) { return NULL; @@ -3732,7 +3732,8 @@ static PyObject * _getattr_unsafe_ptrs(ImagingObject *self, void *closure) { if (PyErr_WarnEx( PyExc_DeprecationWarning, - "unsafe_ptrs property is deprecated and will be removed in Pillow 12.0", + "unsafe_ptrs property is deprecated and will be removed in Pillow 12 " + "(2025-10-15)", 1 ) < 0) { return NULL; diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 637295be7..b4da47b77 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -29,8 +29,8 @@ static PyObject * apply(PyObject *self, PyObject *args) { const char *lut; - PyObject *i0, *i1; Py_ssize_t lut_len; + PyObject *i0, *i1; Imaging imgin, imgout; int width, height; int row_idx, col_idx; @@ -124,8 +124,8 @@ apply(PyObject *self, PyObject *args) { static PyObject * match(PyObject *self, PyObject *args) { const char *lut; - PyObject *i0; Py_ssize_t lut_len; + PyObject *i0; Imaging imgin; int width, height; int row_idx, col_idx; diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 870f5ed97..31052c68a 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -60,7 +60,7 @@ typedef struct ImagingHistogramInstance *ImagingHistogram; typedef struct ImagingOutlineInstance *ImagingOutline; typedef struct ImagingPaletteInstance *ImagingPalette; -/* handle magics (used with PyCObject). */ +/* handle magics (used with PyCapsule). */ #define IMAGING_MAGIC "Pillow Imaging" /* pixel types */ From a227f22fd979c86d8d2ae8b9bdb0a393fb2e386c Mon Sep 17 00:00:00 2001 From: Alexander Karpinsky Date: Mon, 7 Oct 2024 14:35:25 +0400 Subject: [PATCH 310/652] Apply suggestions from code review [ci skip] Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/deprecations.rst | 2 +- docs/releasenotes/11.0.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 3934f059d..25607e27c 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -165,7 +165,7 @@ Specific WebP Feature Checks ``True`` if the WebP module is installed, until they are removed in Pillow 12.0.0 (2025-10-15). -Get Internal Pointers to Objects +Get internal pointers to objects ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 11.0.0 diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 3194f1a52..fde11662e 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -73,7 +73,7 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ -Get Internal Pointers to Objects +Get internal pointers to objects ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 11.0.0 From 7097a9a3b869f2cf8d335aeb1a6a77c1b31378dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Oct 2024 22:12:41 +1100 Subject: [PATCH 311/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cc891ed1d..4e940d7ff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Use ImagingCore.ptr instead of ImagingCore.id #8341 + [homm, radarhere, hugovk] + - Updated EPS mode when opening images without transparency #8281 [Yay295, radarhere] From ef47d6e5e53c68c0018755d0dc23924d0e03c8ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:55:54 +0000 Subject: [PATCH 312/652] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.3 → v0.6.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.3...v0.6.9) - [github.com/PyCQA/bandit: 1.7.9 → 1.7.10](https://github.com/PyCQA/bandit/compare/1.7.9...1.7.10) - [github.com/pre-commit/mirrors-clang-format: v18.1.8 → v19.1.1](https://github.com/pre-commit/mirrors-clang-format/compare/v18.1.8...v19.1.1) - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0) - [github.com/python-jsonschema/check-jsonschema: 0.29.2 → 0.29.3](https://github.com/python-jsonschema/check-jsonschema/compare/0.29.2...0.29.3) - [github.com/sphinx-contrib/sphinx-lint: v0.9.1 → v1.0.0](https://github.com/sphinx-contrib/sphinx-lint/compare/v0.9.1...v1.0.0) - [github.com/tox-dev/pyproject-fmt: 2.2.1 → 2.2.4](https://github.com/tox-dev/pyproject-fmt/compare/2.2.1...2.2.4) - [github.com/abravalheri/validate-pyproject: v0.19 → v0.20.2](https://github.com/abravalheri/validate-pyproject/compare/v0.19...v0.20.2) - [github.com/tox-dev/tox-ini-fmt: 1.3.1 → 1.4.1](https://github.com/tox-dev/tox-ini-fmt/compare/1.3.1...1.4.1) --- .pre-commit-config.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14d75c689..bdc335f32 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.3 + rev: v0.6.9 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.7.9 + rev: 1.7.10 hooks: - id: bandit args: [--severity-level=high] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v18.1.8 + rev: v19.1.1 hooks: - id: clang-format types: [c] @@ -36,7 +36,7 @@ repos: - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable @@ -50,29 +50,29 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.2 + rev: 0.29.3 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v0.9.1 + rev: v1.0.0 hooks: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.2.1 + rev: 2.2.4 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.19 + rev: v0.20.2 hooks: - id: validate-pyproject - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.3.1 + rev: 1.4.1 hooks: - id: tox-ini-fmt From a609d65b33b659d5bbdd7ff09c6464d7b1028754 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:56:20 +0000 Subject: [PATCH 313/652] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/libImaging/QuantOctree.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index 7e02ebf65..ec60b214e 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -188,7 +188,7 @@ create_sorted_color_palette(const ColorCube cube) { buckets, cube->size, sizeof(struct _ColorBucket), - (int (*)(void const *, void const *)) & compare_bucket_count + (int (*)(void const *, void const *))&compare_bucket_count ); return buckets; From a9b9a63614c7838be9efe421062252c1c8c0546a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Oct 2024 20:48:32 +1100 Subject: [PATCH 314/652] Do not create core image in seek(), when load() might not run --- src/PIL/TiffImagePlugin.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index d4c46a797..f3571fda3 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1193,11 +1193,11 @@ class TiffImageFile(ImageFile.ImageFile): if not self._seek_check(frame): return self._seek(frame) - # Create a new core image object on second and - # subsequent frames in the image. Image may be - # different size/mode. - Image._decompression_bomb_check(self._tile_size) - self.im = Image.core.new(self.mode, self._tile_size) + if self._im is not None and ( + self.im.size != self._tile_size or self.im.mode != self.mode + ): + # The core image will no longer be used + self._im = None def _seek(self, frame: int) -> None: self.fp = self._fp @@ -1279,6 +1279,7 @@ class TiffImageFile(ImageFile.ImageFile): def load_prepare(self) -> None: if self._im is None: + Image._decompression_bomb_check(self._tile_size) self.im = Image.core.new(self.mode, self._tile_size) ImageFile.ImageFile.load_prepare(self) From e37209ace5bc9aaf3683dfaf25b6c6c75a23ba0f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:11:46 +0000 Subject: [PATCH 315/652] Update dependency cibuildwheel to v2.21.3 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index a1424ecc0..c4511439c 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.21.2 +cibuildwheel==2.21.3 From ff245fcb4084a0bb61c14e0c416c048c63da78cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Oct 2024 19:43:11 +1100 Subject: [PATCH 316/652] Revert "Skip QEMU-emulated wheels on workflow dispatch event" This reverts commit f39ca5db5a39c2c82e62fbee1ebf95d5b61bdfba. --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2a4c86cd7..837e5ea91 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -41,7 +41,7 @@ env: jobs: build-1-QEMU-emulated-wheels: - if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch' + if: github.event_name != 'schedule' name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} runs-on: ubuntu-latest strategy: From 97438cb8fd51f016117953072a7a68c2f74ccc74 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Oct 2024 22:50:26 +1100 Subject: [PATCH 317/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4e940d7ff..277a57f0c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Support ImageFilter.BuiltinFilter for I;16* images #8438 + [radarhere] + - Use ImagingCore.ptr instead of ImagingCore.id #8341 [homm, radarhere, hugovk] From fdd5d0c0fba5864d914044a8046e9916e2f0e1eb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:35:48 +0300 Subject: [PATCH 318/652] Use CVE and CWE roles from Sphinx 8.1 --- Makefile | 2 -- docs/Makefile | 2 +- docs/conf.py | 4 +--- pyproject.toml | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index ec4159627..53164b08a 100644 --- a/Makefile +++ b/Makefile @@ -17,12 +17,10 @@ coverage: .PHONY: doc .PHONY: html doc html: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs html .PHONY: htmlview htmlview: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs htmlview .PHONY: doccheck diff --git a/docs/Makefile b/docs/Makefile index 8f13f1aea..e90af0519 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -46,7 +46,7 @@ clean: -rm -rf $(BUILDDIR)/* install-sphinx: - $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinxext-opengraph + $(PYTHON) -m pip install -e ..[docs] .PHONY: html html: diff --git a/docs/conf.py b/docs/conf.py index a0f5867d7..bcdc18b20 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ import PIL # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "7.3" +needs_sphinx = "8.1" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -338,8 +338,6 @@ linkcheck_allowed_redirects = { # https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html _repo = "https://github.com/python-pillow/Pillow/" extlinks = { - "cve": ("https://www.cve.org/CVERecord?id=CVE-%s", "CVE-%s"), - "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"), "issue": (_repo + "issues/%s", "#%s"), "pr": (_repo + "pull/%s", "#%s"), "pypi": ("https://pypi.org/project/%s/", "%s"), diff --git a/pyproject.toml b/pyproject.toml index 0d0a6f170..132030c99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dynamic = [ optional-dependencies.docs = [ "furo", "olefile", - "sphinx>=7.3", + "sphinx>=8.1", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph", From 7edf9528328f64bf7b299babe2fc3d9e79afdc18 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Oct 2024 19:37:56 +1100 Subject: [PATCH 319/652] Do not close provided file handles with libtiff --- src/PIL/TiffImagePlugin.py | 7 +------ src/libImaging/TiffDecode.c | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index d4c46a797..2dbd3c601 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1864,7 +1864,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if hasattr(fp, "fileno"): try: fp.seek(0) - _fp = os.dup(fp.fileno()) + _fp = fp.fileno() except io.UnsupportedOperation: pass @@ -1943,11 +1943,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(data) if errcode: break - if _fp: - try: - os.close(_fp) - except OSError: - pass if errcode < 0: msg = f"encoder error {errcode} when writing image file" raise OSError(msg) diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 18a54f633..e4da9162d 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -780,7 +780,7 @@ ImagingLibTiffDecode( decode_err: // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup if (clientstate->fp) { - // Pillow will manage the closing of the file rather than libtiff + // Python will manage the closing of the file rather than libtiff // So only call TIFFCleanup TIFFCleanup(tiff); } else { @@ -1008,7 +1008,17 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt ) == -1) { TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; - TIFFClose(tiff); + + // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup + if (clientstate->fp) { + // Python will manage the closing of the file rather than libtiff + // So only call TIFFCleanup + TIFFCleanup(tiff); + } else { + // When tif_closeproc refers to our custom _tiffCloseProc though, + // that is fine, as it does not close the file + TIFFClose(tiff); + } if (!clientstate->fp) { free(clientstate->data); } @@ -1025,14 +1035,22 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Error flushing the tiff")); // likely reason is memory. state->errcode = IMAGING_CODEC_MEMORY; - TIFFClose(tiff); + if (clientstate->fp) { + TIFFCleanup(tiff); + } else { + TIFFClose(tiff); + } if (!clientstate->fp) { free(clientstate->data); } return -1; } TRACE(("Closing \n")); - TIFFClose(tiff); + if (clientstate->fp) { + TIFFCleanup(tiff); + } else { + TIFFClose(tiff); + } // reset the clientstate metadata to use it to read out the buffer. clientstate->loc = 0; clientstate->size = clientstate->eof; // redundant? From afbf45055a8e938aacb28cb412cad125a1e29a4f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Oct 2024 20:59:15 +1100 Subject: [PATCH 320/652] Always raise warnings for deprecated feature checks --- Tests/test_features.py | 6 +++--- src/PIL/features.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 807782847..ed7929973 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -56,17 +56,17 @@ def test_version() -> None: def test_webp_transparency() -> None: with pytest.warns(DeprecationWarning): - assert features.check("transp_webp") == features.check_module("webp") + assert (features.check("transp_webp") or False) == features.check_module("webp") def test_webp_mux() -> None: with pytest.warns(DeprecationWarning): - assert features.check("webp_mux") == features.check_module("webp") + assert (features.check("webp_mux") or False) == features.check_module("webp") def test_webp_anim() -> None: with pytest.warns(DeprecationWarning): - assert features.check("webp_anim") == features.check_module("webp") + assert (features.check("webp_anim") or False) == features.check_module("webp") @skip_unless_feature("libjpeg_turbo") diff --git a/src/PIL/features.py b/src/PIL/features.py index 24c5ee978..75d59e01c 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -146,10 +146,11 @@ def check_feature(feature: str) -> bool | None: module, flag, ver = features[feature] + if isinstance(flag, bool): + deprecate(f'check_feature("{feature}")', 12) try: imported_module = __import__(module, fromlist=["PIL"]) if isinstance(flag, bool): - deprecate(f'check_feature("{feature}")', 12) return flag return getattr(imported_module, flag) except ModuleNotFoundError: From e93dcc15780d29af45fa64cf54ff6b11c2c27c46 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Oct 2024 21:09:52 +1100 Subject: [PATCH 321/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 277a57f0c..c965d60a6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Do not close provided file handles with libtiff when saving #8458 + [radarhere] + - Support ImageFilter.BuiltinFilter for I;16* images #8438 [radarhere] From 00c5989d4ed963f9d39568c6212969bafe84e4d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Oct 2024 13:44:48 +1100 Subject: [PATCH 322/652] Use MAXBLOCK --- src/PIL/TiffImagePlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2dbd3c601..a05cd0f17 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1937,8 +1937,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) encoder.setimage(im.im, (0, 0) + im.size) while True: - # undone, change to self.decodermaxblock: - errcode, data = encoder.encode(16 * 1024)[1:] + errcode, data = encoder.encode(ImageFile.MAXBLOCK)[1:] if not _fp: fp.write(data) if errcode: From b5e1115bf2646f7ce5eb1b9409e4540fee3b9f49 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Oct 2024 21:10:47 +1100 Subject: [PATCH 323/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c965d60a6..0a1703668 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Conditionally define ImageCms type hint to avoid requiring core #8197 + [radarhere] + +- Support writing LONG8 offsets in AppendingTiffWriter #8417 + [radarhere] + +- Use ImageFile.MAXBLOCK when saving TIFF images #8461 + [radarhere] + - Do not close provided file handles with libtiff when saving #8458 [radarhere] From e74994ed370a668c8f3e6045278de5a5eb3f47ab Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:28:41 +0300 Subject: [PATCH 324/652] Update licence to MIT-CMU --- .pre-commit-config.yaml | 1 + pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bdc335f32..6254b8941 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,6 +70,7 @@ repos: rev: v0.20.2 hooks: - id: validate-pyproject + additional_dependencies: [trove-classifiers>=2024.10.12] - repo: https://github.com/tox-dev/tox-ini-fmt rev: 1.4.1 diff --git a/pyproject.toml b/pyproject.toml index 132030c99..c55be7693 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,14 +14,14 @@ readme = "README.md" keywords = [ "Imaging", ] -license = { text = "HPND" } +license = { text = "MIT-CMU" } authors = [ { name = "Jeffrey A. Clark", email = "aclark@aclark.net" }, ] requires-python = ">=3.9" classifiers = [ "Development Status :: 6 - Mature", - "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", + "License :: OSI Approved :: CMU License (MIT-CMU)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", From a60610c93f1a7fb2738156470d613793e5fa7a89 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 13 Oct 2024 07:42:17 +1100 Subject: [PATCH 325/652] Added type hints --- src/PIL/ImageMath.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 15464947d..484797f91 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -173,10 +173,10 @@ class _Operand: return self.apply("rshift", self, other) # logical - def __eq__(self, other): + def __eq__(self, other: _Operand | float) -> _Operand: # type: ignore[override] return self.apply("eq", self, other) - def __ne__(self, other): + def __ne__(self, other: _Operand | float) -> _Operand: # type: ignore[override] return self.apply("ne", self, other) def __lt__(self, other: _Operand | float) -> _Operand: From c3d81d6375d2d78301a40d0e55ece746cf41502f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 12 Oct 2024 23:52:57 +0300 Subject: [PATCH 326/652] Update Python 3.13 release date --- docs/releasenotes/11.0.0.rst | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index d5f5934f4..c3f18140f 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -1,19 +1,6 @@ 11.0.0 ------ -Security -======== - -TODO -^^^^ - -TODO - -:cve:`YYYY-XXXXX`: TODO -^^^^^^^^^^^^^^^^^^^^^^^ - -TODO - Backwards Incompatible Changes ============================== @@ -159,7 +146,7 @@ Python 3.13 Pillow 10.4.0 had wheels built against Python 3.13 beta, available as a preview to help others prepare for 3.13, and to ensure Pillow could be used immediately at the release -of 3.13.0 final (2024-10-01, :pep:`719`). +of 3.13.0 final (2024-10-07, :pep:`719`). Pillow 11.0.0 now officially supports Python 3.13. From f2cc87b1f0627dfce07ab2867e29ba07e5055f33 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 13 Oct 2024 11:19:41 +1100 Subject: [PATCH 327/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0a1703668..fcc8c324f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Update licence to MIT-CMU #8460 + [hugovk] + - Conditionally define ImageCms type hint to avoid requiring core #8197 [radarhere] From 3911afbc97f5b3195609800f471794f6b3e349dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 14 Oct 2024 16:56:48 +1100 Subject: [PATCH 328/652] Removed Fedora 39 --- .github/workflows/test-docker.yml | 1 - docs/installation/platform-support.rst | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 21e1275e7..880fe3eea 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -46,7 +46,6 @@ jobs: centos-stream-9-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, - fedora-39-amd64, fedora-40-amd64, gentoo, ubuntu-22.04-jammy-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 00dec41d1..21ebd1ad5 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -29,8 +29,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ -| Fedora 39 | 3.12 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 40 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | From 204aae6682fc936f5350b3fe70335776f81480a7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 15 Oct 2024 08:55:00 +0300 Subject: [PATCH 329/652] 11.0.0 version bump --- CHANGES.rst | 2 +- src/PIL/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index fcc8c324f..8a20fe16a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,7 +2,7 @@ Changelog (Pillow) ================== -11.0.0 (unreleased) +11.0.0 (2024-10-15) ------------------- - Update licence to MIT-CMU #8460 diff --git a/src/PIL/_version.py b/src/PIL/_version.py index c4a72ad7e..963d8c906 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "11.0.0.dev0" +__version__ = "11.0.0" From a433f24f0bf5a9df98bbfe3c076a060131e92854 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 15 Oct 2024 22:14:26 +1100 Subject: [PATCH 330/652] tile is already an empty list --- src/PIL/GifImagePlugin.py | 1 - src/PIL/WebPImagePlugin.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 57c291792..a7c4f8b2c 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -103,7 +103,6 @@ class GifImageFile(ImageFile.ImageFile): self.info["version"] = s[:6] self._size = i16(s, 6), i16(s, 8) - self.tile = [] flags = s[10] bits = (flags & 7) + 1 diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 64188f28c..c7f855527 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -60,7 +60,6 @@ class WebPImageFile(ImageFile.ImageFile): self.is_animated = self.n_frames > 1 self._mode = "RGB" if mode == "RGBX" else mode self.rawmode = mode - self.tile = [] # Attempt to read ICC / EXIF / XMP chunks from file icc_profile = self._decoder.get_chunk("ICCP") From 455e6da119ac01ffc3aeb68486cef47e1f3f8dcb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:30:25 +0300 Subject: [PATCH 331/652] 11.1.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 963d8c906..0807f949c 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "11.0.0" +__version__ = "11.1.0.dev0" From 8393972c3540cf7bba737b0687d79c5d8ebca3ef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Oct 2024 09:56:33 +1100 Subject: [PATCH 332/652] Skip PyPy3.9 wheels --- .github/workflows/wheels.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 837e5ea91..34452fa56 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -152,6 +152,7 @@ jobs: CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_PRERELEASE_PYTHONS: True + CIBW_SKIP: pp39-* MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - uses: actions/upload-artifact@v4 @@ -224,6 +225,7 @@ jobs: CIBW_CACHE_PATH: "C:\\cibw" CIBW_FREE_THREADED_SUPPORT: True CIBW_PRERELEASE_PYTHONS: True + CIBW_SKIP: pp39-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm -v {project}:C:\pillow From 7c194b54ef3e7a04c5b669353622687075b5ebb4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Oct 2024 21:37:14 +1100 Subject: [PATCH 333/652] Fixed disabling a feature --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 60707083f..def341784 100644 --- a/setup.py +++ b/setup.py @@ -389,7 +389,7 @@ class pil_build_ext(build_ext): pass for x in self.feature: if getattr(self, f"disable_{x}"): - setattr(self.feature, x, False) + self.feature.set(x, False) self.feature.required.discard(x) _dbg("Disabling %s", x) if getattr(self, f"enable_{x}"): From 11c654c187ffbede40c54e6344ddec49f495d4a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Oct 2024 23:07:35 +1100 Subject: [PATCH 334/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8a20fe16a..ff126c611 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +11.1.0 (unreleased) +------------------- + +- Fixed disabling a feature during install #8469 + [radarhere] + 11.0.0 (2024-10-15) ------------------- From c252b708ee7f90dbc3b6a86056f390b7b4712f52 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:41:38 +0000 Subject: [PATCH 335/652] Update dependency mypy to v1.12.0 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index dcb3996e2..852444d37 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.11.2 +mypy==1.12.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 5ff20273d90c1be49a9662cf6d4c53c9bd53aadb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Oct 2024 10:46:26 +1100 Subject: [PATCH 336/652] Updated type hints --- src/PIL/TiffImagePlugin.py | 46 +++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ff5a6f9e9..198bd422e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -685,22 +685,33 @@ class ImageFileDirectory_v2(_IFDv2Base): else: self.tagtype[tag] = TiffTags.UNDEFINED if all(isinstance(v, IFDRational) for v in values): - self.tagtype[tag] = ( - TiffTags.RATIONAL - if all(v >= 0 for v in values) - else TiffTags.SIGNED_RATIONAL - ) - elif all(isinstance(v, int) for v in values): - if all(0 <= v < 2**16 for v in values): - self.tagtype[tag] = TiffTags.SHORT - elif all(-(2**15) < v < 2**15 for v in values): - self.tagtype[tag] = TiffTags.SIGNED_SHORT + for v in values: + assert isinstance(v, IFDRational) + if v < 0: + self.tagtype[tag] = TiffTags.SIGNED_RATIONAL + break else: - self.tagtype[tag] = ( - TiffTags.LONG - if all(v >= 0 for v in values) - else TiffTags.SIGNED_LONG - ) + self.tagtype[tag] = TiffTags.RATIONAL + elif all(isinstance(v, int) for v in values): + short = True + signed_short = True + long = True + for v in values: + assert isinstance(v, int) + if short and not (0 <= v < 2**16): + short = False + if signed_short and not (-(2**15) < v < 2**15): + signed_short = False + if long and v < 0: + long = False + if short: + self.tagtype[tag] = TiffTags.SHORT + elif signed_short: + self.tagtype[tag] = TiffTags.SIGNED_SHORT + elif long: + self.tagtype[tag] = TiffTags.LONG + else: + self.tagtype[tag] = TiffTags.SIGNED_LONG elif all(isinstance(v, float) for v in values): self.tagtype[tag] = TiffTags.DOUBLE elif all(isinstance(v, str) for v in values): @@ -718,7 +729,10 @@ class ImageFileDirectory_v2(_IFDv2Base): is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) if not is_ifd: - values = tuple(info.cvt_enum(value) for value in values) + values = tuple( + info.cvt_enum(value) if isinstance(value, str) else value + for value in values + ) dest = self._tags_v1 if legacy_api else self._tags_v2 From 4611a246618b1b3e09efc829e36205184376dc75 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Oct 2024 10:58:48 +1100 Subject: [PATCH 337/652] Fix IFDRational with a zero denominator --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 198bd422e..6bf39b75a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -294,7 +294,7 @@ def _accept(prefix: bytes) -> bool: def _limit_rational( val: float | Fraction | IFDRational, max_val: int ) -> tuple[IntegralLike, IntegralLike]: - inv = abs(float(val)) > 1 + inv = abs(val) > 1 n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) return n_d[::-1] if inv else n_d From 0beb2228f9adb387a87c6b944cda2e427f5f264a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Oct 2024 12:44:25 +1100 Subject: [PATCH 338/652] Include JpegImageFile layers in state --- Tests/test_pickle.py | 11 +++++++++++ src/PIL/Image.py | 2 +- src/PIL/JpegImagePlugin.py | 7 +++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index d250ba369..c4f8de013 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -74,6 +74,17 @@ def test_pickle_image( helper_pickle_file(tmp_path, protocol, test_file, test_mode) +def test_pickle_jpeg() -> None: + # Arrange + with Image.open("Tests/images/hopper.jpg") as image: + # Act: roundtrip + unpickled_image = pickle.loads(pickle.dumps(image)) + + # Assert + assert len(unpickled_image.layer) == 3 + assert unpickled_image.layers == 3 + + def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange filename = str(tmp_path / "temp.pkl") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 44270392c..ec5ce6cb1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -763,7 +763,7 @@ class Image: def __setstate__(self, state: list[Any]) -> None: Image.__init__(self) - info, mode, size, palette, data = state + info, mode, size, palette, data = state[:5] self.info = info self._mode = mode self._size = size diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6510e072e..7dec75218 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -395,6 +395,13 @@ class JpegImageFile(ImageFile.ImageFile): return getattr(self, "_" + name) raise AttributeError(name) + def __getstate__(self) -> list[Any]: + return super().__getstate__() + [self.layers, self.layer] + + def __setstate__(self, state: list[Any]) -> None: + super().__setstate__(state) + self.layers, self.layer = state[5:] + def load_read(self, read_bytes: int) -> bytes: """ internal: read more image data From d59b169ed257ca14d86c90dbf3384b8b3999fc5c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Oct 2024 23:10:48 +1100 Subject: [PATCH 339/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ff126c611..ae3841569 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Fix IFDRational with a zero denominator #8474 + [radarhere] + - Fixed disabling a feature during install #8469 [radarhere] From a337138f92d88d56488e5150d7ce8a3e0f0a91f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 08:32:14 +1100 Subject: [PATCH 340/652] Updated type hint --- Tests/test_image_resize.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 8548fb5da..57fcf9a34 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -10,7 +10,7 @@ from pathlib import Path import pytest -from PIL import Image +from PIL import Image, ImageFile from .helper import ( assert_image_equal, @@ -179,7 +179,7 @@ class TestImagingCoreResize: @pytest.fixture -def gradients_image() -> Generator[Image.Image, None, None]: +def gradients_image() -> Generator[ImageFile.ImageFile, None, None]: with Image.open("Tests/images/radial_gradients.png") as im: im.load() try: @@ -189,7 +189,7 @@ def gradients_image() -> Generator[Image.Image, None, None]: class TestReducingGapResize: - def test_reducing_gap_values(self, gradients_image: Image.Image) -> None: + def test_reducing_gap_values(self, gradients_image: ImageFile.ImageFile) -> None: ref = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, reducing_gap=None ) @@ -210,7 +210,7 @@ class TestReducingGapResize: ) def test_reducing_gap_1( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -230,7 +230,7 @@ class TestReducingGapResize: ) def test_reducing_gap_2( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -250,7 +250,7 @@ class TestReducingGapResize: ) def test_reducing_gap_3( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -266,7 +266,9 @@ class TestReducingGapResize: @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) def test_reducing_gap_8( - self, gradients_image: Image.Image, box: tuple[float, float, float, float] + self, + gradients_image: ImageFile.ImageFile, + box: tuple[float, float, float, float], ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( @@ -281,7 +283,7 @@ class TestReducingGapResize: ) def test_box_filter( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: From 98f975dbbe6ae0ef521522cdea6e00fdc81449c0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 18:56:23 +1100 Subject: [PATCH 341/652] Do not save XMP from info --- Tests/test_file_jpeg.py | 9 +++++++-- src/PIL/JpegImagePlugin.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index cde951395..1b889eb40 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -998,8 +998,13 @@ class TestFileJpeg: with Image.open(f) as reloaded: assert reloaded.info["xmp"] == b"XMP test" - im.info["xmp"] = b"1" * 65504 - im.save(f) + # Check that XMP is not saved from image info + reloaded.save(f) + + with Image.open(f) as reloaded: + assert "xmp" not in reloaded.info + + im.save(f, xmp=b"1" * 65504) with Image.open(f) as reloaded: assert reloaded.info["xmp"] == b"1" * 65504 diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6510e072e..6937f2650 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -751,7 +751,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: extra = info.get("extra", b"") MAX_BYTES_IN_MARKER = 65533 - xmp = info.get("xmp", im.info.get("xmp")) + xmp = info.get("xmp") if xmp: overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len From 203ca12626b22d185b59199b60ceb55081f6c3b2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 19:09:22 +1100 Subject: [PATCH 342/652] Allow encoderinfo to be set for appended images --- Tests/test_file_mpo.py | 12 ++++++++++++ src/PIL/Image.py | 7 ++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index e0f42a266..39aaa1613 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -293,3 +293,15 @@ def test_save_all() -> None: # Test that a single frame image will not be saved as an MPO jpg = roundtrip(im, save_all=True) assert "mp" not in jpg.info + + +def test_save_xmp() -> None: + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im2.encoderinfo = {"xmp": b"Second frame"} + im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2]) + + assert im_reloaded.info["xmp"] == b"First frame" + + im_reloaded.seek(1) + assert im_reloaded.info["xmp"] == b"Second frame" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 44270392c..586da7d3d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2565,7 +2565,7 @@ class Image: self._ensure_mutable() save_all = params.pop("save_all", False) - self.encoderinfo = params + self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} self.encoderconfig: tuple[Any, ...] = () preinit() @@ -2612,6 +2612,11 @@ class Image: except PermissionError: pass raise + finally: + try: + del self.encoderinfo + except AttributeError: + pass if open_fp: fp.close() From 5b065970753d464c0d283f9e0c2f4a0971800ed1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 19:29:22 +1100 Subject: [PATCH 343/652] Use fixture to re-open image for each test --- Tests/test_file_jpeg2k.py | 57 +++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 26b085601..79f53e211 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -2,6 +2,7 @@ from __future__ import annotations import os import re +from collections.abc import Generator from io import BytesIO from pathlib import Path from typing import Any @@ -29,8 +30,16 @@ EXTRA_DIR = "Tests/images/jpeg2000" pytestmark = skip_unless_feature("jpg_2000") -test_card = Image.open("Tests/images/test-card.png") -test_card.load() + +@pytest.fixture +def test_card() -> Generator[ImageFile.ImageFile, None, None]: + with Image.open("Tests/images/test-card.png") as im: + im.load() + try: + yield im + finally: + im.close() + # OpenJPEG 2.0.0 outputs this debugging message sometimes; we should # ignore it---it doesn't represent a test failure. @@ -74,7 +83,7 @@ def test_invalid_file() -> None: Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) -def test_bytesio() -> None: +def test_bytesio(test_card: ImageFile.ImageFile) -> None: with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) with Image.open(data) as im: @@ -86,7 +95,7 @@ def test_bytesio() -> None: # PIL (they were made using Adobe Photoshop) -def test_lossless(tmp_path: Path) -> None: +def test_lossless(test_card: ImageFile.ImageFile, tmp_path: Path) -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() outfile = str(tmp_path / "temp_test-card.png") @@ -94,54 +103,56 @@ def test_lossless(tmp_path: Path) -> None: assert_image_similar(im, test_card, 1.0e-3) -def test_lossy_tiled() -> None: +def test_lossy_tiled(test_card: ImageFile.ImageFile) -> None: assert_image_similar_tofile( test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 ) -def test_lossless_rt() -> None: +def test_lossless_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card) assert_image_equal(im, test_card) -def test_lossy_rt() -> None: +def test_lossy_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_tiled_rt() -> None: +def test_tiled_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, tile_size=(128, 128)) assert_image_equal(im, test_card) -def test_tiled_offset_rt() -> None: +def test_tiled_offset_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) assert_image_equal(im, test_card) -def test_tiled_offset_too_small() -> None: +def test_tiled_offset_too_small(test_card: ImageFile.ImageFile) -> None: with pytest.raises(ValueError): roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) -def test_irreversible_rt() -> None: +def test_irreversible_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, irreversible=True, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_prog_qual_rt() -> None: +def test_prog_qual_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") assert_image_similar(im, test_card, 2.0) -def test_prog_res_rt() -> None: +def test_prog_res_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, num_resolutions=8, progression="RLCP") assert_image_equal(im, test_card) @pytest.mark.parametrize("num_resolutions", range(2, 6)) -def test_default_num_resolutions(num_resolutions: int) -> None: +def test_default_num_resolutions( + test_card: ImageFile.ImageFile, num_resolutions: int +) -> None: d = 1 << (num_resolutions - 1) im = test_card.resize((d - 1, d - 1)) with pytest.raises(OSError): @@ -205,7 +216,7 @@ def test_header_errors() -> None: pass -def test_layers_type(tmp_path: Path) -> None: +def test_layers_type(test_card: ImageFile.ImageFile, tmp_path: Path) -> None: outfile = str(tmp_path / "temp_layers.jp2") for quality_layers in [[100, 50, 10], (100, 50, 10), None]: test_card.save(outfile, quality_layers=quality_layers) @@ -215,7 +226,7 @@ def test_layers_type(tmp_path: Path) -> None: test_card.save(outfile, quality_layers=quality_layers_str) -def test_layers() -> None: +def test_layers(test_card: ImageFile.ImageFile) -> None: out = BytesIO() test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") out.seek(0) @@ -245,7 +256,13 @@ def test_layers() -> None: (None, {"no_jp2": False}, 4, b"jP"), ), ) -def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: +def test_no_jp2( + test_card: ImageFile.ImageFile, + name: str, + args: dict[str, bool], + offset: int, + data: bytes, +) -> None: out = BytesIO() if name: out.name = name @@ -254,7 +271,7 @@ def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> N assert out.read(2) == data -def test_mct() -> None: +def test_mct(test_card: ImageFile.ImageFile) -> None: # Three component for val in (0, 1): out = BytesIO() @@ -419,7 +436,7 @@ def test_comment() -> None: pass -def test_save_comment() -> None: +def test_save_comment(test_card: ImageFile.ImageFile) -> None: for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() test_card.save(out, "JPEG2000", comment=comment) @@ -457,7 +474,7 @@ def test_crashes(test_file: str) -> None: @skip_unless_feature_version("jpg_2000", "2.4.0") -def test_plt_marker() -> None: +def test_plt_marker(test_card: ImageFile.ImageFile) -> None: # Search the start of the codesteam for PLT out = BytesIO() test_card.save(out, "JPEG2000", no_jp2=True, plt=True) From 55579084cd57461517dfe77d7804dfa24219a9f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Oct 2024 20:40:13 +1100 Subject: [PATCH 344/652] Corrected EMF DPI --- Tests/test_file_wmf.py | 7 +++++++ src/PIL/WmfImagePlugin.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 79e707263..424640d7b 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path from typing import IO @@ -61,6 +62,12 @@ def test_load_float_dpi() -> None: with Image.open("Tests/images/drawing.emf") as im: assert im.info["dpi"] == 1423.7668161434979 + with open("Tests/images/drawing.emf", "rb") as fp: + data = fp.read() + b = BytesIO(data[:8] + b"\x06\xFA" + data[10:]) + with Image.open(b) as im: + assert im.info["dpi"][0] == 2540 + def test_load_set_dpi() -> None: with Image.open("Tests/images/drawing.wmf") as im: diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 68f8a74f5..cad6c98d5 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -128,7 +128,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): size = x1 - x0, y1 - y0 # calculate dots per inch from bbox and frame - xdpi = 2540.0 * (x1 - y0) / (frame[2] - frame[0]) + xdpi = 2540.0 * (x1 - x0) / (frame[2] - frame[0]) ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) self.info["wmf_bbox"] = x0, y0, x1, y1 From beb32bbb1f9bb713a840421e3e9f5191d5f51944 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 00:18:00 +0000 Subject: [PATCH 345/652] Update dependency mypy to v1.12.1 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 852444d37..047307bb5 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.12.0 +mypy==1.12.1 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 22c05e232c67343b193b9fdeca9e7b683d470bc6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 22 Oct 2024 17:56:02 +1100 Subject: [PATCH 346/652] Update license to MIT-CMU --- LICENSE | 2 +- docs/about.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 7990a6e57..8837c290c 100644 --- a/LICENSE +++ b/LICENSE @@ -7,7 +7,7 @@ Pillow is the friendly PIL fork. It is Copyright © 2010-2024 by Jeffrey A. Clark and contributors -Like PIL, Pillow is licensed under the open source HPND License: +Like PIL, Pillow is licensed under the open source MIT-CMU License: By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply diff --git a/docs/about.rst b/docs/about.rst index 98cdd8e5a..c51ddebd0 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -18,7 +18,7 @@ The fork author's goal is to foster and support active development of PIL throug License ------- -Like PIL, Pillow is `licensed under the open source HPND License `_ +Like PIL, Pillow is `licensed under the open source MIT-CMU License `_ Why a fork? ----------- From 6a55f2df0346e3d97515ee46e4dbb966886fdc94 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 02:36:15 +0000 Subject: [PATCH 347/652] Update dependency mypy to v1.13.0 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 047307bb5..c84a3533b 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.12.1 +mypy==1.13.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 8af2d7640ead3ddeba9fdc598dc8e72d580dfdfb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Oct 2024 23:26:13 +1100 Subject: [PATCH 348/652] Pass palette mode to putpalette --- Tests/test_file_gif.py | 6 ++++-- src/PIL/GifImagePlugin.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 16c8466f3..4eb8c0d02 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -4,6 +4,7 @@ import warnings from collections.abc import Generator from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -1431,7 +1432,8 @@ def test_saving_rgba(tmp_path: Path) -> None: assert reloaded_rgba.load()[0, 0][3] == 0 -def test_optimizing_p_rgba(tmp_path: Path) -> None: +@pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) +def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: out = str(tmp_path / "temp.gif") im1 = Image.new("P", (100, 100)) @@ -1443,7 +1445,7 @@ def test_optimizing_p_rgba(tmp_path: Path) -> None: im2 = Image.new("P", (100, 100)) im2.putpalette(data, "RGBA") - im1.save(out, save_all=True, append_images=[im2]) + im1.save(out, save_all=True, append_images=[im2], **params) with Image.open(out) as reloaded: assert reloaded.n_frames == 2 diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index a7c4f8b2c..47022d584 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -695,8 +695,9 @@ def _write_multiple_frames( ) background = _get_background(im_frame, color) background_im = Image.new("P", im_frame.size, background) - assert im_frames[0].im.palette is not None - background_im.putpalette(im_frames[0].im.palette) + first_palette = im_frames[0].im.palette + assert first_palette is not None + background_im.putpalette(first_palette, first_palette.mode) bbox = _getbbox(background_im, im_frame)[1] elif encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: From c8e301c47456114f245261fccd316b4fd8964f3e Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 24 Oct 2024 15:52:59 +0200 Subject: [PATCH 349/652] Fix SEGFAULT from calling FT_New_Face/FT_Done_Face in multiple threads --- src/_imagingft.c | 12 +- src/thirdparty/pythoncapi_compat.h | 341 ++++++++++++++++++++++++++++- 2 files changed, 351 insertions(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 7f2464124..6fed821bc 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -81,7 +81,10 @@ struct { /* -------------------------------------------------------------------- */ /* font objects */ - static FT_Library library; +static FT_Library library; +#ifdef Py_GIL_DISABLED +PyMutex ft_library_mutex; +#endif typedef struct { PyObject_HEAD FT_Face face; @@ -187,7 +190,9 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { if (filename && font_bytes_size <= 0) { self->font_bytes = NULL; + MUTEX_LOCK(&ft_library_mutex); error = FT_New_Face(library, filename, index, &self->face); + MUTEX_UNLOCK(&ft_library_mutex); } else { /* need to have allocated storage for font_bytes for the life of the object.*/ /* Don't free this before FT_Done_Face */ @@ -197,6 +202,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { } if (!error) { memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); + MUTEX_LOCK(&ft_library_mutex); error = FT_New_Memory_Face( library, (FT_Byte *)self->font_bytes, @@ -204,6 +210,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { index, &self->face ); + MUTEX_UNLOCK(&ft_library_mutex); } } @@ -1433,7 +1440,9 @@ font_setvaraxes(FontObject *self, PyObject *args) { static void font_dealloc(FontObject *self) { if (self->face) { + MUTEX_LOCK(&ft_library_mutex); FT_Done_Face(self->face); + MUTEX_UNLOCK(&ft_library_mutex); } if (self->font_bytes) { PyMem_Free(self->font_bytes); @@ -1639,6 +1648,7 @@ PyInit__imagingft(void) { } #ifdef Py_GIL_DISABLED + ft_library_mutex = (PyMutex) {0}; PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif diff --git a/src/thirdparty/pythoncapi_compat.h b/src/thirdparty/pythoncapi_compat.h index 51e8c0de7..ca23d5ffa 100644 --- a/src/thirdparty/pythoncapi_compat.h +++ b/src/thirdparty/pythoncapi_compat.h @@ -7,7 +7,10 @@ // https://github.com/python/pythoncapi_compat // // Latest version: -// https://raw.githubusercontent.com/python/pythoncapi_compat/master/pythoncapi_compat.h +// https://raw.githubusercontent.com/python/pythoncapi-compat/main/pythoncapi_compat.h +// +// This file was vendored from the following commit: +// https://github.com/python/pythoncapi-compat/commit/0041177c4f348c8952b4c8980b2c90856e61c7c7 // // SPDX-License-Identifier: 0BSD @@ -45,6 +48,13 @@ extern "C" { # define _PyObject_CAST(op) _Py_CAST(PyObject*, op) #endif +#ifndef Py_BUILD_ASSERT +# define Py_BUILD_ASSERT(cond) \ + do { \ + (void)sizeof(char [1 - 2 * !(cond)]); \ + } while(0) +#endif + // bpo-42262 added Py_NewRef() to Python 3.10.0a3 #if PY_VERSION_HEX < 0x030A00A3 && !defined(Py_NewRef) @@ -1338,6 +1348,166 @@ PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, } #endif +#if PY_VERSION_HEX < 0x030D00B3 +# define Py_BEGIN_CRITICAL_SECTION(op) { +# define Py_END_CRITICAL_SECTION() } +# define Py_BEGIN_CRITICAL_SECTION2(a, b) { +# define Py_END_CRITICAL_SECTION2() } +#endif + +#if PY_VERSION_HEX < 0x030E0000 && PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) +typedef struct PyUnicodeWriter PyUnicodeWriter; + +static inline void PyUnicodeWriter_Discard(PyUnicodeWriter *writer) +{ + _PyUnicodeWriter_Dealloc((_PyUnicodeWriter*)writer); + PyMem_Free(writer); +} + +static inline PyUnicodeWriter* PyUnicodeWriter_Create(Py_ssize_t length) +{ + if (length < 0) { + PyErr_SetString(PyExc_ValueError, + "length must be positive"); + return NULL; + } + + const size_t size = sizeof(_PyUnicodeWriter); + PyUnicodeWriter *pub_writer = (PyUnicodeWriter *)PyMem_Malloc(size); + if (pub_writer == _Py_NULL) { + PyErr_NoMemory(); + return _Py_NULL; + } + _PyUnicodeWriter *writer = (_PyUnicodeWriter *)pub_writer; + + _PyUnicodeWriter_Init(writer); + if (_PyUnicodeWriter_Prepare(writer, length, 127) < 0) { + PyUnicodeWriter_Discard(pub_writer); + return NULL; + } + writer->overallocate = 1; + return pub_writer; +} + +static inline PyObject* PyUnicodeWriter_Finish(PyUnicodeWriter *writer) +{ + PyObject *str = _PyUnicodeWriter_Finish((_PyUnicodeWriter*)writer); + assert(((_PyUnicodeWriter*)writer)->buffer == NULL); + PyMem_Free(writer); + return str; +} + +static inline int +PyUnicodeWriter_WriteChar(PyUnicodeWriter *writer, Py_UCS4 ch) +{ + if (ch > 0x10ffff) { + PyErr_SetString(PyExc_ValueError, + "character must be in range(0x110000)"); + return -1; + } + + return _PyUnicodeWriter_WriteChar((_PyUnicodeWriter*)writer, ch); +} + +static inline int +PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj) +{ + PyObject *str = PyObject_Str(obj); + if (str == NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} + +static inline int +PyUnicodeWriter_WriteRepr(PyUnicodeWriter *writer, PyObject *obj) +{ + PyObject *str = PyObject_Repr(obj); + if (str == NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} + +static inline int +PyUnicodeWriter_WriteUTF8(PyUnicodeWriter *writer, + const char *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)strlen(str); + } + + PyObject *str_obj = PyUnicode_FromStringAndSize(str, size); + if (str_obj == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str_obj); + Py_DECREF(str_obj); + return res; +} + +static inline int +PyUnicodeWriter_WriteWideChar(PyUnicodeWriter *writer, + const wchar_t *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)wcslen(str); + } + + PyObject *str_obj = PyUnicode_FromWideChar(str, size); + if (str_obj == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str_obj); + Py_DECREF(str_obj); + return res; +} + +static inline int +PyUnicodeWriter_WriteSubstring(PyUnicodeWriter *writer, PyObject *str, + Py_ssize_t start, Py_ssize_t end) +{ + if (!PyUnicode_Check(str)) { + PyErr_Format(PyExc_TypeError, "expect str, not %T", str); + return -1; + } + if (start < 0 || start > end) { + PyErr_Format(PyExc_ValueError, "invalid start argument"); + return -1; + } + if (end > PyUnicode_GET_LENGTH(str)) { + PyErr_Format(PyExc_ValueError, "invalid end argument"); + return -1; + } + + return _PyUnicodeWriter_WriteSubstring((_PyUnicodeWriter*)writer, str, + start, end); +} + +static inline int +PyUnicodeWriter_Format(PyUnicodeWriter *writer, const char *format, ...) +{ + va_list vargs; + va_start(vargs, format); + PyObject *str = PyUnicode_FromFormatV(format, vargs); + va_end(vargs); + if (str == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} +#endif // PY_VERSION_HEX < 0x030E0000 // gh-116560 added PyLong_GetSign() to Python 3.14.0a0 #if PY_VERSION_HEX < 0x030E00A0 @@ -1354,6 +1524,175 @@ static inline int PyLong_GetSign(PyObject *obj, int *sign) #endif +// gh-124502 added PyUnicode_Equal() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyUnicode_Equal(PyObject *str1, PyObject *str2) +{ + if (!PyUnicode_Check(str1)) { + PyErr_Format(PyExc_TypeError, "first argument must be str, not %s", + Py_TYPE(str1)->tp_name); + return -1; + } + if (!PyUnicode_Check(str2)) { + PyErr_Format(PyExc_TypeError, "second argument must be str, not %s", + Py_TYPE(str2)->tp_name); + return -1; + } + +#if PY_VERSION_HEX >= 0x030d0000 && !defined(PYPY_VERSION) + PyAPI_FUNC(int) _PyUnicode_Equal(PyObject *str1, PyObject *str2); + + return _PyUnicode_Equal(str1, str2); +#elif PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) + return _PyUnicode_EQ(str1, str2); +#elif PY_VERSION_HEX >= 0x03090000 && defined(PYPY_VERSION) + return _PyUnicode_EQ(str1, str2); +#else + return (PyUnicode_Compare(str1, str2) == 0); +#endif +} +#endif + + +// gh-121645 added PyBytes_Join() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable) +{ + return _PyBytes_Join(sep, iterable); +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline Py_hash_t Py_HashBuffer(const void *ptr, Py_ssize_t len) +{ +#if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) + PyAPI_FUNC(Py_hash_t) _Py_HashBytes(const void *src, Py_ssize_t len); + + return _Py_HashBytes(ptr, len); +#else + Py_hash_t hash; + PyObject *bytes = PyBytes_FromStringAndSize((const char*)ptr, len); + if (bytes == NULL) { + return -1; + } + hash = PyObject_Hash(bytes); + Py_DECREF(bytes); + return hash; +#endif +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyIter_NextItem(PyObject *iter, PyObject **item) +{ + iternextfunc tp_iternext; + + assert(iter != NULL); + assert(item != NULL); + + tp_iternext = Py_TYPE(iter)->tp_iternext; + if (tp_iternext == NULL) { + *item = NULL; + PyErr_Format(PyExc_TypeError, "expected an iterator, got '%s'", + Py_TYPE(iter)->tp_name); + return -1; + } + + if ((*item = tp_iternext(iter))) { + return 1; + } + if (!PyErr_Occurred()) { + return 0; + } + if (PyErr_ExceptionMatches(PyExc_StopIteration)) { + PyErr_Clear(); + return 0; + } + return -1; +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline PyObject* PyLong_FromInt32(int32_t value) +{ + Py_BUILD_ASSERT(sizeof(long) >= 4); + return PyLong_FromLong(value); +} + +static inline PyObject* PyLong_FromInt64(int64_t value) +{ + Py_BUILD_ASSERT(sizeof(long long) >= 8); + return PyLong_FromLongLong(value); +} + +static inline PyObject* PyLong_FromUInt32(uint32_t value) +{ + Py_BUILD_ASSERT(sizeof(unsigned long) >= 4); + return PyLong_FromUnsignedLong(value); +} + +static inline PyObject* PyLong_FromUInt64(uint64_t value) +{ + Py_BUILD_ASSERT(sizeof(unsigned long long) >= 8); + return PyLong_FromUnsignedLongLong(value); +} + +static inline int PyLong_AsInt32(PyObject *obj, int32_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(int) == 4); + int value = PyLong_AsInt(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (int32_t)value; + return 0; +} + +static inline int PyLong_AsInt64(PyObject *obj, int64_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long long) == 8); + long long value = PyLong_AsLongLong(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (int64_t)value; + return 0; +} + +static inline int PyLong_AsUInt32(PyObject *obj, uint32_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long) >= 4); + unsigned long value = PyLong_AsUnsignedLong(obj); + if (value == (unsigned long)-1 && PyErr_Occurred()) { + return -1; + } +#if SIZEOF_LONG > 4 + if ((unsigned long)UINT32_MAX < value) { + PyErr_SetString(PyExc_OverflowError, + "Python int too large to convert to C uint32_t"); + return -1; + } +#endif + *pvalue = (uint32_t)value; + return 0; +} + +static inline int PyLong_AsUInt64(PyObject *obj, uint64_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long long) == 8); + unsigned long long value = PyLong_AsUnsignedLongLong(obj); + if (value == (unsigned long long)-1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (uint64_t)value; + return 0; +} +#endif + + #ifdef __cplusplus } #endif From 7999da38a7ff5aa359aa6ae3a7f88b835c7f7f06 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:07:43 +0000 Subject: [PATCH 350/652] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_imagingft.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 6fed821bc..884b8f96c 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -81,7 +81,7 @@ struct { /* -------------------------------------------------------------------- */ /* font objects */ -static FT_Library library; + static FT_Library library; #ifdef Py_GIL_DISABLED PyMutex ft_library_mutex; #endif @@ -1648,7 +1648,7 @@ PyInit__imagingft(void) { } #ifdef Py_GIL_DISABLED - ft_library_mutex = (PyMutex) {0}; + ft_library_mutex = (PyMutex){0}; PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif From 0fe55d611505fefaaa0d4b7b3a87b803714762de Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 09:23:13 +0800 Subject: [PATCH 351/652] Isolate macOS build from Homebrew. --- .github/workflows/wheels-dependencies.sh | 96 ++++++++++++++++++------ .github/workflows/wheels-test.sh | 15 ++-- .gitignore | 1 + pyproject.toml | 7 ++ setup.py | 94 +++++++++++++---------- wheels/multibuild | 2 +- 6 files changed, 140 insertions(+), 75 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97c1adf09..35b04c6d6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -23,6 +23,8 @@ OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 +RAQM_VERSION=0.7.1 +FRIBIDI_VERSION=1.0.16 if [[ -n "$IS_MACOS" ]]; then GIFLIB_VERSION=5.2.2 else @@ -38,7 +40,23 @@ BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 +function build_pkg_config { + if [ -e pkg-config-stamp ]; then return; fi + # This essentially duplicates the Homebrew recipe: + # https://github.com/Homebrew/homebrew-core/blob/master/Formula/p/pkg-config.rb + ORIGINAL_CFLAGS=$CFLAGS + CFLAGS="$CFLAGS -Wno-int-conversion" + build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ + --disable-debug --disable-host-tool --with-internal-glib \ + --with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \ + --with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include + CFLAGS=$ORIGINAL_CFLAGS + export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config + touch pkg-config-stamp +} + function build_brotli { + if [ -e brotli-stamp ]; then return; fi local cmake=$(get_modern_cmake) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ @@ -48,25 +66,25 @@ function build_brotli { cp /usr/local/lib64/libbrotli* /usr/local/lib cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig fi + touch brotli-stamp } function build_harfbuzz { - python3 -m pip install meson ninja + if [ -e harfbuzz-stamp ]; then return; fi + python -m pip install meson ninja local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ - && meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled) + && meson setup build --prefix=$BUILD_PREFIX --buildtype=release -Dfreetype=enabled -Dglib=disabled) (cd $out_dir/build \ && meson install) if [[ "$MB_ML_LIBC" == "manylinux" ]]; then cp /usr/local/lib64/libharfbuzz* /usr/local/lib fi + touch harfbuzz-stamp } function build { - if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then - sudo chown -R runner /usr/local - fi build_xz if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel @@ -77,17 +95,23 @@ function build { if [ -n "$IS_MACOS" ]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + build_simple libXdmcp 1.1.5 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist - if [[ "$CIBW_ARCHS" == "arm64" ]]; then - cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig - fi else sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc fi build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib build_libjpeg_turbo - build_tiff + if [ -n "$IS_MACOS" ]; then + # Custom tiff build to include jpeg; by default, configure won't include + # headers/libs in the custom macOS prefix + build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ + --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib + else + build_tiff + fi + build_libpng build_lcms2 build_openjpeg @@ -113,33 +137,55 @@ function build { fi build_harfbuzz + + if [ -n "$IS_MACOS" ]; then + build_simple fribidi $FRIBIDI_VERSION https://github.com/fribidi/fribidi/releases/download/v$FRIBIDI_VERSION tar.xz --enable-shared + build_simple raqm $RAQM_VERSION https://github.com/Host_Oman/libraqm/releases/download/v$RAQM_VERSION tar.gz --enable-shared + fi } +# Perform all dependency builds in the build subfolder. +mkdir -p build +pushd build > /dev/null + # Any stuff that you need to do before you start building the wheels # Runs in the root directory of this repository. -curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip -untar pillow-depends-main.zip +if [[ ! -d pillow-depends-main ]]; then + if [[ ! -f pillow-depends-main.zip ]]; then + echo "Download pillow dependency sources..." + curl -fSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip + fi + untar pillow-depends-main.zip +fi if [[ -n "$IS_MACOS" ]]; then - # libtiff and libxcb cause a conflict with building libtiff and libxcb - # libxau and libxdmcp cause an issue on macOS < 11 - # remove cairo to fix building harfbuzz on arm64 - # remove lcms2 and libpng to fix building openjpeg on arm64 - # remove jpeg-turbo to avoid inclusion on arm64 - # remove webp and zstd to avoid inclusion on x86_64 - # curl from brew requires zstd, use system curl - brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd - if [[ "$CIBW_ARCHS" == "arm64" ]]; then - brew remove --ignore-dependencies jpeg-turbo - else - brew remove --ignore-dependencies webp - fi + # Build and install into the `deps` folder. + BUILD_PREFIX=$(pwd)/deps - brew install pkg-config + # Homebrew (or similar packaging environments) install can contain some of + # the libraries that we're going to build. However, they may be compiled + # with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use, + # and they may bring in other dependencies that we don't want. The same will + # be true of any other locations on the path. To avoid conflicts, strip the + # path down to the bare mimimum (which, on macOS, won't include any + # development dependencies). + export PATH="$BUILD_PREFIX/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:$(dirname $(which python))" + export CMAKE_PREFIX_PATH=$BUILD_PREFIX + + # Link the brew command into our isolated build directory. + mkdir -p "$BUILD_PREFIX/bin" + mkdir -p "$BUILD_PREFIX/lib" + + # Ensure pkg-confg and cmake are available + build_pkg_config + python -m pip install cmake fi wrap_wheel_builder build +# Return to the project root to finish the build +popd > /dev/null + # Append licenses for filename in wheels/dependency_licenses/*; do echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index b30b1725f..05f5162c7 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,16 +1,13 @@ #!/bin/bash set -e -if [[ "$OSTYPE" == "darwin"* ]]; then - brew install fribidi - export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" - if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then - sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib +# For Unix, Ensure fibidi is installed by the system. +if [[ "$OSTYPE" != "darwin"* ]]; then + if [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then + apk add curl fribidi + else + yum install -y fribidi fi -elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then - apk add curl fribidi -else - yum install -y fribidi fi python3 -m pip install numpy diff --git a/.gitignore b/.gitignore index 1dd6c9175..9b5773257 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ lib64/ parts/ sdist/ var/ +wheelhouse/ *.egg-info/ .installed.cfg *.egg diff --git a/pyproject.toml b/pyproject.toml index c55be7693..53546f0ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,10 +93,17 @@ version = { attr = "PIL.__version__" } [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 + config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" +# Add an explicit dependencies prefix for macOS, and don't request vendored libraries. +macos.config-settings = "raqm=enable imagequant=disable dependencies-prefix=./build/deps" + test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" +[tool.cibuildwheel.macos.environment] +PATH = "$(pwd)/build/deps/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:$(dirname $(which python))" + [tool.black] exclude = "wheels/multibuild" diff --git a/setup.py b/setup.py index 60707083f..cb1da7c73 100644 --- a/setup.py +++ b/setup.py @@ -344,6 +344,11 @@ class pil_build_ext(build_ext): for x in ("raqm", "fribidi") ] + [ + ( + "dependencies-prefix", + None, + "The prefix where build dependencies are located.", + ), ("disable-platform-guessing", None, "Disable platform guessing on Linux"), ("debug", None, "Debug logging"), ] @@ -355,6 +360,7 @@ class pil_build_ext(build_ext): return True if value in configuration.get(option, []) else None def initialize_options(self) -> None: + self.dependencies_prefix = configuration.get("dependencies-prefix", []) self.disable_platform_guessing = self.check_configuration( "platform-guessing", "disable" ) @@ -564,48 +570,56 @@ class pil_build_ext(build_ext): ) elif sys.platform == "darwin": - # attempt to make sure we pick freetype2 over other versions - _add_directory(include_dirs, "/sw/include/freetype2") - _add_directory(include_dirs, "/sw/lib/freetype2/include") - # fink installation directories - _add_directory(library_dirs, "/sw/lib") - _add_directory(include_dirs, "/sw/include") - # darwin ports installation directories - _add_directory(library_dirs, "/opt/local/lib") - _add_directory(include_dirs, "/opt/local/include") - - # if Homebrew is installed, use its lib and include directories - try: - prefix = ( - subprocess.check_output(["brew", "--prefix"]) - .strip() - .decode("latin1") - ) - except Exception: - # Homebrew not installed - prefix = None - - ft_prefix = None - - if prefix: - # add Homebrew's include and lib directories - _add_directory(library_dirs, os.path.join(prefix, "lib")) - _add_directory(include_dirs, os.path.join(prefix, "include")) - _add_directory( - include_dirs, os.path.join(prefix, "opt", "zlib", "include") - ) - ft_prefix = os.path.join(prefix, "opt", "freetype") - - if ft_prefix and os.path.isdir(ft_prefix): - # freetype might not be linked into Homebrew's prefix - _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) - _add_directory(include_dirs, os.path.join(ft_prefix, "include")) + if self.dependencies_prefix: + # Use the explicitly provided prefixes for dependencies. + for prefix in self.dependencies_prefix: + _add_directory(library_dirs, os.path.join(prefix, "lib")) + _add_directory(include_dirs, os.path.join(prefix, "include")) else: - # fall back to freetype from XQuartz if - # Homebrew's freetype is missing - _add_directory(library_dirs, "/usr/X11/lib") - _add_directory(include_dirs, "/usr/X11/include") + # Guess the dependency locations based on homebrew/fink/macports + # attempt to make sure we pick freetype2 over other versions + _add_directory(include_dirs, "/sw/include/freetype2") + _add_directory(include_dirs, "/sw/lib/freetype2/include") + # fink installation directories + _add_directory(library_dirs, "/sw/lib") + _add_directory(include_dirs, "/sw/include") + # darwin ports installation directories + _add_directory(library_dirs, "/opt/local/lib") + _add_directory(include_dirs, "/opt/local/include") + # if Homebrew is installed, use its lib and include directories + try: + prefix = ( + subprocess.check_output(["brew", "--prefix"]) + .strip() + .decode("latin1") + ) + except Exception: + # Homebrew not installed + prefix = None + + ft_prefix = None + + if prefix: + # add Homebrew's include and lib directories + _add_directory(library_dirs, os.path.join(prefix, "lib")) + _add_directory(include_dirs, os.path.join(prefix, "include")) + _add_directory( + include_dirs, os.path.join(prefix, "opt", "zlib", "include") + ) + ft_prefix = os.path.join(prefix, "opt", "freetype") + + if ft_prefix and os.path.isdir(ft_prefix): + # freetype might not be linked into Homebrew's prefix + _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) + _add_directory(include_dirs, os.path.join(ft_prefix, "include")) + else: + # fall back to freetype from XQuartz if + # Homebrew's freetype is missing + _add_directory(library_dirs, "/usr/X11/lib") + _add_directory(include_dirs, "/usr/X11/include") + + # Add the macOS SDK path. sdk_path = self.get_macos_sdk_path() if sdk_path: _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) diff --git a/wheels/multibuild b/wheels/multibuild index 452dd2d17..8a9333e30 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 452dd2d1705f6b2375369a6570c415beb3163f70 +Subproject commit 8a9333e307fa3f24db2089a66f25e38a8272c261 From fc35fcc5ea844cd0184af6e999d0f35835a4254a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 10:55:30 +0800 Subject: [PATCH 352/652] Cleanups and typos identified by code review. --- .github/workflows/wheels-dependencies.sh | 8 ++++---- .github/workflows/wheels-test.sh | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 35b04c6d6..2c0835e7e 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -23,7 +23,7 @@ OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 -RAQM_VERSION=0.7.1 +RAQM_VERSION=0.10.2 FRIBIDI_VERSION=1.0.16 if [[ -n "$IS_MACOS" ]]; then GIFLIB_VERSION=5.2.2 @@ -71,7 +71,7 @@ function build_brotli { function build_harfbuzz { if [ -e harfbuzz-stamp ]; then return; fi - python -m pip install meson ninja + python3 -m pip install meson ninja local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ @@ -176,9 +176,9 @@ if [[ -n "$IS_MACOS" ]]; then mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/lib" - # Ensure pkg-confg and cmake are available + # Ensure pkg-config and cmake are available build_pkg_config - python -m pip install cmake + python3 -m pip install cmake fi wrap_wheel_builder build diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 05f5162c7..3a8fc87ba 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -# For Unix, Ensure fibidi is installed by the system. +# For Unix, ensure fribidi is installed by the system. if [[ "$OSTYPE" != "darwin"* ]]; then if [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then apk add curl fribidi From 00809a25516e1396e3e3ee38a760733efffbd1a9 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 13:42:46 +0800 Subject: [PATCH 353/652] Tweaks to ensure isolation from Homebrew on x86_64. --- .github/workflows/wheels-dependencies.sh | 34 +++++++++++++++++------- pyproject.toml | 2 +- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2c0835e7e..dbabd4aef 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -4,6 +4,9 @@ if [ -z "$IS_MACOS" ]; then export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} export MB_ML_VER=${AUDITWHEEL_POLICY:9} + + # Build and install into the `build/deps` folder. + BUILD_PREFIX=$(pwd)/build/deps fi export PLAT=$CIBW_ARCHS source wheels/multibuild/common_utils.sh @@ -84,6 +87,18 @@ function build_harfbuzz { touch harfbuzz-stamp } +function build_raqm { + if [ -e raqm-stamp ]; then return; fi + python3 -m pip install meson ninja + + local out_dir=$(fetch_unpack https://github.com/HOST-Oman/libraqm/releases/download/v$RAQM_VERSION/raqm-$RAQM_VERSION.tar.xz raqm-$RAQM_VERSION.tar.xz) + (cd $out_dir \ + && meson setup build --prefix=$BUILD_PREFIX) + (cd $out_dir/build \ + && meson install) + touch raqm-stamp +} + function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then @@ -105,9 +120,12 @@ function build { build_libjpeg_turbo if [ -n "$IS_MACOS" ]; then # Custom tiff build to include jpeg; by default, configure won't include - # headers/libs in the custom macOS prefix + # headers/libs in the custom macOS prefix. Explicitly disable webp and + # zstd, because on x86_64 macs, it will pick up the Homebrew versions of + # webp and zstd from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ - --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib + --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ + --disable-webp --disable-zstd else build_tiff fi @@ -140,7 +158,7 @@ function build { if [ -n "$IS_MACOS" ]; then build_simple fribidi $FRIBIDI_VERSION https://github.com/fribidi/fribidi/releases/download/v$FRIBIDI_VERSION tar.xz --enable-shared - build_simple raqm $RAQM_VERSION https://github.com/Host_Oman/libraqm/releases/download/v$RAQM_VERSION tar.gz --enable-shared + build_raqm fi } @@ -159,9 +177,6 @@ if [[ ! -d pillow-depends-main ]]; then fi if [[ -n "$IS_MACOS" ]]; then - # Build and install into the `deps` folder. - BUILD_PREFIX=$(pwd)/deps - # Homebrew (or similar packaging environments) install can contain some of # the libraries that we're going to build. However, they may be compiled # with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use, @@ -169,16 +184,17 @@ if [[ -n "$IS_MACOS" ]]; then # be true of any other locations on the path. To avoid conflicts, strip the # path down to the bare mimimum (which, on macOS, won't include any # development dependencies). - export PATH="$BUILD_PREFIX/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:$(dirname $(which python))" + export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" export CMAKE_PREFIX_PATH=$BUILD_PREFIX # Link the brew command into our isolated build directory. mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/lib" - # Ensure pkg-config and cmake are available + # Ensure pkg-config is available build_pkg_config - python3 -m pip install cmake + # Ensure cmake is available + python3 -m pip cmake fi wrap_wheel_builder build diff --git a/pyproject.toml b/pyproject.toml index 53546f0ab..3839cf840 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,7 @@ test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" [tool.cibuildwheel.macos.environment] -PATH = "$(pwd)/build/deps/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:$(dirname $(which python))" +PATH = "$(pwd)/build/deps/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" [tool.black] exclude = "wheels/multibuild" From 06dbfedb0fbfcf38f6d6298ac4979e82fb653956 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 13:51:37 +0800 Subject: [PATCH 354/652] Bump the multibuild version to fix jpeg-turbo issue. --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 8a9333e30..22e8ad0c6 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 8a9333e307fa3f24db2089a66f25e38a8272c261 +Subproject commit 22e8ad0c69acdb0c94abab74da6e09f2810d50d5 From 5a8373e527bbea93cb9c3be87c206683f488ad8e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 14:47:51 +0800 Subject: [PATCH 355/652] Correct a dumb pip invocation error. --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index dbabd4aef..0990943d3 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -194,7 +194,7 @@ if [[ -n "$IS_MACOS" ]]; then # Ensure pkg-config is available build_pkg_config # Ensure cmake is available - python3 -m pip cmake + python3 -m pip install cmake fi wrap_wheel_builder build From 140a06e0fda60b7474dbdfde53b2ad2f1d344603 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 16:04:56 +0800 Subject: [PATCH 356/652] Explicitly disable libdeflate on libtiff. --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0990943d3..c6f57200f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -125,7 +125,7 @@ function build { # webp and zstd from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ - --disable-webp --disable-zstd + --disable-webp --disable-zstd --disable-libdeflate else build_tiff fi From 0961d3d22bc966f19f68ce01db6d07373dfbe6bc Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 16:10:29 +0800 Subject: [PATCH 357/652] Possible fix for linux build failures. --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index c6f57200f..4d58bee90 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -113,7 +113,7 @@ function build { build_simple libXdmcp 1.1.5 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else - sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc + sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc fi build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib From c46946f3a4c89a52637cc8e0c0f67d8e85d18a16 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Oct 2024 19:13:39 +1100 Subject: [PATCH 358/652] Added filename placeholder in URL --- winbuild/build_prepare.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a21fbef91..b106eb7de 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -131,7 +131,7 @@ V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") DEPS: dict[str, dict[str, Any]] = { "libjpeg": { "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" - f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz/download", + "FILENAME/download", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], @@ -161,7 +161,7 @@ DEPS: dict[str, dict[str, Any]] = { "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": f"https://zlib.net/zlib{V['ZLIB_DOTLESS']}.zip", + "url": "https://zlib.net/FILENAME", "filename": f"zlib{V['ZLIB_DOTLESS']}.zip", "dir": f"zlib-{V['ZLIB']}", "license": "README", @@ -175,7 +175,7 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"*.lib"], }, "xz": { - "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/xz-{V['XZ']}.tar.gz", + "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/FILENAME", "filename": f"xz-{V['XZ']}.tar.gz", "dir": f"xz-{V['XZ']}", "license": "COPYING", @@ -188,7 +188,7 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"lzma.lib"], }, "libwebp": { - "url": f"http://downloads.webmproject.org/releases/webp/libwebp-{V['LIBWEBP']}.tar.gz", + "url": "http://downloads.webmproject.org/releases/webp/FILENAME", "filename": f"libwebp-{V['LIBWEBP']}.tar.gz", "dir": f"libwebp-{V['LIBWEBP']}", "license": "COPYING", @@ -210,7 +210,7 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"libsharpyuv.lib", r"libwebp*.lib"], }, "libtiff": { - "url": f"https://download.osgeo.org/libtiff/tiff-{V['TIFF']}.tar.gz", + "url": "https://download.osgeo.org/libtiff/FILENAME", "filename": f"tiff-{V['TIFF']}.tar.gz", "dir": f"tiff-{V['TIFF']}", "license": "LICENSE.md", @@ -268,7 +268,7 @@ DEPS: dict[str, dict[str, Any]] = { "libs": ["*.lib"], }, "freetype": { - "url": f"https://download.savannah.gnu.org/releases/freetype/freetype-{V['FREETYPE']}.tar.gz", + "url": "https://download.savannah.gnu.org/releases/freetype/FILENAME", "filename": f"freetype-{V['FREETYPE']}.tar.gz", "dir": f"freetype-{V['FREETYPE']}", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], @@ -303,7 +303,7 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], }, "lcms2": { - "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/lcms2-{V['LCMS2']}.tar.gz/download", # noqa: E501 + "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/FILENAME/download", "filename": f"lcms2-{V['LCMS2']}.tar.gz", "dir": f"lcms2-{V['LCMS2']}", "license": "LICENSE", @@ -497,7 +497,7 @@ def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: except RuntimeError as exc: # Otherwise try upstream print(exc) - download_dep(url, file) + download_dep(url.replace("FILENAME", filename), file) print("Extracting " + filename) sources_dir_abs = os.path.abspath(sources_dir) From fa7678987e034599344304dbfac5d533c30f7820 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Oct 2024 19:56:36 +1100 Subject: [PATCH 359/652] Simplified code --- winbuild/build_prepare.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b106eb7de..c8332d11c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -130,8 +130,7 @@ V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") # dependencies, listed in order of compilation DEPS: dict[str, dict[str, Any]] = { "libjpeg": { - "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" - "FILENAME/download", + "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/FILENAME/download", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], From 43c34fc0a5c22b1c00152a246fd39fe02925043d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 17:20:34 +0800 Subject: [PATCH 360/652] Copy manylinux lib64 files from the correct built prefix. --- .github/workflows/wheels-dependencies.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4d58bee90..9dae28717 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -66,8 +66,8 @@ function build_brotli { && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libbrotli* /usr/local/lib - cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig + cp $BUILD_PREFIX/lib64/libbrotli* $BUILD_PREFIX/lib + cp $BUILD_PREFIX/lib64/pkgconfig/libbrotli* $BUILD_PREFIX/lib/pkgconfig fi touch brotli-stamp } @@ -82,7 +82,7 @@ function build_harfbuzz { (cd $out_dir/build \ && meson install) if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libharfbuzz* /usr/local/lib + cp $BUILD_PREFIX/lib64/libharfbuzz* $BUILD_PREFIX/lib fi touch harfbuzz-stamp } @@ -113,7 +113,7 @@ function build { build_simple libXdmcp 1.1.5 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else - sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc + sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc fi build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib @@ -133,8 +133,8 @@ function build { build_libpng build_lcms2 build_openjpeg - if [ -f /usr/local/lib64/libopenjp2.so ]; then - cp /usr/local/lib64/libopenjp2.so /usr/local/lib + if [ -f $BUILD_PREFIX/lib64/libopenjp2.so ]; then + cp $BUILD_PREFIX/lib64/libopenjp2.so $BUILD_PREFIX/lib fi ORIGINAL_CFLAGS=$CFLAGS From 4b9399f8bfb08d4ed1a1113ed6fd4b6137f7c2aa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Oct 2024 22:00:45 +1100 Subject: [PATCH 361/652] Use register_handler --- Tests/test_file_bufrstub.py | 2 +- Tests/test_file_gribstub.py | 2 +- Tests/test_file_hdf5stub.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 77ee5b0ea..fc8920317 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None: im.save(temp_file) assert handler.saved - BufrStubImagePlugin._handler = None + BufrStubImagePlugin.register_handler(None) diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index aba473d24..02e464ff1 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None: im.save(temp_file) assert handler.saved - GribStubImagePlugin._handler = None + GribStubImagePlugin.register_handler(None) diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 8275bd0d8..024be9e80 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -85,4 +85,4 @@ def test_handler(tmp_path: Path) -> None: im.save(temp_file) assert handler.saved - Hdf5StubImagePlugin._handler = None + Hdf5StubImagePlugin.register_handler(None) From bb3515d649cda179037161ee3bea264a4d289b32 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Fri, 25 Oct 2024 17:32:29 +0200 Subject: [PATCH 362/652] Make PyMutex static and get rid of initialization --- src/_imagingft.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 884b8f96c..d38279f3e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -83,7 +83,7 @@ struct { static FT_Library library; #ifdef Py_GIL_DISABLED -PyMutex ft_library_mutex; +static PyMutex ft_library_mutex; #endif typedef struct { @@ -1648,7 +1648,6 @@ PyInit__imagingft(void) { } #ifdef Py_GIL_DISABLED - ft_library_mutex = (PyMutex){0}; PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif From 3e0849bfb56f6fa8c73ff1bde044551ed72ac2d3 Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 25 Oct 2024 20:35:17 +0200 Subject: [PATCH 363/652] winbuild: Replace zlib with zlib-ng built with CMake --- winbuild/build_prepare.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a21fbef91..2065e41a9 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -120,11 +120,10 @@ V = { "OPENJPEG": "2.5.2", "TIFF": "4.6.0", "XZ": "5.6.3", - "ZLIB": "1.3.1", + "ZLIBNG": "2.2.2", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) -V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") # dependencies, listed in order of compilation @@ -161,18 +160,22 @@ DEPS: dict[str, dict[str, Any]] = { "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": f"https://zlib.net/zlib{V['ZLIB_DOTLESS']}.zip", - "filename": f"zlib{V['ZLIB_DOTLESS']}.zip", - "dir": f"zlib-{V['ZLIB']}", - "license": "README", - "license_pattern": "Copyright notice:\n\n(.+)$", + "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.zip", + "filename": f"zlib-ng-{V['ZLIBNG']}.zip", + "dir": f"zlib-ng-{V['ZLIBNG']}", + "license": "LICENSE.md", + "patch": { + r"CMakeLists.txt": { + "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlibstatic${{SUFFIX}})": "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlib)", # noqa: E501 + }, + }, "build": [ - cmd_nmake(r"win32\Makefile.msc", "clean"), - cmd_nmake(r"win32\Makefile.msc", "zlib.lib"), - cmd_copy("zlib.lib", "z.lib"), + *cmds_cmake( + "zlib", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DZLIB_COMPAT:BOOL=ON" + ), ], "headers": [r"z*.h"], - "libs": [r"*.lib"], + "libs": [r"zlib.lib"], }, "xz": { "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/xz-{V['XZ']}.tar.gz", From 7885066e5fc6ae56db0bef18eb2c188a6aa9a181 Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 25 Oct 2024 20:37:21 +0200 Subject: [PATCH 364/652] PIL.features: Add a compile-time zlib-ng feature flag and version number --- Tests/check_wheel.py | 3 +++ docs/reference/features.rst | 1 + src/PIL/features.py | 7 ++++++- src/_imaging.c | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 4b91984f5..374e48e8a 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -34,10 +34,13 @@ def test_wheel_features() -> None: "fribidi", "harfbuzz", "libjpeg_turbo", + "zlib_ng", "xcb", } if sys.platform == "win32": expected_features.remove("xcb") + else: + expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features diff --git a/docs/reference/features.rst b/docs/reference/features.rst index fcff96735..427c0f606 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -54,6 +54,7 @@ Feature version numbers are available only where stated. Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. +* ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. diff --git a/src/PIL/features.py b/src/PIL/features.py index 75d59e01c..3645e3def 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -127,6 +127,7 @@ features: dict[str, tuple[str, str | bool, str | None]] = { "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), + "zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"), "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), "xcb": ("PIL._imaging", "HAVE_XCB", None), } @@ -308,7 +309,11 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: # this check is also in src/_imagingcms.c:setup_module() version_static = tuple(int(x) for x in v.split(".")) < (2, 7) t = "compiled for" if version_static else "loaded" - if name == "raqm": + if name == "zlib": + zlib_ng_version = version_feature("zlib_ng") + if zlib_ng_version is not None: + v += ", compiled for zlib-ng " + zlib_ng_version + elif name == "raqm": for f in ("fribidi", "harfbuzz"): v2 = version_feature(f) if v2 is not None: diff --git a/src/_imaging.c b/src/_imaging.c index 2db4486b2..5d6d97bed 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4397,6 +4397,20 @@ setup_module(PyObject *m) { } #endif + PyObject *have_zlibng; +#ifdef ZLIBNG_VERSION + have_zlibng = Py_True; + { + PyObject *v = PyUnicode_FromString(ZLIBNG_VERSION); + PyDict_SetItemString(d, "zlib_ng_version", v ? v : Py_None); + Py_XDECREF(v); + } +#else + have_zlibng = Py_False; +#endif + Py_INCREF(have_zlibng); + PyModule_AddObject(m, "HAVE_ZLIBNG", have_zlibng); + #ifdef HAVE_LIBTIFF { extern const char *ImagingTiffVersion(void); From a43e5bb7354006766a67a353aa21d9a0198cdcab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 14:26:47 +1100 Subject: [PATCH 365/652] brew remove libdeflate --- .github/workflows/wheels-dependencies.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97c1adf09..7970d4d15 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -121,6 +121,7 @@ curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-de untar pillow-depends-main.zip if [[ -n "$IS_MACOS" ]]; then + # libdeflate may cause a minimum target error when repairing the wheel # libtiff and libxcb cause a conflict with building libtiff and libxcb # libxau and libxdmcp cause an issue on macOS < 11 # remove cairo to fix building harfbuzz on arm64 @@ -132,7 +133,7 @@ if [[ -n "$IS_MACOS" ]]; then if [[ "$CIBW_ARCHS" == "arm64" ]]; then brew remove --ignore-dependencies jpeg-turbo else - brew remove --ignore-dependencies webp + brew remove --ignore-dependencies libdeflate webp fi brew install pkg-config From e1f4b5a68fa783126f5a81ccba5fac4526f6ab00 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 15:10:41 +1100 Subject: [PATCH 366/652] Move MPO into "Fully supported formats" --- docs/handbook/image-file-formats.rst | 48 ++++++++++++++-------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 8183473e4..bf3087f6f 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -692,6 +692,30 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: you fail to do this, you will get errors about not being able to load the ``_imaging`` DLL). +MPO +^^^ + +Pillow reads and writes Multi Picture Object (MPO) files. When first opened, it loads +the primary image. The :py:meth:`~PIL.Image.Image.seek` and +:py:meth:`~PIL.Image.Image.tell` methods may be used to read other pictures from the +file. The pictures are zero-indexed and random access is supported. + +.. _mpo-saving: + +Saving +~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +option will also be available. + +**append_images** + A list of images to append as additional pictures. Each of the + images in the list can be single or multiframe images. + + .. versionadded:: 9.3.0 + MSP ^^^ @@ -1435,30 +1459,6 @@ Note that there may be an embedded gamma of 2.2 in MIC files. To enable MIC support, you must install :pypi:`olefile`. -MPO -^^^ - -Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary -image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` -methods may be used to read other pictures from the file. The pictures are -zero-indexed and random access is supported. - -.. _mpo-saving: - -Saving -~~~~~~ - -When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default -only the first frame of a multiframe image will be saved. If the ``save_all`` -argument is present and true, then all frames will be saved, and the following -option will also be available. - -**append_images** - A list of images to append as additional pictures. Each of the - images in the list can be single or multiframe images. - - .. versionadded:: 9.3.0 - PCD ^^^ From 413bbb31c959a3e2a6929d4a12861a79d82f8e80 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 16:15:46 +1100 Subject: [PATCH 367/652] Fixed catching warnings --- Tests/test_bmp_reference.py | 2 ++ Tests/test_file_dcx.py | 4 ++++ Tests/test_file_fli.py | 4 ++++ Tests/test_file_gif.py | 4 ++++ Tests/test_file_icns.py | 2 ++ Tests/test_file_im.py | 4 ++++ Tests/test_file_jpeg.py | 2 ++ Tests/test_file_mpo.py | 4 ++++ Tests/test_file_png.py | 2 ++ Tests/test_file_psd.py | 4 ++++ Tests/test_file_spider.py | 4 ++++ Tests/test_file_tar.py | 4 ++++ Tests/test_file_tiff.py | 4 ++++ Tests/test_file_webp.py | 2 ++ Tests/test_image.py | 2 ++ Tests/test_imageqt.py | 2 ++ Tests/test_numpy.py | 2 ++ 17 files changed, 52 insertions(+) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 7f8487921..82cab39c6 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -22,6 +22,8 @@ def test_bad() -> None: for f in get_files("b"): # Assert that there is no unclosed file warning with warnings.catch_warnings(): + warnings.simplefilter("error") + try: with Image.open(f) as im: im.load() diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 65337cad9..5deacd878 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -36,6 +36,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_FILE) im.load() im.close() @@ -43,6 +45,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index f86fb8d09..0a7740cc8 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -65,6 +65,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(static_test_file) im.load() im.close() @@ -81,6 +83,8 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(static_test_file) as im: im.load() diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 16c8466f3..248347d5b 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -46,6 +46,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_GIF) im.load() im.close() @@ -67,6 +69,8 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_GIF) as im: im.load() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 16f6b3651..141b88dfa 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -21,6 +21,8 @@ def test_sanity() -> None: with Image.open(TEST_FILE) as im: # Assert that there is no unclosed file warning with warnings.catch_warnings(): + warnings.simplefilter("error") + im.load() assert im.mode == "RGBA" diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 036965bf5..1d3fa485f 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -41,6 +41,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_IM) im.load() im.close() @@ -48,6 +50,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_IM) as im: im.load() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index cde951395..2c66652e5 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -850,6 +850,8 @@ class TestFileJpeg: out = str(tmp_path / "out.jpg") with warnings.catch_warnings(): + warnings.simplefilter("error") + im.save(out, exif=exif) with Image.open(out) as reloaded: diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index e0f42a266..949583185 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -48,6 +48,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(test_files[0]) im.load() im.close() @@ -63,6 +65,8 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(test_files[0]) as im: im.load() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0abf9866f..ffafc3c58 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -338,6 +338,8 @@ class TestFilePng: with Image.open(TEST_PNG_FILE) as im: # Assert that there is no unclosed file warning with warnings.catch_warnings(): + warnings.simplefilter("error") + im.verify() with Image.open(TEST_PNG_FILE) as im: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index e6c79e40b..5f22001f3 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -35,6 +35,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(test_file) im.load() im.close() @@ -42,6 +44,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(test_file) as im: im.load() diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 66c88e9d8..4cafda865 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -34,6 +34,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_FILE) im.load() im.close() @@ -41,6 +43,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 6217ebedd..49220a8b6 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -37,11 +37,15 @@ def test_unclosed_file() -> None: def test_close() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() def test_contextmanager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): pass diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index af766022b..6f51d4651 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -72,6 +72,8 @@ class TestFileTiff: def test_closed_file(self) -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open("Tests/images/multipage.tiff") im.load() im.close() @@ -88,6 +90,8 @@ class TestFileTiff: def test_context_manager(self) -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open("Tests/images/multipage.tiff") as im: im.load() diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 79f6bb4e0..ad5aa9ed6 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -191,6 +191,8 @@ class TestFileWebp: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: with warnings.catch_warnings(): + warnings.simplefilter("error") + image.save(tmp_path / "temp.webp") def test_file_pointer_could_be_reused(self) -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 9b65041f4..c8df474f4 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -737,6 +737,8 @@ class TestImage: # Act/Assert with Image.open(test_file) as im: with warnings.catch_warnings(): + warnings.simplefilter("error") + im.save(temp_file) def test_no_new_file_on_error(self, tmp_path: Path) -> None: diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 22cd674ce..2d7ca0ae0 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -52,4 +52,6 @@ def test_image(mode: str) -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + ImageQt.ImageQt("Tests/images/hopper.gif") diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 040472d69..79cd14b66 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -264,4 +264,6 @@ def test_no_resource_warning_for_numpy_array() -> None: with Image.open(test_file) as im: # Act/Assert with warnings.catch_warnings(): + warnings.simplefilter("error") + array(im) From f92599aa9394d9b1f59a503a796bc0fb6ddb8cda Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 19:05:16 +1100 Subject: [PATCH 368/652] Renamed fixture --- Tests/test_file_jpeg2k.py | 102 +++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 79f53e211..fbf72ae05 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -32,7 +32,7 @@ pytestmark = skip_unless_feature("jpg_2000") @pytest.fixture -def test_card() -> Generator[ImageFile.ImageFile, None, None]: +def card() -> Generator[ImageFile.ImageFile, None, None]: with Image.open("Tests/images/test-card.png") as im: im.load() try: @@ -83,78 +83,76 @@ def test_invalid_file() -> None: Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) -def test_bytesio(test_card: ImageFile.ImageFile) -> None: +def test_bytesio(card: ImageFile.ImageFile) -> None: with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) with Image.open(data) as im: im.load() - assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, card, 1.0e-3) # These two test pre-written JPEG 2000 files that were not written with # PIL (they were made using Adobe Photoshop) -def test_lossless(test_card: ImageFile.ImageFile, tmp_path: Path) -> None: +def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() outfile = str(tmp_path / "temp_test-card.png") im.save(outfile) - assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, card, 1.0e-3) -def test_lossy_tiled(test_card: ImageFile.ImageFile) -> None: - assert_image_similar_tofile( - test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 - ) +def test_lossy_tiled(card: ImageFile.ImageFile) -> None: + assert_image_similar_tofile(card, "Tests/images/test-card-lossy-tiled.jp2", 2.0) -def test_lossless_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card) - assert_image_equal(im, test_card) +def test_lossless_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card) + assert_image_equal(im, card) -def test_lossy_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, quality_layers=[20]) - assert_image_similar(im, test_card, 2.0) +def test_lossy_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, quality_layers=[20]) + assert_image_similar(im, card, 2.0) -def test_tiled_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, tile_size=(128, 128)) - assert_image_equal(im, test_card) +def test_tiled_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, tile_size=(128, 128)) + assert_image_equal(im, card) -def test_tiled_offset_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) - assert_image_equal(im, test_card) +def test_tiled_offset_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) + assert_image_equal(im, card) -def test_tiled_offset_too_small(test_card: ImageFile.ImageFile) -> None: +def test_tiled_offset_too_small(card: ImageFile.ImageFile) -> None: with pytest.raises(ValueError): - roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) + roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) -def test_irreversible_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, irreversible=True, quality_layers=[20]) - assert_image_similar(im, test_card, 2.0) +def test_irreversible_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, irreversible=True, quality_layers=[20]) + assert_image_similar(im, card, 2.0) -def test_prog_qual_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") - assert_image_similar(im, test_card, 2.0) +def test_prog_qual_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, quality_layers=[60, 40, 20], progression="LRCP") + assert_image_similar(im, card, 2.0) -def test_prog_res_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, num_resolutions=8, progression="RLCP") - assert_image_equal(im, test_card) +def test_prog_res_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, num_resolutions=8, progression="RLCP") + assert_image_equal(im, card) @pytest.mark.parametrize("num_resolutions", range(2, 6)) def test_default_num_resolutions( - test_card: ImageFile.ImageFile, num_resolutions: int + card: ImageFile.ImageFile, num_resolutions: int ) -> None: d = 1 << (num_resolutions - 1) - im = test_card.resize((d - 1, d - 1)) + im = card.resize((d - 1, d - 1)) with pytest.raises(OSError): roundtrip(im, num_resolutions=num_resolutions) reloaded = roundtrip(im) @@ -216,31 +214,31 @@ def test_header_errors() -> None: pass -def test_layers_type(test_card: ImageFile.ImageFile, tmp_path: Path) -> None: +def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None: outfile = str(tmp_path / "temp_layers.jp2") for quality_layers in [[100, 50, 10], (100, 50, 10), None]: - test_card.save(outfile, quality_layers=quality_layers) + card.save(outfile, quality_layers=quality_layers) for quality_layers_str in ["quality_layers", ("100", "50", "10")]: with pytest.raises(ValueError): - test_card.save(outfile, quality_layers=quality_layers_str) + card.save(outfile, quality_layers=quality_layers_str) -def test_layers(test_card: ImageFile.ImageFile) -> None: +def test_layers(card: ImageFile.ImageFile) -> None: out = BytesIO() - test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") + card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") out.seek(0) with Image.open(out) as im: im.layers = 1 im.load() - assert_image_similar(im, test_card, 13) + assert_image_similar(im, card, 13) out.seek(0) with Image.open(out) as im: im.layers = 3 im.load() - assert_image_similar(im, test_card, 0.4) + assert_image_similar(im, card, 0.4) @pytest.mark.parametrize( @@ -257,7 +255,7 @@ def test_layers(test_card: ImageFile.ImageFile) -> None: ), ) def test_no_jp2( - test_card: ImageFile.ImageFile, + card: ImageFile.ImageFile, name: str, args: dict[str, bool], offset: int, @@ -266,20 +264,20 @@ def test_no_jp2( out = BytesIO() if name: out.name = name - test_card.save(out, "JPEG2000", **args) + card.save(out, "JPEG2000", **args) out.seek(offset) assert out.read(2) == data -def test_mct(test_card: ImageFile.ImageFile) -> None: +def test_mct(card: ImageFile.ImageFile) -> None: # Three component for val in (0, 1): out = BytesIO() - test_card.save(out, "JPEG2000", mct=val, no_jp2=True) + card.save(out, "JPEG2000", mct=val, no_jp2=True) assert out.getvalue()[59] == val with Image.open(out) as im: - assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, card, 1.0e-3) # Single component should have MCT disabled for val in (0, 1): @@ -436,22 +434,22 @@ def test_comment() -> None: pass -def test_save_comment(test_card: ImageFile.ImageFile) -> None: +def test_save_comment(card: ImageFile.ImageFile) -> None: for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() - test_card.save(out, "JPEG2000", comment=comment) + card.save(out, "JPEG2000", comment=comment) with Image.open(out) as im: assert im.info["comment"] == b"Created by Pillow" out = BytesIO() long_comment = b" " * 65531 - test_card.save(out, "JPEG2000", comment=long_comment) + card.save(out, "JPEG2000", comment=long_comment) with Image.open(out) as im: assert im.info["comment"] == long_comment with pytest.raises(ValueError): - test_card.save(out, "JPEG2000", comment=long_comment + b" ") + card.save(out, "JPEG2000", comment=long_comment + b" ") @pytest.mark.parametrize( @@ -474,10 +472,10 @@ def test_crashes(test_file: str) -> None: @skip_unless_feature_version("jpg_2000", "2.4.0") -def test_plt_marker(test_card: ImageFile.ImageFile) -> None: +def test_plt_marker(card: ImageFile.ImageFile) -> None: # Search the start of the codesteam for PLT out = BytesIO() - test_card.save(out, "JPEG2000", no_jp2=True, plt=True) + card.save(out, "JPEG2000", no_jp2=True, plt=True) out.seek(0) while True: marker = out.read(2) From ed910a68d637661ccb4c4a7c1245769176fec93d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 11:42:35 +1100 Subject: [PATCH 369/652] Only replace version suffix if zlib-ng is present --- Tests/test_features.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index ed7929973..f8f7f6eec 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -36,10 +36,11 @@ def test_version() -> None: else: assert function(name) == version if name != "PIL": - if name == "zlib" and version is not None: - version = re.sub(".zlib-ng$", "", version) - elif name == "libtiff" and version is not None: - version = re.sub("t$", "", version) + if version is not None: + if name == "zlib" and features.check_feature("zlib_ng"): + version = re.sub(".zlib-ng$", "", version) + elif name == "libtiff": + version = re.sub("t$", "", version) assert version is None or re.search(r"\d+(\.\d+)*$", version) for module in features.modules: From 29cdbce39e1d3860486319b46ea829e0c3566279 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 21:13:01 +1100 Subject: [PATCH 370/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ae3841569..87945bc84 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Corrected EMF DPI #8485 + [radarhere] + - Fix IFDRational with a zero denominator #8474 [radarhere] From 73600eea94bfb695c52f7746c3523b5ce332d567 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Oct 2024 00:34:24 +1100 Subject: [PATCH 371/652] Detach PyQt6 QPixmap instance before returning --- .github/workflows/test-windows.yml | 6 ++++++ src/PIL/ImageQt.py | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c8842e37b..f6d0aeb1d 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -80,6 +80,12 @@ jobs: pytest-cov pytest-timeout + - name: Install CPython dependencies + if: "!contains(matrix.python-version, 'pypy')" + run: > + python3 -m pip install + PyQt6 + - name: Install dependencies id: install run: | diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index a3d647138..2cc40f855 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -213,4 +213,7 @@ def toqimage(im: Image.Image | str | QByteArray) -> ImageQt: def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap: qimage = toqimage(im) - return getattr(QPixmap, "fromImage")(qimage) + pixmap = getattr(QPixmap, "fromImage")(qimage) + if qt_version == "6": + pixmap.detach() + return pixmap From e5706a590b16fcfc2b826de19addc2e33d3c3508 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Oct 2024 09:04:06 +1100 Subject: [PATCH 372/652] Upgraded multibuild to remove openjpeg lib64 copy --- .github/workflows/wheels-dependencies.sh | 3 --- wheels/multibuild | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 7970d4d15..3a80a7e74 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -91,9 +91,6 @@ function build { build_libpng build_lcms2 build_openjpeg - if [ -f /usr/local/lib64/libopenjp2.so ]; then - cp /usr/local/lib64/libopenjp2.so /usr/local/lib - fi ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" diff --git a/wheels/multibuild b/wheels/multibuild index 452dd2d17..9a9d1275f 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 452dd2d1705f6b2375369a6570c415beb3163f70 +Subproject commit 9a9d1275f025f737cdaa3c451ba07129dd95f361 From 08554684b3f382f3d729db5e6cd1fe94126dab66 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 07:46:59 +0800 Subject: [PATCH 373/652] Revert fribidi/raqm changes for macOS builds. --- .github/workflows/wheels-dependencies.sh | 5 ----- .github/workflows/wheels-test.sh | 21 ++++++++++++++++----- pyproject.toml | 4 ++-- setup.py | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 9dae28717..220842371 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -155,11 +155,6 @@ function build { fi build_harfbuzz - - if [ -n "$IS_MACOS" ]; then - build_simple fribidi $FRIBIDI_VERSION https://github.com/fribidi/fribidi/releases/download/v$FRIBIDI_VERSION tar.xz --enable-shared - build_raqm - fi } # Perform all dependency builds in the build subfolder. diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 3a8fc87ba..e671c8d45 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,13 +1,24 @@ #!/bin/bash set -e -# For Unix, ensure fribidi is installed by the system. -if [[ "$OSTYPE" != "darwin"* ]]; then - if [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then - apk add curl fribidi +# Ensure fribidi is installed by the system. +if [[ "$OSTYPE" == "darwin"* ]]; then + # If Homebrew is on the path during the build, it may leak into the wheels. + # However, we need a *do* need Homebrew to provide a copy of fribidi for + # testing purposes so that we can verify the fribidi shim works as expected. + if [[ "$(uname -m)" == "x86_64" ]]; then + HOMEBREW_HOME=/usr/local/homebrew else - yum install -y fribidi + HOMEBREW_HOME=/opt/homebrew fi + $HOMEBREW_HOME/bin/brew install fribidi + + # Add the Homebrew lib folder so that vendored libraries can be found. + export DYLD_LIBRARY_PATH=$HOMEBREW_HOME/lib +elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then + apk add curl fribidi +else + yum install -y fribidi fi python3 -m pip install numpy diff --git a/pyproject.toml b/pyproject.toml index 3839cf840..da8548dcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,8 +95,8 @@ before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" -# Add an explicit dependencies prefix for macOS, and don't request vendored libraries. -macos.config-settings = "raqm=enable imagequant=disable dependencies-prefix=./build/deps" +# Add an explicit dependencies prefix for macOS. +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable dependencies-prefix=./build/deps" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" diff --git a/setup.py b/setup.py index 16c25cbda..38d793b14 100644 --- a/setup.py +++ b/setup.py @@ -454,7 +454,7 @@ class pil_build_ext(build_ext): def get_macos_sdk_path(self) -> str | None: try: sdk_path = ( - subprocess.check_output(["xcrun", "--show-sdk-path"]) + subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", "macosx"]) .strip() .decode("latin1") ) From 8308bf361e7d4bac39b1710e4c3c0725c7ad07ac Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 07:47:30 +0800 Subject: [PATCH 374/652] Bump multibuild to include more cmake changes. --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 22e8ad0c6..9a9d1275f 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 22e8ad0c69acdb0c94abab74da6e09f2810d50d5 +Subproject commit 9a9d1275f025f737cdaa3c451ba07129dd95f361 From c74a5bdd85c3314fbdbbe2a72d17adad85fae5f0 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 09:02:05 +0800 Subject: [PATCH 375/652] Correct paths used for Linux build. --- .github/workflows/wheels-dependencies.sh | 31 ++++-------------------- pyproject.toml | 4 +++ setup.py | 3 +-- 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 220842371..0eb15d553 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -26,8 +26,6 @@ OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 -RAQM_VERSION=0.10.2 -FRIBIDI_VERSION=1.0.16 if [[ -n "$IS_MACOS" ]]; then GIFLIB_VERSION=5.2.2 else @@ -63,12 +61,8 @@ function build_brotli { local cmake=$(get_modern_cmake) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ - && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ + && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp $BUILD_PREFIX/lib64/libbrotli* $BUILD_PREFIX/lib - cp $BUILD_PREFIX/lib64/pkgconfig/libbrotli* $BUILD_PREFIX/lib/pkgconfig - fi touch brotli-stamp } @@ -78,27 +72,12 @@ function build_harfbuzz { local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ - && meson setup build --prefix=$BUILD_PREFIX --buildtype=release -Dfreetype=enabled -Dglib=disabled) + && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled) (cd $out_dir/build \ && meson install) - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp $BUILD_PREFIX/lib64/libharfbuzz* $BUILD_PREFIX/lib - fi touch harfbuzz-stamp } -function build_raqm { - if [ -e raqm-stamp ]; then return; fi - python3 -m pip install meson ninja - - local out_dir=$(fetch_unpack https://github.com/HOST-Oman/libraqm/releases/download/v$RAQM_VERSION/raqm-$RAQM_VERSION.tar.xz raqm-$RAQM_VERSION.tar.xz) - (cd $out_dir \ - && meson setup build --prefix=$BUILD_PREFIX) - (cd $out_dir/build \ - && meson install) - touch raqm-stamp -} - function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then @@ -133,9 +112,6 @@ function build { build_libpng build_lcms2 build_openjpeg - if [ -f $BUILD_PREFIX/lib64/libopenjp2.so ]; then - cp $BUILD_PREFIX/lib64/libopenjp2.so $BUILD_PREFIX/lib - fi ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" @@ -190,6 +166,9 @@ if [[ -n "$IS_MACOS" ]]; then build_pkg_config # Ensure cmake is available python3 -m pip install cmake +else + # Ensure that any built libraries are on the linker path. + export LD_LIBRARY_PATH=$BUILD_PREFIX/lib fi wrap_wheel_builder build diff --git a/pyproject.toml b/pyproject.toml index da8548dcb..af3d04757 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,10 @@ test-extras = "tests" [tool.cibuildwheel.macos.environment] PATH = "$(pwd)/build/deps/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +[tool.cibuildwheel.linux.environment] +LD_LIBRARY_PATH = "$(pwd)/build/deps/lib" +PKG_CONFIG_PATH = "$(pwd)/build/deps/lib/pkgconfig:$(pwd)/build/deps/share/pkgconfig" + [tool.black] exclude = "wheels/multibuild" diff --git a/setup.py b/setup.py index 38d793b14..d7e305fc0 100644 --- a/setup.py +++ b/setup.py @@ -131,7 +131,7 @@ _LIB_IMAGING = ( "codec_fd", ) -DEBUG = False +DEBUG = True class DependencyException(Exception): @@ -479,7 +479,6 @@ class pil_build_ext(build_ext): pkg_config = None if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): pkg_config = _pkg_config - # # add configured kits for root_name, lib_name in { From 72d81e24a81e6e765f21e5794218f080e7718ec7 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 09:25:47 +0800 Subject: [PATCH 376/652] Simplify Linux config by correcting a logic error in macOS config. --- .github/workflows/wheels-dependencies.sh | 7 ++----- pyproject.toml | 4 ---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0eb15d553..2e915364a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -4,8 +4,8 @@ if [ -z "$IS_MACOS" ]; then export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} export MB_ML_VER=${AUDITWHEEL_POLICY:9} - - # Build and install into the `build/deps` folder. +else + # Build and install macOS builds into the `build/deps` folder. BUILD_PREFIX=$(pwd)/build/deps fi export PLAT=$CIBW_ARCHS @@ -166,9 +166,6 @@ if [[ -n "$IS_MACOS" ]]; then build_pkg_config # Ensure cmake is available python3 -m pip install cmake -else - # Ensure that any built libraries are on the linker path. - export LD_LIBRARY_PATH=$BUILD_PREFIX/lib fi wrap_wheel_builder build diff --git a/pyproject.toml b/pyproject.toml index af3d04757..da8548dcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,10 +104,6 @@ test-extras = "tests" [tool.cibuildwheel.macos.environment] PATH = "$(pwd)/build/deps/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" -[tool.cibuildwheel.linux.environment] -LD_LIBRARY_PATH = "$(pwd)/build/deps/lib" -PKG_CONFIG_PATH = "$(pwd)/build/deps/lib/pkgconfig:$(pwd)/build/deps/share/pkgconfig" - [tool.black] exclude = "wheels/multibuild" From ec214e4d7caa471c087e0d690b20d75b6dd929fc Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 09:39:58 +0800 Subject: [PATCH 377/652] Can't check IS_MACOS until common_utils is invoked. --- .github/workflows/wheels-dependencies.sh | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2e915364a..9ce851f0f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,13 +1,5 @@ #!/bin/bash # Define custom utilities -# Test for macOS with [ -n "$IS_MACOS" ] -if [ -z "$IS_MACOS" ]; then - export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} - export MB_ML_VER=${AUDITWHEEL_POLICY:9} -else - # Build and install macOS builds into the `build/deps` folder. - BUILD_PREFIX=$(pwd)/build/deps -fi export PLAT=$CIBW_ARCHS source wheels/multibuild/common_utils.sh source wheels/multibuild/library_builders.sh @@ -15,6 +7,16 @@ if [ -z "$IS_MACOS" ]; then source wheels/multibuild/manylinux_utils.sh fi +# Test for macOS with [ -n "$IS_MACOS" ] +echo "IS MACOS: $IS_MACOS" +if [ -z "$IS_MACOS" ]; then + export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} + export MB_ML_VER=${AUDITWHEEL_POLICY:9} +elseac + # Build and install macOS builds into the `build/deps` folder. + BUILD_PREFIX=$(pwd)/build/deps +fi + ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds From d1a4f8029b2d89f19e4652c96e3b1909b90aa5df Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 09:54:15 +0800 Subject: [PATCH 378/652] Don't use multibuild variables before invoking multibuild. --- .github/workflows/wheels-dependencies.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 9ce851f0f..fd13930db 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,4 +1,14 @@ #!/bin/bash + +# Setup that needs to be done before multibuild utils are invoked +if [[ "$(uname -s)" == "Darwin" ]]; then + # Build and install macOS builds into the `build/deps` folder. + BUILD_PREFIX=$(pwd)/build/deps +else + export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} + export MB_ML_VER=${AUDITWHEEL_POLICY:9} +fi + # Define custom utilities export PLAT=$CIBW_ARCHS source wheels/multibuild/common_utils.sh @@ -7,16 +17,6 @@ if [ -z "$IS_MACOS" ]; then source wheels/multibuild/manylinux_utils.sh fi -# Test for macOS with [ -n "$IS_MACOS" ] -echo "IS MACOS: $IS_MACOS" -if [ -z "$IS_MACOS" ]; then - export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} - export MB_ML_VER=${AUDITWHEEL_POLICY:9} -elseac - # Build and install macOS builds into the `build/deps` folder. - BUILD_PREFIX=$(pwd)/build/deps -fi - ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds From 6d137043f712d6790eba835a8ec727e0a4fbb603 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 10:01:46 +0800 Subject: [PATCH 379/652] Remove stray debug. --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d7e305fc0..38d793b14 100644 --- a/setup.py +++ b/setup.py @@ -131,7 +131,7 @@ _LIB_IMAGING = ( "codec_fd", ) -DEBUG = True +DEBUG = False class DependencyException(Exception): @@ -479,6 +479,7 @@ class pil_build_ext(build_ext): pkg_config = None if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): pkg_config = _pkg_config + # # add configured kits for root_name, lib_name in { From 2d1d801ec032bfb760bede88da6dcaab5ff3c75e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Oct 2024 22:15:56 +1100 Subject: [PATCH 380/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 87945bc84..9d45e2214 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Detach PyQt6 QPixmap instance before returning #8509 + [radarhere] + - Corrected EMF DPI #8485 [radarhere] From 624848ff97e438e5ae7bd239254edceaa2751af1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Oct 2024 22:21:53 +1100 Subject: [PATCH 381/652] Do not repeatedly save to the same path --- Tests/test_file_jpeg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 2c66652e5..c41a61f4b 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -543,10 +543,10 @@ class TestFileJpeg: ) def test_qtables(self, tmp_path: Path) -> None: def _n_qtables_helper(n: int, test_file: str) -> None: + b = BytesIO() with Image.open(test_file) as im: - f = str(tmp_path / "temp.jpg") - im.save(f, qtables=[[n] * 64] * n) - with Image.open(f) as im: + im.save(b, "JPEG", qtables=[[n] * 64] * n) + with Image.open(b) as im: assert len(im.quantization) == n reloaded = self.roundtrip(im, qtables="keep") assert im.quantization == reloaded.quantization From 80cf74030d533a5ebc8e921ae8094316123b6837 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:13:01 +1100 Subject: [PATCH 382/652] Removed fixture Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- 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 c41a61f4b..347a162a5 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -541,7 +541,7 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_qtables(self, tmp_path: Path) -> None: + def test_qtables(self) -> None: def _n_qtables_helper(n: int, test_file: str) -> None: b = BytesIO() with Image.open(test_file) as im: From c6912f81ff20211c2638be64bc198024aa9a5b40 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 30 Oct 2024 05:43:23 +0800 Subject: [PATCH 383/652] Corrected typo in code comment. Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index e671c8d45..9bcf42458 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -4,7 +4,7 @@ set -e # Ensure fribidi is installed by the system. if [[ "$OSTYPE" == "darwin"* ]]; then # If Homebrew is on the path during the build, it may leak into the wheels. - # However, we need a *do* need Homebrew to provide a copy of fribidi for + # However, we *do* need Homebrew to provide a copy of fribidi for # testing purposes so that we can verify the fribidi shim works as expected. if [[ "$(uname -m)" == "x86_64" ]]; then HOMEBREW_HOME=/usr/local/homebrew From 67c2e04f706f9f7f8efa6bd6437632ab8e546cae Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 30 Oct 2024 08:37:19 +0200 Subject: [PATCH 384/652] Add trove-classifiers>=2024.10.12 to 'tests' extra and use for Windows CI --- .github/workflows/test-windows.yml | 13 +------------ pyproject.toml | 1 + 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f6d0aeb1d..4ac6f8769 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -69,17 +69,6 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: Install Python dependencies - run: > - python3 -m pip install - coverage>=7.4.2 - defusedxml - olefile - pyroma - pytest - pytest-cov - pytest-timeout - - name: Install CPython dependencies if: "!contains(matrix.python-version, 'pypy')" run: > @@ -184,7 +173,7 @@ jobs: - name: Build Pillow run: | $FLAGS="-C raqm=vendor -C fribidi=vendor" - cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." + cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS .[tests]" & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh diff --git a/pyproject.toml b/pyproject.toml index c55be7693..45d5c2692 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ optional-dependencies.tests = [ "pytest", "pytest-cov", "pytest-timeout", + "trove-classifiers>=2024.10.12", ] optional-dependencies.typing = [ "typing-extensions; python_version<'3.10'", From 0bf15f0f2a856907bf925fc797a107a33069b734 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:24:37 +0200 Subject: [PATCH 385/652] Upgrade pip --- .github/workflows/test-windows.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 4ac6f8769..728182b1e 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -71,9 +71,9 @@ jobs: - name: Install CPython dependencies if: "!contains(matrix.python-version, 'pypy')" - run: > - python3 -m pip install - PyQt6 + run: | + python3 -m pip install --upgrade pip + python3 -m pip install PyQt6 - name: Install dependencies id: install From 01270b5859800355b12fa4403c7684d9bf72c6f5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 31 Oct 2024 06:27:54 +0800 Subject: [PATCH 386/652] Use the intended entry point for the x86_64 brew binary. Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 9bcf42458..e2ef33711 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -7,7 +7,7 @@ if [[ "$OSTYPE" == "darwin"* ]]; then # However, we *do* need Homebrew to provide a copy of fribidi for # testing purposes so that we can verify the fribidi shim works as expected. if [[ "$(uname -m)" == "x86_64" ]]; then - HOMEBREW_HOME=/usr/local/homebrew + HOMEBREW_HOME=/usr/local else HOMEBREW_HOME=/opt/homebrew fi From 51e36230660667df38c68379314b652b34da47e5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 31 Oct 2024 09:50:38 +0800 Subject: [PATCH 387/652] Revert x86_64 homebrew location change (with explanation). --- .github/workflows/wheels-test.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index e2ef33711..b697b740e 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -7,7 +7,10 @@ if [[ "$OSTYPE" == "darwin"* ]]; then # However, we *do* need Homebrew to provide a copy of fribidi for # testing purposes so that we can verify the fribidi shim works as expected. if [[ "$(uname -m)" == "x86_64" ]]; then - HOMEBREW_HOME=/usr/local + # Use the "installed" location, rather than /usr/local, for two reasons: + # firstly, Homebrew allows libraries to be *installed*, but not linked; + # and secondly, we don't want any *other* leakage from /usr/local. + HOMEBREW_HOME=/usr/local/Homebrew else HOMEBREW_HOME=/opt/homebrew fi From aae90c3c016ee36b838d1ca5d522301d98731261 Mon Sep 17 00:00:00 2001 From: "Pietro F. Fontana" Date: Fri, 1 Nov 2024 15:42:08 +0100 Subject: [PATCH 388/652] Allow linking to shared zlib If compiled as shared library zlib produces `zlib1.dll` and `zdll.lib`. --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index def341784..48863c397 100644 --- a/setup.py +++ b/setup.py @@ -690,6 +690,8 @@ class pil_build_ext(build_ext): feature.set("zlib", "z") elif sys.platform == "win32" and _find_library_file(self, "zlib"): feature.set("zlib", "zlib") # alternative name + elif sys.platform == "win32" and _find_library_file(self, "zdll"): + feature.set("zlib", "zdll") # different name if shared if feature.want("jpeg"): _dbg("Looking for jpeg") From 71016f23b4b441536a5c713d3cad53e2870655fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Nov 2024 17:51:01 +1100 Subject: [PATCH 389/652] Added Fedora 41 --- .github/workflows/test-docker.yml | 1 + docs/installation/platform-support.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 880fe3eea..5bfbe1bdb 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -47,6 +47,7 @@ jobs: debian-12-bookworm-x86, debian-12-bookworm-amd64, fedora-40-amd64, + fedora-41-amd64, gentoo, ubuntu-22.04-jammy-amd64, ubuntu-24.04-noble-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 21ebd1ad5..da6763736 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -31,6 +31,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 40 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 41 | 3.13 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 13 Ventura | 3.9 | x86-64 | From 141e8d25462cfaa56e6d707b704c832b950b86bf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 2 Nov 2024 12:08:23 +0200 Subject: [PATCH 390/652] Remove unused 'gcov: true' for codecov-action@v4 --- .github/workflows/test-docker.yml | 1 - .github/workflows/test.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 5bfbe1bdb..101807745 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -102,7 +102,6 @@ jobs: with: flags: GHA_Docker name: ${{ matrix.docker }} - gcov: true token: ${{ secrets.CODECOV_ORG_TOKEN }} success: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6576292b5..29949f4e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -158,7 +158,6 @@ jobs: with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} - gcov: true token: ${{ secrets.CODECOV_ORG_TOKEN }} success: From 6fe7160cb97f13a93735a365ff33b5f388d8067d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Sat, 2 Nov 2024 14:03:53 +0100 Subject: [PATCH 391/652] Update Windows 11 Arm64 tested versions --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 21ebd1ad5..d8671ee14 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -146,7 +146,7 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+----------------------------+------------------+--------------+ | FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 | +| Windows 11 23H2 | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm64 | +----------------------------------+----------------------------+------------------+--------------+ | Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ From 327e13ffd0d4999fb9cd63d12407f08f20710677 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 6 Oct 2024 00:57:58 +0300 Subject: [PATCH 392/652] Stop testing on AppVeyor --- .appveyor.yml | 99 ----------------------------------------- .github/CONTRIBUTING.md | 4 +- .github/mergify.yml | 1 - MANIFEST.in | 1 - README.md | 3 -- RELEASING.md | 4 +- Tests/helper.py | 9 ---- docs/about.rst | 3 +- docs/index.rst | 4 -- winbuild/README.md | 4 +- winbuild/build.rst | 4 +- 11 files changed, 9 insertions(+), 127 deletions(-) delete mode 100644 .appveyor.yml diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 781ad4a4b..000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,99 +0,0 @@ -skip_commits: - files: - - ".github/**/*" - - ".gitmodules" - - "docs/**/*" - - "wheels/**/*" - -version: '{build}' -clone_folder: c:\pillow -init: -- ECHO %PYTHON% -#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) -# Uncomment previous line to get RDP access during the build. - -environment: - COVERAGE_CORE: sysmon - EXECUTABLE: python.exe - TEST_OPTIONS: - DEPLOY: YES - matrix: - - PYTHON: C:/Python313 - ARCHITECTURE: x86 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - - PYTHON: C:/Python39-x64 - ARCHITECTURE: AMD64 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - - -install: -- '%PYTHON%\%EXECUTABLE% --version' -- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' -- 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\* c:\pillow\tests\images -- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip -- 7z x nasm-win64.zip -oc:\ -- choco install ghostscript --version=10.4.0 -- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.04.0\bin;%PATH% -- cd c:\pillow\winbuild\ -- ps: | - c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ - c:\pillow\winbuild\build\build_dep_all.cmd - $host.SetShouldExit(0) -- path C:\pillow\winbuild\build\bin;%PATH% - -build_script: -- cd c:\pillow -- winbuild\build\build_env.cmd -- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .' -- '%PYTHON%\%EXECUTABLE% selftest.py --installed' - -test_script: -- cd c:\pillow -- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma' -- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% -- path %PYTHON%;%PATH% -- .ci\test.cmd - -after_test: -- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe -- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor - -matrix: - fast_finish: true - -cache: -- '%LOCALAPPDATA%\pip\Cache' - -artifacts: -- path: pillow\*.egg - name: egg -- path: pillow\*.whl - name: wheel - -before_deploy: - - cd c:\pillow - - '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .' - - ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } - -deploy: - provider: S3 - region: us-west-2 - access_key_id: AKIAIRAXC62ZNTVQJMOQ - secret_access_key: - secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi - bucket: pillow-nightly - folder: win/$(APPVEYOR_BUILD_NUMBER)/ - artifact: /.*egg|wheel/ - on: - APPVEYOR_REPO_NAME: python-pillow/Pillow - branch: main - deploy: YES - - -# Uncomment the following lines to get RDP access after the build/test and block for -# up to the timeout limit (~1hr) -# -#on_finish: -#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d03fcf0d9..c372da7d2 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -9,7 +9,7 @@ Please send a pull request to the `main` branch. Please include [documentation]( - Fork the Pillow repository. - Create a branch from `main`. - Develop bug fixes, features, tests, etc. -- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. +- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. - Create a pull request to pull the changes from your branch to the Pillow `main`. ### Guidelines @@ -17,7 +17,7 @@ Please send a pull request to the `main` branch. Please include [documentation]( - Separate code commits from reformatting commits. - Provide tests for any newly added code. - Follow PEP 8. -- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. +- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running extra tests. - 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. diff --git a/.github/mergify.yml b/.github/mergify.yml index 3c2066137..9bb089615 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -9,7 +9,6 @@ pull_request_rules: - status-success=Windows Test Successful - status-success=MinGW - status-success=Cygwin Test Successful - - status-success=continuous-integration/appveyor/pr actions: merge: method: merge diff --git a/MANIFEST.in b/MANIFEST.in index af25dfd2d..48085b82e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -20,7 +20,6 @@ graft docs graft _custom_build # build/src control detritus -exclude .appveyor.yml exclude .clang-format exclude .coveragerc exclude .editorconfig diff --git a/README.md b/README.md index 5bbebaccb..c6e61453f 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,6 @@ As of 2019, Pillow development is GitHub Actions build status (Test Docker) - AppVeyor CI build status (Windows) GitHub Actions build status (Wheels) diff --git a/RELEASING.md b/RELEASING.md index 9e6ec5dd4..490f6d6bd 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -9,7 +9,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Develop and prepare release in `main` branch. -* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. +* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch. * [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Update `CHANGES.rst`. @@ -40,7 +40,7 @@ Released as needed for security, installation or critical bug fixes. git checkout -t remotes/origin/5.2.x ``` * [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. -* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`. +* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in release branch e.g. `5.2.x`. * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Run pre-release check via `make release-test`. * [ ] Create tag for release e.g.: diff --git a/Tests/helper.py b/Tests/helper.py index d6a93a803..cf1ef1997 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -327,16 +327,7 @@ def magick_command() -> list[str] | None: return None -def on_appveyor() -> bool: - return "APPVEYOR" in os.environ - - -def on_github_actions() -> bool: - return "GITHUB_ACTIONS" in os.environ - - def on_ci() -> bool: - # GitHub Actions and AppVeyor have "CI" return "CI" in os.environ diff --git a/docs/about.rst b/docs/about.rst index c51ddebd0..7df895b8f 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -6,12 +6,11 @@ Goals The fork author's goal is to foster and support active development of PIL through: -- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_ +- Continuous integration testing via `GitHub Actions`_ - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions -.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow .. _GitHub: https://github.com/python-pillow/Pillow .. _Python Package Index: https://pypi.org/project/pillow/ diff --git a/docs/index.rst b/docs/index.rst index 18f5c3d13..689088d48 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,10 +33,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Sun, 6 Oct 2024 20:16:55 +0300 Subject: [PATCH 393/652] Test the oldest Python on 32-bit Windows 2019 --- .github/workflows/test-windows.yml | 14 ++++++++++---- docs/installation/platform-support.rst | 4 +--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f6d0aeb1d..2e76c05ae 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -31,15 +31,20 @@ env: jobs: build: - runs-on: windows-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy3.10", "3.10", "3.11", "3.12", "3.13"] + architecture: ["x64"] + os: ["windows-latest"] + include: + # Test the oldest Python on 32-bit + - { python-version: "3.9", architecture: "x86", os: "windows-2019" } timeout-minutes: 30 - name: Python ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) steps: - name: Checkout Pillow @@ -63,6 +68,7 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true + architecture: ${{ matrix.architecture }} cache: pip cache-dependency-path: ".github/workflows/test-windows.yml" @@ -81,7 +87,7 @@ jobs: pytest-timeout - name: Install CPython dependencies - if: "!contains(matrix.python-version, 'pypy')" + if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'" run: > python3 -m pip install PyQt6 diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index a0bada7b4..cd196e4f8 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -48,13 +48,11 @@ These platforms are built and tested for every change. | Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, | | | | s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2019 | 3.9 | x86-64 | +| Windows Server 2019 | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 | | | 3.12, 3.13, PyPy3 | | | +----------------------------+---------------------+ -| | 3.13 | x86 | -| +----------------------------+---------------------+ | | 3.9 (MinGW) | x86-64 | | +----------------------------+---------------------+ | | 3.9 (Cygwin) | x86-64 | From 9faf598c890ced5828ba13ac7f5445b4310fff0c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 2 Nov 2024 23:29:56 +0200 Subject: [PATCH 394/652] Fix warning[artipacked]: credential persistence through GitHub Actions artifacts --- .github/workflows/docs.yml | 2 ++ .github/workflows/lint.yml | 2 ++ .github/workflows/test-cygwin.yml | 2 ++ .github/workflows/test-docker.yml | 2 ++ .github/workflows/test-mingw.yml | 2 ++ .github/workflows/test-valgrind.yml | 2 ++ .github/workflows/test-windows.yml | 4 ++++ .github/workflows/test.yml | 2 ++ .github/workflows/wheels.yml | 7 +++++++ 9 files changed, 25 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 92e860cb5..626824f38 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -33,6 +33,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cc4760288..8e789a734 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,6 +21,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: pre-commit cache uses: actions/cache@v4 diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 0aa79e423..656054e89 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -48,6 +48,8 @@ jobs: - name: Checkout Pillow uses: actions/checkout@v4 + with: + persist-credentials: false - name: Install Cygwin uses: cygwin/cygwin-install-action@v4 diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 101807745..03608319a 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -65,6 +65,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Build system information run: python3 .github/workflows/system-info.py diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index c7a73439c..bfd393db5 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -46,6 +46,8 @@ jobs: steps: - name: Checkout Pillow uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up shell run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 63aec586b..8818b3b23 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -40,6 +40,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Build system information run: python3 .github/workflows/system-info.py diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f6d0aeb1d..c1ba52719 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -44,16 +44,20 @@ jobs: steps: - name: Checkout Pillow uses: actions/checkout@v4 + with: + persist-credentials: false - name: Checkout cached dependencies uses: actions/checkout@v4 with: + persist-credentials: false repository: python-pillow/pillow-depends path: winbuild\depends - name: Checkout extra test images uses: actions/checkout@v4 with: + persist-credentials: false repository: python-pillow/test-images path: Tests\test-images diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 29949f4e0..87acd7ddb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,6 +63,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 34452fa56..45f186341 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -61,6 +61,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: true - uses: actions/setup-python@v5 @@ -132,6 +133,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: true - uses: actions/setup-python@v5 @@ -173,10 +175,13 @@ jobs: - cibw_arch: ARM64 steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Checkout extra test images uses: actions/checkout@v4 with: + persist-credentials: false repository: python-pillow/test-images path: Tests\test-images @@ -253,6 +258,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@v5 From d3db931f21bcaa723071c7b4d669eead0fbdabab Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 2 Nov 2024 23:31:21 +0200 Subject: [PATCH 395/652] Fix error[excessive-permissions]: overly broad workflow or job-level permissions --- .github/workflows/stale.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 545c2e364..61ccf58e2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: permissions: - issues: write + contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -15,6 +15,8 @@ concurrency: jobs: stale: if: github.repository_owner == 'python-pillow' + permissions: + issues: write runs-on: ubuntu-latest From 924df0ac5c44815b397f375c7bed7a69ba02b956 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 Nov 2024 23:22:57 +0000 Subject: [PATCH 396/652] Migrate config .github/renovate.json --- .github/renovate.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index d1d824335..f48b670ec 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,7 +1,7 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base" + "config:recommended" ], "labels": [ "Dependency" @@ -9,9 +9,13 @@ "packageRules": [ { "groupName": "github-actions", - "matchManagers": ["github-actions"], - "separateMajorMinor": "false" + "matchManagers": [ + "github-actions" + ], + "separateMajorMinor": false } ], - "schedule": ["on the 3rd day of the month"] + "schedule": [ + "on the 3rd day of the month" + ] } From 37679c8673e44f31be060ff7bc6772c7f178bee7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 4 Nov 2024 20:55:00 +1100 Subject: [PATCH 397/652] Pass IFDs to libtiff as TIFF_LONG8 --- src/PIL/TiffImagePlugin.py | 4 +++- src/encode.c | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6bf39b75a..c95b65b8e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1915,7 +1915,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if not getattr(Image.core, "libtiff_support_custom_tags", False): continue - if tag in ifd.tagtype: + if tag in TiffTags.TAGS_V2_GROUPS: + types[tag] = TiffTags.LONG8 + elif tag in ifd.tagtype: types[tag] = ifd.tagtype[tag] elif not (isinstance(value, (int, float, str, bytes))): continue diff --git a/src/encode.c b/src/encode.c index 1a4cd489d..09a86a715 100644 --- a/src/encode.c +++ b/src/encode.c @@ -736,7 +736,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { } if (tag_type) { int type_int = PyLong_AsLong(tag_type); - if (type_int >= TIFF_BYTE && type_int <= TIFF_DOUBLE) { + if (type_int >= TIFF_BYTE && type_int <= TIFF_LONG8) { type = (TIFFDataType)type_int; } } @@ -929,7 +929,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { ); } else if (type == TIFF_LONG) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value) + &encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value) ); } else if (type == TIFF_SSHORT) { status = ImagingLibTiffSetField( @@ -959,6 +959,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) ); + } else if (type == TIFF_LONG8) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value) + ); } else { TRACE( ("Unhandled type for key %d : %s \n", From 4b7f6a6eb083b3c112f4713cd1fb74bd93ee3e7c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:35:35 +0000 Subject: [PATCH 398/652] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.9 → v0.7.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.9...v0.7.2) - [github.com/psf/black-pre-commit-mirror: 24.8.0 → 24.10.0](https://github.com/psf/black-pre-commit-mirror/compare/24.8.0...24.10.0) - [github.com/pre-commit/mirrors-clang-format: v19.1.1 → v19.1.3](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.1...v19.1.3) - [github.com/python-jsonschema/check-jsonschema: 0.29.3 → 0.29.4](https://github.com/python-jsonschema/check-jsonschema/compare/0.29.3...0.29.4) - [github.com/tox-dev/pyproject-fmt: 2.2.4 → v2.5.0](https://github.com/tox-dev/pyproject-fmt/compare/2.2.4...v2.5.0) - [github.com/abravalheri/validate-pyproject: v0.20.2 → v0.22](https://github.com/abravalheri/validate-pyproject/compare/v0.20.2...v0.22) --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6254b8941..ddc98fdc3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.7.2 hooks: - id: ruff args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.8.0 + rev: 24.10.0 hooks: - id: black @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.1 + rev: v19.1.3 hooks: - id: clang-format types: [c] @@ -50,7 +50,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.3 + rev: 0.29.4 hooks: - id: check-github-workflows - id: check-readthedocs @@ -62,12 +62,12 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.2.4 + rev: v2.5.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.20.2 + rev: v0.22 hooks: - id: validate-pyproject additional_dependencies: [trove-classifiers>=2024.10.12] From 5628213ab03b59e2a70087c65cf4f05aa8de8cdf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:17:47 +0200 Subject: [PATCH 399/652] Upgrade pip for all --- .github/workflows/test-windows.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 728182b1e..9fcb8cb94 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -69,10 +69,13 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py + - name: Upgrade pip + run: | + python3 -m pip install --upgrade pip + - name: Install CPython dependencies if: "!contains(matrix.python-version, 'pypy')" run: | - python3 -m pip install --upgrade pip python3 -m pip install PyQt6 - name: Install dependencies From b6413cd58818c6518ede6638a391bac15d812db0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 07:16:49 +1100 Subject: [PATCH 400/652] Cast to uint64_t --- src/encode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encode.c b/src/encode.c index 09a86a715..d369a1b45 100644 --- a/src/encode.c +++ b/src/encode.c @@ -961,7 +961,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { ); } else if (type == TIFF_LONG8) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value) + &encoder->state, (ttag_t)key_int, (uint64_t)PyLong_AsLongLong(value) ); } else { TRACE( From 2d23a84049eb22f62107eb5f4cbe7c421b074d12 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 17:18:46 +1100 Subject: [PATCH 401/652] Fixed type hint --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index def341784..1a8c03eb3 100644 --- a/setup.py +++ b/setup.py @@ -1001,7 +1001,7 @@ def debug_build() -> bool: return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD -files = ["src/_imaging.c"] +files: list[str | os.PathLike[str]] = ["src/_imaging.c"] for src_file in _IMAGING: files.append("src/" + src_file + ".c") for src_file in _LIB_IMAGING: From 48db4a1d4d1baec7fdd77474db00029f24c2cef1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 19:40:03 +1100 Subject: [PATCH 402/652] Use test image filename --- docs/reference/ImageFile.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index fdfeb60f9..64abd71d1 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -19,7 +19,7 @@ Example: Parse an image from PIL import ImageFile - fp = open("hopper.pgm", "rb") + fp = open("hopper.ppm", "rb") p = ImageFile.Parser() From 3cdaee45f50a1a4c020c382b516299a2bb531ec7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 20:15:23 +1100 Subject: [PATCH 403/652] Raise UnidentifiedImageError when opening TIFF without dimensions --- Tests/test_imagefile.py | 13 +++++++++++++ src/PIL/TiffImagePlugin.py | 8 ++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 1ee684926..8bef90ce4 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -93,6 +93,19 @@ class TestImageFile: assert p.image is not None assert (48, 48) == p.image.size + @pytest.mark.filterwarnings("ignore:Corrupt EXIF data") + def test_incremental_tiff(self) -> None: + with ImageFile.Parser() as p: + with open("Tests/images/hopper.tif", "rb") as f: + p.feed(f.read(1024)) + + # Check that insufficient data was given in the first feed + assert not p.image + + p.feed(f.read()) + assert p.image is not None + assert (128, 128) == p.image.size + @skip_unless_feature("webp") def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6bf39b75a..d4dcab286 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1433,8 +1433,12 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) # size - xsize = self.tag_v2.get(IMAGEWIDTH) - ysize = self.tag_v2.get(IMAGELENGTH) + try: + xsize = self.tag_v2[IMAGEWIDTH] + ysize = self.tag_v2[IMAGELENGTH] + except KeyError as e: + msg = "Missing dimensions" + raise TypeError(msg) from e if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) From a1d862f1d67e621213e0239d3e45838abeab363e Mon Sep 17 00:00:00 2001 From: "Pietro F. Fontana" Date: Tue, 5 Nov 2024 15:39:03 +0100 Subject: [PATCH 404/652] Be more specific in comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 48863c397..68252f3a9 100644 --- a/setup.py +++ b/setup.py @@ -691,7 +691,7 @@ class pil_build_ext(build_ext): elif sys.platform == "win32" and _find_library_file(self, "zlib"): feature.set("zlib", "zlib") # alternative name elif sys.platform == "win32" and _find_library_file(self, "zdll"): - feature.set("zlib", "zdll") # different name if shared + feature.set("zlib", "zdll") # dll import library if feature.want("jpeg"): _dbg("Looking for jpeg") From e82b5398e750c5dd37dfcc2edaf6b947371b53ef Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 6 Nov 2024 14:12:07 +0800 Subject: [PATCH 405/652] Correct handling of vendored fribidi. --- .github/workflows/wheels-dependencies.sh | 46 ++++++++++++++++-------- .github/workflows/wheels-test.sh | 8 +++-- .gitignore | 4 +++ pyproject.toml | 4 +-- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index fd13930db..ff06e99d2 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,16 +1,34 @@ #!/bin/bash # Setup that needs to be done before multibuild utils are invoked +PROJECTDIR=$(pwd) if [[ "$(uname -s)" == "Darwin" ]]; then - # Build and install macOS builds into the `build/deps` folder. - BUILD_PREFIX=$(pwd)/build/deps + # Safety check - macOS builds require that CIBW_ARCHS is set, and that it + # only contains a single value (even though cwbuildwheel) allows multiple + # values in CIBW_ARCHS. + if [[ -z "$CIBW_ARCHS" ]]; then + echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." + exit 1 + fi + if [[ "$CIBW_ARCHS" == *" "* ]]; then + echo "ERROR: Pillow macOS builds only support a single architecture in CIBW_ARCHS." + exit 1 + fi + + # Build macOS dependencies in `build/darwin` + # Install them into `build/deps/darwin` + WORKDIR=$(pwd)/build/darwin + BUILD_PREFIX=$(pwd)/build/deps/darwin + PLAT=$CIBW_ARCHS else - export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} - export MB_ML_VER=${AUDITWHEEL_POLICY:9} + # Build prefix will default to /usr/local + WORKDIR=$(pwd)/build + PLAT=$CIBW_ARCHS + MB_ML_LIBC=${AUDITWHEEL_POLICY::9} + MB_ML_VER=${AUDITWHEEL_POLICY:9} fi # Define custom utilities -export PLAT=$CIBW_ARCHS source wheels/multibuild/common_utils.sh source wheels/multibuild/library_builders.sh if [ -z "$IS_MACOS" ]; then @@ -117,9 +135,6 @@ function build { ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" - if [[ -n "$IS_MACOS" ]]; then - CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" - fi build_libwebp CFLAGS=$ORIGINAL_CFLAGS @@ -127,7 +142,7 @@ function build { if [ -n "$IS_MACOS" ]; then # Custom freetype build - build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no + build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --without-harfbuzz else build_freetype fi @@ -136,17 +151,18 @@ function build { } # Perform all dependency builds in the build subfolder. -mkdir -p build -pushd build > /dev/null +mkdir -p $WORKDIR +pushd $WORKDIR > /dev/null # Any stuff that you need to do before you start building the wheels # Runs in the root directory of this repository. -if [[ ! -d pillow-depends-main ]]; then - if [[ ! -f pillow-depends-main.zip ]]; then +if [[ ! -d $WORKDIR/pillow-depends-main ]]; then + if [[ ! -f $PROJECTDIR/pillow-depends-main.zip ]]; then echo "Download pillow dependency sources..." - curl -fSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip + curl -fSL -o $PROJECTDIR/pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip fi - untar pillow-depends-main.zip + echo "Unpacking pillow dependency sources..." + untar $PROJECTDIR/pillow-depends-main.zip fi if [[ -n "$IS_MACOS" ]]; then diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index b697b740e..d1db8e8a5 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -16,8 +16,12 @@ if [[ "$OSTYPE" == "darwin"* ]]; then fi $HOMEBREW_HOME/bin/brew install fribidi - # Add the Homebrew lib folder so that vendored libraries can be found. - export DYLD_LIBRARY_PATH=$HOMEBREW_HOME/lib + # Add the lib folder for fribidi so that the vendored library can be found. + # Don't use /opt/homebrew/lib directly - use the lib folder where the + # installed copy of fribidi is cellared. This ensures we don't pick up the + # Homebrew version of any other library that we're dependent on (most notably, + # freetype). + export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_HOME/lib/libfribidi.dylib)) elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then apk add curl fribidi else diff --git a/.gitignore b/.gitignore index 9b5773257..3033c2ea7 100644 --- a/.gitignore +++ b/.gitignore @@ -91,5 +91,9 @@ Tests/images/msp Tests/images/picins Tests/images/sunraster +# Test and dependency downloads +pillow-depends-main.zip +pillow-test-images.zip + # pyinstaller *.spec diff --git a/pyproject.toml b/pyproject.toml index da8548dcb..26fa45817 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,13 +96,13 @@ build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" # Add an explicit dependencies prefix for macOS. -macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable dependencies-prefix=./build/deps" +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable dependencies-prefix=./build/deps/darwin" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" [tool.cibuildwheel.macos.environment] -PATH = "$(pwd)/build/deps/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" [tool.black] exclude = "wheels/multibuild" From b0345c1c7b7605063064118563e8068fde5f9343 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 Nov 2024 22:08:22 +1100 Subject: [PATCH 406/652] Updated macOS tested Pillow versions --- docs/installation/platform-support.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index a0bada7b4..b9d633142 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -75,7 +75,9 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+============================+==================+==============+ -| macOS 15 Sequoia | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | +| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm | +| +----------------------------+------------------+ | +| | 3.8 | 10.4.0 | | +----------------------------------+----------------------------+------------------+--------------+ | macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | +----------------------------------+----------------------------+------------------+--------------+ From 042f3ff083d98d211c2035dd45e511a63800d06e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 7 Nov 2024 07:52:18 +1100 Subject: [PATCH 407/652] Require coverage>=7.4.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 45d5c2692..bff295bc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ optional-dependencies.mic = [ ] optional-dependencies.tests = [ "check-manifest", - "coverage", + "coverage>=7.4.2", "defusedxml", "markdown2", "olefile", From a262b1991b439574578b60fac932b7065e02f0b6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 7 Nov 2024 12:54:28 +0200 Subject: [PATCH 408/652] Update winbuild/README.md Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- winbuild/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/winbuild/README.md b/winbuild/README.md index 0d3ec8d8a..c474f12ce 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -11,7 +11,8 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. * Requires CMake 3.15 or newer (available as Visual Studio component). -* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). +* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise and Windows Server + 2019 with Visual Studio 2019 Enterprise (GitHub Actions). Here's an example script to build on Windows: From 09bf28e9d7dbe20c13dc05ebe62a9e706abd273c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:46:08 +0200 Subject: [PATCH 409/652] Update platform support Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/installation/platform-support.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 4e8620fc4..373708a61 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -50,8 +50,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Windows Server 2019 | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | +| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 | +| | PyPy3 | | | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86-64 | | +----------------------------+---------------------+ From 4e35852ccc97f3e455f5130f1cd22f2f10292955 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 8 Nov 2024 05:34:35 +0800 Subject: [PATCH 410/652] Correct typo in comment. Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index ff06e99d2..2dea8c62a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -4,7 +4,7 @@ PROJECTDIR=$(pwd) if [[ "$(uname -s)" == "Darwin" ]]; then # Safety check - macOS builds require that CIBW_ARCHS is set, and that it - # only contains a single value (even though cwbuildwheel) allows multiple + # only contains a single value (even though cibuildwheel) allows multiple # values in CIBW_ARCHS. if [[ -z "$CIBW_ARCHS" ]]; then echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." From 0a5b0f29e58cf9763b886e3ce300a79b5f4bac63 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Nov 2024 00:18:59 +1100 Subject: [PATCH 411/652] Run gcc problem matcher on Python 3.13 --- .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 87acd7ddb..c33309bbf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -116,7 +116,7 @@ jobs: GHA_PYTHON_VERSION: ${{ matrix.python-version }} - name: Register gcc problem matcher - if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'" + if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'" run: echo "::add-matcher::.github/problem-matchers/gcc.json" - name: Build From 681a03b1c38b26a36cddda812ead882b4af24eb4 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 9 Nov 2024 10:29:29 +0800 Subject: [PATCH 412/652] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels-dependencies.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2dea8c62a..f4106429d 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -4,8 +4,8 @@ PROJECTDIR=$(pwd) if [[ "$(uname -s)" == "Darwin" ]]; then # Safety check - macOS builds require that CIBW_ARCHS is set, and that it - # only contains a single value (even though cibuildwheel) allows multiple - # values in CIBW_ARCHS. + # only contains a single value (even though cibuildwheel allows multiple + # values in CIBW_ARCHS). if [[ -z "$CIBW_ARCHS" ]]; then echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." exit 1 @@ -171,7 +171,7 @@ if [[ -n "$IS_MACOS" ]]; then # with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use, # and they may bring in other dependencies that we don't want. The same will # be true of any other locations on the path. To avoid conflicts, strip the - # path down to the bare mimimum (which, on macOS, won't include any + # path down to the bare minimum (which, on macOS, won't include any # development dependencies). export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" export CMAKE_PREFIX_PATH=$BUILD_PREFIX From 9f09d48f3710409725beef1ca1be66da33855922 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Nov 2024 18:20:52 +1100 Subject: [PATCH 413/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9d45e2214..7e8e94ed9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Allow linking to zlib import library on Windows #8519 + [cubanpit, nulano] + - Detach PyQt6 QPixmap instance before returning #8509 [radarhere] From 7674b1a07a1dfdf1c0944aea1fefb95c4f62f5b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Nov 2024 18:43:12 +1100 Subject: [PATCH 414/652] Removed quotes after dropping support for Python 3.8 --- src/PIL/_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 0a7d87cc2..335804b79 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -47,7 +47,7 @@ class SupportsRead(Protocol[_T_co]): def read(self, __length: int = ...) -> _T_co: ... -StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] +StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] __all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] From 2303018ebf751b7d6bc9d0e3d830cdd5f529a5b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Nov 2024 20:06:33 +1100 Subject: [PATCH 415/652] Removed use of os.path.realpath --- src/PIL/Image.py | 6 +++--- src/PIL/ImageFile.py | 2 +- src/PIL/ImageFont.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 44270392c..9b76ce8bd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2550,7 +2550,7 @@ class Image: filename: str | bytes = "" open_fp = False if is_path(fp): - filename = os.path.realpath(os.fspath(fp)) + filename = os.fspath(fp) open_fp = True elif fp == sys.stdout: try: @@ -2559,7 +2559,7 @@ class Image: pass if not filename and hasattr(fp, "name") and is_path(fp.name): # only set the name for metadata purposes - filename = os.path.realpath(os.fspath(fp.name)) + filename = os.fspath(fp.name) # may mutate self! self._ensure_mutable() @@ -3463,7 +3463,7 @@ def open( exclusive_fp = False filename: str | bytes = "" if is_path(fp): - filename = os.path.realpath(os.fspath(fp)) + filename = os.fspath(fp) if filename: fp = builtins.open(filename, "rb") diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index d69d84568..7f27d54dc 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -130,7 +130,7 @@ class ImageFile(Image.Image): if is_path(fp): # filename self.fp = open(fp, "rb") - self.filename = os.path.realpath(os.fspath(fp)) + self.filename = os.fspath(fp) self._exclusive_fp = True else: # stream diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b694b817e..d8c265560 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -270,7 +270,7 @@ class FreeTypeFont: ) if is_path(font): - font = os.path.realpath(os.fspath(font)) + font = os.fspath(font) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: From 5bff2f3b2894ec6923c590d0c37b18177d0634bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 10 Nov 2024 21:32:54 +1100 Subject: [PATCH 416/652] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7e8e94ed9..f6ed5eb82 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Removed use of os.path.realpath #8545 + [radarhere] + - Allow linking to zlib import library on Windows #8519 [cubanpit, nulano] From 378df7a5b22e47a0652619bd2f3218674c579d91 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 12 Nov 2024 22:41:57 +1100 Subject: [PATCH 417/652] Disable platform guessing instead of adding dependencies-prefix --- pyproject.toml | 4 +-- setup.py | 93 ++++++++++++++++++++++---------------------------- 2 files changed, 42 insertions(+), 55 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 26fa45817..abb7be645 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,8 +95,8 @@ before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" -# Add an explicit dependencies prefix for macOS. -macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable dependencies-prefix=./build/deps/darwin" +# Disable platform guessing on macOS +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" diff --git a/setup.py b/setup.py index e4ce04883..4770c5eb7 100644 --- a/setup.py +++ b/setup.py @@ -344,11 +344,6 @@ class pil_build_ext(build_ext): for x in ("raqm", "fribidi") ] + [ - ( - "dependencies-prefix", - None, - "The prefix where build dependencies are located.", - ), ("disable-platform-guessing", None, "Disable platform guessing on Linux"), ("debug", None, "Debug logging"), ] @@ -360,7 +355,6 @@ class pil_build_ext(build_ext): return True if value in configuration.get(option, []) else None def initialize_options(self) -> None: - self.dependencies_prefix = configuration.get("dependencies-prefix", []) self.disable_platform_guessing = self.check_configuration( "platform-guessing", "disable" ) @@ -570,54 +564,47 @@ class pil_build_ext(build_ext): ) elif sys.platform == "darwin": - if self.dependencies_prefix: - # Use the explicitly provided prefixes for dependencies. - for prefix in self.dependencies_prefix: - _add_directory(library_dirs, os.path.join(prefix, "lib")) - _add_directory(include_dirs, os.path.join(prefix, "include")) + # attempt to make sure we pick freetype2 over other versions + _add_directory(include_dirs, "/sw/include/freetype2") + _add_directory(include_dirs, "/sw/lib/freetype2/include") + # fink installation directories + _add_directory(library_dirs, "/sw/lib") + _add_directory(include_dirs, "/sw/include") + # darwin ports installation directories + _add_directory(library_dirs, "/opt/local/lib") + _add_directory(include_dirs, "/opt/local/include") + + # if Homebrew is installed, use its lib and include directories + try: + prefix = ( + subprocess.check_output(["brew", "--prefix"]) + .strip() + .decode("latin1") + ) + except Exception: + # Homebrew not installed + prefix = None + + ft_prefix = None + + if prefix: + # add Homebrew's include and lib directories + _add_directory(library_dirs, os.path.join(prefix, "lib")) + _add_directory(include_dirs, os.path.join(prefix, "include")) + _add_directory( + include_dirs, os.path.join(prefix, "opt", "zlib", "include") + ) + ft_prefix = os.path.join(prefix, "opt", "freetype") + + if ft_prefix and os.path.isdir(ft_prefix): + # freetype might not be linked into Homebrew's prefix + _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) + _add_directory(include_dirs, os.path.join(ft_prefix, "include")) else: - # Guess the dependency locations based on homebrew/fink/macports - # attempt to make sure we pick freetype2 over other versions - _add_directory(include_dirs, "/sw/include/freetype2") - _add_directory(include_dirs, "/sw/lib/freetype2/include") - # fink installation directories - _add_directory(library_dirs, "/sw/lib") - _add_directory(include_dirs, "/sw/include") - # darwin ports installation directories - _add_directory(library_dirs, "/opt/local/lib") - _add_directory(include_dirs, "/opt/local/include") - - # if Homebrew is installed, use its lib and include directories - try: - prefix = ( - subprocess.check_output(["brew", "--prefix"]) - .strip() - .decode("latin1") - ) - except Exception: - # Homebrew not installed - prefix = None - - ft_prefix = None - - if prefix: - # add Homebrew's include and lib directories - _add_directory(library_dirs, os.path.join(prefix, "lib")) - _add_directory(include_dirs, os.path.join(prefix, "include")) - _add_directory( - include_dirs, os.path.join(prefix, "opt", "zlib", "include") - ) - ft_prefix = os.path.join(prefix, "opt", "freetype") - - if ft_prefix and os.path.isdir(ft_prefix): - # freetype might not be linked into Homebrew's prefix - _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) - _add_directory(include_dirs, os.path.join(ft_prefix, "include")) - else: - # fall back to freetype from XQuartz if - # Homebrew's freetype is missing - _add_directory(library_dirs, "/usr/X11/lib") - _add_directory(include_dirs, "/usr/X11/include") + # fall back to freetype from XQuartz if + # Homebrew's freetype is missing + _add_directory(library_dirs, "/usr/X11/lib") + _add_directory(include_dirs, "/usr/X11/include") # Add the macOS SDK path. sdk_path = self.get_macos_sdk_path() From 6fa775e324a27dac54ff8a9249553e06e1928051 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 12 Nov 2024 22:46:24 +1100 Subject: [PATCH 418/652] Platform guessing affects more than just Linux --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e78f87fe9..fea21ee3f 100644 --- a/setup.py +++ b/setup.py @@ -344,7 +344,7 @@ class pil_build_ext(build_ext): for x in ("raqm", "fribidi") ] + [ - ("disable-platform-guessing", None, "Disable platform guessing on Linux"), + ("disable-platform-guessing", None, "Disable platform guessing"), ("debug", None, "Debug logging"), ] + [("add-imaging-libs=", None, "Add libs to _imaging build")] From 9dc690427f009f898ef74b8799569346eb0b3fd5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 13 Nov 2024 11:03:26 +0800 Subject: [PATCH 419/652] Correct the lookup of libfribidi on x86 macOS installs. --- .github/workflows/wheels-test.sh | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index d1db8e8a5..ce83a4278 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -7,21 +7,18 @@ if [[ "$OSTYPE" == "darwin"* ]]; then # However, we *do* need Homebrew to provide a copy of fribidi for # testing purposes so that we can verify the fribidi shim works as expected. if [[ "$(uname -m)" == "x86_64" ]]; then - # Use the "installed" location, rather than /usr/local, for two reasons: - # firstly, Homebrew allows libraries to be *installed*, but not linked; - # and secondly, we don't want any *other* leakage from /usr/local. - HOMEBREW_HOME=/usr/local/Homebrew + HOMEBREW_PREFIX=/usr/local else - HOMEBREW_HOME=/opt/homebrew + HOMEBREW_PREFIX=/opt/homebrew fi - $HOMEBREW_HOME/bin/brew install fribidi + $HOMEBREW_PREFIX/bin/brew install fribidi # Add the lib folder for fribidi so that the vendored library can be found. - # Don't use /opt/homebrew/lib directly - use the lib folder where the + # Don't use $HOMEWBREW_PREFIX/lib directly - use the lib folder where the # installed copy of fribidi is cellared. This ensures we don't pick up the # Homebrew version of any other library that we're dependent on (most notably, # freetype). - export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_HOME/lib/libfribidi.dylib)) + export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_PREFIX/lib/libfribidi.dylib)) elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then apk add curl fribidi else From 48c7eb22c0aeba649892dcb4766600f6dcfb6bdc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 13 Nov 2024 22:45:52 +1100 Subject: [PATCH 420/652] Added default value for _Tile args --- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/FliImagePlugin.py | 2 +- src/PIL/ImageFile.py | 2 +- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PcdImagePlugin.py | 2 +- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/XbmImagePlugin.py | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index fb1e301c0..35cc623e7 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -454,7 +454,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) - if hasattr(fp, "flush"): fp.flush() - ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0)]) fp.write(b"\n%%%%EndBinary\n") fp.write(b"grestore end\n") diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 666390be9..b534b30ab 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -159,7 +159,7 @@ class FliImageFile(ImageFile.ImageFile): framesize = i32(s) self.decodermaxblock = framesize - self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset, None)] + self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)] self.__offset += framesize diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 7f27d54dc..6c07c85e7 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -99,7 +99,7 @@ class _Tile(NamedTuple): codec_name: str extents: tuple[int, int, int, int] | None offset: int - args: tuple[Any, ...] | str | None + args: tuple[Any, ...] | str | None = None # diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index f3460a787..50fb6a2d9 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -72,7 +72,7 @@ class MspImageFile(ImageFile.ImageFile): if s[:4] == b"DanM": self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))] else: - self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32, None)] + self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] class MspDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index e8ea800a4..ac40383f9 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -47,7 +47,7 @@ class PcdImageFile(ImageFile.ImageFile): self._mode = "RGB" self._size = 768, 512 # FIXME: not correct for rotated images! - self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048, None)] + self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)] def load_end(self) -> None: if self.tile_post_rotate: diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 010d3f941..01cc868b2 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -32,7 +32,7 @@ class QoiImageFile(ImageFile.ImageFile): self._mode = "RGB" if channels == 3 else "RGBA" self.fp.seek(1, os.SEEK_CUR) # colorspace - self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell(), None)] + self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())] class QoiDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index f3d490a84..cf1a8b9f5 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -67,7 +67,7 @@ class XbmImageFile(ImageFile.ImageFile): self._mode = "1" self._size = xsize, ysize - self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end(), None)] + self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end())] def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: @@ -85,7 +85,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0)]) fp.write(b"};\n") From 871963b8ddab57fec9050135a5b58bceedce23db Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 13 Nov 2024 22:53:18 +1100 Subject: [PATCH 421/652] Replaced tuple args with mode string where equivalent --- src/PIL/BlpImagePlugin.py | 2 +- src/PIL/DdsImagePlugin.py | 4 +--- src/PIL/FpxImagePlugin.py | 2 +- src/PIL/FtexImagePlugin.py | 2 +- src/PIL/GdImageFile.py | 2 +- src/PIL/ImtImagePlugin.py | 2 +- src/PIL/MspImagePlugin.py | 4 ++-- src/PIL/PixarImagePlugin.py | 4 +--- src/PIL/SpiderImagePlugin.py | 8 ++------ src/PIL/XVThumbImagePlugin.py | 4 +--- src/PIL/XpmImagePlugin.py | 4 +--- 11 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index e5605635e..2d03af9d7 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -273,7 +273,7 @@ class BlpImageFile(ImageFile.ImageFile): raise BLPFormatError(msg) self._mode = "RGBA" if self._blp_alpha_depth else "RGB" - self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] + self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, self.mode)] class _BLPBaseDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 1b6408237..9349e2841 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -560,9 +560,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + struct.pack("<4I", *rgba_mask) # dwRGBABitMask + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) ) - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] - ) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) def _accept(prefix: bytes) -> bool: diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 8fef51076..4cfcb067d 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -170,7 +170,7 @@ class FpxImageFile(ImageFile.ImageFile): "raw", (x, y, x1, y1), i32(s, i) + 28, - (self.rawmode,), + self.rawmode, ) ) diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index ddb469bc3..0516b760c 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -95,7 +95,7 @@ class FtexImageFile(ImageFile.ImageFile): self._mode = "RGBA" self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] elif format == Format.UNCOMPRESSED: - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")] else: msg = f"Invalid texture compression format: {repr(format)}" raise ValueError(msg) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index f1b4969f2..fc4801e9d 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -76,7 +76,7 @@ class GdImageFile(ImageFile.ImageFile): "raw", (0, 0) + self.size, 7 + true_color_offset + 4 + 256 * 4, - ("L", 0, 1), + "L", ) ] diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 594c56513..068cd5c33 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -62,7 +62,7 @@ class ImtImageFile(ImageFile.ImageFile): "raw", (0, 0) + self.size, self.fp.tell() - len(buffer), - (self.mode, 0, 1), + self.mode, ) ] diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 50fb6a2d9..ef6ae87f8 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -70,7 +70,7 @@ class MspImageFile(ImageFile.ImageFile): self._size = i16(s, 4), i16(s, 6) if s[:4] == b"DanM": - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")] else: self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] @@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(o16(h)) # image body - ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")]) # diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 36f565f1c..5c465bbdc 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -61,9 +61,7 @@ class PixarImageFile(ImageFile.ImageFile): # FIXME: to be continued... # create tile descriptor (assuming "dumped") - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1)) - ] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1024, self.mode)] # diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 075073f9f..d7f457ae7 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -154,9 +154,7 @@ class SpiderImageFile(ImageFile.ImageFile): self.rawmode = "F;32F" self._mode = "F" - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1)) - ] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, offset, self.rawmode)] self._fp = self.fp # FIXME: hack @property @@ -280,9 +278,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.writelines(hdr) rawmode = "F;32NF" # 32-bit native floating point - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] - ) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 5d1f201a4..75333354d 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -74,9 +74,7 @@ class XVThumbImageFile(ImageFile.ImageFile): self.palette = ImagePalette.raw("RGB", PALETTE) self.tile = [ - ImageFile._Tile( - "raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1) - ) + ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), self.mode) ] diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 1fc6c0c39..b985aa5dc 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -101,9 +101,7 @@ class XpmImageFile(ImageFile.ImageFile): self._mode = "P" self.palette = ImagePalette.raw("RGB", b"".join(palette)) - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1)) - ] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), "P")] def load_read(self, read_bytes: int) -> bytes: # From 84f5c7e5ba51afbeda6c4aaf42a31cea9ee8b082 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 13 Nov 2024 22:52:36 +1100 Subject: [PATCH 422/652] Added default value for _Tile offset --- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/ImageFile.py | 2 +- src/PIL/XbmImagePlugin.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 35cc623e7..36ba15ec5 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -454,7 +454,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) - if hasattr(fp, "flush"): fp.flush() - ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0)]) + ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)]) fp.write(b"\n%%%%EndBinary\n") fp.write(b"grestore end\n") diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 6c07c85e7..bd0e92eb6 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -98,7 +98,7 @@ def _tilesort(t: _Tile) -> int: class _Tile(NamedTuple): codec_name: str extents: tuple[int, int, int, int] | None - offset: int + offset: int = 0 args: tuple[Any, ...] | str | None = None diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index cf1a8b9f5..943a04470 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -85,7 +85,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0)]) + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size)]) fp.write(b"};\n") From 185a03f1a25ba56ae11d655c04645e0a50710dae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Nov 2024 12:05:06 +1100 Subject: [PATCH 423/652] Do not create new image when exif_transpose() is used in place --- src/PIL/ImageOps.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 44aad0c3c..bb29cc0d3 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -698,10 +698,11 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image 8: Image.Transpose.ROTATE_90, }.get(orientation) if method is not None: - transposed_image = image.transpose(method) if in_place: - image.im = transposed_image.im - image._size = transposed_image._size + image.im = image.im.transpose(method) + image._size = image.im.size + else: + transposed_image = image.transpose(method) exif_image = image if in_place else transposed_image exif = exif_image.getexif() From 54f23345d2fd1894c7cf89475e385fcbb3d3f16d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 16 Nov 2024 09:58:29 +0800 Subject: [PATCH 424/652] More tweaks from code review. --- .github/workflows/wheels-dependencies.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f4106429d..ce2e40103 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -19,14 +19,13 @@ if [[ "$(uname -s)" == "Darwin" ]]; then # Install them into `build/deps/darwin` WORKDIR=$(pwd)/build/darwin BUILD_PREFIX=$(pwd)/build/deps/darwin - PLAT=$CIBW_ARCHS else # Build prefix will default to /usr/local WORKDIR=$(pwd)/build - PLAT=$CIBW_ARCHS MB_ML_LIBC=${AUDITWHEEL_POLICY::9} MB_ML_VER=${AUDITWHEEL_POLICY:9} fi +PLAT=$CIBW_ARCHS # Define custom utilities source wheels/multibuild/common_utils.sh @@ -119,9 +118,9 @@ function build { build_libjpeg_turbo if [ -n "$IS_MACOS" ]; then # Custom tiff build to include jpeg; by default, configure won't include - # headers/libs in the custom macOS prefix. Explicitly disable webp and - # zstd, because on x86_64 macs, it will pick up the Homebrew versions of - # webp and zstd from /usr/local. + # headers/libs in the custom macOS prefix. Explicitly disable webp, + # libdeflate and zstd, because on x86_64 macs, it will pick up the + # Homebrew versions of those libraries from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ --disable-webp --disable-zstd --disable-libdeflate From 6136a92d061e77e622c974c35aa0605f96eb05e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Nov 2024 09:48:28 +0000 Subject: [PATCH 425/652] Update codecov/codecov-action action to v5 --- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-docker.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 656054e89..b906ada92 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -135,7 +135,7 @@ jobs: bash.exe .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: file: ./coverage.xml flags: GHA_Cygwin diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 03608319a..cc5f9d4a5 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -100,7 +100,7 @@ jobs: MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: flags: GHA_Docker name: ${{ matrix.docker }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index bfd393db5..a14e7e1ed 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -85,7 +85,7 @@ jobs: .ci/test.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 9b5da137d..d1924eb20 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -215,7 +215,7 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c33309bbf..1405c64f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -156,7 +156,7 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From c70dacff62c07341c4bb9d9c28e26e4e703ab5ae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Nov 2024 21:44:00 +1100 Subject: [PATCH 426/652] Updated from deprecated "file" to "files" --- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-windows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index b906ada92..2076cad7a 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -137,7 +137,7 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: GHA_Cygwin name: Cygwin Python 3.${{ matrix.python-minor-version }} token: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a14e7e1ed..cca25edba 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -87,7 +87,7 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: GHA_Windows name: "MSYS2 MinGW" token: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index d1924eb20..d905a3925 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -217,7 +217,7 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: GHA_Windows name: ${{ runner.os }} Python ${{ matrix.python-version }} token: ${{ secrets.CODECOV_ORG_TOKEN }} From 96b898c826982c5b99f2a9387e0d2011a73fe20a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 18 Nov 2024 09:56:45 +0800 Subject: [PATCH 427/652] A couple more cleanups from code review. --- .github/workflows/wheels-dependencies.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index ce2e40103..f86099af1 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -62,8 +62,7 @@ BROTLI_VERSION=1.1.0 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi - # This essentially duplicates the Homebrew recipe: - # https://github.com/Homebrew/homebrew-core/blob/master/Formula/p/pkg-config.rb + # This essentially duplicates the Homebrew recipe ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -Wno-int-conversion" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ @@ -108,7 +107,6 @@ function build { if [ -n "$IS_MACOS" ]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib - build_simple libXdmcp 1.1.5 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc @@ -123,7 +121,7 @@ function build { # Homebrew versions of those libraries from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ - --disable-webp --disable-zstd --disable-libdeflate + --disable-webp --disable-libdeflate --disable-zstd else build_tiff fi @@ -134,6 +132,9 @@ function build { ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" + if [[ -n "$IS_MACOS" ]]; then + CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" + fi build_libwebp CFLAGS=$ORIGINAL_CFLAGS @@ -141,7 +142,7 @@ function build { if [ -n "$IS_MACOS" ]; then # Custom freetype build - build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --without-harfbuzz + build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no else build_freetype fi @@ -175,7 +176,7 @@ if [[ -n "$IS_MACOS" ]]; then export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" export CMAKE_PREFIX_PATH=$BUILD_PREFIX - # Link the brew command into our isolated build directory. + # Ensure the basic structure of the build prefix directory exists. mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/lib" From e02e4aaf1de9503e6e88a60f292998d174d23034 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 19:31:54 +1100 Subject: [PATCH 428/652] Updated harfbuzz to 10.1.0 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f86099af1..89226810a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -38,7 +38,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -HARFBUZZ_VERSION=10.0.1 +HARFBUZZ_VERSION=10.1.0 LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.0.4 OPENJPEG_VERSION=2.5.2 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index c8332d11c..5606ac7f7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -112,7 +112,7 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "10.0.1", + "HARFBUZZ": "10.1.0", "JPEGTURBO": "3.0.4", "LCMS2": "2.16", "LIBPNG": "1.6.44", From c7a9582eab34bc46b74c245aa137424b9cfaee74 Mon Sep 17 00:00:00 2001 From: Tom Flanagan Date: Tue, 19 Nov 2024 20:40:14 -0800 Subject: [PATCH 429/652] Fix file position desync when calling into libtiff --- Tests/test_libtiff_file_position.py | 23 +++++++++++++++++++++++ src/PIL/TiffImagePlugin.py | 17 +++++++++-------- 2 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 Tests/test_libtiff_file_position.py diff --git a/Tests/test_libtiff_file_position.py b/Tests/test_libtiff_file_position.py new file mode 100644 index 000000000..d614ea8ec --- /dev/null +++ b/Tests/test_libtiff_file_position.py @@ -0,0 +1,23 @@ +from __future__ import annotations + + +import pytest + +from PIL import Image + + +@pytest.mark.parametrize('test_file', [ + 'Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif', + 'Tests/images/old-style-jpeg-compression.tif', +]) +def test_libtiff_exif_loading(test_file) -> None: + # loading image before exif + im1 = Image.open(open(test_file, 'rb', buffering=1048576)) + im1.load() + exif1 = dict(im1.getexif()) + + # loading exif before image + im2 = Image.open(open(test_file, 'rb', buffering=1048576)) + exif2 = dict(im2.getexif()) + + assert exif1 == exif2 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6bf39b75a..726286772 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1216,10 +1216,6 @@ class TiffImageFile(ImageFile.ImageFile): def _seek(self, frame: int) -> None: self.fp = self._fp - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - self.fp.tell() - while len(self._frame_pos) <= frame: if not self.__next: msg = "no more images in TIFF file" @@ -1303,10 +1299,6 @@ class TiffImageFile(ImageFile.ImageFile): if not self.is_animated: self._close_exclusive_fp_after_loading = True - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - self.fp.tell() - # load IFD data from fp before it is closed exif = self.getexif() for key in TiffTags.TAGS_V2_GROUPS: @@ -1381,8 +1373,17 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("have fileno, calling fileno version of the decoder.") if not close_self_fp: self.fp.seek(0) + # Save and restore the file position, because libtiff will move it + # outside of the python runtime, and that will confuse + # io.BufferedReader and possible others. + # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), + # because the buffer read head already may not equal the actual + # file position, and fp.seek() may just adjust it's internal + # pointer and not actually seek the OS file handle. + pos = os.lseek(fp, 0, os.SEEK_CUR) # 4 bytes, otherwise the trace might error out n, err = decoder.decode(b"fpfp") + os.lseek(fp, pos, os.SEEK_SET) else: # we have something else. logger.debug("don't have fileno or getvalue. just reading") From 44cc0bebb8998897f03d77479df750f208d9d00e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 05:52:50 +0000 Subject: [PATCH 430/652] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_libtiff_file_position.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Tests/test_libtiff_file_position.py b/Tests/test_libtiff_file_position.py index d614ea8ec..43ed82c30 100644 --- a/Tests/test_libtiff_file_position.py +++ b/Tests/test_libtiff_file_position.py @@ -1,23 +1,25 @@ from __future__ import annotations - import pytest from PIL import Image -@pytest.mark.parametrize('test_file', [ - 'Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif', - 'Tests/images/old-style-jpeg-compression.tif', -]) +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif", + "Tests/images/old-style-jpeg-compression.tif", + ], +) def test_libtiff_exif_loading(test_file) -> None: # loading image before exif - im1 = Image.open(open(test_file, 'rb', buffering=1048576)) + im1 = Image.open(open(test_file, "rb", buffering=1048576)) im1.load() exif1 = dict(im1.getexif()) # loading exif before image - im2 = Image.open(open(test_file, 'rb', buffering=1048576)) + im2 = Image.open(open(test_file, "rb", buffering=1048576)) exif2 = dict(im2.getexif()) assert exif1 == exif2 From cb1653f6272ebc543530ae03cd7cf8577ed13231 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 20 Nov 2024 22:33:23 +1100 Subject: [PATCH 431/652] Updated comment --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 726286772..08bb78915 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1374,7 +1374,7 @@ class TiffImageFile(ImageFile.ImageFile): if not close_self_fp: self.fp.seek(0) # Save and restore the file position, because libtiff will move it - # outside of the python runtime, and that will confuse + # outside of the Python runtime, and that will confuse # io.BufferedReader and possible others. # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), # because the buffer read head already may not equal the actual From 925db4552658ac0d8c037b0ac0b39bd7eb96f8f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 20 Nov 2024 22:40:29 +1100 Subject: [PATCH 432/652] Moved test --- Tests/test_file_libtiff.py | 19 +++++++++++++++++++ Tests/test_libtiff_file_position.py | 25 ------------------------- 2 files changed, 19 insertions(+), 25 deletions(-) delete mode 100644 Tests/test_libtiff_file_position.py diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 62f8719af..9c49b1534 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1098,6 +1098,25 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_similar(base_im, im, 0.7) + @pytest.mark.parametrize( + "test_file", + [ + "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif", + "Tests/images/old-style-jpeg-compression.tif", + ], + ) + def test_buffering(self, test_file: str) -> None: + # load exif first + with Image.open(open(test_file, "rb", buffering=1048576)) as im: + exif = dict(im.getexif()) + + # load image before exif + with Image.open(open(test_file, "rb", buffering=1048576)) as im2: + im2.load() + exif_after_load = dict(im2.getexif()) + + assert exif == exif_after_load + @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") def test_sampleformat_not_corrupted(self) -> None: # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted diff --git a/Tests/test_libtiff_file_position.py b/Tests/test_libtiff_file_position.py deleted file mode 100644 index 43ed82c30..000000000 --- a/Tests/test_libtiff_file_position.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - - -@pytest.mark.parametrize( - "test_file", - [ - "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif", - "Tests/images/old-style-jpeg-compression.tif", - ], -) -def test_libtiff_exif_loading(test_file) -> None: - # loading image before exif - im1 = Image.open(open(test_file, "rb", buffering=1048576)) - im1.load() - exif1 = dict(im1.getexif()) - - # loading exif before image - im2 = Image.open(open(test_file, "rb", buffering=1048576)) - exif2 = dict(im2.getexif()) - - assert exif1 == exif2 From 82dfbc35b190a818a1cbdbee6524eaf31605b7bd Mon Sep 17 00:00:00 2001 From: Tom Flanagan Date: Wed, 20 Nov 2024 05:21:14 -0800 Subject: [PATCH 433/652] Update src/PIL/TiffImagePlugin.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 726286772..08bb78915 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1374,7 +1374,7 @@ class TiffImageFile(ImageFile.ImageFile): if not close_self_fp: self.fp.seek(0) # Save and restore the file position, because libtiff will move it - # outside of the python runtime, and that will confuse + # outside of the Python runtime, and that will confuse # io.BufferedReader and possible others. # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), # because the buffer read head already may not equal the actual From bfe40b5129e9250243b1d425172c18f7c4e15e10 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 21 Nov 2024 19:22:39 +1100 Subject: [PATCH 434/652] Prevent Cygwin bash from being used by codecov-action --- .github/workflows/test-cygwin.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 2076cad7a..5b0a03946 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -133,6 +133,7 @@ jobs: - name: After success run: | bash.exe .ci/after_success.sh + rm C:\cygwin\bin\bash.EXE - name: Upload coverage uses: codecov/codecov-action@v5 From 69c9a7ffcfd42b0486f02f17119c6d05f1d04969 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 20 Nov 2024 20:03:15 +1100 Subject: [PATCH 435/652] Use zlib-ng on Linux --- .github/workflows/wheels-dependencies.sh | 20 +++++++++++++++++--- Tests/check_wheel.py | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f86099af1..5d23fc036 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -50,10 +50,10 @@ if [[ -n "$IS_MACOS" ]]; then else GIFLIB_VERSION=5.2.1 fi -if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then +if [[ -n "$IS_MACOS" ]]; then ZLIB_VERSION=1.3.1 else - ZLIB_VERSION=1.2.8 + ZLIB_NG_VERSION=2.2.2 fi LIBWEBP_VERSION=1.4.0 BZIP2_VERSION=1.0.8 @@ -74,6 +74,16 @@ function build_pkg_config { touch pkg-config-stamp } +function build_zlib_ng { + if [ -e zlib-stamp ]; then return; fi + fetch_unpack https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz + (cd zlib-ng-$ZLIB_NG_VERSION \ + && ./configure --prefix=$BUILD_PREFIX --zlib-compat \ + && make -j4 \ + && make install) + touch zlib-stamp +} + function build_brotli { if [ -e brotli-stamp ]; then return; fi local cmake=$(get_modern_cmake) @@ -101,7 +111,11 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - build_new_zlib + if [ -n "$IS_MACOS" ]; then + build_new_zlib + else + build_zlib_ng + fi build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 374e48e8a..787d4f743 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -40,7 +40,7 @@ def test_wheel_features() -> None: if sys.platform == "win32": expected_features.remove("xcb") - else: + elif sys.platform == "darwin": expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features From 9c3d8fb581cab18abb60117f08358b51b18a44b0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Nov 2024 11:02:50 +1100 Subject: [PATCH 436/652] Do not install Pyroma, to use only system packages --- .github/workflows/test-mingw.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index bfd393db5..fd8faba71 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -68,12 +68,13 @@ jobs: mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-olefile \ + mingw-w64-x86_64-python3-pip \ mingw-w64-x86_64-python3-setuptools \ + mingw-w64-x86_64-python-pytest \ + mingw-w64-x86_64-python-pytest-cov \ + mingw-w64-x86_64-python-pytest-timeout \ mingw-w64-x86_64-python-pyqt6 - python3 -m ensurepip - python3 -m pip install pyroma pytest pytest-cov pytest-timeout - pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow From 30f03b2bff689af1f395c183cf991bc34f8b0bab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Nov 2024 11:05:33 +1100 Subject: [PATCH 437/652] Removed SETUPTOOLS_USE_DISTUTILS="stdlib" --- .github/workflows/test-mingw.yml | 3 +-- docs/installation/building-from-source.rst | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index fd8faba71..93fe8a064 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -69,7 +69,6 @@ jobs: mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-olefile \ mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools \ mingw-w64-x86_64-python-pytest \ mingw-w64-x86_64-python-pytest-cov \ mingw-w64-x86_64-python-pytest-timeout \ @@ -78,7 +77,7 @@ jobs: pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow - run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install . + run: CFLAGS="-coverage" python3 -m pip install . - name: Test Pillow run: | diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 4b5175827..19294f72c 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -195,11 +195,6 @@ 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 16e409a8d28fecbf83cd67409224e65ebb371a93 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 24 Nov 2024 13:46:28 +0200 Subject: [PATCH 438/652] Test 3.13t (free-threaded) from Quansight-Labs/setup-python on Linux/macOS --- .github/workflows/test.yml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1405c64f9..83a696f5f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,7 @@ jobs: ] python-version: [ "pypy3.10", + "3.13t", "3.13", "3.12", "3.11", @@ -52,14 +53,14 @@ jobs: - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.10", PYTHONOPTIMIZE: 2 } # Free-threaded - - { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true } + - { python-version: "3.13t", disable-gil: true } # M1 only available for 3.10+ - { os: "macos-13", python-version: "3.9" } exclude: - { os: "macos-latest", python-version: "3.9" } runs-on: ${{ matrix.os }} - name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v4 @@ -67,8 +68,7 @@ jobs: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - if: "${{ !matrix.disable-gil }}" + uses: Quansight-Labs/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -77,13 +77,6 @@ jobs: ".ci/*.sh" "pyproject.toml" - - name: Set up Python ${{ matrix.python-version }} (free-threaded) - uses: deadsnakes/action@v3.2.0 - if: "${{ matrix.disable-gil }}" - with: - python-version: ${{ matrix.python-version }} - nogil: ${{ matrix.disable-gil }} - - name: Set PYTHON_GIL if: "${{ matrix.disable-gil }}" run: | From 540a0b73a7ebb3ec5571e3ea027c73cbe95b2fc6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 23 Nov 2024 00:20:40 +1100 Subject: [PATCH 439/652] Use installed cmake --- .github/workflows/wheels-dependencies.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f86099af1..ddda43423 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -76,10 +76,9 @@ function build_pkg_config { function build_brotli { if [ -e brotli-stamp ]; then return; fi - local cmake=$(get_modern_cmake) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ - && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ + && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) touch brotli-stamp } From 7dcf4d8ab319348239d4f8695c6d3366bc257cbf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Nov 2024 16:43:21 +1100 Subject: [PATCH 440/652] Added logging to fixIFD() --- src/PIL/TiffImagePlugin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6bf39b75a..a246994ef 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2202,6 +2202,12 @@ class AppendingTiffWriter(io.BytesIO): if tag in self.Tags: cur_pos = self.f.tell() + tagname = TiffTags.lookup(tag).name + typname = TYPES.get(field_type, "unknown") + msg = f"fixIFD: {tagname} ({tag}) - type: {typname} ({field_type})" + msg += f"- type size: {field_size} - count: {count}" + logger.debug(msg) + if is_local: self._fixOffsets(count, field_size) self.f.seek(cur_pos + 4) From 5174c596ca6c78725efdb2cda8ace39f11d8e215 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 26 Nov 2024 22:06:47 +0200 Subject: [PATCH 441/652] Release drafter: move removals, deprecations, documentation up, and uncategorised changes last --- .github/release-drafter.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 3711d91f0..de0ab4805 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -3,18 +3,19 @@ tag-template: "$NEXT_MINOR_VERSION" change-template: '- $TITLE #$NUMBER [@$AUTHOR]' categories: - - title: "Dependencies" - label: "Dependency" + - title: "Removals" + label: "Removal" - title: "Deprecations" label: "Deprecation" - title: "Documentation" label: "Documentation" - - title: "Removals" - label: "Removal" + - title: "Dependencies" + label: "Dependency" - title: "Testing" label: "Testing" - title: "Type hints" label: "Type hints" + - title: "Other changes" exclude-labels: - "changelog: skip" @@ -23,6 +24,4 @@ template: | https://pillow.readthedocs.io/en/stable/releasenotes/$NEXT_MINOR_VERSION.html - ## Changes - $CHANGES From 02250e81b729ddb32952f96e57bf9d952ed70cf8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 28 Nov 2024 07:58:10 +1100 Subject: [PATCH 442/652] Added link to GitHub releases --- CHANGES.rst | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f6ed5eb82..dfbbd24b3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,26 +2,12 @@ Changelog (Pillow) ================== -11.1.0 (unreleased) -------------------- +11.1.0 and newer +---------------- -- Removed use of os.path.realpath #8545 - [radarhere] +See GitHub Releases: -- Allow linking to zlib import library on Windows #8519 - [cubanpit, nulano] - -- Detach PyQt6 QPixmap instance before returning #8509 - [radarhere] - -- Corrected EMF DPI #8485 - [radarhere] - -- Fix IFDRational with a zero denominator #8474 - [radarhere] - -- Fixed disabling a feature during install #8469 - [radarhere] +- https://github.com/python-pillow/Pillow/releases 11.0.0 (2024-10-15) ------------------- From c36a5b669b225fdcb6b4b3a41fc5b5e647a651c1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:29:05 +0200 Subject: [PATCH 443/652] Update/remove references to CHANGES.rst --- .github/CONTRIBUTING.md | 1 - README.md | 2 +- RELEASING.md | 2 -- codecov.yml | 2 +- pyproject.toml | 2 +- 5 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d03fcf0d9..ba2b7d8ed 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,7 +19,6 @@ 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 diff --git a/README.md b/README.md index 5bbebaccb..057d0acf0 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ The core image library is designed for fast access to data stored in a few basic - [Issues](https://github.com/python-pillow/Pillow/issues) - [Pull requests](https://github.com/python-pillow/Pillow/pulls) - [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) -- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) +- [Changelog](https://github.com/python-pillow/Pillow/releases) - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) ## Report a Vulnerability diff --git a/RELEASING.md b/RELEASING.md index 9e6ec5dd4..ebdbb6406 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,7 +12,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. * [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` -* [ ] Update `CHANGES.rst`. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Create branch and tag for release e.g.: ```bash @@ -34,7 +33,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. Released as needed for security, installation or critical bug fixes. * [ ] Make necessary changes in `main` branch. -* [ ] Update `CHANGES.rst`. * [ ] Check out release branch e.g.: ```bash git checkout -t remotes/origin/5.2.x diff --git a/codecov.yml b/codecov.yml index 8646576bb..84920238f 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,7 +1,7 @@ # Documentation: https://docs.codecov.com/docs/codecov-yaml codecov: - # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" + # Avoid "Missing base report" due to committing with "[CI skip]" # https://github.com/codecov/support/issues/363 # https://docs.codecov.com/docs/comparing-commits allow_coverage_offsets: true diff --git a/pyproject.toml b/pyproject.toml index c6d95f2ab..1c7b2c4b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ optional-dependencies.typing = [ optional-dependencies.xmp = [ "defusedxml", ] -urls.Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst" +urls.Changelog = "https://github.com/python-pillow/Pillow/releases" urls.Documentation = "https://pillow.readthedocs.io" urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" urls.Homepage = "https://python-pillow.org" From 4986609938c6fd38af6efd8501fe157b0443e5bc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Nov 2024 23:44:32 +1100 Subject: [PATCH 444/652] Use zlib-ng on macOS --- .github/workflows/wheels-dependencies.sh | 12 ++---------- Tests/check_wheel.py | 2 -- pyproject.toml | 1 + 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 5d23fc036..a2aec07c3 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -50,11 +50,7 @@ if [[ -n "$IS_MACOS" ]]; then else GIFLIB_VERSION=5.2.1 fi -if [[ -n "$IS_MACOS" ]]; then - ZLIB_VERSION=1.3.1 -else - ZLIB_NG_VERSION=2.2.2 -fi +ZLIB_NG_VERSION=2.2.2 LIBWEBP_VERSION=1.4.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 @@ -111,11 +107,7 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - if [ -n "$IS_MACOS" ]; then - build_new_zlib - else - build_zlib_ng - fi + build_zlib_ng build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 787d4f743..563be0b74 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -40,7 +40,5 @@ def test_wheel_features() -> None: if sys.platform == "win32": expected_features.remove("xcb") - elif sys.platform == "darwin": - expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features diff --git a/pyproject.toml b/pyproject.toml index c6d95f2ab..af09ccbc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ test-extras = "tests" [tool.cibuildwheel.macos.environment] PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +DYLD_LIBRARY_PATH = "$(pwd)/build/deps/darwin/lib" [tool.black] exclude = "wheels/multibuild" From 10527918e3570f1e6772ac90db42d2288cce7ae4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Nov 2024 17:13:07 +1100 Subject: [PATCH 445/652] Use brew formula to install libraqm --- docs/installation/building-from-source.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 19294f72c..ff60120ae 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -148,13 +148,7 @@ Many of Pillow's features require external libraries: The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: - brew install libjpeg libtiff little-cms2 openjpeg webp - - To install libraqm on macOS use Homebrew to install its dependencies:: - - brew install freetype harfbuzz fribidi - - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + brew install libjpeg libraqm libtiff little-cms2 openjpeg webp .. tab:: Windows From 1fab591f42fa5a9502fade7873481b567c033849 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Nov 2024 21:17:31 +1100 Subject: [PATCH 446/652] Python 3.12 is tested on MinGW --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index b9d633142..35f863374 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -55,7 +55,7 @@ These platforms are built and tested for every change. | +----------------------------+---------------------+ | | 3.13 | x86 | | +----------------------------+---------------------+ -| | 3.9 (MinGW) | x86-64 | +| | 3.12 (MinGW) | x86-64 | | +----------------------------+---------------------+ | | 3.9 (Cygwin) | x86-64 | +----------------------------------+----------------------------+---------------------+ From 1d15ce8c494075f521600c21b9efb0d5eaa9aa03 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Nov 2024 23:17:42 +1100 Subject: [PATCH 447/652] Updated build option error messages --- setup.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index fbd23a568..eea21fa34 100644 --- a/setup.py +++ b/setup.py @@ -393,13 +393,14 @@ class pil_build_ext(build_ext): self.feature.required.discard(x) _dbg("Disabling %s", x) if getattr(self, f"enable_{x}"): - msg = f"Conflicting options: --enable-{x} and --disable-{x}" + msg = f"Conflicting options: '-C {x}=enable' and '-C {x}=disable'" raise ValueError(msg) if x == "freetype": - _dbg("--disable-freetype implies --disable-raqm") + _dbg("'-C freetype=disable' implies '-C raqm=disable'") if getattr(self, "enable_raqm"): msg = ( - "Conflicting options: --enable-raqm and --disable-freetype" + "Conflicting options: " + "'-C raqm=enable' and '-C freetype=disable'" ) raise ValueError(msg) setattr(self, "disable_raqm", True) @@ -407,15 +408,17 @@ class pil_build_ext(build_ext): _dbg("Requiring %s", x) self.feature.required.add(x) if x == "raqm": - _dbg("--enable-raqm implies --enable-freetype") + _dbg("'-C raqm=enable' implies '-C freetype=enable'") self.feature.required.add("freetype") for x in ("raqm", "fribidi"): if getattr(self, f"vendor_{x}"): if getattr(self, "disable_raqm"): - msg = f"Conflicting options: --vendor-{x} and --disable-raqm" + msg = f"Conflicting options: '-C {x}=vendor' and '-C raqm=disable'" raise ValueError(msg) if x == "fribidi" and not getattr(self, "vendor_raqm"): - msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm" + msg = ( + f"Conflicting options: '-C {x}=vendor' and not '-C raqm=vendor'" + ) raise ValueError(msg) _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) @@ -1047,7 +1050,7 @@ except DependencyException as err: msg = f""" The headers or library files could not be found for {str(err)}, -which was requested by the option flag --enable-{str(err)} +which was requested by the option flag '-C {str(err)}=enable' """ sys.stderr.write(msg) From 513fe2ca2d23c7f1685b089fc6087ab02b2828df Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:15:54 +0200 Subject: [PATCH 448/652] Only use start year in copyright, remove end years --- LICENSE | 2 +- docs/COPYING | 2 +- docs/conf.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 8837c290c..10dd42d9e 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2024 by Jeffrey A. Clark and contributors + Copyright © 2010 by Jeffrey A. Clark and contributors Like PIL, Pillow is licensed under the open source MIT-CMU License: diff --git a/docs/COPYING b/docs/COPYING index d5ee19f81..17fba5b87 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2024 by Jeffrey A. Clark and contributors + Copyright © 2010 by Jeffrey A. Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/conf.py b/docs/conf.py index b81d86c6c..e1e3f1b8f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,7 +55,7 @@ master_doc = "index" project = "Pillow (PIL Fork)" copyright = ( "1995-2011 Fredrik Lundh and contributors, " - "2010-2024 Jeffrey A. Clark and contributors." + "2010 Jeffrey A. Clark and contributors." ) author = "Fredrik Lundh (PIL), Jeffrey A. Clark (Pillow)" From 0000729f2a79da62ba64332484103ffacf1700a1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:34:54 +0000 Subject: [PATCH 449/652] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.7.2 → v0.8.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.2...v0.8.1) - [github.com/PyCQA/bandit: 1.7.10 → 1.8.0](https://github.com/PyCQA/bandit/compare/1.7.10...1.8.0) - [github.com/pre-commit/mirrors-clang-format: v19.1.3 → v19.1.4](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.3...v19.1.4) - [github.com/python-jsonschema/check-jsonschema: 0.29.4 → 0.30.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.29.4...0.30.0) - [github.com/abravalheri/validate-pyproject: v0.22 → v0.23](https://github.com/abravalheri/validate-pyproject/compare/v0.22...v0.23) --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ddc98fdc3..f91260c72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.2 + rev: v0.8.1 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.7.10 + rev: 1.8.0 hooks: - id: bandit args: [--severity-level=high] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.3 + rev: v19.1.4 hooks: - id: clang-format types: [c] @@ -50,7 +50,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.4 + rev: 0.30.0 hooks: - id: check-github-workflows - id: check-readthedocs @@ -67,7 +67,7 @@ repos: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.22 + rev: v0.23 hooks: - id: validate-pyproject additional_dependencies: [trove-classifiers>=2024.10.12] From 0ab21dff1ed0b16944a5458ab73dfa3a5c060b88 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:29:38 +0200 Subject: [PATCH 450/652] Fix new Ruff errors --- src/PIL/ImImagePlugin.py | 2 +- src/PIL/Image.py | 21 ++++++--------------- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/PcxImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 13 +++++++------ src/PIL/_typing.py | 2 +- 7 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index f9f47348c..b4215a0b1 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -357,7 +357,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: name = "".join([name[: 92 - len(ext)], ext]) fp.write(f"Name: {name}\r\n".encode("ascii")) - fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii")) + fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii")) fp.write(f"File size (no of images): {frames}\r\n".encode("ascii")) if im.mode in ["P", "PA"]: fp.write(b"Lut: 1\r\n") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9b76ce8bd..1e289b6c3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -692,13 +692,10 @@ class Image: ) def __repr__(self) -> str: - return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( - self.__class__.__module__, - self.__class__.__name__, - self.mode, - self.size[0], - self.size[1], - id(self), + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"image mode={self.mode} size={self.size[0]}x{self.size[1]} " + f"at 0x{id(self):X}>" ) def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: @@ -707,14 +704,8 @@ class Image: # Same as __repr__ but without unpredictable id(self), # to keep Jupyter notebook `text/plain` output stable. p.text( - "<%s.%s image mode=%s size=%dx%d>" - % ( - self.__class__.__module__, - self.__class__.__name__, - self.mode, - self.size[0], - self.size[1], - ) + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"image mode={self.mode} size={self.size[0]}x{self.size[1]}>" ) def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6510e072e..ab6a2f497 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -72,7 +72,7 @@ def APP(self: JpegImageFile, marker: int) -> None: n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) - app = "APP%d" % (marker & 15) + app = f"APP{marker & 15}" self.app[app] = s # compatibility self.applist.append((app, s)) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 8445d5cc7..32436cea3 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -86,7 +86,7 @@ class PcxImageFile(ImageFile.ImageFile): elif bits == 1 and planes in (2, 4): mode = "P" - rawmode = "P;%dL" % planes + rawmode = f"P;{planes}L" self.palette = ImagePalette.raw("RGB", s[16:64]) elif version == 5 and bits == 8 and planes == 1: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 4e1227204..4b97992a3 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -523,7 +523,7 @@ class PngStream(ChunkStream): assert self.fp is not None s = ImageFile._safe_read(self.fp, length) - raw_vals = struct.unpack(">%dI" % (len(s) // 4), s) + raw_vals = struct.unpack(f">{len(s) // 4}I", s) self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) return s diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 08bb78915..9ee55347a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -935,9 +935,9 @@ class ImageFileDirectory_v2(_IFDv2Base): self._tagdata[tag] = data self.tagtype[tag] = typ - msg += " - value: " + ( - "" % size if size > 32 else repr(data) - ) + bytes_value = size if size > 32 else repr(data) + msg += f" - value: " + logger.debug(msg) (self.next,) = ( @@ -981,9 +981,10 @@ class ImageFileDirectory_v2(_IFDv2Base): tagname = TiffTags.lookup(tag, self.group).name typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") - msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})" - msg += " - value: " + ( - "" % len(data) if len(data) >= 16 else str(values) + bytes_value = len(data) if len(data) >= 16 else str(values) + msg = ( + f"save: {tagname} ({tag}) - type: {typname} ({typ})" + f" - value: " ) logger.debug(msg) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 335804b79..34a9a81e1 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -44,7 +44,7 @@ _T_co = TypeVar("_T_co", covariant=True) class SupportsRead(Protocol[_T_co]): - def read(self, __length: int = ...) -> _T_co: ... + def read(self, length: int = ..., /) -> _T_co: ... StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] From c16ae6fefa6cb939d274c95d1969889dced9dd15 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Dec 2024 09:19:26 +1100 Subject: [PATCH 451/652] Do not describe raw data as a table --- src/PIL/TiffImagePlugin.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 9ee55347a..be4a57b74 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -935,8 +935,8 @@ class ImageFileDirectory_v2(_IFDv2Base): self._tagdata[tag] = data self.tagtype[tag] = typ - bytes_value = size if size > 32 else repr(data) - msg += f" - value: " + msg += " - value: " + msg += f"" if size > 32 else repr(data) logger.debug(msg) @@ -981,11 +981,8 @@ class ImageFileDirectory_v2(_IFDv2Base): tagname = TiffTags.lookup(tag, self.group).name typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") - bytes_value = len(data) if len(data) >= 16 else str(values) - msg = ( - f"save: {tagname} ({tag}) - type: {typname} ({typ})" - f" - value: " - ) + msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: " + msg += f"" if len(data) >= 16 else str(values) logger.debug(msg) # count is sum of lengths for string and arbitrary data From 80c562573a4b5723b1094fb9b07a582912e3d62b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 02:28:52 +0000 Subject: [PATCH 452/652] Update dependency cibuildwheel to v2.22.0 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index c4511439c..833aca23d 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.21.3 +cibuildwheel==2.22.0 From f4a9304bb358f45a088d3c237ada88703e0e0d8e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Dec 2024 13:35:00 +1100 Subject: [PATCH 453/652] Use CIBW_ENABLE --- .github/workflows/wheels.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 45f186341..c5e55aa62 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -85,7 +85,7 @@ jobs: CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" - CIBW_PRERELEASE_PYTHONS: True + CIBW_ENABLE: cpython-prerelease # Extra options for manylinux. CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} @@ -150,10 +150,9 @@ jobs: env: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} - CIBW_FREE_THREADED_SUPPORT: True + CIBW_ENABLE: cpython-prerelease cpython-freethreading CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} - CIBW_PRERELEASE_PYTHONS: True CIBW_SKIP: pp39-* MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} @@ -228,8 +227,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" - CIBW_FREE_THREADED_SUPPORT: True - CIBW_PRERELEASE_PYTHONS: True + CIBW_ENABLE: cpython-prerelease cpython-freethreading CIBW_SKIP: pp39-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm From 3ce33a253a1bd76fdde6eb557cf271b3b2f01553 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Dec 2024 20:14:03 +1100 Subject: [PATCH 454/652] self.tile is not None --- src/PIL/ImageFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 7f27d54dc..9836cd688 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -120,7 +120,7 @@ class ImageFile(Image.Image): self.custom_mimetype: str | None = None self.tile: list[_Tile] = [] - """ A list of tile descriptors, or ``None`` """ + """ A list of tile descriptors """ self.readonly = 1 # until we know better From c5474ed43363e24602c92e5ca7b37899ce37b9a5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 6 Dec 2024 20:26:07 +0200 Subject: [PATCH 455/652] Replace python-pillow.org with python-pillow.github.io --- docs/handbook/tutorial.rst | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 3df8e0d20..f771ae7ae 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -678,7 +678,7 @@ Reading from URL from PIL import Image from urllib.request import urlopen - url = "https://python-pillow.org/assets/images/pillow-logo.png" + url = "https://python-pillow.github.io/assets/images/pillow-logo.png" img = Image.open(urlopen(url)) diff --git a/pyproject.toml b/pyproject.toml index 1c7b2c4b2..aaaba0032 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ optional-dependencies.xmp = [ urls.Changelog = "https://github.com/python-pillow/Pillow/releases" urls.Documentation = "https://pillow.readthedocs.io" urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" -urls.Homepage = "https://python-pillow.org" +urls.Homepage = "https://python-pillow.github.io" urls.Mastodon = "https://fosstodon.org/@pillow" urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" urls.Source = "https://github.com/python-pillow/Pillow" From 2215eaf21d8e9096fdd13c34225c67c0893d82db Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Dec 2024 06:25:48 +1100 Subject: [PATCH 456/652] Updated openjpeg to 2.5.3 --- .github/workflows/wheels-dependencies.sh | 2 +- depends/install_openjpeg.sh | 2 +- docs/installation/building-from-source.rst | 2 +- winbuild/build_prepare.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index ddda43423..e88a010ef 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -41,7 +41,7 @@ FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=10.0.1 LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.0.4 -OPENJPEG_VERSION=2.5.2 +OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index 8c2967bc2..1f8d78193 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -1,7 +1,7 @@ #!/bin/bash # install openjpeg -archive=openjpeg-2.5.2 +archive=openjpeg-2.5.3 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index ff60120ae..03359de31 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -58,7 +58,7 @@ Many of Pillow's features require external libraries: * **openjpeg** provides JPEG 2000 functionality. * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, - **2.4.0**, **2.5.0** and **2.5.2**. + **2.4.0**, **2.5.0**, **2.5.2** and **2.5.3**. * Pillow does **not** support the earlier **1.5** series which ships with Debian Jessie. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index c8332d11c..dc802a3e5 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ V = { "LCMS2": "2.16", "LIBPNG": "1.6.44", "LIBWEBP": "1.4.0", - "OPENJPEG": "2.5.2", + "OPENJPEG": "2.5.3", "TIFF": "4.6.0", "XZ": "5.6.3", "ZLIB": "1.3.1", From 522505b714085ca94a2cab2a71e77a4fc5df7a3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Dec 2024 17:52:34 +1100 Subject: [PATCH 457/652] Support saving CMYK JPEG2000 images --- Tests/test_file_jpeg2k.py | 12 ++++++++++++ src/libImaging/Jpeg2KEncode.c | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index fbf72ae05..8bb290bf3 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -325,6 +325,18 @@ def test_cmyk() -> None: assert im.getpixel((0, 0)) == (185, 134, 0, 0) +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +@skip_unless_feature_version("jpg_2000", "2.5.3") +def test_cmyk_save() -> None: + with Image.open(f"{EXTRA_DIR}/issue205.jp2") as jp2: + assert jp2.mode == "CMYK" + + im = roundtrip(jp2) + assert_image_equal(im, jp2) + + @pytest.mark.parametrize("ext", (".j2k", ".jp2")) def test_16bit_monochrome_has_correct_mode(ext: str) -> None: with Image.open("Tests/images/16bit.cropped" + ext) as im: diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index d30ccde60..34d1a2294 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -330,6 +330,13 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { components = 4; color_space = OPJ_CLRSPC_SRGB; pack = j2k_pack_rgba; +#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \ + (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2) + } else if (strcmp(im->mode, "CMYK") == 0) { + components = 4; + color_space = OPJ_CLRSPC_CMYK; + pack = j2k_pack_rgba; +#endif } else { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; From 9ed96704d3788fa58d9682d293c4190e5da07c2d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Dec 2024 07:28:48 +1100 Subject: [PATCH 458/652] Do not attempt to install cmake if it is already present --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 9a9d1275f..74a9795bc 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 9a9d1275f025f737cdaa3c451ba07129dd95f361 +Subproject commit 74a9795bc64ff786b7e7d33bdec2843cf17e512e From e361930936cd1208beeb292654a3af7548d863b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Dec 2024 18:56:59 +1100 Subject: [PATCH 459/652] Updated libjpeg-turbo to 3.1.0 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 5f8a4cf78..86f51a586 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -40,7 +40,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=10.1.0 LIBPNG_VERSION=1.6.44 -JPEGTURBO_VERSION=3.0.4 +JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1efe0dcac..fd104eb23 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ V = { "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "10.1.0", - "JPEGTURBO": "3.0.4", + "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBPNG": "1.6.44", "LIBWEBP": "1.4.0", @@ -155,7 +155,7 @@ DEPS: dict[str, dict[str, Any]] = { cmd_copy("cjpeg-static.exe", "cjpeg.exe"), cmd_copy("djpeg-static.exe", "djpeg.exe"), ], - "headers": ["j*.h"], + "headers": ["jconfig.h", r"src\j*.h"], "libs": ["libjpeg.lib"], "bins": ["cjpeg.exe", "djpeg.exe"], }, From 642b44c3159077d0863c29181c8c6fbe60039e5b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Dec 2024 01:25:28 +1100 Subject: [PATCH 460/652] Test libjpeg-turbo on macOS --- .github/workflows/macos-install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index ddb421230..2301a3a7e 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -8,8 +8,8 @@ fi brew install \ freetype \ ghostscript \ + jpeg-turbo \ libimagequant \ - libjpeg \ libtiff \ little-cms2 \ openjpeg \ From 0e5f5fcb297b1e08ec83773562663b8ad9e53aee Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Dec 2024 16:33:20 +1100 Subject: [PATCH 461/652] Updated libXau to 1.0.12 --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 86f51a586..36036ff26 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -105,7 +105,7 @@ function build { build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto - build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc From 62d8ccc015d68a9bb3da555f9800786652de4962 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Dec 2024 12:11:41 +1100 Subject: [PATCH 462/652] Derive dir from filename if root is the same --- winbuild/build_prepare.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5bdb5fafe..2d804d557 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -131,7 +131,6 @@ DEPS: dict[str, dict[str, Any]] = { "libjpeg": { "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/FILENAME/download", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", - "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" @@ -161,7 +160,6 @@ DEPS: dict[str, dict[str, Any]] = { "zlib": { "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.zip", "filename": f"zlib-ng-{V['ZLIBNG']}.zip", - "dir": f"zlib-ng-{V['ZLIBNG']}", "license": "LICENSE.md", "patch": { r"CMakeLists.txt": { @@ -179,7 +177,6 @@ DEPS: dict[str, dict[str, Any]] = { "xz": { "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/FILENAME", "filename": f"xz-{V['XZ']}.tar.gz", - "dir": f"xz-{V['XZ']}", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -192,7 +189,6 @@ DEPS: dict[str, dict[str, Any]] = { "libwebp": { "url": "http://downloads.webmproject.org/releases/webp/FILENAME", "filename": f"libwebp-{V['LIBWEBP']}.tar.gz", - "dir": f"libwebp-{V['LIBWEBP']}", "license": "COPYING", "patch": { r"src\enc\picture_csp_enc.c": { @@ -214,7 +210,6 @@ DEPS: dict[str, dict[str, Any]] = { "libtiff": { "url": "https://download.osgeo.org/libtiff/FILENAME", "filename": f"tiff-{V['TIFF']}.tar.gz", - "dir": f"tiff-{V['TIFF']}", "license": "LICENSE.md", "patch": { r"libtiff\tif_lzma.c": { @@ -247,7 +242,6 @@ DEPS: dict[str, dict[str, Any]] = { "url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/" f"lpng{V['LIBPNG_DOTLESS']}.zip/download", "filename": f"lpng{V['LIBPNG_DOTLESS']}.zip", - "dir": f"lpng{V['LIBPNG_DOTLESS']}", "license": "LICENSE", "build": [ *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), @@ -261,7 +255,6 @@ DEPS: dict[str, dict[str, Any]] = { "brotli": { "url": f"https://github.com/google/brotli/archive/refs/tags/v{V['BROTLI']}.tar.gz", "filename": f"brotli-{V['BROTLI']}.tar.gz", - "dir": f"brotli-{V['BROTLI']}", "license": "LICENSE", "build": [ *cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -272,7 +265,6 @@ DEPS: dict[str, dict[str, Any]] = { "freetype": { "url": "https://download.savannah.gnu.org/releases/freetype/FILENAME", "filename": f"freetype-{V['FREETYPE']}.tar.gz", - "dir": f"freetype-{V['FREETYPE']}", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { @@ -307,7 +299,6 @@ DEPS: dict[str, dict[str, Any]] = { "lcms2": { "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/FILENAME/download", "filename": f"lcms2-{V['LCMS2']}.tar.gz", - "dir": f"lcms2-{V['LCMS2']}", "license": "LICENSE", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { @@ -333,7 +324,6 @@ DEPS: dict[str, dict[str, Any]] = { "openjpeg": { "url": f"https://github.com/uclouvain/openjpeg/archive/v{V['OPENJPEG']}.tar.gz", "filename": f"openjpeg-{V['OPENJPEG']}.tar.gz", - "dir": f"openjpeg-{V['OPENJPEG']}", "license": "LICENSE", "build": [ *cmds_cmake( @@ -348,7 +338,6 @@ DEPS: dict[str, dict[str, Any]] = { # commit: Merge branch 'master' into msvc (matches 2.17.0 tag) "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip", - "dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab", "license": "COPYRIGHT", "patch": { "CMakeLists.txt": { @@ -368,7 +357,6 @@ DEPS: dict[str, dict[str, Any]] = { "harfbuzz": { "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", "filename": f"harfbuzz-{V['HARFBUZZ']}.zip", - "dir": f"harfbuzz-{V['HARFBUZZ']}", "license": "COPYING", "build": [ *cmds_cmake( @@ -383,7 +371,6 @@ DEPS: dict[str, dict[str, Any]] = { "fribidi": { "url": f"https://github.com/fribidi/fribidi/archive/v{V['FRIBIDI']}.zip", "filename": f"fribidi-{V['FRIBIDI']}.zip", - "dir": f"fribidi-{V['FRIBIDI']}", "license": "COPYING", "build": [ cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['FRIBIDI']}-COPYING"), @@ -763,6 +750,8 @@ def main() -> None: } for k, v in DEPS.items(): + if "dir" not in v: + v["dir"] = re.sub(r"\.(tar\.gz|zip)", "", v["filename"]) prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) print() From 6373e8bcc470ffa8849a17fa8e90e1146e0c3c6c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Dec 2024 22:58:35 +1100 Subject: [PATCH 463/652] Use .tar.gz zlib to match macOS and Linux --- winbuild/build_prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5bdb5fafe..a68a0b132 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -159,8 +159,8 @@ DEPS: dict[str, dict[str, Any]] = { "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.zip", - "filename": f"zlib-ng-{V['ZLIBNG']}.zip", + "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.tar.gz", + "filename": f"zlib-ng-{V['ZLIBNG']}.tar.gz", "dir": f"zlib-ng-{V['ZLIBNG']}", "license": "LICENSE.md", "patch": { From 7f6ebfb8c597d8d1a074993f463992b6dac34c41 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Dec 2024 23:40:38 +1100 Subject: [PATCH 464/652] Added release notes for #8500 --- docs/releasenotes/11.1.0.rst | 59 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 60 insertions(+) create mode 100644 docs/releasenotes/11.1.0.rst diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst new file mode 100644 index 000000000..c5d0afd58 --- /dev/null +++ b/docs/releasenotes/11.1.0.rst @@ -0,0 +1,59 @@ +11.1.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Check for zlib-ng +^^^^^^^^^^^^^^^^^ + +You can check if Pillow has been built against the zlib-ng version of the +zlib library, and what version of zlib-ng is being used:: + + from PIL import features + features.check_feature("zlib_ng") # True or False + features.version_feature("zlib_ng") # "2.2.2" for example, or None + +Other Changes +============= + +zlib-ng in wheels +^^^^^^^^^^^^^^^^^ + +Wheels are now built against zlib-ng for improved speed. In tests, saving a PNG +was found to be more than twice as fast at higher compression levels. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 641cda4ef..bd8e5536f 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 11.1.0 11.0.0 10.4.0 10.3.0 From 5e35ca359c4182af07c0c02e61ebd8d270bb34b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Dec 2024 20:17:36 +1100 Subject: [PATCH 465/652] Updated libjpeg-turbo URL --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a68a0b132..e702c7014 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -129,7 +129,7 @@ V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) # dependencies, listed in order of compilation DEPS: dict[str, dict[str, Any]] = { "libjpeg": { - "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/FILENAME/download", + "url": f"https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/{V['JPEGTURBO']}/libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], From 095811452172bc4efd4be172f850c2f564e35cba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Dec 2024 21:03:33 +1100 Subject: [PATCH 466/652] Corrected harfbuzz URL --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index de2471c3f..3e2a14462 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -93,7 +93,7 @@ function build_harfbuzz { if [ -e harfbuzz-stamp ]; then return; fi python3 -m pip install meson ninja - local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) + local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled) (cd $out_dir/build \ From 962bfc6fd5e949cfdf8c0d18877acc2b8f82a78c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Dec 2024 10:39:52 +1100 Subject: [PATCH 467/652] Updated libjpeg-turbo URL --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 74a9795bc..42d761728 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 74a9795bc64ff786b7e7d33bdec2843cf17e512e +Subproject commit 42d761728d141d8462cd9943f4329f12fe62b155 From de8335ba8fe121cf1f1b7b1565f535fb066094bf Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:07:34 +1100 Subject: [PATCH 468/652] Extract tar files with "data" filter in Windows build scripts (#8606) Co-authored-by: Andrew Murray Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- winbuild/build_prepare.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 607f672ba..188872dfc 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,7 @@ import re import shutil import struct import subprocess +import sys from typing import Any @@ -507,7 +508,10 @@ def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: if sources_dir_abs != member_prefix: msg = "Attempted Path Traversal in Tar File" raise RuntimeError(msg) - tgz.extractall(sources_dir) + if sys.version_info >= (3, 12): + tgz.extractall(sources_dir, filter="data") + else: + tgz.extractall(sources_dir) else: msg = "Unknown archive type: " + filename raise RuntimeError(msg) From cbc55c4621b79176702bf14f188e5a439c7804a4 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:08:51 +1100 Subject: [PATCH 469/652] Raise ValueError when WMF inch is zero (#8600) Co-authored-by: Andrew Murray --- Tests/test_file_wmf.py | 7 +++++++ src/PIL/WmfImagePlugin.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 424640d7b..2f1f8cdbc 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -35,6 +35,13 @@ def test_load() -> None: assert im.load()[0, 0] == (255, 255, 255) +def test_load_zero_inch() -> None: + b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x00" * 10) + with pytest.raises(ValueError): + with Image.open(b): + pass + + def test_register_handler(tmp_path: Path) -> None: class TestHandler(ImageFile.StubHandler): methodCalled = False diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index cad6c98d5..48e9823e8 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -92,6 +92,9 @@ class WmfStubImageFile(ImageFile.StubImageFile): # get units per inch self._inch = word(s, 14) + if self._inch == 0: + msg = "Invalid inch" + raise ValueError(msg) # get bounding box x0 = short(s, 6) From c6f42cb6a59aaaf29acf8831ef5865a688139480 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 20 Dec 2024 14:46:01 +1100 Subject: [PATCH 470/652] Updated libwebp to 1.5.0 --- .github/workflows/wheels-dependencies.sh | 2 +- depends/install_webp.sh | 2 +- winbuild/build_prepare.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3e2a14462..4e0fad79f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -51,7 +51,7 @@ else GIFLIB_VERSION=5.2.1 fi ZLIB_NG_VERSION=2.2.2 -LIBWEBP_VERSION=1.4.0 +LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 diff --git a/depends/install_webp.sh b/depends/install_webp.sh index c47fb35f1..9d2977715 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.4.0 +archive=libwebp-1.5.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 188872dfc..0674a9a15 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ V = { "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBPNG": "1.6.44", - "LIBWEBP": "1.4.0", + "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.6.0", "XZ": "5.6.3", From 5bd2f489fa7ed1ca4097cf15634d37723919434d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:44:50 +1100 Subject: [PATCH 471/652] Install libjpeg-turbo8-dev (#8610) Co-authored-by: Andrew Murray --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index e85e6bdc5..5c20e7f37 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -21,7 +21,7 @@ set -e if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ - ghostscript libjpeg-turbo-progs libopenjp2-7-dev\ + ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ sway wl-clipboard libopenblas-dev fi From cf4110ff06b4d4e0ec43b4a9ca13a28b7d3b11d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 21 Dec 2024 00:55:44 +1100 Subject: [PATCH 472/652] Replace constants with enums (#8611) Co-authored-by: Andrew Murray --- src/PIL/Image.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1e289b6c3..440728b08 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1565,7 +1565,7 @@ class Image: for subifd_offset in subifd_offsets: ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) - if ifd1 and ifd1.get(513): + if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): assert exif._info is not None ifds.append((ifd1, exif._info.next)) @@ -1577,11 +1577,11 @@ class Image: fp = self.fp if ifd is not None: - thumbnail_offset = ifd.get(513) + thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset) if thumbnail_offset is not None: thumbnail_offset += getattr(self, "_exif_offset", 0) self.fp.seek(thumbnail_offset) - data = self.fp.read(ifd.get(514)) + data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount)) fp = io.BytesIO(data) with open(fp) as im: From 3d0f4389499314ca793f6244d13e00cd3ad44de0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 01:38:05 +0000 Subject: [PATCH 473/652] Update dependency mypy to v1.14.0 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index c84a3533b..cd1b1a1a1 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.13.0 +mypy==1.14.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 08e1f9ebc11285c94a2706dd02146f685b10aec0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Dec 2024 13:04:17 +1100 Subject: [PATCH 474/652] Lint fixes --- Tests/test_color_lut.py | 2 ++ src/PIL/ImageFilter.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 36ab187f2..baa899df5 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -388,10 +388,12 @@ class TestColorLut3DFilter: table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert isinstance(lut.table, numpy.ndarray) assert lut.table.shape == (table.size,) table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert isinstance(lut.table, numpy.ndarray) assert lut.table.shape == (table.size,) # Check application diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 8b0974b2c..b350e56f4 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -553,7 +553,7 @@ class Color3DLUT(MultibandFilter): ch_out = channels or ch_in size_1d, size_2d, size_3d = self.size - table = [0] * (size_1d * size_2d * size_3d * ch_out) + table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out) idx_in = 0 idx_out = 0 for b in range(size_3d): From b99a00f44f8e34c18b89395eaf67e796ecdf1bf0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 Dec 2024 07:24:08 +1100 Subject: [PATCH 475/652] Updated MakerNote IFD capitalization --- src/PIL/ExifTags.py | 2 +- src/PIL/Image.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 39b4aa552..231e80f40 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -352,7 +352,7 @@ class Interop(IntEnum): class IFD(IntEnum): Exif = 34665 GPSInfo = 34853 - Makernote = 37500 + MakerNote = 37500 Interop = 40965 IFD1 = -1 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1e289b6c3..fb3f01fc8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3884,7 +3884,7 @@ class Exif(_ExifBase): gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) print(gps_ifd) - Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, + 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:: @@ -4047,11 +4047,11 @@ class Exif(_ExifBase): ifd = self._get_ifd_dict(offset, tag) if ifd is not None: self._ifds[tag] = ifd - elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: + elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.MakerNote]: if ExifTags.IFD.Exif not in self._ifds: self.get_ifd(ExifTags.IFD.Exif) tag_data = self._ifds[ExifTags.IFD.Exif][tag] - if tag == ExifTags.IFD.Makernote: + if tag == ExifTags.IFD.MakerNote: from .TiffImagePlugin import ImageFileDirectory_v2 if tag_data[:8] == b"FUJIFILM": @@ -4138,7 +4138,7 @@ class Exif(_ExifBase): ifd = { k: v for (k, v) in ifd.items() - if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote) + if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote) } return ifd From 5fe80e1c48eff0b74a14377107895c3e2d2f91b6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 Dec 2024 07:30:59 +1100 Subject: [PATCH 476/652] Use hex values more consistently --- src/PIL/ExifTags.py | 124 ++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 231e80f40..207d4de4e 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -303,38 +303,38 @@ TAGS = { class GPS(IntEnum): - GPSVersionID = 0 - GPSLatitudeRef = 1 - GPSLatitude = 2 - GPSLongitudeRef = 3 - GPSLongitude = 4 - GPSAltitudeRef = 5 - GPSAltitude = 6 - GPSTimeStamp = 7 - GPSSatellites = 8 - GPSStatus = 9 - GPSMeasureMode = 10 - GPSDOP = 11 - GPSSpeedRef = 12 - GPSSpeed = 13 - GPSTrackRef = 14 - GPSTrack = 15 - GPSImgDirectionRef = 16 - GPSImgDirection = 17 - GPSMapDatum = 18 - GPSDestLatitudeRef = 19 - GPSDestLatitude = 20 - GPSDestLongitudeRef = 21 - GPSDestLongitude = 22 - GPSDestBearingRef = 23 - GPSDestBearing = 24 - GPSDestDistanceRef = 25 - GPSDestDistance = 26 - GPSProcessingMethod = 27 - GPSAreaInformation = 28 - GPSDateStamp = 29 - GPSDifferential = 30 - GPSHPositioningError = 31 + GPSVersionID = 0x00 + GPSLatitudeRef = 0x01 + GPSLatitude = 0x02 + GPSLongitudeRef = 0x03 + GPSLongitude = 0x04 + GPSAltitudeRef = 0x05 + GPSAltitude = 0x06 + GPSTimeStamp = 0x07 + GPSSatellites = 0x08 + GPSStatus = 0x09 + GPSMeasureMode = 0x0A + GPSDOP = 0x0B + GPSSpeedRef = 0x0C + GPSSpeed = 0x0D + GPSTrackRef = 0x0E + GPSTrack = 0x0F + GPSImgDirectionRef = 0x10 + GPSImgDirection = 0x11 + GPSMapDatum = 0x12 + GPSDestLatitudeRef = 0x13 + GPSDestLatitude = 0x14 + GPSDestLongitudeRef = 0x15 + GPSDestLongitude = 0x16 + GPSDestBearingRef = 0x17 + GPSDestBearing = 0x18 + GPSDestDistanceRef = 0x19 + GPSDestDistance = 0x1A + GPSProcessingMethod = 0x1B + GPSAreaInformation = 0x1C + GPSDateStamp = 0x1D + GPSDifferential = 0x1E + GPSHPositioningError = 0x1F """Maps EXIF GPS tags to tag names.""" @@ -342,40 +342,40 @@ GPSTAGS = {i.value: i.name for i in GPS} class Interop(IntEnum): - InteropIndex = 1 - InteropVersion = 2 - RelatedImageFileFormat = 4096 - RelatedImageWidth = 4097 - RelatedImageHeight = 4098 + InteropIndex = 0x0001 + InteropVersion = 0x0002 + RelatedImageFileFormat = 0x1000 + RelatedImageWidth = 0x1001 + RelatedImageHeight = 0x1002 class IFD(IntEnum): - Exif = 34665 - GPSInfo = 34853 - MakerNote = 37500 - Interop = 40965 + Exif = 0x8769 + GPSInfo = 0x8825 + MakerNote = 0x927C + Interop = 0xA005 IFD1 = -1 class LightSource(IntEnum): - Unknown = 0 - Daylight = 1 - Fluorescent = 2 - Tungsten = 3 - Flash = 4 - Fine = 9 - Cloudy = 10 - Shade = 11 - DaylightFluorescent = 12 - DayWhiteFluorescent = 13 - CoolWhiteFluorescent = 14 - WhiteFluorescent = 15 - StandardLightA = 17 - StandardLightB = 18 - StandardLightC = 19 - D55 = 20 - D65 = 21 - D75 = 22 - D50 = 23 - ISO = 24 - Other = 255 + Unknown = 0x00 + Daylight = 0x01 + Fluorescent = 0x02 + Tungsten = 0x03 + Flash = 0x04 + Fine = 0x09 + Cloudy = 0x0A + Shade = 0x0B + DaylightFluorescent = 0x0C + DayWhiteFluorescent = 0x0D + CoolWhiteFluorescent = 0x0E + WhiteFluorescent = 0x0F + StandardLightA = 0x11 + StandardLightB = 0x12 + StandardLightC = 0x13 + D55 = 0x14 + D65 = 0x15 + D75 = 0x16 + D50 = 0x17 + ISO = 0x18 + Other = 0xFF From 0074c3bf349f0055aaff73fd7864c6a5e785b220 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Dec 2024 11:45:36 +1100 Subject: [PATCH 477/652] Assert that a tuple is returned by getpixel() --- Tests/test_file_dds.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 9a826ebe8..7cc4d79d4 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -331,11 +331,13 @@ def test_dxt5_colorblock_alpha_issue_4142() -> None: with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: px = im.getpixel((0, 0)) + assert isinstance(px, tuple) assert px[0] != 0 assert px[1] != 0 assert px[2] != 0 px = im.getpixel((1, 0)) + assert isinstance(px, tuple) assert px[0] != 0 assert px[1] != 0 assert px[2] != 0 From 5d5543b35caacef1a7f5912caa14b9d257680291 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Dec 2024 11:57:27 +1100 Subject: [PATCH 478/652] Assert that load() does not return None --- Tests/test_file_eps.py | 8 ++++++-- Tests/test_file_gbr.py | 6 +++++- Tests/test_file_gif.py | 22 +++++++++++++++++----- Tests/test_file_icns.py | 8 ++++++-- Tests/test_file_ico.py | 4 +++- Tests/test_file_jpeg2k.py | 1 + Tests/test_file_ppm.py | 1 + Tests/test_file_tga.py | 8 ++++++-- Tests/test_file_wal.py | 8 ++++++-- Tests/test_file_wmf.py | 4 +++- 10 files changed, 54 insertions(+), 16 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 672c04a4d..a0c2f9216 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -95,10 +95,14 @@ def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_load() -> None: with Image.open(FILE1) as im: - assert im.load()[0, 0] == (255, 255, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (255, 255, 255) # Test again now that it has already been loaded once - assert im.load()[0, 0] == (255, 255, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (255, 255, 255) def test_binary() -> None: diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index be98b08f2..5b59cc07a 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -14,10 +14,14 @@ def test_gbr_file() -> None: def test_load() -> None: with Image.open("Tests/images/gbr.gbr") as im: + px = im.load() + assert px is not None assert im.load()[0, 0] == (0, 0, 0, 0) # Test again now that it has already been loaded once - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) def test_multiple_load_operations() -> None: diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 5d46b157d..f25d819ea 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -86,12 +86,16 @@ def test_invalid_file() -> None: def test_l_mode_transparency() -> None: with Image.open("Tests/images/no_palette_with_transparency.gif") as im: assert im.mode == "L" - assert im.load()[0, 0] == 128 + px = im.load() + assert px is not None + assert px[0, 0] == 128 assert im.info["transparency"] == 255 im.seek(1) assert im.mode == "L" - assert im.load()[0, 0] == 128 + px = im.load() + assert px is not None + assert px[0, 0] == 128 def test_l_mode_after_rgb() -> None: @@ -310,7 +314,9 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None: with Image.open(path) as im: assert im.mode == "P" first_frame_colors = im.palette.colors.keys() - original_color = im.convert("RGB").load()[0, 0] + px = im.convert("RGB").load() + assert px is not None + original_color = px[0, 0] im.seek(1) assert im.mode == mode @@ -318,10 +324,14 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None: im = im.convert("RGB") # Check a color only from the old palette - assert im.load()[0, 0] == original_color + px = im.load() + assert px is not None + assert px[0, 0] == original_color # Check a color from the new palette - assert im.load()[24, 24] not in first_frame_colors + px = im.load() + assert px is not None + assert px[24, 24] not in first_frame_colors def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: @@ -488,6 +498,7 @@ def test_eoferror() -> None: def test_first_frame_transparency() -> None: with Image.open("Tests/images/first_frame_transparency.gif") as im: px = im.load() + assert px is not None assert px[0, 0] == im.info["transparency"] @@ -528,6 +539,7 @@ def test_dispose_background_transparency() -> None: with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: img.seek(2) px = img.load() + assert px is not None assert px[35, 30][3] == 0 diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 141b88dfa..94f16aeec 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -32,10 +32,14 @@ def test_sanity() -> None: def test_load() -> None: with Image.open(TEST_FILE) as im: - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) # Test again now that it has already been loaded once - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) def test_save(tmp_path: Path) -> None: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 37770498a..10f3aac9a 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -24,7 +24,9 @@ def test_sanity() -> None: def test_load() -> None: with Image.open(TEST_ICO_FILE) as im: - assert im.load()[0, 0] == (1, 1, 9, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (1, 1, 9, 255) def test_mask() -> None: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index fbf72ae05..b761fcd37 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -63,6 +63,7 @@ def test_sanity() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: px = im.load() + assert px is not None assert px[0, 0] == (0, 0, 0) assert im.mode == "RGB" assert im.size == (640, 480) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fb08d613a..f4acedb30 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -79,6 +79,7 @@ def test_arbitrary_maxval( assert im.mode == mode px = im.load() + assert px is not None assert tuple(px[x, 0] for x in range(3)) == pixels diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index a03a6a6e1..63d1e7615 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -213,10 +213,14 @@ def test_save_orientation(tmp_path: Path) -> None: def test_horizontal_orientations() -> None: # These images have been manually hexedited to have the relevant orientations with Image.open("Tests/images/rgb32rle_top_right.tga") as im: - assert im.load()[90, 90][:3] == (0, 0, 0) + px = im.load() + assert px is not None + assert px[90, 90][:3] == (0, 0, 0) with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im: - assert im.load()[90, 90][:3] == (0, 255, 0) + px = im.load() + assert px is not None + assert px[90, 90][:3] == (0, 255, 0) def test_save_rle(tmp_path: Path) -> None: diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index b34975e83..b15d79d61 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -21,7 +21,11 @@ def test_open() -> None: def test_load() -> None: with WalImageFile.open(TEST_FILE) as im: - assert im.load()[0, 0] == 122 + px = im.load() + assert px is not None + assert px[0, 0] == 122 # Test again now that it has already been loaded once - assert im.load()[0, 0] == 122 + px = im.load() + assert px is not None + assert px[0, 0] == 122 diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 2f1f8cdbc..f849453f6 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -32,7 +32,9 @@ def test_load_raw() -> None: def test_load() -> None: with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): - assert im.load()[0, 0] == (255, 255, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (255, 255, 255) def test_load_zero_inch() -> None: From 601a56def1dfdacd76759302595d9d904f2467c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Dec 2024 12:03:13 +1100 Subject: [PATCH 479/652] Assert palette is not None --- Tests/test_file_gif.py | 2 ++ Tests/test_file_jpeg2k.py | 2 ++ Tests/test_file_tga.py | 1 + Tests/test_image.py | 1 + Tests/test_image_convert.py | 1 + Tests/test_image_transform.py | 1 + Tests/test_imagepalette.py | 1 + 7 files changed, 9 insertions(+) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index f25d819ea..9fe5f4fbb 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -313,6 +313,7 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None: def test_loading_multiple_palettes(path: str, mode: str) -> None: with Image.open(path) as im: assert im.mode == "P" + assert im.palette is not None first_frame_colors = im.palette.colors.keys() px = im.convert("RGB").load() assert px is not None @@ -1325,6 +1326,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None: with Image.open(out) as im: # Assert that the frames are correct, and each frame has the same palette assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) + assert im.palette is not None assert im.palette.palette == im.global_palette.palette im.seek(1) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index b761fcd37..af6b9b2db 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -413,6 +413,7 @@ def test_subsampling_decode(name: str) -> None: def test_pclr() -> None: with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im: assert im.mode == "P" + assert im.palette is not None assert len(im.palette.colors) == 256 assert im.palette.colors[(255, 255, 255)] == 0 @@ -420,6 +421,7 @@ def test_pclr() -> None: f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2" ) as im: assert im.mode == "P" + assert im.palette is not None assert len(im.palette.colors) == 139 assert im.palette.colors[(0, 0, 0, 0)] == 0 diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 63d1e7615..b6396bd64 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -72,6 +72,7 @@ def test_palette_depth_8(tmp_path: Path) -> None: def test_palette_depth_16(tmp_path: Path) -> None: with Image.open("Tests/images/p_16.tga") as im: + assert im.palette is not None assert im.palette.mode == "RGBA" assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png") diff --git a/Tests/test_image.py b/Tests/test_image.py index c8df474f4..1d7bd19ca 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -662,6 +662,7 @@ class TestImage: im.putpalette(list(range(256)) * 4, "RGBA") im_remapped = im.remap_palette(list(range(256))) assert_image_equal(im, im_remapped) + assert im.palette is not None assert im.palette.palette == im_remapped.palette.palette # Test illegal image mode diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 6a925975e..03061ceb1 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -236,6 +236,7 @@ def test_gif_with_rgba_palette_to_p() -> None: with Image.open("Tests/images/hopper.gif") as im: im.info["transparency"] = 255 im.load() + assert im.palette is not None assert im.palette.mode == "RGB" im_p = im.convert("P") diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 7e83396de..77916929b 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -47,6 +47,7 @@ class TestImageTransform: transformed = im.transform( im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] ) + assert im.palette is not None assert im.palette.palette == transformed.palette.palette def test_extent(self) -> None: diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 6cf0079dd..e2f8308ea 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -17,6 +17,7 @@ def test_sanity() -> None: def test_reload() -> None: with Image.open("Tests/images/hopper.gif") as im: original = im.copy() + assert im.palette is not None im.palette.dirty = 1 assert_image_equal(im.convert("RGB"), original.convert("RGB")) From fb3d80e390b8ed3111a4658755b035f5e20eb5e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Dec 2024 00:41:27 +1100 Subject: [PATCH 480/652] Fixed connecting discontiguous corners --- .../imagedraw/discontiguous_corners_polygon.png | Bin 486 -> 533 bytes Tests/test_imagedraw.py | 3 +++ src/libImaging/Draw.c | 9 ++++----- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/images/imagedraw/discontiguous_corners_polygon.png b/Tests/images/imagedraw/discontiguous_corners_polygon.png index 509c42b26e0cbc5e01853915b083d367c1587579..1b58889c8f3ae45243a7509c907f1928534bcbde 100644 GIT binary patch delta 507 zcmV$=WdTX_c%swU!U485w>wyG`7FR7NL{=~F7gqE=@exLJl^>&(VxOp^oSf8ia zhL}fVLnf5pHi+D7J^>rS>@}Z&jbnD1M`NRzjpotVIm~MFXn*Xdtc%T~u_cKYm`7u4 zb5Aml#`f$rpm{%7q?w2WMdmZ9Jmv$tYM1zlSN+e25z{{|T< xlUzDe$vZP-&cnLR`Fl4@D(PjuQYa0yf8_cULF8KV4%i50t$7D*9J9(i8XL_lG>^viVP>00V}E-xGt8s0{h7DSqp_oy z7v|B}vCOaL^=6`p{ zx-yW@%sX@;oL`>6ss&}tQP{jy7s{L0??ngmnl7|5FWZZ5 z<^^5oX`ZzgZGX-0ccHoYoxSL9e%Xan%un{>AoFjtaGZJeq=YAkoRJ&<-`JdWJ^yZh zPW{aTo}5yhZ}4mL8Pdz|GqL&VQ?P{M?R+?oa?YPd*|&W-o5uFwQh3{kbINRgm&E+= z{-We$^*vu@&WF;P59Bw=Z25$j-}aE!{@K0+IYFQ86GhJXGa>d@T{hc8HjiI*d2A1< zc9ZEp%#(a~rkpp^WX{df`g}hP-I2X&ex`d$_dGxT#Oc95ub{>N0000 None: def test_discontiguous_corners_polygon() -> None: img, draw = create_base_image_draw((84, 68)) draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) + draw.polygon( + ((82, 29), (82, 26), (82, 24), (67, 22), (52, 29), (52, 15), (67, 22)), BLACK + ) draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) draw.polygon( ((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)), diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index f1c8ffcff..ea6f8805e 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -501,7 +501,8 @@ polygon_generic( // Needed to draw consistent polygons xx[j] = xx[j - 1]; j++; - } else if (current->dx != 0 && roundf(xx[j - 1]) == xx[j - 1]) { + } else if (current->dx != 0 && j % 2 == 1 && + roundf(xx[j - 1]) == xx[j - 1]) { // Connect discontiguous corners for (k = 0; k < i; k++) { Edge *other_edge = edge_table[k]; @@ -510,10 +511,8 @@ polygon_generic( continue; } // Check if the two edges join to make a corner - if (((ymin == current->ymin && ymin == other_edge->ymin) || - (ymin == current->ymax && ymin == other_edge->ymax)) && - xx[j - 1] == (ymin - other_edge->y0) * other_edge->dx + - other_edge->x0) { + if (xx[j - 1] == + (ymin - other_edge->y0) * other_edge->dx + other_edge->x0) { // Determine points from the edges on the next row // Or if this is the last row, check the previous row int offset = ymin == ymax ? -1 : 1; From dd410e4b32c6b7e412304f30c27b8a226c24ebd3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Dec 2024 10:51:45 +1100 Subject: [PATCH 481/652] Added reading of J2K comments --- Tests/test_file_jpeg2k.py | 5 +++-- src/PIL/Jpeg2KImagePlugin.py | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index fbf72ae05..34176d3ce 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -424,8 +424,9 @@ def test_pclr() -> None: def test_comment() -> None: - with Image.open("Tests/images/comment.jp2") as im: - assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" + for path in ("Tests/images/9bit.j2k", "Tests/images/comment.jp2"): + with Image.open(path) as im: + assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" # Test an image that is truncated partway through a codestream with open("Tests/images/comment.jp2", "rb") as fp: diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index b6ebd562b..67828358d 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -252,6 +252,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): if sig == b"\xff\x4f\xff\x51": self.codec = "j2k" self._size, self._mode = _parse_codestream(self.fp) + self._parse_comment() else: sig = sig + self.fp.read(8) @@ -262,6 +263,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile): if dpi is not None: self.info["dpi"] = dpi if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): + hdr = self.fp.read(2) + length = _binary.i16be(hdr) + self.fp.seek(length - 2, os.SEEK_CUR) self._parse_comment() else: msg = "not a JPEG 2000 file" @@ -296,10 +300,6 @@ class Jpeg2KImageFile(ImageFile.ImageFile): ] def _parse_comment(self) -> None: - hdr = self.fp.read(2) - length = _binary.i16be(hdr) - self.fp.seek(length - 2, os.SEEK_CUR) - while True: marker = self.fp.read(2) if not marker: From 62b7cb62f4ee339677170b0d0fa5943e490ddab7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Dec 2024 19:06:23 +1100 Subject: [PATCH 482/652] Fixed indentation --- src/_imagingcms.c | 54 +++++++++++++++++++++++------------------------ src/display.c | 14 ++++++------ 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 1823bcf03..1805ebde1 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -346,10 +346,10 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { return -1; } - Py_BEGIN_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; - // transform color channels only - for (i = 0; i < im->ysize; i++) { + // transform color channels only + for (i = 0; i < im->ysize; i++) { cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize); } @@ -362,9 +362,9 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { // enough available on all platforms, so we polyfill it here for now. pyCMScopyAux(hTransform, imOut, im); - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - return 0; + return 0; } static cmsHTRANSFORM @@ -378,17 +378,17 @@ _buildTransform( ) { cmsHTRANSFORM hTransform; - Py_BEGIN_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; - /* create the transform */ - hTransform = cmsCreateTransform( - hInputProfile, - findLCMStype(sInMode), - hOutputProfile, - findLCMStype(sOutMode), - iRenderingIntent, - cmsFLAGS - ); + /* create the transform */ + hTransform = cmsCreateTransform( + hInputProfile, + findLCMStype(sInMode), + hOutputProfile, + findLCMStype(sOutMode), + iRenderingIntent, + cmsFLAGS + ); Py_END_ALLOW_THREADS; @@ -412,19 +412,19 @@ _buildProofTransform( ) { cmsHTRANSFORM hTransform; - Py_BEGIN_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; - /* create the transform */ - hTransform = cmsCreateProofingTransform( - hInputProfile, - findLCMStype(sInMode), - hOutputProfile, - findLCMStype(sOutMode), - hProofProfile, - iRenderingIntent, - iProofIntent, - cmsFLAGS - ); + /* create the transform */ + hTransform = cmsCreateProofingTransform( + hInputProfile, + findLCMStype(sInMode), + hOutputProfile, + findLCMStype(sOutMode), + hProofProfile, + iRenderingIntent, + iProofIntent, + cmsFLAGS + ); Py_END_ALLOW_THREADS; diff --git a/src/display.c b/src/display.c index b4e2e3899..eed75975d 100644 --- a/src/display.c +++ b/src/display.c @@ -690,24 +690,26 @@ PyImaging_CreateWindowWin32(PyObject *self, PyObject *args) { SetWindowLongPtr(wnd, 0, (LONG_PTR)callback); SetWindowLongPtr(wnd, sizeof(callback), (LONG_PTR)PyThreadState_Get()); - Py_BEGIN_ALLOW_THREADS ShowWindow(wnd, SW_SHOWNORMAL); + Py_BEGIN_ALLOW_THREADS; + ShowWindow(wnd, SW_SHOWNORMAL); SetForegroundWindow(wnd); /* to make sure it's visible */ - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - return Py_BuildValue(F_HANDLE, wnd); + return Py_BuildValue(F_HANDLE, wnd); } PyObject * PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { MSG msg; - Py_BEGIN_ALLOW_THREADS while (mainloop && GetMessage(&msg, NULL, 0, 0)) { + Py_BEGIN_ALLOW_THREADS; + while (mainloop && GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - Py_INCREF(Py_None); + Py_INCREF(Py_None); return Py_None; } From 622722f295ab137ea6bf54ede8efb8f376d1194b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Dec 2024 20:04:27 +1100 Subject: [PATCH 483/652] Corrected loadImageSeries type hint --- src/PIL/SpiderImagePlugin.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 075073f9f..c83d02885 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -211,26 +211,27 @@ class SpiderImageFile(ImageFile.ImageFile): # given a list of filenames, return a list of images -def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None: +def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | None: """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" if filelist is None or len(filelist) < 1: return None - imglist = [] + byte_imgs = [] for img in filelist: if not os.path.exists(img): print(f"unable to find {img}") continue try: with Image.open(img) as im: - im = im.convert2byte() + assert isinstance(im, SpiderImageFile) + byte_im = im.convert2byte() except Exception: if not isSpiderImage(img): print(f"{img} is not a Spider image file") continue - im.info["filename"] = img - imglist.append(im) - return imglist + byte_im.info["filename"] = img + byte_imgs.append(byte_im) + return byte_imgs # -------------------------------------------------------------------- From aef3aa2ab35fdf847aa13017c5b4b315562a1f77 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Dec 2024 11:26:07 +1100 Subject: [PATCH 484/652] Pass file handle to ContainerIO --- Tests/test_file_container.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 237045acc..597ab5083 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -4,8 +4,6 @@ import pytest from PIL import ContainerIO, Image -from .helper import hopper - TEST_FILE = "Tests/images/dummy.container" @@ -15,15 +13,15 @@ def test_sanity() -> None: def test_isatty() -> None: - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) assert container.isatty() is False def test_seekable() -> None: - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) assert container.seekable() is True From 0148684c2412cc69b9606e7e2b6d5d581030e41c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Dec 2024 11:29:47 +1100 Subject: [PATCH 485/652] Use monkeypatch --- Tests/test_image_thumbnail.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 01bd4b1d7..aa625f229 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -104,20 +104,20 @@ def test_transposed() -> None: assert im.size == (590, 88) -def test_load_first_unless_jpeg() -> None: +def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None: # Test that thumbnail() still uses draft() for JPEG with Image.open("Tests/images/hopper.jpg") as im: - draft = im.draft + original_draft = im.draft def im_draft( mode: str, size: tuple[int, int] ) -> tuple[str, tuple[int, int, float, float]] | None: - result = draft(mode, size) + result = original_draft(mode, size) assert result is not None return result - im.draft = im_draft + monkeypatch.setattr(im, "draft", im_draft) im.thumbnail((64, 64)) From 89f1498796be0053bc40ef757680c9f7bc49e7ce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Dec 2024 11:38:47 +1100 Subject: [PATCH 486/652] Updated argument types to match Image draft --- Tests/test_image_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index aa625f229..1181f6fca 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -110,7 +110,7 @@ def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None: original_draft = im.draft def im_draft( - mode: str, size: tuple[int, int] + mode: str | None, size: tuple[int, int] | None ) -> tuple[str, tuple[int, int, float, float]] | None: result = original_draft(mode, size) assert result is not None From 0220b025c5f56ebe4c2f1678e24195672d4d4373 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 12:33:59 +1100 Subject: [PATCH 487/652] Updated documentation for #7947 and #8592 --- docs/handbook/image-file-formats.rst | 8 +++++--- docs/releasenotes/11.1.0.rst | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index bf3087f6f..364e1802a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -573,9 +573,11 @@ Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``, ``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to ``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel. Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``, -``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports -JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files -(``.jp2`` or ``.jpx`` files). +``RGBA``, and ``YCbCr`` images with subsampled components. Pillow 10.4.0 and +later can read ``CMYK`` images with OpenJPEG 2.5.1 and later, and Pillow 11.1.0 +and later can write ``CMYK`` images with OpenJPEG 2.5.3 and later. Pillow +supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 +files (``.jp2`` or ``.jpx`` files). When loading, if you set the ``mode`` on the image prior to the :py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index c5d0afd58..7fd622beb 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -52,6 +52,11 @@ zlib library, and what version of zlib-ng is being used:: Other Changes ============= +Saving JPEG 2000 CMYK images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files. + zlib-ng in wheels ^^^^^^^^^^^^^^^^^ From ad747f3fd8024145ddc8dbc5f3f95d7e396b3351 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 12:38:50 +1100 Subject: [PATCH 488/652] Added release notes --- docs/releasenotes/11.1.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index c5d0afd58..cccf2323d 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -52,6 +52,12 @@ zlib library, and what version of zlib-ng is being used:: Other Changes ============= +Reading JPEG 2000 comments +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG 2000 image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images. + zlib-ng in wheels ^^^^^^^^^^^^^^^^^ From cfe8379d905bb92a1407d1809987fb74163b5024 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 13:02:23 +1100 Subject: [PATCH 489/652] Added release notes for #8483 --- docs/releasenotes/11.1.0.rst | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index c5d0afd58..90a0492bd 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -31,10 +31,26 @@ TODO API Changes =========== -TODO -^^^^ +Writing XMP bytes to JPEG and MPO +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Pillow 11.0.0 added writing XMP data to JPEG and MPO images:: + + im.info["xmp"] = b"test" + im.save("out.jpg") + +However, this meant that XMP data was automatically kept from an opened image, +which is inconsistent with the rest of Pillow's behaviour. This functionality +has been removed. To write XMP data, the ``xmp`` argument can still be used for +JPEG files:: + + im.save("out.jpg", xmp=b"test") + +To save XMP data to the second frame of an MPO image, ``encoderinfo`` can now +be used:: + + second_im.encoderinfo = {"xmp": b"test"} + im.save("out.mpo", save_all=True, append_images=[second_im]) API Additions ============= From 23083f28abbf0a79bc44c2e7b755663c04368e14 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 14:02:19 +1100 Subject: [PATCH 490/652] Use monkeypatch --- Tests/test_file_png.py | 8 ++------ Tests/test_file_ppm.py | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ffafc3c58..974e1e75f 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -772,22 +772,18 @@ class TestFilePng: im.seek(1) @pytest.mark.parametrize("buffer", (True, False)) - def test_save_stdout(self, buffer: bool) -> None: - old_stdout = sys.stdout + def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None: class MyStdOut: buffer = BytesIO() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() - sys.stdout = mystdout + monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_PNG_FILE) as im: im.save(sys.stdout, "PNG") - # Reset stdout - sys.stdout = old_stdout - if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fb08d613a..ee51a5e5a 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -367,22 +367,18 @@ def test_mimetypes(tmp_path: Path) -> None: @pytest.mark.parametrize("buffer", (True, False)) -def test_save_stdout(buffer: bool) -> None: - old_stdout = sys.stdout +def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None: class MyStdOut: buffer = BytesIO() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() - sys.stdout = mystdout + monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_FILE) as im: im.save(sys.stdout, "PPM") - # Reset stdout - sys.stdout = old_stdout - if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: From f10e9f42d3e434c34a16df514a5435381b21aefb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 14:29:29 +1100 Subject: [PATCH 491/652] Do not use temporary file in grabclipboard() on macOS --- src/PIL/ImageGrab.py | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index e27ca7e50..fe27bfaeb 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -104,28 +104,17 @@ def grab( def grabclipboard() -> Image.Image | list[str] | None: if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - commands = [ - 'set theFile to (open for access POSIX file "' - + filepath - + '" with write permission)', - "try", - " write (the clipboard as «class PNGf») to theFile", - "end try", - "close access theFile", - ] - script = ["osascript"] - for command in commands: - script += ["-e", command] - subprocess.call(script) + p = subprocess.run( + ["osascript", "-e", "get the clipboard as «class PNGf»"], + capture_output=True, + ) + if p.returncode != 0: + return None - im = None - if os.stat(filepath).st_size != 0: - im = Image.open(filepath) - im.load() - os.unlink(filepath) - return im + import binascii + + data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3])) + return Image.open(data) elif sys.platform == "win32": fmt, data = Image.core.grabclipboard_win32() if fmt == "file": # CF_HDROP From 05c981ffd74f6b4de932a1a14f44dc6a3058bb75 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 15:41:19 +1100 Subject: [PATCH 492/652] Removed buffer_size variable --- src/libImaging/Jpeg2KDecode.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index fc927d2f0..4f185b529 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -640,7 +640,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { opj_dparameters_t params; OPJ_COLOR_SPACE color_space; j2k_unpacker_t unpack = NULL; - size_t buffer_size = 0, tile_bytes = 0; + size_t tile_bytes = 0; unsigned n, tile_height, tile_width; int subsampling; int total_component_width = 0; @@ -870,7 +870,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { tile_info.data_size = tile_bytes; } - if (buffer_size < tile_info.data_size) { + if (tile_info.data_size > 0) { /* malloc check ok, overflow and tile size sanity check above */ UINT8 *new = realloc(state->buffer, tile_info.data_size); if (!new) { @@ -883,7 +883,6 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { to valgrind errors. */ memset(new, 0, tile_info.data_size); state->buffer = new; - buffer_size = tile_info.data_size; } if (!opj_decode_tile_data( From 8945875c6ce48fee62d17cea1414938e24808a9f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 21:28:44 +1100 Subject: [PATCH 493/652] Populate DPI from JFIF cm density --- Tests/images/jfif_unit_cm.jpg | Bin 0 -> 391 bytes Tests/test_file_jpeg.py | 4 ++++ src/PIL/JpegImagePlugin.py | 3 +++ 3 files changed, 7 insertions(+) create mode 100644 Tests/images/jfif_unit_cm.jpg diff --git a/Tests/images/jfif_unit_cm.jpg b/Tests/images/jfif_unit_cm.jpg new file mode 100644 index 0000000000000000000000000000000000000000..78b50e60a236bf320a29c433eae2c9afeb759fe9 GIT binary patch literal 391 zcmex=5D>Bm<7<_#hv=|r|IDm3|AQ=W8AeI7R zhP2G0;u4@jeu%>2)a3lU6o&t|8Jrn7*x1UW@aW9W>!`f7NBe`P@aKBkX1<0(2-3z zFp*uUP{gQl;zAB(r;P_igD!qhF-|IK;^Yz&myncFRa4i{)G{$OGqmaka3 zYSZQ|TeofBv2)j None: else: if jfif_unit == 1: self.info["dpi"] = jfif_density + elif jfif_unit == 2: # cm + # 1 dpcm = 2.54 dpi + self.info["dpi"] = tuple(d * 2.54 for d in jfif_density) self.info["jfif_unit"] = jfif_unit self.info["jfif_density"] = jfif_density elif marker == 0xFFE1 and s[:6] == b"Exif\0\0": From 9bebecf36d66b19ac7ba0241ef9eb7febdcaf866 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 22:18:02 +1100 Subject: [PATCH 494/652] Use versionadded --- docs/handbook/image-file-formats.rst | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 364e1802a..2ea49282e 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -572,12 +572,19 @@ JPEG 2000 Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``, ``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to ``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel. -Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``, -``RGBA``, and ``YCbCr`` images with subsampled components. Pillow 10.4.0 and -later can read ``CMYK`` images with OpenJPEG 2.5.1 and later, and Pillow 11.1.0 -and later can write ``CMYK`` images with OpenJPEG 2.5.3 and later. Pillow -supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 -files (``.jp2`` or ``.jpx`` files). + +.. versionadded:: 8.3.0 + Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with + subsampled components. + +.. versionadded:: 10.4.0 + Pillow can read ``CMYK`` images with OpenJPEG 2.5.1 and later. + +.. versionadded:: 11.1.0 + Pillow can write ``CMYK`` images with OpenJPEG 2.5.3 and later. + +Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed +JPEG 2000 files (``.jp2`` or ``.jpx`` files). When loading, if you set the ``mode`` on the image prior to the :py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to From 9368a86397a41817f671c3c0bce7b8745bc5e218 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Dec 2024 07:43:47 +1100 Subject: [PATCH 495/652] Keep new IFDs when converting EXIF to bytes --- Tests/test_image.py | 4 ++++ src/PIL/Image.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index c8df474f4..092bc07f6 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -793,6 +793,10 @@ class TestImage: ifd[36864] = b"0220" assert exif.get_ifd(0x8769) == {36864: b"0220"} + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert reloaded_exif.get_ifd(0x8769) == {36864: b"0220"} + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 90374d804..dff3d063b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -4023,6 +4023,9 @@ class Exif(_ExifBase): head = self._get_head() ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) + for tag, ifd_dict in self._ifds.items(): + if tag not in self: + ifd[tag] = ifd_dict for tag, value in self.items(): if tag in [ ExifTags.IFD.Exif, From ea962bf1d8dab61d526f885eccb34863ea85228f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Dec 2024 16:59:32 +1100 Subject: [PATCH 496/652] Added RGBX;16N to RGB unpacker --- src/libImaging/Unpack.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index c23d5d889..e9203fe4d 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1695,6 +1695,7 @@ static struct { #ifdef WORDS_BIGENDIAN {"RGB", "RGB;16N", 48, unpackRGB16B}, + {"RGB", "RGBX;16N", 64, unpackRGBA16B}, {"RGBA", "RGBa;16N", 64, unpackRGBa16B}, {"RGBA", "RGBA;16N", 64, unpackRGBA16B}, {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, @@ -1708,6 +1709,7 @@ static struct { {"RGBA", "A;16N", 16, band316B}, #else {"RGB", "RGB;16N", 48, unpackRGB16L}, + {"RGB", "RGBX;16N", 64, unpackRGBA16L}, {"RGBA", "RGBa;16N", 64, unpackRGBa16L}, {"RGBA", "RGBA;16N", 64, unpackRGBA16L}, {"RGBX", "RGBX;16N", 64, unpackRGBA16L}, From 8d28514e409bf3ecbeb3721d8ccb508c09f2b975 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:16:42 +0200 Subject: [PATCH 497/652] Add zizmor to pre-commit and fix potential cache-poisoning in wheels workflow --- .github/workflows/wheels.yml | 2 -- .pre-commit-config.yaml | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index c5e55aa62..3b22ee98a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -263,8 +263,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.x" - cache: pip - cache-dependency-path: "Makefile" - run: make sdist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f91260c72..b76f92ec0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.1 + rev: v0.8.4 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.4 + rev: v19.1.5 hooks: - id: clang-format types: [c] @@ -56,6 +56,11 @@ repos: - id: check-readthedocs - id: check-renovate + - repo: https://github.com/woodruffw/zizmor-pre-commit + rev: v0.10.0 + hooks: + - id: zizmor + - repo: https://github.com/sphinx-contrib/sphinx-lint rev: v1.0.0 hooks: From 167ed55d8b43de26c5ce01c239ce848062e5e995 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Dec 2024 19:37:38 +1100 Subject: [PATCH 498/652] Use elif --- src/PIL/TiffImagePlugin.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 16c521bea..2ab0b7ebe 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1559,17 +1559,6 @@ class TiffImageFile(ImageFile.ImageFile): # fillorder==2 modes have a corresponding # fillorder=1 mode self._mode, rawmode = OPEN_INFO[key] - # libtiff always returns the bytes in native order. - # we're expecting image byte order. So, if the rawmode - # contains I;16, we need to convert from native to image - # byte order. - if rawmode == "I;16": - rawmode = "I;16N" - if ";16B" in rawmode: - rawmode = rawmode.replace(";16B", ";16N") - if ";16L" in rawmode: - rawmode = rawmode.replace(";16L", ";16N") - # YCbCr images with new jpeg compression with pixels in one plane # unpacked straight into RGB values if ( @@ -1578,6 +1567,16 @@ class TiffImageFile(ImageFile.ImageFile): and self._planar_configuration == 1 ): rawmode = "RGB" + # libtiff always returns the bytes in native order. + # we're expecting image byte order. So, if the rawmode + # contains I;16, we need to convert from native to image + # byte order. + elif rawmode == "I;16": + rawmode = "I;16N" + elif ";16B" in rawmode: + rawmode = rawmode.replace(";16B", ";16N") + elif ";16L" in rawmode: + rawmode = rawmode.replace(";16L", ";16N") # Offset in the tile tuple is 0, we go from 0,0 to # w,h, and we only do this once -- eds From 7cee64ad1b1cbd558cdb01edaa9444f60467947b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Dec 2024 19:45:46 +1100 Subject: [PATCH 499/652] Use endswith --- src/PIL/TiffImagePlugin.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2ab0b7ebe..ab760c8fb 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1573,10 +1573,8 @@ class TiffImageFile(ImageFile.ImageFile): # byte order. elif rawmode == "I;16": rawmode = "I;16N" - elif ";16B" in rawmode: - rawmode = rawmode.replace(";16B", ";16N") - elif ";16L" in rawmode: - rawmode = rawmode.replace(";16L", ";16N") + elif rawmode.endswith(";16B") or rawmode.endswith(";16L"): + rawmode = rawmode[:-1] + "N" # Offset in the tile tuple is 0, we go from 0,0 to # w,h, and we only do this once -- eds From 050caa9cae5a5844e934e7ec29c0c5bc42537e32 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Dec 2024 21:14:23 +1100 Subject: [PATCH 500/652] Restored Makernote as a deprecated enum --- docs/deprecations.rst | 8 ++++++++ docs/releasenotes/11.1.0.rst | 7 ++++--- src/PIL/ExifTags.py | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 25607e27c..80966ca36 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -175,6 +175,14 @@ deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obt raw pointers to ``ImagingCore`` internals. To interact with C code, you can use ``Image.Image.getim()``, which returns a ``Capsule`` object. +ExifTags.IFD.Makernote +^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.1.0 + +``ExifTags.IFD.Makernote`` has been deprecated. Instead, use +``ExifTags.IFD.MakerNote``. + Removed features ---------------- diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index c5d0afd58..57a8eef40 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -23,10 +23,11 @@ TODO Deprecations ============ -TODO -^^^^ +ExifTags.IFD.Makernote +^^^^^^^^^^^^^^^^^^^^^^ -TODO +``ExifTags.IFD.Makernote`` has been deprecated. Instead, use +``ExifTags.IFD.MakerNote``. API Changes =========== diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 207d4de4e..2280d5ce8 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -353,6 +353,7 @@ class IFD(IntEnum): Exif = 0x8769 GPSInfo = 0x8825 MakerNote = 0x927C + Makernote = 0x927C # Deprecated Interop = 0xA005 IFD1 = -1 From 2ac383028a1983bb2bee27cd8998c25c81e93e49 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 01:26:13 +1100 Subject: [PATCH 501/652] Allow saving as BigTIFF --- Tests/test_file_tiff.py | 7 +++++ docs/handbook/image-file-formats.rst | 3 ++ src/PIL/TiffImagePlugin.py | 44 +++++++++++++++++----------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 6f51d4651..df2c4ebea 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -115,6 +115,13 @@ class TestFileTiff: outfile = str(tmp_path / "temp.tif") im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) + def test_bigtiff_save(self, tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.tif") + hopper().save(outfile, bigtiff=True) + + with Image.open(outfile) as im: + assert im.tag_v2._bigtiff is True + def test_seek_too_large(self) -> None: with pytest.raises(ValueError, match="Unable to seek to frame"): Image.open("Tests/images/seek_too_large.tif") diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 2ea49282e..d956d12d1 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1208,6 +1208,9 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 8.4.0 +**bigtiff** + If true, the image will be saved as a BigTIFF. + **compression** A string containing the desired compression method for the file. (valid only with libtiff installed) Valid compression diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ab760c8fb..013f34a4f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -582,7 +582,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def __init__( self, - ifh: bytes = b"II\052\0\0\0\0\0", + ifh: bytes = b"II\x2A\x00\x00\x00\x00\x00", prefix: bytes | None = None, group: int | None = None, ) -> None: @@ -949,16 +949,26 @@ class ImageFileDirectory_v2(_IFDv2Base): warnings.warn(str(msg)) return + def _get_ifh(self): + ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42) + if self._bigtiff: + ifh += self._pack("HH", 8, 0) + ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8) + + return ifh + def tobytes(self, offset: int = 0) -> bytes: # FIXME What about tagdata? - result = self._pack("H", len(self._tags_v2)) + result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2)) entries: list[tuple[int, int, int, bytes, bytes]] = [] - offset = offset + len(result) + len(self._tags_v2) * 12 + 4 + offset += len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + 4 stripoffsets = None # pass 1: convert tags to binary format # always write tags in ascending order + fmt = "Q" if self._bigtiff else "L" + fmt_size = 8 if self._bigtiff else 4 for tag, value in sorted(self._tags_v2.items()): if tag == STRIPOFFSETS: stripoffsets = len(entries) @@ -966,11 +976,7 @@ class ImageFileDirectory_v2(_IFDv2Base): logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) is_ifd = typ == TiffTags.LONG and isinstance(value, dict) if is_ifd: - if self._endian == "<": - ifh = b"II\x2A\x00\x08\x00\x00\x00" - else: - ifh = b"MM\x00\x2A\x00\x00\x00\x08" - ifd = ImageFileDirectory_v2(ifh, group=tag) + ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag) values = self._tags_v2[tag] for ifd_tag, ifd_value in values.items(): ifd[ifd_tag] = ifd_value @@ -993,10 +999,10 @@ class ImageFileDirectory_v2(_IFDv2Base): else: count = len(values) # figure out if data fits into the entry - if len(data) <= 4: - entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) + if len(data) <= fmt_size: + entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b"")) else: - entries.append((tag, typ, count, self._pack("L", offset), data)) + entries.append((tag, typ, count, self._pack(fmt, offset), data)) offset += (len(data) + 1) // 2 * 2 # pad to word # update strip offset data to point beyond auxiliary data @@ -1007,13 +1013,15 @@ class ImageFileDirectory_v2(_IFDv2Base): values = [val + offset for val in handler(self, data, self.legacy_api)] data = self._write_dispatch[typ](self, *values) else: - value = self._pack("L", self._unpack("L", value)[0] + offset) + value = self._pack(fmt, self._unpack(fmt, value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data # pass 2: write entries to file for tag, typ, count, value, data in entries: logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data)) - result += self._pack("HHL4s", tag, typ, count, value) + result += self._pack( + "HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value + ) # -- overwrite here for multi-page -- result += b"\0\0\0\0" # end of entries @@ -1028,8 +1036,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def save(self, fp: IO[bytes]) -> int: if fp.tell() == 0: # skip TIFF header on subsequent pages - # tiff header -- PIL always starts the first IFD at offset 8 - fp.write(self._prefix + self._pack("HL", 42, 8)) + fp.write(self._get_ifh()) offset = fp.tell() result = self.tobytes(offset) @@ -1680,10 +1687,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: msg = f"cannot write mode {im.mode} as TIFF" raise OSError(msg) from e - ifd = ImageFileDirectory_v2(prefix=prefix) - encoderinfo = im.encoderinfo encoderconfig = im.encoderconfig + + ifd = ImageFileDirectory_v2(prefix=prefix) + if encoderinfo.get("bigtiff"): + ifd._bigtiff = True + try: compression = encoderinfo["compression"] except KeyError: From 8bdcadcbe999c9a2becd6aa2997eb4d74f8ddf2b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 10:16:00 +1100 Subject: [PATCH 502/652] Renamed argument to big_tiff --- Tests/test_file_tiff.py | 2 +- docs/handbook/image-file-formats.rst | 2 +- src/PIL/TiffImagePlugin.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index df2c4ebea..dedd48c20 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -117,7 +117,7 @@ class TestFileTiff: def test_bigtiff_save(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") - hopper().save(outfile, bigtiff=True) + hopper().save(outfile, big_tiff=True) with Image.open(outfile) as im: assert im.tag_v2._bigtiff is True diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index d956d12d1..4a220aae6 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1208,7 +1208,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 8.4.0 -**bigtiff** +**big_tiff** If true, the image will be saved as a BigTIFF. **compression** diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 013f34a4f..61eb15243 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1691,7 +1691,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: encoderconfig = im.encoderconfig ifd = ImageFileDirectory_v2(prefix=prefix) - if encoderinfo.get("bigtiff"): + if encoderinfo.get("big_tiff"): ifd._bigtiff = True try: From e27115ee8da08ace01308b6cf6f66ccb75bda360 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 23:31:05 +0000 Subject: [PATCH 503/652] Update dependency mypy to v1.14.1 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index cd1b1a1a1..10e59b885 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.14.0 +mypy==1.14.1 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 1de617fbe725dcf0862b0d036e3d9cffe05b089f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 11:13:14 +1100 Subject: [PATCH 504/652] Added release notes --- docs/handbook/image-file-formats.rst | 2 ++ docs/releasenotes/11.1.0.rst | 26 +++++++------------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 4a220aae6..a915ee4e2 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1211,6 +1211,8 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum **big_tiff** If true, the image will be saved as a BigTIFF. + .. versionadded:: 11.1.0 + **compression** A string containing the desired compression method for the file. (valid only with libtiff installed) Valid compression diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index 27264d99a..1505310fa 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -1,25 +1,6 @@ 11.1.0 ------ -Security -======== - -TODO -^^^^ - -TODO - -:cve:`YYYY-XXXXX`: TODO -^^^^^^^^^^^^^^^^^^^^^^^ - -TODO - -Backwards Incompatible Changes -============================== - -TODO -^^^^ - Deprecations ============ @@ -66,6 +47,13 @@ zlib library, and what version of zlib-ng is being used:: features.check_feature("zlib_ng") # True or False features.version_feature("zlib_ng") # "2.2.2" for example, or None +Saving TIFF as BigTIFF +^^^^^^^^^^^^^^^^^^^^^^ + +TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument:: + + im.save("out.tiff", big_tiff=True) + Other Changes ============= From f91b111fac15e7e10be7323b291a15e238ba25b5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 20:42:49 +1100 Subject: [PATCH 505/652] Removed pre-C99 definitions --- src/libImaging/ImPlatform.h | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index c9b7e43b4..2ce282241 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -44,8 +44,6 @@ 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 @@ -55,34 +53,6 @@ #define INT32 int32_t #define UINT32 uint32_t -#else /* < C99 */ - -#define INT8 signed char - -#if SIZEOF_SHORT == 2 -#define INT16 short -#elif SIZEOF_INT == 2 -#define INT16 int -#else -#error Cannot find required 16-bit integer type -#endif - -#if SIZEOF_SHORT == 4 -#define INT32 short -#elif SIZEOF_INT == 4 -#define INT32 int -#elif SIZEOF_LONG == 4 -#define INT32 long -#else -#error Cannot find required 32-bit integer type -#endif - -#define UINT8 unsigned char -#define UINT16 unsigned INT16 -#define UINT32 unsigned INT32 - -#endif /* < C99 */ - #endif /* not WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ From d42f22baafca30050f4fc8b6bafcc39ef624d685 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 21:38:05 +1100 Subject: [PATCH 506/652] Added release notes --- docs/releasenotes/11.1.0.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index 27264d99a..aec7633a2 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -80,6 +80,11 @@ Saving JPEG 2000 CMYK images With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files. +Minimum C version +^^^^^^^^^^^^^^^^^ + +C99 is now the minimum version of C required to compile Pillow from source. + zlib-ng in wheels ^^^^^^^^^^^^^^^^^ From 06e02cc1d98dbcfb09839da080ee8eb318baa4ba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 20:48:55 +1100 Subject: [PATCH 507/652] Added compile-time mozjpeg feature flag --- src/PIL/features.py | 4 +++- src/_imaging.c | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index 3645e3def..ae7ea4255 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -127,6 +127,7 @@ features: dict[str, tuple[str, str | bool, str | None]] = { "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), + "mozjpeg": ("PIL._imaging", "HAVE_MOZJPEG", "libjpeg_turbo_version"), "zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"), "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), "xcb": ("PIL._imaging", "HAVE_XCB", None), @@ -300,7 +301,8 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: if name == "jpg": libjpeg_turbo_version = version_feature("libjpeg_turbo") if libjpeg_turbo_version is not None: - v = "libjpeg-turbo " + libjpeg_turbo_version + v = "mozjpeg" if check_feature("mozjpeg") else "libjpeg-turbo" + v += " " + libjpeg_turbo_version if v is None: v = version(name) if v is not None: diff --git a/src/_imaging.c b/src/_imaging.c index 5d6d97bed..00772d012 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -76,6 +76,13 @@ #ifdef HAVE_LIBJPEG #include "jconfig.h" +#ifdef LIBJPEG_TURBO_VERSION +#define JCONFIG_INCLUDED +#ifdef __CYGWIN__ +#define _BASETSD_H +#endif +#include "jpeglib.h" +#endif #endif #ifdef HAVE_LIBZ @@ -4367,6 +4374,15 @@ setup_module(PyObject *m) { Py_INCREF(have_libjpegturbo); PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo); + PyObject *have_mozjpeg; +#ifdef JPEG_C_PARAM_SUPPORTED + have_mozjpeg = Py_True; +#else + have_mozjpeg = Py_False; +#endif + Py_INCREF(have_mozjpeg); + PyModule_AddObject(m, "HAVE_MOZJPEG", have_mozjpeg); + PyObject *have_libimagequant; #ifdef HAVE_LIBIMAGEQUANT have_libimagequant = Py_True; From ae59b039564eefdebec4ea67a712a115d0e1ab67 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 21:40:12 +1100 Subject: [PATCH 508/652] Do not use MozJPEG progressive default --- Tests/test_file_jpeg.py | 13 ++++++++++--- src/libImaging/JpegEncode.c | 11 ++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index bf0dec4b8..d4c0636c6 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -281,7 +281,10 @@ class TestFileJpeg: assert not im2.info.get("progressive") assert im3.info.get("progressive") - assert_image_equal(im1, im3) + if features.check_feature("mozjpeg"): + assert_image_similar(im1, im3, 9.39) + else: + assert_image_equal(im1, im3) assert im1_bytes >= im3_bytes def test_progressive_large_buffer(self, tmp_path: Path) -> None: @@ -424,8 +427,12 @@ class TestFileJpeg: im2 = self.roundtrip(hopper(), progressive=1) im3 = self.roundtrip(hopper(), progression=1) # compatibility - assert_image_equal(im1, im2) - assert_image_equal(im1, im3) + if features.check_feature("mozjpeg"): + assert_image_similar(im1, im2, 9.39) + assert_image_similar(im1, im3, 9.39) + else: + assert_image_equal(im1, im2) + assert_image_equal(im1, im3) assert im2.info.get("progressive") assert im2.info.get("progression") assert im3.info.get("progressive") diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 4372d51d5..3c11eac22 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -134,7 +134,16 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { return -1; } - /* Compressor configuration */ + /* Compressor configuration */ +#ifdef JPEG_C_PARAM_SUPPORTED + /* MozJPEG */ + if (!context->progressive) { + /* Do not use MozJPEG progressive default */ + jpeg_c_set_int_param( + &context->cinfo, JINT_COMPRESS_PROFILE, JCP_FASTEST + ); + } +#endif jpeg_set_defaults(&context->cinfo); /* Prevent RGB -> YCbCr conversion */ From e34427167ddbaeece43490c4054c1e17fa21d77b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 23:26:09 +1100 Subject: [PATCH 509/652] Added CentOS Stream 10 --- .github/workflows/test-docker.yml | 1 + docs/installation/platform-support.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index cc5f9d4a5..4b01a10e4 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -44,6 +44,7 @@ jobs: amazon-2023-amd64, arch, centos-stream-9-amd64, + centos-stream-10-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, fedora-40-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 35f863374..3741c5956 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -27,6 +27,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| CentOS Stream 10 | 3.12 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 40 | 3.12 | x86-64 | From d626e6ab9f37c6bc27036982e282d42012ed0cab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Jan 2025 09:07:41 +1100 Subject: [PATCH 510/652] text is a property --- Tests/test_file_png.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 974e1e75f..d87883279 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -618,7 +618,7 @@ class TestFilePng: with Image.open("Tests/images/truncated_image.png") as im: # The file is truncated with pytest.raises(OSError): - im.text() + im.text ImageFile.LOAD_TRUNCATED_IMAGES = True assert isinstance(im.text, dict) ImageFile.LOAD_TRUNCATED_IMAGES = False From 8d78cfcc5a798c59a193b80e200d9845992326ab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Jan 2025 09:10:16 +1100 Subject: [PATCH 511/652] Added return types --- Tests/test_file_jpeg.py | 2 +- src/PIL/TiffImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index bf0dec4b8..52fc9239c 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -181,7 +181,7 @@ class TestFileJpeg: assert test(100, 200) == (100, 200) assert test(0) is None # square pixels - def test_dpi_jfif_cm(self): + def test_dpi_jfif_cm(self) -> None: with Image.open("Tests/images/jfif_unit_cm.jpg") as im: assert im.info["dpi"] == (2.54, 5.08) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 61eb15243..93ad89032 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -949,7 +949,7 @@ class ImageFileDirectory_v2(_IFDv2Base): warnings.warn(str(msg)) return - def _get_ifh(self): + def _get_ifh(self) -> bytes: ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42) if self._bigtiff: ifh += self._pack("HH", 8, 0) From beda2b6e8d20050a16fbc261753fd30e410fba93 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Jan 2025 10:49:24 +1100 Subject: [PATCH 512/652] Removed unused image open --- Tests/test_file_ico.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 37770498a..e81aae669 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -253,8 +253,7 @@ def test_truncated_mask() -> None: try: with Image.open(io.BytesIO(data)) as im: - with Image.open("Tests/images/hopper_mask.png") as expected: - assert im.mode == "1" + assert im.mode == "1" # 32 bpp output = io.BytesIO() From b89cc09944b4add584967bf1fa21208e92442def Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Jan 2025 12:22:55 +1100 Subject: [PATCH 513/652] Corrected BLP1 alpha depth handling --- Tests/test_file_blp.py | 1 + src/PIL/BlpImagePlugin.py | 43 ++++++++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 1e2f20c40..1f32be9c1 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -19,6 +19,7 @@ def test_load_blp1() -> None: assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im: + assert im.mode == "RGBA" im.load() diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 2d03af9d7..0d882fe96 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -260,18 +260,21 @@ class BlpImageFile(ImageFile.ImageFile): def _open(self) -> None: self.magic = self.fp.read(4) - self.fp.seek(5, os.SEEK_CUR) - (self._blp_alpha_depth,) = struct.unpack(" None: assert im.palette is not None fp.write(struct.pack(" Date: Wed, 1 Jan 2025 22:58:04 +1100 Subject: [PATCH 514/652] Do not reread start of header in decoder --- src/PIL/BlpImagePlugin.py | 127 +++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 65 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 0d882fe96..c932b3b9c 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -259,24 +259,36 @@ class BlpImageFile(ImageFile.ImageFile): def _open(self) -> None: self.magic = self.fp.read(4) - - if self.magic == b"BLP1": - self.fp.seek(4, os.SEEK_CUR) - (self._blp_alpha_depth,) = struct.unpack(" tuple[int, int]: try: - self._read_blp_header() + self._read_header() self._load() except struct.error as e: msg = "Truncated BLP file" @@ -295,28 +307,9 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): def _load(self) -> None: pass - def _read_blp_header(self) -> None: - assert self.fd is not None - self.fd.seek(4) - (self._blp_compression,) = struct.unpack(" None: + self._offsets = struct.unpack("<16I", self._safe_read(16 * 4)) + self._lengths = struct.unpack("<16I", self._safe_read(16 * 4)) def _safe_read(self, length: int) -> bytes: assert self.fd is not None @@ -332,9 +325,11 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): ret.append((b, g, r, a)) return ret - def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray: + def _read_bgra( + self, palette: list[tuple[int, int, int, int]], alpha: bool + ) -> bytearray: data = bytearray() - _data = BytesIO(self._safe_read(self._blp_lengths[0])) + _data = BytesIO(self._safe_read(self._lengths[0])) while True: try: (offset,) = struct.unpack(" None: - if self._blp_compression == Format.JPEG: + self._compression, self._encoding, alpha = self.args + + if self._compression == Format.JPEG: self._decode_jpeg_stream() - elif self._blp_compression == 1: - if self._blp_encoding in (4, 5): + elif self._compression == 1: + if self._encoding in (4, 5): palette = self._read_palette() - data = self._read_bgra(palette) + data = self._read_bgra(palette, alpha) self.set_as_raw(data) else: - msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}" + msg = f"Unsupported BLP encoding {repr(self._encoding)}" raise BLPFormatError(msg) else: - msg = f"Unsupported BLP compression {repr(self._blp_encoding)}" + msg = f"Unsupported BLP compression {repr(self._encoding)}" raise BLPFormatError(msg) def _decode_jpeg_stream(self) -> None: @@ -371,8 +368,8 @@ class BLP1Decoder(_BLPBaseDecoder): (jpeg_header_size,) = struct.unpack(" None: + self._compression, self._encoding, alpha, self._alpha_encoding = self.args + palette = self._read_palette() assert self.fd is not None - self.fd.seek(self._blp_offsets[0]) + self.fd.seek(self._offsets[0]) - if self._blp_compression == 1: + if self._compression == 1: # Uncompressed or DirectX compression - if self._blp_encoding == Encoding.UNCOMPRESSED: - data = self._read_bgra(palette) + if self._encoding == Encoding.UNCOMPRESSED: + data = self._read_bgra(palette, alpha) - elif self._blp_encoding == Encoding.DXT: + elif self._encoding == Encoding.DXT: data = bytearray() - if self._blp_alpha_encoding == AlphaEncoding.DXT1: - linesize = (self.size[0] + 3) // 4 * 8 - for yb in range((self.size[1] + 3) // 4): - for d in decode_dxt1( - self._safe_read(linesize), alpha=bool(self._blp_alpha_depth) - ): + if self._alpha_encoding == AlphaEncoding.DXT1: + linesize = (self.state.xsize + 3) // 4 * 8 + for yb in range((self.state.ysize + 3) // 4): + for d in decode_dxt1(self._safe_read(linesize), alpha): data += d - elif self._blp_alpha_encoding == AlphaEncoding.DXT3: - linesize = (self.size[0] + 3) // 4 * 16 - for yb in range((self.size[1] + 3) // 4): + elif self._alpha_encoding == AlphaEncoding.DXT3: + linesize = (self.state.xsize + 3) // 4 * 16 + for yb in range((self.state.ysize + 3) // 4): for d in decode_dxt3(self._safe_read(linesize)): data += d - elif self._blp_alpha_encoding == AlphaEncoding.DXT5: - linesize = (self.size[0] + 3) // 4 * 16 - for yb in range((self.size[1] + 3) // 4): + elif self._alpha_encoding == AlphaEncoding.DXT5: + linesize = (self.state.xsize + 3) // 4 * 16 + for yb in range((self.state.ysize + 3) // 4): for d in decode_dxt5(self._safe_read(linesize)): data += d else: - msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}" + msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}" raise BLPFormatError(msg) else: - msg = f"Unknown BLP encoding {repr(self._blp_encoding)}" + msg = f"Unknown BLP encoding {repr(self._encoding)}" raise BLPFormatError(msg) else: - msg = f"Unknown BLP compression {repr(self._blp_compression)}" + msg = f"Unknown BLP compression {repr(self._compression)}" raise BLPFormatError(msg) self.set_as_raw(data) From 5d998d3fedb06666ae680e3ebe3f3547a9059727 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Jan 2025 23:38:24 +1100 Subject: [PATCH 515/652] Improved coverage --- Tests/test_file_blp.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 1f32be9c1..9f2de8f98 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from PIL import Image +from PIL import BlpImagePlugin, Image from .helper import ( assert_image_equal, @@ -38,6 +38,13 @@ def test_load_blp2_dxt1a() -> None: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(BlpImagePlugin.BLPFormatError): + BlpImagePlugin.BlpImageFile(invalid_file) + + def test_save(tmp_path: Path) -> None: f = str(tmp_path / "temp.blp") From f636cb8c156f53cb3acd3ebf7164113850df3f27 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Jan 2025 10:28:51 +1100 Subject: [PATCH 516/652] Updated freetype to 2.13.3 --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4e0fad79f..9059c04a4 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -37,7 +37,7 @@ fi ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds -FREETYPE_VERSION=2.13.2 +FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=10.1.0 LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.1.0 From 4c1aed801e43c6b307e7135279ca1dbc02bbf052 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Jan 2025 16:00:59 +1100 Subject: [PATCH 517/652] 11.1.0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 0807f949c..9938a0afc 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "11.1.0.dev0" +__version__ = "11.1.0" From 57786a252b2e3abd63242800ab06511bb315b2d8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Jan 2025 19:04:18 +1100 Subject: [PATCH 518/652] 11.2.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 9938a0afc..e93c7887b 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "11.1.0" +__version__ = "11.2.0.dev0" From 6b4619c4f5998d8d40de32de7b17b664d9b8a0db Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Jan 2025 20:46:58 +1100 Subject: [PATCH 519/652] Updated macOS tested Pillow versions --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 3741c5956..756194679 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -77,7 +77,7 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+============================+==================+==============+ -| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm | +| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm | | +----------------------------+------------------+ | | | 3.8 | 10.4.0 | | +----------------------------------+----------------------------+------------------+--------------+ From ade15fcdd3c9f41606ce560c4b5fdeb01f0025e2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:46:24 +0200 Subject: [PATCH 520/652] Upgrade zlib-ng to 2.2.3 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4e0fad79f..e89db5020 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -50,7 +50,7 @@ if [[ -n "$IS_MACOS" ]]; then else GIFLIB_VERSION=5.2.1 fi -ZLIB_NG_VERSION=2.2.2 +ZLIB_NG_VERSION=2.2.3 LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0674a9a15..75d6aa1bd 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ V = { "OPENJPEG": "2.5.3", "TIFF": "4.6.0", "XZ": "5.6.3", - "ZLIBNG": "2.2.2", + "ZLIBNG": "2.2.3", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From 2d7597ac6a431d283a65d1d17622a6d8f9918010 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Jan 2025 22:50:25 +1100 Subject: [PATCH 521/652] Updated to giflib 5.2.2 on Linux --- .github/workflows/wheels-dependencies.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4e0fad79f..71609a6f4 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -45,11 +45,7 @@ OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 -if [[ -n "$IS_MACOS" ]]; then - GIFLIB_VERSION=5.2.2 -else - GIFLIB_VERSION=5.2.1 -fi +GIFLIB_VERSION=5.2.2 ZLIB_NG_VERSION=2.2.2 LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 @@ -139,6 +135,14 @@ function build { CFLAGS="$CFLAGS -O3 -DNDEBUG" if [[ -n "$IS_MACOS" ]]; then CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" + # For giflib 5.2.2 + elif [ -n "$IS_ALPINE" ]; then + apk add imagemagick + else + if [[ "$MB_ML_VER" == "_2_28" ]]; then + yum install -y epel-release + fi + yum install -y ImageMagick fi build_libwebp CFLAGS=$ORIGINAL_CFLAGS From 1678f7f2155beafa594c3561179f4069f9318d35 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:38:21 +0100 Subject: [PATCH 522/652] Add overloads for exif_transpose --- src/PIL/ImageOps.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index bb29cc0d3..fef1d7328 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -22,7 +22,7 @@ import functools import operator import re from collections.abc import Sequence -from typing import Protocol, cast +from typing import Literal, Protocol, cast, overload from . import ExifTags, Image, ImagePalette @@ -673,6 +673,16 @@ def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: return _lut(image, lut) +@overload +def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ... + + +@overload +def exif_transpose( + image: Image.Image, *, in_place: Literal[False] = False +) -> Image.Image: ... + + def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: """ If an image has an EXIF Orientation tag, other than 1, transpose the image From 1d771ff4a40e8eb9a38c150d18767cddf01c8a47 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 Jan 2025 10:26:47 +1100 Subject: [PATCH 523/652] Do not call yum on cifuzz --- .github/workflows/wheels-dependencies.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 1ebc49a88..ceb7911be 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -99,7 +99,7 @@ function build_harfbuzz { function build { build_xz - if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then + if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi build_zlib_ng @@ -138,6 +138,8 @@ function build { # For giflib 5.2.2 elif [ -n "$IS_ALPINE" ]; then apk add imagemagick + elif [ -n "$SANITIZER" ]; then + apt-get install -y imagemagick else if [[ "$MB_ML_VER" == "_2_28" ]]; then yum install -y epel-release From d12e78badf1fc4a102b4bec044eb12a6bfd5d0aa Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 3 Jan 2025 11:00:19 +1100 Subject: [PATCH 524/652] Removed exif_transpose return type checks --- Tests/test_file_jpeg.py | 1 - Tests/test_imageops.py | 6 ------ 2 files changed, 7 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index bf0dec4b8..dd62460bb 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -353,7 +353,6 @@ class TestFileJpeg: assert exif.get_ifd(0x8825) == {} transposed = ImageOps.exif_transpose(im) - assert transposed is not None exif = transposed.getexif() assert exif.get_ifd(0x8825) == {} diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 2fb2a60b6..7262f29e6 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -405,7 +405,6 @@ def test_exif_transpose() -> None: else: original_exif = im.info["exif"] transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert_image_similar(base_im, transposed_im, 17) if orientation_im is base_im: assert "exif" not in im.info @@ -417,7 +416,6 @@ def test_exif_transpose() -> None: # Repeat the operation to test that it does not keep transposing transposed_im2 = ImageOps.exif_transpose(transposed_im) - assert transposed_im2 is not None assert_image_equal(transposed_im2, transposed_im) check(base_im) @@ -433,7 +431,6 @@ def test_exif_transpose() -> None: assert im.getexif()[0x0112] == 3 transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() transposed_im._reload_exif() @@ -446,14 +443,12 @@ def test_exif_transpose() -> None: assert im.getexif()[0x0112] == 3 transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() # Orientation set directly on Image.Exif im = hopper() im.getexif()[0x0112] = 3 transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() @@ -464,7 +459,6 @@ def test_exif_transpose_xml_without_xmp() -> None: del im.info["xmp"] transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() From 036db2da87dba7283207b8b61cf9ca131d1223a3 Mon Sep 17 00:00:00 2001 From: "Harm.van.den.brand@alliander.com" Date: Thu, 2 Jan 2025 16:47:24 +0100 Subject: [PATCH 525/652] OSError caused by decode error should use string argument to be in line with rest of module --- Tests/test_file_libtiff.py | 2 +- src/PIL/TiffImagePlugin.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 9c49b1534..49d71aca7 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1146,7 +1146,7 @@ class TestFileLibTiff(LibTiffTestCase): im.load() # Assert that the error code is IMAGING_CODEC_MEMORY - assert str(e.value) == "-9" + assert str(e.value) == "decoder error -9" @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 61eb15243..bbbd656c6 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1406,7 +1406,8 @@ class TiffImageFile(ImageFile.ImageFile): self.fp = None # might be shared if err < 0: - raise OSError(err) + msg = f"decoder error {err}" + raise OSError(msg) return Image.Image.load(self) From cce0f5b653abcdf99a7bbba6757f000b3fc4cd7e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 4 Jan 2025 10:34:59 +1100 Subject: [PATCH 526/652] Removed giflib as webp dependency --- .github/workflows/wheels-dependencies.sh | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 1ebc49a88..05167d969 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -45,7 +45,6 @@ OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 -GIFLIB_VERSION=5.2.2 ZLIB_NG_VERSION=2.2.3 LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 @@ -135,16 +134,10 @@ function build { CFLAGS="$CFLAGS -O3 -DNDEBUG" if [[ -n "$IS_MACOS" ]]; then CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" - # For giflib 5.2.2 - elif [ -n "$IS_ALPINE" ]; then - apk add imagemagick - else - if [[ "$MB_ML_VER" == "_2_28" ]]; then - yum install -y epel-release - fi - yum install -y ImageMagick fi - build_libwebp + build_simple libwebp $LIBWEBP_VERSION \ + https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \ + --enable-libwebpmux --enable-libwebpdemux CFLAGS=$ORIGINAL_CFLAGS build_brotli From bd56a956594445c9b2e0bd5004f1b5c1a3f96b38 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Jan 2025 12:43:50 +1100 Subject: [PATCH 527/652] Use namedtuple _replace --- src/PIL/BlpImagePlugin.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index c932b3b9c..b8a95db87 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -374,11 +374,9 @@ class BLP1Decoder(_BLPBaseDecoder): image = JpegImageFile(BytesIO(data)) Image._decompression_bomb_check(image.size) if image.mode == "CMYK": - decoder_name, extents, offset, args = image.tile[0] + args = image.tile[0].args assert isinstance(args, tuple) - image.tile = [ - ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK")) - ] + image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))] r, g, b = image.convert("RGB").split() reversed_image = Image.merge("RGB", (b, g, r)) self.set_as_raw(reversed_image.tobytes()) From 73a383fa7211adf5ed8ffa43288e6bc47daa125e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 5 Jan 2025 06:11:54 +1100 Subject: [PATCH 528/652] Use rawmode instead of splitting and merging --- src/PIL/BlpImagePlugin.py | 4 +--- src/libImaging/Unpack.c | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index b8a95db87..8585a8e60 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -377,9 +377,7 @@ class BLP1Decoder(_BLPBaseDecoder): args = image.tile[0].args assert isinstance(args, tuple) image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))] - r, g, b = image.convert("RGB").split() - reversed_image = Image.merge("RGB", (b, g, r)) - self.set_as_raw(reversed_image.tobytes()) + self.set_as_raw(image.convert("RGB").tobytes(), "BGR") class BLP2Decoder(_BLPBaseDecoder): diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index e9203fe4d..9c3ee2665 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1664,6 +1664,7 @@ static struct { {"RGBA", "RGBaXX", 48, unpackRGBaskip2}, {"RGBA", "RGBa;16L", 64, unpackRGBa16L}, {"RGBA", "RGBa;16B", 64, unpackRGBa16B}, + {"RGBA", "BGR", 24, ImagingUnpackBGR}, {"RGBA", "BGRa", 32, unpackBGRa}, {"RGBA", "RGBA;I", 32, unpackRGBAI}, {"RGBA", "RGBA;L", 32, unpackRGBAL}, From 4ecf8cbd75051c7213a433f80b6a9f24e4367311 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 5 Jan 2025 14:49:34 +1100 Subject: [PATCH 529/652] Simplified code --- src/_imagingft.c | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index d38279f3e..3a65007a5 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -339,29 +339,23 @@ text_layout_raqm( len = PySequence_Fast_GET_SIZE(seq); for (j = 0; j < len; j++) { PyObject *item = PySequence_Fast_GET_ITEM(seq, j); - char *feature = NULL; - Py_ssize_t size = 0; - PyObject *bytes; - if (!PyUnicode_Check(item)) { Py_DECREF(seq); PyErr_SetString(PyExc_TypeError, "expected a string"); goto failed; } - bytes = PyUnicode_AsUTF8String(item); - if (bytes == NULL) { + + Py_ssize_t size; + const char *feature = PyUnicode_AsUTF8AndSize(item, &size); + if (feature == NULL) { Py_DECREF(seq); goto failed; } - feature = PyBytes_AS_STRING(bytes); - size = PyBytes_GET_SIZE(bytes); if (!raqm_add_font_feature(rq, feature, size)) { Py_DECREF(seq); - Py_DECREF(bytes); PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); goto failed; } - Py_DECREF(bytes); } Py_DECREF(seq); } From 7708e4b524aca2e7d56fcc75eee59834622f75e8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 6 Jan 2025 20:30:47 +1100 Subject: [PATCH 530/652] Improved Docker coverage reporting --- .ci/after_success.sh | 6 +----- .github/workflows/test-docker.yml | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.ci/after_success.sh b/.ci/after_success.sh index c71546f00..6da27b975 100755 --- a/.ci/after_success.sh +++ b/.ci/after_success.sh @@ -2,8 +2,4 @@ # gather the coverage data python3 -m pip install coverage -if [[ $MATRIX_DOCKER ]]; then - python3 -m coverage xml --ignore-errors -else - python3 -m coverage xml -fi +python3 -m coverage xml diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 4b01a10e4..bebb9cda2 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -90,15 +90,15 @@ jobs: - name: After success run: | - PATH="$PATH:~/.local/bin" docker start pillow_container + sudo docker cp pillow_container:/Pillow /Pillow + sudo chown -R runner /Pillow pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'` docker stop pillow_container sudo mkdir -p $pil_path sudo cp src/PIL/*.py $pil_path + cd /Pillow .ci/after_success.sh - env: - MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage uses: codecov/codecov-action@v5 From b1749dff08ab96a05234e1492759011ef54cbd59 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:35:41 +0000 Subject: [PATCH 531/652] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.8.4 → v0.8.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.4...v0.8.6) - [github.com/pre-commit/mirrors-clang-format: v19.1.5 → v19.1.6](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.5...v19.1.6) - [github.com/woodruffw/zizmor-pre-commit: v0.10.0 → v1.0.0](https://github.com/woodruffw/zizmor-pre-commit/compare/v0.10.0...v1.0.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b76f92ec0..20fa7d04f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.4 + rev: v0.8.6 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.5 + rev: v19.1.6 hooks: - id: clang-format types: [c] @@ -57,7 +57,7 @@ repos: - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v0.10.0 + rev: v1.0.0 hooks: - id: zizmor From 618339e2d2e9313ab8f2da0d78efec25477c4b43 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 6 Jan 2025 07:28:38 +1100 Subject: [PATCH 532/652] Allow saving multiple frames as BigTIFF --- Tests/test_file_tiff.py | 19 +++++++++-- src/PIL/TiffImagePlugin.py | 69 +++++++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index dedd48c20..c4a334881 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -117,10 +117,16 @@ class TestFileTiff: def test_bigtiff_save(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") - hopper().save(outfile, big_tiff=True) + im = hopper() + im.save(outfile, big_tiff=True) - with Image.open(outfile) as im: - assert im.tag_v2._bigtiff is True + with Image.open(outfile) as reloaded: + assert reloaded.tag_v2._bigtiff is True + + im.save(outfile, save_all=True, append_images=[im], big_tiff=True) + + with Image.open(outfile) as reloaded: + assert reloaded.tag_v2._bigtiff is True def test_seek_too_large(self) -> None: with pytest.raises(ValueError, match="Unable to seek to frame"): @@ -753,6 +759,13 @@ class TestFileTiff: with pytest.raises(RuntimeError): a.fixOffsets(1) + def test_appending_tiff_writer_writelong(self) -> None: + data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b = BytesIO(data) + with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.writeLong(2**32 - 1) + assert b.getvalue() == data + b"\xff\xff\xff\xff" + def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 61eb15243..5dd56d92b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -962,13 +962,16 @@ class ImageFileDirectory_v2(_IFDv2Base): result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2)) entries: list[tuple[int, int, int, bytes, bytes]] = [] - offset += len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + 4 + + fmt = "Q" if self._bigtiff else "L" + fmt_size = 8 if self._bigtiff else 4 + offset += ( + len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size + ) stripoffsets = None # pass 1: convert tags to binary format # always write tags in ascending order - fmt = "Q" if self._bigtiff else "L" - fmt_size = 8 if self._bigtiff else 4 for tag, value in sorted(self._tags_v2.items()): if tag == STRIPOFFSETS: stripoffsets = len(entries) @@ -1024,7 +1027,7 @@ class ImageFileDirectory_v2(_IFDv2Base): ) # -- overwrite here for multi-page -- - result += b"\0\0\0\0" # end of entries + result += self._pack(fmt, 0) # end of entries # pass 3: write auxiliary data to file for tag, typ, count, value, data in entries: @@ -2043,20 +2046,21 @@ class AppendingTiffWriter(io.BytesIO): self.offsetOfNewPage = 0 self.IIMM = iimm = self.f.read(4) + self._bigtiff = b"\x2B" in iimm if not iimm: # empty file - first page self.isFirst = True return self.isFirst = False - if iimm == b"II\x2a\x00": - self.setEndian("<") - elif iimm == b"MM\x00\x2a": - self.setEndian(">") - else: + if iimm not in PREFIXES: msg = "Invalid TIFF file header" raise RuntimeError(msg) + self.setEndian("<" if iimm.startswith(II) else ">") + + if self._bigtiff: + self.f.seek(4, os.SEEK_CUR) self.skipIFDs() self.goToEnd() @@ -2076,11 +2080,13 @@ class AppendingTiffWriter(io.BytesIO): msg = "IIMM of new page doesn't match IIMM of first page" raise RuntimeError(msg) - ifd_offset = self.readLong() + if self._bigtiff: + self.f.seek(4, os.SEEK_CUR) + ifd_offset = self._read(8 if self._bigtiff else 4) ifd_offset += self.offsetOfNewPage assert self.whereToWriteNewIFDOffset is not None self.f.seek(self.whereToWriteNewIFDOffset) - self.writeLong(ifd_offset) + self._write(ifd_offset, 8 if self._bigtiff else 4) self.f.seek(ifd_offset) self.fixIFD() @@ -2126,18 +2132,20 @@ class AppendingTiffWriter(io.BytesIO): self.endian = endian self.longFmt = f"{self.endian}L" self.shortFmt = f"{self.endian}H" - self.tagFormat = f"{self.endian}HHL" + self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L") def skipIFDs(self) -> None: while True: - ifd_offset = self.readLong() + ifd_offset = self._read(8 if self._bigtiff else 4) if ifd_offset == 0: - self.whereToWriteNewIFDOffset = self.f.tell() - 4 + self.whereToWriteNewIFDOffset = self.f.tell() - ( + 8 if self._bigtiff else 4 + ) break self.f.seek(ifd_offset) - num_tags = self.readShort() - self.f.seek(num_tags * 12, os.SEEK_CUR) + num_tags = self._read(8 if self._bigtiff else 2) + self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR) def write(self, data: Buffer, /) -> int: return self.f.write(data) @@ -2185,13 +2193,17 @@ class AppendingTiffWriter(io.BytesIO): def rewriteLastLong(self, value: int) -> None: return self._rewriteLast(value, 4) + def _write(self, value: int, field_size: int) -> None: + bytes_written = self.f.write( + struct.pack(self.endian + self._fmt(field_size), value) + ) + self._verify_bytes_written(bytes_written, field_size) + def writeShort(self, value: int) -> None: - bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - self._verify_bytes_written(bytes_written, 2) + self._write(value, 2) def writeLong(self, value: int) -> None: - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - self._verify_bytes_written(bytes_written, 4) + self._write(value, 4) def close(self) -> None: self.finalize() @@ -2199,24 +2211,27 @@ class AppendingTiffWriter(io.BytesIO): self.f.close() def fixIFD(self) -> None: - num_tags = self.readShort() + num_tags = self._read(8 if self._bigtiff else 2) for i in range(num_tags): - tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8)) + tag, field_type, count = struct.unpack( + self.tagFormat, self.f.read(12 if self._bigtiff else 8) + ) field_size = self.fieldSizes[field_type] total_size = field_size * count - is_local = total_size <= 4 + fmt_size = 8 if self._bigtiff else 4 + is_local = total_size <= fmt_size if not is_local: - offset = self.readLong() + self.offsetOfNewPage - self.rewriteLastLong(offset) + offset = self._read(fmt_size) + self.offsetOfNewPage + self._rewriteLast(offset, fmt_size) if tag in self.Tags: cur_pos = self.f.tell() if is_local: self._fixOffsets(count, field_size) - self.f.seek(cur_pos + 4) + self.f.seek(cur_pos + fmt_size) else: self.f.seek(offset) self._fixOffsets(count, field_size) @@ -2224,7 +2239,7 @@ class AppendingTiffWriter(io.BytesIO): elif is_local: # skip the locally stored value that is not an offset - self.f.seek(4, os.SEEK_CUR) + self.f.seek(fmt_size, os.SEEK_CUR) def _fixOffsets(self, count: int, field_size: int) -> None: for i in range(count): From a8381c619de0c244785377322ed8bf115a899146 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 6 Jan 2025 07:28:51 +1100 Subject: [PATCH 533/652] Allow upgrading LONG to LONG8 --- Tests/test_file_tiff.py | 26 ++++++++++++++++++++++++- src/PIL/TiffImagePlugin.py | 39 ++++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index c4a334881..757d3f96a 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -746,7 +746,7 @@ class TestFileTiff: assert reread.n_frames == 3 def test_fixoffsets(self) -> None: - b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") + b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00") with TiffImagePlugin.AppendingTiffWriter(b) as a: b.seek(0) a.fixOffsets(1, isShort=True) @@ -759,6 +759,23 @@ class TestFileTiff: with pytest.raises(RuntimeError): a.fixOffsets(1) + b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00") + with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.offsetOfNewPage = 2**16 + + b.seek(0) + a.fixOffsets(1, isShort=True) + + b = BytesIO(b"II\x2B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") + with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.offsetOfNewPage = 2**32 + + b.seek(0) + a.fixOffsets(1, isShort=True) + + b.seek(0) + a.fixOffsets(1, isLong=True) + def test_appending_tiff_writer_writelong(self) -> None: data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b = BytesIO(data) @@ -766,6 +783,13 @@ class TestFileTiff: a.writeLong(2**32 - 1) assert b.getvalue() == data + b"\xff\xff\xff\xff" + def test_appending_tiff_writer_rewritelastshorttolong(self) -> None: + data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b = BytesIO(data) + with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.rewriteLastShortToLong(2**32 - 1) + assert b.getvalue() == data[:-2] + b"\xff\xff\xff\xff" + def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 5dd56d92b..8179b7f5b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2175,17 +2175,19 @@ class AppendingTiffWriter(io.BytesIO): msg = f"wrote only {bytes_written} bytes but wanted {expected}" raise RuntimeError(msg) - def rewriteLastShortToLong(self, value: int) -> None: - self.f.seek(-2, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - self._verify_bytes_written(bytes_written, 4) - - def _rewriteLast(self, value: int, field_size: int) -> None: + def _rewriteLast( + self, value: int, field_size: int, new_field_size: int = 0 + ) -> None: self.f.seek(-field_size, os.SEEK_CUR) + if not new_field_size: + new_field_size = field_size bytes_written = self.f.write( - struct.pack(self.endian + self._fmt(field_size), value) + struct.pack(self.endian + self._fmt(new_field_size), value) ) - self._verify_bytes_written(bytes_written, field_size) + self._verify_bytes_written(bytes_written, new_field_size) + + def rewriteLastShortToLong(self, value: int) -> None: + self._rewriteLast(value, 2, 4) def rewriteLastShort(self, value: int) -> None: return self._rewriteLast(value, 2) @@ -2245,18 +2247,27 @@ class AppendingTiffWriter(io.BytesIO): for i in range(count): offset = self._read(field_size) offset += self.offsetOfNewPage - if field_size == 2 and offset >= 65536: - # offset is now too large - we must convert shorts to longs + + new_field_size = 0 + if self._bigtiff and field_size in (2, 4) and offset >= 2**32: + # offset is now too large - we must convert long to long8 + new_field_size = 8 + elif field_size == 2 and offset >= 2**16: + # offset is now too large - we must convert short to long + new_field_size = 4 + if new_field_size: if count != 1: msg = "not implemented" raise RuntimeError(msg) # XXX TODO # simple case - the offset is just one and therefore it is # local (not referenced with another offset) - self.rewriteLastShortToLong(offset) - self.f.seek(-10, os.SEEK_CUR) - self.writeShort(TiffTags.LONG) # rewrite the type to LONG - self.f.seek(8, os.SEEK_CUR) + self._rewriteLast(offset, field_size, new_field_size) + # Move back past the new offset, past 'count', and before 'field_type' + rewind = -new_field_size - 4 - 2 + self.f.seek(rewind, os.SEEK_CUR) + self.writeShort(new_field_size) # rewrite the type + self.f.seek(2 - rewind, os.SEEK_CUR) else: self._rewriteLast(offset, field_size) From aef6df2d04bfe86b4a69ab7d93786ea55a3e7340 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 7 Jan 2025 21:33:57 +1100 Subject: [PATCH 534/652] Use ImageFile._Tile --- 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 dd62460bb..526c6a5b6 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1030,7 +1030,7 @@ class TestFileJpeg: with Image.open(TEST_FILE) as im: im.tile = [ - ("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), + ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), ] ImageFile.LOAD_TRUNCATED_IMAGES = True im.load() From f36c66746705245dec44b225868bce727dca0385 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 7 Jan 2025 22:24:08 +1100 Subject: [PATCH 535/652] Improved test coverage --- Tests/test_file_spider.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 4cafda865..713db848d 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -7,7 +7,7 @@ from pathlib import Path import pytest -from PIL import Image, ImageSequence, SpiderImagePlugin +from PIL import Image, SpiderImagePlugin from .helper import assert_image_equal, hopper, is_pypy @@ -153,8 +153,8 @@ def test_nonstack_file() -> None: def test_nonstack_dos() -> None: with Image.open(TEST_FILE) as im: - for i, frame in enumerate(ImageSequence.Iterator(im)): - assert i <= 1, "Non-stack DOS file test failed" + with pytest.raises(EOFError): + im.seek(0) # for issue #4093 From 86b8e1e45fa1d425aafa444024003eb3b75d8a9d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Jan 2025 10:19:09 +1100 Subject: [PATCH 536/652] Updated libpng to 1.6.45 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 58621bca1..410255b7e 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -39,7 +39,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=10.1.0 -LIBPNG_VERSION=1.6.44 +LIBPNG_VERSION=1.6.45 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 75d6aa1bd..912579ce7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ V = { "HARFBUZZ": "10.1.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.16", - "LIBPNG": "1.6.44", + "LIBPNG": "1.6.45", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.6.0", From ee2b8c525632f76bde730805d11d19bbb22f1b2b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Jan 2025 10:26:21 +1100 Subject: [PATCH 537/652] Switch to .tar.gz for libpng --- winbuild/build_prepare.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 912579ce7..b9695d1d8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -123,7 +123,6 @@ V = { "XZ": "5.6.3", "ZLIBNG": "2.2.3", } -V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) @@ -241,8 +240,8 @@ DEPS: dict[str, dict[str, Any]] = { }, "libpng": { "url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/" - f"lpng{V['LIBPNG_DOTLESS']}.zip/download", - "filename": f"lpng{V['LIBPNG_DOTLESS']}.zip", + f"FILENAME/download", + "filename": f"libpng-{V['LIBPNG']}.tar.gz", "license": "LICENSE", "build": [ *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), From 120ba1c13d482b6f8763ab287ac5811a838f8828 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 8 Jan 2025 14:01:06 +0800 Subject: [PATCH 538/652] Rewrite the install_name of the ZLIB-NG library on macOS. --- .github/workflows/wheels-dependencies.sh | 8 ++++++++ pyproject.toml | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 58621bca1..2eac4d3d7 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -72,6 +72,14 @@ function build_zlib_ng { && ./configure --prefix=$BUILD_PREFIX --zlib-compat \ && make -j4 \ && make install) + + if [ -n "$IS_MACOS" ]; then + # Ensure that on macOS, the library name is an absolute path, not an + # @rpath, so that delocate picks up the right library (and doesn't need + # DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an + # option to control the install_name. + install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib + fi touch zlib-stamp } diff --git a/pyproject.toml b/pyproject.toml index 2c6c7bcd0..aaaba0032 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,6 @@ test-extras = "tests" [tool.cibuildwheel.macos.environment] PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" -DYLD_LIBRARY_PATH = "$(pwd)/build/deps/darwin/lib" [tool.black] exclude = "wheels/multibuild" From f281eb9b469320f29006d7454d9c38a974ad65c1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Jan 2025 18:27:20 +1100 Subject: [PATCH 539/652] Trigger from changes in pyproject.toml --- .github/workflows/wheels.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3b22ee98a..fd89f7585 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -13,6 +13,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "pyproject.toml" - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" @@ -23,6 +24,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "pyproject.toml" - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" From 84c8e38b2d88d0c4fb733d9c6e44e6af13e2e05e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 07:38:51 +0000 Subject: [PATCH 540/652] Update cygwin/cygwin-install-action action to v5 --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 5b0a03946..abfeaa77f 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -52,7 +52,7 @@ jobs: persist-credentials: false - name: Install Cygwin - uses: cygwin/cygwin-install-action@v4 + uses: cygwin/cygwin-install-action@v5 with: packages: > gcc-g++ From 2eb112329e4df9ca4ea15235c2743da8f6b0eab8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jan 2025 08:32:42 +1100 Subject: [PATCH 541/652] Use python-pip instead of python3-pip --- .github/workflows/test-mingw.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a1d6ba61c..6b1b36cb0 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -68,7 +68,7 @@ jobs: mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-olefile \ - mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python-pip \ mingw-w64-x86_64-python-pytest \ mingw-w64-x86_64-python-pytest-cov \ mingw-w64-x86_64-python-pytest-timeout \ From 440b09e831873624f9e843adf658633b25983e77 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jan 2025 14:32:17 +1100 Subject: [PATCH 542/652] Removed unused mode argument from assert_image_similar_tofile --- Tests/helper.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index d6a93a803..3f45498bb 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -140,11 +140,8 @@ def assert_image_similar_tofile( filename: str, epsilon: float, msg: str | None = None, - mode: str | None = None, ) -> None: with Image.open(filename) as img: - if mode: - img = img.convert(mode) assert_image_similar(a, img, epsilon, msg) From aa686894a63cb2f15349e9592eedb95aebfc6f1f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jan 2025 14:32:46 +1100 Subject: [PATCH 543/652] Removed unused assert_all_same --- Tests/helper.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 3f45498bb..126644c15 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -145,10 +145,6 @@ def assert_image_similar_tofile( assert_image_similar(a, img, epsilon, msg) -def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None: - assert items.count(items[0]) == len(items), msg - - def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None: assert items.count(items[0]) != len(items), msg From f938af5c3cc9dd7c48f8a06a7af4870f1faf2e76 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jan 2025 14:38:07 +1100 Subject: [PATCH 544/652] Do not catch exception only to assert it is None --- Tests/test_file_apng.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index ee6c867c3..9d5154fca 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -307,13 +307,8 @@ def test_apng_syntax_errors() -> None: im.load() # we can handle this case gracefully - exception = None with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: - try: - im.seek(im.n_frames - 1) - except Exception as e: - exception = e - assert exception is None + im.seek(im.n_frames - 1) with pytest.raises(OSError): with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: @@ -405,13 +400,8 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: append_images=frames, ) with Image.open(test_file) as im: - exception = None - try: - im.seek(im.n_frames - 1) - im.load() - except Exception as e: - exception = e - assert exception is None + im.seek(im.n_frames - 1) + im.load() def test_apng_save_duration_loop(tmp_path: Path) -> None: From a34a9cd6d1d4b346573fca7336f1cb82918af4b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jan 2025 14:49:48 +1100 Subject: [PATCH 545/652] Improved test coverage --- Tests/test_file_iptc.py | 5 +---- Tests/test_file_jpeg2k.py | 3 +-- Tests/test_file_libtiff.py | 6 +----- Tests/test_image.py | 2 -- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 8a7c59fb1..c6c0c1aab 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -58,10 +58,7 @@ def test_getiptcinfo_fotostation() -> None: # Assert assert iptc is not None - for tag in iptc.keys(): - if tag[0] == 240: - return - pytest.fail("FotoStation tag not found") + assert 240 in (tag[0] for tag in iptc.keys()), "FotoStation tag not found" def test_getiptcinfo_zero_padding() -> None: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index dbc2e49ec..711e988df 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -492,8 +492,7 @@ def test_plt_marker(card: ImageFile.ImageFile) -> None: out.seek(0) while True: marker = out.read(2) - if not marker: - pytest.fail("End of stream without PLT") + assert marker, "End of stream without PLT" jp2_boxid = _binary.i16be(marker) if jp2_boxid == 0xFF4F: diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 49d71aca7..18dd11182 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -36,11 +36,7 @@ class LibTiffTestCase: im.load() im.getdata() - try: - assert im._compression == "group4" - except AttributeError: - print("No _compression") - print(dir(im)) + assert im._compression == "group4" # can we write it back out, in a different form. out = str(tmp_path / "temp.png") diff --git a/Tests/test_image.py b/Tests/test_image.py index 092bc07f6..fe43cea40 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -189,8 +189,6 @@ class TestImage: if ext == ".jp2" and not features.check_codec("jpg_2000"): pytest.skip("jpg_2000 not available") temp_file = str(tmp_path / ("temp." + ext)) - if os.path.exists(temp_file): - os.remove(temp_file) im.save(Path(temp_file)) def test_fp_name(self, tmp_path: Path) -> None: From 4d14991604d34f73aa5d426340adf773c81cdc6a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jan 2025 14:58:58 +1100 Subject: [PATCH 546/652] Corrected argument types --- Tests/test_image.py | 4 ++-- Tests/test_image_resize.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 092bc07f6..a72694ec1 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -667,7 +667,7 @@ class TestImage: # Test illegal image mode with hopper() as im: with pytest.raises(ValueError): - im.remap_palette(None) + im.remap_palette([]) def test_remap_palette_transparency(self) -> None: im = Image.new("P", (1, 2), (0, 0, 0)) @@ -770,7 +770,7 @@ class TestImage: assert dict(exif) # Test that exif data is cleared after another load - exif.load(None) + exif.load(b"") assert not dict(exif) # Test loading just the EXIF header diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 57fcf9a34..1166371b8 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -309,7 +309,7 @@ class TestImageResize: # Test unknown resampling filter with hopper() as im: with pytest.raises(ValueError): - im.resize((10, 10), "unknown") + im.resize((10, 10), -1) @skip_unless_feature("libtiff") def test_transposed(self) -> None: From 8603d6512a2e6f534d6e0fb4a9307bc3edd4f0fa Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:22:29 +0200 Subject: [PATCH 547/652] Use python-numpy and python-olefile instead of python3-numpy and python3-olefile --- .github/workflows/test-mingw.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 6b1b36cb0..bb6d7dc37 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -66,8 +66,8 @@ jobs: mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-python3-numpy \ - mingw-w64-x86_64-python3-olefile \ + mingw-w64-x86_64-python-numpy \ + mingw-w64-x86_64-python-olefile \ mingw-w64-x86_64-python-pip \ mingw-w64-x86_64-python-pytest \ mingw-w64-x86_64-python-pytest-cov \ From 0d93c030a576e30d3991bf5547f2e0c3e47889a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Jan 2025 19:10:42 +1100 Subject: [PATCH 548/652] Test passes in Python 3.13 --- Tests/test_image_access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index bb30b462d..6d8b4d355 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -271,7 +271,7 @@ class TestImagePutPixelError: class TestEmbeddable: - @pytest.mark.xfail(reason="failing test") + @pytest.mark.xfail(not (sys.version_info >= (3, 13)), reason="failing test") @pytest.mark.skipif(not is_win32(), reason="requires Windows") def test_embeddable(self) -> None: import ctypes From 64bfdff6c8111225a1c7e4480376738123f2cb15 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Jan 2025 21:51:33 +1100 Subject: [PATCH 549/652] Only F mode starts with F --- src/PIL/SpiderImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 3a87d009a..b26e1a996 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -267,7 +267,7 @@ def makeSpiderHeader(im: Image.Image) -> list[bytes]: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode[0] != "F": + if im.mode != "F": im = im.convert("F") hdr = makeSpiderHeader(im) From 7166a09538bbadbeb9d02f1d9af0c8d70022a80b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Jan 2025 18:57:41 +1100 Subject: [PATCH 550/652] Skip test_embeddable if compiler cannot be initialized --- Tests/test_image_access.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 6d8b4d355..14a5e2e7b 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -278,6 +278,18 @@ class TestEmbeddable: from setuptools.command import build_ext + compiler = getattr(build_ext, "new_compiler")() + compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) + + libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( + "INCLUDEPY" + ).replace("include", "libs") + compiler.add_library_dir(libdir) + try: + compiler.initialize() + except Exception: + pytest.skip("Compiler could not be initialized") + with open("embed_pil.c", "w", encoding="utf-8") as fh: home = sys.prefix.replace("\\", "\\\\") fh.write( @@ -305,13 +317,6 @@ int main(int argc, char* argv[]) """ ) - compiler = getattr(build_ext, "new_compiler")() - compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) - - libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( - "INCLUDEPY" - ).replace("include", "libs") - compiler.add_library_dir(libdir) objects = compiler.compile(["embed_pil.c"]) compiler.link_executable(objects, "embed_pil") From 5ad98e7abb19710cfb0c6c70ad52b543b1c5769b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Jan 2025 07:54:43 +1100 Subject: [PATCH 551/652] Moved get_child_images() --- src/PIL/Image.py | 46 ------------------------------------------ src/PIL/ImageFile.py | 48 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index dff3d063b..0648161be 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1553,52 +1553,6 @@ class Image: self._exif._loaded = False self.getexif() - def get_child_images(self) -> list[ImageFile.ImageFile]: - child_images = [] - exif = self.getexif() - ifds = [] - if ExifTags.Base.SubIFDs in exif: - subifd_offsets = exif[ExifTags.Base.SubIFDs] - if subifd_offsets: - if not isinstance(subifd_offsets, tuple): - subifd_offsets = (subifd_offsets,) - for subifd_offset in subifd_offsets: - ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) - ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) - if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): - assert exif._info is not None - ifds.append((ifd1, exif._info.next)) - - offset = None - for ifd, ifd_offset in ifds: - current_offset = self.fp.tell() - if offset is None: - offset = current_offset - - fp = self.fp - if ifd is not None: - thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset) - if thumbnail_offset is not None: - thumbnail_offset += getattr(self, "_exif_offset", 0) - self.fp.seek(thumbnail_offset) - data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount)) - fp = io.BytesIO(data) - - with open(fp) as im: - from . import TiffImagePlugin - - if thumbnail_offset is None and isinstance( - im, TiffImagePlugin.TiffImageFile - ): - im._frame_pos = [ifd_offset] - im._seek(0) - im.load() - child_images.append(im) - - if offset is not None: - self.fp.seek(offset) - return child_images - def getim(self) -> CapsuleType: """ Returns a capsule that points to the internal image memory. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 5d0f87a9f..b2a44cb4d 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -36,7 +36,7 @@ import struct import sys from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast -from . import Image +from . import ExifTags, Image from ._deprecate import deprecate from ._util import is_path @@ -163,6 +163,52 @@ class ImageFile(Image.Image): def _open(self) -> None: pass + def get_child_images(self) -> list[ImageFile]: + child_images = [] + exif = self.getexif() + ifds = [] + if ExifTags.Base.SubIFDs in exif: + subifd_offsets = exif[ExifTags.Base.SubIFDs] + if subifd_offsets: + if not isinstance(subifd_offsets, tuple): + subifd_offsets = (subifd_offsets,) + for subifd_offset in subifd_offsets: + ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) + ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) + if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): + assert exif._info is not None + ifds.append((ifd1, exif._info.next)) + + offset = None + for ifd, ifd_offset in ifds: + current_offset = self.fp.tell() + if offset is None: + offset = current_offset + + fp = self.fp + if ifd is not None: + thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset) + if thumbnail_offset is not None: + thumbnail_offset += getattr(self, "_exif_offset", 0) + self.fp.seek(thumbnail_offset) + data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount)) + fp = io.BytesIO(data) + + with Image.open(fp) as im: + from . import TiffImagePlugin + + if thumbnail_offset is None and isinstance( + im, TiffImagePlugin.TiffImageFile + ): + im._frame_pos = [ifd_offset] + im._seek(0) + im.load() + child_images.append(im) + + if offset is not None: + self.fp.seek(offset) + return child_images + def get_format_mimetype(self) -> str | None: if self.custom_mimetype: return self.custom_mimetype From 34762ded7500a20f938f98250df6e650608cd57e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Jan 2025 07:57:28 +1100 Subject: [PATCH 552/652] Assert JpegIFByteCount is int --- src/PIL/ImageFile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index b2a44cb4d..f905b34b6 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -191,7 +191,10 @@ class ImageFile(Image.Image): if thumbnail_offset is not None: thumbnail_offset += getattr(self, "_exif_offset", 0) self.fp.seek(thumbnail_offset) - data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount)) + + length = ifd.get(ExifTags.Base.JpegIFByteCount) + assert isinstance(length, int) + data = self.fp.read(length) fp = io.BytesIO(data) with Image.open(fp) as im: From a922126ed7495466b2193c6cf582ade11f0f8fe5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Jan 2025 07:57:50 +1100 Subject: [PATCH 553/652] Assert fp is not None --- src/PIL/ImageFile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f905b34b6..3476e48ff 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -180,6 +180,7 @@ class ImageFile(Image.Image): ifds.append((ifd1, exif._info.next)) offset = None + assert self.fp is not None for ifd, ifd_offset in ifds: current_offset = self.fp.tell() if offset is None: From a4018d192cf8a305c3da622a53df7d144d11432c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Jan 2025 21:07:32 +1100 Subject: [PATCH 554/652] Added Sphinx configuration key --- .readthedocs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index def6282dd..3e03c76ea 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,8 @@ version: 2 +sphinx: + configuration: docs/conf.py + formats: [pdf] build: From 2ce2ff297c9d3577e3bcc8f723107f661590afd2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Jan 2025 20:37:26 +1100 Subject: [PATCH 555/652] Test Python 3.14 pre-release --- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index d905a3925..b76b00eaa 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] timeout-minutes: 30 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 83a696f5f..e3efe0b59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,7 @@ jobs: ] python-version: [ "pypy3.10", + "3.14", "3.13t", "3.13", "3.12", From 0f2c554c698266ec0ba464f21a616cd0eda9a7fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Jan 2025 21:03:20 +1100 Subject: [PATCH 556/652] Improved comment --- Tests/oss-fuzz/test_fuzzers.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 90eb8713a..50d1467fc 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -32,21 +32,18 @@ def test_fuzz_images(path: str) -> None: fuzzers.fuzz_image(f.read()) assert True except ( + # Known exceptions from Pillow OSError, SyntaxError, MemoryError, ValueError, NotImplementedError, OverflowError, - ): - # Known exceptions that are through from Pillow - assert True - except ( + # Known Image.* exceptions Image.DecompressionBombError, Image.DecompressionBombWarning, UnidentifiedImageError, ): - # Known Image.* exceptions assert True finally: fuzzers.disable_decompressionbomb_error() From cf438c53eed627e62baff752cb874aad3bcf63d5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Jan 2025 21:04:08 +1100 Subject: [PATCH 557/652] Removed UnidentifiedImageError, as it inherits from OSError --- Tests/oss-fuzz/test_fuzzers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 50d1467fc..e42ec90aa 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -7,7 +7,7 @@ import fuzzers import packaging import pytest -from PIL import Image, UnidentifiedImageError, features +from PIL import Image, features from Tests.helper import skip_unless_feature if sys.platform.startswith("win32"): @@ -42,7 +42,6 @@ def test_fuzz_images(path: str) -> None: # Known Image.* exceptions Image.DecompressionBombError, Image.DecompressionBombWarning, - UnidentifiedImageError, ): assert True finally: From e70c821436763e09813f0f53c76aaa3bb4fa76f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Jan 2025 20:57:49 +1100 Subject: [PATCH 558/652] Removed miniconda CPPFLAGS --- .ci/build.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/.ci/build.sh b/.ci/build.sh index e678f68ec..ae10cb671 100755 --- a/.ci/build.sh +++ b/.ci/build.sh @@ -3,8 +3,5 @@ set -e python3 -m coverage erase -if [ $(uname) == "Darwin" ]; then - export CPPFLAGS="-I/usr/local/miniconda/include"; -fi make clean make install-coverage From 536aee5bbde0fb432c1b22d2d0eed7bd35dbfca2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Jan 2025 22:12:53 +1100 Subject: [PATCH 559/652] Test Numpy on amd64 --- .github/workflows/wheels-test.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index f593c7228..a1edc14ef 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -11,6 +11,9 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") { $env:path += ";$pillow\winbuild\build\bin\" & "$venv\Scripts\activate.ps1" & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f +if ("$venv" -like "*\cibw-run-*-win_amd64\*") { + & python -m pip install numpy +} cd $pillow & python -VV if (!$?) { exit $LASTEXITCODE } From c67ed4678bf16be8630561ce0eb49eb0a2d3be40 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 16 Jan 2025 23:48:44 +1100 Subject: [PATCH 560/652] Moved strings inside debug statement Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/TiffImagePlugin.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index a246994ef..c871342fc 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2202,11 +2202,15 @@ class AppendingTiffWriter(io.BytesIO): if tag in self.Tags: cur_pos = self.f.tell() - tagname = TiffTags.lookup(tag).name - typname = TYPES.get(field_type, "unknown") - msg = f"fixIFD: {tagname} ({tag}) - type: {typname} ({field_type})" - msg += f"- type size: {field_size} - count: {count}" - logger.debug(msg) + logger.debug( + "fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d", + TiffTags.lookup(tag).name, + tag, + TYPES.get(field_type, "unknown"), + field_type, + field_size, + count, + ) if is_local: self._fixOffsets(count, field_size) From a04e76a84ff7122de52ad381e1d44dc556040c36 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Jan 2025 11:51:21 +1100 Subject: [PATCH 561/652] Use arm64 Linux runners --- .github/workflows/wheels.yml | 71 +++++++----------------------------- 1 file changed, 13 insertions(+), 58 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index fd89f7585..0402f1b54 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -42,62 +42,7 @@ env: FORCE_COLOR: 1 jobs: - build-1-QEMU-emulated-wheels: - if: github.event_name != 'schedule' - name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: - - pp310 - - cp3{9,10,11} - - cp3{12,13} - spec: - - manylinux2014 - - manylinux_2_28 - - musllinux - exclude: - - { python-version: pp310, spec: musllinux } - - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - submodules: true - - - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Install cibuildwheel - run: | - python3 -m pip install -r .ci/requirements-cibw.txt - - - name: Build wheels - run: | - python3 -m cibuildwheel --output-dir wheelhouse - env: - # Build only the currently selected Linux architecture (so we can - # parallelise for speed). - CIBW_ARCHS: "aarch64" - # Likewise, select only one Python version per job to speed this up. - CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" - CIBW_ENABLE: cpython-prerelease - # Extra options for manylinux. - CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} - - - uses: actions/upload-artifact@v4 - with: - name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} - path: ./wheelhouse/*.whl - - build-2-native-wheels: + build-native-wheels: if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' name: ${{ matrix.name }} runs-on: ${{ matrix.os }} @@ -132,6 +77,14 @@ jobs: cibw_arch: x86_64 build: "*manylinux*" manylinux: "manylinux_2_28" + - name: "manylinux2014 and musllinux aarch64" + os: ubuntu-24.04-arm + cibw_arch: aarch64 + - name: "manylinux_2_28 aarch64" + os: ubuntu-24.04-arm + cibw_arch: aarch64 + build: "*manylinux*" + manylinux: "manylinux_2_28" steps: - uses: actions/checkout@v4 with: @@ -153,6 +106,8 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} CIBW_ENABLE: cpython-prerelease cpython-freethreading + CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux }} + CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_SKIP: pp39-* @@ -275,7 +230,7 @@ jobs: scientific-python-nightly-wheels-publish: if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - needs: [build-2-native-wheels, windows] + needs: [build-native-wheels, windows] runs-on: ubuntu-latest name: Upload wheels to scientific-python-nightly-wheels steps: @@ -292,7 +247,7 @@ jobs: pypi-publish: if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] + needs: [build-native-wheels, windows, sdist] runs-on: ubuntu-latest name: Upload release to PyPI environment: From 176c5b3749fe4642186dceb4c4253e4cd3e60e03 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Jan 2025 11:51:42 +1100 Subject: [PATCH 562/652] Added pypy to CIBW_ENABLE --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 0402f1b54..db8e4d58b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -105,7 +105,7 @@ jobs: env: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} - CIBW_ENABLE: cpython-prerelease cpython-freethreading + CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} @@ -184,7 +184,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" - CIBW_ENABLE: cpython-prerelease cpython-freethreading + CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy CIBW_SKIP: pp39-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm From be8e55d28d3525b05769aee5f36b945bd6e01f77 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Jan 2025 18:34:23 +1100 Subject: [PATCH 563/652] Added deprecation warning --- Tests/test_image.py | 5 ++++ docs/deprecations.rst | 10 +++++++ docs/releasenotes/11.2.0.rst | 58 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + src/PIL/Image.py | 6 ++++ src/PIL/ImageFile.py | 3 +- src/PIL/_deprecate.py | 2 ++ 7 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 docs/releasenotes/11.2.0.rst diff --git a/Tests/test_image.py b/Tests/test_image.py index fe43cea40..108013463 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -989,6 +989,11 @@ class TestImage: else: assert im.getxmp() == {"xmpmeta": None} + def test_get_child_images(self) -> None: + im = Image.new("RGB", (1, 1)) + with pytest.warns(DeprecationWarning): + assert im.get_child_images() == [] + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) def test_zero_tobytes(self, size: tuple[int, int]) -> None: im = Image.new("RGB", size) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 80966ca36..634cee689 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -183,6 +183,16 @@ ExifTags.IFD.Makernote ``ExifTags.IFD.Makernote`` has been deprecated. Instead, use ``ExifTags.IFD.MakerNote``. +Image.Image.get_child_images() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.2.0 + +``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow +13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The +method uses an image's file pointer, and so child images could only be retrieved from +an :py:class:`PIL.ImageFile.ImageFile` instance. + Removed features ---------------- diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst new file mode 100644 index 000000000..025b64660 --- /dev/null +++ b/docs/releasenotes/11.2.0.rst @@ -0,0 +1,58 @@ +11.2.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +Image.Image.get_child_images() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.2.0 + +``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow +13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The +method uses an image's file pointer, and so child images could only be retrieved from +an :py:class:`PIL.ImageFile.ImageFile` instance. + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index bd8e5536f..be9f623d0 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 11.2.0 11.1.0 11.0.0 10.4.0 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0648161be..e512e6fc7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1553,6 +1553,12 @@ class Image: self._exif._loaded = False self.getexif() + def get_child_images(self) -> list[ImageFile.ImageFile]: + from . import ImageFile + + deprecate("Image.Image.get_child_images", 13) + return ImageFile.ImageFile.get_child_images(self) # type: ignore[arg-type] + def getim(self) -> CapsuleType: """ Returns a capsule that points to the internal image memory. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 3476e48ff..93fb47874 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -180,8 +180,8 @@ class ImageFile(Image.Image): ifds.append((ifd1, exif._info.next)) offset = None - assert self.fp is not None for ifd, ifd_offset in ifds: + assert self.fp is not None current_offset = self.fp.tell() if offset is None: offset = current_offset @@ -210,6 +210,7 @@ class ImageFile(Image.Image): child_images.append(im) if offset is not None: + assert self.fp is not None self.fp.seek(offset) return child_images diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 83952b397..9f9d8bbc9 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -47,6 +47,8 @@ def deprecate( raise RuntimeError(msg) elif when == 12: removed = "Pillow 12 (2025-10-15)" + elif when == 13: + removed = "Pillow 13 (2026-10-15)" else: msg = f"Unknown removal version: {when}. Update {__name__}?" raise ValueError(msg) From 6a0ac411e26b2b3436f3790e6830dfe18a914ffc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Jan 2025 18:57:12 +1100 Subject: [PATCH 564/652] Added mozjpeg documentation --- docs/reference/features.rst | 1 + docs/releasenotes/11.2.0.rst | 58 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 3 files changed, 60 insertions(+) create mode 100644 docs/releasenotes/11.2.0.rst diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 427c0f606..e5fdca240 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -54,6 +54,7 @@ Feature version numbers are available only where stated. Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. +* ``mozjpeg``: (compile time) Whether Pillow was compiled against the mozjpeg version of libjpeg. Compile-time version number is available. * ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst new file mode 100644 index 000000000..f9eff3c07 --- /dev/null +++ b/docs/releasenotes/11.2.0.rst @@ -0,0 +1,58 @@ +11.2.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Check for mozjpeg +^^^^^^^^^^^^^^^^^ + +You can check if Pillow has been built against the mozjpeg version of the +libjpeg library, and what version of mozjpeg is being used:: + + from PIL import features + features.check_feature("mozjpeg") # True or False + features.version_feature("mozjpeg") # "4.1.1" for example, or None + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index bd8e5536f..be9f623d0 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 11.2.0 11.1.0 11.0.0 10.4.0 From 30c4ad484c1e784ea316e53083381e823b26a2f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 18 Jan 2025 07:48:15 +1100 Subject: [PATCH 565/652] Updated capitalization Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/reference/features.rst | 2 +- docs/releasenotes/11.2.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/features.rst b/docs/reference/features.rst index e5fdca240..0e173fe87 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -54,7 +54,7 @@ Feature version numbers are available only where stated. Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. -* ``mozjpeg``: (compile time) Whether Pillow was compiled against the mozjpeg version of libjpeg. Compile-time version number is available. +* ``mozjpeg``: (compile time) Whether Pillow was compiled against the MozJPEG version of libjpeg. Compile-time version number is available. * ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index f9eff3c07..f1e15377e 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -42,7 +42,7 @@ API Additions Check for mozjpeg ^^^^^^^^^^^^^^^^^ -You can check if Pillow has been built against the mozjpeg version of the +You can check if Pillow has been built against the MozJPEG version of the libjpeg library, and what version of mozjpeg is being used:: from PIL import features From 284297755a8d982f327c08f391192a6a57937d2b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 Jan 2025 07:55:49 +1100 Subject: [PATCH 566/652] Updated capitalization --- docs/releasenotes/11.2.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index 725de5092..df28d05af 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -44,11 +44,11 @@ TODO API Additions ============= -Check for mozjpeg +Check for MozJPEG ^^^^^^^^^^^^^^^^^ You can check if Pillow has been built against the MozJPEG version of the -libjpeg library, and what version of mozjpeg is being used:: +libjpeg library, and what version of MozJPEG is being used:: from PIL import features features.check_feature("mozjpeg") # True or False From ba606622b4441b87ec74d420a25f0c60882004eb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 Jan 2025 13:53:39 +1100 Subject: [PATCH 567/652] Updated Ubuntu arm to 24.04 with arm64 runner --- .github/workflows/test-docker.yml | 9 +++++---- docs/installation/platform-support.rst | 6 ++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index bebb9cda2..0d9033413 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -29,13 +29,13 @@ concurrency: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + os: ["ubuntu-latest"] docker: [ # Run slower jobs first to give them a headstart and reduce waiting time - ubuntu-22.04-jammy-arm64v8, ubuntu-24.04-noble-ppc64le, ubuntu-24.04-noble-s390x, # Then run the remainder @@ -55,12 +55,13 @@ jobs: ] dockerTag: [main] include: - - docker: "ubuntu-22.04-jammy-arm64v8" - qemu-arch: "aarch64" - docker: "ubuntu-24.04-noble-ppc64le" qemu-arch: "ppc64le" - docker: "ubuntu-24.04-noble-s390x" qemu-arch: "s390x" + - docker: "ubuntu-24.04-noble-arm64v8" + os: "ubuntu-24.04-arm" + dockerTag: main name: ${{ matrix.docker }} diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 814d6a9cf..9eafad3c4 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -44,11 +44,9 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 | | | 3.12, 3.13, PyPy3 | | -| +----------------------------+---------------------+ -| | 3.10 | arm64v8 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, | -| | | s390x | +| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, | +| | | ppc64le, s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2019 | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ From 4ff18e03b8a703bbc15a9d19ce253c19f1820b5c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 Jan 2025 20:57:04 +1100 Subject: [PATCH 568/652] Moved file pointer handling into ImageFile close --- src/PIL/Image.py | 9 --------- src/PIL/ImageFile.py | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e512e6fc7..a0c9ff8eb 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -619,8 +619,6 @@ class Image: def close(self) -> None: """ - Closes the file pointer, if possible. - This operation will destroy the image core and release its memory. The image data will be unusable afterward. @@ -629,13 +627,6 @@ class Image: :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for more information. """ - if hasattr(self, "fp"): - try: - self._close_fp() - self.fp = None - except Exception as msg: - logger.debug("Error closing: %s", msg) - if getattr(self, "map", None): self.map: mmap.mmap | None = None diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 93fb47874..d716e3b5c 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -31,6 +31,7 @@ from __future__ import annotations import abc import io import itertools +import logging import os import struct import sys @@ -43,6 +44,8 @@ from ._util import is_path if TYPE_CHECKING: from ._typing import StrOrBytesPath +logger = logging.getLogger(__name__) + MAXBLOCK = 65536 SAFEBLOCK = 1024 * 1024 @@ -163,6 +166,26 @@ class ImageFile(Image.Image): def _open(self) -> None: pass + def close(self) -> None: + """ + Closes the file pointer, if possible. + + This operation will destroy the image core and release its memory. + The image data will be unusable afterward. + + This function is required to close images that have multiple frames or + have not had their file read and closed by the + :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for + more information. + """ + try: + self._close_fp() + self.fp = None + except Exception as msg: + logger.debug("Error closing: %s", msg) + + super().close() + def get_child_images(self) -> list[ImageFile]: child_images = [] exif = self.getexif() From c78d23d5471dc24b20f0eb387442e63ab0c63f9b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 Jan 2025 21:22:44 +1100 Subject: [PATCH 569/652] Moved _close_fp into ImageFile --- src/PIL/Image.py | 12 +++--------- src/PIL/ImageFile.py | 10 +++++++++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a0c9ff8eb..99b1b9ab3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -603,16 +603,10 @@ class Image: def __enter__(self): return self - def _close_fp(self): - if getattr(self, "_fp", False): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() - def __exit__(self, *args): - if hasattr(self, "fp"): + from . import ImageFile + + if isinstance(self, ImageFile.ImageFile): if getattr(self, "_exclusive_fp", False): self._close_fp() self.fp = None diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index d716e3b5c..c3901d488 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -39,7 +39,7 @@ from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast from . import ExifTags, Image from ._deprecate import deprecate -from ._util import is_path +from ._util import DeferredError, is_path if TYPE_CHECKING: from ._typing import StrOrBytesPath @@ -166,6 +166,14 @@ class ImageFile(Image.Image): def _open(self) -> None: pass + def _close_fp(self): + if getattr(self, "_fp", False): + if self._fp != self.fp: + self._fp.close() + self._fp = DeferredError(ValueError("Operation on closed image")) + if self.fp: + self.fp.close() + def close(self) -> None: """ Closes the file pointer, if possible. From 8d9279dd7329cc8dd330f483df2867fd51ca8dcf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 Jan 2025 21:58:04 +1100 Subject: [PATCH 570/652] Only use outside border of stroke in text() --- Tests/test_imagedraw.py | 22 ++++++++++++++++++++++ Tests/test_imagefont.py | 14 ++++++++++++++ src/PIL/ImageDraw.py | 1 + src/PIL/ImageFont.py | 1 + src/PIL/_imagingft.pyi | 1 + src/_imagingft.c | 7 +++++-- 6 files changed, 44 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 5fc1c2766..28d7ed725 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1396,6 +1396,28 @@ def test_stroke_descender() -> None: assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) +@skip_unless_feature("freetype2") +def test_stroke_inside_gap() -> None: + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.text((12, 12), "i", "#f00", font, stroke_width=20) + + # Assert + for y in range(im.height): + glyph = "" + for x in range(im.width): + if im.getpixel((x, y)) == (0, 0, 0): + if glyph == "started": + glyph = "ended" + else: + assert glyph != "ended", "Gap inside stroked glyph" + glyph = "started" + + @skip_unless_feature("freetype2") def test_split_word() -> None: # Arrange diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 6a0a940b9..f110cc1d0 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -461,6 +461,20 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: assert mask.size == (108, 13) +def test_stroke_mask() -> None: + # Arrange + text = "i" + + # Act + font = ImageFont.truetype(FONT_PATH, 128) + mask = font.getmask(text, stroke_width=2) + + # Assert + assert mask.getpixel((34, 5)) == 255 + assert mask.getpixel((38, 5)) == 0 + assert mask.getpixel((42, 5)) == 255 + + def test_load_when_image_not_found() -> None: with tempfile.NamedTemporaryFile(delete=False) as tmp: pass diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d8e4c0c60..d0f6c5b7d 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -643,6 +643,7 @@ class ImageDraw: features=features, language=language, stroke_width=stroke_width, + stroke_filled=True, anchor=anchor, ink=ink, start=start, diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index d8c265560..a4986aa8c 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -644,6 +644,7 @@ class FreeTypeFont: features, language, stroke_width, + kwargs.get("stroke_filled", False), anchor, ink, start[0], diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 9cc9822f5..813294747 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -28,6 +28,7 @@ class Font: features: list[str] | None, lang: str | None, stroke_width: float, + stroke_filled: bool, anchor: str | None, foreground_ink_long: int, x_start: float, diff --git a/src/_imagingft.c b/src/_imagingft.c index 3a65007a5..4281e0b5e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -834,6 +834,7 @@ font_render(FontObject *self, PyObject *args) { int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */ float stroke_width = 0; + int stroke_filled = 0; PY_LONG_LONG foreground_ink_long = 0; unsigned int foreground_ink; const char *mode = NULL; @@ -853,7 +854,7 @@ font_render(FontObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "OO|zzOzfzLffO:render", + "OO|zzOzfpzLffO:render", &string, &fill, &mode, @@ -861,6 +862,7 @@ font_render(FontObject *self, PyObject *args) { &features, &lang, &stroke_width, + &stroke_filled, &anchor, &foreground_ink_long, &x_start, @@ -1005,7 +1007,8 @@ font_render(FontObject *self, PyObject *args) { if (stroker != NULL) { error = FT_Get_Glyph(glyph_slot, &glyph); if (!error) { - error = FT_Glyph_Stroke(&glyph, stroker, 1); + error = stroke_filled ? FT_Glyph_StrokeBorder(&glyph, stroker, 0, 1) + : FT_Glyph_Stroke(&glyph, stroker, 1); } if (!error) { FT_Vector origin = {0, 0}; From 0318304f9ae63fe81e513bdb32c1497539c66176 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 Jan 2025 17:27:25 +1100 Subject: [PATCH 571/652] Do not draw normal text onto stroke text if they are the same color --- src/PIL/ImageDraw.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d0f6c5b7d..81f8fbce0 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -693,7 +693,8 @@ class ImageDraw: draw_text(stroke_ink, stroke_width) # Draw normal text - draw_text(ink, 0) + if ink != stroke_ink: + draw_text(ink) else: # Only draw normal text draw_text(ink) From 427244877bb243c29fd07543f577ee6036daff4f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 Jan 2025 15:09:12 +1100 Subject: [PATCH 572/652] Support saving cICP chunk --- src/PIL/PngImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 4b97992a3..f56555160 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1382,7 +1382,7 @@ def _save( b"\0", # 12: interlace flag ) - chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"] + chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"] icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) if icc: From 8a90975c14e8176791f431cabbcf14029c1b36a2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Jan 2025 23:14:34 +1100 Subject: [PATCH 573/652] Seek relative to current position --- src/PIL/BufrStubImagePlugin.py | 5 ++--- src/PIL/GribStubImagePlugin.py | 5 ++--- src/PIL/Hdf5StubImagePlugin.py | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 0ee2f653b..50c41c482 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -10,6 +10,7 @@ # from __future__ import annotations +import os from typing import IO from . import Image, ImageFile @@ -40,13 +41,11 @@ class BufrStubImageFile(ImageFile.StubImageFile): format_description = "BUFR" def _open(self) -> None: - offset = self.fp.tell() - if not _accept(self.fp.read(4)): msg = "Not a BUFR file" raise SyntaxError(msg) - self.fp.seek(offset) + self.fp.seek(-4, os.SEEK_CUR) # make something up self._mode = "F" diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index e9aa084b2..eb1b1483b 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -10,6 +10,7 @@ # from __future__ import annotations +import os from typing import IO from . import Image, ImageFile @@ -40,13 +41,11 @@ class GribStubImageFile(ImageFile.StubImageFile): format_description = "GRIB" def _open(self) -> None: - offset = self.fp.tell() - if not _accept(self.fp.read(8)): msg = "Not a GRIB file" raise SyntaxError(msg) - self.fp.seek(offset) + self.fp.seek(-8, os.SEEK_CUR) # make something up self._mode = "F" diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index cc9e73deb..ddc218508 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -10,6 +10,7 @@ # from __future__ import annotations +import os from typing import IO from . import Image, ImageFile @@ -40,13 +41,11 @@ class HDF5StubImageFile(ImageFile.StubImageFile): format_description = "HDF5" def _open(self) -> None: - offset = self.fp.tell() - if not _accept(self.fp.read(8)): msg = "Not an HDF file" raise SyntaxError(msg) - self.fp.seek(offset) + self.fp.seek(-8, os.SEEK_CUR) # make something up self._mode = "F" From e31441fc41ff54217317b61db395dfc9b5a0dc79 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Jan 2025 19:51:07 +1100 Subject: [PATCH 574/652] Use Ubuntu 22.04 for 24.04 ppc64le and s390x --- .github/workflows/test-docker.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 0d9033413..da5e191da 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -35,10 +35,6 @@ jobs: matrix: os: ["ubuntu-latest"] docker: [ - # Run slower jobs first to give them a headstart and reduce waiting time - ubuntu-24.04-noble-ppc64le, - ubuntu-24.04-noble-s390x, - # Then run the remainder alpine, amazon-2-amd64, amazon-2023-amd64, @@ -56,9 +52,13 @@ jobs: dockerTag: [main] include: - docker: "ubuntu-24.04-noble-ppc64le" + os: "ubuntu-22.04" qemu-arch: "ppc64le" + dockerTag: main - docker: "ubuntu-24.04-noble-s390x" + os: "ubuntu-22.04" qemu-arch: "s390x" + dockerTag: main - docker: "ubuntu-24.04-noble-arm64v8" os: "ubuntu-24.04-arm" dockerTag: main From 9d4232101fb84da0d7dbf2622b140ba125f65f76 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Jan 2025 07:05:26 +1100 Subject: [PATCH 575/652] Updated libimagequant to 4.3.4 --- depends/install_imagequant.sh | 2 +- docs/installation/building-from-source.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 8d62d5ac7..88756f8f9 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -2,7 +2,7 @@ # install libimagequant archive_name=libimagequant -archive_version=4.3.3 +archive_version=4.3.4 archive=$archive_name-$archive_version diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 03359de31..46a4c1245 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -64,7 +64,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.3.3** + * Pillow has been tested with libimagequant **2.6-4.3.4** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From f52dbe749b00e97b1c81f0dbc3ef398468d65369 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Jan 2025 14:08:29 +1100 Subject: [PATCH 576/652] Updated libpng to 1.6.46 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 410255b7e..e01ad064a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -39,7 +39,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=10.1.0 -LIBPNG_VERSION=1.6.45 +LIBPNG_VERSION=1.6.46 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b9695d1d8..8818c7402 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ V = { "HARFBUZZ": "10.1.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.16", - "LIBPNG": "1.6.45", + "LIBPNG": "1.6.46", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.6.0", From 16a8e2bde4b4f9616eef58a447f878e664c2486a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Jan 2025 08:48:12 +1100 Subject: [PATCH 577/652] Updated xz to 5.6.4 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 410255b7e..4ab0f1b30 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -42,7 +42,7 @@ HARFBUZZ_VERSION=10.1.0 LIBPNG_VERSION=1.6.45 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 -XZ_VERSION=5.6.3 +XZ_VERSION=5.6.4 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 ZLIB_NG_VERSION=2.2.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b9695d1d8..1c20fad44 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -120,7 +120,7 @@ V = { "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.6.0", - "XZ": "5.6.3", + "XZ": "5.6.4", "ZLIBNG": "2.2.3", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From 569b785371aa717a004adb0166feb565bbb01b7b Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 25 Jan 2025 18:04:52 +1100 Subject: [PATCH 578/652] Updated harfbuzz to 10.2.0 (#8688) Co-authored-by: Andrew Murray --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index b1b5bcf94..dffb36085 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -38,7 +38,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 -HARFBUZZ_VERSION=10.1.0 +HARFBUZZ_VERSION=10.2.0 LIBPNG_VERSION=1.6.46 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d18facab4..54b5d983f 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "10.1.0", + "HARFBUZZ": "10.2.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBPNG": "1.6.46", From e7ae4aaad04483be775b4dda9bb8803ba63e5669 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Wed, 18 Sep 2024 11:42:44 +0200 Subject: [PATCH 579/652] Use Py_RETURN_NONE macro when possible --- src/_imaging.c | 93 ++++++++++++++++------------------------------ src/_imagingcms.c | 90 +++++++++++++++----------------------------- src/_imagingft.c | 6 +-- src/_imagingmath.c | 6 +-- src/_imagingtk.c | 3 +- src/decode.c | 6 +-- src/display.c | 18 +++------ src/encode.c | 6 +-- src/outline.c | 15 +++----- src/path.c | 6 +-- 10 files changed, 83 insertions(+), 166 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 00772d012..2fd2deffb 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -473,8 +473,7 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) { } /* unknown type */ - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char * @@ -965,8 +964,7 @@ _convert2(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1214,8 +1212,7 @@ _getpixel(ImagingObject *self, PyObject *args) { } if (self->access == NULL) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return getpixel(self->image, self->access, x, y); @@ -1417,8 +1414,7 @@ _paste(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1691,8 +1687,7 @@ _putdata(ImagingObject *self, PyObject *args) { Py_XDECREF(seq); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1752,8 +1747,7 @@ _putpalette(ImagingObject *self, PyObject *args) { self->image->palette->size = palettesize * 8 / bits; unpack(self->image->palette->palette, palette, self->image->palette->size); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1777,8 +1771,7 @@ _putpalettealpha(ImagingObject *self, PyObject *args) { strcpy(self->image->palette->mode, "RGBA"); self->image->palette->palette[index * 4 + 3] = (UINT8)alpha; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1805,8 +1798,7 @@ _putpalettealphas(ImagingObject *self, PyObject *args) { self->image->palette->palette[i * 4 + 3] = (UINT8)values[i]; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1842,8 +1834,7 @@ _putpixel(ImagingObject *self, PyObject *args) { self->access->put_pixel(im, x, y, ink); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2010,8 +2001,7 @@ im_setmode(ImagingObject *self, PyObject *args) { } self->access = ImagingAccessNew(im); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2074,8 +2064,7 @@ _transform(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2202,8 +2191,7 @@ _getbbox(ImagingObject *self, PyObject *args) { } if (!ImagingGetBBox(self->image, bbox, alpha_only)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return Py_BuildValue("iiii", bbox[0], bbox[1], bbox[2], bbox[3]); @@ -2283,8 +2271,7 @@ _getextrema(ImagingObject *self) { } } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2347,8 +2334,7 @@ _fillband(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2363,8 +2349,7 @@ _putband(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2950,8 +2935,7 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2988,8 +2972,7 @@ _draw_bitmap(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3045,8 +3028,7 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3100,8 +3082,7 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3164,8 +3145,7 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { free(xy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3196,8 +3176,7 @@ _draw_points(ImagingDrawObject *self, PyObject *args) { free(xy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* from outline.c */ @@ -3225,8 +3204,7 @@ _draw_outline(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3282,8 +3260,7 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3334,8 +3311,7 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { free(ixy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3389,8 +3365,7 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static struct PyMethodDef _draw_methods[] = { @@ -3595,8 +3570,7 @@ _save_ppm(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* -------------------------------------------------------------------- */ @@ -3984,8 +3958,7 @@ _reset_stats(PyObject *self, PyObject *args) { arena->stats_freed_blocks = 0; MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4045,8 +4018,7 @@ _set_alignment(PyObject *self, PyObject *args) { ImagingDefaultArena.alignment = alignment; MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4070,8 +4042,7 @@ _set_block_size(PyObject *self, PyObject *args) { ImagingDefaultArena.block_size = block_size; MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4099,8 +4070,7 @@ _set_blocks_max(PyObject *self, PyObject *args) { return ImagingError_MemoryError(); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4115,8 +4085,7 @@ _clear_cache(PyObject *self, PyObject *args) { ImagingMemoryClearCache(&ImagingDefaultArena, i); MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* -------------------------------------------------------------------- */ diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 1805ebde1..14cf2acd2 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -654,8 +654,7 @@ cms_get_display_profile_win32(PyObject *self, PyObject *args) { return PyUnicode_FromStringAndSize(filename, filename_size - 1); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } #endif @@ -672,20 +671,17 @@ _profile_read_mlu(CmsProfileObject *self, cmsTagSignature info) { wchar_t *buf; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } mlu = cmsReadTag(self->profile, info); if (!mlu) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } len = cmsMLUgetWide(mlu, lc, cc, NULL, 0); if (len == 0) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } buf = malloc(len); @@ -723,14 +719,12 @@ _profile_read_signature(CmsProfileObject *self, cmsTagSignature info) { unsigned int *sig; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } sig = (unsigned int *)cmsReadTag(self->profile, info); if (!sig) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _profile_read_int_as_string(*sig); @@ -780,14 +774,12 @@ _profile_read_ciexyz(CmsProfileObject *self, cmsTagSignature info, int multi) { cmsCIEXYZ *XYZ; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); if (!XYZ) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } if (multi) { return _xyz3_py(XYZ); @@ -801,14 +793,12 @@ _profile_read_ciexyy_triple(CmsProfileObject *self, cmsTagSignature info) { cmsCIExyYTRIPLE *triple; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } triple = (cmsCIExyYTRIPLE *)cmsReadTag(self->profile, info); if (!triple) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* Note: lcms does all the heavy lifting and error checking (nr of @@ -835,21 +825,18 @@ _profile_read_named_color_list(CmsProfileObject *self, cmsTagSignature info) { PyObject *result; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } ncl = (cmsNAMEDCOLORLIST *)cmsReadTag(self->profile, info); if (ncl == NULL) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } n = cmsNamedColorCount(ncl); result = PyList_New(n); if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } for (i = 0; i < n; i++) { @@ -858,8 +845,7 @@ _profile_read_named_color_list(CmsProfileObject *self, cmsTagSignature info) { str = PyUnicode_FromString(name); if (str == NULL) { Py_DECREF(result); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } PyList_SET_ITEM(result, i, str); } @@ -926,8 +912,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) { result = PyDict_New(); if (result == NULL) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } n = cmsGetSupportedIntents(INTENTS, intent_ids, intent_descs); @@ -957,8 +942,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) { Py_XDECREF(id); Py_XDECREF(entry); Py_XDECREF(result); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } PyDict_SetItem(result, id, entry); Py_DECREF(id); @@ -1042,8 +1026,7 @@ cms_profile_getattr_creation_date(CmsProfileObject *self, void *closure) { result = cmsGetHeaderCreationDateTime(self->profile, &ct); if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return PyDateTime_FromDateAndTime( @@ -1141,8 +1124,7 @@ cms_profile_getattr_saturation_rendering_intent_gamut( static PyObject * cms_profile_getattr_red_colorant(CmsProfileObject *self, void *closure) { if (!cmsIsMatrixShaper(self->profile)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _profile_read_ciexyz(self, cmsSigRedColorantTag, 0); } @@ -1150,8 +1132,7 @@ cms_profile_getattr_red_colorant(CmsProfileObject *self, void *closure) { static PyObject * cms_profile_getattr_green_colorant(CmsProfileObject *self, void *closure) { if (!cmsIsMatrixShaper(self->profile)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _profile_read_ciexyz(self, cmsSigGreenColorantTag, 0); } @@ -1159,8 +1140,7 @@ cms_profile_getattr_green_colorant(CmsProfileObject *self, void *closure) { static PyObject * cms_profile_getattr_blue_colorant(CmsProfileObject *self, void *closure) { if (!cmsIsMatrixShaper(self->profile)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _profile_read_ciexyz(self, cmsSigBlueColorantTag, 0); } @@ -1176,21 +1156,18 @@ cms_profile_getattr_media_white_point_temperature( cmsBool result; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); if (XYZ == NULL || XYZ->X == 0) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } cmsXYZ2xyY(&xyY, XYZ); result = cmsTempFromWhitePoint(&tempK, &xyY); if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return PyFloat_FromDouble(tempK); } @@ -1229,8 +1206,7 @@ cms_profile_getattr_red_primary(CmsProfileObject *self, void *closure) { result = _calculate_rgb_primaries(self, &primaries); } if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _xyz_py(&primaries.Red); @@ -1245,8 +1221,7 @@ cms_profile_getattr_green_primary(CmsProfileObject *self, void *closure) { result = _calculate_rgb_primaries(self, &primaries); } if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _xyz_py(&primaries.Green); @@ -1261,8 +1236,7 @@ cms_profile_getattr_blue_primary(CmsProfileObject *self, void *closure) { result = _calculate_rgb_primaries(self, &primaries); } if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _xyz_py(&primaries.Blue); @@ -1321,14 +1295,12 @@ cms_profile_getattr_icc_measurement_condition(CmsProfileObject *self, void *clos const char *geo; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } mc = (cmsICCMeasurementConditions *)cmsReadTag(self->profile, info); if (!mc) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } if (mc->Geometry == 1) { @@ -1362,14 +1334,12 @@ cms_profile_getattr_icc_viewing_condition(CmsProfileObject *self, void *closure) cmsTagSignature info = cmsSigViewingConditionsTag; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } vc = (cmsICCViewingConditions *)cmsReadTag(self->profile, info); if (!vc) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return Py_BuildValue( diff --git a/src/_imagingft.c b/src/_imagingft.c index 3a65007a5..5113cca77 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1371,8 +1371,7 @@ font_setvarname(FontObject *self, PyObject *args) { return geterror(error); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1426,8 +1425,7 @@ font_setvaraxes(FontObject *self, PyObject *args) { return geterror(error); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } #endif diff --git a/src/_imagingmath.c b/src/_imagingmath.c index dbe636707..75b3716b5 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -192,8 +192,7 @@ _unop(PyObject *self, PyObject *args) { unop(out, im1); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -226,8 +225,7 @@ _binop(PyObject *self, PyObject *args) { binop(out, im1, im2); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyMethodDef _functions[] = { diff --git a/src/_imagingtk.c b/src/_imagingtk.c index c70d044bb..c44482651 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -37,8 +37,7 @@ _tkinit(PyObject *self, PyObject *args) { /* This will bomb if interp is invalid... */ TkImaging_Init(interp); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyMethodDef functions[] = { diff --git a/src/decode.c b/src/decode.c index 51d0aced2..1f2c22491 100644 --- a/src/decode.c +++ b/src/decode.c @@ -213,8 +213,7 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { Py_XDECREF(decoder->lock); decoder->lock = op; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -231,8 +230,7 @@ _setfd(ImagingDecoderObject *decoder, PyObject *args) { Py_XINCREF(fd); state->fd = fd; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * diff --git a/src/display.c b/src/display.c index eed75975d..36ab3b237 100644 --- a/src/display.c +++ b/src/display.c @@ -85,8 +85,7 @@ _expose(ImagingDisplayObject *display, PyObject *args) { ImagingExposeDIB(display->dib, hdc); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -112,8 +111,7 @@ _draw(ImagingDisplayObject *display, PyObject *args) { ImagingDrawDIB(display->dib, hdc, dst, src); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } extern Imaging @@ -143,8 +141,7 @@ _paste(ImagingDisplayObject *display, PyObject *args) { ImagingPasteDIB(display->dib, im, xy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -190,8 +187,7 @@ _releasedc(ImagingDisplayObject *display, PyObject *args) { ReleaseDC(window, dc); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -211,8 +207,7 @@ _frombytes(ImagingDisplayObject *display, PyObject *args) { memcpy(display->dib->bits, buffer.buf, buffer.len); PyBuffer_Release(&buffer); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -709,8 +704,7 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { } Py_END_ALLOW_THREADS; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* -------------------------------------------------------------------- */ diff --git a/src/encode.c b/src/encode.c index d369a1b45..0bf5e63c5 100644 --- a/src/encode.c +++ b/src/encode.c @@ -278,8 +278,7 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { Py_XDECREF(encoder->lock); encoder->lock = op; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -296,8 +295,7 @@ _setfd(ImagingEncoderObject *encoder, PyObject *args) { Py_XINCREF(fd); state->fd = fd; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * diff --git a/src/outline.c b/src/outline.c index 27cc255cf..4aa6bd59e 100644 --- a/src/outline.c +++ b/src/outline.c @@ -89,8 +89,7 @@ _outline_move(OutlineObject *self, PyObject *args) { ImagingOutlineMove(self->outline, x0, y0); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -102,8 +101,7 @@ _outline_line(OutlineObject *self, PyObject *args) { ImagingOutlineLine(self->outline, x1, y1); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -115,8 +113,7 @@ _outline_curve(OutlineObject *self, PyObject *args) { ImagingOutlineCurve(self->outline, x1, y1, x2, y2, x3, y3); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -127,8 +124,7 @@ _outline_close(OutlineObject *self, PyObject *args) { ImagingOutlineClose(self->outline); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -140,8 +136,7 @@ _outline_transform(OutlineObject *self, PyObject *args) { ImagingOutlineTransform(self->outline, a); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static struct PyMethodDef _outline_methods[] = { diff --git a/src/path.c b/src/path.c index 067f42f62..b508df2ac 100644 --- a/src/path.c +++ b/src/path.c @@ -415,8 +415,7 @@ path_map(PyPathObject *self, PyObject *args) { } self->mapping = 0; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static int @@ -528,8 +527,7 @@ path_transform(PyPathObject *self, PyObject *args) { } } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static struct PyMethodDef methods[] = { From e19a1496c21ee5cca0fcbfcd0f1d97b9d8aa6bcc Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:17:51 +1100 Subject: [PATCH 580/652] Use monkeypatch (#8707) Co-authored-by: Andrew Murray --- Tests/check_png_dos.py | 21 ++++--- Tests/test_decompression_bomb.py | 19 +++---- Tests/test_file_fli.py | 27 ++++----- Tests/test_file_gif.py | 96 ++++++++++++++++---------------- Tests/test_file_ico.py | 25 ++++----- Tests/test_file_jpeg.py | 12 ++-- Tests/test_file_jpeg2k.py | 13 ++--- Tests/test_file_libtiff.py | 23 ++++---- Tests/test_file_png.py | 64 +++++++++------------ Tests/test_file_tiff.py | 5 +- Tests/test_file_webp.py | 7 +-- Tests/test_imagefile.py | 20 +++---- Tests/test_map.py | 14 ++--- 13 files changed, 153 insertions(+), 193 deletions(-) diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 63d6657bc..1bfb94ab7 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -3,26 +3,25 @@ from __future__ import annotations import zlib from io import BytesIO +import pytest + from PIL import Image, ImageFile, PngImagePlugin TEST_FILE = "Tests/images/png_decompression_dos.png" -def test_ignore_dos_text() -> None: - ImageFile.LOAD_TRUNCATED_IMAGES = True +def test_ignore_dos_text(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) - try: - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False - assert isinstance(im, PngImagePlugin.PngImageFile) - for s in im.text.values(): - assert len(s) < 1024 * 1024, "Text chunk larger than 1M" + assert isinstance(im, PngImagePlugin.PngImageFile) + for s in im.text.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" - for s in im.info.values(): - assert len(s) < 1024 * 1024, "Text chunk larger than 1M" + for s in im.info.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" def test_dos_text() -> None: diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index c140156f9..98d833736 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -12,19 +12,16 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS class TestDecompressionBomb: - def teardown_method(self) -> None: - Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def test_no_warning_small_file(self) -> None: # Implicit assert: no warning. # A warning would cause a failure. with Image.open(TEST_FILE): pass - def test_no_warning_no_limit(self) -> None: + def test_no_warning_no_limit(self, monkeypatch: pytest.MonkeyPatch) -> None: # Arrange # Turn limit off - Image.MAX_IMAGE_PIXELS = None + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None) assert Image.MAX_IMAGE_PIXELS is None # Act / Assert @@ -33,18 +30,18 @@ class TestDecompressionBomb: with Image.open(TEST_FILE): pass - def test_warning(self) -> None: + def test_warning(self, monkeypatch: pytest.MonkeyPatch) -> None: # Set limit to trigger warning on the test file - Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 128 * 128 - 1) assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 with pytest.warns(Image.DecompressionBombWarning): with Image.open(TEST_FILE): pass - def test_exception(self) -> None: + def test_exception(self, monkeypatch: pytest.MonkeyPatch) -> None: # Set limit to trigger exception on the test file - Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 64 * 128 - 1) assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 with pytest.raises(Image.DecompressionBombError): @@ -66,9 +63,9 @@ class TestDecompressionBomb: with pytest.raises(Image.DecompressionBombError): im.seek(1) - def test_exception_gif_zero_width(self) -> None: + def test_exception_gif_zero_width(self, monkeypatch: pytest.MonkeyPatch) -> None: # Set limit to trigger exception on the test file - Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 4 * 64 * 128) assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 with pytest.raises(Image.DecompressionBombError): diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 0a7740cc8..876561a88 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -35,22 +35,19 @@ def test_sanity() -> None: assert im.is_animated -def test_prefix_chunk() -> None: - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - with Image.open(animated_test_file_with_prefix_chunk) as im: - assert im.mode == "P" - assert im.size == (320, 200) - assert im.format == "FLI" - assert im.info["duration"] == 171 - assert im.is_animated +def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + with Image.open(animated_test_file_with_prefix_chunk) as im: + assert im.mode == "P" + assert im.size == (320, 200) + assert im.format == "FLI" + assert im.info["duration"] == 171 + assert im.is_animated - palette = im.getpalette() - assert palette[3:6] == [255, 255, 255] - assert palette[381:384] == [204, 204, 12] - assert palette[765:] == [252, 0, 0] - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + palette = im.getpalette() + assert palette[3:6] == [255, 255, 255] + assert palette[381:384] == [204, 204, 12] + assert palette[765:] == [252, 0, 0] @pytest.mark.skipif(is_pypy(), reason="Requires CPython") diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 5d46b157d..61a9475c7 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -109,7 +109,7 @@ def test_palette_not_needed_for_second_frame() -> None: assert_image_similar(im, hopper("L").convert("RGB"), 8) -def test_strategy() -> None: +def test_strategy(monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/iss634.gif") as im: expected_rgb_always = im.convert("RGB") @@ -119,35 +119,36 @@ def test_strategy() -> None: im.seek(1) expected_different = im.convert("RGB") - try: - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS - with Image.open("Tests/images/iss634.gif") as im: - assert im.mode == "RGB" - assert_image_equal(im, expected_rgb_always) + monkeypatch.setattr( + GifImagePlugin, "LOADING_STRATEGY", GifImagePlugin.LoadingStrategy.RGB_ALWAYS + ) + with Image.open("Tests/images/iss634.gif") as im: + assert im.mode == "RGB" + assert_image_equal(im, expected_rgb_always) - with Image.open("Tests/images/chi.gif") as im: - assert im.mode == "RGBA" - assert_image_equal(im, expected_rgb_always_rgba) + with Image.open("Tests/images/chi.gif") as im: + assert im.mode == "RGBA" + assert_image_equal(im, expected_rgb_always_rgba) - GifImagePlugin.LOADING_STRATEGY = ( - GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY - ) - # Stay in P mode with only a global palette - with Image.open("Tests/images/chi.gif") as im: - assert im.mode == "P" + monkeypatch.setattr( + GifImagePlugin, + "LOADING_STRATEGY", + GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY, + ) + # Stay in P mode with only a global palette + with Image.open("Tests/images/chi.gif") as im: + assert im.mode == "P" - im.seek(1) - assert im.mode == "P" - assert_image_equal(im.convert("RGB"), expected_different) + im.seek(1) + assert im.mode == "P" + assert_image_equal(im.convert("RGB"), expected_different) - # Change to RGB mode when a frame has an individual palette - with Image.open("Tests/images/iss634.gif") as im: - assert im.mode == "P" + # Change to RGB mode when a frame has an individual palette + with Image.open("Tests/images/iss634.gif") as im: + assert im.mode == "P" - im.seek(1) - assert im.mode == "RGB" - finally: - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST + im.seek(1) + assert im.mode == "RGB" def test_optimize() -> None: @@ -555,17 +556,15 @@ def test_dispose_background_transparency() -> None: def test_transparent_dispose( loading_strategy: GifImagePlugin.LoadingStrategy, expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]], + monkeypatch: pytest.MonkeyPatch, ) -> None: - GifImagePlugin.LOADING_STRATEGY = loading_strategy - try: - with Image.open("Tests/images/transparent_dispose.gif") as img: - for frame in range(3): - img.seek(frame) - for x in range(3): - color = img.getpixel((x, 0)) - assert color == expected_colors[frame][x] - finally: - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST + monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) + with Image.open("Tests/images/transparent_dispose.gif") as img: + for frame in range(3): + img.seek(frame) + for x in range(3): + color = img.getpixel((x, 0)) + assert color == expected_colors[frame][x] def test_dispose_previous() -> None: @@ -1398,24 +1397,23 @@ def test_lzw_bits() -> None: ), ) def test_extents( - test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy + test_file: str, + loading_strategy: GifImagePlugin.LoadingStrategy, + monkeypatch: pytest.MonkeyPatch, ) -> None: - GifImagePlugin.LOADING_STRATEGY = loading_strategy - try: - with Image.open("Tests/images/" + test_file) as im: - assert im.size == (100, 100) + monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) + with Image.open("Tests/images/" + test_file) as im: + assert im.size == (100, 100) - # Check that n_frames does not change the size - assert im.n_frames == 2 - assert im.size == (100, 100) + # Check that n_frames does not change the size + assert im.n_frames == 2 + assert im.size == (100, 100) - im.seek(1) - assert im.size == (150, 150) + im.seek(1) + assert im.size == (150, 150) - im.load() - assert im.im.size == (150, 150) - finally: - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST + im.load() + assert im.im.size == (150, 150) def test_missing_background() -> None: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index e81aae669..e240faf1e 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -243,26 +243,23 @@ def test_draw_reloaded(tmp_path: Path) -> None: assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") -def test_truncated_mask() -> None: +def test_truncated_mask(monkeypatch: pytest.MonkeyPatch) -> None: # 1 bpp with open("Tests/images/hopper_mask.ico", "rb") as fp: data = fp.read() - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) data = data[:-3] - try: - with Image.open(io.BytesIO(data)) as im: - assert im.mode == "1" + with Image.open(io.BytesIO(data)) as im: + assert im.mode == "1" - # 32 bpp - output = io.BytesIO() - expected = hopper("RGBA") - expected.save(output, "ico", bitmap_format="bmp") + # 32 bpp + output = io.BytesIO() + expected = hopper("RGBA") + expected.save(output, "ico", bitmap_format="bmp") - data = output.getvalue()[:-1] + data = output.getvalue()[:-1] - with Image.open(io.BytesIO(data)) as im: - assert im.mode == "RGB" - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + with Image.open(io.BytesIO(data)) as im: + assert im.mode == "RGB" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 4be9e16a7..772ecc2bc 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -530,12 +530,13 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_truncated_jpeg_should_read_all_the_data(self) -> None: + def test_truncated_jpeg_should_read_all_the_data( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: filename = "Tests/images/truncated_jpeg.jpg" - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) with Image.open(filename) as im: im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False assert im.getbbox() is not None def test_truncated_jpeg_throws_oserror(self) -> None: @@ -1024,7 +1025,7 @@ class TestFileJpeg: im.save(f, xmp=b"1" * 65505) @pytest.mark.timeout(timeout=1) - def test_eof(self) -> None: + def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished # the image should still end when there is no new data class InfiniteMockPyDecoder(ImageFile.PyDecoder): @@ -1039,9 +1040,8 @@ class TestFileJpeg: im.tile = [ ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), ] - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False def test_separate_tables(self) -> None: im = hopper() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 711e988df..589240191 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -181,14 +181,11 @@ def test_load_dpi() -> None: assert "dpi" not in im.info -def test_restricted_icc_profile() -> None: - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - # JPEG2000 image with a restricted ICC profile and a known colorspace - with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im: - assert im.mode == "RGB" - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False +def test_restricted_icc_profile(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + # JPEG2000 image with a restricted ICC profile and a known colorspace + with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im: + assert im.mode == "RGB" @pytest.mark.skipif( diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 18dd11182..a5715db1b 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1156,23 +1156,22 @@ class TestFileLibTiff(LibTiffTestCase): assert len(im.tag_v2[STRIPOFFSETS]) > 1 @pytest.mark.parametrize("argument", (True, False)) - def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None: + def test_save_single_strip( + self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") if not argument: - TiffImagePlugin.STRIP_SIZE = 2**18 - try: - arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} - if argument: - arguments["strip_size"] = 2**18 - im.save(out, "TIFF", **arguments) + monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18) + arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} + if argument: + arguments["strip_size"] = 2**18 + im.save(out, "TIFF", **arguments) - with Image.open(out) as im: - assert isinstance(im, TiffImagePlugin.TiffImageFile) - assert len(im.tag_v2[STRIPOFFSETS]) == 1 - finally: - TiffImagePlugin.STRIP_SIZE = 65536 + with Image.open(out) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + assert len(im.tag_v2[STRIPOFFSETS]) == 1 @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index d87883279..efd2e5cd9 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -363,7 +363,7 @@ class TestFilePng: with pytest.raises((OSError, SyntaxError)): im.verify() - def test_verify_ignores_crc_error(self) -> None: + def test_verify_ignores_crc_error(self, monkeypatch: pytest.MonkeyPatch) -> None: # check ignores crc errors in ancillary chunks chunk_data = chunk(b"tEXt", b"spam") @@ -373,24 +373,20 @@ class TestFilePng: with pytest.raises(SyntaxError): PngImagePlugin.PngImageFile(BytesIO(image_data)) - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im = load(image_data) - assert im is not None - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + im = load(image_data) + assert im is not None - def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None: + def test_verify_not_ignores_crc_error_in_required_chunk( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: # check does not ignore crc errors in required chunks image_data = MAGIC + IHDR[:-1] + b"q" + TAIL - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - with pytest.raises(SyntaxError): - PngImagePlugin.PngImageFile(BytesIO(image_data)) - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + with pytest.raises(SyntaxError): + PngImagePlugin.PngImageFile(BytesIO(image_data)) def test_roundtrip_dpi(self) -> None: # Check dpi roundtripping @@ -600,7 +596,7 @@ class TestFilePng: (b"prIV", b"VALUE3", True), ] - def test_textual_chunks_after_idat(self) -> None: + def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/hopper.png") as im: assert "comment" in im.text for k, v in { @@ -614,18 +610,17 @@ class TestFilePng: with pytest.raises(OSError): assert isinstance(im.text, dict) + # Raises an EOFError in load_end + with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: + assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} + # Raises a UnicodeDecodeError in load_end with Image.open("Tests/images/truncated_image.png") as im: # The file is truncated with pytest.raises(OSError): im.text - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) assert isinstance(im.text, dict) - ImageFile.LOAD_TRUNCATED_IMAGES = False - - # Raises an EOFError in load_end - with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: - assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} def test_unknown_compression_method(self) -> None: with pytest.raises(SyntaxError, match="Unknown compression method"): @@ -651,15 +646,16 @@ class TestFilePng: @pytest.mark.parametrize( "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") ) - def test_truncated_chunks(self, cid: bytes) -> None: + def test_truncated_chunks( + self, cid: bytes, monkeypatch: pytest.MonkeyPatch + ) -> None: fp = BytesIO() with PngImagePlugin.PngStream(fp) as png: with pytest.raises(ValueError): png.call(cid, 0, 0) - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) png.call(cid, 0, 0) - ImageFile.LOAD_TRUNCATED_IMAGES = False @pytest.mark.parametrize("save_all", (True, False)) def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None: @@ -789,17 +785,14 @@ class TestFilePng: with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - def test_truncated_end_chunk(self) -> None: + def test_truncated_end_chunk(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/truncated_end_chunk.png") as im: with pytest.raises(OSError): im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - with Image.open("Tests/images/truncated_end_chunk.png") as im: - assert_image_equal_tofile(im, "Tests/images/hopper.png") - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + with Image.open("Tests/images/truncated_end_chunk.png") as im: + assert_image_equal_tofile(im, "Tests/images/hopper.png") @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @@ -808,11 +801,11 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): mem_limit = 2 * 1024 # max increase in K iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs - def test_leak_load(self) -> None: + def test_leak_load(self, monkeypatch: pytest.MonkeyPatch) -> None: with open("Tests/images/hopper.png", "rb") as f: DATA = BytesIO(f.read(16 * 1024)) - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) with Image.open(DATA) as im: im.load() @@ -820,7 +813,4 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): with Image.open(DATA) as im: im.load() - try: - self._test_leak(core) - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + self._test_leak(core) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 757d3f96a..67f808b60 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -939,11 +939,10 @@ class TestFileTiff: @pytest.mark.timeout(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") - def test_timeout(self) -> None: + def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False @pytest.mark.parametrize( "test_file", diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index ad5aa9ed6..abe888241 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -28,9 +28,9 @@ except ImportError: class TestUnsupportedWebp: - def test_unsupported(self) -> None: + def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: if HAVE_WEBP: - WebPImagePlugin.SUPPORTED = False + monkeypatch.setattr(WebPImagePlugin, "SUPPORTED", False) file_path = "Tests/images/hopper.webp" with pytest.warns(UserWarning): @@ -38,9 +38,6 @@ class TestUnsupportedWebp: with Image.open(file_path): pass - if HAVE_WEBP: - WebPImagePlugin.SUPPORTED = True - @skip_unless_feature("webp") class TestFileWebp: diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 8bef90ce4..b05d29dae 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -191,13 +191,10 @@ class TestImageFile: im.load() @skip_unless_feature("zlib") - def test_truncated_without_errors(self) -> None: + def test_truncated_without_errors(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/truncated_image.png") as im: - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + im.load() @skip_unless_feature("zlib") def test_broken_datastream_with_errors(self) -> None: @@ -206,13 +203,12 @@ class TestImageFile: im.load() @skip_unless_feature("zlib") - def test_broken_datastream_without_errors(self) -> None: + def test_broken_datastream_without_errors( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: with Image.open("Tests/images/broken_data_stream.png") as im: - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + im.load() class MockPyDecoder(ImageFile.PyDecoder): diff --git a/Tests/test_map.py b/Tests/test_map.py index 93140f6e5..1278ba3a6 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -7,36 +7,30 @@ import pytest from PIL import Image -def test_overflow() -> None: +def test_overflow(monkeypatch: pytest.MonkeyPatch) -> None: # There is the potential to overflow comparisons in map.c # if there are > SIZE_MAX bytes in the image or if # the file encodes an offset that makes # (offset + size(bytes)) > SIZE_MAX # Note that this image triggers the decompression bomb warning: - max_pixels = Image.MAX_IMAGE_PIXELS - Image.MAX_IMAGE_PIXELS = None + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None) # This image hits the offset test. with Image.open("Tests/images/l2rgb_read.bmp") as im: with pytest.raises((ValueError, MemoryError, OSError)): im.load() - Image.MAX_IMAGE_PIXELS = max_pixels - -def test_tobytes() -> None: +def test_tobytes(monkeypatch: pytest.MonkeyPatch) -> None: # Note that this image triggers the decompression bomb warning: - max_pixels = Image.MAX_IMAGE_PIXELS - Image.MAX_IMAGE_PIXELS = None + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None) # Previously raised an access violation on Windows with Image.open("Tests/images/l2rgb_read.bmp") as im: with pytest.raises((ValueError, MemoryError, OSError)): im.tobytes() - Image.MAX_IMAGE_PIXELS = max_pixels - @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_ysize() -> None: From a9d05a1e5122f1bab86d9c4d1b34a9cd40093d51 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 28 Jan 2025 07:59:44 +1100 Subject: [PATCH 581/652] Fixed unclosed file warnings (#8705) Co-authored-by: Andrew Murray --- Tests/test_file_libtiff.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index a5715db1b..033294710 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1103,13 +1103,15 @@ class TestFileLibTiff(LibTiffTestCase): ) def test_buffering(self, test_file: str) -> None: # load exif first - with Image.open(open(test_file, "rb", buffering=1048576)) as im: - exif = dict(im.getexif()) + with open(test_file, "rb", buffering=1048576) as f: + with Image.open(f) as im: + exif = dict(im.getexif()) # load image before exif - with Image.open(open(test_file, "rb", buffering=1048576)) as im2: - im2.load() - exif_after_load = dict(im2.getexif()) + with open(test_file, "rb", buffering=1048576) as f: + with Image.open(f) as im2: + im2.load() + exif_after_load = dict(im2.getexif()) assert exif == exif_after_load From 849768df7afbe02094fea2cda31f71e9267265d6 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 17 Sep 2024 15:49:20 +0200 Subject: [PATCH 582/652] Remove unused declaration --- src/libImaging/Imaging.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 31052c68a..0c2d3fc2e 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -609,10 +609,6 @@ ImagingLibTiffDecode( extern int ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); #endif -#ifdef HAVE_LIBMPEG -extern int -ImagingMpegDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -#endif extern int ImagingMspDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); extern int From f598c0323392f12f0bd6d0b26e3c7de106c0b11d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Jan 2025 17:33:55 +1100 Subject: [PATCH 583/652] Removed unused file --- src/libImaging/Except.c | 72 ----------------------------------------- 1 file changed, 72 deletions(-) delete mode 100644 src/libImaging/Except.c diff --git a/src/libImaging/Except.c b/src/libImaging/Except.c deleted file mode 100644 index f42ff9aec..000000000 --- a/src/libImaging/Except.c +++ /dev/null @@ -1,72 +0,0 @@ -/* - * The Python Imaging Library - * $Id$ - * - * default exception handling - * - * This module is usually overridden by application code (e.g. - * _imaging.c for PIL's standard Python bindings). If you get - * linking errors, remove this file from your project/library. - * - * history: - * 1995-06-15 fl Created - * 1998-12-29 fl Minor tweaks - * 2003-09-13 fl Added ImagingEnter/LeaveSection() - * - * Copyright (c) 1997-2003 by Secret Labs AB. - * Copyright (c) 1995-2003 by Fredrik Lundh. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -/* exception state */ - -void * -ImagingError_OSError(void) { - fprintf(stderr, "*** exception: file access error\n"); - return NULL; -} - -void * -ImagingError_MemoryError(void) { - fprintf(stderr, "*** exception: out of memory\n"); - return NULL; -} - -void * -ImagingError_ModeError(void) { - return ImagingError_ValueError("bad image mode"); -} - -void * -ImagingError_Mismatch(void) { - return ImagingError_ValueError("images don't match"); -} - -void * -ImagingError_ValueError(const char *message) { - if (!message) { - message = "exception: bad argument to function"; - } - fprintf(stderr, "*** %s\n", message); - return NULL; -} - -void -ImagingError_Clear(void) { - /* nop */; -} - -/* thread state */ - -void -ImagingSectionEnter(ImagingSectionCookie *cookie) { - /* pass */ -} - -void -ImagingSectionLeave(ImagingSectionCookie *cookie) { - /* pass */ -} From 9a4f39588dc082f6a6fbab3354b2a55fc588c195 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Jan 2025 18:58:53 +1100 Subject: [PATCH 584/652] Use embedded color for text length in multiline_text --- src/PIL/ImageDraw.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d8e4c0c60..dd691eeec 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -742,7 +742,12 @@ class ImageDraw: line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: line_width = self.textlength( - line, font, direction=direction, features=features, language=language + line, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, ) widths.append(line_width) max_width = max(max_width, line_width) From 7093de46a7629956b77fba1ce1bfaf4ebb9c194d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Jan 2025 19:42:25 +1100 Subject: [PATCH 585/652] Moved common multiline code into _prepare_multiline_text --- src/PIL/ImageDraw.py | 263 +++++++++++++++++++++---------------------- 1 file changed, 127 insertions(+), 136 deletions(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index dd691eeec..d8b5180de 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -557,21 +557,6 @@ class ImageDraw: return split_character in text - def _multiline_split(self, text: AnyStr) -> list[AnyStr]: - return text.split("\n" if isinstance(text, str) else b"\n") - - def _multiline_spacing( - self, - font: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, - spacing: float, - stroke_width: float, - ) -> float: - return ( - self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] - + stroke_width - + spacing - ) - def text( self, xy: tuple[float, float], @@ -697,6 +682,101 @@ class ImageDraw: # Only draw normal text draw_text(ink) + def _prepare_multiline_text( + self, + xy: tuple[float, float], + text: AnyStr, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ), + anchor: str | None, + spacing: float, + align: str, + direction: str | None, + features: list[str] | None, + language: str | None, + stroke_width: float, + embedded_color: bool, + font_size: float | None, + ) -> tuple[ + ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, + str, + list[tuple[tuple[float, float], AnyStr]], + ]: + if direction == "ttb": + msg = "ttb direction is unsupported for multiline text" + raise ValueError(msg) + + if anchor is None: + anchor = "la" + elif len(anchor) != 2: + msg = "anchor must be a 2 character string" + raise ValueError(msg) + elif anchor[1] in "tb": + msg = "anchor not supported for multiline text" + raise ValueError(msg) + + if font is None: + font = self._getfont(font_size) + + widths = [] + max_width: float = 0 + lines = text.split("\n" if isinstance(text, str) else b"\n") + line_spacing = ( + self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] + + stroke_width + + spacing + ) + + for line in lines: + line_width = self.textlength( + line, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + widths.append(line_width) + max_width = max(max_width, line_width) + + top = xy[1] + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + + parts = [] + for idx, line in enumerate(lines): + left = xy[0] + width_difference = max_width - widths[idx] + + # first align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + + # then align by align parameter + if align == "left": + pass + elif align == "center": + left += width_difference / 2.0 + elif align == "right": + left += width_difference + else: + msg = 'align must be "left", "center" or "right"' + raise ValueError(msg) + + parts.append(((left, top), line)) + + top += line_spacing + + return font, anchor, parts + def multiline_text( self, xy: tuple[float, float], @@ -720,67 +800,24 @@ class ImageDraw: *, font_size: float | None = None, ) -> None: - if direction == "ttb": - msg = "ttb direction is unsupported for multiline text" - raise ValueError(msg) - - if anchor is None: - anchor = "la" - elif len(anchor) != 2: - msg = "anchor must be a 2 character string" - raise ValueError(msg) - elif anchor[1] in "tb": - msg = "anchor not supported for multiline text" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - - widths = [] - max_width: float = 0 - lines = self._multiline_split(text) - line_spacing = self._multiline_spacing(font, spacing, stroke_width) - for line in lines: - line_width = self.textlength( - line, - font, - direction=direction, - features=features, - language=language, - embedded_color=embedded_color, - ) - widths.append(line_width) - max_width = max(max_width, line_width) - - top = xy[1] - if anchor[1] == "m": - top -= (len(lines) - 1) * line_spacing / 2.0 - elif anchor[1] == "d": - top -= (len(lines) - 1) * line_spacing - - for idx, line in enumerate(lines): - left = xy[0] - width_difference = max_width - widths[idx] - - # first align left by anchor - if anchor[0] == "m": - left -= width_difference / 2.0 - elif anchor[0] == "r": - left -= width_difference - - # then align by align parameter - if align == "left": - pass - elif align == "center": - left += width_difference / 2.0 - elif align == "right": - left += width_difference - else: - msg = 'align must be "left", "center" or "right"' - raise ValueError(msg) + font, anchor, lines = self._prepare_multiline_text( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + font_size, + ) + for xy, line in lines: self.text( - (left, top), + xy, line, fill, font, @@ -792,7 +829,6 @@ class ImageDraw: stroke_fill=stroke_fill, embedded_color=embedded_color, ) - top += line_spacing def textlength( self, @@ -894,69 +930,26 @@ class ImageDraw: *, font_size: float | None = None, ) -> tuple[float, float, float, float]: - if direction == "ttb": - msg = "ttb direction is unsupported for multiline text" - raise ValueError(msg) - - if anchor is None: - anchor = "la" - elif len(anchor) != 2: - msg = "anchor must be a 2 character string" - raise ValueError(msg) - elif anchor[1] in "tb": - msg = "anchor not supported for multiline text" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - - widths = [] - max_width: float = 0 - lines = self._multiline_split(text) - line_spacing = self._multiline_spacing(font, spacing, stroke_width) - for line in lines: - line_width = self.textlength( - line, - font, - direction=direction, - features=features, - language=language, - embedded_color=embedded_color, - ) - widths.append(line_width) - max_width = max(max_width, line_width) - - top = xy[1] - if anchor[1] == "m": - top -= (len(lines) - 1) * line_spacing / 2.0 - elif anchor[1] == "d": - top -= (len(lines) - 1) * line_spacing + font, anchor, lines = self._prepare_multiline_text( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + font_size, + ) bbox: tuple[float, float, float, float] | None = None - for idx, line in enumerate(lines): - left = xy[0] - width_difference = max_width - widths[idx] - - # first align left by anchor - if anchor[0] == "m": - left -= width_difference / 2.0 - elif anchor[0] == "r": - left -= width_difference - - # then align by align parameter - if align == "left": - pass - elif align == "center": - left += width_difference / 2.0 - elif align == "right": - left += width_difference - else: - msg = 'align must be "left", "center" or "right"' - raise ValueError(msg) - + for xy, line in lines: bbox_line = self.textbbox( - (left, top), + xy, line, font, anchor, @@ -976,8 +969,6 @@ class ImageDraw: max(bbox[3], bbox_line[3]), ) - top += line_spacing - if bbox is None: return xy[0], xy[1], xy[0], xy[1] return bbox From 10eaff8ac7548ff50cefc003b27e2ba1a46ed71b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Jan 2025 20:12:45 +1100 Subject: [PATCH 586/652] Added "justify" align for multiline text --- Tests/images/multiline_text_justify.png | Bin 0 -> 3244 bytes Tests/test_imagefont.py | 3 ++- docs/reference/ImageDraw.rst | 20 ++++++++++++-------- src/PIL/ImageDraw.py | 24 +++++++++++++++++++++--- 4 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 Tests/images/multiline_text_justify.png diff --git a/Tests/images/multiline_text_justify.png b/Tests/images/multiline_text_justify.png new file mode 100644 index 0000000000000000000000000000000000000000..32eed34cd219df9a59b7056c0ed17fa3879adea9 GIT binary patch literal 3244 zcmb_e`#;nBA6F?WN@eDfDD`zGk!#kDvE(-98qIwn<~q5|rBiVzq2(;M;nb2em*YCu z7KL1vM$9FI<~ogR=HBnL`T7eL-pN1@)#)qt}h96@N!gmjVFJ<>~Q^+ut5F5)EPlg<(<=sE-o%kl-qG}&1AAq&r|h@ zGNJ{`2DZ!XgZ&f{6=e<$DJUpZ6)rC?7b$fwM-~?qF#=DDiG_TrH%5Ifvdr4AcL*f& z!(jYxFB414)bU{I*z%$KUhn=m+G{_-JUu-xYrIU>X4r5zALK7L4x~R*O$lCA*psH7@7rT5RcxmL?Szk zqf?zIWu>Lx);4>kYhB|vR=j*K>ge!#bF`x-z)V}hljAblE6g=i6r$3G{NM}mM2I6l>I>n?3Ij=oGyFI-#rdXUO|r z)Ye{GrcS;In>n=I?pxhJp{Ob=KX6%Vm+-(~FrXwjp3pbPnP_QhZZ@)InS9;MAIQHP z^^{0FDk@s-p<`r(c6Wc{)0zI)kf9#IJ=>)Y>tbms%=(9-;@q=jS?^qfd}Kb78pUzP zUxY$;fed_ne7;B=Yn_fl7TGOS(DoL0;grF~xXmAD91XGvvWW){9LUoRnzosnXOP)X zZ{ECVU|_&K{BCb|+tSjqp+fpz51+Ko=T}^>Y}P zZ2-|MlMC)+eG#=thA((c)(1^}3Krq{r#_?+tVK*jPsu@h18fZ%G|u z85g(HmLz;rYlJ~IyWrlOBCM^)C_IP)JEBm=70T-BMa*HWZe)*NLCnkA+QDFIMn*;+ zeU?8F8#2m28zHKrqqFbA>3bjeq~Ot-E>BH;nPbWCyfh_sgTKs=E5Kks7Y8X~Vq#ax zs1k>lFJ9P!Z|`iZ{^k@fM0xef(?cgIMgV7+b*-qsqoc#tc^|rs^zoux+)C?E*$or7 zgQ16UhI*HDbSh!+OOhhb%0YXoy~@8B`tS92M<*oQ6r<&0V)8D8wmTbPnEb~dTzoth zTWXevoBN0YvqJTwjV5;alNAFCdwbgj^FOf1wncZ*|x+XcAdQ%9JlhdmCeTb5-{MUp|a``Yz~q5q8$OR$G8l08x0TgSIXS}!|Es(&jM>2SYmSbCKKiH-90RoJa>Y|L#adlU*55|vd`Qu3q{ zdRS6YQoZtB%~^l`Y4mevjq5|SaJ+u~!{@FqD^OwnB0CJXvv$tRR1G3$5dNa6Dc0M2 zdbBn$EiLVr4Ba~@+yy>fF5`K2l2ScF zkG~`uZkh<60{(U8vVVkqzi`=gtjW`IYdQ^W@N`ab&T! zmX?;Xau-&23|r?&aeB}`&0wIYV^A^*-5ed(nSNSXS((S|(qQLFQ8ajy^V}1I{BuuobMaGcN$=mk2OI#f zp4Wo`0Lm(_50$$ujSpkAHs>E2StsKX$HDPIC0npYA=A;DfEnMB$*Z^EgYVd&3giPNF z6yTnz?F$j>JYM|PLNS15=0XI!tiq-GkG-ADWk8Cirlw@w2*nG6cfd8Rt*s=I%vPAM z?`(}9Ex~M+t~dGO<;%LdIzyRsplf8te3&$R?IOn`o+sXN$|HK}U}N;d=kq^nB;@35 zP!9I?VlmLSGiS{N&(u6VxBs~JhZFliN57@~OE%sH#Z0Q|pVHKW+JT9Ad8g`+60@@G z!Hymta`o1s!RyT4FOtf~L2$PoHh?Y_Rn>Uv>Kp4Wq0Fuqwn}?AH&ehRB|hDM+&Kf? z_9P{ z^^5`nk$AJ#wX?JHvY}xec4T^0ZM!Gu%7pxrZ@HoEW>>l&jcRi7Q0QCxVq}a_dsn@F zZ}uumI|e4Bz<=fN+9P2TD5h)K(9TZW7x?OhREZ1gdxRf<4F>S=@s5s;R#sLR%nP&z zEu3Wmhqu+F#gmE&-*S6XGJ*)-rXSaM6?(8(EFC?)Nn9WFuB-%ycC&Y*Eaz`I8c#D$ zkq?GI>S}6A-I{de<>eI=Ue?#wH#f`7B~p*eCK8E6_N042?bgcFy8+$xFwYITPEN^K zQ0jTPB+bjon|C!-RQeYpWL=amee7)CfNA+&3s7KphCoH+x}fIf=EXLJ>-9HS08OI| z^YuY~Pb=-++^kXHYX9nyZ0yPK1e|p}{7DA9AYV*YwtQg0R2~%>S!d>SB#jal_T-|8 zowgT@be^w7>~3p=1DfEzxwp-+v9>){byA%YqLPj6$|6SN2+cP1gB?) zg%N)RFo&W4!GGV;;i?A7AZBJ}4t(FaZ2ebx`qo!HW}@o(O{H&ADs(zM`Pk_IfB#n$ zN-Tg_=rk-^CM}inNUAM&cm17Xc;BZ_j~!G_%gA6VfXT@6sano0=e0gMEfEN492@xk zf1$YBD|L0cX;*PSzL|3}91Wx-oTaoAb+(t2Dc=gj6p?@5|NT}|iXLy@{hm>oFgV)|3OCopCL9&#jU8gPyR zAXOJEmQ9^ZxU8i$9jf2h*qAP3&~`=n`A?672$ZtDUmOtkDt{1?I5tgSO>YFx>_U5ohMe#?_L2taQEDfuL&S@ouzj{9S+hY=P0fnT z;I6MDk;py3+z<$aFhosLI?YPq=zk`F|Ht=jDIZooCBA+rAiU~VJaYunv>fhv@1HSP BV(I_@ literal 0 HcmV?d00001 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 6a0a940b9..3ccbf9b7d 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -254,7 +254,8 @@ def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: @pytest.mark.parametrize( - "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) + "align, ext", + (("left", ""), ("center", "_center"), ("right", "_right"), ("justify", "_justify")), ) def test_render_multiline_text_align( font: ImageFont.FreeTypeFont, align: str, ext: str diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 3e9aa73f8..602a8f3e3 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -387,8 +387,9 @@ Methods the number of pixels between lines. :param align: If the text is passed on to :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, - ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. - Use the ``anchor`` parameter to specify the alignment to ``xy``. + ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines + the relative alignment of lines. Use the ``anchor`` parameter to + specify the alignment to ``xy``. :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -455,8 +456,9 @@ Methods of Pillow, but implemented only in version 8.0.0. :param spacing: The number of pixels between lines. - :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. - Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines + the relative alignment of lines. Use the ``anchor`` parameter to + specify the alignment to ``xy``. :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -599,8 +601,9 @@ Methods the number of pixels between lines. :param align: If the text is passed on to :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`, - ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. - Use the ``anchor`` parameter to specify the alignment to ``xy``. + ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines + the relative alignment of lines. Use the ``anchor`` parameter to + specify the alignment to ``xy``. :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -650,8 +653,9 @@ Methods vertical text. See :ref:`text-anchors` for details. This parameter is ignored for non-TrueType fonts. :param spacing: The number of pixels between lines. - :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. - Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines + the relative alignment of lines. Use the ``anchor`` parameter to + specify the alignment to ``xy``. :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d8b5180de..da7098789 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -761,17 +761,35 @@ class ImageDraw: left -= width_difference # then align by align parameter - if align == "left": + if align in ("left", "justify"): pass elif align == "center": left += width_difference / 2.0 elif align == "right": left += width_difference else: - msg = 'align must be "left", "center" or "right"' + msg = 'align must be "left", "center", "right" or "justify"' raise ValueError(msg) - parts.append(((left, top), line)) + if align == "justify" and width_difference != 0: + words = line.split(" " if isinstance(text, str) else b" ") + word_widths = [ + self.textlength( + word, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + for word in words + ] + width_difference = max_width - sum(word_widths) + for i, word in enumerate(words): + parts.append(((left, top), word)) + left += word_widths[i] + width_difference / (len(words) - 1) + else: + parts.append(((left, top), line)) top += line_spacing From 1e115987afbc92aef02b489ed8fea1875821d174 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Jan 2025 19:09:03 +1100 Subject: [PATCH 587/652] Do not install libimagequant --- .github/workflows/test-mingw.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index bb6d7dc37..045926482 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -60,7 +60,6 @@ jobs: mingw-w64-x86_64-gcc \ mingw-w64-x86_64-ghostscript \ mingw-w64-x86_64-lcms2 \ - mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libjpeg-turbo \ mingw-w64-x86_64-libraqm \ mingw-w64-x86_64-libtiff \ From 9a58456c9b6a06518f3ce653ce02ec6e25512121 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 1 Feb 2025 00:44:26 +1100 Subject: [PATCH 588/652] Added versionadded for justify Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/reference/ImageDraw.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 602a8f3e3..a2e64a22a 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -390,6 +390,8 @@ Methods ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. + + .. versionadded:: 11.2.0 ``justify`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. From ec72d20d23e1dc2b06792535a6db7df778a8ad94 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Feb 2025 00:47:21 +1100 Subject: [PATCH 589/652] Added release notes --- docs/reference/ImageDraw.rst | 10 ++++++++-- docs/releasenotes/11.2.0.rst | 12 ++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index a2e64a22a..b2f1bdc93 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -390,8 +390,8 @@ Methods ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. - - .. versionadded:: 11.2.0 ``justify`` + + .. versionadded:: 11.2.0 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -461,6 +461,8 @@ Methods :param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. + + .. versionadded:: 11.2.0 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -606,6 +608,8 @@ Methods ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. + + .. versionadded:: 11.2.0 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -658,6 +662,8 @@ Methods :param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. + + .. versionadded:: 11.2.0 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index df28d05af..7e0008e66 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -44,6 +44,18 @@ TODO API Additions ============= +"justify" multiline text alignment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to "left", "center" and "right", multiline text can also be aligned using +"justify":: + + from PIL import Image, ImageDraw + im = Image.new("RGB", (50, 25)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify") + draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify") + Check for MozJPEG ^^^^^^^^^^^^^^^^^ From 5bbbc462403067c42dec782c11920aeecf9206a0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Feb 2025 01:13:04 +1100 Subject: [PATCH 590/652] Fixed exceptions when closing AppendingTiffWriter --- Tests/test_file_tiff.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 67f808b60..af4bae5dc 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -780,15 +780,17 @@ class TestFileTiff: data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b = BytesIO(data) with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.seek(-4, os.SEEK_CUR) a.writeLong(2**32 - 1) - assert b.getvalue() == data + b"\xff\xff\xff\xff" + assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff" def test_appending_tiff_writer_rewritelastshorttolong(self) -> None: data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b = BytesIO(data) with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.seek(-2, os.SEEK_CUR) a.rewriteLastShortToLong(2**32 - 1) - assert b.getvalue() == data[:-2] + b"\xff\xff\xff\xff" + assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff" def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. From fca48db866870dea024e4f627059b17571940349 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 1 Feb 2025 10:02:42 +1100 Subject: [PATCH 591/652] Added quote marks Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/releasenotes/11.2.0.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index 7e0008e66..5929de3b1 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -44,11 +44,11 @@ TODO API Additions ============= -"justify" multiline text alignment -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``"justify"`` multiline text alignment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -In addition to "left", "center" and "right", multiline text can also be aligned using -"justify":: +In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be aligned using +``"justify"``:: from PIL import Image, ImageDraw im = Image.new("RGB", (50, 25)) From 69c95725179c2d7cbba1104ed5c99d2e9092f43d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Feb 2025 10:54:18 +1100 Subject: [PATCH 592/652] Added ImageDraw link --- docs/releasenotes/11.2.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index 5929de3b1..f7e644cf3 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -47,8 +47,8 @@ API Additions ``"justify"`` multiline text alignment ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be aligned using -``"justify"``:: +In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be +aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`:: from PIL import Image, ImageDraw im = Image.new("RGB", (50, 25)) From 347a3865bf809918edaa4391978394dec47c80e3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Feb 2025 12:21:15 +1100 Subject: [PATCH 593/652] Revert "Ignore brew dependencies for libraqm on macOS 13" This reverts commit dfd53564ff6a3fc7d35a5884bc0ef03939bcec0a. --- .github/workflows/macos-install.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 2301a3a7e..6aa59a4ac 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -10,15 +10,11 @@ brew install \ ghostscript \ jpeg-turbo \ libimagequant \ + libraqm \ libtiff \ little-cms2 \ openjpeg \ webp -if [[ "$ImageOS" == "macos13" ]]; then - brew install --ignore-dependencies libraqm -else - brew install libraqm -fi export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" python3 -m pip install coverage From ce1996d8040bd2bab17a16ceb678eed3325194eb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Feb 2025 19:32:59 +1100 Subject: [PATCH 594/652] Use getpixel() instead of load() --- Tests/test_file_gif.py | 13 ++++++------- Tests/test_image.py | 4 +--- Tests/test_image_convert.py | 4 +--- Tests/test_image_quantize.py | 4 +--- Tests/test_imageops.py | 8 ++------ Tests/test_numpy.py | 4 +--- 6 files changed, 12 insertions(+), 25 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 61a9475c7..46215db1f 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -86,12 +86,12 @@ def test_invalid_file() -> None: def test_l_mode_transparency() -> None: with Image.open("Tests/images/no_palette_with_transparency.gif") as im: assert im.mode == "L" - assert im.load()[0, 0] == 128 + assert im.getpixel((0, 0)) == 128 assert im.info["transparency"] == 255 im.seek(1) assert im.mode == "L" - assert im.load()[0, 0] == 128 + assert im.getpixel((0, 0)) == 128 def test_l_mode_after_rgb() -> None: @@ -311,7 +311,7 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None: with Image.open(path) as im: assert im.mode == "P" first_frame_colors = im.palette.colors.keys() - original_color = im.convert("RGB").load()[0, 0] + original_color = im.convert("RGB").getpixel((0, 0)) im.seek(1) assert im.mode == mode @@ -319,10 +319,10 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None: im = im.convert("RGB") # Check a color only from the old palette - assert im.load()[0, 0] == original_color + assert im.getpixel((0, 0)) == original_color # Check a color from the new palette - assert im.load()[24, 24] not in first_frame_colors + assert im.getpixel((24, 24)) not in first_frame_colors def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: @@ -488,8 +488,7 @@ def test_eoferror() -> None: def test_first_frame_transparency() -> None: with Image.open("Tests/images/first_frame_transparency.gif") as im: - px = im.load() - assert px[0, 0] == im.info["transparency"] + assert im.getpixel((0, 0)) == im.info["transparency"] def test_dispose_none() -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 9a2e3c465..e060eb06a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -578,9 +578,7 @@ class TestImage: def test_one_item_tuple(self) -> None: for mode in ("I", "F", "L"): im = Image.new(mode, (100, 100), (5,)) - px = im.load() - assert px is not None - assert px[0, 0] == 5 + assert im.getpixel((0, 0)) == 5 def test_linear_gradient_wrong_mode(self) -> None: # Arrange diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 6a925975e..1e66e84df 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -222,9 +222,7 @@ def test_l_macro_rounding(convert_mode: str) -> None: im.palette.getcolor((0, 1, 2)) converted_im = im.convert(convert_mode) - px = converted_im.load() - assert px is not None - converted_color = px[0, 0] + converted_color = converted_im.getpixel((0, 0)) if convert_mode == "LA": assert isinstance(converted_color, tuple) converted_color = converted_color[0] diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 7c564d967..0ca7ad86e 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -148,10 +148,8 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None: im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) converted = im.quantize(method=method) - converted_px = converted.load() - assert converted_px is not None assert converted.palette is not None - assert converted_px[0, 0] == converted.palette.colors[color] + assert converted.getpixel((0, 0)) == converted.palette.colors[color] def test_small_palette() -> None: diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 7262f29e6..3621aa50f 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -165,14 +165,10 @@ def test_pad() -> None: def test_pad_round() -> None: im = Image.new("1", (1, 1), 1) new_im = ImageOps.pad(im, (4, 1)) - px = new_im.load() - assert px is not None - assert px[2, 0] == 1 + assert new_im.getpixel((2, 0)) == 1 new_im = ImageOps.pad(im, (1, 4)) - px = new_im.load() - assert px is not None - assert px[0, 2] == 1 + assert new_im.getpixel((0, 2)) == 1 @pytest.mark.parametrize("mode", ("P", "PA")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 79cd14b66..c4ad19d23 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -141,9 +141,7 @@ def test_save_tiff_uint16() -> None: a.shape = TEST_IMAGE_SIZE img = Image.fromarray(a) - img_px = img.load() - assert img_px is not None - assert img_px[0, 0] == pixel_value + assert img.getpixel((0, 0)) == pixel_value @pytest.mark.parametrize( From 90d25060743dfc118816378e3f614040b13f9596 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:35:25 +0000 Subject: [PATCH 595/652] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.8.6 → v0.9.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.6...v0.9.4) - [github.com/psf/black-pre-commit-mirror: 24.10.0 → 25.1.0](https://github.com/psf/black-pre-commit-mirror/compare/24.10.0...25.1.0) - [github.com/PyCQA/bandit: 1.8.0 → 1.8.2](https://github.com/PyCQA/bandit/compare/1.8.0...1.8.2) - [github.com/pre-commit/mirrors-clang-format: v19.1.6 → v19.1.7](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.6...v19.1.7) - [github.com/python-jsonschema/check-jsonschema: 0.30.0 → 0.31.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.30.0...0.31.1) - [github.com/woodruffw/zizmor-pre-commit: v1.0.0 → v1.3.0](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.0.0...v1.3.0) - [github.com/tox-dev/tox-ini-fmt: 1.4.1 → 1.5.0](https://github.com/tox-dev/tox-ini-fmt/compare/1.4.1...1.5.0) --- .pre-commit-config.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 20fa7d04f..a8c8cee15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.6 + rev: v0.9.4 hooks: - id: ruff args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.10.0 + rev: 25.1.0 hooks: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.8.0 + rev: 1.8.2 hooks: - id: bandit args: [--severity-level=high] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.6 + rev: v19.1.7 hooks: - id: clang-format types: [c] @@ -50,14 +50,14 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.30.0 + rev: 0.31.1 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.0.0 + rev: v1.3.0 hooks: - id: zizmor @@ -78,7 +78,7 @@ repos: additional_dependencies: [trove-classifiers>=2024.10.12] - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.4.1 + rev: 1.5.0 hooks: - id: tox-ini-fmt From 955d678ca201fd530027d90626f91aad07c64f0e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:35:58 +0000 Subject: [PATCH 596/652] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg.py | 2 +- Tests/test_file_libtiff.py | 2 +- Tests/test_file_pdf.py | 4 ++-- Tests/test_file_ppm.py | 4 ++-- Tests/test_file_tiff.py | 10 ++++----- Tests/test_file_wmf.py | 2 +- Tests/test_imagedraw.py | 2 +- Tests/test_imagefont.py | 6 +++--- Tests/test_imagefontctl.py | 42 +++++++++++++++++++------------------- Tests/test_imagepalette.py | 2 +- Tests/test_imagepath.py | 2 +- Tests/test_pdfparser.py | 18 ++++++++-------- src/PIL/ImImagePlugin.py | 4 ++-- src/PIL/Image.py | 4 ++-- src/PIL/ImtImagePlugin.py | 2 +- src/PIL/JpegImagePlugin.py | 12 +++++------ src/PIL/MpoImagePlugin.py | 4 ++-- src/PIL/PcxImagePlugin.py | 2 +- src/PIL/PdfParser.py | 36 ++++++++++++++++---------------- src/PIL/PngImagePlugin.py | 2 +- src/PIL/PpmImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 16 +++++++-------- src/PIL/_tkinter_finder.py | 3 +-- 23 files changed, 91 insertions(+), 92 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 772ecc2bc..91bf3cf74 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -934,7 +934,7 @@ class TestFileJpeg: def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None: size = 4097 - buffer = BytesIO(b"\xFF" * size) # Many xFF bytes + buffer = BytesIO(b"\xff" * size) # Many xFF bytes max_pos = 0 orig_read = buffer.read diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 033294710..369c2db1b 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -309,7 +309,7 @@ class TestFileLibTiff(LibTiffTestCase): } def check_tags( - tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] + tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str], ) -> None: im = hopper() diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 1d5001b1a..815686a52 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -264,7 +264,7 @@ def test_pdf_append(tmp_path: Path) -> None: # append some info pdf.info.Title = "abc" pdf.info.Author = "def" - pdf.info.Subject = "ghi\uABCD" + pdf.info.Subject = "ghi\uabcd" pdf.info.Keywords = "qw)e\\r(ty" pdf.info.Creator = "hopper()" pdf.start_writing() @@ -292,7 +292,7 @@ def test_pdf_append(tmp_path: Path) -> None: assert pdf.info.Title == "abc" assert pdf.info.Producer == "PdfParser" assert pdf.info.Keywords == "qw)e\\r(ty" - assert pdf.info.Subject == "ghi\uABCD" + assert pdf.info.Subject == "ghi\uabcd" assert b"CreationDate" in pdf.info assert b"ModDate" in pdf.info check_pdf_pages_consistency(pdf) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index ee51a5e5a..bb59767f0 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -49,7 +49,7 @@ def test_sanity() -> None: (b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)), # P6 with maxval < 255 ( - b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11", + b"P6 3 1 17 \x00\x01\x02\x08\x09\x0a\x0f\x10\x11", "RGB", ( (0, 15, 30), @@ -60,7 +60,7 @@ def test_sanity() -> None: # P6 with maxval > 255 ( b"P6 3 1 257 \x00\x00\x00\x01\x00\x02" - b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF", + b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xff\xff", "RGB", ( (0, 1, 2), diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index af4bae5dc..fe8f69848 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -746,7 +746,7 @@ class TestFileTiff: assert reread.n_frames == 3 def test_fixoffsets(self) -> None: - b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00") + b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") with TiffImagePlugin.AppendingTiffWriter(b) as a: b.seek(0) a.fixOffsets(1, isShort=True) @@ -759,14 +759,14 @@ class TestFileTiff: with pytest.raises(RuntimeError): a.fixOffsets(1) - b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00") + b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") with TiffImagePlugin.AppendingTiffWriter(b) as a: a.offsetOfNewPage = 2**16 b.seek(0) a.fixOffsets(1, isShort=True) - b = BytesIO(b"II\x2B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") + b = BytesIO(b"II\x2b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") with TiffImagePlugin.AppendingTiffWriter(b) as a: a.offsetOfNewPage = 2**32 @@ -777,7 +777,7 @@ class TestFileTiff: a.fixOffsets(1, isLong=True) def test_appending_tiff_writer_writelong(self) -> None: - data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + data = b"II\x2a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b = BytesIO(data) with TiffImagePlugin.AppendingTiffWriter(b) as a: a.seek(-4, os.SEEK_CUR) @@ -785,7 +785,7 @@ class TestFileTiff: assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff" def test_appending_tiff_writer_rewritelastshorttolong(self) -> None: - data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + data = b"II\x2a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b = BytesIO(data) with TiffImagePlugin.AppendingTiffWriter(b) as a: a.seek(-2, os.SEEK_CUR) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 2f1f8cdbc..bc14ed9d7 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -71,7 +71,7 @@ def test_load_float_dpi() -> None: with open("Tests/images/drawing.emf", "rb") as fp: data = fp.read() - b = BytesIO(data[:8] + b"\x06\xFA" + data[10:]) + b = BytesIO(data[:8] + b"\x06\xfa" + data[10:]) with Image.open(b) as im: assert im.info["dpi"][0] == 2540 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 28d7ed725..d127175eb 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -812,7 +812,7 @@ def test_rounded_rectangle( tuple[int, int, int, int] | tuple[list[int]] | tuple[tuple[int, int], tuple[int, int]] - ) + ), ) -> None: # Arrange im = Image.new("RGB", (200, 200)) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index f110cc1d0..4b41d8336 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -557,7 +557,7 @@ def test_render_empty(font: ImageFont.FreeTypeFont) -> None: def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: # issue #3777 - text = "A\u278A\U0001F12B" + text = "A\u278a\U0001f12b" target = "Tests/images/unicode_extended.png" ttf = ImageFont.truetype( @@ -1026,7 +1026,7 @@ def test_sbix(layout_engine: ImageFont.Layout) -> None: im = Image.new("RGB", (400, 400), "white") d = ImageDraw.Draw(im) - d.text((50, 50), "\uE901", font=font, embedded_color=True) + d.text((50, 50), "\ue901", font=font, embedded_color=True) assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) except OSError as e: # pragma: no cover @@ -1043,7 +1043,7 @@ def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: im = Image.new("RGB", (400, 400), "white") d = ImageDraw.Draw(im) - d.text((50, 50), "\uE901", (100, 0, 0), font=font) + d.text((50, 50), "\ue901", (100, 0, 0), font=font) assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) except OSError as e: # pragma: no cover diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 24c7b871a..c85eb499c 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -229,7 +229,7 @@ def test_getlength( @pytest.mark.parametrize("direction", ("ltr", "ttb")) @pytest.mark.parametrize( "text", - ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), + ("i" + ("\u030c" * 15) + "i", "i" + "\u032c" * 15 + "i", "\u035cii", "i\u0305i"), ids=("caron-above", "caron-below", "double-breve", "overline"), ) def test_getlength_combine(mode: str, direction: str, text: str) -> None: @@ -272,27 +272,27 @@ def test_anchor_ttb(anchor: str) -> None: combine_tests = ( # extends above (e.g. issue #4553) - ("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08), - ("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08), - ("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08), - ("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08), - ("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3), - ("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3), + ("caron", "a\u030c\u030c\u030c\u030c\u030cb", None, None, 0.08), + ("caron_la", "a\u030c\u030c\u030c\u030c\u030cb", "la", None, 0.08), + ("caron_lt", "a\u030c\u030c\u030c\u030c\u030cb", "lt", None, 0.08), + ("caron_ls", "a\u030c\u030c\u030c\u030c\u030cb", "ls", None, 0.08), + ("caron_ttb", "ca" + ("\u030c" * 15) + "b", None, "ttb", 0.3), + ("caron_ttb_lt", "ca" + ("\u030c" * 15) + "b", "lt", "ttb", 0.3), # extends below - ("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02), - ("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02), - ("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02), - ("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02), - ("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03), - ("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03), + ("caron_below", "a\u032c\u032c\u032c\u032c\u032cb", None, None, 0.02), + ("caron_below_ld", "a\u032c\u032c\u032c\u032c\u032cb", "ld", None, 0.02), + ("caron_below_lb", "a\u032c\u032c\u032c\u032c\u032cb", "lb", None, 0.02), + ("caron_below_ls", "a\u032c\u032c\u032c\u032c\u032cb", "ls", None, 0.02), + ("caron_below_ttb", "a" + ("\u032c" * 15) + "b", None, "ttb", 0.03), + ("caron_below_ttb_lb", "a" + ("\u032c" * 15) + "b", "lb", "ttb", 0.03), # extends to the right (e.g. issue #3745) - ("double_breve_below", "a\u035Ci", None, None, 0.02), - ("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02), - ("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02), - ("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02), - ("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02), - ("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02), - ("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02), + ("double_breve_below", "a\u035ci", None, None, 0.02), + ("double_breve_below_ma", "a\u035ci", "ma", None, 0.02), + ("double_breve_below_ra", "a\u035ci", "ra", None, 0.02), + ("double_breve_below_ttb", "a\u035cb", None, "ttb", 0.02), + ("double_breve_below_ttb_rt", "a\u035cb", "rt", "ttb", 0.02), + ("double_breve_below_ttb_mt", "a\u035cb", "mt", "ttb", 0.02), + ("double_breve_below_ttb_st", "a\u035cb", "st", "ttb", 0.02), # extends to the left (fail=0.064) ("overline", "i\u0305", None, None, 0.02), ("overline_la", "i\u0305", "la", None, 0.02), @@ -346,7 +346,7 @@ def test_combine_multiline(anchor: str, align: str) -> None: path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) - text = "i\u0305\u035C\ntext" # i with overline and double breve, and a word + text = "i\u0305\u035c\ntext" # i with overline and double breve, and a word im = Image.new("RGB", (400, 400), "white") d = ImageDraw.Draw(im) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 6cf0079dd..6d0e6f36f 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -189,7 +189,7 @@ def test_2bit_palette(tmp_path: Path) -> None: rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2 img = Image.frombytes("P", (6, 1), rgb) - img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB + img.putpalette(b"\xff\x00\x00\x00\xff\x00\x00\x00\xff") # RGB img.save(outfile, format="PNG") assert_image_equal_tofile(img, outfile) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 76bdf1e5f..1b1ee6bac 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -79,7 +79,7 @@ def test_path_constructors( ), ) def test_invalid_path_constructors( - coords: tuple[str, str] | Sequence[Sequence[int]] + coords: tuple[str, str] | Sequence[Sequence[int]], ) -> None: # Act with pytest.raises(ValueError) as e: diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index f6b12cb20..d85fb1212 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -20,10 +20,10 @@ from PIL.PdfParser import ( def test_text_encode_decode() -> None: - assert encode_text("abc") == b"\xFE\xFF\x00a\x00b\x00c" - assert decode_text(b"\xFE\xFF\x00a\x00b\x00c") == "abc" + assert encode_text("abc") == b"\xfe\xff\x00a\x00b\x00c" + assert decode_text(b"\xfe\xff\x00a\x00b\x00c") == "abc" assert decode_text(b"abc") == "abc" - assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD" + assert decode_text(b"\x1b a \x1c") == "\u02d9 a \u02dd" def test_indirect_refs() -> None: @@ -45,8 +45,8 @@ def test_parsing() -> None: assert PdfParser.get_value(b"false%", 0) == (False, 5) assert PdfParser.get_value(b"null<", 0) == (None, 4) assert PdfParser.get_value(b"%cmt\n %cmt\n 123\n", 0) == (123, 15) - assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1F\xA3", 8) - assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1F\xA0", 17) + assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1f\xa3", 8) + assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1f\xa0", 17) assert PdfParser.get_value(b"(asd)", 0) == (b"asd", 5) assert PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0) == (b"asd(qwe)zxc", 13) assert PdfParser.get_value(b"(Two \\\nwords.)", 0) == (b"Two words.", 14) @@ -56,9 +56,9 @@ def test_parsing() -> None: assert PdfParser.get_value(b"(One\\(paren).", 0) == (b"One(paren", 12) assert PdfParser.get_value(b"(One\\)paren).", 0) == (b"One)paren", 12) assert PdfParser.get_value(b"(\\0053)", 0) == (b"\x053", 7) - assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2B", 6) - assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2B", 5) - assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2Ba", 6) + assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2b", 6) + assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2b", 5) + assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2ba", 6) assert PdfParser.get_value(b"(\\1111)", 0) == (b"\x491", 7) assert PdfParser.get_value(b" 123 (", 0) == (123, 4) assert round(abs(PdfParser.get_value(b" 123.4 %", 0)[0] - 123.4), 7) == 0 @@ -118,7 +118,7 @@ def test_pdf_repr() -> None: assert pdf_repr(None) == b"null" assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" - assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" + assert pdf_repr(PdfBinary(b"\x90\x1f\xa0")) == b"<901FA0>" def test_duplicate_xref_entry() -> None: diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index b4215a0b1..2a26d0b29 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -145,7 +145,7 @@ class ImImageFile(ImageFile.ImageFile): if s == b"\r": continue - if not s or s == b"\0" or s == b"\x1A": + if not s or s == b"\0" or s == b"\x1a": break # FIXME: this may read whole file if not a text file @@ -209,7 +209,7 @@ class ImImageFile(ImageFile.ImageFile): self._mode = self.info[MODE] # Skip forward to start of image data - while s and s[:1] != b"\x1A": + while s and s[:1] != b"\x1a": s = self.fp.read(1) if not s: msg = "File truncated" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 99b1b9ab3..e723b6a2e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -514,7 +514,7 @@ class ImagePointTransform: def _getscaleoffset( - expr: Callable[[ImagePointTransform], ImagePointTransform | float] + expr: Callable[[ImagePointTransform], ImagePointTransform | float], ) -> tuple[float, float]: a = expr(ImagePointTransform(1, 0)) return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) @@ -3884,7 +3884,7 @@ class Exif(_ExifBase): return self._fixup_dict(dict(info)) def _get_head(self) -> bytes: - version = b"\x2B" if self.bigtiff else b"\x2A" + version = b"\x2b" if self.bigtiff else b"\x2a" if self.endian == "<": head = b"II" + version + b"\x00" + o32le(8) else: diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 068cd5c33..c4eccee34 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -55,7 +55,7 @@ class ImtImageFile(ImageFile.ImageFile): if not s: break - if s == b"\x0C": + if s == b"\x0c": # image data begins self.tile = [ ImageFile._Tile( diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 457690aac..19639f634 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -325,7 +325,7 @@ MARKER = { def _accept(prefix: bytes) -> bool: # Magic number was taken from https://en.wikipedia.org/wiki/JPEG - return prefix[:3] == b"\xFF\xD8\xFF" + return prefix[:3] == b"\xff\xd8\xff" ## @@ -342,7 +342,7 @@ class JpegImageFile(ImageFile.ImageFile): if not _accept(s): msg = "not a JPEG file" raise SyntaxError(msg) - s = b"\xFF" + s = b"\xff" # Create attributes self.bits = self.layers = 0 @@ -417,7 +417,7 @@ class JpegImageFile(ImageFile.ImageFile): # Premature EOF. # Pretend file is finished adding EOI marker self._ended = True - return b"\xFF\xD9" + return b"\xff\xd9" return s @@ -712,7 +712,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def validate_qtables( qtables: ( str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None - ) + ), ) -> list[list[int]] | None: if qtables is None: return qtables @@ -769,7 +769,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: msg = "XMP data is too long" raise ValueError(msg) size = o16(2 + overhead_len + len(xmp)) - extra += b"\xFF\xE1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp + extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp icc_profile = info.get("icc_profile") if icc_profile: @@ -783,7 +783,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: for marker in markers: size = o16(2 + overhead_len + len(marker)) extra += ( - b"\xFF\xE2" + b"\xff\xe2" + size + b"ICC_PROFILE\0" + o8(i) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 71f89a09a..e08f80b6b 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -51,7 +51,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if not offsets: # APP2 marker im_frame.encoderinfo["extra"] = ( - b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 + b"\xff\xe2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 ) exif = im_frame.encoderinfo.get("exif") if isinstance(exif, Image.Exif): @@ -84,7 +84,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ifd[0xB002] = mpentries fp.seek(mpf_offset) - fp.write(b"II\x2A\x00" + o32le(8) + ifd.tobytes(8)) + fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8)) fp.seek(0, os.SEEK_END) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 32436cea3..299405ae0 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + o16(dpi[0]) + o16(dpi[1]) + b"\0" * 24 - + b"\xFF" * 24 + + b"\xff" * 24 + b"\0" + o8(planes) + o16(stride) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 7cb2d241b..41b38ebbf 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -19,14 +19,14 @@ def encode_text(s: str) -> bytes: PDFDocEncoding = { 0x16: "\u0017", - 0x18: "\u02D8", - 0x19: "\u02C7", - 0x1A: "\u02C6", - 0x1B: "\u02D9", - 0x1C: "\u02DD", - 0x1D: "\u02DB", - 0x1E: "\u02DA", - 0x1F: "\u02DC", + 0x18: "\u02d8", + 0x19: "\u02c7", + 0x1A: "\u02c6", + 0x1B: "\u02d9", + 0x1C: "\u02dd", + 0x1D: "\u02db", + 0x1E: "\u02da", + 0x1F: "\u02dc", 0x80: "\u2022", 0x81: "\u2020", 0x82: "\u2021", @@ -36,29 +36,29 @@ PDFDocEncoding = { 0x86: "\u0192", 0x87: "\u2044", 0x88: "\u2039", - 0x89: "\u203A", + 0x89: "\u203a", 0x8A: "\u2212", 0x8B: "\u2030", - 0x8C: "\u201E", - 0x8D: "\u201C", - 0x8E: "\u201D", + 0x8C: "\u201e", + 0x8D: "\u201c", + 0x8E: "\u201d", 0x8F: "\u2018", 0x90: "\u2019", - 0x91: "\u201A", + 0x91: "\u201a", 0x92: "\u2122", - 0x93: "\uFB01", - 0x94: "\uFB02", + 0x93: "\ufb01", + 0x94: "\ufb02", 0x95: "\u0141", 0x96: "\u0152", 0x97: "\u0160", 0x98: "\u0178", - 0x99: "\u017D", + 0x99: "\u017d", 0x9A: "\u0131", 0x9B: "\u0142", 0x9C: "\u0153", 0x9D: "\u0161", - 0x9E: "\u017E", - 0xA0: "\u20AC", + 0x9E: "\u017e", + 0xA0: "\u20ac", } diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index f56555160..5ea87686d 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1433,7 +1433,7 @@ def _save( chunk(fp, b"tRNS", transparency[:alpha_bytes]) else: transparency = max(0, min(255, transparency)) - alpha = b"\xFF" * transparency + b"\0" + alpha = b"\xff" * transparency + b"\0" chunk(fp, b"tRNS", alpha[:alpha_bytes]) elif im.mode in ("1", "L", "I", "I;16"): transparency = max(0, min(65535, transparency)) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 4e779df17..fb228f572 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -230,7 +230,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): msg = b"Invalid token for this mode: %s" % bytes([token]) raise ValueError(msg) data = (data + tokens)[:total_bytes] - invert = bytes.maketrans(b"01", b"\xFF\x00") + invert = bytes.maketrans(b"01", b"\xff\x00") return data.translate(invert) def _decode_blocks(self, maxval: int) -> bytearray: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index f49c09822..f557d104b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -275,12 +275,12 @@ OPEN_INFO = { MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) PREFIXES = [ - b"MM\x00\x2A", # Valid TIFF header with big-endian byte order - b"II\x2A\x00", # Valid TIFF header with little-endian byte order - b"MM\x2A\x00", # Invalid TIFF header, assume big-endian - b"II\x00\x2A", # Invalid TIFF header, assume little-endian - b"MM\x00\x2B", # BigTIFF with big-endian byte order - b"II\x2B\x00", # BigTIFF with little-endian byte order + b"MM\x00\x2a", # Valid TIFF header with big-endian byte order + b"II\x2a\x00", # Valid TIFF header with little-endian byte order + b"MM\x2a\x00", # Invalid TIFF header, assume big-endian + b"II\x00\x2a", # Invalid TIFF header, assume little-endian + b"MM\x00\x2b", # BigTIFF with big-endian byte order + b"II\x2b\x00", # BigTIFF with little-endian byte order ] if not getattr(Image.core, "libtiff_support_custom_tags", True): @@ -582,7 +582,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def __init__( self, - ifh: bytes = b"II\x2A\x00\x00\x00\x00\x00", + ifh: bytes = b"II\x2a\x00\x00\x00\x00\x00", prefix: bytes | None = None, group: int | None = None, ) -> None: @@ -2047,7 +2047,7 @@ class AppendingTiffWriter(io.BytesIO): self.offsetOfNewPage = 0 self.IIMM = iimm = self.f.read(4) - self._bigtiff = b"\x2B" in iimm + self._bigtiff = b"\x2b" in iimm if not iimm: # empty file - first page self.isFirst = True diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index beddfb062..9c0143003 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -1,5 +1,4 @@ -""" Find compiled module linking to Tcl / Tk libraries -""" +"""Find compiled module linking to Tcl / Tk libraries""" from __future__ import annotations From 00790e925dc007a67eb166c69ec87e48678e28b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 4 Feb 2025 06:49:46 +1100 Subject: [PATCH 597/652] Updated comment --- 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 91bf3cf74..a2481c336 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -934,7 +934,7 @@ class TestFileJpeg: def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None: size = 4097 - buffer = BytesIO(b"\xff" * size) # Many xFF bytes + buffer = BytesIO(b"\xff" * size) # Many xff bytes max_pos = 0 orig_read = buffer.read From a7d7a1080ed2e507613201a9f837b40227247ff7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Feb 2025 18:42:35 +1100 Subject: [PATCH 598/652] Removed redundant argument parsing --- src/_imaging.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 2fd2deffb..9ce4b34aa 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1013,10 +1013,6 @@ _convert_transparent(ImagingObject *self, PyObject *args) { static PyObject * _copy(ImagingObject *self, PyObject *args) { - if (!PyArg_ParseTuple(args, "")) { - return NULL; - } - return PyImagingNew(ImagingCopy(self->image)); } From b19506a4993b9003809c711b50fd0e82cba1bbd9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Feb 2025 19:12:50 +1100 Subject: [PATCH 599/652] Simplify Python code by passing tuples to C --- Tests/test_color_lut.py | 20 +++++++++----------- src/PIL/ImageFilter.py | 4 +--- src/PIL/ImageFont.py | 3 +-- src/PIL/JpegImagePlugin.py | 3 +-- src/PIL/WebPImagePlugin.py | 3 +-- src/PIL/_imagingft.pyi | 3 +-- src/_imaging.c | 2 +- src/_imagingft.c | 2 +- src/_webp.c | 2 +- src/encode.c | 2 +- 10 files changed, 18 insertions(+), 26 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index baa899df5..26945ae1a 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -19,7 +19,7 @@ except ImportError: class TestColorLut3DCoreAPI: def generate_identity_table( self, channels: int, size: int | tuple[int, int, int] - ) -> tuple[int, int, int, int, list[float]]: + ) -> tuple[int, tuple[int, int, int], list[float]]: if isinstance(size, tuple): size_1d, size_2d, size_3d = size else: @@ -39,9 +39,7 @@ class TestColorLut3DCoreAPI: ] return ( channels, - size_1d, - size_2d, - size_3d, + (size_1d, size_2d, size_3d), [item for sublist in table for item in sublist], ) @@ -89,21 +87,21 @@ class TestColorLut3DCoreAPI: with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7 + "RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, 0] * 7 ) with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9 + "RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, 0] * 9 ) with pytest.raises(TypeError): im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8 + "RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, "0"] * 8 ) with pytest.raises(TypeError): - im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) + im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), 16) @pytest.mark.parametrize( "lut_mode, table_channels, table_size", @@ -264,7 +262,7 @@ class TestColorLut3DCoreAPI: assert_image_equal( Image.merge('RGB', im.split()[::-1]), im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, - 3, 2, 2, 2, [ + 3, (2, 2, 2), [ 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, @@ -286,7 +284,7 @@ class TestColorLut3DCoreAPI: # fmt: off transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, - 3, 2, 2, 2, + 3, (2, 2, 2), [ -1, -1, -1, 2, -1, -1, -1, 2, -1, 2, 2, -1, @@ -307,7 +305,7 @@ class TestColorLut3DCoreAPI: # fmt: off transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, - 3, 2, 2, 2, + 3, (2, 2, 2), [ -3, -3, -3, 5, -3, -3, -3, 5, -3, 5, 5, -3, diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index b350e56f4..1c8b29b11 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -598,8 +598,6 @@ class Color3DLUT(MultibandFilter): self.mode or image.mode, Image.Resampling.BILINEAR, self.channels, - self.size[0], - self.size[1], - self.size[2], + self.size, self.table, ) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a4986aa8c..c8f05fbb7 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -647,8 +647,7 @@ class FreeTypeFont: kwargs.get("stroke_filled", False), anchor, ink, - start[0], - start[1], + start, ) def font_variant( diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 19639f634..a1c9c443a 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -816,8 +816,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: optimize, info.get("keep_rgb", False), info.get("streamtype", 0), - dpi[0], - dpi[1], + dpi, subsampling, info.get("restart_marker_blocks", 0), info.get("restart_marker_rows", 0), diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index c7f855527..066fe551f 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -223,8 +223,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Setup the WebP animation encoder enc = _webp.WebPAnimEncoder( - im.size[0], - im.size[1], + im.size, background, loop, minimize_size, diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 813294747..1cb1429d6 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -31,8 +31,7 @@ class Font: stroke_filled: bool, anchor: str | None, foreground_ink_long: int, - x_start: float, - y_start: float, + start: tuple[float, float], /, ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... def getsize( diff --git a/src/_imaging.c b/src/_imaging.c index 2fd2deffb..975c700dc 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -866,7 +866,7 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "siiiiiO:color_lut_3d", + "sii(iii)O:color_lut_3d", &mode, &filter, &table_channels, diff --git a/src/_imagingft.c b/src/_imagingft.c index c202a8059..2aa425e32 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -854,7 +854,7 @@ font_render(FontObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "OO|zzOzfpzLffO:render", + "OO|zzOzfpzL(ff)O:render", &string, &fill, &mode, diff --git a/src/_webp.c b/src/_webp.c index dfda7048d..308f031e0 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -164,7 +164,7 @@ _anim_encoder_new(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "iiIiiiiii", + "(ii)Iiiiiii", &width, &height, &bgcolor, diff --git a/src/encode.c b/src/encode.c index 0bf5e63c5..74dd4a3fd 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1097,7 +1097,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnpnnnnnnOz#y#y#", + "ss|nnnnpn(nn)nnnOz#y#y#", &mode, &rawmode, &quality, From a37702dd8a02aff7a16d8d0a4d3b94279737be34 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Feb 2025 18:36:14 +1100 Subject: [PATCH 600/652] Removed unused format character --- src/_imagingft.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 2aa425e32..a668ac411 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -854,7 +854,7 @@ font_render(FontObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "OO|zzOzfpzL(ff)O:render", + "OO|zzOzfpzL(ff):render", &string, &fill, &mode, From 7924b6a11f37902c8a1a080741384bf79fbd8905 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Feb 2025 20:20:57 +1100 Subject: [PATCH 601/652] Use member names to initialize modules --- src/_imaging.c | 7 +++---- src/_imagingcms.c | 7 +++---- src/_imagingft.c | 7 +++---- src/_imagingmath.c | 7 +++---- src/_imagingmorph.c | 8 ++++---- src/_imagingtk.c | 7 +++---- src/_webp.c | 7 +++---- 7 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 2fd2deffb..cd9bde273 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4439,10 +4439,9 @@ PyInit__imaging(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imaging", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - functions, /* m_methods */ + .m_name = "_imaging", + .m_size = -1, + .m_methods = functions, }; m = PyModule_Create(&module_def); diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 14cf2acd2..6037e8bc4 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1520,10 +1520,9 @@ PyInit__imagingcms(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingcms", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - pyCMSdll_methods, /* m_methods */ + .m_name = "_imagingcms", + .m_size = -1, + .m_methods = pyCMSdll_methods, }; m = PyModule_Create(&module_def); diff --git a/src/_imagingft.c b/src/_imagingft.c index c202a8059..ab3bc8dba 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1630,10 +1630,9 @@ PyInit__imagingft(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingft", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - _functions, /* m_methods */ + .m_name = "_imagingft", + .m_size = -1, + .m_methods = _functions, }; m = PyModule_Create(&module_def); diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 75b3716b5..4b9bf08ba 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -308,10 +308,9 @@ PyInit__imagingmath(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingmath", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - _functions, /* m_methods */ + .m_name = "_imagingmath", + .m_size = -1, + .m_methods = _functions, }; m = PyModule_Create(&module_def); diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index b763e3a6f..a20888294 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -252,10 +252,10 @@ PyInit__imagingmorph(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingmorph", /* m_name */ - "A module for doing image morphology", /* m_doc */ - -1, /* m_size */ - functions, /* m_methods */ + .m_name = "_imagingmorph", + .m_doc = "A module for doing image morphology", + .m_size = -1, + .m_methods = functions, }; m = PyModule_Create(&module_def); diff --git a/src/_imagingtk.c b/src/_imagingtk.c index c44482651..4e06fe9b8 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -50,10 +50,9 @@ PyMODINIT_FUNC PyInit__imagingtk(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingtk", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - functions, /* m_methods */ + .m_name = "_imagingtk", + .m_size = -1, + .m_methods = functions, }; PyObject *m; m = PyModule_Create(&module_def); diff --git a/src/_webp.c b/src/_webp.c index dfda7048d..ded9f8ca2 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -835,10 +835,9 @@ PyInit__webp(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_webp", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - webpMethods, /* m_methods */ + .m_name = "_webp", + .m_size = -1, + .m_methods = webpMethods, }; m = PyModule_Create(&module_def); From 41861e8e9ffb968945ff6acba422d31f0d69220b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 6 Feb 2025 19:26:49 +1100 Subject: [PATCH 602/652] Updated AffineTransform docstring to mention it uses the inverse matrix --- src/PIL/ImageTransform.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index a3d8f441a..fb144ff38 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -48,9 +48,9 @@ class AffineTransform(Transform): Define an affine image transform. This function takes a 6-tuple (a, b, c, d, e, f) which contain the first - two rows from an affine transform matrix. For each pixel (x, y) in the - output image, the new value is taken from a position (a x + b y + c, - d x + e y + f) in the input image, rounded to nearest pixel. + two rows from the inverse of an affine transform matrix. For each pixel + (x, y) in the output image, the new value is taken from a position (a x + + b y + c, d x + e y + f) in the input image, rounded to nearest pixel. This function can be used to scale, translate, rotate, and shear the original image. @@ -58,7 +58,7 @@ class AffineTransform(Transform): See :py:meth:`.Image.transform` :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows - from an affine transform matrix. + from the inverse of an affine transform matrix. """ method = Image.Transform.AFFINE From 1b0095fad45db67c723215e1f9235f839b2637d9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Feb 2025 17:23:41 +1100 Subject: [PATCH 603/652] Pass CFLAGS to build_simple directly --- .github/workflows/wheels-dependencies.sh | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index dffb36085..1dd8d5660 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -54,13 +54,10 @@ BROTLI_VERSION=1.1.0 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi # This essentially duplicates the Homebrew recipe - ORIGINAL_CFLAGS=$CFLAGS - CFLAGS="$CFLAGS -Wno-int-conversion" - build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ + CFLAGS="$CFLAGS -Wno-int-conversion" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ --disable-debug --disable-host-tool --with-internal-glib \ --with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \ --with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include - CFLAGS=$ORIGINAL_CFLAGS export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config touch pkg-config-stamp } @@ -130,15 +127,13 @@ function build { build_lcms2 build_openjpeg - ORIGINAL_CFLAGS=$CFLAGS - CFLAGS="$CFLAGS -O3 -DNDEBUG" + webp_cflags="-O3 -DNDEBUG" if [[ -n "$IS_MACOS" ]]; then - CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" + webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names" fi - build_simple libwebp $LIBWEBP_VERSION \ + CFLAGS="$CFLAGS $webp_cflags" build_simple libwebp $LIBWEBP_VERSION \ https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \ --enable-libwebpmux --enable-libwebpdemux - CFLAGS=$ORIGINAL_CFLAGS build_brotli From 166d0b94d938c97ba06f2718a8772d1f8d88ac60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Feb 2025 21:00:54 +1100 Subject: [PATCH 604/652] Use boolean format argument for irreversible --- src/encode.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/encode.c b/src/encode.c index 74dd4a3fd..2a9fd3805 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1253,7 +1253,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { PyObject *quality_layers = NULL; Py_ssize_t num_resolutions = 0; PyObject *cblk_size = NULL, *precinct_size = NULL; - PyObject *irreversible = NULL; + int irreversible = 0; char *progression = "LRCP"; OPJ_PROG_ORDER prog_order; char *cinema_mode = "no"; @@ -1267,7 +1267,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbbnz#p", + "ss|OOOsOnOOpssbbnz#p", &mode, &format, &offset, @@ -1402,7 +1402,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { precinct_size, &context->precinct_width, &context->precinct_height ); - context->irreversible = PyObject_IsTrue(irreversible); + context->irreversible = irreversible; context->progression = prog_order; context->cinema_mode = cine_mode; context->mct = mct; From b59dea60a6a7c83545f83f9e1f723c1a40f3f7cb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Feb 2025 21:07:25 +1100 Subject: [PATCH 605/652] Simplify Python code by receiving tuple from C --- src/PIL/WebPImagePlugin.py | 3 +-- src/_webp.c | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 066fe551f..cbbc24af0 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -46,8 +46,7 @@ class WebPImageFile(ImageFile.ImageFile): self._decoder = _webp.WebPAnimDecoder(self.fp.read()) # Get info from decoder - width, height, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() - self._size = width, height + self._size, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() self.info["loop"] = loop_count bg_a, bg_r, bg_g, bg_b = ( (bgcolor >> 24) & 0xFF, diff --git a/src/_webp.c b/src/_webp.c index 26a5ebbc6..48b1c0a74 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -449,7 +449,7 @@ _anim_decoder_get_info(PyObject *self) { WebPAnimInfo *info = &(decp->info); return Py_BuildValue( - "IIIIIs", + "(II)IIIs", info->canvas_width, info->canvas_height, info->loop_count, From bfa2d64e0e41285d7cbc1016eb98b56b51255575 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 9 Feb 2025 16:02:50 +1100 Subject: [PATCH 606/652] Use member names to initialize PyTypeObjects --- src/_imaging.c | 125 ++++++++-------------------------------------- src/_imagingcms.c | 71 ++++---------------------- src/_imagingft.c | 36 +++---------- src/_webp.c | 70 ++++---------------------- src/decode.c | 36 +++---------- src/display.c | 36 +++---------- src/encode.c | 36 +++---------- src/outline.c | 35 ++----------- src/path.c | 38 +++----------- 9 files changed, 79 insertions(+), 404 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index ee373e964..6482bcc5e 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3769,102 +3769,29 @@ static PySequenceMethods image_as_sequence = { /* type description */ static PyTypeObject Imaging_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingCore", /*tp_name*/ - sizeof(ImagingObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - &image_as_sequence, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getsetters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingCore", + .tp_basicsize = sizeof(ImagingObject), + .tp_dealloc = (destructor)_dealloc, + .tp_as_sequence = &image_as_sequence, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = methods, + .tp_getset = getsetters, }; static PyTypeObject ImagingFont_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingFont", /*tp_name*/ - sizeof(ImagingFontObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_font_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _font_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingFont", + .tp_basicsize = sizeof(ImagingFontObject), + .tp_dealloc = (destructor)_font_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _font_methods, }; static PyTypeObject ImagingDraw_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingDraw", /*tp_name*/ - sizeof(ImagingDrawObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_draw_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _draw_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDraw", + .tp_basicsize = sizeof(ImagingDrawObject), + .tp_dealloc = (destructor)_draw_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _draw_methods, }; static PyMappingMethods pixel_access_as_mapping = { @@ -3876,20 +3803,10 @@ static PyMappingMethods pixel_access_as_mapping = { /* type description */ static PyTypeObject PixelAccess_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PixelAccess", /*tp_name*/ - sizeof(PixelAccessObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)pixel_access_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - &pixel_access_as_mapping, /*tp_as_mapping*/ - 0 /*tp_hash*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PixelAccess", + .tp_basicsize = sizeof(PixelAccessObject), + .tp_dealloc = (destructor)pixel_access_dealloc, + .tp_as_mapping = &pixel_access_as_mapping, }; /* -------------------------------------------------------------------- */ diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 6037e8bc4..e177feee9 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1410,36 +1410,12 @@ static struct PyGetSetDef cms_profile_getsetters[] = { }; static PyTypeObject CmsProfile_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PIL.ImageCms.core.CmsProfile", /*tp_name*/ - sizeof(CmsProfileObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)cms_profile_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - cms_profile_methods, /*tp_methods*/ - 0, /*tp_members*/ - cms_profile_getsetters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PIL.ImageCms.core.CmsProfile", + .tp_basicsize = sizeof(CmsProfileObject), + .tp_dealloc = (destructor)cms_profile_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = cms_profile_methods, + .tp_getset = cms_profile_getsetters, }; static struct PyMethodDef cms_transform_methods[] = { @@ -1447,36 +1423,11 @@ static struct PyMethodDef cms_transform_methods[] = { }; static PyTypeObject CmsTransform_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PIL.ImageCms.core.CmsTransform", /*tp_name*/ - sizeof(CmsTransformObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)cms_transform_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - cms_transform_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PIL.ImageCms.core.CmsTransform", + .tp_basicsize = sizeof(CmsTransformObject), + .tp_dealloc = (destructor)cms_transform_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = cms_transform_methods, }; static int diff --git a/src/_imagingft.c b/src/_imagingft.c index 7d754e168..922c3da32 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1518,36 +1518,12 @@ static struct PyGetSetDef font_getsetters[] = { }; static PyTypeObject Font_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "Font", /*tp_name*/ - sizeof(FontObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)font_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - font_methods, /*tp_methods*/ - 0, /*tp_members*/ - font_getsetters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Font", + .tp_basicsize = sizeof(FontObject), + .tp_dealloc = (destructor)font_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = font_methods, + .tp_getset = font_getsetters, }; static PyMethodDef _functions[] = { diff --git a/src/_webp.c b/src/_webp.c index 26a5ebbc6..942f275da 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -530,36 +530,11 @@ static struct PyMethodDef _anim_encoder_methods[] = { // WebPAnimEncoder type definition static PyTypeObject WebPAnimEncoder_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimEncoder", /*tp_name */ - sizeof(WebPAnimEncoderObject), /*tp_basicsize */ - 0, /*tp_itemsize */ - /* methods */ - (destructor)_anim_encoder_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _anim_encoder_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "WebPAnimEncoder", + .tp_basicsize = sizeof(WebPAnimEncoderObject), + .tp_dealloc = (destructor)_anim_encoder_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _anim_encoder_methods, }; // WebPAnimDecoder methods @@ -573,36 +548,11 @@ static struct PyMethodDef _anim_decoder_methods[] = { // WebPAnimDecoder type definition static PyTypeObject WebPAnimDecoder_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimDecoder", /*tp_name */ - sizeof(WebPAnimDecoderObject), /*tp_basicsize */ - 0, /*tp_itemsize */ - /* methods */ - (destructor)_anim_decoder_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _anim_decoder_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "WebPAnimDecoder", + .tp_basicsize = sizeof(WebPAnimDecoderObject), + .tp_dealloc = (destructor)_anim_decoder_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _anim_decoder_methods, }; /* -------------------------------------------------------------------- */ diff --git a/src/decode.c b/src/decode.c index 1f2c22491..26211a95f 100644 --- a/src/decode.c +++ b/src/decode.c @@ -256,36 +256,12 @@ static struct PyGetSetDef getseters[] = { }; static PyTypeObject ImagingDecoderType = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingDecoder", /*tp_name*/ - sizeof(ImagingDecoderObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getseters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDecoder", + .tp_basicsize = sizeof(ImagingDecoderObject), + .tp_dealloc = (destructor)_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = methods, + .tp_getset = getseters, }; /* -------------------------------------------------------------------- */ diff --git a/src/display.c b/src/display.c index 36ab3b237..004c7866b 100644 --- a/src/display.c +++ b/src/display.c @@ -248,36 +248,12 @@ static struct PyGetSetDef getsetters[] = { }; static PyTypeObject ImagingDisplayType = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingDisplay", /*tp_name*/ - sizeof(ImagingDisplayObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_delete, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getsetters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDisplay", + .tp_basicsize = sizeof(ImagingDisplayObject), + .tp_dealloc = (destructor)_delete, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = methods, + .tp_getset = getsetters, }; PyObject * diff --git a/src/encode.c b/src/encode.c index 74dd4a3fd..dd7355811 100644 --- a/src/encode.c +++ b/src/encode.c @@ -323,36 +323,12 @@ static struct PyGetSetDef getseters[] = { }; static PyTypeObject ImagingEncoderType = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingEncoder", /*tp_name*/ - sizeof(ImagingEncoderObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getseters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingEncoder", + .tp_basicsize = sizeof(ImagingEncoderObject), + .tp_dealloc = (destructor)_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = methods, + .tp_getset = getseters, }; /* -------------------------------------------------------------------- */ diff --git a/src/outline.c b/src/outline.c index 4aa6bd59e..6eea07c5d 100644 --- a/src/outline.c +++ b/src/outline.c @@ -149,34 +149,9 @@ static struct PyMethodDef _outline_methods[] = { }; static PyTypeObject OutlineType = { - PyVarObject_HEAD_INIT(NULL, 0) "Outline", /*tp_name*/ - sizeof(OutlineObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_outline_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _outline_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Outline", + .tp_basicsize = sizeof(OutlineObject), + .tp_dealloc = (destructor)_outline_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _outline_methods, }; diff --git a/src/path.c b/src/path.c index b508df2ac..24820173e 100644 --- a/src/path.c +++ b/src/path.c @@ -598,34 +598,12 @@ static PyMappingMethods path_as_mapping = { }; static PyTypeObject PyPathType = { - PyVarObject_HEAD_INIT(NULL, 0) "Path", /*tp_name*/ - sizeof(PyPathObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)path_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - &path_as_sequence, /*tp_as_sequence*/ - &path_as_mapping, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getsetters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Path", + .tp_basicsize = sizeof(PyPathObject), + .tp_dealloc = (destructor)path_dealloc, + .tp_as_sequence = &path_as_sequence, + .tp_as_mapping = &path_as_mapping, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = methods, + .tp_getset = getsetters, }; From 422c0f607d04470729768c3204273894c9be9e46 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 9 Feb 2025 16:03:38 +1100 Subject: [PATCH 607/652] Use default tp_flags --- src/_imaging.c | 3 --- src/_imagingcms.c | 2 -- src/_imagingft.c | 1 - src/_webp.c | 2 -- src/decode.c | 1 - src/display.c | 1 - src/encode.c | 1 - src/outline.c | 1 - src/path.c | 1 - 9 files changed, 13 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 6482bcc5e..d5c21fd86 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3773,7 +3773,6 @@ static PyTypeObject Imaging_Type = { .tp_basicsize = sizeof(ImagingObject), .tp_dealloc = (destructor)_dealloc, .tp_as_sequence = &image_as_sequence, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = methods, .tp_getset = getsetters, }; @@ -3782,7 +3781,6 @@ static PyTypeObject ImagingFont_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingFont", .tp_basicsize = sizeof(ImagingFontObject), .tp_dealloc = (destructor)_font_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = _font_methods, }; @@ -3790,7 +3788,6 @@ static PyTypeObject ImagingDraw_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDraw", .tp_basicsize = sizeof(ImagingDrawObject), .tp_dealloc = (destructor)_draw_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = _draw_methods, }; diff --git a/src/_imagingcms.c b/src/_imagingcms.c index e177feee9..ea2f70186 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1413,7 +1413,6 @@ static PyTypeObject CmsProfile_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PIL.ImageCms.core.CmsProfile", .tp_basicsize = sizeof(CmsProfileObject), .tp_dealloc = (destructor)cms_profile_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = cms_profile_methods, .tp_getset = cms_profile_getsetters, }; @@ -1426,7 +1425,6 @@ static PyTypeObject CmsTransform_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PIL.ImageCms.core.CmsTransform", .tp_basicsize = sizeof(CmsTransformObject), .tp_dealloc = (destructor)cms_transform_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = cms_transform_methods, }; diff --git a/src/_imagingft.c b/src/_imagingft.c index 922c3da32..62dab73e5 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1521,7 +1521,6 @@ static PyTypeObject Font_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Font", .tp_basicsize = sizeof(FontObject), .tp_dealloc = (destructor)font_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = font_methods, .tp_getset = font_getsetters, }; diff --git a/src/_webp.c b/src/_webp.c index 942f275da..c280d9513 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -533,7 +533,6 @@ static PyTypeObject WebPAnimEncoder_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "WebPAnimEncoder", .tp_basicsize = sizeof(WebPAnimEncoderObject), .tp_dealloc = (destructor)_anim_encoder_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = _anim_encoder_methods, }; @@ -551,7 +550,6 @@ static PyTypeObject WebPAnimDecoder_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "WebPAnimDecoder", .tp_basicsize = sizeof(WebPAnimDecoderObject), .tp_dealloc = (destructor)_anim_decoder_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = _anim_decoder_methods, }; diff --git a/src/decode.c b/src/decode.c index 26211a95f..03db1ce35 100644 --- a/src/decode.c +++ b/src/decode.c @@ -259,7 +259,6 @@ static PyTypeObject ImagingDecoderType = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDecoder", .tp_basicsize = sizeof(ImagingDecoderObject), .tp_dealloc = (destructor)_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = methods, .tp_getset = getseters, }; diff --git a/src/display.c b/src/display.c index 004c7866b..a05387504 100644 --- a/src/display.c +++ b/src/display.c @@ -251,7 +251,6 @@ static PyTypeObject ImagingDisplayType = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDisplay", .tp_basicsize = sizeof(ImagingDisplayObject), .tp_dealloc = (destructor)_delete, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = methods, .tp_getset = getsetters, }; diff --git a/src/encode.c b/src/encode.c index dd7355811..f610d6638 100644 --- a/src/encode.c +++ b/src/encode.c @@ -326,7 +326,6 @@ static PyTypeObject ImagingEncoderType = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingEncoder", .tp_basicsize = sizeof(ImagingEncoderObject), .tp_dealloc = (destructor)_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = methods, .tp_getset = getseters, }; diff --git a/src/outline.c b/src/outline.c index 6eea07c5d..32ab9109c 100644 --- a/src/outline.c +++ b/src/outline.c @@ -152,6 +152,5 @@ static PyTypeObject OutlineType = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Outline", .tp_basicsize = sizeof(OutlineObject), .tp_dealloc = (destructor)_outline_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = _outline_methods, }; diff --git a/src/path.c b/src/path.c index 24820173e..5affe3a1f 100644 --- a/src/path.c +++ b/src/path.c @@ -603,7 +603,6 @@ static PyTypeObject PyPathType = { .tp_dealloc = (destructor)path_dealloc, .tp_as_sequence = &path_as_sequence, .tp_as_mapping = &path_as_mapping, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = methods, .tp_getset = getsetters, }; From c566a81c647834d5789ab0cc7680eb51effcddec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Feb 2025 21:47:37 +1100 Subject: [PATCH 608/652] Updated libimagequant to 4.3.4 --- winbuild/build_prepare.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 54b5d983f..0ea8f0f9f 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,6 +116,7 @@ V = { "HARFBUZZ": "10.2.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.16", + "LIBIMAGEQUANT": "4.3.4", "LIBPNG": "1.6.46", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", @@ -335,24 +336,15 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"bin\*.lib"], }, "libimagequant": { - # commit: Merge branch 'master' into msvc (matches 2.17.0 tag) - "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", - "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip", + "url": "https://github.com/ImageOptim/libimagequant/archive/{V['LIBIMAGEQUANT']}.tar.gz", + "filename": f"libimagequant-{V['LIBIMAGEQUANT']}.tar.gz", "license": "COPYRIGHT", - "patch": { - "CMakeLists.txt": { - "if(OPENMP_FOUND)": "if(false)", - "install": "#install", - # libimagequant does not detect MSVC x86_arm64 cross-compiler correctly - "if(${{CMAKE_SYSTEM_PROCESSOR}} STREQUAL ARM64)": "if({architecture} STREQUAL ARM64)", # noqa: E501 - } - }, "build": [ - *cmds_cmake("imagequant_a"), - cmd_copy("imagequant_a.lib", "imagequant.lib"), + cmd_cd("imagequant-sys"), + "cargo build --release", ], - "headers": [r"*.h"], - "libs": [r"imagequant.lib"], + "headers": ["libimagequant.h"], + "libs": [r"..\target\release\imagequant_sys.lib"], }, "harfbuzz": { "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", From 45d8d8056767988e0ea58a8676a5244d334b37b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 11 Feb 2025 11:36:55 +1100 Subject: [PATCH 609/652] Updated zlib-ng to 2.2.4 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index dffb36085..edf5ba937 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -45,7 +45,7 @@ OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.4 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 -ZLIB_NG_VERSION=2.2.3 +ZLIB_NG_VERSION=2.2.4 LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 54b5d983f..73e3699d7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ V = { "OPENJPEG": "2.5.3", "TIFF": "4.6.0", "XZ": "5.6.4", - "ZLIBNG": "2.2.3", + "ZLIBNG": "2.2.4", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From 8020d423bc42688af8fb83b70d951dbf9daf34ad Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 12 Feb 2025 18:36:14 +1100 Subject: [PATCH 610/652] Use monkeypatch --- Tests/test_file_gif.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 46215db1f..396e09ba9 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1345,7 +1345,7 @@ def test_save_I(tmp_path: Path) -> None: assert_image_equal(reloaded.convert("L"), im.convert("L")) -def test_getdata() -> None: +def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None: # Test getheader/getdata against legacy values. # Create a 'P' image with holes in the palette. im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) @@ -1354,23 +1354,21 @@ def test_getdata() -> None: passed_palette = bytes(255 - i // 3 for i in range(768)) - GifImagePlugin._FORCE_OPTIMIZE = True - try: - h = GifImagePlugin.getheader(im, passed_palette) - d = GifImagePlugin.getdata(im) + monkeypatch.setattr(GifImagePlugin, "_FORCE_OPTIMIZE", True) - import pickle + h = GifImagePlugin.getheader(im, passed_palette) + d = GifImagePlugin.getdata(im) - # Enable to get target values on pre-refactor version - # with open('Tests/images/gif_header_data.pkl', 'wb') as f: - # pickle.dump((h, d), f, 1) - with open("Tests/images/gif_header_data.pkl", "rb") as f: - (h_target, d_target) = pickle.load(f) + import pickle - assert h == h_target - assert d == d_target - finally: - GifImagePlugin._FORCE_OPTIMIZE = False + # Enable to get target values on pre-refactor version + # with open('Tests/images/gif_header_data.pkl', 'wb') as f: + # pickle.dump((h, d), f, 1) + with open("Tests/images/gif_header_data.pkl", "rb") as f: + (h_target, d_target) = pickle.load(f) + + assert h == h_target + assert d == d_target def test_lzw_bits() -> None: From 8f4bfe1fe5c782329314333eac6284f32ff84b7b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 12 Feb 2025 19:12:27 +1100 Subject: [PATCH 611/652] Only crop when saving with disposal method 2 if transparency is present --- Tests/test_file_gif.py | 15 +++++++++++++++ src/PIL/GifImagePlugin.py | 28 +++++++++++++++++----------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 46215db1f..2f0116434 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -762,6 +762,21 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None: assert im.getpixel((0, 0)) == (0, 0, 0, 255) +def test_dispose2_without_transparency(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("P", (100, 100)) + + im2 = Image.new("P", (100, 100), (0, 0, 0)) + im2.putpixel((50, 50), (255, 0, 0)) + + im.save(out, save_all=True, append_images=[im2], disposal=2) + + with Image.open(out) as reloaded: + reloaded.seek(1) + assert reloaded.tile[0].extents == (0, 0, 100, 100) + + def test_transparency_in_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/different_transparency.gif") as im: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 47022d584..ff7262efc 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -689,16 +689,21 @@ def _write_multiple_frames( im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"] continue if im_frames[-1].encoderinfo.get("disposal") == 2: - if background_im is None: - color = im.encoderinfo.get( - "transparency", im.info.get("transparency", (0, 0, 0)) - ) - background = _get_background(im_frame, color) - background_im = Image.new("P", im_frame.size, background) - first_palette = im_frames[0].im.palette - assert first_palette is not None - background_im.putpalette(first_palette, first_palette.mode) - bbox = _getbbox(background_im, im_frame)[1] + # To appear correctly in viewers using a convention, + # only consider transparency, and not background color + color = im.encoderinfo.get( + "transparency", im.info.get("transparency") + ) + if color is not None: + if background_im is None: + background = _get_background(im_frame, color) + background_im = Image.new("P", im_frame.size, background) + first_palette = im_frames[0].im.palette + assert first_palette is not None + background_im.putpalette(first_palette, first_palette.mode) + bbox = _getbbox(background_im, im_frame)[1] + else: + bbox = (0, 0) + im_frame.size elif encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: assert im_frame.palette is not None @@ -764,7 +769,8 @@ def _write_multiple_frames( if not palette: frame_data.encoderinfo["include_color_table"] = True - im_frame = im_frame.crop(frame_data.bbox) + if frame_data.bbox != (0, 0) + im_frame.size: + im_frame = im_frame.crop(frame_data.bbox) offset = frame_data.bbox[:2] _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo) return True From ad6c4f82f3d85df5e51d99916fd68f0d8b180244 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Feb 2025 09:27:16 +1100 Subject: [PATCH 612/652] Updated lcms2 to 2.17 --- .github/workflows/wheels-dependencies.sh | 2 +- docs/installation/building-from-source.rst | 2 +- winbuild/build_prepare.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index edf5ba937..155e5fb13 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -44,7 +44,7 @@ JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.4 TIFF_VERSION=4.6.0 -LCMS2_VERSION=2.16 +LCMS2_VERSION=2.17 ZLIB_NG_VERSION=2.2.4 LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 46a4c1245..b400a3436 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -51,7 +51,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.16**. + above uses liblcms2. Tested with **1.19** and **2.7-2.17**. * **libwebp** provides the WebP format. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f942716cb..e3509aee6 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -115,7 +115,7 @@ V = { "FRIBIDI": "1.0.16", "HARFBUZZ": "10.2.0", "JPEGTURBO": "3.1.0", - "LCMS2": "2.16", + "LCMS2": "2.17", "LIBIMAGEQUANT": "4.3.4", "LIBPNG": "1.6.46", "LIBWEBP": "1.5.0", From 9f0398ef3239cacf0f0250305f4ccb1db9f6c738 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Feb 2025 21:07:43 +1100 Subject: [PATCH 613/652] Removed unused code --- Tests/helper.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index e7b0db1d6..764935f87 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -9,7 +9,6 @@ import os import shutil import subprocess import sys -import sysconfig import tempfile from collections.abc import Sequence from functools import lru_cache @@ -342,10 +341,6 @@ def is_pypy() -> bool: return hasattr(sys, "pypy_translation_info") -def is_mingw() -> bool: - return sysconfig.get_platform() == "mingw" - - class CachedProperty: def __init__(self, func: Callable[[Any], Any]) -> None: self.func = func From 1c18d29c34789c802e2cfc73d841019bc5f06ca1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 13:26:06 +0200 Subject: [PATCH 614/652] Remove unused bdf_slant and bdf_spacing variables --- src/PIL/BdfFontFile.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index bc1416c74..bfd66aa6a 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -26,17 +26,6 @@ from typing import BinaryIO from . import FontFile, Image -bdf_slant = { - "R": "Roman", - "I": "Italic", - "O": "Oblique", - "RI": "Reverse Italic", - "RO": "Reverse Oblique", - "OT": "Other", -} - -bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"} - def bdf_char( f: BinaryIO, From 8261348fff5b5a653767c348a44a01d98238a190 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:27:52 +0200 Subject: [PATCH 615/652] Don't call pip in tox --- tox.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tox.ini b/tox.ini index e79d88500..4065245ee 100644 --- a/tox.ini +++ b/tox.ini @@ -11,12 +11,8 @@ deps = extras = tests commands = - make clean - {envpython} -m pip install . {envpython} selftest.py {envpython} -m pytest -W always {posargs} -allowlist_externals = - make [testenv:lint] skip_install = true From ff960b884188855c4a8afb2a2724674d80b31e04 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:23:59 +0200 Subject: [PATCH 616/652] Remove debug Image._wedge --- Tests/test_file_gif.py | 2 +- Tests/test_format_hsv.py | 28 +++++++++++++--------------- src/PIL/Image.py | 9 --------- src/_imaging.c | 1 - 4 files changed, 14 insertions(+), 26 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 396e09ba9..974aedeb6 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1348,7 +1348,7 @@ def test_save_I(tmp_path: Path) -> None: def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None: # Test getheader/getdata against legacy values. # Create a 'P' image with holes in the palette. - im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) + im = Image.linear_gradient(mode="L").resize((16, 16), Image.Resampling.NEAREST) im.putpalette(ImagePalette.ImagePalette("RGB")) im.info = {"background": 0} diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index c07024a2c..9cbf18566 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -22,28 +22,26 @@ def test_sanity() -> None: Image.new("HSV", (100, 100)) -def wedge() -> Image.Image: - w = Image._wedge() - w90 = w.rotate(90) +def linear_gradient() -> Image.Image: + im = Image.linear_gradient(mode="L") + im90 = im.rotate(90) - (px, h) = w.size + (px, h) = im.size r = Image.new("L", (px * 3, h)) g = r.copy() b = r.copy() - r.paste(w, (0, 0)) - r.paste(w90, (px, 0)) + r.paste(im, (0, 0)) + r.paste(im90, (px, 0)) - g.paste(w90, (0, 0)) - g.paste(w, (2 * px, 0)) + g.paste(im90, (0, 0)) + g.paste(im, (2 * px, 0)) - b.paste(w, (px, 0)) - b.paste(w90, (2 * px, 0)) + b.paste(im, (px, 0)) + b.paste(im90, (2 * px, 0)) - img = Image.merge("RGB", (r, g, b)) - - return img + return Image.merge("RGB", (r, g, b)) def to_xxx_colorsys( @@ -79,8 +77,8 @@ def to_rgb_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") -def test_wedge() -> None: - src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR) +def test_linear_gradient() -> None: + src = linear_gradient().resize((3 * 32, 32), Image.Resampling.BILINEAR) im = src.convert("HSV") comparable = to_hsv_colorsys(src) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e723b6a2e..a5243549f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2996,15 +2996,6 @@ class ImageTransformHandler: # -------------------------------------------------------------------- # Factories -# -# Debugging - - -def _wedge() -> Image: - """Create grayscale wedge (for debugging only)""" - - return Image()._new(core.wedge("L")) - def _check_size(size: Any) -> None: """ diff --git a/src/_imaging.c b/src/_imaging.c index ee373e964..daaa56c75 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4256,7 +4256,6 @@ static PyMethodDef functions[] = { {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, - {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ /* Drawing support stuff */ {"font", (PyCFunction)_font_new, METH_VARARGS}, From 028f0d6ea9263928f29e5b62fa67d95870f57144 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:42:28 +0200 Subject: [PATCH 617/652] Remove unused data read --- Tests/test_file_gif.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 974aedeb6..6a295e89a 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -22,9 +22,6 @@ from .helper import ( # sample gif stream TEST_GIF = "Tests/images/hopper.gif" -with open(TEST_GIF, "rb") as f: - data = f.read() - def test_sanity() -> None: with Image.open(TEST_GIF) as im: From 126026e5e544ed35a7ea82349f41a1281facccf9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:44:00 +0200 Subject: [PATCH 618/652] Don't shadow builtin open --- Tests/test_file_gif.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 6a295e89a..d50842019 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -34,12 +34,12 @@ def test_sanity() -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(TEST_GIF) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: From 7f414846a3ef1cc5a740a74aa6cdeb49b58a373d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 16 Feb 2025 05:08:22 +1100 Subject: [PATCH 619/652] Don't shadow builtin open --- Tests/test_file_dcx.py | 4 ++-- Tests/test_file_fli.py | 4 ++-- Tests/test_file_im.py | 4 ++-- Tests/test_file_mpo.py | 4 ++-- Tests/test_file_psd.py | 4 ++-- Tests/test_file_spider.py | 4 ++-- Tests/test_file_tiff.py | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 5deacd878..ab6b9f983 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -26,12 +26,12 @@ def test_sanity() -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(TEST_FILE) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 876561a88..8adbd30f5 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -52,12 +52,12 @@ def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(static_test_file) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 1d3fa485f..d29998801 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -31,12 +31,12 @@ def test_name_limit(tmp_path: Path) -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(TEST_IM) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 66fa29177..311085cf7 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -38,12 +38,12 @@ def test_sanity(test_file: str) -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(test_files[0]) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 5f22001f3..1793c269d 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -25,12 +25,12 @@ def test_sanity() -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(test_file) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 713db848d..cdb7b3e0b 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -24,12 +24,12 @@ def test_sanity() -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(TEST_FILE) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index fe8f69848..a8a407963 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -63,12 +63,12 @@ class TestFileTiff: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(self) -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open("Tests/images/multipage.tiff") im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file(self) -> None: with warnings.catch_warnings(): From 0fbe1860c4f2688a7e18a1b4e525b4b5fb1c5d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sun, 16 Feb 2025 16:32:24 +0100 Subject: [PATCH 620/652] Update `pythoncapi_compat.h` to fix building with PyPy3.11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update `pythoncapi_compat.h` to upstream commit c84545f0e1e21757d4901f75c47333d25a3fcff0, which includes fixes necessary for Pillow to build against PyPy3.11. Otherwise, it fails due to duplicate declarations: ``` In file included from src/encode.c:28: src/thirdparty/pythoncapi_compat.h:295:1: error: static declaration of ‘PyThreadState_GetInterpreter’ follows non-static declaration 295 | PyThreadState_GetInterpreter(PyThreadState *tstate) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ In file included from /usr/include/pypy3.11/Python.h:80, from src/encode.c:26: /usr/include/pypy3.11/pystate.h:35:33: note: previous declaration of ‘PyThreadState_GetInterpreter’ with type ‘PyInterpreterState *(PyThreadState *)’ {aka ‘struct _is *(struct _ts *)’} 35 | PyAPI_FUNC(PyInterpreterState*) PyThreadState_GetInterpreter(PyThreadState *tstate); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``` --- src/thirdparty/pythoncapi_compat.h | 521 ++++++++++++++++++++++++++++- 1 file changed, 514 insertions(+), 7 deletions(-) diff --git a/src/thirdparty/pythoncapi_compat.h b/src/thirdparty/pythoncapi_compat.h index ca23d5ffa..04fcf61e0 100644 --- a/src/thirdparty/pythoncapi_compat.h +++ b/src/thirdparty/pythoncapi_compat.h @@ -10,7 +10,7 @@ // https://raw.githubusercontent.com/python/pythoncapi-compat/main/pythoncapi_compat.h // // This file was vendored from the following commit: -// https://github.com/python/pythoncapi-compat/commit/0041177c4f348c8952b4c8980b2c90856e61c7c7 +// https://github.com/python/pythoncapi-compat/commit/c84545f0e1e21757d4901f75c47333d25a3fcff0 // // SPDX-License-Identifier: 0BSD @@ -22,11 +22,15 @@ extern "C" { #endif #include +#include // offsetof() // Python 3.11.0b4 added PyFrame_Back() to Python.h #if PY_VERSION_HEX < 0x030b00B4 && !defined(PYPY_VERSION) # include "frameobject.h" // PyFrameObject, PyFrame_GetBack() #endif +#if PY_VERSION_HEX < 0x030C00A3 +# include // T_SHORT, READONLY +#endif #ifndef _Py_CAST @@ -290,7 +294,7 @@ PyFrame_GetVarString(PyFrameObject *frame, const char *name) // bpo-39947 added PyThreadState_GetInterpreter() to Python 3.9.0a5 -#if PY_VERSION_HEX < 0x030900A5 || defined(PYPY_VERSION) +#if PY_VERSION_HEX < 0x030900A5 || (defined(PYPY_VERSION) && PY_VERSION_HEX < 0x030B0000) static inline PyInterpreterState * PyThreadState_GetInterpreter(PyThreadState *tstate) { @@ -583,7 +587,7 @@ static inline int PyWeakref_GetRef(PyObject *ref, PyObject **pobj) return 0; } *pobj = Py_NewRef(obj); - return (*pobj != NULL); + return 1; } #endif @@ -921,7 +925,7 @@ static inline int PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg) { PyObject **dict = _PyObject_GetDictPtr(obj); - if (*dict == NULL) { + if (dict == NULL || *dict == NULL) { return -1; } Py_VISIT(*dict); @@ -932,7 +936,7 @@ static inline void PyObject_ClearManagedDict(PyObject *obj) { PyObject **dict = _PyObject_GetDictPtr(obj); - if (*dict == NULL) { + if (dict == NULL || *dict == NULL) { return; } Py_CLEAR(*dict); @@ -1207,11 +1211,11 @@ static inline int PyTime_PerfCounter(PyTime_t *result) #endif // gh-111389 added hash constants to Python 3.13.0a5. These constants were -// added first as private macros to Python 3.4.0b1 and PyPy 7.3.9. +// added first as private macros to Python 3.4.0b1 and PyPy 7.3.8. #if (!defined(PyHASH_BITS) \ && ((!defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x030400B1) \ || (defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x03070000 \ - && PYPY_VERSION_NUM >= 0x07090000))) + && PYPY_VERSION_NUM >= 0x07030800))) # define PyHASH_BITS _PyHASH_BITS # define PyHASH_MODULUS _PyHASH_MODULUS # define PyHASH_INF _PyHASH_INF @@ -1523,6 +1527,36 @@ static inline int PyLong_GetSign(PyObject *obj, int *sign) } #endif +// gh-126061 added PyLong_IsPositive/Negative/Zero() to Python in 3.14.0a2 +#if PY_VERSION_HEX < 0x030E00A2 +static inline int PyLong_IsPositive(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == 1; +} + +static inline int PyLong_IsNegative(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == -1; +} + +static inline int PyLong_IsZero(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == 0; +} +#endif + // gh-124502 added PyUnicode_Equal() to Python 3.14.0a0 #if PY_VERSION_HEX < 0x030E00A0 @@ -1693,6 +1727,479 @@ static inline int PyLong_AsUInt64(PyObject *obj, uint64_t *pvalue) #endif +// gh-102471 added import and export API for integers to 3.14.0a2. +#if PY_VERSION_HEX < 0x030E00A2 && PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) +// Helpers to access PyLongObject internals. +static inline void +_PyLong_SetSignAndDigitCount(PyLongObject *op, int sign, Py_ssize_t size) +{ +#if PY_VERSION_HEX >= 0x030C0000 + op->long_value.lv_tag = (uintptr_t)(1 - sign) | ((uintptr_t)(size) << 3); +#elif PY_VERSION_HEX >= 0x030900A4 + Py_SET_SIZE(op, sign * size); +#else + Py_SIZE(op) = sign * size; +#endif +} + +static inline Py_ssize_t +_PyLong_DigitCount(const PyLongObject *op) +{ +#if PY_VERSION_HEX >= 0x030C0000 + return (Py_ssize_t)(op->long_value.lv_tag >> 3); +#else + return _PyLong_Sign((PyObject*)op) < 0 ? -Py_SIZE(op) : Py_SIZE(op); +#endif +} + +static inline digit* +_PyLong_GetDigits(const PyLongObject *op) +{ +#if PY_VERSION_HEX >= 0x030C0000 + return (digit*)(op->long_value.ob_digit); +#else + return (digit*)(op->ob_digit); +#endif +} + +typedef struct PyLongLayout { + uint8_t bits_per_digit; + uint8_t digit_size; + int8_t digits_order; + int8_t digit_endianness; +} PyLongLayout; + +typedef struct PyLongExport { + int64_t value; + uint8_t negative; + Py_ssize_t ndigits; + const void *digits; + Py_uintptr_t _reserved; +} PyLongExport; + +typedef struct PyLongWriter PyLongWriter; + +static inline const PyLongLayout* +PyLong_GetNativeLayout(void) +{ + static const PyLongLayout PyLong_LAYOUT = { + PyLong_SHIFT, + sizeof(digit), + -1, // least significant first + PY_LITTLE_ENDIAN ? -1 : 1, + }; + + return &PyLong_LAYOUT; +} + +static inline int +PyLong_Export(PyObject *obj, PyLongExport *export_long) +{ + if (!PyLong_Check(obj)) { + memset(export_long, 0, sizeof(*export_long)); + PyErr_Format(PyExc_TypeError, "expected int, got %s", + Py_TYPE(obj)->tp_name); + return -1; + } + + // Fast-path: try to convert to a int64_t + PyLongObject *self = (PyLongObject*)obj; + int overflow; +#if SIZEOF_LONG == 8 + long value = PyLong_AsLongAndOverflow(obj, &overflow); +#else + // Windows has 32-bit long, so use 64-bit long long instead + long long value = PyLong_AsLongLongAndOverflow(obj, &overflow); +#endif + Py_BUILD_ASSERT(sizeof(value) == sizeof(int64_t)); + // the function cannot fail since obj is a PyLongObject + assert(!(value == -1 && PyErr_Occurred())); + + if (!overflow) { + export_long->value = value; + export_long->negative = 0; + export_long->ndigits = 0; + export_long->digits = 0; + export_long->_reserved = 0; + } + else { + export_long->value = 0; + export_long->negative = _PyLong_Sign(obj) < 0; + export_long->ndigits = _PyLong_DigitCount(self); + if (export_long->ndigits == 0) { + export_long->ndigits = 1; + } + export_long->digits = _PyLong_GetDigits(self); + export_long->_reserved = (Py_uintptr_t)Py_NewRef(obj); + } + return 0; +} + +static inline void +PyLong_FreeExport(PyLongExport *export_long) +{ + PyObject *obj = (PyObject*)export_long->_reserved; + + if (obj) { + export_long->_reserved = 0; + Py_DECREF(obj); + } +} + +static inline PyLongWriter* +PyLongWriter_Create(int negative, Py_ssize_t ndigits, void **digits) +{ + if (ndigits <= 0) { + PyErr_SetString(PyExc_ValueError, "ndigits must be positive"); + return NULL; + } + assert(digits != NULL); + + PyLongObject *obj = _PyLong_New(ndigits); + if (obj == NULL) { + return NULL; + } + _PyLong_SetSignAndDigitCount(obj, negative?-1:1, ndigits); + + *digits = _PyLong_GetDigits(obj); + return (PyLongWriter*)obj; +} + +static inline void +PyLongWriter_Discard(PyLongWriter *writer) +{ + PyLongObject *obj = (PyLongObject *)writer; + + assert(Py_REFCNT(obj) == 1); + Py_DECREF(obj); +} + +static inline PyObject* +PyLongWriter_Finish(PyLongWriter *writer) +{ + PyObject *obj = (PyObject *)writer; + PyLongObject *self = (PyLongObject*)obj; + Py_ssize_t j = _PyLong_DigitCount(self); + Py_ssize_t i = j; + int sign = _PyLong_Sign(obj); + + assert(Py_REFCNT(obj) == 1); + + // Normalize and get singleton if possible + while (i > 0 && _PyLong_GetDigits(self)[i-1] == 0) { + --i; + } + if (i != j) { + if (i == 0) { + sign = 0; + } + _PyLong_SetSignAndDigitCount(self, sign, i); + } + if (i <= 1) { + long val = sign * (long)(_PyLong_GetDigits(self)[0]); + Py_DECREF(obj); + return PyLong_FromLong(val); + } + + return obj; +} +#endif + + +#if PY_VERSION_HEX < 0x030C00A3 +# define Py_T_SHORT T_SHORT +# define Py_T_INT T_INT +# define Py_T_LONG T_LONG +# define Py_T_FLOAT T_FLOAT +# define Py_T_DOUBLE T_DOUBLE +# define Py_T_STRING T_STRING +# define _Py_T_OBJECT T_OBJECT +# define Py_T_CHAR T_CHAR +# define Py_T_BYTE T_BYTE +# define Py_T_UBYTE T_UBYTE +# define Py_T_USHORT T_USHORT +# define Py_T_UINT T_UINT +# define Py_T_ULONG T_ULONG +# define Py_T_STRING_INPLACE T_STRING_INPLACE +# define Py_T_BOOL T_BOOL +# define Py_T_OBJECT_EX T_OBJECT_EX +# define Py_T_LONGLONG T_LONGLONG +# define Py_T_ULONGLONG T_ULONGLONG +# define Py_T_PYSSIZET T_PYSSIZET + +# if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) +# define _Py_T_NONE T_NONE +# endif + +# define Py_READONLY READONLY +# define Py_AUDIT_READ READ_RESTRICTED +# define _Py_WRITE_RESTRICTED PY_WRITE_RESTRICTED +#endif + + +// gh-127350 added Py_fopen() and Py_fclose() to Python 3.14a4 +#if PY_VERSION_HEX < 0x030E00A4 +static inline FILE* Py_fopen(PyObject *path, const char *mode) +{ +#if 0x030400A2 <= PY_VERSION_HEX && !defined(PYPY_VERSION) + PyAPI_FUNC(FILE*) _Py_fopen_obj(PyObject *path, const char *mode); + + return _Py_fopen_obj(path, mode); +#else + FILE *f; + PyObject *bytes; +#if PY_VERSION_HEX >= 0x03000000 + if (!PyUnicode_FSConverter(path, &bytes)) { + return NULL; + } +#else + if (!PyString_Check(path)) { + PyErr_SetString(PyExc_TypeError, "except str"); + return NULL; + } + bytes = Py_NewRef(path); +#endif + const char *path_bytes = PyBytes_AS_STRING(bytes); + + f = fopen(path_bytes, mode); + Py_DECREF(bytes); + + if (f == NULL) { + PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, path); + return NULL; + } + return f; +#endif +} + +static inline int Py_fclose(FILE *file) +{ + return fclose(file); +} +#endif + + +#if 0x03090000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030E0000 && !defined(PYPY_VERSION) +static inline PyObject* +PyConfig_Get(const char *name) +{ + typedef enum { + _PyConfig_MEMBER_INT, + _PyConfig_MEMBER_UINT, + _PyConfig_MEMBER_ULONG, + _PyConfig_MEMBER_BOOL, + _PyConfig_MEMBER_WSTR, + _PyConfig_MEMBER_WSTR_OPT, + _PyConfig_MEMBER_WSTR_LIST, + } PyConfigMemberType; + + typedef struct { + const char *name; + size_t offset; + PyConfigMemberType type; + const char *sys_attr; + } PyConfigSpec; + +#define PYTHONCAPI_COMPAT_SPEC(MEMBER, TYPE, sys_attr) \ + {#MEMBER, offsetof(PyConfig, MEMBER), \ + _PyConfig_MEMBER_##TYPE, sys_attr} + + static const PyConfigSpec config_spec[] = { + PYTHONCAPI_COMPAT_SPEC(argv, WSTR_LIST, "argv"), + PYTHONCAPI_COMPAT_SPEC(base_exec_prefix, WSTR_OPT, "base_exec_prefix"), + PYTHONCAPI_COMPAT_SPEC(base_executable, WSTR_OPT, "_base_executable"), + PYTHONCAPI_COMPAT_SPEC(base_prefix, WSTR_OPT, "base_prefix"), + PYTHONCAPI_COMPAT_SPEC(bytes_warning, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(exec_prefix, WSTR_OPT, "exec_prefix"), + PYTHONCAPI_COMPAT_SPEC(executable, WSTR_OPT, "executable"), + PYTHONCAPI_COMPAT_SPEC(inspect, BOOL, _Py_NULL), +#if 0x030C0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(int_max_str_digits, UINT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(interactive, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(module_search_paths, WSTR_LIST, "path"), + PYTHONCAPI_COMPAT_SPEC(optimization_level, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(parser_debug, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(platlibdir, WSTR, "platlibdir"), + PYTHONCAPI_COMPAT_SPEC(prefix, WSTR_OPT, "prefix"), + PYTHONCAPI_COMPAT_SPEC(pycache_prefix, WSTR_OPT, "pycache_prefix"), + PYTHONCAPI_COMPAT_SPEC(quiet, BOOL, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(stdlib_dir, WSTR_OPT, "_stdlib_dir"), +#endif + PYTHONCAPI_COMPAT_SPEC(use_environment, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(verbose, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(warnoptions, WSTR_LIST, "warnoptions"), + PYTHONCAPI_COMPAT_SPEC(write_bytecode, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(xoptions, WSTR_LIST, "_xoptions"), + PYTHONCAPI_COMPAT_SPEC(buffered_stdio, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(check_hash_pycs_mode, WSTR, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(code_debug_ranges, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(configure_c_stdio, BOOL, _Py_NULL), +#if 0x030D0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(cpu_count, INT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(dev_mode, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(dump_refs, BOOL, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(dump_refs_file, WSTR_OPT, _Py_NULL), +#endif +#ifdef Py_GIL_DISABLED + PYTHONCAPI_COMPAT_SPEC(enable_gil, INT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(faulthandler, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(filesystem_encoding, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(filesystem_errors, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(hash_seed, ULONG, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(home, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(import_time, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(install_signal_handlers, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(isolated, BOOL, _Py_NULL), +#ifdef MS_WINDOWS + PYTHONCAPI_COMPAT_SPEC(legacy_windows_stdio, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(malloc_stats, BOOL, _Py_NULL), +#if 0x030A0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(orig_argv, WSTR_LIST, "orig_argv"), +#endif + PYTHONCAPI_COMPAT_SPEC(parse_argv, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(pathconfig_warnings, BOOL, _Py_NULL), +#if 0x030C0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(perf_profiling, UINT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(program_name, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_command, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_filename, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_module, WSTR_OPT, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(safe_path, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(show_ref_count, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(site_import, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(skip_source_first_line, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(stdio_encoding, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(stdio_errors, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(tracemalloc, UINT, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(use_frozen_modules, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(use_hash_seed, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(user_site_directory, BOOL, _Py_NULL), +#if 0x030A0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(warn_default_encoding, BOOL, _Py_NULL), +#endif + }; + +#undef PYTHONCAPI_COMPAT_SPEC + + const PyConfigSpec *spec; + int found = 0; + for (size_t i=0; i < sizeof(config_spec) / sizeof(config_spec[0]); i++) { + spec = &config_spec[i]; + if (strcmp(spec->name, name) == 0) { + found = 1; + break; + } + } + if (found) { + if (spec->sys_attr != NULL) { + PyObject *value = PySys_GetObject(spec->sys_attr); + if (value == NULL) { + PyErr_Format(PyExc_RuntimeError, "lost sys.%s", spec->sys_attr); + return NULL; + } + return Py_NewRef(value); + } + + PyAPI_FUNC(const PyConfig*) _Py_GetConfig(void); + + const PyConfig *config = _Py_GetConfig(); + void *member = (char *)config + spec->offset; + switch (spec->type) { + case _PyConfig_MEMBER_INT: + case _PyConfig_MEMBER_UINT: + { + int value = *(int *)member; + return PyLong_FromLong(value); + } + case _PyConfig_MEMBER_BOOL: + { + int value = *(int *)member; + return PyBool_FromLong(value != 0); + } + case _PyConfig_MEMBER_ULONG: + { + unsigned long value = *(unsigned long *)member; + return PyLong_FromUnsignedLong(value); + } + case _PyConfig_MEMBER_WSTR: + case _PyConfig_MEMBER_WSTR_OPT: + { + wchar_t *wstr = *(wchar_t **)member; + if (wstr != NULL) { + return PyUnicode_FromWideChar(wstr, -1); + } + else { + return Py_NewRef(Py_None); + } + } + case _PyConfig_MEMBER_WSTR_LIST: + { + const PyWideStringList *list = (const PyWideStringList *)member; + PyObject *tuple = PyTuple_New(list->length); + if (tuple == NULL) { + return NULL; + } + + for (Py_ssize_t i = 0; i < list->length; i++) { + PyObject *item = PyUnicode_FromWideChar(list->items[i], -1); + if (item == NULL) { + Py_DECREF(tuple); + return NULL; + } + PyTuple_SET_ITEM(tuple, i, item); + } + return tuple; + } + default: + Py_UNREACHABLE(); + } + } + + PyErr_Format(PyExc_ValueError, "unknown config option name: %s", name); + return NULL; +} + +static inline int +PyConfig_GetInt(const char *name, int *value) +{ + PyObject *obj = PyConfig_Get(name); + if (obj == NULL) { + return -1; + } + + if (!PyLong_Check(obj)) { + Py_DECREF(obj); + PyErr_Format(PyExc_TypeError, "config option %s is not an int", name); + return -1; + } + + int as_int = PyLong_AsInt(obj); + Py_DECREF(obj); + if (as_int == -1 && PyErr_Occurred()) { + PyErr_Format(PyExc_OverflowError, + "config option %s value does not fit into a C int", name); + return -1; + } + + *value = as_int; + return 0; +} +#endif // PY_VERSION_HEX > 0x03090000 && !defined(PYPY_VERSION) + + #ifdef __cplusplus } #endif From 216690ff17d03c80860b59f5ba2311383a09525c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 7 Feb 2025 18:11:54 +0200 Subject: [PATCH 621/652] Add PyPy3.11 to CI --- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 8faab2ef4..ef49ff332 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"] architecture: ["x64"] os: ["windows-latest"] include: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3efe0b59..c4ad88be9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,6 +41,7 @@ jobs: "ubuntu-latest", ] python-version: [ + "pypy3.11", "pypy3.10", "3.14", "3.13t", From 9762c9e30eeb6adc6815b25ad619432f707d4632 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 Feb 2025 20:20:02 +1100 Subject: [PATCH 622/652] Test unexpected end of tar file --- Tests/test_file_tar.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 49220a8b6..d1a4ca9de 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,6 +1,7 @@ from __future__ import annotations import warnings +from pathlib import Path import pytest @@ -29,6 +30,16 @@ def test_sanity(codec: str, test_path: str, format: str) -> None: assert im.format == format +def test_unexpected_end(tmp_path: Path) -> None: + tmpfile = str(tmp_path / "temp.tar") + with open(tmpfile, "w"): + pass + + with pytest.raises(OSError): + with TarIO.TarIO(tmpfile, "test"): + pass + + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: with pytest.warns(ResourceWarning): From 152d982644f4474d14597b4cba878903db400a67 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 Feb 2025 20:20:45 +1100 Subject: [PATCH 623/652] Test missing subfile --- Tests/test_file_tar.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index d1a4ca9de..e1a6a55d7 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -40,6 +40,12 @@ def test_unexpected_end(tmp_path: Path) -> None: pass +def test_cannot_find_subfile(tmp_path: Path) -> None: + with pytest.raises(OSError): + with TarIO.TarIO(TEST_TAR_FILE, "test"): + pass + + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: with pytest.warns(ResourceWarning): From 15e4c1a72451672d035e9e1525ccac539252e742 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 21:34:25 +0200 Subject: [PATCH 624/652] Fix ShellCheck --- .ci/install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 5c20e7f37..e61752750 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -2,12 +2,12 @@ aptget_update() { - if [ ! -z $1 ]; then + if [ -n "$1" ]; then echo "" echo "Retrying apt-get update..." echo "" fi - output=`sudo apt-get update 2>&1` + output=$(sudo apt-get update 2>&1) echo "$output" if [[ $output == *[WE]:\ * ]]; then return 1 From 017b16b803347bceb19361230c751267fab6e660 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:48:09 +1100 Subject: [PATCH 625/652] Removed argument Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_file_tar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index e1a6a55d7..9fc3edcd7 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -40,7 +40,7 @@ def test_unexpected_end(tmp_path: Path) -> None: pass -def test_cannot_find_subfile(tmp_path: Path) -> None: +def test_cannot_find_subfile() -> None: with pytest.raises(OSError): with TarIO.TarIO(TEST_TAR_FILE, "test"): pass From 19010bb301e559dd6c9b255c9e3134bbde31297f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:49:08 +1100 Subject: [PATCH 626/652] Use match Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_file_tar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 9fc3edcd7..084d0f288 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -35,13 +35,13 @@ def test_unexpected_end(tmp_path: Path) -> None: with open(tmpfile, "w"): pass - with pytest.raises(OSError): + with pytest.raises(OSError, match="unexpected end of tar file"): with TarIO.TarIO(tmpfile, "test"): pass def test_cannot_find_subfile() -> None: - with pytest.raises(OSError): + with pytest.raises(OSError, match="cannot find subfile"): with TarIO.TarIO(TEST_TAR_FILE, "test"): pass From 322e121a92ac6c608ef3d626a19b7d3127ab044f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Feb 2025 07:56:11 +1100 Subject: [PATCH 627/652] Corrected type check --- Tests/test_file_gbr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index 5b59cc07a..1b834cd3c 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -16,7 +16,7 @@ def test_load() -> None: with Image.open("Tests/images/gbr.gbr") as im: px = im.load() assert px is not None - assert im.load()[0, 0] == (0, 0, 0, 0) + assert px[0, 0] == (0, 0, 0, 0) # Test again now that it has already been loaded once px = im.load() From 1e574e6f8bfd5862a5875db38d08a1e83cadb0e2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 19:28:43 +0200 Subject: [PATCH 628/652] Replace slice and comparison with startswith --- Tests/test_file_mpo.py | 4 ++-- Tests/test_file_webp_metadata.py | 2 +- Tests/test_image.py | 4 ++-- docs/example/DdsImagePlugin.py | 2 +- .../writing-your-own-image-plugin.rst | 2 +- src/PIL/BdfFontFile.py | 10 +++++----- src/PIL/BlpImagePlugin.py | 2 +- src/PIL/BmpImagePlugin.py | 2 +- src/PIL/BufrStubImagePlugin.py | 2 +- src/PIL/CurImagePlugin.py | 2 +- src/PIL/DdsImagePlugin.py | 2 +- src/PIL/EpsImagePlugin.py | 6 ++++-- src/PIL/FitsImagePlugin.py | 2 +- src/PIL/FpxImagePlugin.py | 2 +- src/PIL/FtexImagePlugin.py | 2 +- src/PIL/GifImagePlugin.py | 4 ++-- src/PIL/GimpGradientFile.py | 2 +- src/PIL/GimpPaletteFile.py | 2 +- src/PIL/GribStubImagePlugin.py | 2 +- src/PIL/Hdf5StubImagePlugin.py | 2 +- src/PIL/IcnsImagePlugin.py | 8 ++++---- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/ImImagePlugin.py | 4 ++-- src/PIL/Image.py | 2 +- src/PIL/Jpeg2KImagePlugin.py | 5 ++--- src/PIL/JpegImagePlugin.py | 20 +++++++++---------- src/PIL/McIdasImagePlugin.py | 2 +- src/PIL/MicImagePlugin.py | 2 +- src/PIL/MpegImagePlugin.py | 2 +- src/PIL/MspImagePlugin.py | 4 ++-- src/PIL/PaletteFile.py | 2 +- src/PIL/PcdImagePlugin.py | 2 +- src/PIL/PixarImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 2 +- src/PIL/PpmImagePlugin.py | 2 +- src/PIL/PsdImagePlugin.py | 2 +- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 4 ++-- src/PIL/WebPImagePlugin.py | 2 +- src/PIL/WmfImagePlugin.py | 8 +++----- src/PIL/XVThumbImagePlugin.py | 2 +- src/PIL/XbmImagePlugin.py | 2 +- src/PIL/XpmImagePlugin.py | 4 ++-- 43 files changed, 72 insertions(+), 73 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 311085cf7..ab8f2d5a1 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -77,8 +77,8 @@ def test_app(test_file: str) -> None: with Image.open(test_file) as im: assert im.applist[0][0] == "APP1" assert im.applist[1][0] == "APP2" - assert ( - im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" + assert im.applist[1][1].startswith( + b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" ) assert len(im.applist) == 2 diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index d9a834c75..c68a20d7a 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -40,7 +40,7 @@ def test_read_exif_metadata() -> None: def test_read_exif_metadata_without_prefix() -> None: with Image.open("Tests/images/flower2.webp") as im: # Assert prefix is not present - assert im.info["exif"][:6] != b"Exif\x00\x00" + assert not im.info["exif"].startswith(b"Exif\x00\x00") exif = im.getexif() assert exif[305] == "Adobe Photoshop CS6 (Macintosh)" diff --git a/Tests/test_image.py b/Tests/test_image.py index 4c8aeaa3d..5474f951c 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -74,12 +74,12 @@ class TestImage: def test_sanity(self) -> None: im = Image.new("L", (100, 100)) - assert repr(im)[:45] == " bool: - return prefix[:4] == b"DDS " + return prefix.startswith(b"DDS ") Image.register_open(DdsImageFile.format, DdsImageFile, _accept) diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 2e853224d..9e7d14c57 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -54,7 +54,7 @@ true color. def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"SPAM" + return prefix.startswith(b"SPAM") class SpamImageFile(ImageFile.ImageFile): diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index bfd66aa6a..f175e2f4f 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -43,7 +43,7 @@ def bdf_char( s = f.readline() if not s: return None - if s[:9] == b"STARTCHAR": + if s.startswith(b"STARTCHAR"): break id = s[9:].strip().decode("ascii") @@ -51,7 +51,7 @@ def bdf_char( props = {} while True: s = f.readline() - if not s or s[:6] == b"BITMAP": + if not s or s.startswith(b"BITMAP"): break i = s.find(b" ") props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") @@ -60,7 +60,7 @@ def bdf_char( bitmap = bytearray() while True: s = f.readline() - if not s or s[:7] == b"ENDCHAR": + if not s or s.startswith(b"ENDCHAR"): break bitmap += s[:-1] @@ -96,7 +96,7 @@ class BdfFontFile(FontFile.FontFile): super().__init__() s = fp.readline() - if s[:13] != b"STARTFONT 2.1": + if not s.startswith(b"STARTFONT 2.1"): msg = "not a valid BDF file" raise SyntaxError(msg) @@ -105,7 +105,7 @@ class BdfFontFile(FontFile.FontFile): while True: s = fp.readline() - if not s or s[:13] == b"ENDPROPERTIES": + if not s or s.startswith(b"ENDPROPERTIES"): break i = s.find(b" ") props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 8585a8e60..5747c1252 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -246,7 +246,7 @@ class BLPFormatError(NotImplementedError): def _accept(prefix: bytes) -> bool: - return prefix[:4] in (b"BLP1", b"BLP2") + return prefix.startswith((b"BLP1", b"BLP2")) class BlpImageFile(ImageFile.ImageFile): diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index bf8f29577..d60ea591a 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -50,7 +50,7 @@ BIT2MODE = { def _accept(prefix: bytes) -> bool: - return prefix[:2] == b"BM" + return prefix.startswith(b"BM") def _dib_accept(prefix: bytes) -> bool: diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 50c41c482..8c5da14f5 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" + return prefix.startswith((b"BUFR", b"ZCZC")) class BufrStubImageFile(ImageFile.StubImageFile): diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index c4be0ceca..b817dbc87 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -26,7 +26,7 @@ from ._binary import i32le as i32 def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\0\0\2\0" + return prefix.startswith(b"\0\0\2\0") ## diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 9349e2841..cdae8dfee 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -564,7 +564,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"DDS " + return prefix.startswith(b"DDS ") Image.register_open(DdsImageFile.format, DdsImageFile, _accept) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 36ba15ec5..5e2ddad99 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -170,7 +170,9 @@ def Ghostscript( def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) + return prefix.startswith(b"%!PS") or ( + len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5 + ) ## @@ -295,7 +297,7 @@ class EpsImageFile(ImageFile.ImageFile): m = field.match(s) if m: k = m.group(1) - if k[:8] == "PS-Adobe": + if k.startswith("PS-Adobe"): self.info["PS-Adobe"] = k[9:] else: self.info[k] = "" diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 6bbd2641a..a3fdc0efe 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -17,7 +17,7 @@ from . import Image, ImageFile def _accept(prefix: bytes) -> bool: - return prefix[:6] == b"SIMPLE" + return prefix.startswith(b"SIMPLE") class FitsImageFile(ImageFile.ImageFile): diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 4cfcb067d..fd992cd9e 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -42,7 +42,7 @@ MODES = { def _accept(prefix: bytes) -> bool: - return prefix[:8] == olefile.MAGIC + return prefix.startswith(olefile.MAGIC) ## diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 0516b760c..26e5bd4a6 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -108,7 +108,7 @@ class FtexImageFile(ImageFile.ImageFile): def _accept(prefix: bytes) -> bool: - return prefix[:4] == MAGIC + return prefix.startswith(MAGIC) Image.register_open(FtexImageFile.format, FtexImageFile, _accept) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index ff7262efc..259e93f09 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -67,7 +67,7 @@ LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST def _accept(prefix: bytes) -> bool: - return prefix[:6] in [b"GIF87a", b"GIF89a"] + return prefix.startswith((b"GIF87a", b"GIF89a")) ## @@ -257,7 +257,7 @@ class GifImageFile(ImageFile.ImageFile): # application extension # info["extension"] = block, self.fp.tell() - if block[:11] == b"NETSCAPE2.0": + if block.startswith(b"NETSCAPE2.0"): block = self.data() if block and len(block) >= 3 and block[0] == 1: self.info["loop"] = i16(block, 1) diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index 220eac57e..ec62f8e4e 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -116,7 +116,7 @@ class GimpGradientFile(GradientFile): """File handler for GIMP's gradient format.""" def __init__(self, fp: IO[bytes]) -> None: - if fp.readline()[:13] != b"GIMP Gradient": + if not fp.readline().startswith(b"GIMP Gradient"): msg = "not a GIMP gradient file" raise SyntaxError(msg) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 4cad0ebee..1b7a394c0 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -29,7 +29,7 @@ class GimpPaletteFile: def __init__(self, fp: IO[bytes]) -> None: palette = [o8(i) * 3 for i in range(256)] - if fp.readline()[:12] != b"GIMP Palette": + if not fp.readline().startswith(b"GIMP Palette"): msg = "not a GIMP palette file" raise SyntaxError(msg) diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index eb1b1483b..439fc5a3e 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"GRIB" and prefix[7] == 1 + return prefix.startswith(b"GRIB") and prefix[7] == 1 class GribStubImageFile(ImageFile.StubImageFile): diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index ddc218508..76e640f15 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:8] == b"\x89HDF\r\n\x1a\n" + return prefix.startswith(b"\x89HDF\r\n\x1a\n") class HDF5StubImageFile(ImageFile.StubImageFile): diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 9757b2b14..a5d5b93ae 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -117,14 +117,14 @@ def read_png_or_jpeg2000( sig = fobj.read(12) im: Image.Image - if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a": + if sig.startswith(b"\x89PNG\x0d\x0a\x1a\x0a"): fobj.seek(start) im = PngImagePlugin.PngImageFile(fobj) Image._decompression_bomb_check(im.size) return {"RGBA": im} elif ( - sig[:4] == b"\xff\x4f\xff\x51" - or sig[:4] == b"\x0d\x0a\x87\x0a" + sig.startswith(b"\xff\x4f\xff\x51") + or sig.startswith(b"\x0d\x0a\x87\x0a") or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" ): if not enable_jpeg2k: @@ -387,7 +387,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == MAGIC + return prefix.startswith(MAGIC) Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index e879f1801..55c57f203 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -118,7 +118,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == _MAGIC + return prefix.startswith(_MAGIC) class IconHeader(NamedTuple): diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 2a26d0b29..270a29467 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -209,7 +209,7 @@ class ImImageFile(ImageFile.ImageFile): self._mode = self.info[MODE] # Skip forward to start of image data - while s and s[:1] != b"\x1a": + while s and not s.startswith(b"\x1a"): s = self.fp.read(1) if not s: msg = "File truncated" @@ -247,7 +247,7 @@ class ImImageFile(ImageFile.ImageFile): self._fp = self.fp # FIXME: hack - if self.rawmode[:2] == "F;": + if self.rawmode.startswith("F;"): # ifunc95 formats try: # use bit decoder (if necessary) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a5243549f..6a2aa3e4c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3998,7 +3998,7 @@ class Exif(_ExifBase): if tag == ExifTags.IFD.MakerNote: from .TiffImagePlugin import ImageFileDirectory_v2 - if tag_data[:8] == b"FUJIFILM": + if tag_data.startswith(b"FUJIFILM"): ifd_offset = i32le(tag_data, 8) ifd_data = tag_data[ifd_offset:] diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 67828358d..e0f4ecae5 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -352,9 +352,8 @@ class Jpeg2KImageFile(ImageFile.ImageFile): def _accept(prefix: bytes) -> bool: - return ( - prefix[:4] == b"\xff\x4f\xff\x51" - or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" + return prefix.startswith( + (b"\xff\x4f\xff\x51", b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a") ) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a1c9c443a..3e882403b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -77,7 +77,7 @@ def APP(self: JpegImageFile, marker: int) -> None: self.app[app] = s # compatibility self.applist.append((app, s)) - if marker == 0xFFE0 and s[:4] == b"JFIF": + if marker == 0xFFE0 and s.startswith(b"JFIF"): # extract JFIF information self.info["jfif"] = version = i16(s, 5) # version self.info["jfif_version"] = divmod(version, 256) @@ -95,19 +95,19 @@ def APP(self: JpegImageFile, marker: int) -> None: self.info["dpi"] = tuple(d * 2.54 for d in jfif_density) self.info["jfif_unit"] = jfif_unit self.info["jfif_density"] = jfif_density - elif marker == 0xFFE1 and s[:6] == b"Exif\0\0": + elif marker == 0xFFE1 and s.startswith(b"Exif\0\0"): # extract EXIF information if "exif" in self.info: self.info["exif"] += s[6:] else: self.info["exif"] = s self._exif_offset = self.fp.tell() - n + 6 - elif marker == 0xFFE1 and s[:29] == b"http://ns.adobe.com/xap/1.0/\x00": + elif marker == 0xFFE1 and s.startswith(b"http://ns.adobe.com/xap/1.0/\x00"): self.info["xmp"] = s.split(b"\x00", 1)[1] - elif marker == 0xFFE2 and s[:5] == b"FPXR\0": + elif marker == 0xFFE2 and s.startswith(b"FPXR\0"): # extract FlashPix information (incomplete) self.info["flashpix"] = s # FIXME: value will change - elif marker == 0xFFE2 and s[:12] == b"ICC_PROFILE\0": + elif marker == 0xFFE2 and s.startswith(b"ICC_PROFILE\0"): # Since an ICC profile can be larger than the maximum size of # a JPEG marker (64K), we need provisions to split it into # multiple markers. The format defined by the ICC specifies @@ -120,7 +120,7 @@ def APP(self: JpegImageFile, marker: int) -> None: # reassemble the profile, rather than assuming that the APP2 # markers appear in the correct sequence. self.icclist.append(s) - elif marker == 0xFFED and s[:14] == b"Photoshop 3.0\x00": + elif marker == 0xFFED and s.startswith(b"Photoshop 3.0\x00"): # parse the image resource block offset = 14 photoshop = self.info.setdefault("photoshop", {}) @@ -153,7 +153,7 @@ def APP(self: JpegImageFile, marker: int) -> None: except struct.error: break # insufficient data - elif marker == 0xFFEE and s[:5] == b"Adobe": + elif marker == 0xFFEE and s.startswith(b"Adobe"): self.info["adobe"] = i16(s, 5) # extract Adobe custom properties try: @@ -162,7 +162,7 @@ def APP(self: JpegImageFile, marker: int) -> None: pass else: self.info["adobe_transform"] = adobe_transform - elif marker == 0xFFE2 and s[:4] == b"MPF\0": + elif marker == 0xFFE2 and s.startswith(b"MPF\0"): # extract MPO information self.info["mp"] = s[4:] # offset is current location minus buffer size @@ -325,7 +325,7 @@ MARKER = { def _accept(prefix: bytes) -> bool: # Magic number was taken from https://en.wikipedia.org/wiki/JPEG - return prefix[:3] == b"\xff\xd8\xff" + return prefix.startswith(b"\xff\xd8\xff") ## @@ -547,7 +547,7 @@ def _getmp(self: JpegImageFile) -> dict[int, Any] | None: return None file_contents = io.BytesIO(data) head = file_contents.read(8) - endianness = ">" if head[:4] == b"\x4d\x4d\x00\x2a" else "<" + endianness = ">" if head.startswith(b"\x4d\x4d\x00\x2a") else "<" # process dictionary from . import TiffImagePlugin diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 5dd031be3..b4460a9a5 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -23,7 +23,7 @@ from . import Image, ImageFile def _accept(prefix: bytes) -> bool: - return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" + return prefix.startswith(b"\x00\x00\x00\x00\x00\x00\x00\x04") ## diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 5f23a34b9..eb10a4c82 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -26,7 +26,7 @@ from . import Image, TiffImagePlugin def _accept(prefix: bytes) -> bool: - return prefix[:8] == olefile.MAGIC + return prefix.startswith(olefile.MAGIC) ## diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index ad4d3e937..5aa00d05b 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -54,7 +54,7 @@ class BitStream: def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\x00\x00\x01\xb3" + return prefix.startswith(b"\x00\x00\x01\xb3") ## diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index ef6ae87f8..277087a86 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -37,7 +37,7 @@ from ._binary import o16le as o16 def _accept(prefix: bytes) -> bool: - return prefix[:4] in [b"DanM", b"LinS"] + return prefix.startswith((b"DanM", b"LinS")) ## @@ -69,7 +69,7 @@ class MspImageFile(ImageFile.ImageFile): self._mode = "1" self._size = i16(s, 4), i16(s, 6) - if s[:4] == b"DanM": + if s.startswith(b"DanM"): self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")] else: self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 81652e5ee..2a26e5d4e 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -32,7 +32,7 @@ class PaletteFile: if not s: break - if s[:1] == b"#": + if s.startswith(b"#"): continue if len(s) > 100: msg = "bad palette file" diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index ac40383f9..3aa249988 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -34,7 +34,7 @@ class PcdImageFile(ImageFile.ImageFile): self.fp.seek(2048) s = self.fp.read(2048) - if s[:4] != b"PCD_": + if not s.startswith(b"PCD_"): msg = "not a PCD file" raise SyntaxError(msg) diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 5c465bbdc..d2b6d0a97 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -28,7 +28,7 @@ from ._binary import i16le as i16 def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\200\350\000\000" + return prefix.startswith(b"\200\350\000\000") ## diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 5ea87686d..4fc6217e1 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -740,7 +740,7 @@ class PngStream(ChunkStream): def _accept(prefix: bytes) -> bool: - return prefix[:8] == _MAGIC + return prefix.startswith(_MAGIC) ## diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index fb228f572..03afa2d2e 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -47,7 +47,7 @@ MODES = { def _accept(prefix: bytes) -> bool: - return prefix[0:1] == b"P" and prefix[1] in b"0123456fy" + return prefix.startswith(b"P") and prefix[1] in b"0123456fy" ## diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 8ff5e3908..c59d302e5 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -47,7 +47,7 @@ MODES = { def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"8BPS" + return prefix.startswith(b"8BPS") ## diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 01cc868b2..df552243e 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -14,7 +14,7 @@ from ._binary import i32be as i32 def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"qoif" + return prefix.startswith(b"qoif") class QoiImageFile(ImageFile.ImageFile): diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index f557d104b..0454038e8 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -288,7 +288,7 @@ if not getattr(Image.core, "libtiff_support_custom_tags", True): def _accept(prefix: bytes) -> bool: - return prefix[:4] in PREFIXES + return prefix.startswith(tuple(PREFIXES)) def _limit_rational( @@ -1280,7 +1280,7 @@ class TiffImageFile(ImageFile.ImageFile): blocks = {} val = self.tag_v2.get(ExifTags.Base.ImageResources) if val: - while val[:4] == b"8BIM": + while val.startswith(b"8BIM"): id = i16(val[4:6]) n = math.ceil((val[6] + 1) / 2) * 2 size = i32(val[6 + n : 10 + n]) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index cbbc24af0..c2dde4431 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -21,7 +21,7 @@ _VP8_MODES_BY_IDENTIFIER = { def _accept(prefix: bytes) -> bool | str: - is_riff_file_format = prefix[:4] == b"RIFF" + is_riff_file_format = prefix.startswith(b"RIFF") is_webp_file = prefix[8:12] == b"WEBP" is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 48e9823e8..04abd52f0 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -68,9 +68,7 @@ if hasattr(Image.core, "drawwmf"): def _accept(prefix: bytes) -> bool: - return ( - prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or prefix[:4] == b"\x01\x00\x00\x00" - ) + return prefix.startswith((b"\xd7\xcd\xc6\x9a\x00\x00", b"\x01\x00\x00\x00")) ## @@ -87,7 +85,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): # check placable header s = self.fp.read(80) - if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00": + if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): # placeable windows metafile # get units per inch @@ -116,7 +114,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): msg = "Unsupported WMF file format" raise SyntaxError(msg) - elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF": + elif s.startswith(b"\x01\x00\x00\x00") and s[40:44] == b" EMF": # enhanced metafile # get bounding box diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 75333354d..cde28388f 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -34,7 +34,7 @@ for r in range(8): def _accept(prefix: bytes) -> bool: - return prefix[:6] == _MAGIC + return prefix.startswith(_MAGIC) ## diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 943a04470..1e57aa162 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -38,7 +38,7 @@ xbm_head = re.compile( def _accept(prefix: bytes) -> bool: - return prefix.lstrip()[:7] == b"#define" + return prefix.lstrip().startswith(b"#define") ## diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index b985aa5dc..328f88223 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -25,7 +25,7 @@ xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') def _accept(prefix: bytes) -> bool: - return prefix[:9] == b"/* XPM */" + return prefix.startswith(b"/* XPM */") ## @@ -81,7 +81,7 @@ class XpmImageFile(ImageFile.ImageFile): rgb = s[i + 1] if rgb == b"None": self.info["transparency"] = c - elif rgb[:1] == b"#": + elif rgb.startswith(b"#"): # FIXME: handle colour names (see ImagePalette.py) rgb = int(rgb[1:], 16) palette[c] = ( From 9665eb39726b000bf59c0c5e61793fcbb3c6ecd0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 19:38:03 +0200 Subject: [PATCH 629/652] Replace slice and comparison with endswith --- src/PIL/ImImagePlugin.py | 4 ++-- src/PIL/MicImagePlugin.py | 2 +- src/PIL/XpmImagePlugin.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 270a29467..9f20b30f8 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -155,9 +155,9 @@ class ImImageFile(ImageFile.ImageFile): msg = "not an IM file" raise SyntaxError(msg) - if s[-2:] == b"\r\n": + if s.endswith(b"\r\n"): s = s[:-2] - elif s[-1:] == b"\n": + elif s.endswith(b"\n"): s = s[:-1] try: diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index eb10a4c82..bbddd972e 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -54,7 +54,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): self.images = [ path for path in self.ole.listdir() - if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image" + if path[1:] and path[0].endswith(".ACI") and path[1] == "Image" ] # if we didn't find any images, this is probably not diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 328f88223..3c932c41b 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -67,9 +67,9 @@ class XpmImageFile(ImageFile.ImageFile): for _ in range(pal): s = self.fp.readline() - if s[-2:] == b"\r\n": + if s.endswith(b"\r\n"): s = s[:-2] - elif s[-1:] in b"\r\n": + elif s.endswith((b"\r", b"\n")): s = s[:-1] c = s[1] From 4b7e75be2d9305ffa25e113740aa08ff5d4fed74 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Feb 2025 20:47:17 +1100 Subject: [PATCH 630/652] Test errors --- Tests/test_file_sun.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 6cfff8730..ebb069379 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,10 +1,11 @@ from __future__ import annotations +import io import os import pytest -from PIL import Image, SunImagePlugin +from PIL import Image, SunImagePlugin, _binary from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -33,6 +34,44 @@ def test_im1() -> None: assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png") +def _sun_header( + depth: int = 0, file_type: int = 0, palette_length: int = 0 +) -> io.BytesIO: + return io.BytesIO( + _binary.o32be(0x59A66A95) + + b"\x00" * 8 + + _binary.o32be(depth) + + b"\x00" * 4 + + _binary.o32be(file_type) + + b"\x00" * 4 + + _binary.o32be(palette_length) + ) + + +def test_unsupported_mode_bit_depth() -> None: + with pytest.raises(SyntaxError, match="Unsupported Mode/Bit Depth"): + with SunImagePlugin.SunImageFile(_sun_header()): + pass + + +def test_unsupported_color_palette_length() -> None: + with pytest.raises(SyntaxError, match="Unsupported Color Palette Length"): + with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1025)): + pass + + +def test_unsupported_palette_type() -> None: + with pytest.raises(SyntaxError, match="Unsupported Palette Type"): + with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1)): + pass + + +def test_unsupported_file_type() -> None: + with pytest.raises(SyntaxError, match="Unsupported Sun Raster file type"): + with SunImagePlugin.SunImageFile(_sun_header(depth=1, file_type=6)): + pass + + @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) From 5d40e6aead98fd7154c889b027c79f3e15c06661 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Feb 2025 20:29:35 +1100 Subject: [PATCH 631/652] Test RGBX raw mode --- Tests/test_file_sun.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index ebb069379..c2f162cf9 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -72,6 +72,22 @@ def test_unsupported_file_type() -> None: pass +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_rgbx() -> None: + with open(os.path.join(EXTRA_DIR, "32bpp.ras"), "rb") as fp: + data = fp.read() + + # Set file type to 3 + data = data[:20] + _binary.o32be(3) + data[24:] + + with Image.open(io.BytesIO(data)) as im: + r, g, b = im.split() + im = Image.merge("RGB", (b, g, r)) + assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png")) + + @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) From b096018fdd5c6c36211023fd372c16a66b7a52f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Feb 2025 22:27:13 +1100 Subject: [PATCH 632/652] Update Sphinx to 8.2 to remove nitpick ignore --- docs/conf.py | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e1e3f1b8f..bfbcf9151 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ import PIL # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "8.1" +needs_sphinx = "8.2" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -121,7 +121,7 @@ nitpicky = True # generating warnings in “nitpicky mode”. Note that type should include the domain name # if present. Example entries would be ('py:func', 'int') or # ('envvar', 'LD_LIBRARY_PATH'). -nitpick_ignore = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")] +nitpick_ignore = [("py:class", "_CmsProfileCompatible")] # -- Options for HTML output ---------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index aaaba0032..2ffd9faca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dynamic = [ optional-dependencies.docs = [ "furo", "olefile", - "sphinx>=8.1", + "sphinx>=8.2", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph", From 4415b4ad3631a96fb70610ba6672e5c14dcfa174 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Feb 2025 08:47:04 +1100 Subject: [PATCH 633/652] Updated libpng to 1.6.47 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f0c96d160..0f8eac5bb 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -39,7 +39,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=10.2.0 -LIBPNG_VERSION=1.6.46 +LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.4 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f942716cb..5665abaab 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ V = { "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBIMAGEQUANT": "4.3.4", - "LIBPNG": "1.6.46", + "LIBPNG": "1.6.47", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.6.0", From dc94d1d8bba208cf7f24e207a3536ac6eaf22fa9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Feb 2025 18:27:05 +1100 Subject: [PATCH 634/652] Test opening file with plugin directly --- Tests/test_file_mpo.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index ab8f2d5a1..6b4f6423b 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -29,12 +29,17 @@ def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile: @pytest.mark.parametrize("test_file", test_files) def test_sanity(test_file: str) -> None: - with Image.open(test_file) as im: + def check(im: ImageFile.ImageFile) -> None: im.load() assert im.mode == "RGB" assert im.size == (640, 480) assert im.format == "MPO" + with Image.open(test_file) as im: + check(im) + with MpoImagePlugin.MpoImageFile(test_file) as im: + check(im) + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: From ae6bb4cac2f666715666a05b46fa43942ef19201 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Feb 2025 23:28:25 +1100 Subject: [PATCH 635/652] Test invalid texture compression format --- Tests/test_file_ftex.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index 0c544245a..fdd7b3757 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,5 +1,8 @@ from __future__ import annotations +import io +import struct + import pytest from PIL import FtexImagePlugin, Image @@ -23,3 +26,15 @@ def test_invalid_file() -> None: with pytest.raises(SyntaxError): FtexImagePlugin.FtexImageFile(invalid_file) + + +def test_invalid_texture() -> None: + with open("Tests/images/ftex_dxt1.ftc", "rb") as fp: + data = fp.read() + + # Change texture compression format + data = data[:24] + struct.pack(" Date: Thu, 20 Feb 2025 07:57:10 +1100 Subject: [PATCH 636/652] Only set mode when necessary --- src/PIL/FtexImagePlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 26e5bd4a6..d60e75bb6 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -79,8 +79,6 @@ class FtexImageFile(ImageFile.ImageFile): self._size = struct.unpack("<2i", self.fp.read(8)) mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8)) - self._mode = "RGB" - # Only support single-format files. # I don't know of any multi-format file. assert format_count == 1 @@ -95,6 +93,7 @@ class FtexImageFile(ImageFile.ImageFile): self._mode = "RGBA" self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] elif format == Format.UNCOMPRESSED: + self._mode = "RGB" self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")] else: msg = f"Invalid texture compression format: {repr(format)}" From ae7c4920c9fba33b93c8938d07a9c16495dd6d69 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Feb 2025 08:09:44 +1100 Subject: [PATCH 637/652] Test that subsequent compile() calls do not change anything --- Tests/test_fontfile.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index 206499a04..575dada86 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -4,7 +4,20 @@ from pathlib import Path import pytest -from PIL import FontFile +from PIL import FontFile, Image + + +def test_compile() -> None: + font = FontFile.FontFile() + font.glyph[0] = ((0, 0), (0, 0, 0, 0), (0, 0, 0, 1), Image.new("L", (0, 0))) + font.compile() + assert font.ysize == 1 + + font.ysize = 2 + font.compile() + + # Assert that compiling again did not change anything + assert font.ysize == 2 def test_save(tmp_path: Path) -> None: From 85f439f575a1ae5e3e11262e9b0a6d838f541aa6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 25 Feb 2025 18:46:22 +1100 Subject: [PATCH 638/652] _seek_check already raises an EOFError --- src/PIL/MicImagePlugin.py | 7 +------ src/PIL/PsdImagePlugin.py | 14 +++++--------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index bbddd972e..9ce38c427 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -73,12 +73,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): def seek(self, frame: int) -> None: if not self._seek_check(frame): return - try: - filename = self.images[frame] - except IndexError as e: - msg = "no such frame" - raise EOFError(msg) from e - + filename = self.images[frame] self.fp = self.ole.openstream(filename) TiffImagePlugin.TiffImageFile._open(self) diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index c59d302e5..0aada8a06 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -169,15 +169,11 @@ class PsdImageFile(ImageFile.ImageFile): return # seek to given layer (1..max) - try: - _, mode, _, tile = self.layers[layer - 1] - self._mode = mode - self.tile = tile - self.frame = layer - self.fp = self._fp - except IndexError as e: - msg = "no such layer" - raise EOFError(msg) from e + _, mode, _, tile = self.layers[layer - 1] + self._mode = mode + self.tile = tile + self.frame = layer + self.fp = self._fp def tell(self) -> int: # return layer number (0=image, 1..max=layers) From 153fd4801c2344a41260fe8b4b83bd491e51f535 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 27 Feb 2025 22:24:48 +1100 Subject: [PATCH 639/652] Revert "Do not install libimagequant" This reverts commit 1e115987afbc92aef02b489ed8fea1875821d174. --- .github/workflows/test-mingw.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 045926482..bb6d7dc37 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -60,6 +60,7 @@ jobs: mingw-w64-x86_64-gcc \ mingw-w64-x86_64-ghostscript \ mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libjpeg-turbo \ mingw-w64-x86_64-libraqm \ mingw-w64-x86_64-libtiff \ From d6b94421d0eff83249adf0c4191d3b3eda2b5b90 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Mar 2025 11:37:49 +1100 Subject: [PATCH 640/652] Updated harfbuzz to 10.4.0 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f0c96d160..50b7ad488 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -38,7 +38,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 -HARFBUZZ_VERSION=10.2.0 +HARFBUZZ_VERSION=10.4.0 LIBPNG_VERSION=1.6.46 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f942716cb..c21258cb9 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "10.2.0", + "HARFBUZZ": "10.4.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBIMAGEQUANT": "4.3.4", From ff4f5d4cb68f6cfe9713dd72ad343e505b57b1f5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Mar 2025 21:41:30 +1100 Subject: [PATCH 641/652] Test ValueError --- Tests/test_font_bdf.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 136070f9e..8d78019b3 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,5 +1,7 @@ from __future__ import annotations +import io + import pytest from PIL import BdfFontFile, FontFile @@ -8,13 +10,20 @@ filename = "Tests/images/courB08.bdf" def test_sanity() -> None: - with open(filename, "rb") as test_file: - font = BdfFontFile.BdfFontFile(test_file) + with open(filename, "rb") as fp: + font = BdfFontFile.BdfFontFile(fp) assert isinstance(font, FontFile.FontFile) assert len([_f for _f in font.glyph if _f]) == 190 +def test_valueerror() -> None: + with open(filename, "rb") as fp: + data = fp.read() + data = data[:2650] + b"\x00\x00" + data[2652:] + BdfFontFile.BdfFontFile(io.BytesIO(data)) + + def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): From 397f8c752b583cd781fa21fe03149c8781035247 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 1 Mar 2025 20:50:23 +0000 Subject: [PATCH 642/652] Update dependency cibuildwheel to v2.23.0 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 833aca23d..2fd3eb6ff 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.22.0 +cibuildwheel==2.23.0 From db4534a8cf35bd7d3a531f5e19e889c7b7c63c30 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:00:26 +0200 Subject: [PATCH 643/652] Build PyPy3.11 wheel for macOS 10.15 x86_64 --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index db8e4d58b..1fe6badae 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -63,7 +63,7 @@ jobs: - name: "macOS 10.15 x86_64" os: macos-13 cibw_arch: x86_64 - build: "pp310*" + build: "pp3*" macosx_deployment_target: "10.15" - name: "macOS arm64" os: macos-latest From c60682af67a364fe185c3b9eea0477980af07cdd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 2 Mar 2025 22:34:58 +1100 Subject: [PATCH 644/652] JPEG comments are from the COM marker --- docs/handbook/image-file-formats.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a915ee4e2..991cadaa2 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -454,7 +454,8 @@ The :py:meth:`~PIL.Image.open` method may set the following Raw EXIF data from the image. **comment** - A comment about the image. + A comment about the image, from the COM marker. This is separate from the + UserComment tag that may be stored in the EXIF data. .. versionadded:: 7.1.0 From ebc7a17d86a7789119e1b9c4ea5d186306ed276c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Mar 2025 07:24:13 +1100 Subject: [PATCH 645/652] Removed _show --- src/PIL/ImageTk.py | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index bf29fdba5..e6a9d8eea 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -28,7 +28,7 @@ from __future__ import annotations import tkinter from io import BytesIO -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from . import Image, ImageFile @@ -263,28 +263,3 @@ def getimage(photo: PhotoImage) -> Image.Image: _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim()) return im - - -def _show(image: Image.Image, title: str | None) -> None: - """Helper for the Image.show method.""" - - class UI(tkinter.Label): - def __init__(self, master: tkinter.Toplevel, im: Image.Image) -> None: - self.image: BitmapImage | PhotoImage - if im.mode == "1": - self.image = BitmapImage(im, foreground="white", master=master) - else: - self.image = PhotoImage(im, master=master) - if TYPE_CHECKING: - image = cast(tkinter._Image, self.image) - else: - image = self.image - super().__init__(master, image=image, bg="black", bd=0) - - if not getattr(tkinter, "_default_root"): - msg = "tkinter not initialized" - raise OSError(msg) - top = tkinter.Toplevel() - if title: - top.title(title) - UI(top, image).pack() From 2d97521aa3a7a5f4ab114354e881e05916fae483 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 02:38:52 +0000 Subject: [PATCH 646/652] Update dependency mypy to v1.15.0 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 10e59b885..2e3610478 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.14.1 +mypy==1.15.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From d6272297fc6c8e2e796c264b4229e5d20045aa9c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Mar 2025 14:48:00 +1100 Subject: [PATCH 647/652] Ignore override --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0454038e8..3d36d1abc 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -404,7 +404,7 @@ class IFDRational(Rational): def __repr__(self) -> str: return str(float(self._val)) - def __hash__(self) -> int: + def __hash__(self) -> int: # type: ignore[override] return self._val.__hash__() def __eq__(self, other: object) -> bool: From 4161bb1645fc66c9d587aafc53214f797831fa52 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Mar 2025 19:10:55 +1100 Subject: [PATCH 648/652] Corrected error when XMP is tuple --- Tests/test_imageops.py | 9 +++++++++ src/PIL/ImageOps.py | 14 +++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 3621aa50f..9f2fd5ba2 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -448,6 +448,15 @@ def test_exif_transpose() -> None: assert 0x0112 not in transposed_im.getexif() +def test_exif_transpose_with_xmp_tuple() -> None: + with Image.open("Tests/images/xmp_tags_orientation.png") as im: + assert im.getexif()[0x0112] == 3 + + im.info["xmp"] = (b"test",) + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() + + def test_exif_transpose_xml_without_xmp() -> None: with Image.open("Tests/images/xmp_tags_orientation.png") as im: assert im.getexif()[0x0112] == 3 diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index fef1d7328..75dfbee22 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -729,11 +729,15 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image r"([0-9])", ): value = exif_image.info[key] - exif_image.info[key] = ( - re.sub(pattern, "", value) - if isinstance(value, str) - else re.sub(pattern.encode(), b"", value) - ) + if isinstance(value, str): + value = re.sub(pattern, "", value) + elif isinstance(value, tuple): + value = tuple( + re.sub(pattern.encode(), b"", v) for v in value + ) + else: + value = re.sub(pattern.encode(), b"", value) + exif_image.info[key] = value if not in_place: return transposed_image elif not in_place: From 51183c22042303e464d86a26245c272f733f35f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Mar 2025 21:58:29 +1100 Subject: [PATCH 649/652] Fixed loading images --- Tests/test_file_gd.py | 3 +++ src/PIL/GdImageFile.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index d512df284..806532c17 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -4,6 +4,8 @@ import pytest from PIL import GdImageFile, UnidentifiedImageError +from .helper import assert_image_similar_tofile + TEST_GD_FILE = "Tests/images/hopper.gd" @@ -11,6 +13,7 @@ def test_sanity() -> None: with GdImageFile.open(TEST_GD_FILE) as im: assert im.size == (128, 128) assert im.format == "GD" + assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14) def test_bad_mode() -> None: diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index fc4801e9d..891225ce2 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -56,7 +56,7 @@ class GdImageFile(ImageFile.ImageFile): msg = "Not a valid GD 2.x .gd file" raise SyntaxError(msg) - self._mode = "L" # FIXME: "P" + self._mode = "P" self._size = i16(s, 2), i16(s, 4) true_color = s[6] @@ -68,14 +68,14 @@ class GdImageFile(ImageFile.ImageFile): self.info["transparency"] = tindex self.palette = ImagePalette.raw( - "XBGR", s[7 + true_color_offset + 4 : 7 + true_color_offset + 4 + 256 * 4] + "RGBX", s[7 + true_color_offset + 6 : 7 + true_color_offset + 6 + 256 * 4] ) self.tile = [ ImageFile._Tile( "raw", (0, 0) + self.size, - 7 + true_color_offset + 4 + 256 * 4, + 7 + true_color_offset + 6 + 256 * 4, "L", ) ] From a1a467bda2d79baa70775c6cd0d52ddcc1496ee8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Mar 2025 23:55:19 +1100 Subject: [PATCH 650/652] Image.core.outline will no longer raise an AttributeError --- Tests/test_imagedraw.py | 4 ---- src/PIL/ImageDraw.py | 6 +----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index d127175eb..232cbb16c 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -448,7 +448,6 @@ def test_shape1() -> None: x3, y3 = 95, 5 # Act - assert ImageDraw.Outline is not None s = ImageDraw.Outline() s.move(x0, y0) s.curve(x1, y1, x2, y2, x3, y3) @@ -470,7 +469,6 @@ def test_shape2() -> None: x3, y3 = 5, 95 # Act - assert ImageDraw.Outline is not None s = ImageDraw.Outline() s.move(x0, y0) s.curve(x1, y1, x2, y2, x3, y3) @@ -489,7 +487,6 @@ def test_transform() -> None: draw = ImageDraw.Draw(im) # Act - assert ImageDraw.Outline is not None s = ImageDraw.Outline() s.line(0, 0) s.transform((0, 0, 0, 0, 0, 0)) @@ -1526,7 +1523,6 @@ def test_same_color_outline(bbox: Coords) -> None: x2, y2 = 95, 50 x3, y3 = 95, 5 - assert ImageDraw.Outline is not None s = ImageDraw.Outline() s.move(x0, y0) s.curve(x1, y1, x2, y2, x3, y3) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 742b5f587..c4ebc5931 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -42,11 +42,7 @@ from ._deprecate import deprecate from ._typing import Coords # experimental access to the outline API -Outline: Callable[[], Image.core._Outline] | None -try: - Outline = Image.core.outline -except AttributeError: - Outline = None +Outline: Callable[[], Image.core._Outline] = Image.core.outline if TYPE_CHECKING: from . import ImageDraw2, ImageFont From c1703f53307e668a1eef54e78350f22bb5cbe194 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:15:48 +0000 Subject: [PATCH 651/652] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.9.4 → v0.9.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.4...v0.9.9) - [github.com/PyCQA/bandit: 1.8.2 → 1.8.3](https://github.com/PyCQA/bandit/compare/1.8.2...1.8.3) - [github.com/python-jsonschema/check-jsonschema: 0.31.1 → 0.31.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.1...0.31.2) - [github.com/woodruffw/zizmor-pre-commit: v1.3.0 → v1.4.1](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.3.0...v1.4.1) - [github.com/tox-dev/pyproject-fmt: v2.5.0 → v2.5.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.5.0...v2.5.1) --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a8c8cee15..5ff947d41 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.4 + rev: v0.9.9 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.8.2 + rev: 1.8.3 hooks: - id: bandit args: [--severity-level=high] @@ -50,14 +50,14 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.31.1 + rev: 0.31.2 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.3.0 + rev: v1.4.1 hooks: - id: zizmor @@ -67,7 +67,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.5.0 + rev: v2.5.1 hooks: - id: pyproject-fmt From 5ce8929ed467712025c82311f0cdfa196224a06a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 4 Mar 2025 07:48:12 +1100 Subject: [PATCH 652/652] Updated test name Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_font_bdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 8d78019b3..2ece5457a 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -17,7 +17,7 @@ def test_sanity() -> None: assert len([_f for _f in font.glyph if _f]) == 190 -def test_valueerror() -> None: +def test_zero_width_chars() -> None: with open(filename, "rb") as fp: data = fp.read() data = data[:2650] + b"\x00\x00" + data[2652:]