Over the past few months I've been using the Closure Library to create my own JavaScript library that carries out some graphical processing. This is done entirely in JavaScript, using Closure's graphics package.
An important factor in the development of this library was compatibility. I wanted assurance that, no matter what browser the user hit the page from, the graphic would be displayed. The Library's graphics package is already very good at this; determining the browser from the User-Agent header and then deciding whether to produce an SVG, VML or Canvas drawing context. However, some devices either don't support any of these drawing methods, or cannot process the JavaScript involved.
The solution to this problem was to move the processing of the JavaScript and rendering of any graphics to the server. Should the client attempt to access the site from an unsupported browser, the page could then fetch an image representation of the graphic.
This blog article will explain the changes I had to make to the Library to make this possible, as well as a brief look into the Servlet handling the processing of the JavaScript.
At the Server
The purpose of the Servlet was to process the JavaScript on the page, and produce an image of the interactive element. To do this, it had to provide access to a CanvasRenderingContext2D object which in turn provided drawing functionality.
CanvasRenderingContext2D
CanvasRenderingContext2D had to provide the drawing methods that the HTML5 canvas implementation provides, and then convert them into the equivalent Graphics2D drawing methods. The aim was to create the following transition:
The emphasis here is that the function calls in the JavaScript code haven't changed; the only difference is the ctx reference.
The context also had to provide a function to get the image of the Graphics2D object. The Servlet could then return this once it had finished processing and rendering.
Server-side JavaScript
Rhino
Rhino is a package developed by the Mozilla Foundation that allows the execution of JavaScript in a Java environment. Rhino was at the heart of the Servlet, allowing me to move any JavaScript processing from the client (which may not be capable), to the server.
It also allowed me to inject references into the JavaScript code, so that an instance of CanvasRenderingContext2D may be accessed.
Envjs
Envjs is a browser environment written in JavaScript, which is used in many server-side Java applications. The reasoning behind using Envjs in my implementation was two-fold:
- Provide a browser environment for the DOM references in the libraries
- Provide a User-Agent field to determine when the JavaScript is running on a server
Processing the JavaScript
Rhino provides static methods to process JavaScript source from a file, which is the equivalent of a browser reading a script tag. It's done like this:
Processing the JavaScript in this order ensures that when I run compiled.js, which contains the library sources, Envjs has finished processing and has implemented a browser environment.
Talking to the Java from the JavaScript
Rhino allowed me to inject references to the Java into the JavaScript through global variables. This was done like so:
This puts a variable on the Global object, called adapter, which references 'this' ('this' being the Servlet object itself), allowing me to access the Java methods on the Servlet from the JavaScript. I then supplied methods to get the CanvasRenderingContext2D like so:
And then in the JavaScript:
Rhino also provides a convenient way of accessing properties of JavaBeans, so we could actually write:
ctx was then a reference to the Java object which provided the drawing methods. This meant that any function calls on ctx would equate to method calls on an instance of CanvasRenderingContext2D.
Canvas2D and Graphics2D
The next step was to extend AbstractGraphics in a way that would allow me to draw to a Server. I was aiming to implement SeverGraphics in the following way:
CanvasGraphics and ServerGraphics
I recognised that the graphics mode of Canvas2D and Java's Graphics2D were both immediate, making CanvasGraphics an ideal place to start. I could then create the same interface in CanvasRenderingContext2D, to bridge the gap between the JavaScript and Graphics2D.
This is done in the getContext function of CanvasGraphics, which is the only place I need to change. Once I modify getContext to return a reference to my Java object (which, remember, provides the same functions as a Canvas context), all function calls will go directly to the Java code. I changed getContext to read the following (similar to the example above):
Provided the methods I implement maintain the logic of the Canvas methods, I can safely change this reference and rely on the existing logic of CanvasGraphics to ensure that the correct behaviour will occur.
The Problem of Text
To say CanvasGraphics doesn't rely on the DOM and uses JavaScript functions entirely isn't strictly true. Although the newer version of the Canvas supports it, Closure's implementation doesn't use the canvas object to draw text; it uses the DOM.
CanvasTextElement
This is all done using a separate object in the Closure Library, called CanvasTextElement. The purpose of this object is to create a DIV element on the DOM with the text as the inner HTML, and then style the DIV based on the parameters to the element. This wouldn't work on the server, as there is no browser to display the text.
ServerTextElement
I created a ServerTextElement, which is almost identical to CanvasTextElement save the following:
- The constructor of the element doesn't create a DOM element
- There is no updating of Styles - just one draw function
The draw function makes a call to the context and performs its drawing processing as if it were any other element. A simple implementation of this method would be as follows:
.. but that's not the best solution. The Canvas API does provide methods to draw text, so we should use those:
This allows us to create a patch for CanvasGraphics, updating it to use the Canvas element to draw text, rather than the DOM.
Measuring Text
Graphics2D also provides a TextMetrics object to get the measured width of a String in pixels; something which the Closure Library is lacking.
Similarly, measureText is specified by the Canvas2D API, so was implemented in my Java adapter. I could then add this functionality to ServerGraphics like so:
User Agents
The final step was to configure Closure Library to recognise when it was running in a Server environment, and to react in an appropriate way.
Closure Library has its own user agent analysis; it determines which browser the viewer is using and their platform, using the User-Agent HTTP header. For example, if you're using Google Chrome, your User-Agent header may be something like this:
Closure will then pick up on the key word 'WebKit', and flag that you're running a WebKit browser. Other aspects of the Library code can then use this information. For example, a basic implementation of the createGraphics function in the graphics package would look something like this:
Creating and Using the User Agent
The user agent module of the Closure Library had to be extended so that it could recognise when it was being executed from a server environment. This was done by analysing the User-Agent header when ran through Rhino, which is the following:
Using the keyword 'Rhino', we can assert that we are running in a server. The first step to incorporating this information into the Library was to mimic the behaviour for other user agents in useragent.js, but for Rhino. This involved appending the following to the check in useragent.js:
The second step was to incorporate the new Rhino user agent defined in the first step, into the createGraphics function, so that a ServerGraphics object will be created. I extended Closure's original code by adding the following statement in the above conditional statement:
Support for the NodeList Interface
There was one additional problem I experienced when running the Library on the server.
I was trying to convert a Java XML Document (org.w3c.dom.Document) into a JavaScript Document object, using XMLDataSource, which has one difference; the way the JavaScript NodeList interface retrieves Nodes by index.
.. the solution was simple:
The problem only occurred in XMLDataSource, and was simple to fix, making it an ideal patch submission!
Conclusion
I was very surprised at how well the Closure Library ported over to a server environment. With the exception of some DOM methods, which were easily solved using Envjs, and the NodeList interface, there were no problems in moving the Library to the server.
Server-side JavaScript is a very interesting topic; one that I feel will become more prevalent in the future. To know that Google's Closure Library is well on its way to being fully compliant with standards to the point it can be executed on a server, is a very good sign.