Architecture
One library with no I/O in it, three hosts that have all of it, and a scene in the middle that neither SVG nor draw.io can see.
The shape of it
SVG text ─parse─▶ resolved elements ─scene─▶ Scene ─layout─▶ Scene ─emit─▶ mxfile XML
The scene in the middle is the point. It knows nothing about SVG and nothing about draw.io, so reading a drawing and guessing what it means can be tested apart from each other, and a profile can build the scene directly without reimplementing the half that writes draw.io.
The crates
| Crate | What it is | What it may touch |
|---|---|---|
| the library | The conversion, at the workspace root | Nothing. No filesystem, clock, randomness, threads or environment |
accent-draw-cli | The accent-draw command | Files, arguments, exit codes |
accent-draw-wasm | The browser binding | Whatever the page gives it |
accent-draw-miro | The Miro command, for folders | Files |
The library's restriction is enforced rather than intended: a lint configuration bans the standard library's routes to all five, and a host that needs them carries its own configuration allowing them. That is what makes the WebAssembly build work without shims, and it is why the same input gives the same bytes in a browser and in a shell.
Why not usvg
usvg is the obvious crate for reading SVG in Rust, and it is the wrong one
here. It is built for rendering: it turns rect, circle and ellipse into
paths, dissolves markers into plain geometry, and needs fonts loaded to keep
text at all. Every one of those is precisely the information a diagram
converter exists to keep.
So the reading is roxmltree for the tree, svgtypes for path data, transforms,
colours and lengths, and simplecss for stylesheet rules, with the cascade and
the transform resolution written here. resvg is still used - as the reference
rasterizer in tests, which is a different job.
Deterministic output
Cell ids are stable and numbers are formatted one way, so:
- converting the same SVG twice gives the same file, byte for byte;
- a change to the converter shows up as a readable diff over the whole corpus, rather than as noise;
- the browser and the command line can be compared exactly - and they are, on every change, by running the corpus through the WebAssembly build under a WASI runtime and diffing it against the native one.
The roundtrip
- SVG to
.drawio- this project. Lossy, the first time, and the interface says so. - Edit in draw.io, embedded in a page: an iframe on the configured origin,
sent
{action: "load", xml}. - Back to SVG with
{action: "export", format: "xmlsvg"}- an SVG picture carrying the diagram in its root'scontentattribute.
Which is why an SVG that already carries a diagram is never converted from its picture: that diagram is returned as it is. The first trip is an approximation; every one after it is exact.
Editing elsewhere
draw.io's documentation says embed mode is supported only on
embed.diagrams.net. It was tested against a self-hosted draw.io 31.4.6 anyway,
and the whole flow - open, load, edit, export, download - ran unchanged three
ways: from the official Docker image, from that image's webapp served as plain
static files, and from those files under a path prefix.
So a draw.io of your own works, with two cautions. Nothing promises the next
draw.io release keeps it, so pin the version and try editing again after each
upgrade. And the image sends no framing header of its own - the policy it does
set is a <meta> tag, which cannot restrict framing - so have your server send
Content-Security-Policy: frame-ancestors <your page's origin>.
A Rust and WebAssembly port of the editor was sized instead of built: about
200,000 lines of draw.io's JavaScript before any shape libraries are counted,
against a converter of a few thousand, with two dozen draw.io releases in the
three months the question was asked. The recommendation was not to port, to
self-host where content must stay in-house, and to keep embed.diagrams.net as
the default.
How it is verified
By looking at it. Every change converts the whole corpus and renders each source SVG beside the exported diagram. Several defects in this project were invisible in the XML and obvious in the picture, which is the whole argument for doing it this way.
By counting. A selfcheck run reports, per file, the cell count, the shapes that moved, the style keys gained or lost, and the edges that lost a binding. "No shapes moved across the corpus" is a claim worth making; "it looks right in one file" is not.
Against other tools. Two existing converters - one JavaScript, one Python - were run over the same corpus first, and their numbers are the bar: connectors glued to real shapes, and labels sitting inside the shapes they belong to. A connector bound to the page rather than to a shape does not count as glued, because moving the page would then drag the arrows.
Against golden data. One of those projects publishes its scene for each of its samples - which connector joins which shape, which text became which label, which shape sits in which frame. Those are expected answers for the layout stage and are checked as such.