6.2 Images

Java Servlet Programming, 2nd Edition > 6. Sending Multimedia Content > 6.2 Images

 
< BACKCONTINUE >

6.2 Images

People are visually oriented they like to see, not just read, their information. Consequently, it's nearly impossible to find a web site that doesn't use images in some way, and those you do find tend to look unprofessional. To cite the well-worn cliche (translated into programmer-speak), "An image is worth a thousand words."

Luckily, it's relatively simple for a servlet to send an image as its response. In fact, we've already seen a servlet that does just this: the ViewResource servlet from Chapter 4. As you may recall, this servlet can return any file under the server's document root. When the file happens to be an image file, it detects that fact with the getMimeType() method and sets its response's content type with setContentType() before sending the raw bytes to the client.

This technique requires that we already have the needed image files saved on disk, which isn't always the case. Often, a servlet must generate or manipulate an image before sending it to the client. Imagine, for example, a web page that contains an image of an analog clock that displays the current time. Sure, someone could save 720 images (60 minutes times 12 hours) to disk and use a servlet to dispatch the appropriate one. But that someone isn't me, and it shouldn't be you. Instead, the wise servlet programmer writes a servlet that dynamically generates the image of the clock face and its hands or as a variant, a servlet that loads an image of the clock face and adds just the hands. And, of course, the frugal programmer also has the servlet cache the image (for about a minute) to save server cycles.

There are many other reasons you might want a servlet to return an image. By generating images, a servlet can display things such as an up-to-the-minute stock chart, the current score for a baseball game (complete with icons representing the runners on base), or a graphical representation of the Cokes left in the Coke machine. By manipulating preexisting images, a servlet can do even more. It can draw on top of them; change their color, size, or appearance; or combine several images into one.

6.2.1 Image Generation

Suppose you have an image as raw pixel data that you want to send to someone. How do you do it? Let's assume it's a true-color, 24-bit image (3 bytes per pixel) and that it's 100 pixels tall and 100 pixels wide. You could take the obvious approach and send it one pixel at a time, in a stream of 30,000 bytes. But is that enough? How does the receiver know what to do with the 30,000 bytes he received? The answer is that he doesn't. You also need to say that you are sending raw, true-color pixel values, that you're beginning in the upper-left corner, that you're sending row by row, and that each row is 100 pixels wide. Yikes! And what if you decide to send fewer bytes by using compression? You have to say what kind of compression you are using, so the receiver can decompress the image. Suddenly this has become a complicated problem.

Fortunately this is a problem that has been solved, and solved several different ways. Each image format (GIF, JPEG, PNG, TIFF, etc.) represents one solution. Each image format defines a standard way to encode an image so that it can later be decoded for viewing or manipulation. Each encoding technique has certain advantages and limitations. For example, the compression used for GIF encoding excels at handling computer-generated images, but the GIF format is limited to just 256 colors. The compression used for JPEG encoding, on the other hand, works best on photo-realistic images that contain millions of colors, but it works so well because it uses "lossy" compression that can blur the photo's details. PNG (pronounced "ping") is a relatively new encoding intended to replace GIF because it's smaller, supports millions of colors, employs "lossless" compression, and has an alpha channel for great transparency effects plus it's free from patent issues that have plagued GIF.[3]

[3] For more information on PNG, see http://graphicswiz.com/png.

Understanding image encoding helps you understand how servlets handle images. A servlet like ViewResource can return a preexisting image by sending its encoded representation unmodified to the client the browser decodes the image for viewing. But a servlet that generates or modifies an image must construct an internal representation of that image, manipulate it, and then encode it, before sending it to the client.

6.2.1.1 A "Hello World" image

Example 6-6 gives a simple example of a servlet that generates and returns a GIF image. The graphic says "Hello World!," as shown in Figure 6-5.

Example 6-6. Hello World Graphics
import java.io.*; import java.awt.*; import javax.servlet.*; import javax.servlet.http.*; import Acme.JPM.Encoders.GifEncoder; public class HelloWorldGraphics extends HttpServlet {   public void doGet(HttpServletRequest req, HttpServletResponse res)                                throws ServletException, IOException {     ServletOutputStream out = res.getOutputStream();  // binary output!     Frame frame = null;     Graphics g = null;     try {       // Create an unshown frame       frame = new Frame();       frame.addNotify();       // Get a graphics region, using the Frame       Image image = frame.createImage(400, 60);       g = image.getGraphics();       // Draw "Hello World!" to the off-screen graphics context       g.setFont(new Font("Serif", Font.ITALIC, 48));       g.drawString("Hello World!", 10, 50);       // Encode the off-screen image into a GIF and send it to the client       res.setContentType("image/gif");       GifEncoder encoder = new GifEncoder(image, out);       encoder.encode();     }     finally {       // Clean up resources       if (g != null) g.dispose();       if (frame != null) frame.removeNotify();     }   } }

Although this servlet uses the java.awt package, it never actually displays a window on the server's display. Nor does it display a window on the client's display. It performs all its work in an off-screen graphics context and lets the browser display the image. The strategy is as follows: create an off-screen image, get its graphics context, draw to the graphics context, and then encode the resulting image for transmission to the client.

Figure 6-5. Hello World graphics

Obtaining an off-screen image involves jumping through several hoops. In Java, an image is represented by the java.awt.Image class. Unfortunately, in JDK 1.1, an Image object cannot be instantiated directly through a constructor. It must be obtained through a factory method like the createImage() method of Component or the getImage() method of Toolkit. Because we're creating a new image, we use createImage(). Note that before a component can create an image, its native peer must already exist. Thus, to create our Image we must create a Frame, create the frame's peer with a call to addNotify(), and then use the frame to create our Image.[4]

[4] For web servers running on Unix systems, the frame's native peer has to be created inside an X server. Thus, for optimal performance, make sure the DISPLAY environment variable (which specifies the X server to use) is unset or set to a local X server. Also make sure the web server has been granted access to the X server, which may require the use of xhost or xauth. "Headless" server machines without an X server running can use Xvfb (X virtual frame buffer) to handle the graphics chores; just make sure to point the DISPLAY at the Xvfb server.

In JDK 1.2, the process has been simplified and an Image can be created directly by constructing a new java.awt.image.BufferedImage . However, our examples in this chapter use the frame.createImage() technique for maximum portability across JDK versions.

Once we have an image, we draw onto it using its graphics context, which can be retrieved with a call to the getGraphics() method of Image. In this example, we just draw a simple string.

After drawing into the graphics context, we call setContentType() to set the MIME type to image/gif since we're going to use the GIF encoding. For the examples in this chapter, we use a GIF encoder written by Jef Poskanzer. It's well written and freely available with source from http://www.acme.com.

Note that the LZW compression algorithm used for GIF encoding is protected by Unisys and IBM patents which, according to the Free Software Foundation, make it impossible to have free software that generates the GIF format. The Acme GIF encoder uses LZ compression, so perhaps that avoids the patent. For more information, see http://www.fsf.org/philosophy/gif.html and http://www.burnallgifs.org. Of course, a servlet can encode its Image into any image format. For web content, JPEG and PNG exist as the most viable alternatives to GIF. There are a variety of JPEG and PNG encoders available. For users of JDK 1.2 and later there's a JPEG encoder built-in in the com.sun.image.codec.jpeg package; encoders for PNG and other formats are available for JDK 1.2 in the official Java Advanced Imaging API from http://java.sun.com/products/java-media/jai. . For users who need JDK 1.1 support as well, there's the Java Image Management Interface (JIMI) tool, formerly a commercial product from Activated Intelligence but now a Sun library available for free from http://java.sun.com/products/jimi.

To encode the image, we create a GifEncoder object, passing it the Image object and the ServletOutputStream for the servlet. When we call encode() on the GifEncoder object, the image is encoded and sent to the client.

After sending the image, the servlet does what all well-behaved servlets should do: it releases its graphical resources. These would be reclaimed automatically during garbage collection, but releasing them immediately helps on systems with limited resources. The code to release the resources is placed in a finally block to guarantee its execution, even when the servlet throws an exception.

6.2.1.2 A dynamically generated chart

Now let's look at a servlet that generates a more interesting image. Example 6-7 creates a bar chart that compares apples to oranges (who said it couldn't be done?), with regard to their annual consumption.

Example 6-7. A Chart Comparing Apples and Oranges
import java.awt.*; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import Acme.JPM.Encoders.GifEncoder; import javachart.chart.*;  // from Visual Engineering public class SimpleChart extends HttpServlet {   static final int WIDTH = 450;   static final int HEIGHT = 320;   public void doGet(HttpServletRequest req, HttpServletResponse res)                                throws ServletException ,IOException {     ServletOutputStream out = res.getOutputStream();     Frame frame = null;     Graphics g = null;     try {       // Create a simple chart       BarChart chart = new BarChart("Apples and Oranges");       // Give it a title       chart.getBackground().setTitleFont(new Font("Serif", Font.PLAIN, 24));       chart.getBackground().setTitleString("Comparing Apples and Oranges");       // Show, place, and customize its legend       chart.setLegendVisible(true);       chart.getLegend().setLlX(0.4);  // normalized from lower left       chart.getLegend().setLlY(0.75); // normalized from lower left       chart.getLegend().setIconHeight(0.04);       chart.getLegend().setIconWidth(0.04);       chart.getLegend().setIconGap(0.02);       chart.getLegend().setVerticalLayout(false);       // Give it its data and labels       double[] appleData = {950, 1005, 1210, 1165, 1255};       chart.addDataset("Apples", appleData);       double[] orangeData = {1435, 1650, 1555, 1440, 1595};       chart.addDataset("Oranges", orangeData);       String[] labels = {"1993", "1994", "1995", "1996", "1997"};       chart.getXAxis().addLabels(labels);       // Color apples red and oranges orange       chart.getDatasets()[0].getGc().setFillColor(Color.red);       chart.getDatasets()[1].getGc().setFillColor(Color.orange);       // Name the axes       chart.getXAxis().setTitleString("Year");       chart.getYAxis().setTitleString("Tons Consumed");       // Size it appropriately       chart.resize(WIDTH, HEIGHT);       // Create an unshown frame       frame = new Frame();       frame.addNotify();       // Get a graphics region of appropriate size, using the Frame       Image image = frame.createImage(WIDTH, HEIGHT);       g = image.getGraphics();       // Ask the chart to draw itself to the off screen graphics context       chart.drawGraph(g);       // Encode and return what it painted       res.setContentType("image/gif");       GifEncoder encoder = new GifEncoder(image, out);       encoder.encode();     }     finally {       // Clean up resources       if (g != null) g.dispose();       if (frame != null) frame.removeNotify();     }   } }
Figure 6-6. A chart comparing apples and oranges

The basics are the same: create an off-screen image and get its graphics context, draw to the graphics context, and then encode the image for transmission to the client. The difference is that this servlet constructs a BarChart object to do the drawing. There are more than a dozen charting packages available in Java. The BarChart class from this example came from Visual Engineering's KavaChart (formerly JavaChart) package, available at http://www.ve.com/kavachart. It's a commercial product, but for readers of this book they have granted free permission to use the portion of the API presented in our example. The KavaChart package also includes a set of free chart-generating servlets at http://www.ve.com/kavachart/servlets.html. Another good charting package is JClass Chart from Sitraka at http://www.sitraka.com (formerly KL Group).

6.2.2 Image Composition

So far, we've drawn our graphics onto empty images. In this section, we discuss how to take preexisting images and either draw on top of them or combine them to make conglomerate images. We also examine error handling in servlets that return images.

6.2.2.1 Drawing over an image

Sometimes it's useful for a servlet to draw on top of an existing image. A good example is a building locator servlet that knows where every employee sits. When queried for a specific employee, it can draw a big red dot over that employee's office.

One deceptively easy technique for drawing over a preexisting image is to retrieve the image with Toolkit.getDefaultToolkit().getImage(imagename), get its graphics context with a call to the getGraphics() method of Image, and then use the returned graphics context to draw on top of the image. Unfortunately, it isn't quite that easy. The reason is that you cannot use getGraphics() unless the image was created with the createImage() method of Component. With the AWT, you always need to have a native peer in the background doing the actual graphics rendering.

Here's what you have to do instead: retrieve the preexisting image via the Toolkit.getDefaultToolkit().getImage(imagename) method and then tell it to draw itself into another graphics context created with the createImage() method of Component, as shown in the previous two examples. Now you can use that graphics context to draw on top of the original image.

Example 6-8 clarifies this technique with an example. It's a servlet that writes "CONFIDENTIAL" over every image it returns. The image name is passed to the servlet as extra path information; the servlet will load the image from the corresponding location under the server's document root, using the getResource() method to support distributed execution.

Example 6-8. Drawing Over an Image to Mark It Confidential
import java.awt.*; import java.io.*; import java.net.*; import javax.servlet.*; import javax.servlet.http.*; import com.oreilly.servlet.ServletUtils; import Acme.JPM.Encoders.GifEncoder; public class Confidentializer extends HttpServlet {   Frame frame = null;   public void init() throws ServletException {     // Construct a reusable unshown frame     frame = new Frame();     frame.addNotify();   }   public void doGet(HttpServletRequest req, HttpServletResponse res)                                throws ServletException, IOException {     ServletOutputStream out = res.getOutputStream();     Graphics g = null;     try {       // Get the image location from the path info       // Use ServletUtils (Chapter 4) for safety       URL source =         ServletUtils.getResource(getServletContext(), req.getPathInfo());       }       // Load the image (from bytes to an Image object)       MediaTracker mt = new MediaTracker(frame);  // frame acts as ImageObserver       Image image = Toolkit.getDefaultToolkit().getImage(source);       mt.addImage(image, 0);       try {         mt.waitForAll();       }       catch (InterruptedException e) {         res.sendError(res.SC_INTERNAL_SERVER_ERROR,                 "Interrupted while loading image: " +                 ServletUtils.getStackTraceAsString(e));         return;       }       // Get the width and height       int w = image.getWidth(frame);       int h = image.getHeight(frame);       // Make sure we are reading valid image data       if (w <= 0 || h <= 0) {         res.sendError(res.SC_NOT_FOUND,                 "Extra path information must point to a valid image");         return;       }       // Construct a matching-size off screen graphics context       Image offscreen = frame.createImage(w, h);       g = offscreen.getGraphics();       // Draw the image to the off-screen graphics context       g.drawImage(image, 0, 0, frame);       // Write CONFIDENTIAL over its top       g.setFont(new Font("Monospaced", Font.BOLD | Font.ITALIC, 30));       g.drawString("CONFIDENTIAL", 10, 30);       // Encode the off-screen graphics into a GIF and send it to the client       res.setContentType("image/gif");       GifEncoder encoder = new GifEncoder(offscreen, out);       encoder.encode();     }     finally {       // Clean up resources       if (g != null) g.dispose();     }   }   public void destroy() {     // Clean up resources     if (frame != null) frame.removeNotify();   } }

Some example output in shown in Figure 6-7

Figure 6-7. Drawing over an image to mark it confidential

You can see that this servlet performs each step exactly as described earlier, along with some additional housekeeping. The servlet creates its unshown Frame in its init() method. Creating the Frame once and reusing it is an optimization previously left out for the sake of clarity. For each request, the servlet begins by retrieving the name of the preexisting image from the extra path information and converts the path to a resource using getResource() . Then it retrieves a reference to the image with the getImage() method of Toolkit and physically loads it into memory with the help of a MediaTracker . Normally it's fine for an image to load asynchronously with its partial results painted as it loads, but in this case we paint the image just once and need to guarantee it's fully loaded beforehand. Then the servlet gets the width and height of the loaded image and creates an off-screen image to match. Finally, the big moment: the loaded image is drawn on top of the newly constructed, empty image. After that it's old hat. The servlet writes its big "CONFIDENTIAL" and encodes the image for transmission.

Notice how this servlet handles error conditions by calling sendError() . When returning images, it's difficult to do much more. This approach allows the server to do whatever it deems appropriate.

6.2.2.2 Combining images

A servlet can also combine images into one conglomerate image. Using this ability, a building locator servlet could display an employee's smiling face, instead of a red dot, over her office. The technique used for combining images is similar to the one we used to draw over the top of an image: the appropriate images are loaded, they're drawn onto a properly created Image object, and that image is encoded for transmission.

Example 6-9 shows how to do this for a servlet that displays a hit count as a sequence of individual number images combined into one large image. The number images it uses are available at http://www.geocities.com/SiliconValley/6742, along with several other styles.

Example 6-9. Combining Images to Form a Graphical Counter
import java.awt.*; import java.io.*; import java.net.*; import javax.servlet.*; import javax.servlet.http.*; import com.oreilly.servlet.ServletUtils; import Acme.JPM.Encoders.GifEncoder; public class GraphicalCounter extends HttpServlet {   public static final String DIR = "/images/odometer";   public static final String COUNT = "314159";   public void doGet(HttpServletRequest req, HttpServletResponse res)                                throws ServletException, IOException {     ServletOutputStream out = res.getOutputStream();     Frame frame = null;     Graphics g = null;     try {       // Get the count to display, must be sole value in the raw query string       // Or use the default       String count = (String)req.getQueryString();       if (count == null) count = COUNT;       int countlen = count.length();       Image images[] = new Image[countlen];       for (int i = 0; i < countlen; i++) {         URL imageSrc =            getServletContext().getResource(DIR + "/" + count.charAt(i) + ".GIF");         if (imageSrc == null) {           imageSrc = new URL("file:");  // placeholder, handle errors later         }         images[i] = Toolkit.getDefaultToolkit().getImage(imageSrc);       }       // Create an unshown frame       frame = new Frame();       frame.addNotify();       // Load the images       MediaTracker mt = new MediaTracker(frame);       for (int i = 0; i < countlen; i++) {         mt.addImage(images[i], i);       }       try {         mt.waitForAll();       }       catch (InterruptedException e) {         res.sendError(res.SC_INTERNAL_SERVER_ERROR,                 "Interrupted while loading image: " +                 ServletUtils.getStackTraceAsString(e));         return;       }       // Check for problems loading the images       if (mt.isErrorAny()) {         // We had a problem, find which image(s)         StringBuffer problemChars = new StringBuffer();         for (int i = 0; i < countlen; i++) {           if (mt.isErrorID(i)) {             problemChars.append(count.charAt(i));           }         }         res.sendError(res.SC_INTERNAL_SERVER_ERROR,                 "Could not load an image for these characters: " +                 problemChars.toString());         return;       }       // Get the cumulative size of the images       int width = 0;       int height = 0;       for (int i = 0; i < countlen; i++) {         width += images[i].getWidth(frame);         height = Math.max(height, images[i].getHeight(frame));       }       // Get a graphics region to match, using the Frame       Image image = frame.createImage(width, height);       g = image.getGraphics();       // Draw the images       int xindex = 0;       for (int i = 0; i < countlen; i++) {         g.drawImage(images[i], xindex, 0, frame);         xindex += images[i].getWidth(frame);       }       // Encode and return the composite       res.setContentType("image/gif");       GifEncoder encoder = new GifEncoder(image, out);       encoder.encode();     }     finally {       // Clean up resources       if (g != null) g.dispose();       if (frame != null) frame.removeNotify();     }   } }

The output can be seen in Figure 6-8.

Figure 6-8. Combining images to form a graphical counter

This servlet receives the number to display by reading its raw query string. For each number in the count, it retrieves and loads the corresponding number image from the directory given by DIR. (DIR is always under the server's document root. It's given as a virtual path and converted to an abstract resource path.) Then it calculates the combined width and the maximum height of all these images and constructs an off-screen image to match. The servlet draws each number image into this off-screen image in turn from left to right. Finally, it encodes the image for transmission.

To be of practical use, this servlet must be called by another servlet that knows the hit count to be displayed and places the count in the query string. For example, it could be called by a JSP page or other dynamically created page using syntax like the following:

<IMG src="/books/1/318/1/html/2//servlet/GraphicalCounter?121672">

This servlet handles error conditions in the same way as the previous servlet, by calling sendError() and leaving it to the server to behave appropriately.

6.2.3 Image Effects

We've seen how servlets can create and combine images. In this section, we look at how servlets can also perform special effects on images. For example, a servlet can reduce the transmission time for an image by scaling down its size before transmission. Or it can add some special shading to an image to make it resemble a clickable button. As an example, let's look at how a servlet can convert a color image to grayscale.

6.2.3.1 Converting an image to grayscale

Example 6-10 shows a servlet that converts an image to grayscale before returning it. The servlet performs this effect without ever actually creating an off-screen graphics context. Instead, it creates the image using a special ImageFilter. (We'd show you before and after images, but they wouldn't look very convincing in a black-and-white book.)

Example 6-10. An Image Effect Converting an Image to Grayscale
import java.awt.*; import java.awt.image.*; import java.io.*; import java.net.*; import javax.servlet.*; import javax.servlet.http.*; import com.oreilly.servlet.ServletUtils; import Acme.JPM.Encoders.*; public class DeColorize extends HttpServlet {   public void doGet(HttpServletRequest req, HttpServletResponse res)                                throws ServletException, IOException {     res.setContentType("image/gif");     ServletOutputStream out = res.getOutputStream();     // Get the image location from the path info     URL source = ServletUtils.getResource(getServletContext(),                                           req.getPathInfo());     if (source == null) {       res.sendError(res.SC_NOT_FOUND,               "Extra path information must point to an image");       return;     }     // Construct an unshown frame     // No addNotify() because its peer isn't needed     Frame frame = new Frame();     // Load the image     Image image = Toolkit.getDefaultToolkit().getImage(source);     MediaTracker mt = new MediaTracker(frame);     mt.addImage(image, 0);     try {       mt.waitForAll();     }     catch (InterruptedException e) {       res.sendError(res.SC_INTERNAL_SERVER_ERROR,               "Interrupted while loading image: " +               ServletUtils.getStackTraceAsString(e));       return;     }     // Get the size of the image     int width = image.getWidth(frame);     int height = image.getHeight(frame);     // Make sure we are reading valid image data     if (width <= 0 || height <= 0) {       res.sendError(res.SC_NOT_FOUND,               "Extra path information must point to a valid image");       return;     }     // Create an image to match, run through a filter     Image filtered = frame.createImage(       new FilteredImageSource(image.getSource(),                               new GrayscaleImageFilter()));     // Encode and return the filtered image     GifEncoder encoder = new GifEncoder(filtered, out);     encoder.encode();   } }

Much of the code for this servlet matches that of the Confidentializer example. The major difference is shown here:

// Create an image to match, run through a filter Image filtered = frame.createImage(   new FilteredImageSource(image.getSource(),                           new GrayscaleImageFilter()));

This servlet doesn't use the createImage(int, int) method of Component we've used until now. It takes advantage of the createImage(ImageProducer) method of Component instead. The servlet creates an image producer with a FilteredImageSource that then passes the image through a GrayscaleImageFilter. This filter converts each color pixel to its grayscale counterpart. Thus, the image is converted to grayscale as it is being created. The code for the GrayscaleImageFilter is shown in Example 6-11.

Example 6-11. The GrayscaleImageFilter Class
import java.awt.*; import java.awt.image.*; public class GrayscaleImageFilter extends RGBImageFilter {   public GrayscaleImageFilter() {     canFilterIndexColorModel = true;   }   // Convert color pixels to grayscale   // The algorithm matches the NTSC specification   public int filterRGB(int x, int y, int pixel) {     // Get the average RGB intensity     int red = (pixel & 0x00ff0000) >> 16;     int green = (pixel & 0x0000ff00) >> 8;     int blue = pixel & 0x000000ff;     int luma = (int) (0.299 * red + 0.587 * green + 0.114 * blue);     // Return the luma value as the value for each RGB component     // Note: Alpha (transparency) is always set to max (not transparent)     return (0xff << 24) | (luma << 16) | (luma << 8) | luma;   } }

For each value in the colormap, this filter receives a pixel value and returns a new filtered pixel value. By setting the canFilterIndexColorModel variable to true, we signify that this filter can operate on the colormap and not on individual pixel values. The pixel value is given as a 32-bit int, where the first octet represents the alpha (transparency) value, the second octet the intensity of red, the third octet the intensity of green, and the fourth octet the intensity of blue. To convert a pixel value to grayscale, the red, green, and blue intensities must be set to identical values. We could average the red, green, and blue values and use that average value for each color intensity. That would convert the image to grayscale. Taking into account how people actually perceive color (and other factors), however, demands a weighted average. The 0.299, 0.587, 0.114 weighting used here matches that used by the National Television Systems Committee for black-and-white television. For more information, see Charles A. Poynton's book A Technical Introduction to Digital Video (Wiley) and the web site http://www.color.org.

6.2.3.2 Caching a converted image

The process of creating and encoding an image can be expensive, taking both time and server CPU cycles. Caching encoded images can often improve performance dramatically. Instead of doing all the work for every request, the results can be saved and resent for subsequent requests. The clock face idea that we mentioned earlier is a perfect example. The clock image needs to be created at most once per minute. Any other requests during that minute can be sent the same image. A chart for vote tabulation is another example. It can be created once and changed only as new votes come in.

The com.oreilly.servlet.CacheHttpServlet superclass from Chapter 3 provides a simple caching mechanism for images as well as text. A servlet generating the clock image could extend CacheHttpServlet and implement a getLastModified() method that returns the current time rounded down to the nearest minute. A servlet generating the vote tabulation chart could implement a getLastModified() method that returns the time the last vote was placed.

Our DeColorize example could extend CacheHttpServlet with a getLastModified() method that returns the time the image resource last changed. Unfortunately, CacheHttpServlet caches only the last response, providing little benefit in the likely scenario that DeColorize is called on more than one image. DeColorize should probably use a more sophisticated caching algorithm to handle multiple images. The servlet lifecycle makes this fairly simple. Our new DeColorize servlet can save each converted image as a byte array stored in a Hashtable keyed by the image name. First, our servlet needs to create a Hashtable instance variable. This must be declared outside doGet():

Hashtable gifs = new Hashtable();

To fill this hashtable, we need to capture the encoded graphics. So, instead of giving the GifEncoder the ServletOutputStream, we give it a ByteArrayOutputStream. Then, when we encode the image with encode(), the encoded image is stored in the ByteArrayOutputStream. Finally, we store the captured bytes in the hashtable and then write them to the ServletOutputStream to send the image to the client. Here's the new code to encode, store, and return the filtered image:

// Encode, store, and return the filtered image ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);  // 1K initial GifEncoder encoder = new GifEncoder(filtered, baos); encoder.encode(); gifs.put(source, baos); baos.writeTo(out);

This fills the hashtable with encoded images keyed by image name. Now, earlier in the servlet, we can go directly to the cache when asked to return a previously encoded image. This code should go immediately after the code executed if source==null:

// Short circuit if it's been done before if (gifs.containsKey(source)) {   ByteArrayOutputStream baos = (ByteArrayOutputStream) gifs.get(source);   baos.writeTo(out);   return; }

With these modifications, any image found in the cache is returned quickly, directly from memory.

Of course, caching multiple images tends to consume large amounts of memory. To cache a single image is rarely a problem, but a servlet such as this should use some method for cleaning house. For example, it could cache only the 100 most recently requested images. A more robust version of the servlet could also examine the file's timestamp to make sure the original image hasn't changed since the black-and-white version was cached.


Last updated on 3/20/2003
Java Servlet Programming, 2nd Edition, © 2001 O'Reilly

< BACKCONTINUE >


Java servlet programming
Java Servlet Programming (Java Series)
ISBN: 0596000405
EAN: 2147483647
Year: 2000
Pages: 223

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net