Donner
C++20 SVG rendering library
Loading...
Searching...
No Matches
Donner API

Overview

Donner provides several standalone APIs for core functionality which can be used in any project.

Each library has minimal dependencies and can be used in external projects. For example, the CSS API may be used to add CSS support to your project using an adapter that satisfies the donner::ElementLike concept.

flowchart TD
  SVG(SVG)
  CSS(CSS)
  Rendering(SVG Rendering)
  XML(SVG XML Parsing)

  SVG --> CSS
  Rendering --> SVG
  XML --> SVG

Principles

Minimal memory allocations

  • API invocations do not incur memory allocations unless required, and internally memory allocations are kept to a minimum. Supporting constructs such as donner::RcString and donner::SmallVector are used to reduce the number of allocations.

Value types

  • Implementation details about memory allocation are hidden, and the API is designed to be used with value types. For example, donner::svg::SVGElement is can be copied and passed by value.

Error handling

  • Donner does not use C++ exceptions.
  • std::optional and donner::base::parser::ParseResult are used to return values from functions. This allows the caller to check for errors and handle them as needed. ParseResult behaves similarly to std::expected (new in C++23), and may be replaced with it in the future.
  • Callers can if init-statements to check for success:

    if (ParseResult<int> maybeInt = NumberParser::parse("123"); maybeInt.hasResult()) {
    int intValue = maybeInt.result();
    }

C++20 design patterns

  • Donner uses C++20 features such as concepts when possible to improve usability and readability.
  • For example, the donner::ElementLike concept is used to make the CSS library usable without depending on the SVG library, or within a test context with donner::FakeElement.

    template <typename T>
    concept ElementLike = requires(const T t, const T otherT, const XMLQualifiedNameRef attribName) {
    { t.operator==(otherT) } -> std::same_as<bool>;
    { t.parentElement() } -> std::same_as<std::optional<T>>;
    // ...
    };
  • Coroutine-based generators are used to implement tree traversal, see ElementTraversalGenerators.h.

String handling

  • donner::RcString is used to store strings. This is a reference counted string, which allows cheap copy and move operations. RcString implements the small-string optimization and does not allocate memory for strings shorter than 31 characters (on 64-bit platforms).
  • Use std::string_view for APIs that only need to read the input string and not store it.
  • For APIs that want to store the string, use donner::RcStringOrRef which can hold either a std::string_view or a donner::RcString, and is used to avoid unnecessary copies. This allows string literals to be used at the API surface while still allowing RcString references to be transferred.

Limitations

  • Donner is NOT thread-safe, the caller must ensure they are not accessing the same document from multiple threads.

Details

Namespace hierarchy

Parsing an SVG

To parse an SVG document, use the SVGParser class:

// Call ParseSVG to load the SVG file
ParseResult<donner::svg::SVGDocument> maybeResult =

donner::base::parser::ParseResult contains either the document or an error, which can be checked with hasError() and error():

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::parser::SVGParser::ParseSVG accepts a string containing SVG data.

// Load the file and store it in a mutable std::vector<char>.
std::ifstream file(argv[1]);
if (!file) {
std::cerr << "Could not open file " << argv[1] << "\n";
std::abort();
}
std::string fileData;
file.seekg(0, std::ios::end);
const size_t fileLength = file.tellg();
file.seekg(0);
file.read(fileData.data(), static_cast<std::streamsize>(fileLength));

Store the resulting donner::svg::SVGDocument to keep the document in-memory, and use it to inspect or modify the document.

For example, to get the donner::svg::SVGPathElement for a "<path>" element:

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.has_value(), "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.

Using the DOM

donner::svg::SVGElement implements a DOM-like API for querying and modifying the SVG document.

The document tree is traversable with firstChild(), lastChild(), nextSibling(), and previousSibling(). The element's tag name can be retrieved with tagName().

Example iterating over children:

for (std::optional<SVGElement> child = element.firstChild(); child;
child = child->nextSibling()) {
std::cout << "Child tag name: " << child->tagName() << "\n";
}

Every SVG element has its own DOM type:

SVGElement element = ...;
if (element.isa<SVGCircleElement>()) {
SVGCircleElement circle = element.cast<SVGCircleElement>();
// Use circle
}

Full list of DOM types

DOM Type XML Tag Name
donner::svg::SVGCircleElement "<circle>"
donner::svg::SVGClipPathElement "<clipPath>"
donner::svg::SVGDefsElement "<defs>"
donner::svg::SVGElement (none, base class)
donner::svg::SVGEllipseElement "<ellipse>"
donner::svg::SVGFEGaussianBlurElement "<feGaussianBlur>"
donner::svg::SVGFilterElement "<filter>"
donner::svg::SVGGElement "<g>"
donner::svg::SVGGeometryElement (none, base class)
donner::svg::SVGGradientElement (none, base class)
donner::svg::SVGGraphicsElement (none, base class)
donner::svg::SVGLinearGradientElement "<linearGradient>"
donner::svg::SVGLineElement "<line>"
donner::svg::SVGMaskElement "<mask>"
donner::svg::SVGPathElement "<path>"
donner::svg::SVGPatternElement "<pattern>"
donner::svg::SVGPolygonElement "<polygon>"
donner::svg::SVGPolylineElement "<polyline>"
donner::svg::SVGRadialGradientElement "<radialGradient>"
donner::svg::SVGRectElement "<rect>"
donner::svg::SVGStopElement "<stop>"
donner::svg::SVGStyleElement "<style>"
donner::svg::SVGSVGElement "<svg>"
donner::svg::SVGUnknownElement any unknown tag
donner::svg::SVGUseElement "<use>"

Manipulating the tree

To add a child to an element, use one of these methods:

To remove an element from the tree:

NOTE: The remove() method will remove the element from the tree, but the underlying data storage is not currently cleaned up.

To create an element, use the element-specific Create method:

// Add a circle to the document
circle.setCx(donner::Lengthd(5));
circle.setCy(donner::Lengthd(5));
circle.setR(donner::Lengthd(4));
circle.setStyle("color: #AAA");
document.svgElement().insertBefore(circle, path);

Rendering

To render an SVG document, use the donner::svg::RendererSkia class.

// Draw the document, store the image in-memory.
renderer.draw(document);
std::cout << "Final size: " << renderer.width() << "x" << renderer.height() << "\n";
// Then save it out using the save API.
if (renderer.save("output.png")) {
std::cout << "Saved to file: " << std::filesystem::absolute("output.png") << "\n";
return 0;
} else {
std::cerr << "Failed to save to file: " << std::filesystem::absolute("output.png") << "\n";
return 1;
}

RendererSkia is prototype-level, and has limited documentation and may be subject to change.

The output size is determined by donner::svg::SVGDocument, which can either be detected from the file itself or overridden with SVGDocument APIs:

Using the CSS API

The CSS API is used internally within the SVG library, but can be used standalone.

To parse CSS stylesheets, style strings, or selectors, use the donner::css::CSS wrapper API.

Parsing a stylesheet

Matching selectors

  • To use Selectors, implement the donner::ElementLike concept for your own element type. This allows the CSS library to traverse your document tree.
  • Selectors are available within a parsed stylesheet, or can be parsed from a string using donner::css::CSS::ParseSelector(std::string_view).

    // CSS Selectors can also be parsed directly from a string, for implementing querySelector.
    if (std::optional<Selector> selector = CSS::ParseSelector("g > #path1")) {
    std::cout << "Parsed selector: " << *selector << "\n";
    if (SelectorMatchResult match = selector->matches(path1)) {
    std::cout << "Matched " << path1 << " - " << match.specificity << "\n";
    } else {
    std::cout << "No match\n";
    }
    } else {
    std::cerr << "Failed to parse selector\n";
    std::abort();
    }
  • Use Selector::matches(const ElementLike& target) to see if a selector matches against a specific element. This can be repeatedly applied to all elements of a document to find all matches.

    for (const auto& rule : stylesheet.rules()) {
    bool foundMatch = false;
    std::cout << "Matching " << rule.selector << ":\n";
    for (const auto& element : {group, path1, path2}) {
    if (SelectorMatchResult match = rule.selector.matches(element)) {
    foundMatch = true;
    std::cout << " - Matched " << element << " - " << match.specificity << "\n";
    }
    }
    if (foundMatch) {
    std::cout << "\n";
    } else {
    std::cout << " - No match\n\n";
    }
    }