Skip to content
Closed
Changes from 7 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
60 changes: 41 additions & 19 deletions svgpathtools/svg_to_paths.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#
# Modified by NXP 2024
#
"""This submodule contains tools for creating path objects from SVG files.
The main tool being the svg2paths() function."""

Expand Down Expand Up @@ -31,27 +34,42 @@ def ellipse2pathd(ellipse):
"""converts the parameters from an ellipse or a circle to a string for a
Path object d-attribute"""

cx = ellipse.get('cx', 0)
cy = ellipse.get('cy', 0)
cx = (ellipse.get('cx', 0) or 0)
cy = (ellipse.get('cy', 0) or 0)
rx = ellipse.get('rx', None)
ry = ellipse.get('ry', None)
r = ellipse.get('r', None)

if r is not None:
rx = ry = float(r)
else:
rx = float(rx)
ry = float(ry)
rx = float(rx or 0)
ry = float(ry or 0)

cx = float(cx)
cy = float(cy)

d = ''
d += 'M' + str(cx - rx) + ',' + str(cy)
d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(2 * rx) + ',0'
d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(-2 * rx) + ',0'
PATH_KAPPA = 0.552284
rxKappa = rx * PATH_KAPPA;
ryKappa = ry * PATH_KAPPA;

return d + 'z'
#According to the SVG specification (https://lists.w3.org/Archives/Public/www-archive/2005May/att-0005/SVGT12_Main.pdf),
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think this should be an option, we should let users continue using arcs if they want, and for now at least.

Let's make that the default unless there is a very good reason this new behavior should be the default.

We could

  • add an argument like use_cubics=False
  • we could also default to True if some environmental variable is set

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hello @mathandy , We have incorporated this suggestion. We'll upstream our changes in this PR in 1-2 days.

#Section 9.4, "The 'ellipse' element": "The arc of an 'ellipse' element begins at the "3 o'clock" point on
#the radius and progresses towards the "9 o'clock". Therefore, the ellipse begins at the rightmost point
#and progresses clockwise.
d = ''
# Move to the rightmost point
d += 'M ' + str(cx + rx) + ' ' + str(cy)
# Draw bottom-right quadrant
d += ' C ' + str(cx + rx) + ' ' + str(cy + ryKappa) + ' ' + str(cx + rxKappa) + ' ' + str(cy + ry) + ' ' + str(cx) + ' ' + str(cy + ry)
# Draw bottom-left quadrant
d += ' C ' + str(cx - rxKappa) + ' ' + str(cy + ry) + ' ' + str(cx - rx) + ' ' + str(cy + ryKappa) + ' ' + str(cx - rx) + ' ' + str(cy)
# Draw top-left quadrant
d += ' C ' + str(cx - rx) + ' ' + str(cy - ryKappa) + ' ' + str(cx - rxKappa) + ' ' + str(cy - ry) + ' ' + str(cx) + ' ' + str(cy - ry)
# Draw top-right quadrant
d += ' C ' + str(cx + rxKappa) + ' ' + str(cy - ry) + ' ' + str(cx + rx) + ' ' + str(cy - ryKappa) + ' ' + str(cx + rx) + ' ' + str(cy)

return d + ' Z'


def polyline2pathd(polyline, is_polygon=False):
Expand All @@ -60,39 +78,43 @@ def polyline2pathd(polyline, is_polygon=False):
if isinstance(polyline, str):
points = polyline
else:
points = COORD_PAIR_TMPLT.findall(polyline.get('points', ''))
raw_points = polyline.get('points', '')
if not raw_points:
points = [(0,0)]
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

points = [(0,0)]

This doesn't seem right, can you explain?

Copy link
Copy Markdown
Contributor Author

@NGExplorer NGExplorer May 5, 2025

Choose a reason for hiding this comment

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

Hello @mathandy ,

Explanation:
In polyline2pathd() Line #L65 closed variable computation don't validate if there are points.

e.g. in one of SVGT12 test vector following was generating exception

As a result we used [(0,0)].

While more digging into SVGT12 specification we find that
Reference: https://www.w3.org/TR/SVG2/shapes.html#PolygonElement
The initial value, (none), indicates that the polygon element is valid, but does not render.
As a result, we've modified our application to not call polyline2pathd() in such cases.
We will update this PR in 1-2 days after internal testing.

else:
points = COORD_PAIR_TMPLT.findall(raw_points)

closed = (float(points[0][0]) == float(points[-1][0]) and
float(points[0][1]) == float(points[-1][1]))

# The `parse_path` call ignores redundant 'z' (closure) commands
# e.g. `parse_path('M0 0L100 100Z') == parse_path('M0 0L100 100L0 0Z')`
# This check ensures that an n-point polygon is converted to an n-Line path.
if is_polygon and closed:
if is_polygon or closed:
points.append(points[0])

d = 'M' + 'L'.join('{0} {1}'.format(x,y) for x,y in points)
d = 'M ' + ' L '.join('{0} {1}'.format(x,y) for x,y in points)
if is_polygon or closed:
d += 'z'
d += ' z'
return d


def polygon2pathd(polyline):
def polygon2pathd(polyline, is_polygon):
"""converts the string from a polygon points-attribute to a string
for a Path object d-attribute.
Note: For a polygon made from n points, the resulting path will be
composed of n lines (even if some of these lines have length zero).
"""
return polyline2pathd(polyline, True)
return polyline2pathd(polyline, is_polygon)


def rect2pathd(rect):
"""Converts an SVG-rect element to a Path d-string.

The rectangle will start at the (x,y) coordinate specified by the
rectangle object and proceed counter-clockwise."""
x, y = float(rect.get('x', 0)), float(rect.get('y', 0))
w, h = float(rect.get('width', 0)), float(rect.get('height', 0))
x, y = float(rect.get('x', 0) or 0), float(rect.get('y', 0) or 0)
w, h = float(rect.get('width', 0) or 0), float(rect.get('height', 0) or 0)
if 'rx' in rect or 'ry' in rect:

# if only one, rx or ry, is present, use that value for both
Expand Down Expand Up @@ -121,7 +143,7 @@ def rect2pathd(rect):
x2, y2 = x + w, y + h
x3, y3 = x, y + h

d = ("M{} {} L {} {} L {} {} L {} {} z"
d = ("M {} {} L {} {} L {} {} L {} {} z"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Does this change have an effect?

"".format(x0, y0, x1, y1, x2, y2, x3, y3))

return d
Expand Down Expand Up @@ -204,7 +226,7 @@ def dom2dict(element):
# path strings, add to list
if convert_polygons_to_paths:
pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')]
d_strings += [polygon2pathd(pg) for pg in pgons]
d_strings += [polygon2pathd(pg, True) for pg in pgons]
attribute_dictionary_list += pgons

if convert_lines_to_paths:
Expand Down