Antialiasing, images, and alpha compositing in Java 2D

Java 2D adds support for antialiased rendering, image transforms, and alpha compositing

Last month, I introduced some basic notions of Java 2D: that all 2D objects can be manipulated using AffineTransforms, that arbitrary paths can be constructed and shapes filled using GeneralPaths, and that text strings are drawn and operated on just like any other shape in Java 2D. This month, I'll continue the discussion by presenting the solution to last month's aliasing problem. I'll also illustrate how to use one shape to clip another, and delve into the new image-manipulation capabilities provided by Java.

What is aliasing, and how do you avoid it?

Aliasing occurs when a signal (in this case, a 2D graphics signal) is sampled and quantized from a continuous space into a discretized space. Sampling is the process of reading a value from a continuously varying signal. Quantization is the process by which these continuous sampled values are assigned a discrete value in the finite space represented by digital (binary-based) systems.

Aliasing is a by-product of this quantization. Humans perceive this by-product visually as abrupt changes in color from pixel to pixel. Graphics professionals often refer to these jagged edges as jaggies.

In general, aliasing is a bad thing. It leads to lower-quality signals of all kinds. In fact, if you look closely at the examples in last month's column, especially those with slanted and curved edges, you can see aliasing effects all the way back to Example02. (Example01's lines are drawn parallel and perpendicular to the scan-line direction of the computer screen, so there are no quantization errors.)

If you are not familiar with aliasing effects, you can refer to any decent graphics or signal processing textbook for much more in-depth information. Charles Poynton's A Technical Introduction to Digital Video (John Wiley & Sons; ISBN: 047112253X) gives a particularly good description of aliasing as it pertains to video signals. If you are familiar with aliasing, but would like a quick refresher, see the Resources section for a link to "The Truth about Antialiasing" by Jonathan Knudsen.

So, how do you handle aliasing? Java 2D lets you set one of several rendering hints to indicate that you would like for your 2D graphics to be drawn using antialiasing algorithms -- which smoothes the edges. Note that I said hints: Whichever Java 2D implementation you're using, it is allowed to decide whether or not to follow the hint and carry out the antialiasing as requested.

Let's compare the aliased output from last month's Example04 to some antialiased output, generated from Example05.

001   /**
002    * In previous examples, we saw some jagged edges due to aliasing.
003    * Example05 illustrates how to use rendering hints to request
004    * an anti-aliased render from Graphics2D.
005    **/
006   public void paint(Graphics g) {
007     Graphics2D g2d = (Graphics2D) g;
008
009     //This time, we want to use anti-aliasing if possible
010     //to avoid the jagged edges that were so prominent in
011     //our last example.  With ask the Java 2D rendering
012     //engine (Graphics2D) to do this using a "rendering hint".
013     g2d.setRenderingHints(Graphics2D.ANTIALIASING,
014        Graphics2D.ANTIALIAS_ON);
015
016     //We reuse our GeneralPath filled shape.  We translate
017     //and rotate this shape as we did before.
018     GeneralPath path = new GeneralPath(GeneralPath.EVEN_ODD);
019     path.moveTo(0.0f,0.0f);
020     path.lineTo(0.0f,125.0f);
021     path.quadTo(100.0f,100.0f,225.0f,125.0f);
022     path.curveTo(260.0f,100.0f,130.0f,50.0f,225.0f,0.0f);
023     path.closePath();
024
025     AffineTransform at = new AffineTransform();
026     at.setToRotation(-Math.PI/8.0);
027     g2d.transform(at);
028     at.setToTranslation(0.0f,150.0f);
029     g2d.transform(at);
030
031     g2d.setColor(Color.green);
032     g2d.fill(path);
033
034     Font exFont = new Font("TimesRoman",Font.PLAIN,40);
035     g2d.setFont(exFont);
036     g2d.setColor(Color.black);
037     g2d.drawString("JavaWorld",0.0f,0.0f);
038   }

The effects of the antialiasing are shown below.

Antialiasing renders graphics with smoother edges.

You request antialiased rendering by calling Graphics2D.setRenderingHints(Graphics2D.ANTIALIASING,Graphics2D.ANTIALIAS_ON) (lines 013 and 014). Antialiasing and the other rendering hints are discussed in more detail in the Java 2D API javadoc documentation.

So if aliasing is always bad, and antialiasing algorithms are available (assuming your Java runtime environment supports the rendering hint), then why not just perform antialiasing calculations all the time? The simple answer is performance. Antialiasing algorithms take longer to compute than their less-complicated aliasing brethren, slowing down your rendering times. Example05 is noticeably slower than Example04 on most current Pentium PC-class machines, though it still renders in a few hundred milliseconds.

In applications that render something only once, or rather infrequently, and where rendering speed is not particularly important, you should probably turn on the antialiasing hint to get better renders. Such applications include CAD systems and image manipulation tools. However, if an applications requires the fastest possible rendering speed, and aliasing will be less noticeable, antialiasing is undesirable. In twitch video games, for example, fast renders are much more important than jagged edges. As with many programming and engineering details, you as the application programmer need to understand the requirements of your problem domain and decide what is best for your particular application.

If it makes sense for your application, you could even leave the decision to the end user. For a CAD application, for instance, you could provide a switch to turn antialiasing on and off, depending on the user's needs. This switch would let the user change the rendering hints at runtime and then automatically repaint the graphics.

Clipping paths

Last month I told you that any text string can be used as a Shape. This allows you to perform several interesting graphics manipulations with text. One of the more useful of these manipulations is clipping.

Clipping allows you to use a specified path as a sort of stencil set on top of previously laid graphics. Java 2D supports clipping using any arbitrary Shape. You can clip with shapes you draw yourself, including shapes you create using a GeneralPath. Because you can use StyledString.getStringOutline() to get the Shape for a string of text, you can also clip with that text.

Example06 modifies the paint() method one more time. I've left antialiasing on, and draw the same four-sided path. This time, however, instead of drawing the text string, I use its shape to specify where the path-created object is filled. (Note that you also need to import the package containing StyledString by adding import java.awt.font.*; to the source file.)

001   /**
002    * We use StyledString.getStringOutline() to get the Shape of
003    * some text, which we then use to clip our GeneralPath.
004    **/
005   public void paint(Graphics g) {
006     Graphics2D g2d = (Graphics2D) g;
007
008     //Again, we use anti-aliasing if possible.
009     g2d.setRenderingHints(Graphics2D.ANTIALIASING,
010        Graphics2D.ANTIALIAS_ON);
011
012     GeneralPath path = new GeneralPath(GeneralPath.EVEN_ODD);
013     path.moveTo(0.0f,0.0f);
014     path.lineTo(0.0f,125.0f);
015     path.quadTo(100.0f,100.0f,225.0f,125.0f);
016     path.curveTo(260.0f,100.0f,130.0f,50.0f,225.0f,0.0f);
017     path.closePath();
018
019     AffineTransform at = new AffineTransform();
020     at.setToRotation(-Math.PI/8.0);
021     g2d.transform(at);
022     at.setToTranslation(0.0f,150.0f);
023     g2d.transform(at);
024
025     Font exFont = new Font("TimesRoman",Font.PLAIN,80);
026     g2d.setFont(exFont);
027
028     //Now, we want to create a string and then use its outline
029     //to clip our green GeneralPath object drawn above.  We
030     //do this by creating a StyledString, getting its StringOutline
031     //(which is a Shape object like GeneralPath), then setting
032     //the clipping shape to be the string and filling the path.
033     StyledString exString = new StyledString ("JavaWorld", exFont);
034     Shape exShape = exString.getStringOutline();
035     g2d.setClip(exShape);
036     g2d.setColor(Color.green);
037     g2d.fill(path);
038   }

Note that StyledString will be deprecated with JDK 1.2 beta 4 (currently listed by Sun as due some time in late July 1998). Members of the Java 2D team have stated on the java2d-interest mailing list that this functionality will be replaced by a couple of new classes in the beta 4 release. I will update the example code for the JDK 1.2 beta 4 changes and make it available on my personal Web site.

In the following figure, you can see a portion of the green object showing through the overlaid (stencil) portion of the clipping text. Only that portion of the object lying directly beneath the text string's letters can be seen through this stencil.

The string JavaWorld is used to clip the green shape. With Java 2D, you can use any Shape object to clip any other Shape .

Clipping can be used for a lot of interesting effects. You could, for instance, use a circle shape to clip a complicated 2D shape, and move this circle around to simulate a spotlight moving across your 2D graphics. All sorts of intersections and cuts can be created with fairly simple clipping operations. Powerful stuff, indeed!

Imaging in Java 2D

Java 2D expands upon Java 1.0- and 1.1-style producer-consumer imaging (see Resources for more on this model) by allowing any of the regular 2D manipulations to be performed on images -- such as affine transforms and clipping. In addition, Java 2D adds a new buffer-based imaging model. This model gives programmers better access to the image's color information, and control over the raster -- the array that specifies the color values for each individual pixel in the image.

Whereas in Java 1.1 a call to a java.awt.Toolkit's or AWT Component's createImage(int width, int height) method returns a platform-specific implementation of the java.awt.Image interface, a createImage() call to Java 1.2 toolkits and components returns a java.awt.image.BufferedImage object.

In this column, I will illustrate some of the new features available to all Image objects, including BufferedImages. I'll leave the discussion of the new BufferedImageOp manipulations for next month's image processing column.

Transforms galore

You can perform the same kinds of transformations on images that you can perform on shapes, text strings, and other 2D graphics objects -- including the translation, rotation, scaling, and shearing transformations that we discussed last month.

In addition, images are considered to have their own Image Space. This space is a third space, a coordinate system from which the images are converted first into User Space and ultimately into Device Space for rendering. (Recall from last month that Device Space is the area into which the graphics will be rendered on the screen. This is analogous to the coordinates that are used when you create regular AWT-style, Graphics-based 2D graphics. User Space, however, is a translatable, rotatable coordinate system that may be operated on by one or more AffineTransforms).

Just as AffineTransforms may be used to transform the conversion of User Space objects into Device Space, they may also be used to transform Image Space-to-User Space conversions.

Example07 illustrates some Image-to-User and User-to-Device image transformations. For a good chuckle, I decided to transform and distort an image of myself (my JavaWorld column photograph), so you will see three images of me drawn after applying various transformations.

Here is how I manipulated the image.

1 2 Page 1
Page 1 of 2