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:

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

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

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. The photo

Figure 7. The frame
The frame will be drawn on top of the image, as in 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. The mask
Now we can combine the mask with the photo to get Figure 10:

Figure 10. Photo + mask
and then draw the frame on top to get our final image: 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. Frame for a photographer's site

Figure 13. Photo with watermark for a clip art site

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.
Joshua Marinacci first tried Java in 1995 at the request of his favorite TA and has never looked back.