vsvg icon indicating copy to clipboard operation
vsvg copied to clipboard

SVG import

Open abey79 opened this issue 2 years ago • 8 comments

The goal of SVG import is to convert SVG into a Path/Layer/Doc representation "as close as possible" to the original data, within the constraints of our chose data model.

In the scope of vpype2, this includes:

  • sort paths into a Document/Layer/Path structure—this includes correctly interpreting inkscape layers
  • curved path elements should be preserved (#1)
  • as much metadata as possible should be extracted (#4)

usvg offers most of that, but doesn't provide access to original attributes: https://github.com/RazrFalcon/resvg/issues/588

Possible avenues:

  1. fork usvg and add raw attribute to nodes
  2. fork usvg and add ref to roxml element to nodes
  3. migrate to svg (a lot more things would have to be done manually)
  4. use svg in parallel to usvg just to extract top-level groups attributes 4b) use roxmltree in parallel to/before usvg just to extract top-level group attributes
  5. what else?

Of these, (4) appears to be the most immediate hack, though it's not exactly clean. Option (3) would provide the most flexibility in the long term.

abey79 avatar Feb 28 '23 15:02 abey79

For (3), kurbo::BezPath::from_svg() implements SVG path parsing, simplify arcs into cubic bezier. It does keep quad bezier, but they would easily be converted to cubic and/or accepted in the data model.

abey79 avatar Feb 28 '23 15:02 abey79

Full migration to svg (option 3) feels daunting. This basically is the output of that lib for resources/fixtures/multilayer.svg:

Instruction: "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
Tag svg: Start {"xmlns:sodipodi": Value("http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"), "xmlns:ev": Value("http://www.w3.org/2001/xml-events"), "version": Value("1.2"), "baseProfile": Value("tiny"), "xmlns:inkscape": Value("http://www.inkscape.org/namespaces/inkscape"), "width": Value("15.875cm"), "xmlns:rdf": Value("http://www.w3.org/1999/02/22-rdf-syntax-ns#"), "xmlns:cc": Value("http://creativecommons.org/ns#"), "height": Value("15.875cm"), "xmlns:xlink": Value("http://www.w3.org/1999/xlink"), "viewBox": Value("0 0 600.0 600.0"), "xmlns:dc": Value("http://purl.org/dc/elements/1.1/"), "xmlns": Value("http://www.w3.org/2000/svg")}
Tag line: Empty {"x2": Value("420"), "y1": Value("20"), "x1": Value("300"), "stroke": Value("#ccc"), "y2": Value("35")}
Tag g: Start {"style": Value("display:inline"), "fill": Value("none"), "inkscape:label": Value("3"), "id": Value("layer1"), "inkscape:groupmode": Value("layer"), "stroke": Value("#00f")}
Tag polygon: Empty {"points": Value("10.0,10.0 10.0,210.0 210.0,210.0 210.0,10.0")}
Tag g: End {}
Tag g: Start {"fill": Value("none"), "id": Value("layer2"), "stroke": Value("#080"), "style": Value("display:inline"), "inkscape:groupmode": Value("layer"), "inkscape:label": Value("2")}
Tag polygon: Empty {"points": Value("400.0,400.0 400.0,700.0 600.0,700.0 600.0,400.0")}
Tag g: Start {"stroke": Value("#906"), "transform": Value("translate(-50, -50) rotate(35, 400, 200)")}
Tag polygon: Empty {"points": Value("400.0,400.0 400.0,700.0 600.0,700.0 600.0,400.0")}
Tag g: End {}
Tag g: End {}
Tag svg: End {}

All the basic stuff needed to be done: unit, interpreting transforms, default attribute values, etc. And that's not counting advanced stuff (use, nested svg, CSS, reference, etc.). 100% doesn't sound like something I want to undertake.

abey79 avatar Feb 28 '23 16:02 abey79

The problem with option (4) is that it requires double parsing (once with svg, once with roxlmtree (behind usvg). A better approach might be to use roxmltree instead of svg to extract the inkscape:* attributes, as a usvg::Tree can be built from a roxmltree::Document. Hence option (4b).

abey79 avatar Mar 01 '23 09:03 abey79

Option (4) implemented in 3f13cd7190b44fdee0d74e1bf2634745fc6a16d5

abey79 avatar Mar 02 '23 17:03 abey79

Discussions in the resvg issue linked above indicate that there is no path forward were usvg would properly support Inkscape layers. A permanent fork would be needed to pursue something along the lines of (1) or (2).

Btw, both the discussions and my experience so far indicate that (2) is not actually feasible. usvg does way too many tree-level manipulation for this to be viable. Even some top-level path sometime end up in "phantom" groups (which would have to be marked as such).

abey79 avatar Mar 13 '23 10:03 abey79

For the time being, the way forward is to for usvg and rosvgtree to support Inkscape layers. Initial implementation is here: https://github.com/abey79/resvg/pull/1. I'll maintain the vsvg branch on my resvg fork as the dependency for this project.

abey79 avatar Mar 22 '23 10:03 abey79

Proper implementation in edd1d796118d26d26018d9392baa6ef2ab8d7648 :tada:

abey79 avatar Mar 23 '23 08:03 abey79

As I want to publish vsvg & friends to crates.io, I can no longer use a fork of usvg and rosvgtree. So I need an alternate way to:

  1. propagate inkscape:xxx attributes through usvg and rosvgtree
  2. identify "spurious" top-level groups produced by usvg around some drawable

My solution consists of rewriting the input SVG to encode all needed information into the group's id attribute before feeding the SVG to usvg.

If any of the inkscape:groupmode, or inkscape:label attribute exists, they are all (together with id) stored in a struct of Option, serialised to JSON, and base64 encoded after a __vsvg_encoded__ prefix. If only id exists, it's left as is. Otherwise, the id attribute is set to __vsvg_empty__XXX, where XXX is some random characters to preserve unique ID.

This way, after processing with usvg, the id attributes are starting either by __vsvg_encoded__ or __vsvg_empty__, or can be considered "spurious".

After experimenting with the great quick_xml crate, the preprocessing step appears to be <<100ms for SVGs up to 100MB.

I am both ashamed and proud of this solution. SVG does that to you...

abey79 avatar Sep 28 '23 21:09 abey79