Skip to content

Commit 30c316d

Browse files
committed
Only apply dash for width 1
1 parent 91d987a commit 30c316d

9 files changed

Lines changed: 235 additions & 150 deletions

File tree

-185 Bytes
Loading
-53 Bytes
Loading
9 Bytes
Loading

Tests/test_imagedraw.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1782,8 +1782,8 @@ def test_dash_line() -> None:
17821782
draw = ImageDraw.Draw(im)
17831783

17841784
# Act
1785-
draw.line([(10, 90), (90, 90)], "green", 2, dash=(10, 5))
1786-
draw.line([(10, 10), (50, 50), (90, 10)], "green", 2, dash=(8, 4))
1785+
draw.line([(10, 90), (90, 90)], "green", dash=(10, 5))
1786+
draw.line([(10, 10), (50, 50), (90, 10)], "green", dash=(8, 4))
17871787

17881788
# Assert
17891789
assert_image_equal_tofile(im, "Tests/images/imagedraw_dash_line.png")
@@ -1798,14 +1798,12 @@ def test_dash_polygon() -> None:
17981798
draw.polygon(
17991799
[(10, 10), (90, 10), (10, 90)],
18001800
outline="green",
1801-
width=1,
18021801
dash=(10, 5),
18031802
)
18041803
draw.polygon(
18051804
[(20, 20), (60, 20), (20, 60)],
18061805
fill="red",
18071806
outline="green",
1808-
width=1,
18091807
dash=(10, 5),
18101808
)
18111809

@@ -1819,8 +1817,8 @@ def test_dash_rectangle() -> None:
18191817
draw = ImageDraw.Draw(im)
18201818

18211819
# Act
1822-
draw.rectangle([10, 10, 90, 90], outline="green", width=1, dash=(10, 5))
1823-
draw.rectangle([30, 30, 70, 70], fill="red", outline="green", width=1, dash=(10, 5))
1820+
draw.rectangle([10, 10, 90, 90], outline="green", dash=(10, 5))
1821+
draw.rectangle([30, 30, 70, 70], fill="red", outline="green", dash=(10, 5))
18241822

18251823
# Assert
18261824
assert_image_equal_tofile(im, "Tests/images/imagedraw_dash_rectangle.png")

docs/reference/ImageDraw.rst

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,10 @@ Methods
307307
The dash pattern specifies the lengths of alternating drawn and blank segments
308308
(e.g. ``(10, 5)`` draws 10 pixels, skips 5, and repeats). If an odd number of
309309
values is given, it continues to alternate (e.g. ``(1, 2, 3)`` draws 1 pixel,
310-
skips 2, draws 3, skips 1, draws 2, and so on). When ``dash`` is set, ``joint``
311-
is ignored.
310+
skips 2, draws 3, skips 1, draws 2, and so on). When ``dash`` is set, ``width``
311+
and ``joint`` are ignored.
312312

313-
.. versionadded:: 12.3.0
313+
.. versionadded:: 13.0.0
314314

315315
.. py:method:: ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=1)
316316
@@ -354,9 +354,10 @@ Methods
354354
The dash pattern specifies the lengths of alternating drawn and blank segments
355355
(e.g. ``(10, 5)`` draws 10 pixels, skips 5, and repeats). If an odd number of
356356
values is given, it continues to alternate (e.g. ``(1, 2, 3)`` draws 1 pixel,
357-
skips 2, draws 3, skips 1, draws 2, and so on).
357+
skips 2, draws 3, skips 1, draws 2, and so on). When ``dash`` is set, ``width``
358+
is ignored.
358359

359-
.. versionadded:: 12.3.0
360+
.. versionadded:: 13.0.0
360361

361362

362363
.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1)
@@ -393,9 +394,10 @@ Methods
393394
The dash pattern specifies the lengths of alternating drawn and blank segments
394395
(e.g. ``(10, 5)`` draws 10 pixels, skips 5, and repeats). If an odd number of
395396
values is given, it continues to alternate (e.g. ``(1, 2, 3)`` draws 1 pixel,
396-
skips 2, draws 3, skips 1, draws 2, and so on).
397+
skips 2, draws 3, skips 1, draws 2, and so on). When ``dash`` is set, ``width``
398+
is ignored.
397399

398-
.. versionadded:: 12.3.0
400+
.. versionadded:: 13.0.0
399401

400402
.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1, corners=None)
401403

src/PIL/ImageDraw.py

Lines changed: 31 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -222,72 +222,15 @@ def circle(
222222
ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius)
223223
self.ellipse(ellipse_xy, fill, outline, width)
224224

225-
def _normalize_points(self, xy: Coords) -> list[Sequence[float]]:
226-
"""Convert various coordinate formats to a list of (x, y) tuples."""
225+
def _normalize_coords(self, xy: Coords) -> Sequence[Sequence[float]]:
226+
"""Normalize 1 or 2 dimensional coord sequence into 2d sequence."""
227227
if isinstance(xy[0], (list, tuple)):
228-
return list(cast("Sequence[Sequence[float]]", xy))
228+
return cast("Sequence[Sequence[float]]", xy)
229229
else:
230-
flat_xy = cast("Sequence[float]", xy)
231-
return [flat_xy[i : i + 2] for i in range(0, len(flat_xy), 2)]
232-
233-
def _draw_dashed_line(
234-
self,
235-
p1: Sequence[float],
236-
p2: Sequence[float],
237-
dash: tuple[int, ...],
238-
ink: int,
239-
width: int,
240-
dash_offset: int,
241-
) -> int:
242-
"""Draw a single dashed line segment between two points.
243-
244-
Returns the updated dash_offset for continuing the pattern
245-
along the next segment.
246-
"""
247-
dx = p2[0] - p1[0]
248-
dy = p2[1] - p1[1]
249-
segment_length = math.hypot(dx, dy)
250-
if segment_length == 0:
251-
return dash_offset
252-
253-
vx = dx / segment_length
254-
vy = dy / segment_length
255-
256-
remaining = segment_length
257-
x, y = p1
258-
259-
# Determine where we are in the dash pattern
260-
dash_cycle_length = sum(dash)
261-
offset = dash_offset % dash_cycle_length
262-
dash_index = 0
263-
consumed = 0
264-
for i, d in enumerate(dash):
265-
if consumed + d > offset:
266-
dash_index = i
267-
break
268-
consumed += d
269-
pixels_used: float = offset - consumed
270-
271-
while remaining > 0.5:
272-
current_dash_length = dash[dash_index % len(dash)]
273-
step = min(current_dash_length - pixels_used, remaining)
274-
275-
nx = x + vx * step
276-
ny = y + vy * step
277-
278-
if dash_index % 2 == 0:
279-
self.draw.draw_lines([(x, y), (nx, ny)], ink, width)
280-
281-
x = nx
282-
y = ny
283-
remaining -= step
284-
pixels_used += step
285-
286-
if pixels_used >= current_dash_length:
287-
pixels_used = 0
288-
dash_index += 1
289-
290-
return (dash_offset + int(round(segment_length))) % dash_cycle_length
230+
return [
231+
cast("Sequence[float]", tuple(xy[i : i + 2]))
232+
for i in range(0, len(xy), 2)
233+
]
291234

292235
def line(
293236
self,
@@ -303,27 +246,22 @@ def line(
303246
return
304247

305248
if dash is not None:
306-
if len(dash) == 0:
249+
if len(dash) == 0 or any(not isinstance(v, int) for v in dash):
307250
msg = "dash must be a non-empty tuple of ints"
308251
raise ValueError(msg)
309-
points = self._normalize_points(xy)
310-
dash_offset = 0
311-
for i in range(len(points) - 1):
312-
dash_offset = self._draw_dashed_line(
313-
points[i], points[i + 1], dash, ink, width, dash_offset
314-
)
252+
self.draw.draw_lines(xy, ink, 1, dash)
315253
else:
316254
self.draw.draw_lines(xy, ink, width)
317255
if joint == "curve" and width > 4:
318-
joint_points = self._normalize_points(xy)
319-
for i in range(1, len(joint_points) - 1):
320-
point = joint_points[i]
256+
points = self._normalize_coords(xy)
257+
for i in range(1, len(points) - 1):
258+
point = points[i]
321259
angles = [
322260
math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
323261
% 360
324262
for start, end in (
325-
(joint_points[i - 1], point),
326-
(point, joint_points[i + 1]),
263+
(points[i - 1], point),
264+
(point, points[i + 1]),
327265
)
328266
]
329267
if angles[0] == angles[1]:
@@ -425,18 +363,10 @@ def polygon(
425363
return
426364

427365
if dash is not None:
428-
if len(dash) == 0:
366+
if len(dash) == 0 or any(not isinstance(v, int) for v in dash):
429367
msg = "dash must be a non-empty tuple of ints"
430368
raise ValueError(msg)
431-
points = self._normalize_points(xy)
432-
# Close the polygon by connecting last point to first
433-
if points[0] != points[-1]:
434-
points.append(points[0])
435-
dash_offset = 0
436-
for i in range(len(points) - 1):
437-
dash_offset = self._draw_dashed_line(
438-
points[i], points[i + 1], dash, ink, width, dash_offset
439-
)
369+
self.draw.draw_polygon(xy, ink, 0, 1, dash)
440370
elif width == 1:
441371
self.draw.draw_polygon(xy, ink, 0, width)
442372
elif self.im is not None:
@@ -447,7 +377,7 @@ def polygon(
447377
draw = Draw(mask)
448378
draw.draw.draw_polygon(xy, mask_ink, 1)
449379

450-
self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im)
380+
self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, None, mask.im)
451381

452382
def regular_polygon(
453383
self,
@@ -478,27 +408,21 @@ def rectangle(
478408
return
479409

480410
if dash is not None:
481-
if len(dash) == 0:
411+
if len(dash) == 0 or any(not isinstance(v, int) for v in dash):
482412
msg = "dash must be a non-empty tuple of ints"
483413
raise ValueError(msg)
484-
(x0, y0), (x1, y1) = self._normalize_points(xy)
485-
rect_points = [
486-
(x0, y0),
487-
(x1, y0),
488-
(x1, y1),
489-
(x0, y1),
490-
(x0, y0),
491-
]
492-
dash_offset = 0
493-
for i in range(4):
494-
dash_offset = self._draw_dashed_line(
495-
rect_points[i],
496-
rect_points[i + 1],
497-
dash,
498-
ink,
499-
width,
500-
dash_offset,
501-
)
414+
self.draw.draw_lines(
415+
[
416+
(xy[0], xy[1]),
417+
(xy[2], xy[1]),
418+
(xy[2], xy[3]),
419+
(xy[0], xy[3]),
420+
(xy[0], xy[1]),
421+
],
422+
ink,
423+
1,
424+
dash,
425+
)
502426
else:
503427
self.draw.draw_rectangle(xy, ink, 0, width)
504428

@@ -513,7 +437,7 @@ def rounded_rectangle(
513437
corners: tuple[bool, bool, bool, bool] | None = None,
514438
) -> None:
515439
"""Draw a rounded rectangle."""
516-
(x0, y0), (x1, y1) = self._normalize_points(xy)
440+
(x0, y0), (x1, y1) = self._normalize_coords(xy)
517441
if x1 < x0:
518442
msg = "x1 must be greater than or equal to x0"
519443
raise ValueError(msg)

src/_imaging.c

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3217,7 +3217,8 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) {
32173217
PyObject *data;
32183218
int ink;
32193219
int width;
3220-
if (!PyArg_ParseTuple(args, "Oii", &data, &ink, &width)) {
3220+
PyObject *dash = NULL;
3221+
if (!PyArg_ParseTuple(args, "Oii|O", &data, &ink, &width, &dash)) {
32213222
return NULL;
32223223
}
32233224

@@ -3228,6 +3229,7 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) {
32283229

32293230
if (width == 1) {
32303231
double *p = NULL;
3232+
int dash_offset = 0;
32313233
for (i = 0; i < n - 1; i++) {
32323234
p = &xy[i + i];
32333235
if (ImagingDrawLine(
@@ -3237,7 +3239,9 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) {
32373239
(int)p[2],
32383240
(int)p[3],
32393241
&ink,
3240-
self->blend
3242+
self->blend,
3243+
dash,
3244+
&dash_offset
32413245
) < 0) {
32423246
free(xy);
32433247
return NULL;
@@ -3399,8 +3403,9 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
33993403
int fill = 0;
34003404
int width = 0;
34013405
ImagingObject *maskp = NULL;
3406+
PyObject *dash = NULL;
34023407
if (!PyArg_ParseTuple(
3403-
args, "Oi|iiO!", &data, &ink, &fill, &width, &Imaging_Type, &maskp
3408+
args, "Oi|iiOO!", &data, &ink, &fill, &width, &dash, &Imaging_Type, &maskp
34043409
)) {
34053410
return NULL;
34063411
}
@@ -3439,7 +3444,8 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
34393444
fill,
34403445
width,
34413446
self->blend,
3442-
maskp ? maskp->image : NULL
3447+
maskp ? maskp->image : NULL,
3448+
dash != Py_None ? dash : NULL
34433449
) < 0) {
34443450
free(ixy);
34453451
return NULL;
@@ -3459,7 +3465,8 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) {
34593465
int ink;
34603466
int fill = 0;
34613467
int width = 0;
3462-
if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) {
3468+
PyObject *dash = NULL;
3469+
if (!PyArg_ParseTuple(args, "Oi|iiO", &data, &ink, &fill, &width, &dash)) {
34633470
return NULL;
34643471
}
34653472

0 commit comments

Comments
 (0)