svg.path
svg.path copied to clipboard
Can your tool support a function to caculate the boundingbox of path?
I think that would be a nice addition.
something like this tool that is write use javascript. https://github.com/kfitfk/svg-boundings
Your tool is excellent to help me solve to resolve svg path node format. Thanks very much.
I've been working on implementing bounding boxes myself using paths parsed by this package. I was originally planning on releasing it as its own package but if there's any interest I could contribute my work here instead when it's done.
Both works fine. Pull request here, or a separate package, whatever you want. You can also use the "svg." prefix, "svg.boundingbox" if that makes sense.
But if you think it makes more sense to have a "boundingbox()" method on the path objects, that's perfectly fine for me.
We just need good tests. :-)
The svg. prefix is actually a pain in the ass to use. I only recently got bounding boxes to work correctly for svgelements
borrowing the arc code from svgpathtools
but I would say trying to get the package to include in another project is actually the reason I dropped the .
from svg.elements
it was like shockingly harder and in violation of a couple PEPs and really was kinda a jerk.
What is needed to calculate the bounding box out of an SVG path?
Basically for each relevant path element you'd need to calculate the bounding box of each path segment, and take the union of all of those rectangles.
Move
Move is a single point. You take the minimum and maximum x and y values for that point.
Line & Close
This is just two points. You take the minimum and maximum x and y values for those points.
QuadraticBezier
You take the minimum and maximum x and y values of the start and end points, and the one potential inflection in the x and y coordinates.
Because of the Bezout theorem you know that there can be at most one direction change. It's found by calculating t
by calculating n
and d
. I'll use s
e
and c
for start, end and control respectively. But, note this is just looking for the local extreme across that coordinate. And this must be done both x and y.
d = s - 2*c + e
if d == 0 then there is no extrema inflection point along this coordinate axis.
else:
n = s - c
t = n / d
If it is the case that t
falls between 0 and 1 then t is within the curve, and you take the min and max there. You find the point for that t value within the quadratic bezier curve. You'd do this calling the .point()
function. But, only the coordinate of the point you checked is relevant.
You do this same thing for the other coordinate to find the other potential inflection point along the other axis. You then take the maximum and minimum of these potential x and y points. And get the bounding box.
CubicBezierCurves
With cubic bezier curves since you have 4 points there can be a maximum of 2 inflection points and 2 end points. So you do a similar thing to the quad case. (c1 and c2 are control1 and control2.
-
d = s - 3 * c1 + 3 * c2 - e
- If d == 0, then there is no extrema inflection points along this coordinate axis.
- if d isn't zero, find the square root of your delta and your tau.
-
sqdelta = sqrt(c1*c1 - (s + c1) * c2 - c2 * c2 + (s - c1) * e)
-
tau = s - 2 * c1 + c2
From these you calculate the two possible roots.
-
roots = (tau ± sqdelta) / d
Now if either root is between 0 and 1 that's an inflection point in the curve and you add it to the max and min values for the coordinate axis you are working on.
Arc
With arcs, you'd use the same code I stole from svgpathtools
which I don't really understand:
def bbox(self):
"""Find the bounding box of a arc.
Code from: https://github.com/mathandy/svgpathtools
"""
phi = self.get_rotation().as_radians
if cos(phi) == 0:
atan_x = pi / 2
atan_y = 0
elif sin(phi) == 0:
atan_x = 0
atan_y = pi / 2
else:
rx, ry = self.rx, self.ry
atan_x = atan(-(ry / rx) * tan(phi))
atan_y = atan((ry / rx) / tan(phi))
def angle_inv(ang, k): # inverse of angle from Arc.derivative()
return ((ang + pi * k) * (360 / (2 * pi)) - self.theta) / self.delta
xtrema = [self.start[0], self.end[0]]
ytrema = [self.start[1], self.end[1]]
for k in range(-4, 5):
tx = angle_inv(atan_x, k)
ty = angle_inv(atan_y, k)
if 0 <= tx <= 1:
xtrema.append(self.point(tx)[0])
if 0 <= ty <= 1:
ytrema.append(self.point(ty)[1])
return min(xtrema), min(ytrema), max(xtrema), max(ytrema)
Conclusion
You then have the bounding boxes for all the different parts which PathSegments can have. You take the minimums and maximums of all these elements and you get a maximum of maximums for the x and y and minimum of minimums for the x and y. And that's the bounding box of the path.
For now what I did was use inkscape, which is a nasty hack, but I had a time limit to solve this:
inkscape --without-gui --query-all /tmp/test.svg
With a file /tmp/test.svg
:
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M579.91,284.35 q0.00,56.59 56.59,56.59 l0.00,-56.59Z"></path>
</svg>
The output is:
svg4,579.91,284.35,56.59,56.59
path2,579.91,284.35,56.59,56.59
Which is x1, y1, width, height. And then you can change the path to what ever you want.
There's also some tricks you can do to speed such things up. Since you know things like the convex hull of a bezier curve contains the entire curve. If your start/end/control points are already inside the current union of the current bounding box you're working with you can go ahead and move on to the next element since it must be a subset of the current bounding box.
svgelements
will calculate it correctly so will svgpathtools
But yeah basically needs to already be implemented somewhere to be helpful, since it's a different algorithm for each path segment.
Implemented in v6.3