(this is a post mainly to document some new features and fixes I’ve added to SVGKit version 1.2 today – they are already on the main development branch (currently: 1.x), ready for use)
SVGKit overview, November 2013
While a group of us were cleaning up and improving SVGKit about 2 years ago, I did a quick-n-simple re-architect of the in-memory data structures to split it into three wholly independent sets of source code / classes:
- Parsing an SVG file
- Parsing a legal XML file, with a new conforming DOM parser I wrote from scratch (because SVG Spec requires you to use a DOM parser, and Apple won’t let you use/extend their one on iOS/OSX!)
- Parsing the core SVG spec where it differs from XML-DOM
- Adding a system for user-supplied custom-parsers so that you can parse your custom XML in-line with the SVG (this is a major feature of SVG, but difficult to parse!)
- Rendering an SVG file with pixels on-screen
- Converting in-memory SVG data into Apple’s CALayer rendering format (used on all Apple Operating Systems)
- Optionally converting Apple’s CALayer format into highly-optimized hybrid data that lets you render SVG’s *fast*
- Painstakingly implementing every feature of SVG, from radial gradients to rich text (we’re about 90% complete now, still features left to add – please help!)
- Outputting an SVG file back to disk
- …not supported … until now!
I pushed through changes to the code that kept our model of the SVG file “clean”, and allowed us to calculate actual on-screen data as “late” as possible. This is the only correct way to do it: the Spec is clear that an SVG file is intended to be “dynamic” – that users can load it from disk, change it while its in memory and see the graphics update and/or save it back to disk again.
So, our data model has been:
- “Source”: info about where it came from (the original filename; the original URL you downloaded)
- “XML DOM”: a full in-memory version of the SVG data, that can be edited, and fully supports CSS styling (changes you make correctly “cascade” when you render it later)
- “CALayerTree”: a cached version of the CALayer’s that let Apple render your vector graphics … as vector graphics, preserving the infinite sharpness and infinite zoomable detail
- “…”: in theory: a string? or something that you could save back to disk
The new feature: saving a modified SVG back to disk
We already have Inkscape; why would you need the ability to edit and save SVG’s?
Speaking to other devs, I heard these main reasons:
- If you want to use data about how the SVG renders, on Apple hardware, you have to push it through Apple’s APIs!
- Inkscape is awesome, but doesn’t (yet) have an API for external apps to take control of it (*); that means if you want to use it programmatically, you have to learn their custom scripting. The app itself is also huge, not something it’s easy to “embed” in your own apps
- SVGKit already lets you do lots of great stuff with SVG’s in Cocoa/Objective-C, natively on iOS (and OS X, in MaddTheSane’s fork); why not use the skills you already have?
(*) – NB: I’m not sure how true this is – I’ve not tried this myself? But it sounds like it’s either not there yet, or not in great condition yet (not enough languages/platforms supported).
New methods you need to make this work
Today I’ve created several EXPERIMENTAL methods. I am not happy that these pollute the official SVG classes – I’ve been working hard for 2 years to remove all the “pollution” we have and make us closer to the official SVG Spec; I reserve the right to angrily rant at my own uncleanliness ;), and move / refactor these methods to somewhere else in the codebase very soon.
- Node.h (part of our DOM implementation):
- -(void) appendXMLToString:(NSMutableString*) outputString availableNamespaces:(NSDictionary*) prefixesByKNOWNNamespace activeNamespaces:(NSMutableDictionary*) prefixesByACTIVENamespace;
- SVGDocument.h (part of our SVG Spec implementation):
- -(NSMutableDictionary*) allPrefixesByNamespace;
- -(NSMutableDictionary*) allPrefixesByNamespaceNormalized;
The Node method does this:
- Look at the type of the Node, and output “<svg blah=”fdd”> … </svg>” style tags, or “<!– XML COMMENTS –>” etc
- Recurse to do the same with all child nodes
- the clever bit
- Correctly output all the XML Namespaces so that each tag is in its namespace, and all custom user-provided namespaces are retained!
- Intelligently output namespace declarations at the “tightest” place they’re needed
- If the source SVG file explicitly added namespaces “higher up” (e.g. at the root SVG tag) … preserve that!
…but it needs some help. From a first look at the problem, I suspect it’s not possible to correctly and safely write an SVG file to disk without first pre-processing the file and resolving any conflicts on namespace names. The XML Spec is a bit quiet about this (which applies to all XML / DOM saving) – I’ve left comments in the SVGKit source code, and marked it as “experimental”. Any mistakes or misinterpretations of the XML spec are my own, and you’re welcome to correct and patch them :).
So we have the methods in SVGDocument:
- allPrefixesByNamespace finds all the namespaces, and all the prefixes (in an attribute “xmlns:attname=”BLAH””, the “xmlns” text is technically a “prefix”)
- allPrefixesByNamespaceNormalized uses the output from allPrefixesByNamespace (which can contain duplicates, etc) and removes dupes, cleans it up, makes sure that what we output to disk is “clean” and compact, etc.
Using the new methods
I’ve written a sample (private) project to test this, and I’m using it inside my 3D earth game to do some clever things. Here’s the main code:
SVGKImage* svgImage = … load an SVG image
NSMutableString* ms = [NSMutableString string];
/** Find all namespaces */
NSMutableDictionary* allNamespaces = [svgImage.DOMDocument allPrefixesByNamespaceNormalized];
[ms appendString:@"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"]; // required by XML spec when saving/outputting
[original.rootElement appendXMLToString:ms availableNamespaces:allNamespaces activeNamespaces: [NSMutableDictionary dictionary]];
If you dig in the code, you’ll see why appendXMLToString needs “two” dictionaries. One is “namespaces you are ALLOWED to use”, the second is “ones that you’ve ALREADY OUTPUT TO DISK and are valid / in-scope for the remaining XML tags you are outputting”.
Memory usage on this stuff is … pretty damn high. This is because Apple’s implementation of NSString is surprisingly bad performance on iOS right now. At some point, someone needs to replace all the “[NSString stringWithFormat” with C-String equivalents (that are 10x faster, use 3 lines of code instead of 1, but do (almost) the identical thing). It’s a bit trickier than it sounds, but it COULD be done with a search-replace once you got it working.
(generally with iOS you don’t do much string manipulation, so the crappy NSString methods are fine. But when you’re doing actual parsing of text files / creating text files from an AST, like we are here … well, you should really give up on Apple’s library, and do it in C :()
On OS X … it’s very fast already, unoptimized – plenty fast enough for most uses, I think. I’m able to load massive SVG files (e.g. Wikiedia’s world maps) in a fraction of a second, iterate over them changing and removing vectors and paths, and then output a new file to disk in just a couple of seconds.
What I’m using it for
The wikipedia world map has many tiny islands on it. These make rendering slow (on old iPhones, at least – it’s super-fast on OS X!), and are too small to interact with anyway. This is for the 3D game I’ve got in development:
NOTE: those 3D red-borders are being generated in realtime by SVGKit, loading an SVG, outputing directly into Apple’s CALayers, and then rendering straight to an OpenGL texture. No tesselation, no hard stuff – just plain rendering
Using the new features above, and SVGKit’s existing features, it was fewer than 10 lines of real code to remove “all vectors whose bounding-box area is less than a minimum amount:
CALayer* rootLayer = self.imageToProcess.CALayerTree;
[self recursivePruneLayer:rootLayer fromDocument:self.imageToProcess.DOMDocument ifBoundsLessThan:100.0];
-(void) recursivePruneLayer:(CALayer*) layer fromDocument:(SVGDocument*) document ifBoundsLessThan:(float) minArea
float area = layer.bounds.size.width * layer.bounds.size.height;
if( area < minArea )
NSString* xmlTagID = [layer valueForKey:kSVGElementIdentifier];
Node* nodeToPrune = [document getElementById:xmlTagID];
/**** NB: I found a bug in "removeChild" today, and fixed that in SVGKit too ****/
for( CALayer* subLayer in layer.sublayers )
[self recursivePruneLayer:subLayer fromDocument:document ifBoundsLessThan:minArea];