SVG is an awesome image format thats widely used, works in all browsers. SVG graphics make better apps and better games – and automatically “upgrade” themselves for future devices.
This post explains the underlying code architecture of SVGKit – the open-source SVG implementation for iOS/OS X; the target audience is developers who want to help improve SVGKit (adding missing features, fixing bugs, or making it more compliant with the SVG Specification)
Goals
Primary goals of the SVGKit project:
- 100% compliance with the SVG Specification
- Seamless integration with iOS (iPad/iPhone) and OS X
- Performance better than PNG/JPG/bitmap graphics
- …a library good enough that Apple would have liked to have included it in iOS
NB: the license terms for SVGKit are, without prejudice: “you can do anything you want with this, so long as you give credit to the SVGKit authors for their work”. Many of us are using it in commercial projects.
Core structure
The SVG Specification forces us to split the library into two parts, from the very start:
- SVG Spec – 100% defined by the W3 Consortium
- Native rendering – approx 10% defined by the W3 Consortium
The SVG Spec does have *some* requirements on the native rendering, and it has a lot of “guidelines” – but on the whole, it’s undefined, so that we can provide an implementation that makes sense on our platform (iOS/OS X).
I’ve divided this up into independent sections:
- SVG Spec
- Locating an input stream (e.g. a file, or an HTTP URL)
- XML parsing (low-level)
- DOM parsing from XML
- SVG parsing from DOM
- Native rendering – approx 10% defined by the W3 Consortium
- Conversion from SVG + DOM to SVG data (including: cascading, as per CSS (required by SVG Spec!))
- Dynamic changes to render data, to support Vector Graphics (Apple’s runtime support for vectors is – ironically – weak)
- Export to disk, using the latest copy of your modified DOM
- Export from SVG data to OpenGL (via raw bytes), to Apple’s (CAlayer/UIView), and to arbitrary CGContextRef instances
“Apple’s support for vector graphics is weak”
This was the biggest surprise to me: Apple has spent a decade marketing their OS (Mac / OS X) as “vector based”, etc.
In practice … OS X libraries were usually sparsely documented by Apple, and until iPhone came along, they were messy, buggy, poorly designed, and full of “out of date” methods. With iPhone OS (now renamed “iOS”), Apple cleaned their house out, and made some very lean, clear, logical APIs (with many fewer bugs!). They also – finally – documented it all.
That’s an amazing achievement, it’s very impressive. But along the way (probably to save time) they ignored some parts. The original iPhone’s CPU and GPU were very weak (compared to today), so it’s no surprise that Apple didn’t update their vector graphics libraries. iOS (as of 2013) is still using the under-documented and flawed OS X classes.
(NB: the lack of documentation also means that very few people know how to use Core Animation/Quartz/CALayer for high performance – you have to “experiment” and deduce what Apple *might* be doing, and test extensively. [Incidentally, there’s a lot of misinformation around – rumour and theory, in the absence of official docs from Apple])
Find the link for CALayer and bookmark it. This core class is where Apple’s vector libraries and main rendering intersect. It’s powerful – but it’s ugly and bloated too.
Simple bits, see elsewhere
“Locating an input stream (e.g. a file, or an HTTP URL)”
c.f. the SVGKit Usage post. This stuff is very simple, but it’s lacking features. Would be great for you to add some new SVGKSource subclases, with better features.
“XML parsing (low-level)”
Currently uses libxml (because that is built-in to iOS, OS X, and Xcode).
This wraps libxml, and adds three features:
- Captures every parse-error, and provides a list + line numbers when parsing is finished (libxml doesn’t have this feature by default)
- Converts low-level libxml C library to high-level ObjectiveC calls
- Provides a “modular” parsing system, where parsing code is very simple to write
On the whole, we have NO INTENTION of changing the parser – it works, and its intended to be as simple as possible. It’s really just an upgrade to libxml.
But there’s one thing it’s missing that we’d love to add:
- Streaming / interrupt-based parsing
This is potentially more efficient in CPU and memory usage (not much, since we HAVE to use DOM – it’s required by the SVG Spec), but requires making the SVGKParser.m class a bit cleverer.
“Export from SVG data to OpenGL/NSData/CALayer/CGContextRef/etc”
Check out the “Exporters” sub-folder. It contains simple example classes – one per exporter – showing how to efficiently use SVGKImage to help you export stuff.
Note that approximately half of all SVG files have NO SIZE! – they are “infinite” – and you want to re-use SVGKImage’s code for calculating “correct”, or “best guess” sizes.
Since UIView uses CALayer’s internally, you can take any CALayer and add it to a UIView ([UIView.layer addSublayer:(CALayer*)myCALayer]).
Complex parts
“DOM parsing from XML”
The way this works is very rigidly defined by the SVG Spec, and you absolutely must stick to the Spec.
DOM is a major web standard, and the SVG authors thought it would save everyone a lot of time to re-use it.
Unfortunately – tragically! – iOS has no DOM implementation:
- Apple has a private implementation available in Safari. It’s not entirely private (we had to rename one of our classes because of a careless name from Apple), but Apples policy is “if it’s not explicitly public, we can reject your app for using it”. In theory, we could get access to this via WebKit source, or via an embedded WebView. But it would probably be much slower, and use a lot more memory, than our current native implementation
- There are a couple of open-source implementations, most of which have sadly been abandoned by their authors. Also, most of those I looked at are incomplete, and non-compliant; we can’t afford to rely on them.
The process for adding / modifying DOM classes goes like this:
- Copy/paste the DOM official class name (including the capitalization)
- In the header, paste the HTTP link to the *paragraph* of the DOM specification that defines that DOM class
- …then copy/paste the DOM’s interface/class declaration (usually 5-10 lines of code beginning “interface”, and blockquoted)
- Copy/paste that a second time, this time as the ObjectiveC Interface
- Convert every “variable” to an ObjectiveC @property
- Note: by definition, you are supposed to replace DOMString with NSString*
- Convert every “method” to an ObjectiveC “-(something) methodSomething:(something);” method – NB: do *not* implement as C-methods
- Fill the .m file with @synthesize directives
- Create a blank method in the .m file for each method, and put an “NSAssert( FALSE, “Not implemented yet” );” in there (or implement it yourself)
- Any other DOM classes that are used as variables or method parameters … do all the above again
Hiding NSArray and NSDictionary behind SVG Spec methods
The SVG Spec is designed to work in ANY programming language – so it doesn’t support some core features of ObjectiveC, such as fast enumeration (i.e. the “for( NSObject* o in array)” syntax).
A much bigger problem for you is that you can’t include “init” methods, which are necessary for good ObjectiveC code.
Our DOM and SVG classes *must* be spec compliant, so we cannot expose the raw array – and we can’t add methods to provide fast enumeration, nor custom init methods.
Instead … when you have a situation like that, and you want users to be able to (optionally) access them … go ahead and do it, but put the “bonus” methods into a separate header file.
In Xcode, this is called a “class extension”, and it’s a special feature of ObjectiveC. Select “Class Extension” when creating the new file.
e.g. look at the source for Nodelist.h and NodeList.m – and notice that some of the methods are missing from the header file, but appear in NodeList+Mutable.h
In general, you should use the following naming strategies:
- If the bonus features are needed to modify properties that SVG Spec says are “read only”, name the extension “Mutable” to make it clear that’s what it’s for
- If the bonus features are ONLY a convenience, e.g. to enable fast enumeration, name the extension “NotInSpec” or similar
“SVG parsing from DOM”
Again, the SVG spec rigorously defines the name of every “SVG” class, and its methods, and its variables. You must follow these exactly.
The process is identical as for the DOM Spec notes above.
NB: SVG was designed and intended to be implemented on-top-of DOM; many of the SVG Spec methods are trivial to implement if you use the DOM methods that already exist. You are not supposed to re-invent the wheel!
For instance, have a look at DOMDocument, and Node, and Element – they have some very useful methods built-in to them.
Remmember: if you call SVGElement’s init method, then every SVG tag has already been parsed into a DOM Element (which extends DOM Node). It already has all the XML attributes pre-parsed and available to you!
Gotcha 1: SVG attributes are NOT nil
SVG Spec defines that “empty” or “missing” attributes have to be returned NOT AS NULL but as an empty string (“”).
This means you must NEVER write:
[objc]
Attr* fillAttribute = [self getAttribute:@"fill"];
if( fillAttribute ) // DO NOT DO THIS!!!
…
[/objc]
…because according to the spec fillAttribute can be non-null even though in the SVG it’s blank. Instead, you must (according to spec) do:
[objc]
Attr* fillAttribute = [self getAttribute:@"fill"];
if( fillAttribute.length > 0 ) // This is correct, according to SVG Spec
…
[/objc]
Gotcha 2: XML Namespaces
You can parse a lot of SVG’s and ignore namespaces; most SVG’s use the same “convention” for naming the XML tags.
It’s a convention; it’s a default; it IS NOT GUARANTEED.
But XML-namespaes are guaranteed. All SVGKit code should be using namespaces explicitly.
As a convenience for users, the DOM spec allows us to provide methods that do NOT need an explicit namespace – but you should not be using them! They will occasionally fail when used on some input SVG files
So, for instance, you should NOT do this:
[objc]
Attr* fillAttribute = [self getAttribute:@"fill"]; // DON’T DO THIS (it’ll work 99% of the time, but … best not to)
[/objc]
instead do this:
[objc]
Attr* fillAttribute = [self getAttributeNS:svgNamespace localName:@"fill"]; // CORRECT. (svgNamespace is the HTTP URL of the official SVG Spec)
[/objc]
At the moment, we don’t have a convenience method for “get the namespace that means SVG” – this really should be part of the SVGKparserSVG extension.
NB: if you’re afraid this namespace stuff won’t work, note that SVGKparser already has full namespace support, and will automatically create the SVG namespace if needed when parsing incoming SVG files
Gotcha 3: Cascading (as in: “Cascading Style Sheets” i.e. CSS)
The SVG spec officially is based on DOM; but it’s also (officially) based on CSS.
Fortunately, we only have to support a subset of CSS – the two parts that SVG uses are:
- Embedding stylesheets, or referencing them with an external “link” tag
- Cascading
But cascading is tricky. There are approximately 50 XML attributes that – officially – must be “cascaded” when using SVG. There’s a table of them in the spec – http://www.w3.org/TR/SVG/propidx.html
Cascading is tricky, and it’s potentially quite slow – you have to look-up the property in many different places, and check each one “in correct order” until you find the first match.
So, we have a method in SVGElement that does all this for you:
[objc]
-(NSString*) cascadedValueForStylableProperty:(NSString*) stylableProperty
[/objc]
…but that is not part of the SVG Spec, and it’s possible it *is* part of the CSS spec, but located in a different class (I haven’t found it yet). We’ll leave that method there as a convenience, but you might need to import a special header to access it (since it’s not part of the SVG Spec).
To use cascading (which you MUST do), instead of this:
[objc]
// DON’T DO THIS (it ignores cascading and styles and CSS-classes)
Attr* fillAttribute = [self getAttribute:@"fill"];
[/objc]
…do this:
[objc]
// Automatically does all the CSS stuff for you
NSString* fillAttributeValue = [self cascadedValueForStylableProperty:@"fill"];
[/objc]
…eventually, we’ll add an error / NSAssert for cases where you pass in a property that is not one of the cascadeable ones – for now, just use the table as a reference.
Class names and method names
We couldn’t use a classname prefix of “SVG” because the SVG spec reserves all classnames beginning “SVG”. Inside the project, you’ll find all of these in the “SVG DOM” folder – please note: these are MANDATED by the SVG Spec, we did NOT come up with the names.
Apple had a similar problem when they invented GLKit – the prefix “GL” was already used in the OpenGL library they were extending, so they named their classes “GLK” prefix. Hence … “SVGK”)
Whenever you create a new class that is not part of the SVG Spec – for any reason – you must prefix the name with “SVGK”.
Some of our classes – for historic reasons – don’t follow this convention. Yet. Feel free to refactor any you encounter.