p5
p5 copied to clipboard
SVG output
Are there any plans for SVG output? This would be a fantastic feature, as I know it's used a lot in processing and p5? Would this be attached to the PShape class?
Upvote & Fund
- We're using Polar.sh so you can upvote and help fund this issue.
- We receive the funding once the issue is completed & confirmed by you.
- Thank you in advance for helping prioritize & fund our backlog.
Simple support would be great, but SVG reading writing can be extremely complex -- some python libraries that exist and might help with this undertaking are svgwrite, drawsvg, cairo svg.
In terms of what exists currently (read) -- I believe that it is here: https://github.com/p5py/p5/blob/master/p5/core/svg.py https://github.com/p5py/p5/blob/master/p5/core/tests/test_svg.py
PShape in Java mode is a bit of a crazy mess -- partly because its internal representation and how its methods work differs based on whether you create one from SVG, or from as a new primitive, or by drawing vertices. It would be nice to not repeat those mistakes while building out:
https://github.com/p5py/p5/blob/master/p5/core/shape.py
Yes. Handling svgs is surprising complex. Personally, the ultimate reason I’d like this feature is so I can send the output file to my pen plotter.
... any idea if there is any current interchange method that could do this?
I was just taking a peek at the svg parser, and had a question:
Once the parser determines that a path may be a particular shape (for instance, it parses an SVG and finds a 'rectangle'), and it converts that to a PShape, does the PShape know that it is a rectangle? Or no? Seems like that would determine where the SVG functions live. At the PShape level, or it's derived shape classes.
Like the reader does, the writer would need to know what the 'shape' each PShape is. If that logic could be bidirectional (SVG reader and writer), then it could be a step in the right direction.
does the PShape know that it is a rectangle? Or no?
@marcrleonard the SVG parser does not store the meta information of the shape (rect, circle, etc)
the ultimate reason I’d like this feature is so I can send the output file to my pen plotter.
I believe Processing has a good rendering engine for exporting in PDFs and SVGs: https://processing.org/reference/libraries/svg/index.html
I believe Processing has a good rendering engine for exporting in PDFs and SVGs:
It does, but I don't want to use processing... I want to use python ;-)
I would like to start hacking on SVG output for p5py. To mirror p5.js, that would ultimately be part of the save()
method, right? Any other notable things before I start digging?
Sorry to spam this thread, but I've got some more questions: I've been poking around on the flow from a shape/primitives creation, and how it eventually ripples into the Render2D, and the objects are added to the self.draw_queue ... Is this the last place they 'live' until they are rasterized to the output? Does list list contain EVERYTHING that needs to be rendered in a particular frame? I assume that list is emptied once a frame is done being shown?
that would ultimately be part of the save() method, right?
The p5.js save
method saves the image file of the sketch not the svg version: https://p5js.org/reference/#/p5/save
the objects are added to the self.draw_queue ... Is this the last place they 'live' until they are rasterized to the output?
Yes, the vertex data is converted into vertex_buffer
and index_buffer
which is then used by OpenGL shaders (https://github.com/p5py/p5/blob/master/p5/sketch/shaders2d.py#L26) for rendering.
Does list list contain EVERYTHING that needs to be rendered in a particular frame?
Yes, depending on the shape, its relevant shape attributes are added to the draw_queue (https://github.com/p5py/p5/blob/master/p5/sketch/renderer2d.py#L282). This includes: vertices
, idx
(the index triangles which needs to be rendered from the vertices
), fill
, etc.
I assume that list is emptied once a frame is done being shown?
Yes
Here is how an svg is saved in P5.js:
https://editor.p5js.org/dannyrozin/sketches/r1djoVow7
... In classic P5 form, it's a very odd API. You have to first define the canvas to be an SVG canvas, then it is saved.
Is this to be mirrored here? Looking for any implementation details that my be helpful. Is there some sort of hook or plugin architecture I could utilize? That way, it doesn't have to get baked into the core architecture (if the contributors don't want it).
If you look at the Svg output from the p5js sketch above, you’ll see that each transform is applied to each element recursively (to reflect the loop).
It appears that the p5.renderer does not track transforms in a similar way. It appears all transforms are flattened before going to the GPU for final render (which is perfectly logical). Is there a place where transforms are stored individually, so they can be applied to all the ‘underlying’ elements? Or does each transform get applied directly to the vertices, and left there?
Here is a crazy simplistic version I've started with. It doesn't support all the fills/strokes/etc... But it seems to work properly for simple lines and circles (though, the circles use 'triangles' ... so that is probably not ideal).
import drawSvg
def save_svg(output_path):
dq = p5.renderer.draw_queue
# why is this different from the the specified canvas size? is there a transform/scaler soemwhere?
print(p5.sketch.size)
svg_canvas = drawSvg.Drawing(window_w, window_h, origin=(0,0), displayInline=False)
# There may be a better way to do this through the init above, but I found it confusing.
# it was much easier to just hardcode it.
svg_canvas.viewBox = (0, 0, window_w, window_h)
for geo, meta in dq:
if geo == 'lines':
vertices, edges, stroke, stroke_weight, stroke_cap, stroke_join = meta
verts = vertices.tolist()
start_l = verts.pop(0)
start_x = start_l[0]
# the lib wants to always make Y coods negative. This is likely because of the assumption
# the moving physically down on the Y axis puts an object in the correct region.
# Basically, negative Y is 'viewable' where as in P5, positive Y is viewable.
start_y = -start_l[1]
other_verts = []
for o_v in verts:
x=o_v[0]
# See note above about negative Y values.
y=-o_v[1]
z=o_v[2]
other_verts.append(x)
other_verts.append(y)
svg_obj = drawSvg.Lines(start_x, start_y, *other_verts, fill='black', stroke='black', stroke_width=2, close=False)
elif geo == 'triangles':
vertices, idx, fill = meta
verts = vertices.tolist()
start_l = verts.pop(0)
start_x = start_l[0]
# See note above about negative Y values.
start_y = -start_l[1]
other_verts = []
for o_v in verts:
x = o_v[0]
# See note above about negative Y values.
y = -o_v[1]
z = o_v[2]
other_verts.extend([x,y])
svg_obj = drawSvg.Lines(start_x, start_y, *other_verts, fill='black', stroke='black', stroke_width=2,
close=False)
else:
# A good example is 'Points"
# elif geo == 'points':
# vertices, idx, stroke = meta
# raise NotImplemented()
raise NotImplemented("This geometry is not implemented yet.")
svg_canvas.append(svg_obj)
svg_canvas.saveSvg(output_path)
(as per a comment in there) why is p5.sketch.size
different from the the specified canvas size? is there a transform/scaler somewhere?
Is there a place where transforms are stored individually, so they can be applied to all the ‘underlying’ elements?
No, when a function for a shape is called, its vertices are transformed and stored in the queue for rendering: https://github.com/p5py/p5/blob/master/p5/sketch/renderer2d.py#L221
why is p5.sketch.size different from the the specified canvas size? is there a transform/scaler somewhere?
The p5.sketch.size
is updated when size()
function is called:
https://github.com/p5py/p5/blob/master/p5/sketch/userspace.py#L186
Is there a place where transforms are stored individually, so they can be applied to all the ‘underlying’ elements?
No, when a function for a shape is called, its vertices are transformed and stored in the queue for rendering: https://github.com/p5py/p5/blob/master/p5/sketch/renderer2d.py#L221
Ok thanks. This will be a fundamental difference between how the SVG is created (versus p5.js). But honestly, it's better this way. Way less recursive mess :-)
Here is a crazy simplistic version I've started with.
I think this is a really good implementation. I was able to use it with minor changes.
Here is a crazy simplistic version I've started with.
I think this is a really good implementation. I was able to use it with minor changes.
Great. I've also made some tweaks since. If you all want to continue with this code, is there a place I can start contributing/committing?
This can be added in the saveFrame
API: https://github.com/p5py/p5/blob/master/p5/core/image.py#L611
The drawSvg
needs to be added as dependency for this to work inside p5py. I am not sure if adding new dependency will cause any unnecessary issues. @abhikpal , what do you think?
Adding SVG output support would be great for people using pen-plotters! Thanks for working on this!!
I second that! Thanks for working on this! And SVG output would also be great for generative art archiving, and infinitely scalable images!
Maybe one could have a look at what Ricardo Lafonte and Stuart Axon are doing with pycairo & pango at http://github.com/shoebot/shoebot (I suppose Skia anf Flat could be ways to solve this too)