如何使用单独的 alpha 栅格创建 BufferedImage

How to create BufferedImage with a separate alpha raster

动机: 我的目标是以最有效的方式将 AWT BufferedImage 转换为 SWT ImageData。这个问题的典型答案是对整张图片进行逐像素转换,即 O(n^2) 复杂度。如果他们可以按原样交换整个像素矩阵,效率会更高。 BufferedImage 在详细确定颜色和 alpha 的编码方式方面似乎非常灵活。

为了给您提供更广泛的上下文,我使用 Apache Batik 编写了一个 SVG 图标按需光栅器,但它是用于 SWT (Eclipse) 应用程序的。 Batik 仅呈现 java.awt.image.BufferedImage,但 SWT 组件需要 org.eclipse.swt.graphics.Image

它们的支持栅格对象:java.awt.image.Rasterorg.eclipse.swt.graphics.ImageData 表示完全相同的东西,它们只是围绕表示像素的字节值的二维数组进行包装。如果我可以使一个或另一个使用颜色编码,瞧,我可以按原样重用支持数组。

我已经走得很远了,这很有效:

// 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;
}

一切正常,除了透明度:(

看起来 ImageData 要求(总是?)alpha 是一个单独的栅格,请参阅颜色栅格中的 ImageData.alphaData,请参阅 ImageData.data;都是 byte[] 类型。

有没有办法让ImageData接受ARGB模型?那是 alpha 与其他颜色的混合?我怀疑所以我走了另一条路。使 BufferedImage 对颜色和 alpha 使用单独的数组(也称为光栅或 "band")。 ComponentColorModelBandedRaster 似乎正是为了这些东西。

到目前为止我到达这里:

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);
}

这为 alpha 创建了一个单独的光栅(波段),但也为每种颜色分别创建了一个光栅(波段),所以我最终得到 4 个波段(4 个光栅),这对于 SWT Image 来说也是不可用的。 是否可以创建具有 2 个波段的带状光栅:一个用于 RGB 或 BRG 中的颜色,一个仅用于 alpha?

我不太了解 SWT,但根据我对 API 文档的理解,以下内容应该有效:

诀窍是使用伪装成 "banded" 缓冲区的自定义 DataBuffer 实现,但在内部使用交错 RGB 和单独的 alpha 数组的组合进行存储。这与标准 BandedSampleModel 配合得很好。使用此模型,您将失去任何通常应用于 BufferedImages 的特殊(硬件)优化的机会,但这无关紧要,因为无论如何您都在使用 SWT 进行显示。

我建议您先创建 SWT 图像,然后 "wrap" 来自自定义数据缓冲区中 SWT 图像的颜色和 alpha 数组。如果你这样做,Batik 应该直接渲染到你的 SWT 图像,然后你可以扔掉 BufferedImage (如果这不切实际,你当然也可以反过来做,但是您可能需要在下面公开自定义数据缓冲区 class 的内部数组,以创建 SWT 图像)。

代码(重要部分是 SWTDataBuffer class 和 createImage 方法):

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();
    }
}