meerk40t / svgelements

SVG Parsing for Elements, Paths, and other SVG Objects.
MIT License
133 stars 29 forks source link

Write to svg #102

Closed ghost closed 3 years ago

ghost commented 3 years ago

is it also possible to write elements to a svg file?

tatarize commented 3 years ago

Not strictly with the library as is, you could certainly use something like svgwrite for that but, this module is mostly concerned accurately rendering the geometry. You usually end up with a bunch of paths and you can write that stuff out pretty easily since they all have d() as a command to produce the path_d strings. But, it's not strictly concerned with element writing.

ghost commented 3 years ago

Thanks for your reply! Is it possible to apply transformations to objects at object init-time? Like applying rotations to a Line object, so that calling the bbox method returns the correct box of the rotated object?

tatarize commented 3 years ago

Yeah. If you have a SimpleLine you can multiply it by a Matrix or another transformation.

>> from svgelements import SimpleLine
>> (SimpleLine((0,0), (100,100)) * "rotate(45deg)").bbox()
(-70.71067811865474, 0.0, 0.0, 70.71067811865476)
tatarize commented 3 years ago
>> SimpleLine((0,0), (100,100), transform="scale(0.1)").bbox()
(0.0, 0.0, 0.0, 10.0)

And if you wanted to do something like that during init, it's pretty easy to do there too. They will apply according to the svg spec.

>>SimpleLine((0,0), (100,100), transform="scale(0.1) translate(100,200)").bbox()
(10.0, 20.0, 10.0, 30.0)

So the translated location is scaled before bbox() is called. That's true for most things, if SVG said how to do that work, that's how it's done.

As part of #87 I might eventually get around to writing a full reading, writing, and geometric rendering library. But, depending on your uses this one is likely one of the most complete for parsing and remixing of svg geometries.

ghost commented 3 years ago

Excellent! Thanks for your help!

ghost commented 3 years ago

By the way, the stroke_width or stroke_linecap seems to have no effect on the bounding box:

In [91]: se.SimpleLine((0,0),(10,10),stroke_linecap="round",stroke_width=100).bbox()
Out[91]: (0.0, 0.0, 0.0, 10.0)

which could be added manually to the bbox for simple shapes like Line, but not for a complex Path!

tatarize commented 3 years ago
<svg width="285" height="350" viewBox="0 0 285 350" fill="none" xmlns="http://www.w3.org/2000/svg">
<line id="test-line" x0="0" x1="100" y0="0" y1="100" stroke-width="50" stroke="red"/>
</svg>

Then:

document.getElementById('test-line').getBBox()
> SVGRect {x: 0, y: 0, width: 100, height: 100}

The stroke-width is a paint attribute on the geometry, the getBBox() gets the bounding box of the actual object. If you want your object to actually be a filled fat line within geometry you'd want to convert it to a filled closed shape of the outerpath. In the above test on Chrome you'll see that it had a stroke-width of 50 and thus was well outside the SVGRect() returned but only gave the size of the geometry, which is the correct answer and method.

How things paint and things like their line-cap don't actually define the bounding box. If their actual area is within the bbox() not the total area of the paint.

tatarize commented 3 years ago

Also, if you wanted to do this for a path, it wouldn't be that hard. You'd need to do some sampling along the path, finding the normal vector, moving a particular distance from the line on both sides and then use that to define the shape. But, it would be clearly much easier to just add stroke-width to the edges since your value isn't going to exceed that except for perhaps with an line end-cap. svgpathtools has the needed code for finding the tangent and normal vectors for particular paths and even gives an example of offset paths there. It's just that it gets a bit hefty into geometry which isn't really the purpose of the module. Parsing svg solidly and getting the valid geometry is the bulk of the goal here, though that functionality isn't that hard and is still somewhat low-hanging fruit.

ghost commented 3 years ago
<svg width="285" height="350" viewBox="0 0 285 350" fill="none" xmlns="http://www.w3.org/2000/svg">
<line id="test-line" x0="0" x1="100" y0="0" y1="100" stroke-width="50" stroke="red"/>
</svg>

Then:

document.getElementById('test-line').getBBox()
> SVGRect {x: 0, y: 0, width: 100, height: 100}

The stroke-width is a paint attribute on the geometry, the getBBox() gets the bounding box of the actual object. If you want your object to actually be a filled fat line within geometry you'd want to convert it to a filled closed shape of the outerpath. In the above test on Chrome you'll see that it had a stroke-width of 50 and thus was well outside the SVGRect() returned but only gave the size of the geometry, which is the correct answer and method.

How things paint and things like their line-cap don't actually define the bounding box. If their actual area is within the bbox() not the total area of the paint.

Could you maybegive me an example how could I convert a SimpleLine to a filled-closed path so that asking the bbox would consider the stroke_width of the line too?

tatarize commented 3 years ago

If you wanted to do it with just a line, you could easily get the normal vector and expand the line outwards. Basically you find the direction that is exactly 90° off the direction of the line. You would then expand that shape outwards in both directions by stroke_width/2 and in theory you'd have geometry that would be expected be equal to that line with a stroke width. It wouldn't, however, take into account, endcap with is another property of drawing things where the line gets a end bit, either none or butt or miter etc. But, you would have accounted for the width there.

I don't, however have code to calculate the normal or tangent vectors of a shape. This code does exist within svgpathtools but I didn't manage to port it over or see a reason to do so at the time. This code base is much more concerned with correctly parsing svg and pathtools is more about math. And you'd mostly need to sample the normal vector at certainly positions in order to do this operation. Along a line the correct answer is usually pretty easy. arctan2(y2-y1,x2-x1) + 90° would give you the angle then you could simply use polar coords x = cos(angle) * r + x and y = sin(angle) * r + y and you've calculated the correct angle normal angle of the line and can move any point (x,y) a distance r from that line to find the parallel line. But, there might be easier math to do this and it isn't being helped at all by the library. I'll raise an issue to take normal and tangent angles from svgpathtools but it wouldn't come about very soon.

It gets progressively harder in other shapes but svgpathtools has a demonstration of the offset code there. And while it might be a shame to take the svgelements path and take the .d() value and then put that in an svgpathtools path just to take find a sampling of the normal vector as you need to make an offset curve, it's the only way I currently know how to do right away.

ghost commented 3 years ago

Many thanks! Still one last question on this: how could I expand the lines pathd outwards?

ghost commented 3 years ago

I ended up rotating a rect to the angle of the line instead of expanding the shape outwards:

from math import atan2, hypot
import svgwrite as s
import svgelements as se

D = s.drawing.Drawing(filename="/tmp/asd.svg", size=(1000, 1000), debug=True)
x1,y1,x2,y2=10,35,30,20
a=atan2(y2-y1,x2-x1)
thickness =10
ry=y1 - thickness*.5
r=se.Rect(x1,ry,hypot(x2-x1, y2-y1),thickness)*f"rotate({a}rad {x1} {y1})"
# A simpleline to test
D.add(s.path.Path(d=se.SimpleLine(x1,y1,x2,y2).d(), stroke=s.utils.rgb( 0, 0, 205)))
D.add(s.path.Path(d=r.d(),fill=s.utils.rgb(100,0,0,"%")))
D.save(pretty=True)

Screenshot_2021-04-24_17-51-03

This seems to give me what I was looking for.

Thanks for your help again!

tatarize commented 3 years ago

That's some nifty math. Line is a bit easy to do since your normal vector stays the same, though you get some heftier math with curves. I think a order 3 bezier curve needs to be properly offset with an order 10 curve. Though there's some very simple sampling techniques you can do simply that.

Clever trick replacing the line with a rotated rect of a given thickness. In fact, you could set your rx and ry on the rectangle and make rounded endcaps, which is kind of amusing, since I generally said above that endcaps were a non-starter.

ghost commented 3 years ago

That's exactly why I wanted the Rect; it looks like a line plus, I have all nice bbox information even by simulating the line-cap with rx/ry.