{"id":2897,"date":"2013-11-17T23:36:44","date_gmt":"2013-11-17T22:36:44","guid":{"rendered":"http:\/\/t-machine.org\/?p=2897"},"modified":"2014-01-28T14:06:09","modified_gmt":"2014-01-28T13:06:09","slug":"svgkit-programmatic-editing-of-svg-files-on-ios","status":"publish","type":"post","link":"http:\/\/new.t-machine.org\/index.php\/2013\/11\/17\/svgkit-programmatic-editing-of-svg-files-on-ios\/","title":{"rendered":"SVGKit: Programmatic editing of SVG files on iOS"},"content":{"rendered":"<p>(this is a post mainly to document some new features and fixes I&#8217;ve added to <a href=\"https:\/\/github.com\/SVGKit\/SVGKit\/\">SVGKit<\/a> version 1.2 today &#8211; they are already on the main development branch (currently: 1.x), ready for use)<\/p>\n<h2>SVGKit overview, November 2013<\/h2>\n<p>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:<\/p>\n<ol>\n<li>Parsing an SVG file\n<ol>\n<li>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&#8217;t let you use\/extend their one on iOS\/OSX!)\n<li>Parsing the core SVG spec where it differs from XML-DOM\n<li>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!)\n<\/ol>\n<li>Rendering an SVG file with pixels on-screen\n<ol>\n<li>Converting in-memory SVG data into Apple&#8217;s CALayer rendering format (used on all Apple Operating Systems)\n<li>Optionally converting Apple&#8217;s CALayer format into highly-optimized hybrid data that lets you render SVG&#8217;s *fast*\n<li>Painstakingly implementing every feature of SVG, from radial gradients to rich text (we&#8217;re about 90% complete now, still features left to add &#8211; please help!)\n<\/ol>\n<li>Outputting an SVG file back to disk\n<ol>\n<li>&#8230;not supported &#8230; until now!\n<\/ol>\n<\/ol>\n<p><!--more--><br \/>\nI pushed through changes to the code that kept our model of the SVG file &#8220;clean&#8221;, and allowed us to calculate actual on-screen data as &#8220;late&#8221; as possible. This is the only correct way to do it: the Spec is clear that an SVG file is intended to be &#8220;dynamic&#8221; &#8211; that users can load it from disk, <strong>change it while its in memory<\/strong> and see the graphics update and\/or <em>save it back to disk again<\/em>.<\/p>\n<p>So, our data model has been:<\/p>\n<ol>\n<li>&#8220;Source&#8221;: info about where it came from (the original filename; the original URL you downloaded)\n<li>&#8220;XML DOM&#8221;: a full in-memory version of the SVG data, that can be edited, and fully supports CSS styling (changes you make correctly &#8220;cascade&#8221; when you render it later)\n<li>&#8220;CALayerTree&#8221;: a cached version of the CALayer&#8217;s that let Apple render your vector graphics &#8230; as vector graphics, preserving the infinite sharpness and infinite zoomable detail\n<li>&#8220;&#8230;&#8221;: in theory: a string? or something that you could save back to disk\n<\/ol>\n<h2>The new feature: saving a modified SVG back to disk<\/h2>\n<p>We already have <a href=\"http:\/\/inkscape.org\/\">Inkscape<\/a>; why would you need the ability to edit and save SVG&#8217;s?<\/p>\n<p>Speaking to other devs, I heard these main reasons:<\/p>\n<ol>\n<li>If you want to use data about how the SVG renders, on Apple hardware, you have to push it through Apple&#8217;s APIs!\n<li>Inkscape is awesome, but doesn&#8217;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&#8217;s easy to &#8220;embed&#8221; in your own apps\n<li>SVGKit already lets you do lots of great stuff with SVG&#8217;s in Cocoa\/Objective-C, natively on iOS (and OS X, in MaddTheSane&#8217;s fork); why not use the skills you already have?\n<\/ol>\n<p>(*) &#8211; NB: I&#8217;m not sure how true this is &#8211; I&#8217;ve not tried this myself? But it sounds like it&#8217;s either not there yet, or not in great condition yet (not enough languages\/platforms supported).<\/p>\n<h2>New methods you need to make this work<\/h2>\n<p>Today I&#8217;ve created several EXPERIMENTAL methods. <span style=\"color: red\">I am not happy that these pollute the official SVG classes &#8211; I&#8217;ve been working hard for 2 years to remove all the &#8220;pollution&#8221; 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<\/span>.<\/p>\n<ul>\n<li>Node.h (part of our DOM implementation):\n<ul>\n<li>-(void) appendXMLToString:(NSMutableString*) outputString availableNamespaces:(NSDictionary*) prefixesByKNOWNNamespace activeNamespaces:(NSMutableDictionary*) prefixesByACTIVENamespace;\n<\/ul>\n<li>SVGDocument.h (part of our SVG Spec implementation):\n<ul>\n<li>-(NSMutableDictionary*) allPrefixesByNamespace;\n<li>-(NSMutableDictionary*) allPrefixesByNamespaceNormalized;\n<\/ul>\n<\/ul>\n<p>The Node method does this:<\/p>\n<ul>\n<li>Look at the type of the Node, and output &#8220;&lt;svg blah=&#8221;fdd&#8221;&gt; &#8230; &lt;\/svg&gt;&#8221; style tags, or &#8220;&lt;!&#8211; XML COMMENTS &#8211;&gt;&#8221; etc\n<li>Recurse to do the same with all child nodes\n<li><strong>the clever bit<\/strong>\n<ul>\n<li>Correctly output all the XML Namespaces so that each tag is in its namespace, and all custom user-provided namespaces are retained!\n<li>Intelligently output namespace declarations at the &#8220;tightest&#8221; place they&#8217;re needed\n<li>If the source SVG file explicitly added namespaces &#8220;higher up&#8221; (e.g. at the root SVG tag) &#8230; preserve that!\n<li>&#8230;etc\n<\/ul>\n<\/ul>\n<p>&#8230;but it needs some help. From a first look at the problem, I suspect it&#8217;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) &#8211; I&#8217;ve left comments in the SVGKit source code, and marked it as &#8220;experimental&#8221;. Any mistakes or misinterpretations of the XML spec are my own, and you&#8217;re welcome to correct and patch them :).<\/p>\n<p>So we have the methods in SVGDocument:<\/p>\n<ul>\n<li>allPrefixesByNamespace finds all the namespaces, and all the prefixes (in an attribute &#8220;xmlns:attname=&#8221;BLAH&#8221;&#8221;, the &#8220;xmlns&#8221; text is technically a &#8220;prefix&#8221;)\n<li>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 &#8220;clean&#8221; and compact, etc.\n<\/ul>\n<h2>Using the new methods<\/h2>\n<p>I&#8217;ve written a sample (private) project to test this, and I&#8217;m using it inside my 3D earth game to do some clever things. Here&#8217;s the main code:<\/p>\n<p>[objc]<br \/>\n\tSVGKImage* svgImage = &#8230; load an SVG image<\/p>\n<p>\tNSMutableString* ms = [NSMutableString string];<\/p>\n<p>\t\/** Find all namespaces *\/<br \/>\n\tNSMutableDictionary* allNamespaces = [svgImage.DOMDocument allPrefixesByNamespaceNormalized];<\/p>\n<p>\t[ms appendString:@&quot;&lt;?xml version=\\&quot;1.0\\&quot; encoding=\\&quot;UTF-8\\&quot; standalone=\\&quot;no\\&quot;?&gt;\\n&quot;]; \/\/ required by XML spec when saving\/outputting<br \/>\n\t[original.rootElement appendXMLToString:ms availableNamespaces:allNamespaces activeNamespaces: [NSMutableDictionary dictionary]];<br \/>\n[\/objc]<\/p>\n<p>&#8230;that&#8217;s all!<\/p>\n<p>If you dig in the code, you&#8217;ll see why appendXMLToString needs &#8220;two&#8221; dictionaries. One is &#8220;namespaces you are ALLOWED to use&#8221;, the second is &#8220;ones that you&#8217;ve ALREADY OUTPUT TO DISK and are valid \/ in-scope for the remaining XML tags you are outputting&#8221;.<\/p>\n<p>Memory usage on this stuff is &#8230; pretty damn high. This is <em><strong>because Apple&#8217;s implementation of NSString is surprisingly bad performance on iOS right now<\/strong><\/em>. At some point, someone needs to replace all the &#8220;[NSString stringWithFormat&#8221; with C-String equivalents (that are 10x faster, use 3 lines of code instead of 1, but do (almost) the identical thing). It&#8217;s a bit trickier than it sounds, but it COULD be done with a search-replace once you got it working.<\/p>\n<p>(generally with iOS you don&#8217;t do much string manipulation, so the crappy NSString methods are fine. But when you&#8217;re doing actual parsing of text files \/ creating text files from an AST, like we are here &#8230; well, you should really give up on Apple&#8217;s library, and do it in C :()<\/p>\n<p>On OS X &#8230; it&#8217;s very fast already, unoptimized &#8211; plenty fast enough for most uses, I think. I&#8217;m able to load massive SVG files (e.g. Wikiedia&#8217;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.<\/p>\n<h2>What I&#8217;m using it for<\/h2>\n<p>The wikipedia world map has many tiny islands on it. These make rendering slow (on old iPhones, at least &#8211; it&#8217;s super-fast on OS X!), and are too small to interact with anyway. This is for the 3D game I&#8217;ve got in development:<\/p>\n<p><img decoding=\"async\" src=\"http:\/\/t-machine.org\/wp-content\/uploads\/select-usa.png\"\/><\/p>\n<p><em><strong>NOTE:<\/strong> those 3D red-borders are being generated in realtime by SVGKit, loading an SVG, outputing directly into Apple&#8217;s CALayers, and then rendering straight to an OpenGL texture. No tesselation, no hard stuff &#8211; just plain rendering<\/em><\/p>\n<p>Using the new features above, and SVGKit&#8217;s existing features, it was fewer than 10 lines of real code to remove &#8220;all vectors whose bounding-box area is less than a minimum amount:<\/p>\n<p>[objc]<br \/>\n-(void)tappedRemoveTinyPaths:(id)sender<br \/>\n{<br \/>\n\t\tCALayer* rootLayer = self.imageToProcess.CALayerTree;<\/p>\n<p>\t\t[self recursivePruneLayer:rootLayer fromDocument:self.imageToProcess.DOMDocument ifBoundsLessThan:100.0];<br \/>\n}<\/p>\n<p>-(void) recursivePruneLayer:(CALayer*) layer fromDocument:(SVGDocument*) document ifBoundsLessThan:(float) minArea<br \/>\n{<br \/>\n\tfloat area = layer.bounds.size.width * layer.bounds.size.height;<\/p>\n<p>\tif( area &lt; minArea )<br \/>\n\t{<br \/>\n\t\tNSString* xmlTagID = [layer valueForKey:kSVGElementIdentifier];<\/p>\n<p>\t\tNode* nodeToPrune = [document getElementById:xmlTagID];<br \/>\n\t\t[nodeToPrune.parentNode removeChild:nodeToPrune];<br \/>\n\/**** NB: I found a bug in &quot;removeChild&quot; today, and fixed that in SVGKit too ****\/<br \/>\n\t}<br \/>\n\telse<br \/>\n\t{<br \/>\n\t\tfor( CALayer* subLayer in layer.sublayers )<br \/>\n\t\t{<br \/>\n\t\t\t[self recursivePruneLayer:subLayer fromDocument:document ifBoundsLessThan:minArea];<br \/>\n\t\t}<br \/>\n\t}<br \/>\n}<br \/>\n[\/objc]<\/p>\n","protected":false},"excerpt":{"rendered":"<p>(this is a post mainly to document some new features and fixes I&#8217;ve added to SVGKit version 1.2 today &#8211; 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 [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[51,20],"tags":[],"_links":{"self":[{"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/posts\/2897"}],"collection":[{"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/comments?post=2897"}],"version-history":[{"count":8,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/posts\/2897\/revisions"}],"predecessor-version":[{"id":3111,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/posts\/2897\/revisions\/3111"}],"wp:attachment":[{"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/media?parent=2897"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/categories?post=2897"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/tags?post=2897"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}