Server Side SVG
February 27, 2002
If you've been using SVG or reading XML.com, you probably know about the Adobe SVG Viewer, and you may have heard of the Apache Batik project. Although Batik is most widely known for its SVG viewer component, it's more than that. Batik, according to its web site, is a “Java technology-based toolkit for applications that want to use images in the Scalable Vector Graphics (SVG) format for various purposes, such as viewing, generation or manipulation.”
The Batik viewer application uses the JSVGCanvas
component, which is a Java
class that accepts SVG as input and displays it on screen. In this article, we'll
use two of
the other Batik components, SVGGraphics2D
, and the Batik transcoders. The
SVGGraphics2D
class is the inverse of JSVGCanvas
; you draw into
an SVGGraphics2D
environment using the standard Java two-dimensional graphics
methods, and the result is an SVG document. The transcoders take an SVG document as
input
and produce either JPG or PNG as output.
The context in which we'll use these tools is a servlet that generates geometric art in the style of Piet Mondrian. If the client supports SVG, the servlet will return an SVG document. Otherwise, it will return a JPEG or PNG image, depending upon client support for those image formats.
The Client Side
The web page that we show users will let them choose the orientation and color scheme of their painting. See the screenshot below for an example of how this might look.
Here's the HTML:
<html> <head> <title>Art-O-Matic</title> </head> <body> <h2>Art-O-Matic</h2> <p> Yes, you too can become a famous artist! With a few simple clicks of the mouse, you can generate geometric art in the style of Piet Mondrian. </p> <form id="artForm" action="http://jde:8080/artmaker/servlet/ArtMaker"> <p> What kind of picture would you like? <br /> <input type="radio" name="picType" value="landscape" checked="checked" /> Landscape <input type="radio" name="picType" value="portrait" /> Portrait <input type="radio" name="picType" value="square" /> Square </p> <p> Which color scheme would you like? <br /> <input type="radio" name="scheme" value="bright" checked="checked" /> Vivid colors <input type="radio" name="scheme" value="pastel" /> Soft pastel </p> </body> </html>
Determining whether the client has SVG support or not must be handled on the client side. We'll add this script, taken from a Sun Microsystems technical note, to do this detection.
<script type="text/javascript"> <!-- <![CDATA[ var hasSVGSupport = false; // does client have SVG support? var useVBMethod = false; // use VBScript or JavaScript? /* Internet Explorer returns 0 as the number of MIME types, so this code will not be executed by it. This is our indication to use VBScript to detect SVG support. */ if (navigator.mimeTypes != null && navigator.mimeTypes.length > 0) { if (navigator.mimeTypes["image/svg+xml"] != null) { hasSVGSupport = true; } } else { useVBMethod = true; } // ]]> --> </script> <!-- Visual Basic Script to detect support of Adobe SVG plugin. This code is not run on browsers which report they have MIME types, and it is also not run by browsers which do not have VBScript support. --> <script type="text/vbscript"> On Error Resume Next If useVBMethod = true Then hasSVGSupport = IsObject(CreateObject("Adobe.SVGCtl")) End If </script>
In order to have the web page send this information back to the server, we'll have
to
change the <form>
tag and add a hidden field to our form:
<form id="artForm" action="http://jde:8080/artmaker/servlet/ArtMaker" onsubmit="return setSVGStatus();"> <input type="hidden" name="imgType" value="jpg" />
Which requires an additional script to set the hidden field before we send the data to the server:
<script type="text/javascript"> <!-- <![CDATA[ function setSVGStatus() { if (hasSVGSupport) { var theForm = document.getElementById("artForm"); theForm.imgType.value = "svg"; } return true; } // ]]> --> </script>
You may see the source for the entire HTML file.
The Server Side
First, we need Java code to draw the actual painting into a Graphics2D
context. You may see the Artist.java
source. There's nothing particularly special about it; it's just a few
draw
and fill
calls in a recursive function. The only thing to
note is the constructor, which requires a width, height, and palette (which will be
BRIGHT
or PASTEL
):
public Artist( int width, int height, int colorType );
Now, on to the servlet. We'll do all the work in the doPost
method. We start
by sending out header information to keep the results from being cached:
public class ArtMaker extends HttpServlet { public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Artist mondrian = null; try { response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); response.setHeader("Cache-Control", "post-check=0, pre-check=0"); String agent = request.getHeader("User-Agent").toLowerCase(); /* netscape chokes on Pragma no-cache so only send it to explorer */ if (agent.indexOf("explorer") > -1){ response.setHeader("Pragma", "no-cache"); } response.setHeader("Expires", "Thu, 01 Dec 1994 16:00:00 GMT");
We then grab the parameters, use them to set some local variables, and create an
appropriate Artist
.
String picType = request.getParameter( "picType" ); String colorScheme = request.getParameter( "scheme" ); String imgType = request.getParameter( "imgType" ); Artist mondrian = null; int width; int height; int palette; if (picType.equals("landscape")) { width = 400; height = 300; } else if (picType.equals("portrait")) { width = 200; height = 300; } else { width = 250; height = 250; } palette = (colorScheme.equals("bright")) ? Artist.BRIGHT : Artist.PASTEL; mondrian = new Artist( width, height, palette );
Once this is set, we need to create an SVG document and an SVG graphics environment.
Since
every parser implementation has its own way of storing the objects, you must ask the
SVGDOMImplementation
class to give you the details. Once you know the
details, you can ask the implementation to create a document with the appropriate
namespace,
which is in svgNS
. The third argument to createDocument
is a
reference to a list of entities defined for the document. There aren't any in this
case, so
we set the third argument to null
.
DOMImplementation domImpl = SVGDOMImplementation.getDOMImplementation(); String svgNS = SVGDOMImplementation.SVG_NAMESPACE_URI; Document document = domImpl.createDocument(svgNS, "svg", null);
We then construct the SVGGraphics2D
object and associate it with the document
that it will create. We set the graphic environment's dimensions to the desired height
and
width with ten extra pixels for padding around the edges.
SVGGraphics2D svgGraphicsEnvironment = new SVGGraphics2D(document); svgGraphicsEnvironment.setSVGCanvasSize( new Dimension( width + 10, height + 10 ) );
Related Reading |
Everything is now set up. We just tell our artist to paint into that graphic environment, and the corresponding SVG document will be built up automatically.
mondrian.paint( svgGraphicsEnvironment );
Now the document is in memory, but we will need it in text form to send back to the
client.
We accomplish this by streaming the document to a ByteArrayOutputStream
, whose
initial size is 8192 bytes, which should be enough for most normal paintings that
this code
will generate. The second parameter to stream
is set to true
,
indicating that the output should use CSS styles instead of style attributes.
ByteArrayOutputStream baos = new ByteArrayOutputStream( 8192 ); Writer svgOutput = new OutputStreamWriter( baos, "UTF-8" ); svgGraphicsEnvironment.stream( svgOutput, true );
We now determine whether we need to send back an SVG, JPEG, or PNG image. The client
has
told us whether we can use SVG or not. The information about PNG support is in the
HTTP
request's Accept
header. In each case, we take the SVG output byte array,
convert it to a string, and pass it on to the appropriate emit
function.
if (imgType.equals("svg")) { emitSVG( request, response, baos.toString() ); } else { if (request.getHeader("Accept").indexOf("png") >= 0) { emitPNG( request, response, baos.toString() ); } else { emitJPG( request, response, baos.toString() ); } }
The remainder of the doPost
method is the error trapping; in case anything
fails, we send back an HTML page with an error message.
catch (Exception e) { PrintWriter out = response.getWriter(); response.setContentType("text/html"); out.println("<?xml version="1.0" encoding="utf-8"?>"); out.println("<!DOCTYPE"); out.println("html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\""); out.println("\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">"); out.println("<html><head><title>Error</title></head>"); out.println("<body>"); out.println("<p>Unable to create painting.</p>"); out.println("<pre>"); e.printStackTrace(out); out.println("</pre>"); out.println("</body></html>"); }
This leaves the three emit...
methods. emitSVG
is trivial; we
just send the string back to the client with an appropriate MIME type:
public void emitSVG( HttpServletRequest request, HttpServletResponse response, String svgString ) { response.setContentType( "image/svg+xml" ); try { response.getWriter().write( svgString ); response.getWriter().flush(); } catch (Exception e) { e.printStackTrace(); } }
Transcoding
To send back a JPEG or PNG file, we have to use Batik's transcoder. Input to the transcoder
can be a document, a stream, or a Reader
; output can be a URI, stream, or
Writer
. For the JPEG transcoder, you may set the output quality by calling
the addTranscodingHint
method. Since we're sending a series of bytes back to
the client rather than a string, we're sending output to
response.getOutputStream()
rather than response.getWriter()
.
public void emitJPG( HttpServletRequest request, HttpServletResponse response, String svgString ) { response.setContentType("image/jpeg"); JPEGTranscoder t = new JPEGTranscoder(); t.addTranscodingHint(JPEGTranscoder.KEY_QUALITY, new Float(.8)); TranscoderInput input = new TranscoderInput( new StringReader(svgString) ); try { TranscoderOutput output = new TranscoderOutput(response.getOutputStream()); t.transcode(input, output); response.getOutputStream().close(); } catch (Exception e) { e.printStackTrace(); } }
Similar code sends PNG output; you may give it an additional hint with
KEY_FORCE_TRANSPARENT_WHITE
if you want fully transparent pixels to appear as
white. This is good for older browsers that don't support PNG transparency completely.
public void emitPNG ( HttpServletRequest request, HttpServletResponse response, String svgString ) { response.setContentType("image/png"); PNGTranscoder t = new PNGTranscoder(); TranscoderInput input = new TranscoderInput( new StringReader(svgString) ); try { TranscoderOutput output = new TranscoderOutput(response.getOutputStream()); t.transcode(input, output); response.getOutputStream().close(); } catch (Exception e) { e.printStackTrace(); } }
You may examine the entire servlet as one
file. If you install Artist
and Artmaker
in a suitable
servlet container such as Jakarta Tomcat, you too
can produce art like this, which I've shrunk to half size to take up less screen space.
Summary
Other Batik tools which we haven't covered here let you convert TrueType fonts to SVG and extend SVG to include custom tags. If you want a powerful, cross-platform toolkit for building applications that let you view, construct, and convert SVG documents, Batik is the answer.