SVG Render Service
Published: 2023-05-20
To Render or Not to Render. That is the Question!
Our current video rendering engine uses QT5 for rendering SVG files on-the-fly during the final video generation step on the backend. Unfortunately, the library only supports a small subset of SVG functionality and often times gives unexpected results for anything more complex.
It is also, as we learned through painful debugging sessions, notoriously inaccurate in finding the bounding edges of text. This has been such a large problem for KlipMaker, that we ended up building our own interpolation layer on top for adjusting the “guesstimated” text bounding box size by taking into account things like number of uppercase letters, spaces, special characters, etc. Converting to run our own SVG rending service would help fix many of those issues. It would also give us the flexibility required for implementing some new features we have planned for KlipMaker.
Initially the idea was to do this in NodeJS. There are many libraries for rendering SVG via a browser engine (like Chromium) and even more approaches to do it in JavaScript running inside the browser. This seemed like overkill for what is required here as running a full browser engine is going to be heavy and introduce more complexity into our infrastructure stack.
After few more coffee-fueled evenings, we found the other approach for rendering on the backend was to call a third-party tool like ImageMagic or Inkscape via the command-line. ImageMagic is not going to be as heavy as a headless browser, but it is still another dependency to manage. From experience, deploying C++ tools together with NodeJS code is not a trivial exercise.
Golang to the Rescue!
Around the same time, yours truly was looking to learn Go. Having been discouraged with with our research on NodeJS solutions, we took a look at Golang library support. Right away we came across oksvg. It was able to correctly render various SVG shapes but refused to handle text. Close, but no cigar!
// create our sample SVG
in := strings.NewReader(
"<svg width='300' height='150'>" +
"<text x='60' y='60'>Can you see me?</text>" +
"<rect width='220' height='120'"+
"style='fill:lightblue' x='20' y='10'/>" +
"</svg>")
w, h := 300, 150
icon, _ := oksvg.ReadIconStream(in) // parse it
icon.SetTarget(0, 0, float64(w), float64(h))
// create a new image canvas area
canvas := image.NewRGBA(image.Rect(0, 0, w, h))
// resterize parsed svg onto the canvas
gv := rasterx.NewScannerGV(w, h, canvas, canvas.Bounds())
icon.Draw(rasterx.NewDasher(w, h, gv), 1)
// save back to disk
out, err := os.Create("out-sample.png")
if err != nil {
panic(err)
}
defer out.Close()
Notice in the above image that our SVG contained two elements. However, only the rectangle got rendered.
More research revealed ajstarks/svgo, svgparser and GoGi. Same thing: shapes rendered great but text elements are ignored or the library supports rendering text but not parsing SVG. A related keyword seems to be “resterizer”: a tool that converts from lossless format into a lossy format of sorts.
Tackling the problem from the opposite side, Draw2D is able to correctly draw text if we specify it in code and provide the font files. We might be on to something here!
gc := draw2dimg.NewGraphicContext(canvas)
draw2d.SetFontFolder("")
gc.SetFontData( draw2d.FontData{Name: "OpenSans-Regular" } )
gc.SetFontSize(14)
gc.SetFillColor(color.Black)
gc.FillStringAt("Can you see me?", 60, 60)
Render-as-a-Service
Our text rendering requirements are actually very simple at the moment. KlipMaker has its own composition and animation engine. As such, SVG files are usually either basic shapes like rectangles or just text. We decided to write our own parser for the SVG text attribute and combine that with oksvg
library for the shape parsing.
The code was deployed as an AWS lambda service. This can probably be another article at a future date. Current server has a couple of limitations, but should be good enough for KlipMaker to start using it:
- Only top-level
text
elements are parsed. Nested elements or various other elements liketspan
are ignored. - Style options are limited to just the basic name, font-size and color attributes.
- For text to be rendered, we need the
ttf
font file. We got a few initial ones but more are easy to add later as needed. Currently supported:- Montserrat
- OpenSans Condensed
- OpenSans SemiCondensed
- OpenSans
- Roboto
- RobotoMono
- SourceCodePro
Finally result can be seen below:
The link above returns an image that is rendered live from the server on every page request.
Closing Thoughts
Overall we learned that rendering SVG is not a trivial problem to solve. Many solutions exist, but they all have various drawbacks, including our own. However, the service we built for KlipMaker does not have any external dependencies, is lightweight enough to run in milliseconds inside AWS lambda, and simple enough to be easily extended in the future as and when we need to parse more complex text elements.
Throughout this research odyssey, a persistent and growing problem in both NodeJS and Golang is the amount of “dead code”. There are really layers upon layers of various packages and dependencies and tools that worked at some point in the past but do not any more and nobody is around to fix it or in most cases even to point out that you are heading down a dead end. This seems to be becoming a growing problem over the past ~5 years. Not sure what is the best solution here yet or if one is even possible. Perhaps some way to auto-magically run code from various repos and blog posts to validate if it “works”. Maybe a future project for ChatGPT integration?