Love Generating SVG With JavaScript? Move It To The Server!

Advertisement

I hope that by now, in 2014, there is no need to explain why SVG is a blessing to developers who want to ensure that their graphics look sharp on all devices, especially with their huge diversity of resolutions.

But just like any other technology, SVG has its limitations. And in this article, we’ll talk about how to bypass some of them.

What’s The Problem?

Why would you even need to generate SVG on the server? The technology is entirely client-side, so what would motivate anyone to move it from there?

Before answering these questions, let’s look at the state of the industry. When we talk about “generating SVG” nowadays, we mean “generating SVG with JavaScript.” The current state of browser support and libraries makes the creation of complex visuals (even with animations and user interaction) a trivial task: Just pick the library that suits your need.

And there are a lot to choose from, from general-purpose ones such as Raphaël.js1, Snap.svg2 and svg.js3 to the myriad of smaller ones, as well as, if you do plotting and data visualization, gRaphaël4, Highcharts5 and D36.

So the right question is, how do we continue generating SVG with JavaScript while also putting the results of the generation on the server? The question is a bit long, but here are the reasons why we should answer it:

  • To enable the user to download a graphic
    If we don’t want to scare the user with this “unknown” format, then we should convert the SVG to a PNG or PDF.
  • To enable a graphic to inserted in an email
    We all love charts in our emails, and we prefer Retina-ready ones.
  • To enable a graphic to be displayed on another website
    Think API.
  • To improve performance
    Complex visualization logic can easily hang the browser for multiple seconds. By generating the SVG on the server, we can cache the result and then deliver the cached SVG when the next user wants it.

Solutions

Simply Recreate All Logic on the Server

This solution is possible, just not practical. The majority of mature back-end languages have libraries for generating SVG. But we are developers. We do not want to recreate the same logic twice, because that would lead to bugs, integration problems and a burdensome need for support.

Straightforward Solution

The easiest way to put the generated SVG on the server is just to send the generated data with an AJAX request when it’s complete.

Below is a simple example with Raphaël.js and jQuery. Let’s start with simple HTML as a boilerplate:

<!DOCTYPE html>
<html>
   <head>
      <meta charset="utf-8">
      <script src="http://code.jquery.com/jquery-2.1.0.min.js"></script>
      <script src="http://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.2/raphael-min.js"></script>
   </head>
   <body>
      <div id="svg"></div>
   </body>
</html>

Now, add our drawing code to it:

$(function() {
   var svgContainer = document.getElementById("svg");
   var paper = Raphael(svgContainer, 640, 480);
  
   paper
      .rect(0, 0, 640, 480, 10)
      .attr({
         fill: '#fff',
         stroke: 'none'
      });
    
   var circle = paper
                .circle(320, 240, 60)
                .attr({
                   fill: '#223fa3', 
                   stroke: '#000',
                   'stroke-width': 80,
                   'stroke-opacity': 0.5
                });
   paper
      .rect(circle.attr('cx') - 10, circle.attr('cy') - 10, 20, 20)
      .attr({
         fill: '#fff',
         stroke: 'none'
      });
  
   $.post(
      '/svg_catcher',
      { content: svgContainer.innerHTML }
  );
});

On the server, we just cache the result and store or process it any way we’d like. Here is the resulting image, in case you’re curious:

8
In case your browser does not support this image, here’s a PNG version9.

This technique is absolutely suitable not only for Raphaël, but for any SVG-generating library. I just picked Raphaël because it is a popular, battle-tested solution, with an API that is easy to understand. It drastically simplifies the creation and manipulation of images via JavaScript, and also supports VML for old browsers that don’t support SVG.

This approach is easy and straightforward, but it has a lot of downsides, too:

  • You will have to inline all external resources (mainly images) after uploading them; otherwise, all of the converters will show a white placeholder instead of the original source.
  • If the SVG is big and the user’s network is not reliable, then the resulting file might end up invalid. We could verify validity server-side, but that wouldn’t help with the next point.
  • Malicious input is a problem. Replacing a beautiful colorful business chart with the (SVG) image of a kitten is easy. Technically, it would be valid, but you can see why it would be harmful.

You could certainly take this approach if you trust your users (if it’s an intranet application, for example). Otherwise, the security risk might be too great.

PhantomJS

To eliminate the user’s input from the equation (or to minimize it as much as possible), we should move our generation script to the server.

A lot of complex code is running in the browser. The easiest way to run it on the server is to move the browser to the server. Thanks to PhantomJS10, that is totally doable.

PhantomJS is a WebKit implementation that can be controlled with JavaScript. Roughly speaking, it is an actual — yet headless — browser, meaning that Web pages are never rendered.

Start by installing PhantomJS on your system, which is easy by following the official documentation11. Then, we’ll change our script a bit to make it work with PhantomJS: First, create index.html with the boilerplate code for our generator:

<!DOCTYPE html>
<html>
   <head>
      <script src="http://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.2/raphael-min.js"></script>
      <meta charset="utf-8">
   </head>
   <body>
      <div id="svg"></div>
   </body>
</html>

Then, create generator.js, which will drive PhantomJS:

var fs = require('fs');
var page = require('webpage').create();
var url = 'file://' + fs.absolute('./index.html');

var svgDrawer = function() {
   var svgContainer = document.getElementById("svg");
   var paper = Raphael(svgContainer, 640, 480);
   paper
      .rect(0, 0, 640, 480, 10)
      .attr({
         fill: '#fff',
         stroke: 'none'
    });
   var circle = paper
                .circle(320, 240, 60)
                .attr({
                   fill: '#223fa3',
                   stroke: '#000',
                   'stroke-width': 80,
                   'stroke-opacity': 0.5
                });
   paper
      .rect(circle.attr('cx') - 10, circle.attr('cy') - 10, 20, 20)
      .attr({
         fill: '#fff',
         stroke: 'none'
    });

   return svgContainer.innerHTML;
};

page.open(url, function (status) {
   console.log(page.evaluate(svgDrawer));
   phantom.exit();
});

Now it is ready to generate SVG for us:

phantomjs generator.js > result.svg

So, we have a generator that fully works on our server, without our having written a lot of code.

Can we make it better? Yes!

Running time phantomjs generator.js > result.svg took about 0.2 seconds on my machine.

0.14s user 0.03s system 74% cpu 0.232 total

The most resource-consuming task was, of course, the “browser warming.” We have to do it on every request, which is somewhat inefficient. We can prevent this by turning our PhantomJS script from a “single-run” solution to a real server that listens to data and responds with SVG.

Let’s do that. We do not want to generate the same SVG every time, so we will also add dynamic support for our visualization. Our final workflow will look like this:

  1. Start the server, which will listen on the special port for our requests.
  2. Pass it the request with our data payload for visualization.
  3. Receive the response with SVG based on our data.

To run a server, we will use the built-in module13:

var port, server, page, url, fs = require('fs');

port = 9494;
server = require('webserver').create();

page = require('webpage').create();
url = 'file://' + fs.absolute('./index.html');

Then, we’ll change our drawer to support dynamic data:

var svgDrawer = function(data) {
   var svgContainer = document.getElementById("svg");
   var paper = Raphael(svgContainer, 640, 480);
   paper
      .rect(data.x, data.y, 640, 480, 10)
      .attr({
         fill: '#fff',
         stroke: 'none'
      });
   var circle = paper
                .circle(data.x/2, data.y/2, 60)
                .attr({
                   fill: '#223fa3',
                   stroke: '#000',
                   'stroke-width': 80,
                   'stroke-opacity': 0.5
                });
   paper
      .rect(circle.attr('cx') - 10, circle.attr('cy') - 10, 20, 20)
      .attr({
         fill: '#fff',
         stroke: 'none'
      });

   return svgContainer.innerHTML;
}

Just some minor changes, as you can see. We’re passing the data parameter and taking the x and y properties from it to define our rect and circle.

Now, let’s prepare the function that will run on every request to the server. It will parse the request’s payload, evaluate our drawing code in the context of the page and return the result to us.

var service = server.listen(port, function (request, response) {
   var drawerPayload = JSON.parse(request.post).data;
   page.open(url, function (status) {
      var svg = page.evaluate(svgDrawer, drawerPayload);

      response.statusCode = 200;
      response.write(svg);
      response.close();
   });
});

The last part is to check that everything has gone smoothly and to notify the user that we are ready to go:

if (service) {
   console.log('Web server running on port ' + port);
} else {
   console.log('Error: Could not create web server listening on port ' + port);
   phantom.exit();
}

Now we are ready to test our server! Run phantom server.js and create the test payload file, payload.json:

{
   "data": {
      "x": 700,
      "y": 490
   }
}

And we’ll use cURL to request our server:

curl -X POST -d @payload.json -H "Content-Type: application/json" localhost:9494

You should have received your custom SVG as a response. And the speed should have been drastically better:

0.00s user 0.00s system 30% cpu 0.026 total

All in all, evaluating code with PhantomJS is the most functional way to generate SVG on the server.

Highcharts, a popular charting library, has its own scripts15 that you can use to easily generate images for emailed reports and other purposes.

JSDOM

A curious mind might ask, “But why do we need to move an actual browser to the server? Can’t we just use Node.js to run our JavaScript drawer in the back end?”

We could. The main obstacle, though, is not only how to require a browser-specific library in Node.js, but how to make it run without the DOM implementation.

Raphael, Snap.svg and other solutions use the DOM API extensively to create an SVG document, append nodes to it and manipulate it in different ways. Node.js lacks this kind of API.

JSDOM16, then, is exactly what we are looking for: a JavaScript implementation of the DOM that can be used with Node.js.

Let’s convert our drawer to use it and see if any problems occur:

var jsdom = require('jsdom').jsdom;
var fs = require('fs');

var boilerplate = fs.readFileSync('index.html');

var doc = jsdom(boilerplate);
doc.implementation.addFeature(
  'http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1'
)

Here, we have called JSDOM with our boilerplate HTML (identical to the previous examples) and added SVG 1.1 to the list of supported features.

Now, let’s add our drawing code to the window.onload callback:

var window = doc.parentWindow;

window.onload = function() {
   window.Raphael.prototype.renderfix = function(){};
   var svgContainer = window.document.getElementById('svg');
   var paper = window.Raphael(svgContainer, 640, 480);
   paper
      .rect(0, 0, 640, 480, 10)
      .attr({
         fill: '#fff',
         stroke: 'none'
      });
   var circle = paper
                .circle(320, 240, 60)
                .attr({
                  fill: '#223fa3',
                  stroke: '#000',
                  'stroke-width': 80,
                  'stroke-opacity': 0.5
                });
   paper
      .rect(circle.attr('cx') - 10, circle.attr('cy') - 10, 20, 20)
      .attr({
         fill: '#fff',
         stroke: 'none'
      });
   console.log(svgContainer.innerHTML);
};

The drawing code remains untouched, except that we’ve prefixed all calls to the window-scoped variables with window and added one magic line:

window.Raphael.prototype.renderfix = function(){};

This is a hack because JSDOM does not support certain SVG APIs, so be aware of that. In our example, if we remove this line, we will get this:

TypeError: Object [ SVG ] has no method 'createSVGMatrix'

Here, we’ve overridden Raphael’s renderfix function, which uses createSVGMatrix, with an empty one. The solution is totally not production-ready, but it’s OK for our experiment.

Now we can run this example with node index.js. And what about speed?

0.49s user 0.06s system 89% cpu 0.612 total

So, using JSDOM is possible, but way more unstable and slower then PhantomJS for our purpose. But be aware of this approach, and treat it as an interesting experiment.

Svable

The final touch: Svable18. Its conversion API frees us from having to set up of any of the tools mentioned just above. Just send your SVG and you’ll get a PDF or PNG back:

curl -X POST -d @payload.json \
      -H "Content-Type: application/json" \  
      -H "Authorization: Bearer your-svable-token" \
      https://svable.com/api/convert > result.pdf

And here is the payload.json:

{
   "content": "<svg>…",
   "format": "pdf"
}

For more details on the API, please consult the documentation on Svable’s website19.

The second (and more interesting) part of this service is the functionality to generate SVGs. You can generate images without PhantomJS or any DOM emulation and without changing the drawing code.

Svable provides the API and special adapters for the most popular SVG-generation libraries. So, for our Raphaël example:

var Svable = require('svable');
var paper = Svable(0, 0, 640, 480, 'raphael');
paper
   .rect(0, 0, 640, 480, 10)
   .attr({
   fill: '#fff',
   stroke: 'none'
   });
var circle = paper
   .circle(320, 240, 60)
   .attr({
      fill: '#223fa3',
      stroke: '#000',
      'stroke-width': 80,
      'stroke-opacity': 0.5
   });
paper
   .rect(circle.attr('cx') - 10, circle.attr('cy') - 10, 20, 20)
   .attr({
      fill: '#fff',
      stroke: 'none'
   });

console.log(paper.burnSync());

This will return our SVG, and paper.burnSync({ format:"pdf" }) will return the converted PDF file.

This functionality is in the private beta now, so stay tuned to it.

Conclusion

It’s fair to say that, when it comes to generating SVG with JavaScript, we can no longer say, “It only works in the browser.”

A number of techniques will enable you to generate SVG on the server with the same code that you use in the browser, and resources and infrastructure are available for every type of visualization.

Start with the easiest one that satisfies your basic business needs, and then tune it to your ideal once you understand the drawbacks and bottlenecks.

Happy SVG’ing!

(al, il)

Footnotes

  1. 1 http://raphaeljs.com/
  2. 2 http://snapsvg.io/
  3. 3 http://www.svgjs.com/
  4. 4 http://g.raphaeljs.com/
  5. 5 http://www.highcharts.com/
  6. 6 http://d3js.org/
  7. 7 https://github.com/somebody32/generate-svg-on-the-server-with-js/tree/master/straightforward
  8. 8 http://www.smashingmagazine.com/wp-content/uploads/2014/05/result.svg
  9. 9 http://www.smashingmagazine.com/wp-content/uploads/2014/05/result.png
  10. 10 http://phantomjs.org/
  11. 11 http://phantomjs.org/download.html
  12. 12 https://github.com/somebody32/generate-svg-on-the-server-with-js/tree/master/phantomjs
  13. 13 https://github.com/ariya/phantomjs/wiki/API-Reference-WebServer
  14. 14 https://github.com/somebody32/generate-svg-on-the-server-with-js/tree/master/phantomjs-server
  15. 15 https://github.com/highslide-software/highcharts.com/tree/master/exporting-server/phantomjs
  16. 16 https://github.com/tmpvar/jsdom
  17. 17 https://github.com/somebody32/generate-svg-on-the-server-with-js/tree/master/jsdom
  18. 18 http://svable.com
  19. 19 http://svable.com

↑ Back to topShare on Twitter

Ilya is a full stack developer that loves meeting new people and motivating them to build awesome things together. Formerly working as a back-end engineer, he is now very passionate with front-end technologies and JS frameworks (especially Ember). He currently works as a Team Leader at ResumUP and has created the local Ember User Group in Saint Petersburg that is helping newcomers to find out how glorious Ember is.

Advertising
  1. 1

    Thanks a lot for this! Now I just have to understand how to make it work with d3.js, crossfilter.js and dc.js…

    3
  2. 2

    This is an interesting and useful article; thank you.

    But there’s an important alternative that’s been overlooked – templating. For years, web applications have relied on templating systems to dynamically generate pages on both server and client. SVG has been sidelined by the rise of client-side templating. Fundamentally, this is because there’s no such thing as element.innerSVG, unlike element.innerHTML (which is how HTML from traditional templating systems is turned into DOM).

    That’s changing because of tools like Ractive (disclaimer: I’m on the core team), Meteor, and the upcoming version of Ember, which can parse SVG just as easily as HTML. Ractive is completely isomorphic and can render dynamic SVG on the server without any PhantomJS or JSDOM hacks.

    I believe the days of generating SVG programmatically are numbered. For most purposes, intelligent live templating systems are just light-years ahead in terms of their ease of use, and they’re getting better every day.

    5
    • 3

      Thanks for such useful addition!

      The idea of this article was just to give readers some information about moving existing codebase to server without massive rewrite (assuming that they are already using more “conventional” technologies)

      Also, I’m totally agree that reactive mindset is a very big step forward in webdev and it will ease a lot of pain points, especially if we’re talking about efficiency. So, big thanks for your work on Ractive! I think that I’ll give it a try in my current project and post a results of migrating from Raphael to it.

      0
  3. 4

    I was working on a similar idea a few hours ago, nice :)

    0
  4. 5

    WFTU Son,

    SVG is Heaven in theory but Hell in application. It’s like marrying a SuperModel and expecting her to cook. No – worse than that…

    -1
    • 6

      steve nordquist

      May 28, 2014 3:30 am

      It is like dating a Latvian Soccer Pro and getting the blessing of Latvia (or other default DOM scope) before being seen doing advanced header practice with them. It’s soccer; you know they can work a sous vide.

      -2
    • 7

      That is an extremely offensive comment for all women. Please be more considerate in future.

      -7
  5. 8

    Thanks for the great post, covering many topics, I enjoyed reading it :-)

    0
  6. 9

    Paulita Zastawny

    May 27, 2014 3:08 pm

    I am glad that it turned out so properly and I hope it will continue in the future because it really is so worthwhile and meaningful towards the community.

    0
  7. 10

    Well done. SVG actually makes sense, I think.
    DMSwanson

    0
  8. 11

    Great article!

    If you’re using Raphael or snap, I’ve built a parser that makes it super fast to create the JavaScript code.

    Check it out at readysetraphael.com. Hopefully, it helps save you some time!

    Thanks!

    0
  9. 12

    [rant]
    str_replace( $title, ‘Server’, ‘node.js’);

    All node.js hype over and over again. Now, if someone would come up with a solution for “classic” web programming languagues like PHP or Perl – THAT would be interesting, spiffy’n’spanking.

    [/rant]

    cu, w0lf.

    -1

Leave a Comment

Yay! You've decided to leave a comment. That's fantastic! Please keep in mind that comments are moderated and rel="nofollow" is in use. So, please do not use a spammy keyword or a domain as your name, or else it will be deleted. Let's have a personal and meaningful conversation instead. Thanks for dropping by!

↑ Back to top