Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions Tests/test_imagedraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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=())
52 changes: 15 additions & 37 deletions src/PIL/ImageDraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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 = [
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -504,19 +488,16 @@ 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),
(x0, y1),
(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(
Expand All @@ -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)
Expand Down