Create dynamic images in Java servlets

Dynamically convert users' requests into an image

Many Websites now use charts and graphs to represent some type of numeric information. In some cases, the chart or graph image is best constructed in realtime. Two reasons could motivate you to build an image dynamically: the data arrives continuously, which lends itself to dynamic image construction; or the data's volume is very large, and dynamic construction dramatically reduces storage. Of course, not every image is a candidate for dynamic construction; it's just another tool available in the Web developer's toolbox.

Image I/O packages, JDK-level requirements

If your servlet is to generate images on the fly, you first need to do image I/O (input/output). Specifically, you need to generate an image in response to an HTTP request. The core Java API provides no direct facility for saving images of any type. However, you can use Sun's Java 1.1 class library to do image I/O. Also, Sun's 1.2 image I/O offering includes a proprietary package for encoding and decoding JPEG images. Since this code is in a com.sun package, it is not part of the core API and is not a standard extension; therefore, its dependent code is not portable. The good news is that the approved Java Specification Request, JSR-000015, for a standard Java 2 extension, provides a framework for performing image I/O. This adds support to the Java 2 platform for image I/O and provides a mechanism to write portable code capable of dynamic image I/O. For this article, I create two examples, one using the Java 1.1 image I/O framework and the other using Java 1.2 code.

Image formats

The most common image format on the Web is GIF. GIF is widely supported by browsers, even old ones. Unfortunately, for writing image-generation code, GIF is potentially encumbered legally by a patent on its compression scheme. The two examples in this article create a JPEG and a PNG image. I will generate a JPEG image primarily because Sun's Java 1.2 code lets me generate this type of image. One difference between the GIF format and the JPEG format is that the GIF-compression algorithm is lossless, meaning that when the image data is subjected to a compress/decompress cycle the resulting data is identical to the original data (no information is lost); the JPEG algorithm is not lossless. Usually, the image degradation is not visible for the type of image you are generating. The second example generates a PNG image, which uses a lossless compression algorithm free of legal issues.

Image servlet design

For this article, I will decompose the image presentation system into two parts. The first part, the Image servlet, is responsible for responding to image requests and returning either an image or an error to the client. The second part is a class capable of performing image I/O. For simplicity, the type of image generated is determined by passing the name of a Java class, which should generate the image to the Image servlet. The Java class implements a simple interface for communication with the servlet. The following code shows the interface that the ImageProducer class must implement:

public interface ImageProducer
{
  /**
   *  Request the producer create an image
   *
   *  @param stream stream to write image into
   *  @return image type
   */
  public String createImage(OutputStream stream) throws IOException;
}

The ImageProducer interface consists of a single method, with a parameter to provide a stream for which the image is output and a returned string indicating the type of image -- for example, image/jpg.

The following code indicates how the servlet works with the ImageProducer to produce the image:

ImageProducer imageProducer = 
         (ImageProducer) 
Class.forName(request.getQueryString()).newInstance();
String type = imageProducer.createImage(response.getOutputStream());
response.setContentType(type); 

This code attempts to load a class named by the query string in the URL. The query string is the portion of the URL after the ?. The loaded class is then cast to an ImageProducer and asked to create an image. If everything goes well, the resulting image is returned to the client.

There are several exception types that can arise from this code; the most common are ClassNotFoundException and ClassCastException. The former results from the named ImageProducer not being available to the Classloader that loaded the servlet, and the latter results from the named class not implementing the ImageProducer interface. In the event an exception occurs, the client of course does not receive an image, and the browser displays a graphic indicating that no image was returned by the Web server. The code was tested using the Java Server Web Development Kit (JSWDK) 1.0.1, but you should get similar results on most Web servers supporting Java servlets.

Image generation on Java 1.1

For my first generated image, I will create code I can deploy on the Java 1.1 platform. Although 1.2 JVMs are fairly common, not everyone can count on deploying code on the latest platform. Some commercial Web-hosting environments, for example, are somewhat slow to update the Java Runtime Environments (JREs) on their servers. This means that any servlets you deploy must run on Java 1.1. Designing the code for Java 1.1 does not greatly complicate matters; however, I do discuss a troublesome issue regarding in-memory image creation later in the article.

I will create the image of a simple pie chart. A pie chart is a useful, easy-to-read chart. This pie chart will be complete with colorization and labeling of the pie's pieces. The chart will indicate soft drinks favored by software developers. Now I'll take a look at how you can go about this.

For doing image I/O on Java 1.1, Sun offers a class library known as Jimi. Sun purchased this class library from a small software vendor. After Sun began distributing the class library, it repackaged the classes into com.sun, but otherwise left them unchanged. The following are the steps you should take to produce a PNG image using the Jimi toolkit.

  1. Construct a Frame window to provide an AWT image.
  2. Use the AWT image to get a Graphics instance.
  3. Draw into the Graphics object.
  4. Create a JimiWriter using the HTTP output stream.
  5. Have the JimiWriter write out the AWT image previously created.

On the Java 1.1 platform, constructing an in-memory image appears to require that an AWT component exist on the desktop. Normally, you should not use Java servlet code to create AWT components. It just doesn't make sense for the Web server to create user interface components to respond to a client request. In this case however, you need an image to provide a Graphics instance that you can draw into. So, you will have to live with the small window that appears on the Web server's desktop. If you know of a way to create an image on Java 1.1 without an AWT component, please let me know. One possible design would use a template image on the Web server's disk. You could load the image data from the disk, and construct the AWT image using java.awt.Toolkit.createImage(). After you construct the AWT image, it can provide the Graphics instance, which you can draw into as usual. If this works, you could avoid the spurious window. I will leave this experiment as an exercise to the interested reader.

First of all, the connection between the servlet and the class to produce the pie chart is the interface discussed earlier, ImageProducer. When you construct JIMIProducer, the ImageProducer for the pie chart, it will create an AWT frame, an AWT image, and subsequently an AWT graphics context. The following code sample is from the JIMIProducer:

    Frame f = new Frame();
    f.setVisible(true);
    image = f.createImage(ImageWidth, ImageHeight);
    graphics = image.getGraphics();
    f.setVisible(false);

The key method in the JIMIProducer class is drawSlice(). Given a label and the number of degrees the slice should consume, the class draws a slice of the pie chart and colorizes it. Next, I'll discuss the way this method works and revisit some high school geometry while I'm at it.

You begin drawing the pie chart internals at the three o'clock position; that is, you draw a line from the chart's center to the three o'clock point on the bounding oval. After you draw the initial line, you draw each additional line in a clockwise direction. The following code takes the number of degrees passed to the method and draws a line reflecting a pie slice with that angle. The calculation first converts the angle to radians; I start with degrees because it seems more natural. At any rate, the angle is converted to Cartesian coordinates. This x and y value is then used as an offset to the chart's center point. A line is subsequently drawn from the center to the calculated point. This process is repeated for each call to the drawSlice method; the accumulated calls to this method should total 360 degrees, or the pie chart will not look right.

    //***************************************************************
    // Convert to radians
    // 1 degree = pi/180 radians
    //***************************************************************
    double theta = degrees * (3.14/180);
    currentTheta += theta;
    //***************************************************************
    // Convert to x/y
    // x = r cos @
    // y = r sin @
    //***************************************************************
    double x = Radius * Math.cos(currentTheta);
    double y = Radius * Math.sin(currentTheta);
    
    Point mark2 = new Point(center);
    mark2.translate((int)x,(int)y);
graphics.drawLine(center.x, center.y, mark2.x, mark2.y);

The next step is to provide a unique color for the new pie slice. The following code illustrates how you can do this. The handy fillArc() method makes it easy to color the pie slice.

    graphics.setColor(colors[colorIndex++]);
    graphics.fillArc(Inset,
                     Inset,
                     PieWidth,
                     PieHeight,
                     -1 * lastAngle,
                     -1 * degrees);

The final task for drawSlice() is to label the new pie slice. The code calculates an appropriate place for the label based on the font metrics and then draws the text. The complete pie chart is found in Figure 1.

Figure 1. The completed pie chart. (PNG files are not viewable in all browsers, so we used a GIF file for this figure. To view the PNG file, visit Resources .)

Image generation on Java 1.2

Now I'll produce another image that is similar to something you might find on the Web, and use the tools available on Java 1.2. Almost everyone is interested in the stock market these days, so I will create a graph of the daily closing price of a stock for a particular week. As an example, I'll take Sun Microsystems' stock price for a week in March. For simplicity, I will ignore fractional amounts.

First, I'll discuss the major Java classes involved in creating the JPEG image. The class com.sun.image.codec.jpeg.JPEGImageEncoder contains the logic to actually encode (create) a valid JPEG format file. I'll reiterate here that the com.sun package indicates that this is not core Java and is subject to the usual warnings about nonportability as such.

The JPEGImageEncoder understands how to encode a java.awt.image.BufferedImage, so I will use it for these purposes. The BufferedImage is constructed by indicating the desired image size and type. After construction, the BufferedImage provides a java.awt.Graphics2D for the purpose of drawing into the BufferedImage. Easy, right? So, to review, here are the main steps to produce the JPEG image:

  1. Construct a JPEGImageEncoder, providing it with an OutputStream (from HTTP response).
  2. Construct a BufferedImage of the desired size and type.
  3. Use the Graphics2D instance from the BufferedImage to draw to your heart's content.
  4. Use the JPEGImageEncoder previously constructed to encode the BufferedImage into which you drew.

Now I'll discuss the details of drawing into the Graphics2D instance you get from the BufferedImage.

To create the stock-price graph, you essentially draw a series of lines to represent the graph's parts. The Java 2D package, java.awt.geom, provides more than enough capability for this use. The abstract class Line2D defines a line segment. It has two subclasses that are differentiated by the type used to store the coordinates. The Line2D.Double class uses the primitive Java type double and the Line2D.Float class uses a floating-point number for coordinates. A significant amount of precision is overkill for the stock-price graph, but I will use Line2D.Double to draw my graph.

The following code, used to create the horizontal axis, illustrates the line drawing process.

    horAxis = new Line2D.Double(HorzInset, 
                                ImageHeight - VertInset, 
                                ImageWidth -  HorzInset, 
                                ImageHeight - VertInset);
    graphics.draw(horAxis);

The horizontal axis is constructed at a location inset from the image's boundaries. This provides a space next to the line to draw text. After construction, you draw the line just by a call to draw() on the Graphics2D instance. Remember that you acquired the Graphics2D instance from the BufferedImage instance created earlier. The Graphics2D class provides numerous drawing methods to draw a variety of things from images to strings. Figure 2 displays the stock-price graph.

Figure 2. The completed stock-price graph

Image performance

"Poor performance, poor performance!" This has been the battle cry of Java naysayers for years. I don't buy it. I believe Java code that provides realtime images can be tuned for acceptable performance in most situations. As a software developer, I can offer you this advice: if you have a performance issue, buy faster hardware!

Conclusion

In this article, I have shown how you can generate simple images in Java and then use that capability to dynamically create an image to return to a Web client. This process has myriad uses, limited only by the imagination. Two areas of concern are the performance of the actual image creation on the server and bandwidth limitations that require lengthy delays for image transport to the client. You can mediate both of these concerns by constructing reasonably sized images and tuning the Java code for performance in standard ways to construct a quality dynamic imaging system.

Happy imaging!

Ken McCrary is a Sun-certified Java developer living in Research Triangle Park, N.C. He has worked on Java projects ranging from Web applets to embedded Java systems. You can visit his Website at http://www.KenMcCrary.com.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies