I was recently working on a project that was rapidly approaching its deadline. A week and a half before it was due to be turned over for user-acceptence testing (UAT), a bug was submitted stating that the tester could not save. At this point in the project, bugs had dwindled down to be small, tweaky things like changing text, or modifying the workflow so I didn’t expect anything too dramatic here. Sometimes I’m very wrong…
My company has a native application that hosts a WebView, through which we deliver our content as web applications. The application that I was working on is a drawing application that will utilize some pretty edgy HTML5 and CSS3 technologies to attempt to deliver a near-native experience via a web app.
The Problem
The application is based on the canvas tag. It allows the user to add a combination of free-drawn lines and images onto the canvas to produce their desired image. Additionally, the user can reselect the items and manipulate them (i.e. translate, rotate, and scale) so that they fit together well. Originally, all of the images were in .PNG format. This worked well, except for the scaling operation would often leave the images fuzzy, or improperly rendered. This was especially true with the stencil images, which consist mostly of black and white line art.
To improve the rendering of the stencils, we elected to change to the .SVG vector graphic format. Since vector graphics are resolution independent, the image quality was always excellect. As an additional bonus, .SVG images tend to be smaller than .PNGs so we got a network load reduction for free.
Then the sky fell down.
It turns out that there is an issue with rendering .SVG images onto a canvas. As discussed here, many browsers won’t let you get the image data from a canvas after an .SVG has been rendered to it. Ever. There is no work around, it just doesn’t work. This has been corrected in many browsers, but the native browser for Android devices (pre-KitKat) have never been patched.
The Solution
In a previous post, we had parsed the original SVG image’s XML document and generated a representation of that document (called an Abstract Syntax Tree or AST) like this:

From here, we can convert the document into any format that we want. For this project, we want to get a Dojo module that creates an image that is a representation of the original SVG at whatever size we want.
The Renderer
A renderer, in this context, will take an AST and export it to a different format. To accomplish this, I created an SvgModuleRenderer that looks like this:
import com.sterling.custlibrary.converters.svgtokens.SvgRootToken;
public class SvgModuleRenderer {
private SvgModuleInitializerRenderer svgModuleInitializerRenderer = new SvgModuleInitializerRenderer();
private SvgGetBaseImageSizeFunctionRenderer svgGetBaseImageSizeFunctionRenderer =
new SvgGetBaseImageSizeFunctionRenderer();
private SvgSetImageSizeFunctionRenderer svgSetImageSizeFunctionRenderer = new SvgSetImageSizeFunctionRenderer();
private SvgDrawingHelperFunctionRenderer svgDrawingHelperFunctionRenderer = new SvgDrawingHelperFunctionRenderer();
private SvgGetImageFunctionRenderer svgGetImageFunctionRenderer = new SvgGetImageFunctionRenderer();
private LineRenderer lineRenderer = new LineRenderer();
public String render(SvgRootToken root) {
String result = "";
lineRenderer.increaseIndent();
result += renderHeader();
result += renderInitializers(root);
result += renderGetImageFunction(root);
result += renderDrawingHelperFunctions();
result += renderSetImageSizeFunction();
result += renderGetBaseImageSizeFunction();
result += renderFooter();
return result;
}
private String renderInitializers(SvgRootToken root) {
String result = null;
result = svgModuleInitializerRenderer.render(root, lineRenderer);
return result;
}
private String renderGetImageFunction(SvgRootToken root) {
String result = null;
result = svgGetImageFunctionRenderer.render(root, lineRenderer);
return result;
}
private String renderDrawingHelperFunctions() {
String result = null;
result = svgDrawingHelperFunctionRenderer.render(lineRenderer);
return result;
}
private String renderSetImageSizeFunction() {
String result = null;
result = svgSetImageSizeFunctionRenderer.render(lineRenderer);
return result;
}
private String renderGetBaseImageSizeFunction() {
String result = null;
result = svgGetBaseImageSizeFunctionRenderer.render(lineRenderer);
return result;
}
private String renderHeader() {
String result = "";
result += lineRenderer.render("define([], function() {");
result += lineRenderer.renderEmptyLine();
return result;
}
private String renderFooter() {
String result = "";
lineRenderer.increaseIndent();
result += lineRenderer.renderEmptyLine();
result += lineRenderer.render("return {");
lineRenderer.increaseIndent();
;
result += lineRenderer.render("getImage: getImage,");
result += lineRenderer.render("setImageSize: setImageSize,");
result += lineRenderer.render("getBaseImageSize: getBaseImageSize");
lineRenderer.decreaseIndent();
result += lineRenderer.render("};");
lineRenderer.decreaseIndent();
result += lineRenderer.render("});");
lineRenderer.decreaseIndent();
return result;
}
}
It, more or less, follows the template pattern where it coordinates other renders and tells them when to render their content, providing them whatever data or helper methods are required. The major pieces of the module are:
- The header
- The initializers
- The “getImage” function
- The drawing helper functions
- The “setImageSize” function
- The “getBaseImageSizeFunction”
- The footer
The Header
This renderer exports static text that is common to every module we are going to render. It is very simple and simply exports the following code:
define([], function() {
The Initializers
When Dojo loads a module, it executes the function that is provided by the second argument to the define function (added in the header section above). Whatever is returned from this function is the public definition of the module. We can take advantage of the fact that we are guaraneteed to have this function executed once, and only once, to provide some initialization logic. That is what the Initializer Renderer does. It declares the variables that are going to be used by the public methods, but aren’t exported from the module for public consumption. The output of this section looks like this:
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var currentX = 0;
var currentY = 0;
var baseWidth = {root.getWidth() + 4};
var baseHeight = {root.getHeight() + 4}
var width = baseWidth;
var height = baseHeight;
var ratio = width / baseWidth;
The only dynamic code that we find here is the injection of the SVG images base height and width. These are used in the scaling operations to determine where the final points will be placed when rendering the output.
The “getImage” function
The getImage() function represents the core functionality of the module. This is the function that is responsible for generating the image at the currently requested resolution. It is also the longest method, by far, when rendered since each rendering instruction will be added here. The code (without rendering instructions) looks like this:
function getImage() {
canvas.height = height;
canvas.width = width;
ctx.translate(scale(-1 * {root.getXOffset()}), scale(-1 * {root.getYOffset()}));
}
.
.
.
<<rendering instructions>>>
.
.
.
var result = new Image();
result.src = canvas.toDataURL();
return result;
}
The method starts by assigning the canvas to the currently required height and width (simultaneously clearing the canvas) and setting up an offset for the drawing. This offset is requried to handle SVG images whose viewBox is does not start at (0, 0). Next all of the rendering instructions are added (discussed below) and then the resultant image is generated and returned.
The rendering of the instructions is done by iterating through the AST and passing each token, based on its type, to a method that knows how to render the associated JavaScript code. As an example, let’s consider a <circle> element. The original element is normally in the form:
<circle cx="75" cy="100" r="50" opacity="0.5"/>
The exported JavaScript for this tag will look like this:
ctx.beginPath();
ctx.arc(scale(75 + 2), scale(100 + 2), scale(50), 0, Math.PI * 2);
ctx.globalAlpha = "0.5";
ctx.fillStyle = "#fff";
ctx.fill();
Every instruction will be processed using a similar strategy until the entire document is processed.
The drawing helper functions
These functions are present to streamline the code in the getImage() function as much as possible. The provide helpers for path operations, like move and bezier curve so that the getImage() function can make a single call and all of the steps to make that happen. For example, the getImage() function might have a statement:
curveTo({cp1x}, {cp1y}, {cp2x}, {cp2y}, {x}, {y}, {isAbsolute})
this would call the curveTo helper function that looks like this:
function curveTo(cp1X, cp1Y, cp2X, cp2Y, x, y, isAbsolute) {
cp1X = isAbsolute? (scale(cp1X) + scale(2)) : scale(cp1X) + currentX;
cp1Y = isAbsolute? (scale(cp1Y) + scale(2)) : scale(cp1Y) + currentY;
cp2X = isAbsolute? (scale(cp2X) + scale(2)) : scale(cp2X) + currentX;
cp2Y = isAbsolute? (scale(cp2Y) + scale(2)) : scale(cp2Y) + currentY;
x = isAbsolute? (scale(x) + scale(2)) : scale(x) + currentX;
y = isAbsolute? (scale(y) + scale(2)) : scale(y) + currentY;
ctx.bezierCurveTo(cp1X, cp1Y, cp2X, cp2Y, x, y);
currentX = x;
currentY = y;
}
As you can see, there is a lot involved in getting the correct values for the points, rendering the curve, and then updating the current point on the canvas for future operations that are relative to the current point.
The “setImageSize” function
The setImageSize function is another static function that allows the consuming code to request getImage() to generate an image at a different size. It exports this code:
function setImageSize(newWidth, newHeight) {
height = newHeight;
width = newWidth;
ratio = newWidth / newHeight;
}
The “getBaseImageSize” function
This is another simple function that tells allows the consuming code to find out how large the image wants to render itself as a “native” size.
function getBaseImageSize() {
var result = {
height: baseHeight,
width: baseWidth
};
return result;
}
The footer
Finally, the module renderer will add the footer to the module. This contains the code that returns the object that defines the module’s public API and the required code to close off the module itself.
return {
getImage: getImage,
setImageSize: setImageSize,
getBaseImageSize: getBaseImageSize,
};
});
h
Results
After all of this work, we can take an SVG document that looks like this:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 93 78" enable-background="new 0 0 93 78">
<g stroke="#231F20" stroke-miterlimit="10">
<path fill="#fff" d="M4.5 8.5h83v61h-83z"/>
<path fill="#D1D3D4" d="M10.5 8.5h10v61h-10zM71.5 8.5h10v61h-10z"/>
<path fill="#fff" d="M.5.5h11v77h-11zM81.5.5h11v77h-11zM11.3.4l9.8 7.7M82.1.4l-9.8 7.7M11.3 77.3l9.8-7.7M72.3 69.6l9.8 7.7"/>
<path fill="#BCBEC0" d="M15.5 8.5h62v61h-62z"/>
</g>
</svg>
Which renders this image:

Is now generated by a module with this code:
define([], function() {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var currentX = 0;
var currentY = 0;
var baseWidth = 97.0;
var baseHeight = 82.0;
var width = baseWidth;
var height = baseHeight;
var ratio = width / baseWidth;
function getImage() {
canvas.height = height;
canvas.width = width;
ctx.translate(scale(-1 * 0.0), scale(-1 * 0.0));
ctx.beginPath();
moveTo(4.5, 8.5, true);
lineTo(83.0, 0.0, false);
lineTo(0.0, 61.0, false);
lineTo(-83.0, 0.0, false);
ctx.closePath();
ctx.globalAlpha = 1.0;
ctx.lineWidth = '1'
ctx.strokeStyle = '#231F20';
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.fill();
ctx.beginPath();
ctx.globalAlpha = 1.0;
ctx.lineWidth = '1'
ctx.strokeStyle = '#231F20';
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.fill();
ctx.beginPath();
moveTo(10.5, 8.5, true);
lineTo(10.0, 0.0, false);
lineTo(0.0, 61.0, false);
lineTo(-10.0, 0.0, false);
ctx.closePath();
ctx.globalAlpha = 1.0;
ctx.lineWidth = '1'
ctx.strokeStyle = '#231F20';
ctx.stroke();
ctx.fillStyle = '#D1D3D4';
ctx.fill();
ctx.beginPath();
moveTo(71.5, 8.5, true);
lineTo(10.0, 0.0, false);
lineTo(0.0, 61.0, false);
lineTo(-10.0, 0.0, false);
ctx.closePath();
ctx.globalAlpha = 1.0;
ctx.lineWidth = '1'
ctx.strokeStyle = '#231F20';
ctx.stroke();
ctx.fillStyle = '#D1D3D4';
ctx.fill();
ctx.beginPath();
ctx.globalAlpha = 1.0;
ctx.lineWidth = '1'
ctx.strokeStyle = '#231F20';
ctx.stroke();
ctx.fillStyle = '#D1D3D4';
ctx.fill();
ctx.beginPath();
moveTo(0.5, 0.5, true);
lineTo(11.0, 0.0, false);
lineTo(0.0, 77.0, false);
lineTo(-11.0, 0.0, false);
ctx.closePath();
ctx.globalAlpha = 1.0;
ctx.lineWidth = '1'
ctx.strokeStyle = '#231F20';
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.fill();
ctx.beginPath();
moveTo(81.5, 0.5, true);
lineTo(11.0, 0.0, false);
lineTo(0.0, 77.0, false);
lineTo(-11.0, 0.0, false);
ctx.closePath();
ctx.globalAlpha = 1.0;
ctx.lineWidth = '1'
ctx.strokeStyle = '#231F20';
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.fill();
ctx.beginPath();
moveTo(11.3, 0.4, true);
lineTo(9.8, 7.7, false);
moveTo(82.1, 0.4, true);
lineTo(-9.8, 7.7, false);
moveTo(11.3, 77.3, true);
lineTo(9.8, -7.7, false);
moveTo(72.3, 69.6, true);
lineTo(9.8, 7.7, false);
ctx.globalAlpha = 1.0;
ctx.lineWidth = '1'
ctx.strokeStyle = '#231F20';
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.fill();
ctx.beginPath();
moveTo(15.5, 8.5, true);
lineTo(62.0, 0.0, false);
lineTo(0.0, 61.0, false);
lineTo(-62.0, 0.0, false);
ctx.closePath();
ctx.globalAlpha = 1.0;
ctx.lineWidth = '1'
ctx.strokeStyle = '#231F20';
ctx.stroke();
ctx.fillStyle = '#BCBEC0';
ctx.fill();
ctx.beginPath();
ctx.globalAlpha = 1.0;
ctx.lineWidth = '1'
ctx.strokeStyle = '#231F20';
ctx.stroke();
ctx.fillStyle = '#BCBEC0';
ctx.fill();
var result = new Image();
result.src = canvas.toDataURL();
return result;
}
function curveTo(cp1X, cp1Y, cp2X, cp2Y, x, y, isAbsolute) {
cp1X = isAbsolute? (scale(cp1X) + scale(2)) : scale(cp1X) + currentX;
cp1Y = isAbsolute? (scale(cp1Y) + scale(2)) : scale(cp1Y) + currentY;
cp2X = isAbsolute? (scale(cp2X) + scale(2)) : scale(cp2X) + currentX;
cp2Y = isAbsolute? (scale(cp2Y) + scale(2)) : scale(cp2Y) + currentY;
x = isAbsolute? (scale(x) + scale(2)) : scale(x) + currentX;
y = isAbsolute? (scale(y) + scale(2)) : scale(y) + currentY;
ctx.bezierCurveTo(cp1X, cp1Y, cp2X, cp2Y, x, y);
currentX = x;
currentY = y;
}
function lineTo(x, y, isAbsolute) {
x = isAbsolute? (scale(x) + scale(2)) : scale(x) + currentX;
y = isAbsolute? (scale(y) + scale(2)) : scale(y) + currentY;
ctx.lineTo(x, y);
currentX = x;
currentY = y;
}
function moveTo(x, y, isAbsolute) {
if (isAbsolute) {
currentX = (scale(x) + scale(2));
currentY = (scale(y) + scale(2));
} else {
currentX += scale(x);
currentY += scale(y);
}
ctx.moveTo(currentX, currentY);
}
function drawText(text, font, textSize, fillStyle, strokeStyle, x, y, isAbsolute) {
if (isAbsolute) {
currentX = (scale(x) + scale(2));
currentY = (scale(y) + scale(2));
} else {
currentX += scale(x);
currentY += scale(y);
}
ctx.font = "'" + textSize + " " + font + "'";
if (fillStyle) {
ctx.fillStyle = fillStyle;
ctx.fillText(text, currentX, currentY);
}
if (strokeStyle) {
ctx.strokeStyle = strokeStyle;
ctx.strokeText(text, currentX, currentY);
}
}
function scale(value) {
return (value) * ratio;
}
function setImageSize(newWidth, newHeight) {
height = newHeight;
width = newWidth;
ratio = newWidth / baseWidth;
}
function getBaseImageSize() {
var result = {
height: baseHeight,
width: baseWidth
};
return result;
}
return {
getImage: getImage,
setImageSize: setImageSize,
getBaseImageSize: getBaseImageSize
};
});
Conclusion
These posts have been about how we can take an SVG XML document and convert it to a Dojo module. While this certainly isn’t the prettiest solution, and promises to be challenging to manage over time, it is possible. In the end, we have a tool that reimplements enough of the SVG standard to allow the project to continue and, at the end of the day, that is sometimes the best we can hope for.
One final comment is that the techniques used here are actually pretty common for a lot of different tasks to convert data from one domain to another. The use of a parser to generate tokens, tokens to generate an abstract syntax tree and then using the AST to generate the desired output gives a nice workflow and allows code to remain maintainable as the sophistication of the converter increases.