From 5f80642579d46f714e2005f4827ab71ef5de9228 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Jun 2026 00:20:37 +1000 Subject: [PATCH 1/5] Inspect arg as Python object, instead of using PyErr_Clear() --- src/_imaging.c | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 09793f36bea..c7a8d10a9b4 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1049,8 +1049,11 @@ static PyObject * _convert_matrix(ImagingObject *self, PyObject *args) { char *mode_name; float m[12]; - if (!PyArg_ParseTuple(args, "s(ffff)", &mode_name, m + 0, m + 1, m + 2, m + 3)) { - PyErr_Clear(); + PyObject *matrix; + if (!PyArg_ParseTuple(args, "sO", &mode_name, &matrix)) { + return NULL; + } + if (PyTuple_Size(matrix) == 12) { if (!PyArg_ParseTuple( args, "s(ffffffffffff)", @@ -1070,27 +1073,35 @@ _convert_matrix(ImagingObject *self, PyObject *args) { )) { return NULL; } + } else if (!PyArg_ParseTuple( + args, "s(ffff)", &mode_name, m + 0, m + 1, m + 2, m + 3 + )) { + return NULL; } const ModeID mode = findModeID(mode_name); - return PyImagingNew(ImagingConvertMatrix(self->image, mode, m)); } static PyObject * _convert_transparent(ImagingObject *self, PyObject *args) { char *mode_name; - int r, g, b; - if (PyArg_ParseTuple(args, "s(iii)", &mode_name, &r, &g, &b)) { - const ModeID mode = findModeID(mode_name); - return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, g, b)); + int r, g = 0, b = 0; + PyObject *transparency; + if (!PyArg_ParseTuple(args, "sO", &mode_name, &transparency)) { + return NULL; } - PyErr_Clear(); - if (PyArg_ParseTuple(args, "si", &mode_name, &r)) { - const ModeID mode = findModeID(mode_name); - return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, 0, 0)); + + if (PyTuple_Check(transparency)) { + if (!PyArg_ParseTuple(args, "s(iii)", &mode_name, &r, &g, &b)) { + return NULL; + } + } else if (!PyArg_ParseTuple(args, "si", &mode_name, &r)) { + return NULL; } - return NULL; + + const ModeID mode = findModeID(mode_name); + return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, g, b)); } static PyObject * From d3b91dea1c4c7ec2528a0f881d38183c9a923c01 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Jun 2026 13:54:21 +1000 Subject: [PATCH 2/5] Check for PyTuple_Size() failure --- src/_imaging.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index c7a8d10a9b4..a87997773ac 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1053,7 +1053,11 @@ _convert_matrix(ImagingObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "sO", &mode_name, &matrix)) { return NULL; } - if (PyTuple_Size(matrix) == 12) { + Py_ssize_t size = PyTuple_Size(matrix); + if (size == -1) { + return NULL; + } + if (size == 12) { if (!PyArg_ParseTuple( args, "s(ffffffffffff)", From a5a7a1802f2a5658a9c0b3f3eb6ad8130ccf08ab Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:56:53 +1000 Subject: [PATCH 3/5] Allow for transparency to be a list Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/_imaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index a87997773ac..aa023682a22 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1096,7 +1096,7 @@ _convert_transparent(ImagingObject *self, PyObject *args) { return NULL; } - if (PyTuple_Check(transparency)) { + if (PySequence_Check(transparency)) { if (!PyArg_ParseTuple(args, "s(iii)", &mode_name, &r, &g, &b)) { return NULL; } From a99854d50fa1575e52d4c05b2bdc951d2bb90bfe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Jun 2026 14:54:27 +1000 Subject: [PATCH 4/5] Added test --- Tests/test_image_convert.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 547a6c2c678..68d97978d82 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -225,6 +225,10 @@ def test_trns_RGB(tmp_path: Path) -> None: assert "transparency" not in im_p.info im_p.save(f) + im.info["transparency"] = list(im.info["transparency"]) + im_rgba = im.convert("RGBA") + assert "transparency" not in im_rgba.info + im = Image.new("RGB", (1, 1)) im.info["transparency"] = im.getpixel((0, 0)) im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) From 91979397098daf7638a79beafa9573a3c9dcae41 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Jun 2026 21:00:32 +1000 Subject: [PATCH 5/5] Accept list as conversion matrix --- Tests/test_image_convert.py | 11 ++++++++--- src/PIL/Image.py | 6 +++--- src/_imaging.c | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 68d97978d82..249b52ca8a0 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -357,19 +357,24 @@ def test_matrix_xyz(mode: str) -> None: def test_matrix_identity() -> None: # Arrange - im = hopper("RGB") + im = hopper() + assert im.mode == "RGB" + # fmt: off identity_matrix = ( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0) # fmt: on - assert im.mode == "RGB" # Act # Convert with an identity matrix - converted_im = im.convert(mode="RGB", matrix=identity_matrix) + converted_im = im.convert("RGB", identity_matrix) # Assert # No change assert_image_equal(converted_im, im) + + # Test list + converted_im = im.convert("RGB", list(identity_matrix)) + assert_image_equal(converted_im, im) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ebbd8fd35f8..90fe6aa63ab 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1017,7 +1017,7 @@ def verify(self) -> None: def convert( self, mode: str | None = None, - matrix: tuple[float, ...] | None = None, + matrix: list[float] | tuple[float, ...] | None = None, dither: Dither | None = None, palette: Palette = Palette.WEB, colors: int = 256, @@ -1052,7 +1052,7 @@ def convert( :param mode: The requested mode. See: :ref:`concept-modes`. :param matrix: An optional conversion matrix. If given, this - should be 4- or 12-tuple containing floating point values. + should be 4- or 12-sequence containing floating point values. :param dither: Dithering method, used when converting from mode "RGB" to "P" or from "RGB" or "L" to "1". Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` @@ -1091,7 +1091,7 @@ def convert( transparency = new_im.info["transparency"] def convert_transparency( - m: tuple[float, ...], v: tuple[int, int, int] + m: list[float] | tuple[float, ...], v: tuple[int, int, int] ) -> int: value = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 return max(0, min(255, int(value))) diff --git a/src/_imaging.c b/src/_imaging.c index aa023682a22..c7917a23715 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1053,7 +1053,7 @@ _convert_matrix(ImagingObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "sO", &mode_name, &matrix)) { return NULL; } - Py_ssize_t size = PyTuple_Size(matrix); + Py_ssize_t size = PySequence_Size(matrix); if (size == -1) { return NULL; }