Search code examples
javaswtawtbufferedimageraster

How to create BufferedImage with a separate alpha raster


Motivation: My goal is to convert AWT BufferedImage to SWT ImageData in the most efficient way. Typical answer to this question is pixel by pixel conversion of the whole picture, that is O(n^2) complexity. Much more efficient would be if they could exchange whole pixel matrix as it is. BufferedImage seems to be very flexible in determining in detail how colors and alpha are encoded.

To provide you with a wider context, I wrote a SVG icon on demand rasterizer, using Apache Batik, but it is for SWT (Eclipse) application. Batik renders only to a java.awt.image.BufferedImage, but SWT components require org.eclipse.swt.graphics.Image.

Their backing raster objects: java.awt.image.Raster and org.eclipse.swt.graphics.ImageData represent exactly same thing, they are just wrapper around a 2D array of byte values representing pixels. If I can make one or the other to use came color encoding, voila, I can reuse the backing array as it is.

I got pretty far, this works:

// defined blank "canvas" for Batik Transcoder for SVG to be rasterized there
public BufferedImage createCanvasForBatik(int w, int h) {
    new BufferedImage(w, h, BufferedImage.TYPE_4BYTE_ABGR);
}

// convert AWT's BufferedImage  to SWT's ImageData to be made into SWT Image later
public ImageData convertToSWT(BufferedImage bufferedImage) {
    DataBuffer db = bufferedImage.getData().getDataBuffer();
    byte[] matrix = ((DataBufferByte) db).getData();

    PaletteData palette =
            new PaletteData(0x0000FF, 0x00FF00, 0xFF0000); // BRG model

    // the last argument contains the byte[] with the image data
    int w = bufferedImage.getWidth(); 
    int h = bufferedImage.getHeight();

    ImageData swtimgdata = new ImageData(w, h, 32, palette);
    swtimgdata.data = matrix; // ImageData has all field public!!

    // ImageData swtimgdata = new ImageData(w, h, 32, palette, 4, matrix);  ..also works
    return swtimgdata;
}

It all works except transparency :(

It looks like ImageData requires (always?) alpha to be a separate raster, see ImageData.alphaData from color raster, see ImageData.data; both are byte[] types.

Is there a way how to make ImageData to accept ARGB model? That is alpha mixed with other colors? I doubt so I went the other way. To make BufferedImage to use separate arrays (aka rasters or "band") for colors and alpha. The ComponentColorModel and BandedRaster seem to intended exactly for these things.

So far I got here:

public BufferedImage createCanvasForBatik(int w, int h) {
    ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
    int[] nBits = {8, 8, 8, 8}; // ??
    ComponentColorModel colorModel = new ComponentColorModel(cs, nBits, true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
    WritableRaster raster = Raster.createBandedRaster(
        DataBuffer.TYPE_BYTE, w, h, 4, new Point(0,0));
    isPremultiplied = false;
    properties = null;
    return new BufferedImage(colorModel, raster, isPremultiplied, properties);
}

That creates a separate raster (band) for alpha but also for every color separately, so I end up with 4 bands (4 rasters) which is again unusable for SWT Image. Is it possible to create a banded raster with 2 bands: one for colors in RGB or BRG, and one for alpha only?


Solution

  • I don't know SWT in detail, but based on my understand of the API doc, the below should work:

    The trick is to use a custom DataBuffer implementation that masquerades as a "banded" buffer, but internally uses a combination of interleaved RGB and separate alpha array for storage. This works nicely with the standard BandedSampleModel. You will lose any chance of special (hardware) optimizations that are normally applied to BufferedImages using this model, but that should not matter as you are using SWT for display anyway.

    I suggest you create your SWT image first, and then "wrap" the color and alpha arrays from the SWT image in the custom data buffer. If you do it this way, Batik should render directly to your SWT image, and you can just throw away the BufferedImage afterwards (if this is not practical, you can of course do it the other way around as well, but you may need to expose the internal arrays of the custom data buffer class below, to create the SWT image).

    Code (important parts are the SWTDataBuffer class and createImage method):

    public class SplitDataBufferTest {
        /** Custom DataBuffer implementation using separate arrays for RGB and alpha.*/
        public static class SWTDataBuffer extends DataBuffer {
            private final byte[] rgb; // RGB or BGR interleaved
            private final byte[] alpha;
    
            public SWTDataBuffer(byte[] rgb, byte[] alpha) {
                super(DataBuffer.TYPE_BYTE, alpha.length, 4); // Masquerade as banded data buffer
                if (alpha.length * 3 != rgb.length) {
                    throw new IllegalArgumentException("Bad RGB/alpha array lengths");
                }
                this.rgb = rgb;
                this.alpha = alpha;
            }
    
            @Override
            public int getElem(int bank, int i) {
                switch (bank) {
                    case 0:
                    case 1:
                    case 2:
                        return rgb[i * 3 + bank];
                    case 3:
                        return alpha[i];
                }
                throw new IndexOutOfBoundsException(String.format("bank %d >= number of banks, %d", bank, getNumBanks()));
            }
    
            @Override
            public void setElem(int bank, int i, int val) {
                switch (bank) {
                    case 0:
                    case 1:
                    case 2:
                        rgb[i * 3 + bank] = (byte) val;
                        return;
                    case 3:
                        alpha[i] = (byte) val;
                        return;
                }
    
                throw new IndexOutOfBoundsException(String.format("bank %d >= number of banks, %d", bank, getNumBanks()));
            }
        }
    
        public static void main(String[] args) {
            // These are given from your SWT image
            int w = 300;
            int h = 200;
            byte[] rgb = new byte[w * h * 3];
            byte[] alpha = new byte[w * h];
    
            // Create an empty BufferedImage around the SWT image arrays
            BufferedImage image = createImage(w, h, rgb, alpha);
    
            // Just to demonstrate that it works
            System.out.println("image: " + image);
            paintSomething(image);
            showIt(image);
        }
    
        private static BufferedImage createImage(int w, int h, byte[] rgb, byte[] alpha) {
            DataBuffer buffer = new SWTDataBuffer(rgb, alpha);
            // SampleModel sampleModel = new BandedSampleModel(DataBuffer.TYPE_BYTE, w, h, 4); // If SWT data is RGB, you can use simpler constructor
            SampleModel sampleModel = new BandedSampleModel(DataBuffer.TYPE_BYTE, w, h, w,
                    new int[] {2, 1, 0, 3}, // Band indices for BGRA
                    new int[] {0, 0, 0, 0});
    
            WritableRaster raster = Raster.createWritableRaster(sampleModel, buffer, null);
            ColorModel colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
            return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null);
        }
    
        private static void showIt(final BufferedImage image) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JFrame frame = new JFrame("Test");
                    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    
                    JLabel label = new JLabel(new ImageIcon(image));
                    label.setOpaque(true);
                    label.setBackground(Color.GRAY);
                    frame.add(label);
    
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        private static void paintSomething(BufferedImage image) {
            int w = image.getWidth();
            int h = image.getHeight();
            int qw = w / 4;
            int qh = h / 4;
    
            Graphics2D g = image.createGraphics();
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    
            g.setColor(Color.ORANGE);
            g.fillOval(0, 0, w, h);
    
            g.setColor(Color.RED);
            g.fillRect(5, 5, qw, qh);
            g.setColor(Color.WHITE);
            g.drawString("R", 5, 30);
    
            g.setColor(Color.GREEN);
            g.fillRect(5 + 5 + qw, 5, qw, qh);
            g.setColor(Color.BLACK);
            g.drawString("G", 5 + 5 + qw, 30);
    
            g.setColor(Color.BLUE);
            g.fillRect(5 + (5 + qw) * 2, 5, qw, qh);
            g.setColor(Color.WHITE);
            g.drawString("B", 5 + (5 + qw) * 2, 30);
    
            g.dispose();
        }
    }