How can I implement java.awt.Composite efficiently?
Asked Answered
G

1

7

Background: I need to be able to create imagery in "disabled" look. The commonly suggested approach is to convert images to grayscale and show the grayscaled image. The drawback is that it only works with images, making it cumbersome to show graphics where you do not have immediate access to an image in disabled state. Now I thought this could be done on the fly with java.awt.Composite (and then I would not need to know how for example an Icon is implemented to render it disabled). Only there seems to be no implementation to convert to grayscale, so I had to create my own...

That said, I hacked together an implementation (and it renders what I expect it to). But I'm not sure it will really work correctly for all cases (Javadocs of Composite/CompositeContext seem extremely thin for such a complex operation). And as you may see from my implementation I go a roundabout way to process pixel by pixel, as there seems to be no simple way to read/write pixels in bulk in a format not dictated by the involved rasters.

Any pointers to more extensive documentation / examples / hints are welcome.

Here's the SSCCE - it renders a (colored) GradientPaint through the DisabledComposite to convert the gradient to grayscale. Note that in the real world you will not know what is rendered an with what calls. The Gradient is really only an example (sorry, but too often people don't get that, so I'll make it explicit this time).

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Composite;
import java.awt.CompositeContext;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class CompositeSSCE implements Runnable {

    static class DisabledComposite implements Composite {
        @Override
        public CompositeContext createContext(
            final ColorModel srcColorModel,
            final ColorModel dstColorModel,
            final RenderingHints hints) {
            return new DisabledCompositeContext(srcColorModel, dstColorModel);
        }
    } 

    static class DisabledCompositeContext implements CompositeContext {

        private final ColorModel srcCM;
        private final ColorModel dstCM;

        final static int PRECBITS = 22;
        final static int WEIGHT_R = (int) ((1 << PRECBITS) * 0.299); 
        final static int WEIGHT_G = (int) ((1 << PRECBITS) * 0.578);
        final static int WEIGHT_B = (int) ((1 << PRECBITS) * 0.114);
        final static int SRCALPHA = (int) ((1 << PRECBITS) * 0.667);

        DisabledCompositeContext(final ColorModel srcCM, final ColorModel dstCM) {
            this.srcCM = srcCM;
            this.dstCM = dstCM;
        }

        public void compose(final Raster src, final Raster dstIn, final WritableRaster dstOut) {
            final int w = Math.min(src.getWidth(), dstIn.getWidth());
            final int h = Math.min(src.getHeight(), dstIn.getHeight());
            for (int y = 0; y < h; ++y) {
                for (int x = 0; x < w; ++x) {
                    int rgb1 = srcCM.getRGB(src.getDataElements(x, y, null));
                    int a1 = ((rgb1 >>> 24) * SRCALPHA) >> PRECBITS;
                    int gray = (
                        ((rgb1 >> 16) & 0xFF) * WEIGHT_R +
                        ((rgb1 >>  8) & 0xFF) * WEIGHT_G +
                        ((rgb1      ) & 0xFF) * WEIGHT_B
                        ) >> PRECBITS;
                    int rgb2 = dstCM.getRGB(dstIn.getDataElements(x, y, null));
                    int a2 = rgb2 >>> 24;
                    int r2 = (rgb2 >> 16) & 0xFF;
                    int g2 = (rgb2 >>  8) & 0xFF;
                    int b2 = (rgb2      ) & 0xFF;
                    // mix the two pixels
                    gray = gray * a1 / 255;
                    final int ta = a2 * (255 - a1);
                    r2 = gray + (r2 * ta / (255*255));
                    g2 = gray + (g2 * ta / (255*255));
                    b2 = gray + (b2 * ta / (255*255));
                    a2 = a1 + (ta / 255);
                    rgb2 = (a2 << 24) | (r2 << 16) | (g2 << 8) | b2; 
                    Object data = dstCM.getDataElements(rgb2, null);
                    dstOut.setDataElements(x, y, data);
                }
            }
        }

        @Override
        public void dispose() {
            // nothing for this implementation
        }
    }

    // from here on out its only the fluff to make this a runnable example
    public static void main(String[] argv) {
        Runnable r = new CompositeSSCE();
        SwingUtilities.invokeLater(r);
    }

    // simple component to use composite to render
    static class DemoComponent extends JComponent {
        // demonstrate rendering an icon in grayscale with the composite
        protected void paintComponent(Graphics g) {
            Graphics2D g2d = (Graphics2D) g;
            GradientPaint p = new GradientPaint(0, 0, Color.GREEN, 127, 127, Color.BLUE, true);
            g2d.setComposite(new DisabledComposite());
            g2d.setPaint(p);
            g2d.fillRect(0, 0, getWidth(), getHeight());
        }
    }

    // Fluff to use the Composite in Swing 
    public void run() {
        try {
            JFrame f = new JFrame("Test grayscale composite");
            DemoComponent c = new DemoComponent();
            c.setPreferredSize(new Dimension(500, 300));
            f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            f.setLayout(new BorderLayout());
            f.add(c, BorderLayout.CENTER);
            f.pack();
            f.setVisible(true);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}
Gerius answered 29/11, 2012 at 16:5 Comment(0)
G
4

You can always create a BufferedImage, draw whatever needs to be grey-scaled to the Graphics object for that image using bufferedImage.createGraphics(), and then use the javax.swing.GreyFilter to create a grey-scaled copy of the image.

This page shows you how to use a Filter.

You might also want to compare your approach with SwingX's BlendComposite, which I expect does the same thing as you are doing. BlendComposite has a Saturation mode that would allow grey-scale. (It also has more modes.)

This page has a demo of BlendComposite.

About efficiency, I expect there's no important difference between them, because there's intermediate steps, and from what I can see, complete copies of the image data with both. But if you retain the grey-scaled image using the first method I suggested, you can prevent recalculation of non-dynamic controls.

If you do one of the above, the performance will be correct, and I expect that's what you really want.

From your comments I guess you might just want to apply the effect to a component. For this you could use JLayer, only available in Java 7. From the Javadoc there is an example of overlaying a translucent green. You could replace this with grey. If you want to use JLayer in previous versions of Java you could use JXLayer, part of the SwingX project.

Goop answered 3/12, 2012 at 15:58 Comment(3)
BlendComposite looks like a useful Tool, thanks for the hint. Although from a quick glance it seems as if does not work with every possible ColorModel in its compose()-method, probably other ColorModels are not relevant in practice?. As for "You can always create a BufferedImage", I don't see how I could do that in the compose()-method implementatin. Doing it somewhere in paintComponent() would be possible, but its a lot less flexible than just setting the Composite and then proceed with the normal rendering chain.Gerius
@Gerius I don't know whether it's suitable for all color models (and I don't really know what ColorModel is used for in this context, although I can guess), what I do know, and the reason I suggested it, is SwingX has/had input from the official Swing team, so it should be flexible for common uses. With my first suggestion, you wouldn't use Composite, and I would do it in paintComponent -- it's just as flexible, just more work (create the image to the drawing size, draw to it, filter it, paint it; and cache the last one to use if the size is the same)....Goop
@Gerius you could create the Graphics for the BufferedImage and then call paintComponent with that graphics instance.Goop

© 2022 - 2024 — McMap. All rights reserved.