From 0abd9bbca3c598258ecb4359946e8b5ad8bd9a26 Mon Sep 17 00:00:00 2001 From: shivanshjain456 <227450065+shivanshjain456@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:55:01 +0000 Subject: [PATCH 1/9] Fix ImagePath coordinate conversion errors --- Tests/test_imagedraw.py | 7 +++++++ Tests/test_imagepath.py | 23 +++++++++++++++++++++++ docs/releasenotes/13.1.0.rst | 8 ++++++++ src/path.c | 31 +++++++++++++++++++++++++------ 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 86df0fc4078..ab206fc5905 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,6 +1,7 @@ from __future__ import annotations import os.path +from decimal import Decimal import pytest @@ -68,6 +69,12 @@ def test_sanity() -> None: draw.rectangle(list(range(4))) +def test_line_invalid_coordinate_conversion() -> None: + draw = ImageDraw.Draw(Image.new("RGB", (4, 4))) + with pytest.raises(ValueError): + draw.line([Decimal("sNaN"), 0, 1, 1]) + + def test_new_color() -> None: with Image.open("Tests/images/chi.gif") as im: draw = ImageDraw.Draw(im) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 1e291ba7c46..efb6a57e133 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -3,6 +3,7 @@ import array import math import struct +from decimal import Decimal import pytest @@ -87,6 +88,28 @@ def test_path_list_of_lists( assert list(p) == expected +def test_path_item_assignment_accepts_list() -> None: + p = ImagePath.Path([(0, 1), (2, 3)]) + + p[0] = [4, 5] + assert p[0] == (4.0, 5.0) + + with pytest.raises(TypeError): + p[1] = [6, "not a coordinate"] + assert p[1] == (2.0, 3.0) + + +def test_path_coordinate_conversion_errors() -> None: + with pytest.raises(ValueError): + ImagePath.Path([Decimal("sNaN"), 0]) + + large_int = (1 << 30) + 1 + assert ImagePath.Path([large_int, 0])[0] == (float(large_int), 0.0) + + with pytest.raises(OverflowError): + ImagePath.Path([1 << 1024, 0]) + + @pytest.mark.parametrize( "coords, message", ( diff --git a/docs/releasenotes/13.1.0.rst b/docs/releasenotes/13.1.0.rst index 140bcbe3115..3c2f507a717 100644 --- a/docs/releasenotes/13.1.0.rst +++ b/docs/releasenotes/13.1.0.rst @@ -54,3 +54,11 @@ Blurring I;16 images :py:class:`~PIL.ImageFilter.GaussianBlur` and :py:class:`~PIL.ImageFilter.BoxBlur` now support I;16, I;16L, I;16B and I;16N images. + +ImagePath.Path coordinate handling +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.ImagePath.Path` item assignment now accepts two-element lists. +Errors during coordinate conversion are raised as normal Python exceptions, and +integer coordinates are converted without losing precision through an intermediate +C ``float``. diff --git a/src/path.c b/src/path.c index 9ad15e1938c..7b6fc62133d 100644 --- a/src/path.c +++ b/src/path.c @@ -117,9 +117,17 @@ assign_item_to_array(double *xy, Py_ssize_t j, PyObject *op) { if (PyFloat_Check(op)) { xy[j++] = PyFloat_AS_DOUBLE(op); } else if (PyLong_Check(op)) { - xy[j++] = (float)PyLong_AS_LONG(op); + double value = PyLong_AsDouble(op); + if (value == -1.0 && PyErr_Occurred()) { + return -1; + } + xy[j++] = value; } else if (PyNumber_Check(op)) { - xy[j++] = PyFloat_AsDouble(op); + double value = PyFloat_AsDouble(op); + if (value == -1.0 && PyErr_Occurred()) { + return -1; + } + xy[j++] = value; } else if (PyList_Check(op)) { if (PyList_GET_SIZE(op) != 2) { PyErr_SetString( @@ -139,9 +147,11 @@ assign_item_to_array(double *xy, Py_ssize_t j, PyObject *op) { } Py_DECREF(op1); if (j == -1) { - PyErr_SetString( - PyExc_ValueError, "coordinate list must contain numbers" - ); + if (!PyErr_Occurred()) { + PyErr_SetString( + PyExc_ValueError, "coordinate list must contain numbers" + ); + } return -1; } } @@ -488,10 +498,19 @@ path_setitem(PyPathObject *self, Py_ssize_t i, PyObject *op) { xy = &self->xy[i + i]; - if (!PyArg_ParseTuple(op, "dd", &xy[0], &xy[1])) { + double x, y; + PyObject *coordinates = PySequence_Tuple(op); + if (coordinates == NULL) { + return -1; + } + int ok = PyArg_ParseTuple(coordinates, "dd", &x, &y); + Py_DECREF(coordinates); + if (!ok) { return -1; } + xy[0] = x; + xy[1] = y; return 0; } From fdffff35b0d991356aa87a94dc057e24111f4bc5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Oct 2026 08:40:47 +1100 Subject: [PATCH 2/9] Lint fix --- Tests/test_imagedraw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index ab206fc5905..dc584d2733b 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -72,7 +72,7 @@ def test_sanity() -> None: def test_line_invalid_coordinate_conversion() -> None: draw = ImageDraw.Draw(Image.new("RGB", (4, 4))) with pytest.raises(ValueError): - draw.line([Decimal("sNaN"), 0, 1, 1]) + draw.line([Decimal("sNaN"), 0, 1, 1]) # type: ignore[arg-type] def test_new_color() -> None: From 7f53c9d7769d0f7f32a2e80b887e79333e075a2c Mon Sep 17 00:00:00 2001 From: shivanshjain456 <227450065+shivanshjain456@users.noreply.github.com> Date: Fri, 23 Oct 2026 05:42:27 +0000 Subject: [PATCH 3/9] Test invalid ImagePath assignment input --- Tests/test_imagepath.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index efb6a57e133..e56b661a024 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -98,6 +98,10 @@ def test_path_item_assignment_accepts_list() -> None: p[1] = [6, "not a coordinate"] assert p[1] == (2.0, 3.0) + with pytest.raises(TypeError): + p[1] = None # type: ignore[arg-type] + assert p[1] == (2.0, 3.0) + def test_path_coordinate_conversion_errors() -> None: with pytest.raises(ValueError): From 4863497d8a55de4df0e60f91d50dccbc41773bc4 Mon Sep 17 00:00:00 2001 From: shivanshjain456 <227450065+shivanshjain456@users.noreply.github.com> Date: Fri, 23 Oct 2026 05:52:05 +0000 Subject: [PATCH 4/9] Test successful Decimal coordinate conversion --- Tests/test_imagepath.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index e56b661a024..87ceeb6b029 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -107,6 +107,8 @@ def test_path_coordinate_conversion_errors() -> None: with pytest.raises(ValueError): ImagePath.Path([Decimal("sNaN"), 0]) + assert ImagePath.Path([Decimal("1.5"), 0])[0] == (1.5, 0.0) + large_int = (1 << 30) + 1 assert ImagePath.Path([large_int, 0])[0] == (float(large_int), 0.0) From 9ea33b09790d1ab114dea84ede82cbd13359e64d Mon Sep 17 00:00:00 2001 From: shivanshjain456 <227450065+shivanshjain456@users.noreply.github.com> Date: Fri, 23 Oct 2026 07:28:08 +0000 Subject: [PATCH 5/9] Remove unused type ignore in ImagePath test --- Tests/test_imagepath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 87ceeb6b029..56bcffdcc2a 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -99,7 +99,7 @@ def test_path_item_assignment_accepts_list() -> None: assert p[1] == (2.0, 3.0) with pytest.raises(TypeError): - p[1] = None # type: ignore[arg-type] + p[1] = None assert p[1] == (2.0, 3.0) From 7ade0f84ce1f4ed1a77e0720c0287b63a05fb7d6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Oct 2026 22:46:55 +1100 Subject: [PATCH 6/9] Do not accept list for Path assignment --- Tests/test_imagedraw.py | 2 +- Tests/test_imagepath.py | 19 +------------------ docs/releasenotes/13.1.0.rst | 8 -------- src/path.c | 12 ++++-------- 4 files changed, 6 insertions(+), 35 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index dc584d2733b..4f615f3ed1d 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -71,7 +71,7 @@ def test_sanity() -> None: def test_line_invalid_coordinate_conversion() -> None: draw = ImageDraw.Draw(Image.new("RGB", (4, 4))) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="cannot convert signaling NaN to float"): draw.line([Decimal("sNaN"), 0, 1, 1]) # type: ignore[arg-type] diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 56bcffdcc2a..75f9b97adba 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -88,27 +88,10 @@ def test_path_list_of_lists( assert list(p) == expected -def test_path_item_assignment_accepts_list() -> None: - p = ImagePath.Path([(0, 1), (2, 3)]) - - p[0] = [4, 5] - assert p[0] == (4.0, 5.0) - - with pytest.raises(TypeError): - p[1] = [6, "not a coordinate"] - assert p[1] == (2.0, 3.0) - - with pytest.raises(TypeError): - p[1] = None - assert p[1] == (2.0, 3.0) - - def test_path_coordinate_conversion_errors() -> None: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="cannot convert signaling NaN to float"): ImagePath.Path([Decimal("sNaN"), 0]) - assert ImagePath.Path([Decimal("1.5"), 0])[0] == (1.5, 0.0) - large_int = (1 << 30) + 1 assert ImagePath.Path([large_int, 0])[0] == (float(large_int), 0.0) diff --git a/docs/releasenotes/13.1.0.rst b/docs/releasenotes/13.1.0.rst index 5814a144779..5edc0d28693 100644 --- a/docs/releasenotes/13.1.0.rst +++ b/docs/releasenotes/13.1.0.rst @@ -61,11 +61,3 @@ Blurring I;16 images :py:class:`~PIL.ImageFilter.GaussianBlur` and :py:class:`~PIL.ImageFilter.BoxBlur` now support I;16, I;16L, I;16B and I;16N images. - -ImagePath.Path coordinate handling -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:class:`~PIL.ImagePath.Path` item assignment now accepts two-element lists. -Errors during coordinate conversion are raised as normal Python exceptions, and -integer coordinates are converted without losing precision through an intermediate -C ``float``. diff --git a/src/path.c b/src/path.c index 7b6fc62133d..74fed97c0db 100644 --- a/src/path.c +++ b/src/path.c @@ -498,19 +498,15 @@ path_setitem(PyPathObject *self, Py_ssize_t i, PyObject *op) { xy = &self->xy[i + i]; - double x, y; - PyObject *coordinates = PySequence_Tuple(op); - if (coordinates == NULL) { + if (!PyTuple_Check(op)) { + PyErr_SetString(PyExc_TypeError, "argument must be tuple of length 2"); return -1; } - int ok = PyArg_ParseTuple(coordinates, "dd", &x, &y); - Py_DECREF(coordinates); - if (!ok) { + + if (!PyArg_ParseTuple(op, "dd", &xy[0], &xy[1])) { return -1; } - xy[0] = x; - xy[1] = y; return 0; } From 7170bf7e65f41e9321ff362e665ec29159170ec9 Mon Sep 17 00:00:00 2001 From: shivanshjain456 <227450065+shivanshjain456@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:19:39 +0000 Subject: [PATCH 7/9] Test tuple-only Path assignment across runtimes --- Tests/test_imagedraw.py | 2 +- Tests/test_imagepath.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4f615f3ed1d..871c016c81e 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -71,7 +71,7 @@ def test_sanity() -> None: def test_line_invalid_coordinate_conversion() -> None: draw = ImageDraw.Draw(Image.new("RGB", (4, 4))) - with pytest.raises(ValueError, match="cannot convert signaling NaN to float"): + with pytest.raises(ValueError, match="(?i)cannot convert signaling NaN to float"): draw.line([Decimal("sNaN"), 0, 1, 1]) # type: ignore[arg-type] diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 75f9b97adba..938e221af5e 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -88,8 +88,19 @@ def test_path_list_of_lists( assert list(p) == expected +def test_path_item_assignment_requires_tuple() -> None: + p = ImagePath.Path([(0, 1)]) + + with pytest.raises(TypeError): + p[0] = [4, 5] + assert p[0] == (0.0, 1.0) + + p[0] = (4, 5) + assert p[0] == (4.0, 5.0) + + def test_path_coordinate_conversion_errors() -> None: - with pytest.raises(ValueError, match="cannot convert signaling NaN to float"): + with pytest.raises(ValueError, match="(?i)cannot convert signaling NaN to float"): ImagePath.Path([Decimal("sNaN"), 0]) large_int = (1 << 30) + 1 From 9e6c873a7e9f84edfa08ab24fd56628b7afb2e10 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Oct 2026 23:25:19 +1100 Subject: [PATCH 8/9] Test error within sublist --- Tests/test_imagepath.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 938e221af5e..9b8d12c95ac 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -117,6 +117,7 @@ def test_path_coordinate_conversion_errors() -> None: ([[1]], "coordinate list must contain exactly 2 coordinates"), ([[[1, 2], [3, 4]]], "coordinate list must contain numbers"), ([["a", "b"]], "coordinate list must contain numbers"), + ([[Decimal("sNaN"), 0]], "cannot convert signaling NaN to float"), ), ) def test_invalid_list_coords(coords: list[list[object]], message: str) -> None: From 043f2d47a29790b82e3b468f43788be3b0431bb2 Mon Sep 17 00:00:00 2001 From: shivanshjain456 <227450065+shivanshjain456@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:47:03 +0000 Subject: [PATCH 9/9] Match Decimal error capitalization across runtimes --- Tests/test_imagedraw.py | 2 +- Tests/test_imagepath.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 871c016c81e..fb27befa287 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -71,7 +71,7 @@ def test_sanity() -> None: def test_line_invalid_coordinate_conversion() -> None: draw = ImageDraw.Draw(Image.new("RGB", (4, 4))) - with pytest.raises(ValueError, match="(?i)cannot convert signaling NaN to float"): + with pytest.raises(ValueError, match="[Cc]annot convert signaling NaN to float"): draw.line([Decimal("sNaN"), 0, 1, 1]) # type: ignore[arg-type] diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 9b8d12c95ac..1ad1475b8ba 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -100,7 +100,7 @@ def test_path_item_assignment_requires_tuple() -> None: def test_path_coordinate_conversion_errors() -> None: - with pytest.raises(ValueError, match="(?i)cannot convert signaling NaN to float"): + with pytest.raises(ValueError, match="[Cc]annot convert signaling NaN to float"): ImagePath.Path([Decimal("sNaN"), 0]) large_int = (1 << 30) + 1 @@ -117,7 +117,7 @@ def test_path_coordinate_conversion_errors() -> None: ([[1]], "coordinate list must contain exactly 2 coordinates"), ([[[1, 2], [3, 4]]], "coordinate list must contain numbers"), ([["a", "b"]], "coordinate list must contain numbers"), - ([[Decimal("sNaN"), 0]], "cannot convert signaling NaN to float"), + ([[Decimal("sNaN"), 0]], "[Cc]annot convert signaling NaN to float"), ), ) def test_invalid_list_coords(coords: list[list[object]], message: str) -> None: