To wrap up this vector graphics rendering series, we’ll expand the capabilities of our SVG rendering solution to cover some critical cases we omitted from the last entry. Then we’ll write a simple parser for the SVG file format so we can import SVG files into the editor and store them on disk as a custom Unreal asset type. Finally, we’ll make a new UMG widget type that can load and render those assets just like the native Image Widget does for raster graphics.
Housekeeping
First, let’s do a little bit of re-organization and cleanup, since the architecture of our SVG rendering solution is in clearer focus now.
Rename Public/SVG/Elements.h to Public/SVG/SVGElementProperties.h
Create Public/SVG/SVGAttributes.h
Move ECanvasElementFillRule, FCanvasElementStyle, and FSVGViewBox into this file
Fix any include paths to make sure existing references to the identifiers above are valid and recognized by your IDE
Rename FCanvasElementStyle to FSVGStyle
Rename ECanvasElementFillRule to ESVGFillRule
Rename Public/SVG/ElementUClasses.h to Public/SVG/SVGElements.h
Rename USVGElementBase to USVGElement
All the files under Public/SVG should now be prefixed with “SVG”, except for CanvasSVGBuilder.h, which follows our established convention of prefixing our canvas-drawn widget types with “Canvas.”
Before starting the editor again, you’ll also want to add the following core redirects to Config/Default<PluginName>.ini if you’ve saved any widget blueprints that use the Canvas SVG Builder widget (be sure to replace “<PluginName>” with your actual plugin name).
Creating a custom asset type
There’s actually nothing particularly special about an Unreal asset type in and of itself — any UObject will do. What makes it usable as an “asset” are several external types, defined in an Editor-only module, that operate on that type. But we do want a class that can represent an SVG in a way that’s fully decoupled from UMG, so let’s go ahead and sketch that out.
To make it an asset, we’ll bootstrap a new Type: "Editor" module under our plugin, and add a few interface implementations to that.
Asset type actions
The first type we need is a class that implements IAssetTypeActions, but we’ll derive from FAssetTypeActions_Base, so we don’t have to implement everything from scratch.
We’ll have to manually register that type, which we can do in our module startup hook.
Asset factories
The main thing that differentiates an “asset type” from any other UObject is being able to save it to (and load it from) disk. To do that, we need a factory.
We’ll start with a “Create New” factory, which will allow us to create a new SVG asset directly from the editor’s Content Browser — this will obviously not be the primary workflow, but it’s by far the easiest to implement, which makes it a good starting point.
Unlike the IAssetTypeActions implementation, there’s no need to manually register our factory — Unreal will automagically find it and make use of it. So with that, we can start up the editor and try creating a new SVG asset. Right-click somewhere in the Content Browser, expand the User Interface submenu, and select Scalable Vector Graphic. Open the newly created asset, and you should see the Details panel populated with the same fields as our Canvas SVG Builder widget from the last entry.
It would be really nice to see the rendered graphic here as well. Achieving that would involve building a custom asset editor with Slate, via a class inheriting from FAssetEditorToolkit. The details of doing that are unfortunately complex enough that I’m going to call it out-of-scope for this post, but if you use your IDE to look up implementations of FAssetEditorToolkit, you’ll find plenty of examples in the engine source code to get a feel for the workflow. We will cover making it trivial to render our USVGAsset via the SCanvasWidget that we created back in Part 1, so if you want to go down that road, it shouldn’t be too hard to get an MVP up and running after we’re done here.
SVG rendering abstraction
In the previous entry, we wrote all of our code for rendering SVGs directly in our UMG widget class. But what if we want to render an SVG asset in some other context? We now have just such a use case (a custom asset editor), and we’ll soon have another (a UMG widget that renders a static SVG asset instead of one created directly in the UMG Designer). Let’s go ahead and refactor that code.
Lifting OnDraw to a separate interface
Currently, OnDraw is a virtual function declared on our UMG Canvas Widget, which is fine if UMG is the only place we’ll be rendering to such a canvas. But our FAssetEditorToolkit doesn’t derive from UObject, let alone UWidget.
To be honest, this isn’t a huge deal. There’s no law that says we must have an interface for this — we could just bind any function with the correct signature to our Slate canvas widget’s OnDraw delegate and call it a day. But an interface would be a good way to clearly signal that a type is canvas-renderable (without an explicit dependency
on Slate or UMG), which will make our codebase a little easier to understand and navigate as its complexity scales.
While we’re at it, I’m also going to pull the delegate declaration out of SCanvasWidget.h, because having it co-located with the interface declaration gives us a single source (file) of truth for the callback signature if we ever need to change it. I’m also renaming the delegate from FOnDraw to FOnCanvasDraw to reduce the risk of name collisions.
Then we can update our base UCanvasWidget. This class, being abstract, doesn’t even actually need the interface, since it doesn’t render anything on its own. We could leave it off here and have each of our concrete UMG widget types directly implement ICanvasRenderer instead. Unreal’s UClass infrastructure muddies the waters a bit here: Deriving from ICanvasRenderer would be a much more obvious win if we could declare UCanvasWidget’s override as pure-virtual — thus explicitly passing down the responsibility of implementation to derived classes — but UClasses are not permitted to have pure-virtual functions, so that’s not an option. We’ll leave the no-op override in place for now, but it may be worth revisiting this choice at a later date.
SVG renderer implementation
Now we can pull the rendering code out of SVG/CanvasSVGBuilder.h and into a new SVG/SVGRenderer.h file, where we’ll declare our context-agnostic SVG Renderer class. We’re basically converting the static DrawX methods from the UMG widget into instance methods on a new class, which will also subsume the fields of the old FDrawArgs struct.
Retaining the same ownership semantics of the previous setup with regards to the SkCanvas, the new class will hold a reference to the canvas, precluding the possibility of a default constructor. We’ll also explicitly delete the copy and move constructors to force its lifetime to be scoped to a single function invocation.
Refactoring the DrawX implementations is a bit tedious but not especially complicated, so I’ll just show one example here and let your imagination fill in the rest. But do hold off on implementing the main Draw dispatcher for now — we’ll be reworking that function in the next section.
The SVG Builder widget now becomes downright spartan compared to its previous form.
With the rendering logic suitably abstracted, we can now trivially extend rendering functionality to our SVG asset:
Improving our SVG implementation
If we want to be able to import even relatively simple graphics from sources like Figma or Adobe Illustrator, we’re going to need to make some core changes to our implementation of the SVG specification to cover some cases that we’re not currently handling correctly.
First, our FSVGStyle struct is missing a critical property, and it’s using the wrong defaults for the ones we have implemented. Secondly, we don’t have any representation for the <g> element
, nor are we implementing the style-inheritance behavior which is its primary use case.
First, the defaults: Somewhat counter-intuitively, the fill attribute defaults to black, while stroke defaults to none, and stroke-width defaults to 1.
For the Group element, we’ll add a new class deriving from USVGElement. We don’t need a separate struct for its properties, because it doesn’t have any besides the shared ones. Instead, the class will get a Children field just like the SVG asset itself has.
Wait, what happened to the tagged unions?
This is probably as good a time as any to mention: Remember all the fuss about tagged unions and data locality from the last entry, where I said we would investigate using an array of tagged unions instead of polymorphic class pointers in our custom asset implementation? And how that was basically the whole justification for separating out the “Properties” structs from the “Element” classes?
Yeah, that ain’t gonna happen. The issue is that the same reflection system that’s responsible for Unreal Editor integrations (and which throws up its hands when it encounters a templated type) is also responsible for serialization, which is how we’re able to save and load a USVGAsset to disk at all. Epic have hard-coded the handling of its common containers like TArray and TMap into Unreal Header Tool’s parser, but handling templates as a general case is not a task they’re interested in undertaking.
It’s possible that we could override some of the inherited UObject behavior to manually (de)serialize those fields instead of relying on UPropertys, and maybe even manually handle editor integration with a Details Customization — but honestly? It’s probably not worth the trouble. Yes, reading from the L1 cache is much faster than reading from system memory, but we’re talking about operations that are measured in nanoseconds either way. We would need an extraordinarily complex SVG to register any real difference between the two, and at that point, our use of Skia’s CPU backend is going to be a much more constraining bottleneck.
However, I do still like the separation between our properties structs and element UClasses. The structs are plain-old data that don’t suffer from the banana / gorilla / jungle problem
of inheriting from a gargantuan base class, which makes them much more agile to work with. You can’t even write a custom constructor for a UClass, because only some black magic ritual buried deep within The Engine is permitted to new them up. So despite the unforeseen change of plans, we’re going to forge ahead with the existing architecture instead of merging all of our structs’ fields directly into their parent USVGElements.
Modeling overridable inherited styles
To make nested groups of SVG elements actually useful, we need to make it possible for child elements to inherit styling properties from their parents. Take the example from MDN:
The code above should render both circles with a green 5px stroke and white fill. However, child elements can also override their inherited style properties:
In that example, the circle in front should be rendered with a magenta outline instead of green. Your first instinct for handling overrides might be to just check whether the child’s style property is defaulted, but that won’t always work. For example:
Now, the circle in front should be filled black, while the one behind it should be filled white. But black is the default fill color, so if we allow the parent style to take precedence wherever the corresponding child style is equal to its default value, we’ll end up doing the wrong thing.
This type of situation is a common challenge when trying to implement a high-level declarative language like HTML or CSS in a lower-level host language like C++, and there’s not necessarily a single “correct” answer. We’re currently representing a set of style properties as a statically-typed struct appended to each element, but the full CSS spec has roughly a zillion valid properties, which would make that approach unwieldy if we needed to implement all of them. Browsers handle this sort of thing with a full-on virtual machine, interpreting the code on-the-fly as the runtime traverses along its execution path. But keep in mind that our goal here isn’t to exhaustively implement the entire spec — we just want to implement enough of it to provide substantial utility, while keeping our runtime overhead minimal and the implementation itself manageable.
What we’d really like for this particular problem is something like optional values for each of our style properties. That would map much more neatly to SVG’s textual representation, where there either is or isn’t a given attribute appended to a given element tag. But just like with the union situation, we can’t use TOptional for a UProperty because it’s a template. (We also wouldn’t necessarily want to have to consult some separate object every time we need to fall back to the default value for a “null” property.)
What I settled on is a set of flags to indicate the properties that were explicitly set for any given style object. The attributes we need to check for are fill, fill-opacity, fill-rule, stroke, stroke-opacity, stroke-width, and finally opacity (which we’re also adding as a new property). This is a little bit ugly, but if we use bitfields instead of full booleans (and reorder the FillRule property for better alignment), we end up only increasing the size of our struct by 4 bytes (on MSVC). That’s a pretty sweet bargain for 8 additional fields! Here’s the revised layout:
I normally dislike the Unreal convention of prefixing booleans with a b, but in this case, since we can’t type the bitfields as bool (another UHT limitation), it definitely helps to clarify the intent. We can also make some adjustments to the UProperty specifiers to keep our Details panels from devolving into an unreadable mess:
Doing that for each corresponding pair of properties makes the Details panel look like this:
Implementing style inheritance
We should now add style fields to our SVG asset and SVG Builder widget, since the root <svg> element can have its own style attributes that should be inherited by its children.
And we’ll add a m_RootStyle field to our SVG renderer, with a matching constructor parameter.
Now, when we call one of our renderer’s DrawX(props, style) functions, we’ll pass it the result of merging the element’s style overrides into the root style object. Let’s go ahead and add a utility function to our style struct to compute that merged result.
In the renderer, I’m going to split the switch statement responsible for dispatching our individual draw calls into a separate function from the logic that handles iteration, setup and cleanup. This is arguably an excessive amount of indirection, but I really prefer small, narrowly-defined functions for maintainability. As-is, for example, it’s easy to completely miss the single line of cleanup that follows the ~40 lines of switch cases unless you’re really looking for it. Since we’re adding a new case for the Group element and making the style declaration less trivial, this feels like a good opportunity to go ahead and slice things up.
To add support for our Group element, we’ll just do a little bit of recursion. We’re basically just treating the Group element as if it were its own SVG, but with an inherited viewBox.
One more thing: you may have noticed that I sneakily replaced the DrawPolyLine and DrawPolygon function calls above with a single DrawPoly call that takes a third argument. If you take a look at the previous DrawPolyLine and DrawPolygon implementations, and note that FSVGPolyLine and FSVGPolygon are literally the same structs with different names, I’m sure you can connect the dots.
Implementing opacity
Lest we forget — we added a new Opacity field to our style object earlier without a lot of fanfare, but we’re not currently doing anything with it. We’ll handle it in our ConfigureFill and ConfigureStroke methods:
To be honest, I’m not 100% sure if what I’m doing there is correct in terms of gamma and color space. The other option would be to do something like this (which would actually be more efficient since we’re not converting to linear space and back):
The result of the solution as-written looks fine to my eyes, but I haven’t done any rigorous testing and I’m not an expert on color-space conversions, so let me know if I got it wrong.
Importing and parsing SVG files
With all of that done, we’re finally ready to circle back to our Editor module and create an import factory for reading SVG files and parsing them to our asset type.
Creating the import factory
While trying to figure out how to do this, I came across several examples in the engine of single UFactory classes that implement both the “create new” and file importing functionality, but I had some trouble getting the engine to invoke the correct function override when I tried that approach, so I’m going to make a separate class for the import factory.
We have two override options for importing from a file: FactoryCreateFile, and FactoryCreateText. In theory, the latter is simpler to implement — all we have to do is set the bText flag, and the base factory implementation of FactoryCreateFile will read that file and then pass us the text content for processing. Unfortunately, that’s not going to be ideal for our use case (which I’ll explain shortly), so we’ll implement FactoryCreateFile instead.
Just like the “create new” factory, we’ll configure our instance in the constructor:
The Formats field indicates which file formats the factory supports importing. Frankly, I have no idea where, how or why the text after the semicolon is actually used, but all the cool kids (read: all the examples I found in the engine) were doing it, so I filled it in with a reasonable value. The part before the semicolon is the file extension.
Implementing the SVG parser
Before we implement FactoryCreateFile, we need to sketch out the bones of our parser. We’ll make use of a class I found in the API reference, FFastXml, which only has one consumer in the actual engine code, so some caution is probably warranted here. I have no idea how widely used Electra Media Player is (and by extension the XML parser), so it’s possible it hasn’t seen a ton of exhaustive testing. That said, parsing XML isn’t exactly rocket science, and the class is shockingly well-documented (at least by Unreal standards) which inspires confidence.
FFastXml’s entry-point can take either a full path to an XML file, or the full contents of an XML file as a mutable TCHAR* buffer. It’s the mutability requirement that complicates the FactoryCreateText option for our factory, because that method takes the file contents as a const TCHAR*. We could const_cast it and hope for the best, or copy it into a new buffer to pass it safely, but just passing the file path to FFastXml and letting it take over from there is the simpler (and likely faster) option.
Our parser class implements IFastXmlCallback, which is how FFastXml interfaces with our custom parsing behavior, and FGCObject to prevent our temporary USVGAsset instance from being garbage-collected as long as the parser instance is alive. For our own entry-point, we’ll expose a static Parse method, which will instantiate our parser instance and pass it to FFastXml.
With that, we have enough information to fill out the FactoryCreateFile implementation.
The USVGAsset object returned by our parser will be created as a temporary object (with no object flags and GetTransientPackage() as its outer), so returning it directly will not result in a new asset file being created. (Don’t ask me how long it took to figure that one out. There may have been some weeping and/or gnashing of teeth involved.) Instead, our import factory will need to call NewObject with the correct outer argument, name, and flags, and pass the parser’s return value as its “template.” This will write the new object to disk and populate it with all of the same UProperty values as the object returned from the parser.
I have no idea what parms is and the API reference is no help. Is that an unusual abbreviation for “parameters”? Is it some really common thing that I’ve never heard of because I’m just a naive web developer operating way out of my depth? Every other FactoryCreateFile implementation I looked at (including the base implementation) calls ParseParms on that argument when the main operation succeeds, so we’ll do it too. (ParseParms, for what it’s worth, is annotated with a doc comment that reads “Import an object from a file,” which raises more questions than it answers. In any event, this implementation works, so we’ll just accept the mystery
and move on.)
In our parser’s entry-point, we’ll create the new USVGAsset object for the return value. We’ll instantiate the parser instance and pass it to FFastXml’s entry-point. Depending on the result, we’ll either return the new asset, or log an error and return nullptr.
Mapping out the architecture
At this point, let’s take a step back and think about how our parser is going to work. FFastXml will read in the file and invoke our ProcessX implementations as the corresponding symbols are encountered. For example, given this SVG:
Our parser will receive the following method invocations:
At each ProcessAttribute event, we’ll need to know which element we’re currently operating on, which we can only do by keeping track of that state in our class. Similarly, we’ll need to know whose Children array to append the new element to when we receive a ProcessElement event.
We can’t just track the current element in a simple USVGElement* field, because then we’d lose track of which element should become active when a child element, like the first circle above, is closed. One totally valid option would be to add a pointer back to the parent to our USVGElement class, like this:
But I don’t love needing to further bloat our class layout just for the sake of the parser, since we don’t have any other use case for that field currently. It would also always be nullptr after the initial parse, unless we somehow serialized it.
Instead, we’ll use a Stack<USVGElement*> to track our current location in the tree. In the example event sequence above, the indentation level represents the pushes and pops we’ll need, and at any given event, the target for a new element or attribute will be at the top of the stack.
Well… mostly. USVGAsset doesn’t derive from USVGElement, so we can’t put it on the stack. That is, perhaps, an oversight that we could consider correcting. But USVGAsset has some special properties that makes it quite different from the rest of the USVGElement family — most importantly, it can’t be an SVG child element — so I’d rather not do that. Since the root of the tree must be a USVGAsset, and a USVGAsset can only be the root of the tree, we’ll know that we’re operating on the root asset if the stack is empty.
In addition to the stack, it’ll also be handy to have a separate field to track the type of the element we’re currently operating on, so we don’t have to constantly dereference the top of the stack just to access its Type field.
(Small side note: yes, that is a C++ standard library container, not an Unreal container. Unreal’s Containers module doesn’t have a purpose-built stack type, but after writing this implementation, I later discovered that the common recommendation is to just use a TArray since it has Push, Pop, and Last methods with the expected performance characteristics. However, since I’ve already written the implementation with std::stack and this is an editor-only module anyway, I’m going to leave it as-written.)
For ProcessAttribute, we’ll want some sort of dynamic dispatch behavior, similar to the DrawX methods in our renderer, so we don’t need to clutter a single method body with a massive inline switch statement.
Once we’ve dispatched to the correct attribute handler, we need a way to identify the specific struct field to write to (and how to interpret its value string), but comparing strings is slow (and inconvenient, since we’re dealing with raw C strings). Unreal’s FName class is intended as a solution to that problem. I won’t get into the weeds of how it’s implemented (mainly because I don’t actually know all the details), but suffice it to say that both the storage and comparison of FNames is highly optimized compared to other string representations. For that reason, we’ll operate on FNames instead of const TCHAR*s whenever it makes sense to do so.
In addition to attributes for specific elements, there are also style attributes that can exist on any element. We’ll add a method for handling them, and a helper function for identifying them.
When we encounter an attribute we don’t recognize, it’ll be a good idea to make some noise about it in the Output Log instead of just quietly ignoring it, so we’ll add a method to handle that too.
Finally, it’ll be convenient to have a lookup table of element names to their corresponding enum types for the ProcessElement method.
Processing elements
Now we can start implementing these methods, beginning with ProcessElement. We’ll start by creating an FName from the input string, and bailing early with a warning log if it’s unrecognized.
If it’s the root <svg/>, we can just set our type state and bail, since we already have our root asset object and we can’t add it to the stack.
Otherwise, we’ll prepare to append the new element to the tree by identifying its parent.
Here’s that ToString helper function for the error log:
Finally we can instantiate the appropriate element type, append it to its parent, and push it onto the stack.
ProcessAttribute will be a very straightforward dispatcher.
In ProcessClose, we just need to pop the current element from the top of the stack and update our current-type state accordingly.
Now we can start processing attributes. First though, I’m going to define all the attribute names we currently support, both to make the FName comparisons a bit faster and to reduce the likelihood of headaches from unnoticed typos.
Style attributes are the most interesting case, so we’ll look at that first:
Parsing CSS colors
The supported syntax for CSS color values (and by extension, SVG color values) is pretty expansive
. Realistically, we’re expecting our imported assets to be created in some software tool like Figma or Illustrator, so we don’t really need to support the whole range. I would wager that virtually all of these tools stick to two or three representations — likely hex, keywords, and maybe RGB(A) with integral 0-255 values — so those are the three representations that we’ll aim for.
We’ll create another static helper method for the task, and then flesh it out piece by piece.
RGB(A) syntax
Starting with the most interesting case, we’ll use Unreal’s Regex module to parse RGB(A) values. Unfortunately, regular expressions have accumulated a bit of a bad rap over the years, perhaps most famously immortalized by this (admittedly funny) quote from Jamie Zawinski:
Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems.
So I think this warrants a bit of justification, though I won’t spill too much ink over it because it’s a big topic that would be better covered elsewhere.
By my estimation, the most common misgivings about regex stem from one or more of:
Concerns about their performance (when misused)
Concerns about their readability (in general)
Headaches encountered by less experienced developers when trying to shoehorn them into problems for which they’re not a good fit
(And to qualify that third point, by “less experienced” I’m referring pretty specifically to experience with parsers and the formal language theory behind them, not general experience with software engineering more broadly.)
The first and third points above objectively do not apply here. In fact, a well-designed (and well-implemented
) regular expression would represent not just an adequate solution, but an optimal solution to this particular problem. That’s because the formal grammar
that specifies the RGB(A) syntax is a regular grammar, meaning that, taken in isolation, it describes a regular language, which is exactly what regular expressions were designed for. A regular expression is just a concise syntax for constructing a deterministic finite-state machine
designed specifically to match that particular pattern for any given string in O(n) time and O(1) space.
The second point, about readability, is a fair one, but there are some small steps we can take to ease the burden of writing (and reading) the pattern we’ll need here.
Let’s start by inlining the formal grammar from MDN as a comment in our function body, with some modifications to cover the specific forms we intend to support.
Formal grammar specifications typically ignore whitespace, but our regex pattern will need to account for them, so I explicitly included them in our grammar definition above. For the meta-syntax, I’ve used backticks to delimit literal characters, angle braces for the named rules, square brackets for grouping, the standard regex quantifiers, and ellipses to denote a range.
Examples of the variations that grammar describes are:
The first four examples are specified by the (quite new) CSS Color Module level 4, which we include mainly for future proofing, while the last two have been in the spec since level 2 (ca. 2003) and are much more likely to actually be encountered. (All versions of the spec since level 2 also support percentages for the R, G and B values, e.g. rgb(0%, 50%, 100%), but I have personally never witnessed that form in actual use, so I very much doubt we need to worry about it.)
Unreal’s FRegexPattern constructor takes a const FString& as its first argument, so we can actually build the string from named variables instead of needing to supply one long (and confounding) string-literal, which should help with readability.
We can also simplify some of our rules quite a bit if we assume that we’ll only be receiving well-formed inputs. For example, we can use [.0-9]+ to match the the optional alpha value, because we can be reasonably confident that a venerable tool like Figma or Illustrator is not going to try to hand us a number with multiple decimal points or no digits.
Another reason regex is a great fit for this use case is that we can specify capture groups, which are parenthesis-delimited sub-expressions in our expression that can be separately extracted from the match result. In most implementations, captured sub-matches are accessed by an index starting with 1 (because the index 0 is reserved for the full match). We’ll use those to grab the R, G, B and A values out of the match.
Putting it all together (and pretending we can use a whitespace-insignificant syntax), our final expression will look like this:
To briefly recap the syntax there, in case you’re not familiar:
[] delimits a set of characters, with - as the range operator, so [0-9] is equivalent to the interval [0,9].
() delimits a capture group, as explained just above.
(?: and ) delimit a non-capturing group — a way to group a sub-expression without creating an indexed capture for the match. (We’re using it here so we can mark the entire sub-expression containing the alpha value as optional.)
?, * and + are “quantifiers” that modify the preceding sub-expression.
? means 0 or 1 (i.e., optional).
* means 0 or more (optional, potentially repeating).
+ means 1 or more (non-optional, potentially repeating).
\s is a “character class” (basically a pre-defined keyword) which is shorthand for the set of all whitespace characters (equivalent to [ \t\r\n]).
\( and \) are simply escapes, so we can match the literal ’(’ and ’)’ characters instead of creating a match group.
Let’s go ahead and translate that to C++:
Hmm. So admittedly, that’s still kind of a mess. The combined form (as a raw string literal) would be this, if you find it more palatable:
We can match it against the value we’ve received, and extract the capture groups, using an FRegexMatcher:
Hex syntax
Hex colors are much easier to match, since FColor has built-in support for them. We’ll put this code above the RGB(A) logic in our function, to avoid initializing the regex objects if we don’t need them:
Color keywords
Finally, there are the pre-defined CSS color keywords
. This will also be pretty trivial, since we can just look up the value in a hashmap. But hoo boy, that’s a long list of keywords.
My About page states that everything you read here is written by me personally, without AI assistance — except where otherwise noted. Well, it is time to take note, because the one area where I’ve consistently found tools like ChatGPT really, really useful is for writing staggering quantities of boilerplate code.
Don’t get me wrong, I’m totally fine with mindlessly thoking away on my keyboard for long stretches of time when it’s called for. Put on some good music and it can even be pretty therapeutic. But sometimes, the task before me is so tedious that it begs for a different approach. We could sit down and write a script with some fancy regex manipulation to ease the burden — and I’ve definitely gone down that road before — but today, it is the Year of Our Lord two-thousand and twenty-three, and we have robots that can do it for us.
I’m trying to initialize a TMap of const TCHAR* keys to FColor values, where the keys are all of the named color keywords supported by the current CSS specification.
FColor’s main constructor has this signature: FColor(uint8_t r, uint8_t g, uint8_t b), and TMap has a constructor accepting nested braced initializer lists.
Here are a few color definitions as an example:
…
Can you help me fill out the rest of the entries, so that it includes all of the CSS named color keywords with the correct values?
At first, of course, it tried to take the easy way out, spitting out a similarly incomplete list, with a comment just before the closing brace: // Add more color entries here...
Don’t try to teach me how to fish, robot. I know how to fish. I just want you to feed me so that I don’t have to.
Fortunately, it didn’t take much additional prodding:
Thanks, that’s very helpful! But can you fill out the entire list for me, instead of truncating it?
The response:
Certainly! I apologize for the truncation. Here’s an expanded version of the code with the complete list of CSS named color keywords:
And here, for your copy-and-pasting pleasure, is the glorious result that eventually trickled out (modified, of course, to use FName for the key type instead of const TCHAR* — whoops):
What a time to be alive!
Honestly, I don’t know why this sort of thing tickles me so much. At a time when there’s a lot of breathless sensationalism about how smart these language models are, I suppose it’s the irony of asking them to squander considerable compute resources on the sort of simple drudgery that could have been accomplished by a 35-year-old regular expression engine (or literally anyone with access to a web browser, some sort of typing instrument, and plenty of time on their hands) that brings me so much joy. I should think about doing a more thorough writeup on these types of AI tools eventually, because I have some thoughts.
But for now — on with our regularly scheduled programming.
Add the keyword lookup to the top of our function and a warning log at the end in case of failure, and ParseColor is a wrap! All together, here’s what we end up with:
Processing attributes
We can finally get back to our ProcessStyleAttr implementation, and it’s pretty straightforward from here. We just need to remember to set the bExplicitX flag for each attribute we process.
For ProcessRootAttr, we’ll do something very similar for the viewBox attribute to what we did for CSS colors, so we can extract the individual values from the string. This time though, the grammar is dirt simple: four space-separated numbers.
For PolyLine and Polygon, we’ll forward to a shared ProcessPolyAttr method. The points attribute for those elements takes the form of a sequence of space-separated coordinates, where the x and y values can be either comma-separated or space-separated. We’ll create capture groups to match a single x,y pair, and iterate over each match in the string by calling FRegexMatcher::FindNext until it returns false.
The remaining attributes are incredibly mundane and predictable, so I’ll refer you to one final example and leave you to fill in the rest:
Checking our work
This has been a long one, but we’re finally at the end of the road. All that’s left to do is create a new UMG widget to render an SVG asset, drag an SVG file into the Content Browser, and give it a test drive.
And here’s an imported SVG file rendering via the Canvas SVG widget in a new widget blueprint:
What’s next?
There’s plenty of room for improvement on this barebones implementation: rendering with Unreal’s RHI for big performance gains, huge portions of the SVG spec left untouched, a proper asset editor with custom thumbnails, etc. I don’t have any concrete plans at this point, and it might be a good time to pivot to one of my other goals for overhauling the UI workflow in Unreal. Time will tell. But until then, I hope you enjoyed coming along on this journey with me, and thanks for reading!