Do Not Draw Text On Translucent Images

Or more precisely, Do Not Draw LCD-Optimized/Sub-Pixel Anti-Aliased Text On Translucent Images (thanks, Chris.)

I was working last night on a user interface that Ben Galbraith asked me to pimp when I stumbled upon a very interesting rendering bug. To improve the application’s appearance, I changed a scroll pane to make it fade out its content on the edges. As soon as I committed the change, Ben noticed that the antialiased text in the scroll pane’s children was not as nice as it used to. It looked like crap in fact.

Rendering bugs are usually not very hard to catch. Unfortunately, that one in particular proved to be a little subtler than I expected. The following shows what this bug looks like, how it appeared in our application and how you can avoid it.

Our journey starts with a rather dull window, whose background is solid black. Nothing fancy but this will be of importance later on. Here is a screenshot to wet your appetite:

Blank window

The source code itself is not very complicated either. A new JFrame is created and its content pane’s background color is set to black. Because an empty window is more boring than the Mass said in Latin, we add a custom component called TextItem. Its sole purpose is to display a piece of text in a nice casing. The source code of this class is the following:

class TextItem extends JComponent {
    private String text;

    TextItem(String text) {
        this.text = text;

        setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));

    public Dimension getPreferredSize() {
        return new Dimension(400, 60);

    protected void paintComponent(Graphics g) {
        setFont(getFont().deriveFont(Font.BOLD, 22.0f));

        Graphics2D g2 = (Graphics2D) g.create();

        Insets insets = getInsets();
        int x = insets.left;
        int y =;
        int width = getWidth() - insets.left - insets.right;
        int height = getHeight() - - insets.bottom;

        // top gradient
        g2.setPaint(new GradientPaint(0.0f, y,
                                      new Color(220, 220, 220, 140),
                                      0.0f, y + height / 2.0f,
                                      new Color(220, 220, 220, 80)));
        g2.fillRect(x, y, width, height / 2);

        // bottom gradient
        g2.setPaint(new GradientPaint(0.0f, y + height / 2.0f + 5.0f,
                                      new Color(255, 255, 255, 0),
                                      0.0f, y + height,
                                      new Color(255, 255, 255, 70)));
        g2.fillRect(x, y + height / 2, width, height / 2);

        // border
        g2.drawRect(x, y, width, height);

        // text
        FontMetrics fontMetrics = g2.getFontMetrics();
        g2.drawString(text, x + 10, y + (height / 2) + fontMetrics.getAscent() / 3);


The source code is pretty straightforward and does nothing unexpected. It is an non-opaque component that paints a couple of gradients and a String. The result is similar to the following screenshot:

Looks good!

Let us now introduce a new container, called CachedPanel, which paints its children in an offscreen buffer and then draws that buffer on the screen. This class replicates the behavior of the custom scroll pane I wrote for Ben’s application. To create the fade-out effect on the edges, I paint the scroll pane’s content into an image and then apply a couple of gradients with the AlphaComposite.DstIn composite. The following source code shows how to cache the panel’s content in a buffer:

class CachedPanel extends JPanel {
    private BufferedImage buffer = null;

    CachedPanel() {
        super(new BorderLayout());

    protected void paintChildren(Graphics g) {
        if (isCacheDirty()) {

        // paints children in the cache
        Graphics2D g2 = buffer.createGraphics();
        copySettings(g, g2);

        // draws cache on screen
        g.drawImage(buffer, 0, 0, null);

    private static void copySettings(Graphics src, Graphics dst) {

    private void clearCache(Graphics2D g2) {
        Composite composite = g2.getComposite();
        g2.fillRect(0, 0, buffer.getWidth(), buffer.getHeight());

    private void createCache() {
        buffer = new BufferedImage(getWidth(), getHeight(),

    private boolean isCacheDirty() {
        return buffer == null || buffer.getWidth() != getWidth() ||
               buffer.getHeight() != getHeight();

Once again, the code doesn’t do anything fancy. All we need to do now is replace the JFrame’s content pane by this particular panel. When we run the application the result looks similar to the previous screenshot but is not exactly the same. Can you spot the difference?

Crappy anti-aliasing

If you cannot tell the difference, take a look at the following picture. It shows the word “org” from both versions of the application. The first word in the picture comes from the regular application. The second one comes from the cached version. Look at the letters edges: you can clearly see sub-pixel anti-aliasing at work in the first version, and not in the second version.

Anti-aliasings compared

Can you quickly tell the reason why the anti-aliasing are different? I couldn’t. The problem lies in TextItem. Because its container provides a black background, TextItem chose the lazy route and does not paint its own black background. It only paints two translucent gradients.

The code is perfectly valid for Swing: TextItem does not paint all of its allocated pixel with an opaque color and declares itself as being non-opaque (remember, JComponent is non-opaque by default.)

The problem arises when the component is painted in a translucent image. When you do so, the image has an empty background. That means the antialiasing algorithm is not able to use surrounding pixels colors to smooth the text’s edges. On the contrary, when the painting occurs directly on screen, or more precisely on Swing’s back buffer, the parent container’s solid black background is already painted. Therefore, the anti-aliasing algorithm picks the black color to perform its magic.

To better understand what happens, exactly, download a PNG copy of CachedPanel’s buffer (showing this picture embedded in this page would not make sense since you are supposed to see the background is missing.) The picture contains only two semi-translucent gradients and the text.

Fixing this bug is quite simple. If you intend to paint a component that contains anti-aliased in an offscreen image, make sure the text is painted over a solid background in that very same component.

You can download the full source code of the example. This version lacks the fix, which is left as an exercise.

23 Responses to “Do Not Draw Text On Translucent Images”

  1. You should probably qualify that title to say “Do Not Draw *LCD-optimized* Text On Translucent Images”… Your technique of caching text in a translucent image should work perfectly fine for non-AA and grayscale-AA text of any color. The problems you cite are more a specific artifact of the LCD-optimized text rendering algorithm (which is highly dependent on src/dst color values) than a general problem with all text AA algorithms.

  2. Romain Guy says:

    That was the original title but it was too long for the blog’s layout :p And sub-pixel AA is the only true AA for text!

  3. caipiniko says:

    Hello Romain,
    Thx for taking all of us to the next level in Swing. I’ve been trying your code today but I have a incorrect behavior: if I use it your textItem in a pane all other components stop being repainted… Maybe the gradient consumes all the awt time… but i’m sure you have just made the code simple for the sample and you can tell me what to do to avoid this :-)


  4. Romain Guy says:

    caipiniko: I suspect I must dispose() the wrong Graphics but that’d be surprising. Similarly, there’s no reason why the gradient would take all of the AWT time. Do you have a screenshot or a test app I could run?

  5. caipiniko says:

    Hello again,

    actually its quite easy to reproduce:

    just replace the buildContent() method in ApplicationFrame with this one (i just add another component). Then you take the window out of the screen or you put another window above for an instant. After that the second component is not refreshed anymore.

    private void buildContent() {
    LayoutManager layout=new BorderLayout(2,2);
    add(new TextItem(“”),BorderLayout.NORTH);
    add(new JLabel(“repaint error”),BorderLayout.SOUTH);

    I’ve tried to play with the dispose method but without success…


  6. Romain Guy says:

    I can’t reproduce the problem :)

  7. BrianM says:

    When I run the demo, I get no antialiasing at all.
    If I look up the Graphics2D rendering hint at the top of paintChildren in CachedPanel, i.e.


    It reports “Nonantialiased rendering mode”

    If I understand correctly I should be getting some sort of antialiasing even if it’s not subpixel AA without any modification to the code, but I am not seeing that. Is there some sort of system property I should be setting when I run the AA text demo?

    If I go into and add to the paintComponent method:

    g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);

    Then I do get subpixel AA without doing any other modifications to the code. According to your explanation, that should not work since you’re still drawing text on top of the translucent gradients.
    I have tried this on both Ubuntu/Gnome and Windows/Vista with identical results. Could this behavior be specific to Mac OS X?

  8. alexp says:

    Hello Romain

    Interesting article, thanks

    By the way
    using JXLayer with BufferedPainter
    you will do the same effect faster


  9. Romain Guy says:

    Yes but I don’t want to use JXLayer :p Speaking of which, this effect is equally easy with SwingX’s painters with the cache set to true.

  10. alexp says:

    It’s a pity

    See you

  11. andry says:

    Fm9qQf comment3 ,

  12. Justin says:

    Whoa… this site is pretty awesome :) your layout is really well designed, and your blogs are (judging from what i’ve read) very interesting. heehee… consider yourself favorited. :-P

  13. Nith says:


    We are having a Table in which two columns have a Image icon in it. Based on the Data an image is drawn and set to the Column.

    We create a Buffered Image offscreen and place in a Imageholder. This Image holder is added to the label in the Table column.

    We are facing a problem in that. The Table has n No of rows[in thousands]. In some of the rows the image is incorrect. Images in some other screens are displayed in some of the rows. The problem is strange.

    Please let us know if you have any clues on this problem.