|
|
|||||||||||||||||
by Joshua Marinacci | |||||||||||||||||
| ||||||||||
In the old days of web design, we used strange server-side hacks to create sophisticated effects. The first time I saw animated graphics on the Web was the Batman Forever web site in 1995. The folks at Netscape had just implemented a way of streaming animation using a custom program on the server. It was an amazing effect, but difficult to implement and maintain. Fortunately, a decade of advances such as CSS have made most server image hacks unnecessary. Still, there are a few interesting uses.
Server-side image generation gives us the ability to do two things: draw something that is impractical or impossible to do on the client side with HTML and JavaScript, like use a custom font or tables with non-rectangular edges; and draw on the fly to show information that is timely or specific to the current user. Both of these uses are especially powerful on the Java platform, where we have a plethora of readily available tools for image manipulation. In this article, we will explore how to generate images from JSPs and servlets with three examples: a pie chart, rendering text in a custom font, and thumbnail images with composited frames.
Our first example is a pie chart, written as a JSP to keep the code small. It's a simple graphic that's easy to draw, but can't be done with just CSS and HTML. A pie chart is basically a set of pie slices, each with an angle and a color, so we will start by collecting the input parameters. We want our finished pie to look like Figure 1:

Figure 1. 60/300 degree pie chart
The request to our JSP will look like this:
http://server.com/piechart.jsp?slice=60&slice=300&
color=ff0000&color=0000ff&width=100&height=100&
background=ffffff
You'll notice above that there is one slice and one color parameter per pie
slice. Then we have a height and width to draw the actual pie. And finally,
a background to fill in the space between the pie circle and the
rectangular image edges. This is the code to parse the parameters:
String[] slices = request.
getParameterValues("slice");
String[] colors = request.
getParameterValues("color");
int[] sizes = new int[slices.length];
Color[] cols = new Color[slices.length];
for(int i=0; i<slices.length; i++) {
u.p("sizes = " + slices[i]);
u.p("colors = " + colors[i]);
sizes[i] = Integer.parseInt(slices[i]);
cols[i] = new Color(Integer.
parseInt(colors[i],16));
}
int width = Integer.parseInt(request.
getParameter("width"));
int height = Integer.parseInt(request.
getParameter("height"));
Color background = new Color(Integer.parseInt(
request.getParameter("background"),16));
First we get the slice and color parameters. There should be more than one, so
we have to use request.getParameterValues(). Then we create an
array to hold them and convert the string values into integers and colors.
Notice that the color conversion uses the Integer.parseInt()
function with a radix of 16, meaning the values must be in
hexadecimal. A production version could handle named colors such as "blue" or
"teal," as well as hex values. Finally, the code above parses the width,
height, and background parameters.
With the inputs set up, we are ready to actually draw our pie chart. But
where? This is the first place where server-side images differ from those on the client
side. In a normal Swing application, each component already has an image
buffer with a Graphics on it. We just implement the paint method and start
drawing. On the server, however, we need to manually create a place to
draw: the BufferedImage.
A BufferedImage is simply an Image backed by a chunk of memory. After
we draw on the image, we can then write it out using the javax.imageio APIs.
BufferedImage buffer =
new BufferedImage(width,
height,
BufferedImage.TYPE_INT_RGB);
Graphics g = buffer.createGraphics();
g.setColor(background);
g.fillRect(0,0,width,height);
int arc = 0;
for(int i=0; i<sizes.length; i++) {
g.setColor(cols[i]);
g.fillArc(0,0,width,height,arc,sizes[i]);
arc += sizes[i];
}
This code first creates a buffered image with the requested width and height.
Since we don't care about transparency, TYPE_INT_RGB is the best option.
BufferedImage actually supports quite a large number of image types, but most of the
time, you will only need RGB or ARGB (RGB with transparency). Next, we fill
the image completely with the background color, and then draw each section
of the arc, using the size parameters from the URL (specified in degrees).
Now that we have a BufferedImage created and drawn, the last step is to write it
out to an actual image file that the web browser can see. This usually means a GIF, JPEG, or
PNG file. Due to copyright problems, the standard JDK does not include the ability
to write GIF files. JPEG is lossy and better for photos, so that leaves us with PNG.
In older versions of the JDK, writing out an image required using extra libraries
or undocumented Sun classes. As of 1.4, however, the situation has vastly improved.
The imageio package provides an API for pluggable image encoders and decoders, including
ones for reading and writing PNG files. All we have to do is give the API a place to write
the bytes. It looks like this:
response.setContentType("image/png");
OutputStream os = response.getOutputStream();
ImageIO.write(buffer, "png", os);
os.close();
The first two lines above set the content type to
image/png, which is the appropriate MIME type for PNG images,
and get the output stream from the HTTP response object. The next line does
the magic; using the static write method in the
java.imageio.ImageIO class, we pass in the image
(buffer), the desired image format ("png"), and
the output stream to write to (os). Close the output stream, and we're done! If
you drop this into an app server and call with something like this URL (all on one line):
piechart.jsp?slice=30&slice=60&slice=90&slice=180&
color=00ff00&color=99ff99&color=bbffbb&color=ddffdd&
width=100&height=100&background=ffffff
you should see a pie chart like Figure 2:

Figure 2. A 30/60/90/180 degree pie chart
This JSP produces only the image itself, and of course
normally we wouldn't type that long URL
into the browser. Instead, we would create a web page to
link to the JSP as an image. This has the disadvantage of regenerating
the image on every page load, but we'll get to disk caching later. Here's
what a simple web page would look like (again, the
src attribute is all on one line):
<html>
<body>
<h3>A simple piechart</h3>
<img src="piechart.jsp?slice=30&slice=60&
slice=90&slice=180&
color=00ff00&color=99ff99&
color=bbffbb&color=ddffdd&
width=100&height=100&background=ffffff"/>
</body>
</html>
When creating images on the server side, programmers typically hit at least one of two problems. Either the image is output with the wrong MIME type, or the server throws exceptions.
The MIME type problem results from the JSP deciding to set the MIME type to
the default (usually text/html), even though you manually set it to
something else. This varies by app server, but a common thing to look for is
non-code white space near the top of your document. The JSP server will see
the white space and try to output it, using the default MIME type (since you haven't
set one yet).
The code below might output as text/html
<%@ page import="java.io.*" %>
<%@ page import="java.awt.*"%>
<%
// java code
Whereas this code removes the extra white space
<%@ page import="java.io.*"
%><%@ page import="java.awt.*"
%><%
// java code
and should output properly.
MIME type problems also can happen if you call request.getOutputStream() before
request.setMimeType(), so be sure to set the MIME type as early as possible.
The exceptions are caused by a different problem; if you are developing this on Windows or Mac OS X, you probably won't see it, but if you use Linux (in particular, if you're running your code on a remote server), you probably will get an error like the one below. This is Java's way of saying it can't load up the AWT (Abstract Windowing Toolkit) subsystem. The reason goes back to the lowest levels of Java and the early days of AWT.
org.apache.jasper.JasperException: Can't connect to X11
window server using ':0.0' as the value of the DISPLAY variable.
at org.apache.jasper.servlet.JspServletWrapper.
service(JspServletWrapper.java:254)
at org.apache.jasper.servlet.JspServlet.
serviceJspFile(JspServlet.java:295)
...
AWT was originally built to run on slow processors and inside of web browsers. To accomplish this, it depends on the native GUI, which for most Unix-based operating systems means X-Windows. If you don't have X running, then you can't run AWT. Since we aren't writing a GUI app, we don't need AWT, but the minute you try to load up an image it will initialize the entire AWT library. If this is running on a headless server, X won't be there, and AWT will fail, causing your image load to fail. In the old days, people would run special headless X servers, such as xvfb, the X Virtual Frame Buffer, to trick their image code into running. Fortunately, the last few releases of the JDK have an option to put the JVM into headless mode, eliminating the need for resource intensive hacks.
To use headless mode, just add the parameter -Djava.awt.headless=true to your JVM when you start it. For Tomcat, this means editing the catalina.sh file and putting
a line like this near the top:
JAVA_OPTS="-Djava.awt.headless=true"
Another good use for dynamic images is, ironically, displaying text. If you want a header to appear in a particular decorative font that most users are unlikely to have installed, then you can either degrade to a different font or generate an image in Photoshop. However, if the text changes (as in, say, the titles of weblog articles that are updated constantly), then updating it in Photoshop every day would quickly become impractical. Instead, we would like to generate the text on the fly with code and just link to the image with the text as a parameter. This system would also be useful for displaying mathematical symbols or characters in other languages, or any other situation where the desired font probably isn't installed on the end user's computer.
To build a program that renders text in images like Photoshop does, we will take advantage of two key features of Java. First, Java supports anti-aliased rendering of text, just like Photoshop. Second, all JVMs are required to support loading TrueType fonts on the fly. This means we don't need the fonts pre-installed into the environment. If the TTF file is on disk (or inside of a JAR) we can load and render it. It almost sounds too easy!
The top half of text.jsp
// configure all of the parameters
String text = "ABC abc XYZ xyz";
if(request.getParameter("text") != null) {
text = request.getParameter("text");
}
String font_file = "dungeon.ttf";
if(request.getParameter("font-file") != null) {
font_file = request.getParameter("font-file");
}
font_file = request.getRealPath(font_file);
float size = 20.0f;
if(request.getParameter("size") != null) {
size = Float.parseFloat(request.getParameter("size"));
}
Color background = Color.white;
if(request.getParameter("background") != null) {
background = new Color(Integer.parseInt(
request.getParameter("background"),16));
}
Color color = Color.black;
if(request.getParameter("color") != null) {
color = new Color(Integer.parseInt(
request.getParameter("color"),16));
}
The code above is pretty much boilerplate, just collecting the input parameters. The text, font file, font size, background, and foreground colors are pulled from the request, providing reasonable defaults when a parameter is missing. All configuration is passed in on the URL line, so you don't have to reconfigure the app server to support new fonts or text.
Next we need to prepare the font:
Font font = Font.createFont(Font.TRUETYPE_FONT,
new FileInputStream(font_file));
font = font.deriveFont(size);
This code loads a custom font from a file. The first argument is the
TRUETYPE_FONT constant, telling the loader that this font is
in TrueType format, the only type guaranteed to be supported. The second
argument is the input stream to load from (in this case, a file on disk).
The font is created in memory with a point size of 1, so we have to derive
a new font at the proper size (the default we set above is 20 points).
Pages: 1, 2 |
View all java.net Articles.
|
|