In yesterday’s post, we built a renderer for our SVG files. That’s all fine and good, but there’s not much point to having a renderer for static files. What we really want to do is render dynamic files, where the data inside the file is manipulated somehow by ColdFusion. Of course, this is a pretty nebulous request, as your data manipulation needs are only limited by your imagination. But for today, I’ll be going through a few things that I’ve needed to do.
To do all of this, we’re going to build what I’ve called an “SVGmunger”. At its heart, it’s straightforward:
Load an SVG template file.
Iterate through a series of modifications to the XML document.
Save your changes to a new file.
My SVGmunger has gotten more complex as I’ve used it in more applications, but there are a few core functions: load, saveAs, setText, setAttribute.
The load function is exactly what you would expect it to be, as is the saveAs function:
<cffunction name="load" access="public" output="false"> <cfargument name="Filename" type="string" required="true"> <cfset var FileContent=""> <cffile action="read" variable="FileContent" file="#Arguments.Filename#"> <cfset Variables.XML=XMLParse(FileContent)> <cfset Variables.Filename=Arguments.Filename> </cffunction> <cffunction name="SaveAs" access="public" output="false"> <cfargument name="Filename" type="string" required="true"> <cfset var XmlAsString=Replace(ToString(Variables.XML),"UTF-8","ISO-8859-1","ONE")> <cffile action="write" output="#XmlAsString#" file="#Arguments.FileName#" charset="iso-8859-1"> </cffunction>
You’ll notice that there’s a little extra magic in the saveAs function having to do with character encoding. CF and Batik don’t quite see eye-to-eye on XML character encoding, especially not when you start adding international characters into the mix. Beyond that, there’s nothing outrageous, right?
Now that we can load and save our SVG file, we need to be able to make changes to it, which is where our setText and setAttribute functions come in handy. The first, setText, replaces the text part of an element (between the tags), while the second, setAttribute, replaces the content of a single attribute. If you think about it, these are the two primary operations you’ll be needing. If you want to change some of the text in a graphic, such as to put in a person’s name, you’ll be changing an element’s text. If you want to modify the look of a graphic, such as to change a color or transform a layer, you’ll be changing an element’s attributes.
Sure, you could do both of those by hand in code, but why?
<cffunction name="setText" access="public" output="false"> <cfargument name="id" type="string" required="true"> <cfargument name="NewText" type="string" required="false"> <cfset var el=XmlSearch(Variables.XML,"/descendant::*[attribute::id='#Arguments.id#']")> <cfset var i=0> <cfif Arguments.NewText EQ ""><cfset Arguments.NewText=" "></cfif> <cfloop from="1" to="#ArrayLen(el)#" index="i"> <cfset el[i].XmlText=Arguments.NewText> <cfset ArrayClear(el[i].XmlChildren)> </cfloop> </cffunction>
First, we use XmlSearch to pick out the element we’re looking for. Second, we do a bit of cleanup on the NewText argument—setting it to a single space means that the XML handler won’t collapse the tag, which we may not want to happen. (You are welcome to take out that line if it doesn’t suit your needs.) I’ve managed, via dumb copy-n-paste, to have more than one element with the same ID, so I use a loop to get all of them instead of just a single cfset statement. Within the loop we set the new text, then we clear out any child elements. Again, if you don’t need to clear out any child elements then you can eliminate that line.
As you can imagine, setAttribute is the same thing:
<cffunction name="setAttribute" access="public" output="false"> <cfargument name="id" type="string" required="true"> <cfargument name="AttributeName" type="string" required="true"> <cfargument name="NewContent" type="string" required="true"> <cfset var el=XmlSearch(Variables.XML,"/descendant::*[attribute::id='#Arguments.id#']")> <cfset var i=0> <cfloop from="1" to="#ArrayLen(el)#" index="i"> <cfset el[i].XmlAttributes[Arguments.AttributeName]=Arguments.NewContent> </cfloop> </cffunction>
The only thing different from setText is what we do in the loop—this time we set the attribute to the new value, and don’t have to worry about mucking around with child elements.
You’ve probably noticed that both of my functions depend on knowing the ID of the element you want to modify. You could, of course, write functions that operate on XPath instructions instead of IDs. But, really, why not just take a moment and add some IDs to the SVG file? It’ll certainly make your code, and the SVG file, easier to read.
Congratulations, you can now muck about in your SVG internals without having to write a bunch of nasty code:
<cfset svg=createObject("component","SVGMunger")> <cfset svg.load("template.svg")> <cfset svg.setText("message", "Rick O was here!")> <cfset svg.setAttribute("message", "style", "fill-color: red;")> <cfset svg.saveAs("ricko.svg")> <cfset rend=createObject("component","SVGRenderer")> <cfset rend.RenderFile("ricko.svg", "ricko.png")>
Once you have these basics, you’ll quickly find that you want more. I’ve got a function that takes a query full of modifications and iterates through it, making each modification in turn. This is great for the progress graphs on NoWrists.com, as I can make a bunch of extensive changes to a template without having to write much code. You might prefer a function that takes a list or array or another XML document or whatever.
As you really start getting into it, you might find yourself in a position when you want to do more than just modifying a template. In my running graphs, for example, I have a series of mile markers. Logically, I should have just one “Marker” element/layer in my SVG template, then I can clone that element/layer any number of times to create as many markers as I need. However, I know that I’m not going to run an infinite number of miles, so it was just as easy for me to have 20 markers in my template, then only show and move the ones I need. It’s cheating, sure, I admit it. If you want to make more sophisticated functions for your SVGmunger, you go right ahead.
In the next post I’ll go through and make some eye-candy to show off some of the things we can now do with our new SVGmunger and SVGrenderer components.