Skip to content

Add dash parameter for line, polygon, and rectangle drawing - #9490

Open
Krishnachaitanyakc wants to merge 21 commits into
python-pillow:mainfrom
Krishnachaitanyakc:add-dashed-line-support
Open

Add dash parameter for line, polygon, and rectangle drawing#9490
Krishnachaitanyakc wants to merge 21 commits into
python-pillow:mainfrom
Krishnachaitanyakc:add-dashed-line-support

Conversation

@Krishnachaitanyakc

@Krishnachaitanyakc Krishnachaitanyakc commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a dash parameter to ImageDraw.line(), ImageDraw.polygon(), and ImageDraw.rectangle() that enables drawing dashed outlines, implemented in the Python layer.

  • The dash pattern follows the SVG stroke-dasharray specification: a tuple of ints specifying alternating drawn/blank segment lengths (e.g. (10, 5) draws 10px, skips 5px, repeats)
  • Odd-length patterns are automatically doubled per SVG spec
  • The dash pattern is continuous across connected line segments and polygon/rectangle edges
  • Empty dash tuples raise ValueError
  • Fill is drawn normally; only the outline/stroke is dashed
  • Fully backward compatible: all existing behavior is preserved when dash is not specified

Reference

This implementation follows the SVG stroke-dasharray specification:
https://www.w3.org/TR/SVG2/painting.html#StrokeDashing

Example usage

from PIL import Image, ImageDraw

im = Image.new("RGB", (200, 200), "white")
draw = ImageDraw.Draw(im)

# Dashed line
draw.line([(10, 100), (190, 100)], fill="black", width=2, dash=(10, 5))

# Dashed rectangle with fill
draw.rectangle([20, 20, 180, 60], fill="lightyellow", outline="blue", width=2, dash=(15, 5, 5, 5))

# Dashed polygon with fill
draw.polygon([(50, 120), (150, 120), (150, 180), (50, 180)], fill="lightblue", outline="red", width=2, dash=(10, 5))

Closes #9127

Test plan

  • Added 10 new tests covering dashed lines, polygons, and rectangles
  • Tests cover: basic dash, multi-segment lines, odd-length patterns, empty dash error, polygon with/without fill, rectangle with/without fill
  • All 281 existing test_imagedraw.py tests continue to pass (backward compatibility verified)
  • Reference images generated and included for pixel-exact assertions
  • Documentation updated for all three methods with versionadded:: 12.2.0

Krishnachaitanyakc and others added 5 commits March 24, 2026 22:31
Add a `dash` parameter to `ImageDraw.line()`, `ImageDraw.polygon()`, and
`ImageDraw.rectangle()` methods that allows drawing dashed outlines.

The dash pattern follows the SVG `stroke-dasharray` specification:
- Accepts a tuple of ints specifying alternating drawn/blank segment lengths
- Odd-length patterns are automatically doubled per the SVG spec
- The dash pattern is continuous across connected line segments and polygon edges
- Empty dash tuples raise ValueError

Closes python-pillow#9127
- Annotate pixels_used as float to fix float/int assignment mismatch
- Rename redefined 'points' variable to 'joint_points' in curve joint code
- Cast flat xy to Sequence[float] before slicing to fix type compatibility
Comment thread tox.ini Outdated
@radarhere

Copy link
Copy Markdown
Member

Could you link to the SVG specification that you used as a reference?

@Krishnachaitanyakc

Copy link
Copy Markdown
Contributor Author

@radarhere updated the description

@radarhere

Copy link
Copy Markdown
Member

Did you use AI to create this PR?

@Krishnachaitanyakc

Krishnachaitanyakc commented Mar 30, 2026

Copy link
Copy Markdown
Contributor Author

@radarhere I used AI to plan and implement yes, but did manually verify the changes and tested them

@radarhere radarhere added the 🤖-assisted AI-assisted label Mar 30, 2026
@Krishnachaitanyakc

Copy link
Copy Markdown
Contributor Author

@radarhere can you please review this?

@radarhere

Copy link
Copy Markdown
Member

This is on my list of things to look at. However, as I'm doing this in my spare time in an underpaid capacity, and trying to also address other issues and PRs, I may not get to it immediately.

@radarhere

Copy link
Copy Markdown
Member

I've combined the different shape tests, checking the visual output for more operations.

However, when I changed the polygon points to be something other than a rectangle, I found that the dashed line does exactly sit on the edge of the polygon.

I can think of a solution to this when the width is 1 pixel, but more than that could be complicated. Would you be happy with only supporting widths of 1 pixel?

@akx akx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In imagedraw_dash_polygon.png, the diagonal dashed line appears 1px inside the filled triangle. Why is that?

EDIT: ah, radarhere mentioned this just above.

@akx

akx commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

I think this could also be implemented in the C drawing code, where it's rather easy to just skip drawing some pixels that would otherwise be drawn, based on a pattern mask?

@codspeed-hq

codspeed-hq Bot commented Aug 25, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 23.8%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 4 regressed benchmarks
✅ 551 untouched benchmarks
⏩ 335 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
test_draw_lines[1237x811-RGBA] 243 µs 329.6 µs -26.28%
test_draw_lines[1237x811-LA] 242.9 µs 329.4 µs -26.27%
test_draw_lines[1237x811-RGB] 248.5 µs 335.7 µs -25.97%
test_draw_lines[1237x811-L] 405.1 µs 483.4 µs -16.2%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing Krishnachaitanyakc:add-dashed-line-support (9341c21) with main (a9cbfbe)2

Open in CodSpeed

Footnotes

  1. 335 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (807d689) during the generation of this report, so a9cbfbe was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Comment thread src/libImaging/Draw.c
Comment on lines +208 to +232
static inline int
should_draw_dash(int i, PyObject *dash) {
if (dash == NULL) {
return 1;
}
int total = 0;
int tuple_index = -1;
int tuple_size = PyTuple_GET_SIZE(dash);
while (total <= i) {
tuple_index += 1;
if (tuple_index == tuple_size) {
tuple_index = 0;
}
PyObject *value = PyTuple_GetItem(dash, tuple_index);
if (!PyLong_Check(value)) {
return 0;
}
int v = PyLong_AsLongLong(value);
if (v == -1 && PyErr_Occurred()) {
return 0;
}
total += v;
}
return tuple_index % 2 == 0;
}

@akx akx Aug 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmmm, I'd maybe not do Python object conversions for every dashed pixel to be drawn or not drawn, but instead convert the tuple into a C struct before passing to line*?

EDIT: it may also be a good idea to look at separate dashed variants of these functions, since a per-pixel check in the hot path is likely to degrade perf for all drawing operations for a somewhat niche feature.

EDIT 2: In fact, doesn't the dash array basically decompose into an array of on/off states + length, so accessing it to figure out whether to draw is then just [i % state_array_length]...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, doesn't the dash array basically decompose into an array of on/off states + length, so accessing it to figure out whether to draw is then just [i % state_array_length]...

Isn't this complicated by the fact that the dash on/off states might have uneven lengths, under the API of this PR?

instead convert the tuple into a C struct before passing to line*?

You're suggesting allocating a struct that contains the length of the dash array, and the values within? Is that manifestly different from what the Python API is does under the hood?

a per-pixel check in the hot path

It shouldn't be per-pixel anymore, since 1af2a67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're suggesting allocating a struct that contains the length of the dash array, and the values within? Is that manifestly different from what the Python API is does under the hood?

Yep, and I think it is. Right now each call to should_draw_dash involves, at a quick look, (not including the trivial operations like field retrievals that would need to be done with the struct too):

  • Type/subtype check (PyTuple_Check) in PyTuple_GET_SIZE
  • Type/subtype check (PyTuple_Check) in PyTuple_GetItem
  • Bounds check in PyTuple_GetItem
  • Type/subtype check PyLong_Check
  • Type/subtype check (PyLong_Check) in PyLong_AsLongLong
  • _PyLong_CompactValue or _PyLong_AsByteArray of the value

So it does sound like that's worth doing upfront, at least to something like

struct DashArray {
  size_t n_states;
  uint32_t states[];
}

in a single pre-pass. (I'm assuming we might not really need long long (64-bit) values for states?)

Isn't [[i % state_array_length]] complicated by the fact that the dash on/off states might have uneven lengths, under the API of this PR?

Just "exploding" the on-off states into chars, (at the expense of taking up sum(dash_array) bytes of memory plus a size_t for the length) is not complicated. (Could also pack 8 states down to a byte, so the expense is ceil(sum(dash_array) / 8), but the indexing becomes fiddlier.) That implementation of course means we'd probably need to bound the maximum length of the dash states, maybe to... 16K entries altogether? 🤷

But just converting the tuple to something like that DashArray above should be a lot more efficient, anyway, so might not be worth it to bother with exploding on top of that.

It shouldn't be per-pixel anymore, since 1af2a67

Ah yeah, checking for dashness at all before the loops is a good idea. Whether to draw the pixel or not has to be done for every pixel though, and that involves the above Python value operations right now.

Comment thread src/libImaging/Draw.c
@radarhere
radarhere force-pushed the add-dashed-line-support branch from 30c316d to 9341c21 Compare August 25, 2026 07:51
Comment thread src/libImaging/Draw.c
for (i = 0; i < dy; i++) {
point8(im, x0, y0, ink);
y0 += ys;
if (dash != NULL) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be fine to always use the Bresenham algorithm when dashing and leave the fast paths (dx == 0, dy == 0) alone. Dashed line drawing will be slower anyhow due to the dash pattern check.

The GEN_LINE() stuff from #9772 would help there, I think - that could maybe be merged first? :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖-assisted AI-assisted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for dashed lines?

3 participants