Java 2D Trickery: Antialiased Image Transforms
Note: The code snippets in this blog entry are intended to be used with Scott Violet's nifty Interactive Graphics Editor. Just cut and paste the code into that application, et voila: instant gratification. This allows you to tinker with the code and immediately see how your changes affect the rendering.
It's been a while since I posted a "Trickery" blog (previous installments in this series include "Soft Clipping" and "Light and Shadow"). This week at the Desktop Matters conference (which was great, by the way) someone asked if there was a way to antialias the edges of an image when rotating it in Java 2D. The kind gentleman noted that on Mac OS X with Apple's JDK, they do see antialiased edges if they enable
RenderingHints.VALUE_ANTIALIAS_ON, but they don't see the same behavior on Sun's JDK. The reason for that is Quartz: as I mentioned in the "Soft Clipping" entry, Apple's Java 2D implementation uses Quartz under the hood, which does antialias image edges in this particular case. (But as I mentioned there as well, Apple is moving away from their Quartz renderer by default in their JDK 6 implementation, so it would be nice if there was a solution for all platforms.)
It is true that Sun's Java 2D implementation does not automatically antialias image edges when rendering with
Graphics.drawImage(). However, there is a simple workaround: use
TexturePaint and render a transformed/antialiased
fillRect(). The following code demonstrates this technique:
import java.awt.geom.*; import java.awt.image.*; // Clear the background to black g.setColor(Color.BLACK); g.fillRect(0, 0, width, height); // Create the source image BufferedImage srcImage = new BufferedImage(50, 50, BufferedImage.TYPE_INT_RGB); Graphics2D g2 = srcImage.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setColor(Color.WHITE); g2.fillRect(0, 0, 50, 50); g2.setColor(Color.YELLOW); g2.fillOval(5, 5, 40, 40); g2.setColor(Color.BLACK); g2.fillOval(15, 15, 5, 5); g2.fillOval(30, 15, 5, 5); g2.drawOval(20, 30, 10, 7); g2.dispose(); // Render the image untransformed g.drawImage(srcImage, 15, 7, null); g.setColor(Color.WHITE); g.drawString("Untransformed", 80, 25); // Render the image rotated (with NEAREST_NEIGHBOR) AffineTransform xform = new AffineTransform(); xform.setToIdentity(); xform.translate(15, 70); xform.rotate(Math.PI/8, 25, 25); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); g.drawImage(srcImage, xform, null); g.setColor(Color.WHITE); g.drawString("Nearest Neighbor", 80, 85); // Render the image rotated (with BILINEAR) xform.setToIdentity(); xform.translate(15, 130); xform.rotate(Math.PI/8, 25, 25); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.drawImage(srcImage, xform, null); g.setColor(Color.WHITE); g.drawString("Bilinear", 80, 145); // Render the image rotated (with BILINEAR and antialiased edges) g.setColor(Color.WHITE); g.drawString("Bilinear, Antialiased", 80, 205); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setPaint(new TexturePaint(srcImage, new Rectangle2D.Float(0, 0, srcImage.getWidth(), srcImage.getHeight()))); xform.setToIdentity(); xform.translate(15, 190); xform.rotate(Math.PI/8, srcImage.getWidth()/2, srcImage.getHeight()/2); g.transform(xform); g.fillRect(0, 0, srcImage.getWidth(), srcImage.getHeight());
Here's the resulting screenshot showing the different options:
Note that there will likely be some performance hit from transforming the image in this manner, but if quality is of utmost importance (as it was in this developer's case), then the cost is probably acceptable. Hope this helps!
In my ears: The Fall, "Perverted By Language"
In my eyes: Yoshio Tsuchiya, "The Fine Art Of Japanese Food Arrangement"