From a34ac045c3cf191a55e52797263beb352c8edb2c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jul 2024 21:05:48 +1000 Subject: [PATCH 001/126] 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 e938283eb9ff28fbd18432eae3aa5b0ec79967d3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Aug 2024 20:56:14 +1000 Subject: [PATCH 002/126] 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 2303c063cb9660c8fdd4a5d757e1d971cd47dcf8 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 01:24:51 +0400 Subject: [PATCH 003/126] 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 004/126] 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 005/126] 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 006/126] 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 007/126] 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 008/126] 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 009/126] 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 010/126] 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 bf11639626373bd27a80f790d56101d4b4e2f1c6 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 3 Sep 2024 11:54:24 +0400 Subject: [PATCH 011/126] 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 8833548e55977493fcd21b8270ba515e0503a3df Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Thu, 5 Sep 2024 01:04:35 +0400 Subject: [PATCH 012/126] 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 6f9128bfaf4c91a32e075847fa2c629d826c7eaf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 14:28:36 +1000 Subject: [PATCH 013/126] 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 fe002a72606ff33279ce798f77154f5bf8dcef17 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 01:24:51 +0400 Subject: [PATCH 014/126] 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 015/126] 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 016/126] 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 017/126] 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 018/126] 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 019/126] 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 020/126] 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 021/126] 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 022/126] 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 023/126] 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 d25d12f792c88b3bae881c9090b13d2a9f0e3d84 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 11:29:52 +1000 Subject: [PATCH 024/126] 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 a2988da0d2e0057726687c8030b5f2ea2b007f8f Mon Sep 17 00:00:00 2001 From: Alexander Karpinsky Date: Wed, 11 Sep 2024 19:55:07 +0400 Subject: [PATCH 025/126] =?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 6921f83629242d199eaab87af55da6279588a8e0 Mon Sep 17 00:00:00 2001 From: Alexander Karpinsky Date: Fri, 13 Sep 2024 22:54:16 +0200 Subject: [PATCH 026/126] 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 1f3fe6f73350aa9630b236a9f16370aa0c71258d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Sep 2024 10:21:17 +1000 Subject: [PATCH 027/126] 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 028/126] 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 bc973690397a075b94f694ee1cb3f3392fff9cdc Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 16 Sep 2024 15:45:34 +0200 Subject: [PATCH 029/126] 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 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 030/126] 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 aa22b241690e0c0084523bc7e4ea137d3596ee93 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Sep 2024 16:22:50 +1000 Subject: [PATCH 031/126] 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 032/126] 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 033/126] 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 034/126] 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 0a99d63028ebcc92d950cd63cd805eb558a5131c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 20 Sep 2024 22:31:00 +1000 Subject: [PATCH 035/126] 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 036/126] 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 7cf351cfc50dcbda82270c80e44e32643688a7a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Sep 2024 20:37:41 +1000 Subject: [PATCH 037/126] 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 b9d176856135b581bd503e04ab2407318cbe7689 Mon Sep 17 00:00:00 2001 From: Alexander Karpinsky Date: Thu, 26 Sep 2024 16:27:55 +0400 Subject: [PATCH 038/126] 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 6fe4375f28015f0c1a7c3cb6fbc9ea3c4f405679 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 3 Aug 2024 11:13:38 -0500 Subject: [PATCH 039/126] 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 040/126] 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 041/126] 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 042/126] 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 043/126] 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 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 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 044/126] 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 045/126] 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 046/126] 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 047/126] 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 c0d04e8b34f759797e45bccac74f49bddfaa7a41 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 1 Oct 2024 09:33:33 -0500 Subject: [PATCH 048/126] 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 049/126] 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 07be6fca17fc31b3df87bc8b577822ed5b1de51c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Oct 2024 23:26:54 +1000 Subject: [PATCH 050/126] 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 051/126] 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 052/126] 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 053/126] 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 054/126] 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 055/126] 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 056/126] 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 057/126] 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 058/126] 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 059/126] 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 060/126] 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 061/126] 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 062/126] 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 063/126] 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 064/126] 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 065/126] [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 066/126] [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 067/126] 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 068/126] 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 069/126] 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 070/126] 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 071/126] 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 072/126] 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 073/126] 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 074/126] 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 075/126] 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 076/126] 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 077/126] 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 078/126] 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 079/126] 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 080/126] 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 081/126] 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 082/126] 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 083/126] 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 084/126] 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 085/126] 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 086/126] 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 087/126] 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 088/126] 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 089/126] 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 090/126] 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 d59b169ed257ca14d86c90dbf3384b8b3999fc5c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Oct 2024 23:10:48 +1100 Subject: [PATCH 091/126] 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 092/126] 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 5b065970753d464c0d283f9e0c2f4a0971800ed1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 19:29:22 +1100 Subject: [PATCH 093/126] 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 094/126] 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 095/126] 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 096/126] 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 097/126] 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 c8e301c47456114f245261fccd316b4fd8964f3e Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 24 Oct 2024 15:52:59 +0200 Subject: [PATCH 098/126] 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 099/126] [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 c46946f3a4c89a52637cc8e0c0f67d8e85d18a16 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Oct 2024 19:13:39 +1100 Subject: [PATCH 100/126] 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 101/126] 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 bb3515d649cda179037161ee3bea264a4d289b32 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Fri, 25 Oct 2024 17:32:29 +0200 Subject: [PATCH 102/126] 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 a43e5bb7354006766a67a353aa21d9a0198cdcab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 14:26:47 +1100 Subject: [PATCH 103/126] 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 104/126] 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 105/126] 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 106/126] 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 29cdbce39e1d3860486319b46ea829e0c3566279 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 21:13:01 +1100 Subject: [PATCH 107/126] 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 108/126] 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 109/126] 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 2d1d801ec032bfb760bede88da6dcaab5ff3c75e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Oct 2024 22:15:56 +1100 Subject: [PATCH 110/126] 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 111/126] 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 112/126] 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 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 113/126] 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 114/126] 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 71016f23b4b441536a5c713d3cad53e2870655fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Nov 2024 17:51:01 +1100 Subject: [PATCH 115/126] 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 116/126] 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 117/126] 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 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 118/126] 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 119/126] 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 120/126] 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 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 121/126] [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 122/126] 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 2d23a84049eb22f62107eb5f4cbe7c421b074d12 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 17:18:46 +1100 Subject: [PATCH 123/126] 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 124/126] 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 b0345c1c7b7605063064118563e8068fde5f9343 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 Nov 2024 22:08:22 +1100 Subject: [PATCH 125/126] 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 126/126] 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",