Last year, I did a whole multi-part series on how to write an SVG Rendering engine from the ground up: https://williamaadams.wordpress.com/2023/02/18/svg-from-the-ground-up-time-to-wrap-it-up/

Since the time of that writing, I’ve used the SVG engine quite a lot to create a mapping application, as well as be the foundation for creating some UI elements for general application usage. This past summer, a developer in the blend2d community asked if they could use the library as part of the work with the Tcl language.

The Tcl language itself has a multi-decades history, and you can look at some the examples of what this blend2d binding brings to the language here: https://wiki.tcl-lang.org/page/Blend2d+Gallery

What has happened in that intervening year? Well, for starters, I had made enough changes since first publishing the series last year, that I decided to create a whole new project that isolates the SVG renderer from the application it was being developed for. I created the SVGAndMe github project to ensure its isolation, and ability to be compiled cross platform. Same old code, much modified, and with a new mascot.

There are so many changes and additions to this code. First and foremost was probably text support. The original code did not support text, which was ok, when you’re just doing illustrations, but having text makes a lot of UI elements a lot easier to create. Then there was partial support for some CSS constructs. Supporting Cascading Style Sheets (CSS) would be a whole other project in and of itself, but a decent amount can go a long way.

<style type="text/css">

  .fil18 {fill:none}

  .fil17 {fill:#090909}

  .fil11 {fill:#161616}

  .fil1 {fill:#1A1A1A}

  .fil16 {fill:#1C1C1C}

  .fil10 {fill:#1F1F1F}

  .fil2 {fill:#252525}

  .fil13 {fill:#2B2B2B}

  .fil3 {fill:#2E2828}

  .fil12 {fill:#383737}

  .fil7 {fill:#A8A8A8}

  .fil6 {fill:#B3B3B3}

  .fil20 {fill:#BAB4B4}

  .fil15 {fill:silver}

  .fil9 {fill:#C5C5C5}

  .fil19 {fill:#C9C3C3}

  .fil14 {fill:#D6D3D3}

  .fil8 {fill:#DBDBDB}

  .fil0 {fill:white}

  .fil5 {fill:silver;fill-rule:nonzero}

  .fil4 {fill:#D6D3D3;fill-rule:nonzero}

</style>

While I was focused on generally making the thing more robust and useable, I maintained a fairly straightforward API to create documents from scratch. The intention is that you can go from filename to Bitmap image within a single function call. At the same time, you want the flexibility to say “I want to zoom into this particular section of the image, or rotate by this amount”. So, it’s a two step process. Constructing a ‘document’, which parses all the SVG, looks like this:

1

gDoc = SVGDocument::createFromChunk(mappedSpan, &gFontHandler, WIDTH, HEIGHT, DPI);

Basically, get your text into memory, hand it a font handler, and away you go. Then, creating an actual bitmap image looks like this:

// Create a drawing context to render into

SvgDrawingContext ctx(&gFontHandler);

BLImage img(surfaceFrame.w, surfaceFrame.h, BL_FORMAT_PRGB32);

 

ctx.begin(img);

// Render the document into the context

gDoc->draw(&ctx, gDoc.get());

ctx.end();

You can then use the bitmap in your application, or save it to disk, or whatever it is you’re doing.

But, rather than making this a coding tutorial (that could take a whole new series), I want to reflect upon the journey over the past year with this piece of code.

It starts with the choice of the blend2d library for doing vector based 2D rendering. It’s been about 5 years since I first stumbled across this library. Although I’ve been writing code for about 40+ years now, and have written graphics libraries myself along the way, sometimes you come across something that’s truly exceptional, and the build vs buy decision is obvious. In this case, there’s no way I’d be able to create a library as compact, and efficient as blend2d. The code is so fast, it outshines pretty much any other 2D vector graphics library out there, including any based on GPU assisted rendering. You can see this in the performance analysis.

This is fairly esoteric stuff. Noone gets excited about the quality and speed of their 2D vector graphics library do they? Things in tech move so fast these days. As developers, we hardly have time to take a breath, and think about what we’re doing, how we’re doing it, what’s the best way to do it, what’s the long term impact of our decisions. Since I’m not currently on anyone else’s clock, I find it refreshing to be able to take the time and be more thoughtful about perfecting my craft. I have the luxury of spending more time selecting the best tools, constructing my best architectures, and iterating numerous times to come up with solutions that are as compact and efficient as possible. That led me to using blend2d in the first place. Then, putting an SVG parser on top of it was just natural. The blend2d library itself has a design center which is “support the core of SVG capabilities”. Which is great because things like gradients, paths, and the like are super supported.

While creating the parser, I have made several other design decisions, related to either completeness, or performance. One simple example is, using lookup tables for various things. A simple example is a color lookup table. SVG has a well known list of named colors, so I had to think about, what’s the best way to store that, and use it efficiently.

At first glance, you might just use a std::string as a key, and whatever represents color as a ‘value’, something like this:

    static std::map<std::string, BLRgba32> svgcolors =

    {

        {("white"),  BLRgba32(255, 255, 255)},

        {("ivory"), BLRgba32(255, 255, 240)},

        {("lightyellow"), BLRgba32(255, 255, 224)},

        {("mintcream"), BLRgba32(245, 255, 250)},

        {("azure"), BLRgba32(240, 255, 255)},

  };

 

auto aColor = svgcolors["ivory"];

That sort of thing. But, it falls short in a few ways. First of all, using std::string pulls in usage of the ‘std’ library, which I’d like to avoid as much as possible. And yes, I’m obviously using the ‘std::map’ collection, which is also in the library, but limiting exposure is good. Second, I recognize that in most cases where I’m looking up a color, it’s because I’ve gotten it from some part of a .svg document, so the representation in memory is actually a “ByteSpan” object, which has minimal memory allocation. So, I’d like to be able to perform the color lookup using this ByteSpan object instead of having to convert it to a std::string before the lookup is performed. Also, using a std::unordered_map is sufficient. Here we go with an alternative implementation

    static std::unordered_map<ByteSpan, BLRgba32, ByteSpanHash> svgcolors =

    {

        {("white"),  BLRgba32(255, 255, 255)},

        {("ivory"), BLRgba32(255, 255, 240)},

        {("lightyellow"), BLRgba32(255, 255, 224)},

        {("mintcream"), BLRgba32(245, 255, 250)},

        {("azure"), BLRgba32(240, 255, 255)},

    };

 

auto aColor = svgcolors["ivory"];

The usage is still the same, and construction of a temporary ByteSpan for the lookup key is much less intensive than creating a std::string, so that’s ok. In addition, I’m using an unordered_map, and passing in my own hashing function for the keys. What is that for? Well, when you’re storing the keys, the hashing function creates a relatively unique numeric value to be an index value into the underlying map (I’m really shortening the explanation here). The bottom line is, this table implementation, and various lookups into it, are going to be more performant than the previous one based on std::string. It’s an optimization that as rushed developers, we typically don’t have time for.

There are other micro-optimizations that I’ve gone through along the way. One that has had a major impact is simply how to turn numeric strings (-23.1234) into actual numeric values in memory. Again, standard programming allows us time to contemplate just using “strtod()”, or heaven forbid “sscanf()”. These are super slow, but if you’ve never measured them, you won’t think about it, and just move on.

There is code readily available on github to do super fast double parsing: https://github.com/lemire/fast_double_parser and some big projects in the industry have incorporated this into their code bases. I didn’t quite go that route, and invented my own, but borrowed a little bit of learning from there. Why go my own way? Because I wanted to see how good I could do it, and I did not want to pick up another dependency. Turns out, you can get “good enough”, and do way better than the standard libraries, and remain compact and easily maintainable.

To date, I have managed to parse some pretty large SVG files (70Mb), which are heavy with numerical conversions, and they parse and display within the ‘blink of an eye’, so I think it’s now good enough.

While I continue to invest in parser improvements, SVGAndMe is not a product in and of itself, It is a tool. As I get more into applications such as mixed reality, I find the need for 2D vector graphics does not diminish, but being able to do it quickly and easily increases. Whether it be creating UI elements, or heads up displays, or textures to be mapped onto 3D objects, vector graphics is as important as ever, and having a small efficient library makes me more likely to reach this ability.

SVGAndMe is a typical project, that presents various tradeoffs and choices to suit my needs. It evolves, and grows, but I always have in the back of my mind ‘what can I remove’? ‘what can I make smaller and faster’? It is this ethos that I see lacking in the modern software that I see all around me.

Sometimes ‘buying’ is a better choice, and sometimes it’s ‘build’. In the case of vector based graphics, I have found a happy medium where I’m doing both, and my applications are benefitting tremendously.

I will leave this article with a bit of a eye sore, just for fun.

Previous
Previous

Mastering SVG: The Paint-Order Attribute Explained

Next
Next

Knowledge Surfing