A native SVG editor, built on its own SVG2 + CSS3 engine, written from scratch in C++20.
Donner is an SVG-native editor: a fast, native tool for creating and editing SVG, backed by an engine built for browser-grade correctness, security, and performance. The same engine that powers the editor embeds cleanly into your application, from a full GPU-rendered canvas down to a size-optimized software renderer.
- An SVG-native editor. Selection and transform tools with oriented bounding boxes, a pen tool, rich text editing with real font support, and layers. Every icon and cursor is an SVG rendered by Donner itself; the document you edit is the SVG, always.
- High SVG spec conformance. Conformance is tracked continuously against the resvg test suite with visual regression in CI.
- Fully featured. SVG2 rendering with CSS3 styling, text with a full font stack (FreeType, HarfBuzz, WOFF2) or a compact built-in stack, filters, and the first slice of SMIL animation with time-sampled rendering.
- High performance. Geode, a GPU renderer built on WebGPU, drives the editor canvas; a tiny_skia-based CPU backend serves the size-optimized embeddable build.
- Secure. Built for untrusted content: 24 fuzzers run continuously, and the engine's design lineage includes shipping untrusted SVG and glTF rendering inside privileged apps.
- Embeddable. A C++20 Bazel module with an exception-free, RTTI-free API surface; the tiny variant is tuned for binary size.
The editor ships with the engine and is under active development. Open a file, edit visually or in the built-in XML view with two-way sync, and export clean SVG.
Donner targets the SVG 2 static rendering subset. The tables below are an honest snapshot of what
the engine actually parses and renders today, derived from the code and from the conformance run in
donner/svg/renderer/tests/resvg_test_suite.cc, which pixel-compares Donner against the upstream
resvg test suite goldens on every push. That test
and the SVG 2 conformance program in design 0057
are the measurement backbone; this section is the interim human-readable summary until the
conformance suite publishes a generated gap report.
Legend: Yes = parsed and rendered; Partial = supported with the noted gaps; Parsed only = retained on the DOM but not drawn (by design); Experimental = implemented but off by default; No = not recognized (parses to an unknown element).
| Category | Elements | Support |
|---|---|---|
| Structural | <svg> <g> <defs> <symbol> <use> <style> <switch> |
Yes |
| Hyperlink | <a> |
Yes. Renders as a transparent group (its children draw in place); the link target (href / xlink:href) is retained on the DOM. |
| Shapes | <circle> <ellipse> <line> <path> <polygon> <polyline> <rect> |
Yes |
| Raster image | <image> |
Partial. Embedded (data URI) images render; external-URL and cross-file references are not loaded. |
| Text | <text> <tspan> <textPath> |
Partial. See Text features below. Text requires a text-enabled build; the size-optimized build can omit it. |
| Paint servers and markers | <linearGradient> <radialGradient> <stop> <pattern> <marker> |
Yes. Linear and radial gradients (all spread methods, radial focal point) and patterns are supported; conic/sweep gradients are not. |
| Masking and clipping | <mask> <clipPath> |
Partial. Core masking and clipping, mask-type, and vector text children in clip paths work; bitmap text silhouettes, some nested clip-path intersections, and a few mask-unit edge cases are not yet handled. |
| Filters | <filter> and the full <fe*> primitive suite |
Partial. All 17 filter primitives have DOM wrappers and renderer support. The CSS filter: function-list category is active with 26 of 43 files comparing on both backends; 17 explicitly skipped mismatches await normative classification. enable-background / BackgroundImage and some feImage subregion cases also remain. See the filter element reference. |
| Descriptive | <title> <desc> <metadata> |
Parsed only (retained, never drawn, per spec). |
| Animation (SMIL) | <animate> <animateTransform> <set> |
Experimental, off by default. An animation system exists and renders time-sampled frames, but these elements parse to their animation types only when experimental parsing is enabled; otherwise they are treated as unknown. |
Elements outside this list (for example <foreignObject>, <tref>, SVG 1.1 <font> / <glyph>,
<cursor>, <view>, <script>) are not recognized and parse to an unknown element. See
unsupported SVG 1.x features for the SVG 1.1 details.
| Support | Properties |
|---|---|
| Honored | fill / fill-rule / fill-opacity, stroke and all stroke-*, opacity, color, display, visibility, overflow, transform / transform-origin, clip-path / clip-rule, mask / mask-type, filter, color-interpolation-filters, image-rendering (including distinct pixelated, crisp-edges, smooth, quality, and legacy-alias policies for <image> and <feImage>), marker-start / -mid / -end, mix-blend-mode (all 16 modes), isolation, paint-order, and the text properties (font-*, text-anchor, text-decoration, baseline family, letter-spacing, word-spacing, writing-mode). |
| Partial | vector-effect (non-scaling-stroke is exact for uniform scale and rotation, while non-uniform transforms use a scalar approximation and the other at-risk SVG 2 values parse as typed values but render with none behavior); pointer-events (including auto) is used by hit-testing, with text, image, and clip-path edge cases still incomplete; cursor supports every CSS Basic UI keyword, ordered authored url() candidates with optional hotspots, inheritance, and DonnerController::cursorAt. CSS image-set(), declaration-base URL resolution, resource loading, and platform cursor mapping remain. |
| Recognized but not implemented | The rendering hints color-rendering, shape-rendering, text-rendering, and color-interpolation are retained as raw properties but do not yet have typed cascade or runtime behavior. |
| Not implemented | direction / unicode-bidi (bidirectional text), text-orientation, and font-size-adjust. SVG 1.1-only features such as <tref>, glyph-orientation-horizontal, and CSS2 clip: rect(...) are intentionally unsupported; see unsupported SVG 1.x features. |
Text layout supports fills and strokes (including gradient and pattern paints), per-glyph
positioning, text-anchor, textPath along a path, and a full font stack (FreeType, HarfBuzz,
WOFF2) or a compact built-in stack. Known gaps, tracked against the resvg suite: bidirectional
text and direction / unicode-bidi, textLength / lengthAdjust, several SVG 2 <textPath>
features (side, method=stretch, spacing=auto, the path attribute), full SVG 2
text-decoration (independent line, style, and color), and some vertical-rl / vertical-lr
and text-orientation cases. <tref> is intentionally unsupported because SVG 2 removed it.
Donner ships two backends behind one renderer interface. The tiny_skia CPU backend is the default
and the most complete; the Geode WebGPU backend drives the editor canvas and is at broad parity,
with a few narrower gaps (some 0 N dash caps and nested path-clip intersection) where the CPU
backend currently passes conformance cases the GPU backend does not. Both backends honor
paint-order for shapes, markers, text, and tspans, and share the same DOM, layout, paint
resolution, markers, and filter graph.
Donner also ships an end-user CLI for rendering and previewing SVG files.
# Render to PNG
bazel run //donner/svg/tool:donner-svg -- donner_splash.svg --output output.png
# Show a terminal preview
bazel run //donner/svg/tool:donner-svg -- donner_splash.svg --preview
# Interactive terminal mode with mouse selection
bazel run //donner/svg/tool:donner-svg -- donner_splash.svg --interactiveTool docs: donner-svg CLI tool
bazel run //examples:svg_to_png -- donner_splash.svgHow it works: svg_to_png.cc
// This is the base SVG we are loading, a simple path containing a line
const std::string_view svgContents(R"(
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 10 10">
<path d="M 1 1 L 4 5" stroke="blue" />
</svg>
)");
// Call ParseSVG to load the SVG file
donner::ParseWarningSink disabled = donner::ParseWarningSink::Disabled();
donner::ParseResult<donner::svg::SVGDocument> maybeResult =
donner::svg::parser::SVGParser::ParseSVG(svgContents, disabled);
if (maybeResult.hasError()) {
std::cerr << "Parse Error " << maybeResult.error() << "\n"; // Includes line:column and reason
std::abort();
// - or - handle the error per your project's conventions
}
donner::svg::SVGDocument document = std::move(maybeResult.result());
// querySelector supports standard CSS selectors, anything that's valid when defining a CSS rule
// works here too, for example querySelector("svg > path[fill='blue']") is also valid and will
// match the same element.
std::optional<donner::svg::SVGElement> maybePath = document.querySelector("path");
UTILS_RELEASE_ASSERT_MSG(maybePath, "Failed to find path element");
// The result of querySelector is a generic SVGElement, but we know it's a path, so we can cast
// it. If the cast fails, an assertion will be triggered.
donner::svg::SVGPathElement path = maybePath->cast<donner::svg::SVGPathElement>();
if (std::optional<donner::Path> computedPath = path.computedPath()) {
std::cout << "Path: " << *computedPath << "\n";
std::cout << "Length: " << computedPath->pathLength() << " userspace units\n";
} else {
std::cout << "Path is empty\n";
}Detailed docs: svg_tree_interaction.cc
using namespace donner;
using namespace donner::svg;
using namespace donner::svg::parser;
std::ifstream file("test.svg");
if (!file) {
std::cerr << "Could not open file\n";
std::abort();
}
std::string fileData;
file.seekg(0, std::ios::end);
const std::streamsize fileLength = file.tellg();
file.seekg(0);
fileData.resize(fileLength);
file.read(fileData.data(), fileLength);
ParseWarningSink warnings;
ParseResult<SVGDocument> maybeDocument = SVGParser::ParseSVG(fileData, warnings);
if (maybeDocument.hasError()) {
std::cerr << "Parse Error: " << maybeDocument.error() << "\n";
std::abort();
}
Renderer renderer;
renderer.draw(maybeDocument.result());
const bool success = renderer.save("output.png");Detailed docs: svg_to_png.cc
CMake support is available for integrating Donner into CMake-based projects. The CMake build fetches dependencies and builds the library. Both the tiny_skia (CPU) and Geode (WebGPU) backends are selectable via DONNER_RENDERER_BACKEND (default tiny_skia).
See the CMake Documentation for more details.
- C++ | LunaSVG: A lightweight library with an embedded renderer, suitable for embedded applications
- C++ | ThorVG: A production vector graphics engine with software, OpenGL/ES, and WebGPU backends and Lottie support; targets SVG Tiny 1.2 rather than full SVG, so it is not as fully featured an SVG renderer as Donner or resvg
- C | NanoSVG: A minimal single-header SVG parser and rasterizer, widely embedded where footprint matters; supports a small subset of SVG
- Rust | librsvg: Provides a simple way to render SVGs one-shot, does not provide a DOM or animation
- Rust | resvg: Library that focuses on correctness, safety, and portability for static SVGs
- Rust | Vello: A GPU compute-centric 2D renderer from the Linebender ecosystem; renders SVG through companion crates rather than natively
- Java | Apache Batik: A full dynamic SVG implementation with DOM, scripting, and declarative animation; the closest reference for browser-style dynamic SVG outside a browser engine