abey79 / vpype

The Swiss-Army-knife command-line tool for plotter vector graphics.
https://vpype.readthedocs.io/
MIT License
693 stars 60 forks source link

vpype doesn't understand svg clippath #122

Open cbmoore opened 3 years ago

cbmoore commented 3 years ago

I'm using pycairo to generate PDFs and SVGs including some designs that use the clip functionality in cairo. SVGs generated this way use the clipPath but vpype does not seem to understand it. To be fair, neither does inkscape or the Axidraw software. As a result, designs using clipPath fail to plot correctly. Ideally, vpype would remove the content that is clipped by the clipPath.

Python code that generates the file is below. I attached the PDF so you can see what it should look like. The svg is attached as .txt to make it into a supported file type.

import cairo, math

WIDTH, HEIGHT = 5, 5

NLINES = 50
LWIDTH = 0.004

r1 = 0.19

#surface = cairo.PDFSurface('test_ce.pdf', 72 * WIDTH, 72 * HEIGHT)                                      
surface = cairo.SVGSurface('test_ce.svg', 72 * WIDTH, 72 * HEIGHT)
ctx = cairo.Context(surface)

ctx.scale(72 * WIDTH, 72 *  HEIGHT)  # Normalizing the canvas                                            
ctx.set_source_rgb(0,0,0)
ctx.set_line_width(LWIDTH)
ctx.set_fill_rule(cairo.FillRule.EVEN_ODD)

ctx.arc(.5, .5, r1, 0, 2 * math.pi)
ctx.arc(0.5, 0.5, r1 * 1.43, 0, 2 * math.pi)
ctx.clip()

for i in range(0, NLINES):
    x = float(i)/NLINES + LWIDTH/2.0
    ctx.move_to(x, 0)
    ctx.line_to(x, 1)

ctx.stroke()

test_ce.pdf

test_ce.svg.txt

tatarize commented 3 years ago

meerk40t/svgelements#74 svgelements does not properly acknowledge or have a way to fully identify clipPath objects. Though I've encountered them before. There's also some fill types like gradient that get missed. Shapely might be able to do the slicing here, so it could, in theory, actually succeed. But, it would start with fixing it in svgelements to provide the clip-path and url linked objects somehow. Although in that case it would have the clip-path objects but doesn't have the goal or wherewithal to enforce them.

tatarize commented 3 years ago

Having looked at the spec enough now, I must say I think the file generated there is wrong and that's why Inkscape failed to recognize it. The rules shouldn't work.

<g id="surface1">
<g clip-path="url(#clip1)" clip-rule="nonzero">
<g clip-path="url(#clip2)" clip-rule="evenodd">
<path style="fill:none;stroke-width:0.004;...

The problem here is that the clip-path source data does not define the clip-rule attribute. That must be defined within the clipPath object or the children. It's an attribute of the child object. Try:

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="360pt" height="360pt" viewBox="0 0 360 360" version="1.1">
<defs>
<clipPath id="clip1">
  <path d="M 82 82 L 278 82 L 278 278 L 82 278 Z M 82 82 "/>
</clipPath>
<clipPath id="clip2" clip-rule="evenodd">
  <path d="M 248.398438 180 C 248.398438 217.777344 217.777344 248.398438 180 248.398438 C 142.222656 248.398438 111.601562 217.777344 111.601562 180 C 111.601562 142.222656 142.222656 111.601562 180 111.601562 C 217.777344 111.601562 248.398438 142.222656 248.398438 180 L 277.8125 180 C 277.8125 234.019531 234.019531 277.8125 180 277.8125 C 125.980469 277.8125 82.1875 234.019531 82.1875 180 C 82.1875 125.980469 125.980469 82.1875 180 82.1875 C 234.019531 82.1875 277.8125 125.980469 277.8125 180 "/>
</clipPath>
</defs>
<g id="surface1">
<g clip-path="url(#clip1)">
<g clip-path="url(#clip2)">
<path style="fill:none;stroke-width:0.004;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 0.00199653 0 L 0.00199653 1 M 0.0220052 0 L 0.0220052 1 M 0.042003 0 L 0.042003 1 M 0.0620009 0 L 0.0620009 1 M 0.0819987 0 L 0.0819987 1 M 0.101997 0 L 0.101997 1 M 0.122005 0 L 0.122005 1 M 0.142003 0 L 0.142003 1 M 0.162001 0 L 0.162001 1 M 0.181999 0 L 0.181999 1 M 0.201997 0 L 0.201997 1 M 0.222005 0 L 0.222005 1 M 0.242003 0 L 0.242003 1 M 0.262001 0 L 0.262001 1 M 0.281999 0 L 0.281999 1 M 0.301997 0 L 0.301997 1 M 0.322005 0 L 0.322005 1 M 0.342003 0 L 0.342003 1 M 0.362001 0 L 0.362001 1 M 0.381999 0 L 0.381999 1 M 0.401997 0 L 0.401997 1 M 0.422005 0 L 0.422005 1 M 0.442003 0 L 0.442003 1 M 0.462001 0 L 0.462001 1 M 0.481999 0 L 0.481999 1 M 0.501997 0 L 0.501997 1 M 0.522005 0 L 0.522005 1 M 0.542003 0 L 0.542003 1 M 0.562001 0 L 0.562001 1 M 0.581999 0 L 0.581999 1 M 0.601997 0 L 0.601997 1 M 0.622005 0 L 0.622005 1 M 0.642003 0 L 0.642003 1 M 0.662001 0 L 0.662001 1 M 0.681999 0 L 0.681999 1 M 0.701997 0 L 0.701997 1 M 0.722005 0 L 0.722005 1 M 0.742003 0 L 0.742003 1 M 0.762001 0 L 0.762001 1 M 0.781999 0 L 0.781999 1 M 0.801997 0 L 0.801997 1 M 0.822005 0 L 0.822005 1 M 0.842003 0 L 0.842003 1 M 0.862001 0 L 0.862001 1 M 0.881999 0 L 0.881999 1 M 0.901997 0 L 0.901997 1 M 0.922005 0 L 0.922005 1 M 0.942003 0 L 0.942003 1 M 0.962001 0 L 0.962001 1 M 0.981999 0 L 0.981999 1 " transform="matrix(360,0,0,360,0,0)"/>
</g>
</g>
</g>
</svg>
tatarize commented 3 years ago

https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/clip-rule

The clip-rule attribute only applies to graphics elements that are contained within a element. The clip-rule attribute basically works as the fill-rule attribute, except that it applies to definitions.

The following fragment of code will cause an evenodd clipping rule to be applied to the clipping path because clip-rule is specified on the element that defines the clipping shape:

<g>
    <clipPath id="MyClip">
        <path d="..." clip-rule="evenodd" />
    </clipPath>
    <rect clip-path="url(#MyClip)" ... />
</g>

whereas the following fragment of code will not cause an evenodd clipping rule to be applied because the clip-rule is specified on the referencing element, not on the object defining the clipping shape:

<g>
    <clipPath id="MyClip">
        <path d="..." />
    </clipPath>
    <rect clip-path="url(#MyClip)" clip-rule="evenodd" ... />
</g>

As a presentation attribute, it also can be used as a property directly inside a CSS stylesheet

tatarize commented 3 years ago

@abey79 The ability to do this now exists. Though 1.4.0 isn't merged, and likely won't be for a bit. I conducted a code review with the W3C tests and identified a few minor edge cases. Though I also introduced a few breaking changes, though I dunno where the loading code is in vpype to tell you that it should affect nothing (though I certainly seriously doubt it would).

My question, concerns the would-be API for this. I'm not expecting you to actually implement this, but the ability exists, but if you were going to, what api would you prefer?

Currently, if an object has a clip-path reference it gets assigned a .clip_path object. All other objects do not have a .clip_path attribute mean any would-be users would need to check hasattr() or a try-except. These objects do not propagate. They do not merge together. They can't because they can be applied in overlapping ways. The clippaths only exist at the one position where they are referenced. All items inside a ClipPath have an assigned .clip_rule attribute, this is either equal to the value set in the file, or to the default (nonzero).

Potentially Breaking Changes:

I can't really picture any of these affecting your code, but things like fixing stroke-width to be a proper attribute (this was needed since it was a length/percent in CSS and not something that could be solved later outside of the parsing process) could maybe make some earlier code that tried to use this less relevant and incorrect. Mostly I don't want to commit to a ClipPath api and then need to change it later for some reasons.

Are your current clipping done with Shapely? And does it properly support sort of clipping with a path?

abey79 commented 3 years ago

Thanks for the detailed feedback!

Clipping path

It's a bit tough to say without taking a deep dive into it. Using shapely would be the default choice to implement clipping (although I'm not sure if it handles EVEN_ODD rules). I would call it on a per-path basis, so it would be easier if the .clip_path property would be propagated to all nodes, but I understand the issue of potentially nested clip paths.

Now, for performance reason, I could consider switching to PyGEOS. It's the same underlying engine as Shapely (libGEOS), but with a numpy-based API that offers vectorised API which is great to apply the same operation on the large number of geometries (the inner loop is in C rather than Python). I would need to adjust my parsing loop to be properly recursive, which would probably automagically deal with nested clip paths.

I probably won't be able to deal with that in the short time but I'm quite confident I'll be able to use whichever API you have.

Quick question: will the clip path object be immutable/hashable? This would be nice so that I can build a clip_path -> Shapely mask dictionary in the process.

Breaking changes

I see no issue there. The proper support for stroke width will be interesting when I implement per-color/width/etc. layering.

tatarize commented 3 years ago

Breaking Changes.

The proper support for stroke width was in part because I was was talking about the whole implement layers and pen-width stuff and you can just move those layers around, yadda yadda and I realized I didn't really have the ability to make that easy from the SVG side of things. I didn't think anything would be annoying and I spent like a couple days going through all the tests and fixing things. I also have a solid enough list of what I do and don't support because of that. I missed a couple things like SVG is case sensitive but CSS is not so fiLl="black" should fail whereas style="fiLl: black" should not fail. Some degenerate shapes handing and things like truncating infinite looks created by recursive use use such as to not prevent rendering. But, I'm pretty confident I didn't break something new.

I hadn't mentioned but I stopped handing out the viewbox like an element. It gets applied to things but treated a lot more like a transform. Part of that was that I was misunderstanding the concept a bit.

Clipping Path

Obviously the easiest to on your side would be if I implemented render clippath into a path, though that'd require some geometry processing. Which I could code up. My efill routine shares a lot in common with Vatti Polygon Clipping, and I could likely write that routine rather easily, though I think it's goofing with the treatment of every vertex as a single point rather than individual line segments, with 2 endpoints. See: http://what-when-how.com/computer-graphics-and-geometric-modeling/clipping-basic-computer-graphics-part-5/ It's basically like my algorithm there but runs a scan-line across two polygons at the same time. You can use it to implement CAG (Constructive Area Geometry) pretty effectively. Though this requires taking shapes and turning them into straight line segments which I generally don't do, and you do as a rule. And geometry code that is kinda outside the scope of the project.

There might be something to combining the paths into a list of paths and applying it on each individual end-shape. But, I haven't checked much.

An alternative proposal

Most masking operations are basically like pixel operations, and this stuff isn't really supposed to alter underlying geometry. You can't see or click that geometry but it's still technically there even though it's masked off. Even if you have two overlapping circles with fill, part of the circle is not seen but if you put that into a pen plotter you wouldn't get an occluded circle. You'd get two overlapping circles. As far as this issue goes the right answer might actually just be "no", clipping paths affect the visualization of objects not their geometry and your project is strictly concerned with their geometry.

If they want to clip a path, they should use the tools you give them which could well include CAG capabilities directly. So they'd add their clipping path as a real path and indicate somewhere in vpype they would like to apply layer n as a clipping layer to all of the other layers. This would use the same tools as it would take to implement this but make it much more broadly available and consequently useful. CAG would be a strong contender for core operation.


It's technically possible to access clipping path information. I also wrote a couple other things like Patterns but these are harder since they replace fill values and all my fill values are colors so that's a bit more breaking than I wanted to go. So those parse and get stored in the lookup but, aren't directly applied. But, this should be good enough for now.

I'll write that section you suggested outlining the implemented and non-implemented sections of the spec into the readme and merge that later today.

abey79 commented 3 years ago

@cbmoore What are your thoughts regarding the alternative proposal above? It always have been a plan to have a mask command that would use (closed) geometries in one layer to mask geometries in other layer(s). That would be rather easy to implement I think. Can you give us some feedback on how you came up with a SVG containing clip paths? Would the alternative of having the clip paths added in another layer be feasible for you?

cbmoore commented 3 years ago

@abey79 I started out in generative art using python and Cairo. I thought it was a good choice because of its maturity and ability to produce both SVGs and PDFs. I've built a fair amount on top of it to generate PDFs from which to create letterpress plates. The clip functionality in Cairo turns out to be fairly useful in many of my designs and produces PDFs that can be turned into plates without any problem. I thought that I'd use a unified approach that would allow me to create both plotter work and letterpress plates from the same codebase but the lack of a way for a plotter to "see" the clips correctly makes that difficult. I was looking at vpype as the right place for this kind of functionality to live since clips should happen before optimization and (I thought) nearly all the pieces would be already in place within vpype.

I don't think the suggestion of using layers to contain the clip paths works very well (at least for me) for three reasons: 1) Cairo doesn't have a way to create layers in SVGs 2) even if it did, doing so would break the PDF renderings, 3) it would require pairing many layers with corresponding clips thus adding quite a bit of complication to the (eventual) application of vpype.

You can see a (simplified) version of the python Cairo code that generated the SVGs with clip paths at the top of this thread. Thank you for your work on vpype and vsketch-- I realize that my use case might be too niche to be worth it if you're not seeing a lot of other requests for handling clip paths in SVGs.