Skip to main content

Generating Images with JSPs and Servlets

April 22, 2004

{cs.r.title}








Contents
Parsing Input Parameters
Drawing Into a Buffer
Writing the Image
Troubleshooting
Dynamic Text
The FontRenderContext
Image Thumbnails
Thumbnail Examples
Summary

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.

Parsing Input Parameters

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

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.

Drawing Into a Buffer

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).

Writing the Image

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

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&amp;slice=60&amp;
    slice=90&amp;slice=180&amp;
    color=00ff00&amp;color=99ff99&amp;
    color=bbffbb&amp;color=ddffdd&amp;
    width=100&amp;height=100&amp;background=ffffff"/>
   
  </body>
</html>

Troubleshooting

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"

Dynamic Text

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).







The FontRenderContext

With the font loaded, we need to create a buffer to draw it in. We've got
a problem, though. We want the buffer to be the right size for the text, so
we need to know the how big the text is. However, there is no way to get
the size of the text without having a FontRenderContext to tell us. And
that context has to come from the Graphics2D object, which will come from our
buffer, which we haven't created yet! A bit of a chicken-and-egg problem.
The solution is to create a scratch buffer just to get the
FontRenderContext. Then we can call Font.getStringBounds() to get the size
of the text and create the real buffer. The code below does this:

BufferedImage buffer =
    new BufferedImage(1,1,BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = buffer.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
FontRenderContext fc = g2.getFontRenderContext();
Rectangle2D bounds = font.getStringBounds(text,fc);

// calculate the size of the text
int width = (int) bounds.getWidth();
int height = (int) bounds.getHeight();

// prepare some output
buffer = new BufferedImage(width, height,
                           BufferedImage.TYPE_INT_RGB);
g2 = buffer.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
g2.setFont(font);

Note that we set the rendering hint for antialiasing on both the
scratch buffer and the real one. This makes sure that the size of the
rendered text is the same on both.

Now that we've got our font completely set up, it's time to do some drawing:

// actually do the drawing
g2.setColor(background);
g2.fillRect(0,0,width,height);
g2.setColor(color);
g2.drawString(text,0,(int)-bounds.getY());

The code above draws the actual text over a colored background. The y
coordinate for the drawString method is
-bounds.getY(), because text is drawn with the origin at the
baseline (bottom) of the text rather than the upper left corner, as
rectangles and other drawing methods are.

With the drawing done, we can just write the image out as a PNG, as we
did before:

// set the content type and get the output stream
response.setContentType("image/png");
OutputStream os = response.getOutputStream();

// output the image as png
ImageIO.write(buffer, "png", os);
os.close();

Now we have the ability to make the images in Figures 3, 4, and 5:

Figure34
Figure 3. text.jsp?text=Big%20Apple&font-file=bigapple.ttf&size=40

Figure 4
Figure 4. text.jsp?text=Fifties%20Diner&font-file=diner.ttf&size=45

Figure 5
Figure 5. text.jsp?text=jcLdfu&font-file=60schic.ttf&size=50

Image Thumbnails

Our final example is a thumbnail generator. There are plenty of
programs, both graphical and command-line, that will generate thumbnails.
You can even script Photoshop to do it. The advantage of doing it on the
server is that we can scale our thumbnails automatically with no user intervention.
This by itself is great for novice users who just want to dump a bunch of
images into a directory and see a gallery. Whenever images are changed,
the thumbnails will be automatically updated. And since we are already loading and
scaling the images, it will be easy to put in extras such as
attractive frames around each image, and cache images to keep the server running
quickly.

Since this program is a bit more complicated, we'll write it as a servlet
instead of a JSP. This will help keep the code structured and get rid of the
white space problems. Since a great deal of this code is replicated from
the previous examples, we'll only delve into the new parts.

Each thumbnail is going to be composed of two images scaled together.
First is the photo image for which we want to make a thumbnail. The second is the
frame that contains the picture frame, usually a box of some sort with the
rest transparent. This is a bit complicated, so let's start with some
pictures (Figure 6 and 7):

Figure 6

Figure 6. The photo

Figure 7

Figure 7. The frame

The frame will be drawn on top of the image, as in Figure 8.

Figure 8

Figure 8. First composite

We can already see a problem. The frame doesn't extend to the edges of
the image, because it's non-rectangular and has a drop shadow. This lets the
photo image show through. In order to cut out the parts of the photo that
we don't want, we need a third image, called a mask (Figure 9). A mask
indicates what parts of another image are to be retained and what parts are
to be discarded. Here we just want the parts of the picture that will be
inside of the frame.

Figure 9

Figure 9. The mask

Now we can combine the mask with the photo to get Figure 10:

Figure 10

Figure 10. Photo + mask

and then draw the frame on top to get our final image: Figure 11.

Figure 11

Figure 11. Photo + mask + frame

This final image is what gets scaled down into a thumbnail.

Let's get started. First up is calculating the input parameters.

// the actual image file to load
File file = new File (
    request.getRealPath(request.getParameter("file")));
if(!file.exists()) {
    System.out.println("WARNING! the file " +
                       file + " doesn't exist!!!!");
}
String frame_path = request.getParameter("frame");
String mask_path = request.getParameter("mask");

In addition to the parameters we've used before (width, height, and background_color), we want the filename of the image, its frame, and its mask. Next we
create a file for the image in a temp directory that we use for caching. We've
included the requested width in the filename so that we can cache differently
sized thumbnails separately. If the cache file is missing or out of date,
we call generateImage() before writing the image to the output stream.

// this can be used to turn off caching
String cache = request.getParameter("cache");
if(cache == null) { cache = "true"; }

// the actual image file to load
File file = new File(
    request.getRealPath(request.getParameter("file")));
if(!file.exists()) {
    System.out.println("WARNING! the file " + file +
                       " doesn't exist!!!!");
}

// calculate the image type
// (we only support jpeg and png for output)
String type = "jpeg";
if(file.getName().toLowerCase().endsWith(".png")) {
    type = "png";
}
if(request.getParameter("type")!=null) {
    type = request.getParameter("type");
}

// init the cache directory
File temp = new File("c:/temp");
// load up the thumbnail
File thumbfile = new File(temp,
                          file.getName()+
                          ".w"+new_width+".thumb");

// only regenerate if the thumbnail is missing
// or out of date
if(!thumbfile.exists() ||
    (file.lastModified() > thumbfile.lastModified()) ||
    cache.equals("false")) {
    generateImage(file, thumbfile, type, new_width,
                  frame_path, mask_path,
                  background_color, request);
}

// actually write the image to the output stream
outputImage(thumbfile,type,response);

With all of that prep work out of the way, we can implement the actual
drawing code in generateImage():

// load the image
Image image = new ImageIcon(file.toString()).getImage();
double w = image.getWidth(null);
double h = image.getHeight(null);

// if no explicit width then set the width
// to the real image size
double nw = new_width;
if(new_width == -1) {
    nw = w;
}
// calculate the scaling factor
double scalex = nw/w;

The first steps above are to load the photo image, get its dimensions,
and calculate the scaling factor. If no target width was set, then we use
the original width of the photo. This would result in a scale of 1 for no
change in size.

Next we load the frame and mask, if specified, and calculate the scaling
factor to make the frame the same size as the photo.

// load the mask
Image mask = null;
double mask_scale = 1;
if(mask_path != null) {
    mask =
     new ImageIcon(request.getRealPath(mask_path)).getImage();
    mask_scale = nw/mask.getWidth(null);
}

// load the frame
Image frame = null;
double frame_scale = 1;
if(frame_path != null) {
    frame = new ImageIcon(
        request.getRealPath(frame_path)).getImage();
    frame_scale = nw/frame.getWidth(null);
}

Now we want to create the buffer into which to draw these images. Since the
frame controls the size of the final image, we'll use that for the
buffer dimensions, as shown below:

// base the height off of the frame, if there is a frame
double nh = 0;
if(frame != null) {
    nh = frame_scale*frame.getHeight(null);
} else {
    nh = scalex*image.getHeight(null);
}

// create a temporary image
BufferedImage bufimg =
    new BufferedImage ((int)nw,
                       (int)nh,
                       BufferedImage.TYPE_INT_ARGB);
Graphics2D g = bufimg.createGraphics();

With the buffer in hand, we're ready to draw: first the photo, then
the mask, and finally, the frame.

// draw the image scaled
g.setComposite(AlphaComposite.Src);
AffineTransform aft =
    AffineTransform.getScaleInstance(scalex,
                                     scalex);
g.drawImage(image,aft,null);

// draw the mask scaled
if(mask != null) {
    g.setComposite(AlphaComposite.DstIn);
    AffineTransform mask_aft =
        AffineTransform.getScaleInstance(mask_scale,
                                         mask_scale);
    g.drawImage(mask,mask_aft,null);
}

// draw the frame scaled
if(frame != null) {
    g.setComposite(AlphaComposite.SrcOver);
    AffineTransform frame_aft =
        AffineTransform.getScaleInstance(frame_scale,
                                         frame_scale);
    g.drawImage(frame,frame_aft,null);
}

The important things to notice in the code above are the
g.setComposite calls. These set the
Porter-Duff
method to determine how the two images are composited. We are used to
thinking of a second image simply drawn on top of a first, but there
are many of other ways to do it. The first method above,
AlphaComposite.Src, just draws the source image into the
buffer, with no compositing at all. The second method,
AlphaComposite.DstIn, draws the part of the destination
(what's already in the buffer) that's inside of the source (our mask).
This means everywhere the mask is solid, we will see what's already in the
buffer. Whatever is transparent in the mask will be transparent after
compositing. The third composite, for the frame, uses SrcOver,
which draws the source (the frame) over the destination (the result of the
previous composite). This article at IBM has a very detailed explanation of Porter-Duff compositing.

Now that we're done compositing, we want to draw the whole thing on top of a
solid background (if requested) in the final image. This image could be partially
transparent if the images don't cover the whole buffer, which would be useful
for drop shadows that blend with the rest of the web page. However, JPEG
doesn't support transparency, so we'll only create a transparent buffered image
when the type is set to PNG.

// create a background image; we can't use
// transparency if this is a jpeg
BufferedImage background_img = null;
if(type.equals("jpeg")) {
    background_img = new BufferedImage((int)nw,
                          (int)nh,
                          BufferedImage.TYPE_INT_RGB);
} else {
    background_img = new BufferedImage((int)nw,
                          (int)nh,
                          BufferedImage.TYPE_INT_ARGB);
}
Graphics2D back_g = background_img.createGraphics();

// draw the background color, if there is one
if(background_color != null) {
    back_g.setColor(background_color);
    back_g.setComposite(AlphaComposite.Src);
    back_g.fillRect(0,0,(int)nw,(int)nh);
}

// put the foreground on to the background
back_g.setComposite(AlphaComposite.SrcOver);
back_g.drawImage(bufimg,0,0,null);

With the final image created, we can write out to the thumbnail file
as before. We must be sure to close the file before quitting so that the
thumbnail will be ready for the outputImage function:

// open the thumbnail file
FileOutputStream os = new FileOutputStream(thumbfile);

// save the final image
if(type.equals("jpeg")) {
    JPEGImageEncoder enc =
        JPEGCodec.createJPEGEncoder(os);
    enc.encode(background_img);
}
if(type.equals("png")) {
    // output the image as png
    ImageIO.write(background_img, "png", os);
}

// cleanup
os.close();

The outputImage() function loads the file and copies the bytes
to the output stream, setting the MIME type first.

public void outputImage(File thumbfile, String type, 
    HttpServletResponse response) throws IOException {
    response.setContentType("image/"+type);
    OutputStream os = response.getOutputStream();
    FileInputStream fin = new FileInputStream(thumbfile);

    byte[] buf = new byte[4096];
    int count = 0;
    while(true) {
        int n = fin.read(buf);
        if(n == -1) {
            break;
        }
        count = count + n;
        os.write(buf,0,n);
    }
    os.flush();
    os.close();
    fin.close();
}

Thumbnail Examples

Now we have a servlet that can generate (and cache) scaled versions of
photos with custom frames. Having custom frames is especially nice for
non-traditional users, who might like all sorts of wacky frames for their
photos. I've included a few examples below, as Figures 12-14:

Figure 12

Figure 12. Frame for a photographer's site

Figure 13

Figure 13. Photo with watermark for a clip art site

Figure 14

Figure 14. Silly children's frame

Summary

As we've seen, servlets, JSPs, and Java2D make it very easy
to create robust server-side image applications. These three examples just
touch on the possibilities. From here we could go on to advanced charting (like
the stock statistics on Yahoo! Finance),
rasterization of SVG for non-vector-capable browsers, or even image analysis for medical researchers. You can download the code for this article or find the code along with more examples at my site, code.joshy.org.
Try it out and see what other interesting uses you can come up with.

Josh Marinacci first tried Java in 1995 at the request of his favorite TA and has never looked back.
Related Topics >> JSP   |   Servlets   |