From 73275dd2e8b4f582af49a7c3318d6961cb8d8a2e Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:46:19 +0300 Subject: [PATCH 1/4] Honor ImageDraw2 pen widths for shape outlines --- Tests/test_imagedraw2.py | 58 ++++++++++++++++++++++++++++++++++-- docs/releasenotes/13.0.0.rst | 8 +++++ src/PIL/ImageDraw2.py | 9 ++++-- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 47943393fd4..79248cd9fe9 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -68,6 +68,58 @@ def test_mode() -> None: ImageDraw2.Draw("L") +@pytest.mark.parametrize( + "operation", ("arc", "chord", "ellipse", "line", "pieslice", "polygon", "rectangle") +) +@pytest.mark.parametrize("width", (1, 4)) +@pytest.mark.parametrize("pen_as_brush", (False, True)) +def test_pen_width(operation: str, width: int, pen_as_brush: bool) -> None: + im = Image.new("RGB", (30, 30)) + expected = im.copy() + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=width) + brush = ImageDraw2.Brush("red") + first, second = (brush, pen) if pen_as_brush else (pen, brush) + xy = ( + ((5, 5), (24, 7), (12, 24)) + if operation in ("line", "polygon") + else (5, 5, 24, 24) + ) + + if operation in ("arc", "chord", "pieslice"): + getattr(draw, operation)(xy, first, 30, 270, second) + angles = {"start": 30, "end": 270} + else: + getattr(draw, operation)(xy, first, second) + angles = {} + + if operation in ("arc", "line"): + getattr(ImageDraw.Draw(expected), operation)( + xy, fill="blue", width=width, **angles + ) + else: + getattr(ImageDraw.Draw(expected), operation)( + xy, fill="red", outline="blue", width=width, **angles + ) + assert_image_equal(im, expected) + + +def test_rectangle_pen_width() -> None: + im = Image.new("RGB", (20, 20)) + ImageDraw2.Draw(im).rectangle((3, 3, 16, 16), ImageDraw2.Pen("white", width=4)) + assert im.getpixel((5, 8)) == WHITE + assert im.getpixel((7, 8)) == BLACK + + +def test_render_width_override() -> None: + im = Image.new("RGB", (20, 20)) + ImageDraw2.Draw(im).render( + "rectangle", (3, 3, 16, 16), ImageDraw2.Pen("white", width=4), width=2 + ) + assert im.getpixel((4, 8)) == WHITE + assert im.getpixel((5, 8)) == BLACK + + @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) def test_arc(bbox: Coords, start: float, end: float) -> None: @@ -103,7 +155,7 @@ def test_ellipse(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("blue", width=2) + pen = ImageDraw2.Pen("blue") brush = ImageDraw2.Brush("green") # Act @@ -177,7 +229,7 @@ def test_polygon(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("blue", width=2) + pen = ImageDraw2.Pen("blue") brush = ImageDraw2.Brush("red") # Act @@ -192,7 +244,7 @@ def test_rectangle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("green", width=2) + pen = ImageDraw2.Pen("green") brush = ImageDraw2.Brush("black") # Act diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index 0763dccf8fb..c313100272b 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -286,3 +286,11 @@ FriBiDi itself is not included in Pillow's wheels. The shim's source files now include SPDX license identifiers, and the SBOM has been corrected. + +ImageDraw2 shape outline widths +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageDraw2.Draw`` now uses ``Pen.width`` for arc, chord, ellipse, pieslice, +polygon and rectangle outlines. Previously, these outlines were always one +pixel wide, even when a different pen width was requested. Line widths continue +to use the pen width. diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 333edb71d34..680a6c1ecec 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -35,7 +35,11 @@ class Pen: - """Stores an outline color and width.""" + """Stores an outline color and width. + + :param color: The outline color. + :param width: The width in pixels, used for lines and shape outlines. + """ def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None: self.color = ImageColor.getrgb(color) @@ -115,8 +119,7 @@ def render( else: kwargs.setdefault("fill", fill) kwargs.setdefault("outline", outline) - if op == "line": - kwargs.setdefault("width", width) + kwargs.setdefault("width", width) getattr(self.draw, op)(xy, **kwargs) def settransform(self, offset: tuple[float, float]) -> None: From dc772d43c40daed09ebafeba7fc4cbd8e8ab65d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Oct 2026 13:21:12 +1100 Subject: [PATCH 2/4] Remove release note --- docs/releasenotes/13.0.0.rst | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index c313100272b..0763dccf8fb 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -286,11 +286,3 @@ FriBiDi itself is not included in Pillow's wheels. The shim's source files now include SPDX license identifiers, and the SBOM has been corrected. - -ImageDraw2 shape outline widths -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``ImageDraw2.Draw`` now uses ``Pen.width`` for arc, chord, ellipse, pieslice, -polygon and rectangle outlines. Previously, these outlines were always one -pixel wide, even when a different pen width was requested. Line widths continue -to use the pen width. From 687d7e5a16f10fdd7aba6d2da7e18c7a0a9c98b0 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:26:08 +1100 Subject: [PATCH 3/4] Update docstring --- src/PIL/ImageDraw2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 680a6c1ecec..2ae84bbf572 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -38,7 +38,7 @@ class Pen: """Stores an outline color and width. :param color: The outline color. - :param width: The width in pixels, used for lines and shape outlines. + :param width: The line width, in pixels. """ def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None: From 9ca3a96f38c75a1c1f49ec489e069d3bc49ddca2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Oct 2026 13:42:05 +1100 Subject: [PATCH 4/4] Simplify tests --- Tests/test_imagedraw2.py | 62 +++++++--------------------------------- 1 file changed, 10 insertions(+), 52 deletions(-) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 79248cd9fe9..c09850c8083 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -68,58 +68,6 @@ def test_mode() -> None: ImageDraw2.Draw("L") -@pytest.mark.parametrize( - "operation", ("arc", "chord", "ellipse", "line", "pieslice", "polygon", "rectangle") -) -@pytest.mark.parametrize("width", (1, 4)) -@pytest.mark.parametrize("pen_as_brush", (False, True)) -def test_pen_width(operation: str, width: int, pen_as_brush: bool) -> None: - im = Image.new("RGB", (30, 30)) - expected = im.copy() - draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("blue", width=width) - brush = ImageDraw2.Brush("red") - first, second = (brush, pen) if pen_as_brush else (pen, brush) - xy = ( - ((5, 5), (24, 7), (12, 24)) - if operation in ("line", "polygon") - else (5, 5, 24, 24) - ) - - if operation in ("arc", "chord", "pieslice"): - getattr(draw, operation)(xy, first, 30, 270, second) - angles = {"start": 30, "end": 270} - else: - getattr(draw, operation)(xy, first, second) - angles = {} - - if operation in ("arc", "line"): - getattr(ImageDraw.Draw(expected), operation)( - xy, fill="blue", width=width, **angles - ) - else: - getattr(ImageDraw.Draw(expected), operation)( - xy, fill="red", outline="blue", width=width, **angles - ) - assert_image_equal(im, expected) - - -def test_rectangle_pen_width() -> None: - im = Image.new("RGB", (20, 20)) - ImageDraw2.Draw(im).rectangle((3, 3, 16, 16), ImageDraw2.Pen("white", width=4)) - assert im.getpixel((5, 8)) == WHITE - assert im.getpixel((7, 8)) == BLACK - - -def test_render_width_override() -> None: - im = Image.new("RGB", (20, 20)) - ImageDraw2.Draw(im).render( - "rectangle", (3, 3, 16, 16), ImageDraw2.Pen("white", width=4), width=2 - ) - assert im.getpixel((4, 8)) == WHITE - assert im.getpixel((5, 8)) == BLACK - - @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) def test_arc(bbox: Coords, start: float, end: float) -> None: @@ -254,6 +202,16 @@ def test_rectangle(bbox: Coords) -> None: assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_pen_width(bbox: Coords) -> None: + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("white", width=4) + draw.rectangle(bbox, pen) + assert im.getpixel((28, 28)) == WHITE + assert im.getpixel((50, 50)) == BLACK + + def test_big_rectangle() -> None: # Test drawing a rectangle bigger than the image # Arrange