diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 2349ad0d1bf..fa5aa730356 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1765,7 +1765,7 @@ def test_line_dash() -> None: draw = ImageDraw.Draw(im) # Act - draw.line([(10, 50), (90, 50)], fill="yellow", width=2, dash=(10, 5)) + draw.line([(10, 50), (90, 50)], "yellow", 2, dash=(10, 5)) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_line_dash.png") @@ -1777,7 +1777,7 @@ def test_line_dash_multi_segment() -> None: draw = ImageDraw.Draw(im) # Act - draw a dashed multi-segment line - draw.line([(10, 10), (50, 50), (90, 10)], fill="yellow", width=2, dash=(8, 4)) + draw.line([(10, 10), (50, 50), (90, 10)], "yellow", 2, dash=(8, 4)) # Assert - verify the image is not all black (dashes were drawn) assert im.getbbox() is not None @@ -1787,19 +1787,22 @@ def test_line_dash_odd_pattern() -> None: # An odd-length dash pattern should be doubled per SVG spec im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) + draw.line([(10, 50), (90, 50)], "yellow", 2, dash=(10,)) - # Should not raise; odd pattern (10,) becomes (10, 10) - draw.line([(10, 50), (90, 50)], fill="yellow", width=2, dash=(10,)) + expected = Image.new("RGB", (W, H)) + draw2 = ImageDraw.Draw(expected) + draw2.line([(10, 50), (90, 50)], "yellow", 2, dash=(10, 10)) - assert im.getbbox() is not None + # odd pattern (10,) becomes (10, 10) + assert_image_equal(im, expected) -def test_line_dash_empty_raises() -> None: +def test_line_dash_empty() -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - with pytest.raises(ValueError): - draw.line([(10, 50), (90, 50)], fill="yellow", dash=()) + with pytest.raises(ValueError, match="dash must be a non-empty tuple of ints"): + draw.line([(10, 50), (90, 50)], dash=()) def test_polygon_dash() -> None: @@ -1834,15 +1837,14 @@ def test_polygon_dash_with_fill() -> None: # Verify center pixel is red (fill) and some edge pixels are blue (outline) assert im.getpixel((50, 50)) == (255, 0, 0) - assert im.getbbox() is not None -def test_polygon_dash_empty_raises() -> None: +def test_polygon_dash_empty() -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - with pytest.raises(ValueError): - draw.polygon([(10, 10), (90, 10), (90, 90)], outline="blue", dash=()) + with pytest.raises(ValueError, match="dash must be a non-empty tuple of ints"): + draw.polygon([(10, 10), (90, 10), (90, 90)], dash=()) def test_rectangle_dash() -> None: @@ -1866,12 +1868,11 @@ def test_rectangle_dash_with_fill() -> None: # Verify center pixel is red (fill) assert im.getpixel((50, 50)) == (255, 0, 0) - assert im.getbbox() is not None -def test_rectangle_dash_empty_raises() -> None: +def test_rectangle_dash_empty() -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - with pytest.raises(ValueError): - draw.rectangle([10, 10, 90, 90], outline="green", dash=()) + with pytest.raises(ValueError, match="dash must be a non-empty tuple of ints"): + draw.rectangle([10, 10, 90, 90], dash=()) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 4aa2202a40a..f2d044dc03b 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -231,23 +231,18 @@ def circle( ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius) self.ellipse(ellipse_xy, fill, outline, width) - def _normalize_points(self, xy: Coords) -> list[tuple[float, float]]: + def _normalize_points(self, xy: Coords) -> list[Sequence[float]]: """Convert various coordinate formats to a list of (x, y) tuples.""" if isinstance(xy[0], (list, tuple)): - return [ - (float(point[0]), float(point[1])) - for point in cast(Sequence[Sequence[float]], xy) - ] + return list(cast(Sequence[Sequence[float]], xy)) else: - flat = cast(Sequence[float], xy) - return [ - (float(flat[i]), float(flat[i + 1])) for i in range(0, len(flat), 2) - ] + flat_xy = cast(Sequence[float], xy) + return [flat_xy[i : i + 2] for i in range(0, len(flat_xy), 2)] def _draw_dashed_line( self, - p1: tuple[float, float], - p2: tuple[float, float], + p1: Sequence[float], + p2: Sequence[float], dash: tuple[int, ...], fill: _Ink | None, width: int, @@ -260,7 +255,7 @@ def _draw_dashed_line( """ dx = p2[0] - p1[0] dy = p2[1] - p1[1] - segment_length = math.sqrt(dx * dx + dy * dy) + segment_length = math.hypot(dx, dy) if segment_length == 0: return dash_offset @@ -290,11 +285,7 @@ def _draw_dashed_line( ny = y + vy * step if dash_index % 2 == 0: - self.line( - [(x, y), (nx, ny)], - fill=fill, - width=width, - ) + self.line([(x, y), (nx, ny)], fill, width) x = nx y = ny @@ -322,7 +313,7 @@ def line( raise ValueError(msg) # If odd number of elements, double the pattern per SVG spec if len(dash) % 2 != 0: - dash = dash + dash + dash *= 2 points = self._normalize_points(xy) dash_offset = 0 for i in range(len(points) - 1): @@ -334,14 +325,7 @@ def line( if ink is not None: self.draw.draw_lines(xy, ink, width) if joint == "curve" and width > 4: - joint_points: Sequence[Sequence[float]] - if isinstance(xy[0], (list, tuple)): - joint_points = cast(Sequence[Sequence[float]], xy) - else: - flat_xy = cast(Sequence[float], xy) - joint_points = [ - tuple(flat_xy[i : i + 2]) for i in range(0, len(flat_xy), 2) - ] + joint_points = self._normalize_points(xy) for i in range(1, len(joint_points) - 1): point = joint_points[i] angles = [ @@ -452,7 +436,7 @@ def polygon( msg = "dash must be a non-empty tuple of ints" raise ValueError(msg) if len(dash) % 2 != 0: - dash = dash + dash + dash *= 2 points = self._normalize_points(xy) # Close the polygon by connecting last point to first if points[0] != points[-1]: @@ -504,11 +488,8 @@ def rectangle( if len(dash) == 0: msg = "dash must be a non-empty tuple of ints" raise ValueError(msg) - if isinstance(xy[0], (list, tuple)): - (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) - else: - x0, y0, x1, y1 = cast(Sequence[float], xy) - rect_points: list[tuple[float, float]] = [ + (x0, y0), (x1, y1) = self._normalize_points(xy) + rect_points = [ (x0, y0), (x1, y0), (x1, y1), @@ -516,7 +497,7 @@ def rectangle( (x0, y0), ] if len(dash) % 2 != 0: - dash = dash + dash + dash *= 2 dash_offset = 0 for i in range(len(rect_points) - 1): dash_offset = self._draw_dashed_line( @@ -541,10 +522,7 @@ def rounded_rectangle( corners: tuple[bool, bool, bool, bool] | None = None, ) -> None: """Draw a rounded rectangle.""" - if isinstance(xy[0], (list, tuple)): - (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) - else: - x0, y0, x1, y1 = cast(Sequence[float], xy) + (x0, y0), (x1, y1) = self._normalize_points(xy) if x1 < x0: msg = "x1 must be greater than or equal to x0" raise ValueError(msg)