0

I've got two BufferedImages: one is TYPE_INT_ARGB, and the other is TYPE_BYTE_GRAY. How to replace the entire color image's alpha band with the grayscale image using API only, and without disturbing the RGB values?

final int width = 200;
final int height = 200;

final BufferedImage colorImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
final BufferedImage grayscaleImg = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g = colorImg.createGraphics();
// flip some mystical switches
// g.drawImage( grayscaleImg into colorImg's alpha band )
g.dispose();

I can do it manually by masking and copying bytes like this:

WritableRaster clrRaster = colorImg.getRaster();
DataBufferInt clrBuffer = (DataBufferInt) clrRaster.getDataBuffer();
int[] clrData = clrBuffer.getData();

WritableRaster grayRaster = grayscaleImg.getRaster();
DataBufferByte grayBuffer = (DataBufferByte) grayRaster.getDataBuffer();
byte[] grayData = grayBuffer.getData();

int pixel, alphaBits;
for(int i = 0; i < clrData.length; i++) {
    pixel = clrData[i] & 0x00ffffff;
    alphaBits = (int)grayData[i] << 24;
    clrData[i] = pixel | alphaBits;
}

However what's the API way?

UPDATE #1 Sample images: input grayscale alpha, input color cat, and output color cat with hole. grayscale of donutcolor catmerge result

The resulting image has the grayscale in the output color's alpha. View the resulting image in a photo editor, and you will see the hole in the middle is actually transparent.

private void injectAlphaIntoColor() {
    try {
        // BEWARE: this code does not check if both images are the same
        // width and height. May get out of bounds exception if w&h are 
        // different.


        BufferedImage cat = ImageIO.read(new File("s:/temp/cat5.png"));
        BufferedImage gray = ImageIO.read(new File("s:/temp/layermask.png"));

        // convert color cat to TYPE_INT_ARGB
        BufferedImage color = new BufferedImage(cat.getWidth(), cat.getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = color.createGraphics();
        g.drawImage(cat, 0, 0, null);
        g.dispose();

        final WritableRaster clrRaster = color.getRaster();
        final DataBufferInt clrBuffer = (DataBufferInt) clrRaster.getDataBuffer();
        final int[] clrData = clrBuffer.getData();

        final WritableRaster grayRaster = gray.getRaster();
        final DataBufferByte grayBuffer = (DataBufferByte) grayRaster.getDataBuffer();
        final byte[] grayData = grayBuffer.getData();

        int pixel, alphaBits;

        // manually put each grayscale pixel into each color pixel's alpha
        for(int i = 0; i < clrData.length; i++) {
            pixel = clrData[i] & 0x00ffffff;
            alphaBits = (int)grayData[i] << 24;
            clrData[i] = pixel | alphaBits;
        }

        ImageIO.write(color, "png", new File("s:/temp/3rd_output.png"));
    } 

    catch (IOException ex) {
        System.out.println(ex.getMessage());
    }
}

To restate my original objective, what is the Java API way to doing what the above code does?

  • [Have you tried using a `AlphaCompoiste`](https://docs.oracle.com/javase/tutorial/2d/advanced/compositing.html)? – MadProgrammer Nov 23 '17 at 03:19
  • Of course, you could create a third (coloured) `BufferdImage`, paint the grey scaled image to it and then paint the original colored image into of that – MadProgrammer Nov 23 '17 at 03:30
  • @MadProgrammer #1 reply Yes I have seen Graphics2D.setComposite(AlphaComposite). https://docs.oracle.com/javase/tutorial/2d/advanced/compositing.html doesn't present an option to write only to the alpha band. AlphaComposite.DST_IN looks the closest to what I want. The problem is "the alpha from the source is applied to the destination pixels in the overlapping area." Every grayscale pixel has an implicit alpha of 1.0. The final image has alpha = 1.0 on every pixel. I need Java to view my grayscale as an alpha channel and not as an image. – deskwarrior Nov 23 '17 at 03:54
  • @MadProgrammer #2 Wouldn't painting the grayscale into a third image bring me to the same problem I have now? Because Java sees the grayscale as an image, the grayscale values would get drawn into the RGB values; and every alpha would be 1.0. – deskwarrior Nov 23 '17 at 03:55
  • I could be misinterpreting your question, but if you paint the grayscale image onto a new (color based) image and the paint the original color ontop, wouldn't that generate a image where the original image's alpha based pixels would be replaced by the grey scale? – MadProgrammer Nov 23 '17 at 04:01
  • Perhaps, you should provide three images - two original, one desired result, cause, I'm confused (and tired and neck deep in debugging memory issues) – MadProgrammer Nov 23 '17 at 04:28
  • I'm guessing that you will want to use a BufferedImageFilter along with a BufferedImageOp, but I don't know enough about these things to produce a real answer. – Hovercraft Full Of Eels Nov 23 '17 at 05:01
  • @deskwarrior Thank you for the images, it makes more sense. You can use a `AlphaComposite.SRC_OUT` to generate the result you're after, but first you need to convert the grey scale image to a alpha based image (so, for example, the "white" is a alpha of 0) - sorry for my confusion – MadProgrammer Nov 23 '17 at 22:40
  • @MadProgrammer. I have drawn the grayscale image (white border, black circle) to a BufferedImage.TYPE_INT_ARGB. The RGB bands look like the grayscale image; the entire alpha band is white/0xff. How to set the alpha values of the white border to black/0x00 (without manipuating the DataBuffer's bits)? – deskwarrior Nov 23 '17 at 23:18
  • @deskwarrior Yeah, that's kind of the issue - I did find a different solution which applied a grey scaled image (as a mask) to a color image, demonstrated as an answer – MadProgrammer Nov 23 '17 at 23:23

2 Answers2

2

Ahh, you want to cut portion of an image out using a mask, my bad.

The simply solution - use a alpha based mask to start with. But I assume you don't have that option. I did try finding a solution which might do that, but instead, stumbled across this example instead.

Which is capable of producing the result you seem to be looking for

Cutout

(blue is the background color of the panel)

The core functionality comes do to...

public void applyGrayscaleMaskToAlpha(BufferedImage image, BufferedImage mask) {
    int width = image.getWidth();
    int height = image.getHeight();

    int[] imagePixels = image.getRGB(0, 0, width, height, null, 0, width);
    int[] maskPixels = mask.getRGB(0, 0, width, height, null, 0, width);

    for (int i = 0; i < imagePixels.length; i++) {
        int color = imagePixels[i] & 0x00ffffff; // Mask preexisting alpha
        int alpha = maskPixels[i] << 24; // Shift blue to alpha
        imagePixels[i] = color | alpha;
    }

    image.setRGB(0, 0, width, height, imagePixels, 0, width);
}

But as a simple runnable example...

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (Exception ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public void applyGrayscaleMaskToAlpha(BufferedImage image, BufferedImage mask) {
        int width = image.getWidth();
        int height = image.getHeight();

        int[] imagePixels = image.getRGB(0, 0, width, height, null, 0, width);
        int[] maskPixels = mask.getRGB(0, 0, width, height, null, 0, width);

        for (int i = 0; i < imagePixels.length; i++) {
            int color = imagePixels[i] & 0x00ffffff; // Mask preexisting alpha
            int alpha = maskPixels[i] << 24; // Shift blue to alpha
            imagePixels[i] = color | alpha;
        }

        image.setRGB(0, 0, width, height, imagePixels, 0, width);
    }

    public class TestPane extends JPanel {

        private BufferedImage master;
        private BufferedImage mask;

        public TestPane() {
            setBackground(Color.BLUE);
            try {
                master = ImageIO.read(new File("..."));
                mask = ImageIO.read(new File("..."));

                applyGrayscaleMaskToAlpha(master, mask);
            } catch (IOException exp) {
                exp.printStackTrace();
            }
        }

        @Override
        public Dimension getPreferredSize() {
            Dimension size = super.getPreferredSize();
            if (master != null && mask != null) {
                size = new Dimension(master.getWidth() + mask.getWidth(), Math.max(master.getHeight(), mask.getHeight()));
            }
            return size;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            int x = (getWidth() - (master.getWidth() + mask.getWidth())) / 2;
            int y = (getHeight() - master.getHeight()) / 2;
            g.drawImage(master, x, y, this);

            x += mask.getWidth();
            y = (getHeight() - mask.getHeight()) / 2;
            g.drawImage(mask, x, y, this);
        }

    }

}

Sorry for the confusion

Updated - without "manipulating the data buffers"

I was looking for the Java API way of doing the same job without manipuating the bits

Okay, so I'd also prefer a solution which generated a alpha based image from a gray scale image, it fits better with the overall Graphics 2D API. So, after a little more reading of this question which keeps on giving, I stumbled across this idea...

public static BufferedImage grayScaleToTransparency(BufferedImage master) {
    ImageFilter filter = new RGBImageFilter() {
        public final int filterRGB(int x, int y, int rgb) {
            return (rgb << 16) & 0xFF000000;
        }
    };

    ImageProducer ip = new FilteredImageSource(master.getSource(), filter);
    Image img = Toolkit.getDefaultToolkit().createImage(ip);
    
    BufferedImage buffer = createCompatibleImage(img.getWidth(null), img.getHeight(null), Transparency.TRANSLUCENT);
    Graphics2D g2d = buffer.createGraphics();
    g2d.drawImage(img, 0, 0, null);
    g2d.dispose();
    
    return buffer;
}

Now, there might be away to get this using BufferedImageFilter along with a BufferedImageOp but I don't have the time or experience to investigate it further.

Using this technique I was able to produce...

Masked progression

Original Image | Original (gray scale) mask | Alpha based Mask | Masked image

Again, the blue is the background color of the panel.

import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageFilter;
import java.awt.image.ImageProducer;
import java.awt.image.RGBImageFilter;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (Exception ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public static GraphicsConfiguration getGraphicsConfiguration() {
        return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
    }

    public static BufferedImage createCompatibleImage(int width, int height, int transparency) {
        BufferedImage image = getGraphicsConfiguration().createCompatibleImage(width, height, transparency);
        image.coerceData(true);
        return image;
    }

    public static void applyQualityRenderingHints(Graphics2D g2d) {
        g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
        g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
    }

    public static BufferedImage applyMask(BufferedImage master, BufferedImage mask) {
        int imgWidth = master.getWidth();
        int imgHeight = master.getHeight();

        BufferedImage imgMask = createCompatibleImage(imgWidth, imgHeight, Transparency.TRANSLUCENT);
        Graphics2D g2 = imgMask.createGraphics();
        applyQualityRenderingHints(g2);

        g2.drawImage(mask, 0, 0, null);
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN, 1f));
        g2.drawImage(master, 0, 0, null);
        g2.dispose();

        return imgMask;
    }

    public static BufferedImage grayScaleToTransparency(BufferedImage master) {
        ImageFilter filter = new RGBImageFilter() {
            public final int filterRGB(int x, int y, int rgb) {
                return (rgb << 16) & 0xFF000000;
            }
        };

        ImageProducer ip = new FilteredImageSource(master.getSource(), filter);
        Image img = Toolkit.getDefaultToolkit().createImage(ip);

        BufferedImage buffer = createCompatibleImage(img.getWidth(null), img.getHeight(null), Transparency.TRANSLUCENT);
        Graphics2D g2d = buffer.createGraphics();
        g2d.drawImage(img, 0, 0, null);
        g2d.dispose();

        return buffer;
    }

    public class TestPane extends JPanel {

        private BufferedImage master;
        private BufferedImage originalMask;
        private BufferedImage alphaMask;
        private BufferedImage masked;

        public TestPane() {
            setBackground(Color.BLUE);
            try {
                master = ImageIO.read(new File("/Users/swhitehead/Downloads/lIceL.png"));
                originalMask = ImageIO.read(new File("/Users/swhitehead/Downloads/MXmFp.png"));
                alphaMask = grayScaleToTransparency(originalMask);
                masked = applyMask(master, alphaMask);
//                tinted = tint(master, mask);
            } catch (IOException exp) {
                exp.printStackTrace();
            }
        }

        protected int desiredWidth() {
            return master.getWidth() + originalMask.getWidth() + alphaMask.getWidth() + masked.getWidth();
        }

        protected int desiredHeight() {
            return Math.max(Math.max(Math.max(master.getHeight(), originalMask.getHeight()), alphaMask.getHeight()), masked.getHeight());
        }

        @Override
        public Dimension getPreferredSize() {
            Dimension size = super.getPreferredSize();
            if (master != null && originalMask != null) {
                size = new Dimension(desiredWidth(),
                        desiredHeight());
            }
            return size;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            int x = (getWidth() - desiredWidth()) / 2;
            int y = (getHeight() - master.getHeight()) / 2;
            g.drawImage(master, x, y, this);

            x += originalMask.getWidth();
            y = (getHeight() - originalMask.getHeight()) / 2;
            g.drawImage(originalMask, x, y, this);

            x += alphaMask.getWidth();
            y = (getHeight() - alphaMask.getHeight()) / 2;
            g.drawImage(alphaMask, x, y, this);

            x += masked.getWidth();
            y = (getHeight() - masked.getHeight()) / 2;
            g.drawImage(masked, x, y, this);
        }

    }

}
Community
  • 1
  • 1
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Maybe I read the question wrong, but I thought that he wanted to use the gray scale pixels to become the alpha values of the color image. – Hovercraft Full Of Eels Nov 23 '17 at 04:22
  • @Hovercraft Full Of Eels. You did read my question correctly. – deskwarrior Nov 23 '17 at 04:25
  • @HovercraftFullOfEels Okay, I'm confused – MadProgrammer Nov 23 '17 at 04:26
  • @MadProgrammer Your code that masks and sets bits is the same as what I posted at the top. Yes manipuating bits does the job. I was looking for the Java API way of doing the same job without manipuating the bits. – deskwarrior Nov 23 '17 at 23:23
  • @deskwarrior You can't, that's just how it HAS to work. You could have a closer look at [the linked question](https://stackoverflow.com/questions/221830/set-bufferedimage-alpha-mask-in-java) which provides additional options, one uses a `ImageFilter` to convert the greyscale image to a alpha based image, from there you could use the API functionality. This might be my preferred solution, but I don't have time to look at it – MadProgrammer Nov 23 '17 at 23:31
  • @deskwarrior Updated :P – MadProgrammer Nov 23 '17 at 23:58
  • @MadProgrammer Thanks for putting in so much of your time on my question. I viewed the other posting and all the replys touch bits. As far as _that's just how it HAS to work_ maybe not. Please see grayscaleInto4ByteABGR() in my answer; it does the job without ever touching the bits. Is there an even better way? Method grayscaleIntoIntARGB() comes in second because the DataBuffer is accessed once. – deskwarrior Nov 24 '17 at 02:48
  • @deskwarrior All your really doing is copying the data buffer from color domain to another, so, while you not "manipulating" the bits directly, you're still manipulating the buffers, I assume that the copy process takes a full copy of the buffer, so, internally, there's some manipulation. I still prefer a solution which provides a new copy of the image over manipulating the original, which is why I prefer the second solution I presented. You could take a closer look at `BufferedImageFilter` and `BufferedImageOp`, which is still a bit manipulation process, but it's a preferred one – MadProgrammer Nov 24 '17 at 02:56
  • @deskwarrior I dawns on me (yeah, I know, took me a while), you don't seem to want to do any bit arithmetic or shifting - any reason why? – MadProgrammer Nov 24 '17 at 03:13
  • @MadProgrammer API can abstract hardware acceleration. If the API detects a GPU then all the work goes to hardware, otherwise the CPU does the work at worse. Coding the bit arithmetic means the work stays on the CPU. If I call API, then Java may (if supported) send it to the GPU. So sure those API calls are indirectly manipulating buffers and perhaps full copies are taking place, but those operations performed in a GPU are still faster than from a JRE. It's kind of like coding my own verticies*matricies that will only run on the CPU, versus sending all the data and orders to DirectX. – deskwarrior Nov 24 '17 at 04:25
  • @deskwarrior Ahh, cool :) - I'd be interested to know if the buffer transfer is been executed on the GPU or not – MadProgrammer Nov 24 '17 at 04:25
  • Start another question asking how to do that? lol. Good nite. – deskwarrior Nov 24 '17 at 04:28
0

My thanks and credit to @Hovercraft Full Of Eels for suggesting BufferedImageOp, which lead to BandCombineOp. When fed with a suitable matrix, the class can manipulate any or all the bands. Lighten, darken, move, and copy bands without ever accessing DataBuffer or manipulating bits.

How to put an image into the alpha band

  1. Load or paint an image; this will become the alpha band. The image can be color or grayscale.
  2. Convert the image to BufferedImage.TYPE_BYTE_GRAY
  3. Convert the grayscale into a BufferedImage.TYPE_INT_ARGB. The color bands will contain near identical copies of the grayscale.
  4. Copy a color band to the alpha band using BandCombineOp. See toAlpha() for matrix.

This is the better way to move/copy a band over coding bit manipulations because it's mathematically elegant, and API may take advantage of video acceleration hardware (if present and supported by Java.)

Sample Java app: Embed alpha

An app is included to demonstrate the procedure. Select a color source and alpha source, and the app will composite them into one image. The two source images, intermediate images, and the composite are displayed for sanity. Right-click on any image to save.

Color source: cat

Color source

Alpha source: bird Alpha source

Alpha source: green band copied to alpha band Alpha source: green to alpha

Composite

color + alpha composite

To run the app: (1) create a JavaFX project named embedalpha, (2) delete the contents of the auto-generated .java file, (3) paste my code, and (4) run.

To trace the procedure, put a breakpoint in method handleBuildComposite(). Ignore appendGallery(). toAlpha() does the band copy.

/*
Your use of any portion of the code constitutes your acceptance of full
responsibility for all wonderful and aweful outcomes.
 */
package embedalpha;

import java.awt.AlphaComposite;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.BandCombineOp;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.io.File;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.ImagePattern;
import javafx.scene.paint.Paint;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javax.imageio.ImageIO;

/**
 * Demonstrate copying/moving a color band to the alpha band within the same
 * image using only API. The specific method is toAlpha().
 * 
 * @author deskwarrior
 * @see https://stackoverflow.com/questions/47446922/draw-grayscale-image-to-another-bufferedimages-alpha-band-using-api-only
 */
public class TheApp extends Application {
    private final int NODE_SPACING = 5;
    private final int TILE_SIZE = 40;
    private final int APP_WIDTH;
    private final int APP_HEIGHT;

    private final ObjectProperty<BufferedImage> colorSource = new SimpleObjectProperty<>(this, "colorSource", null);
    private final ObjectProperty<BufferedImage> alphaSource = new SimpleObjectProperty<>(this, "alphaSource", null);
    private final Paint backgroundColor;
    private final HBox gallery = new HBox(NODE_SPACING);
    private final IntegerProperty gallerySize = new SimpleIntegerProperty(this, "gallerySize", 0);
    private final Tooltip canvasTip = new Tooltip("Right-click to save image as PNG.");
    private Canvas colorSourceCanvas = null;
    private Canvas alphaSourceCanvas = null;

    private final FileChooser openFC = new FileChooser();
    private final FileChooser saveFC = new FileChooser();
    private File lastDirectoryVisited = null;

    private final RenderingHints colorHints = new RenderingHints(
            RenderingHints.KEY_COLOR_RENDERING, 
            RenderingHints.VALUE_COLOR_RENDER_QUALITY);
    private final ICC_ColorSpace grayColorSpace;
    private final ICC_ColorSpace rgbColorSpace;

    private final BorderPane root = new BorderPane();
    private Stage stage = null;


    public TheApp() {
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        APP_WIDTH = screenSize.width;
        APP_HEIGHT = screenSize.height * 2 / 3;

        openFC.getExtensionFilters().addAll(
            new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg", "*.gif", "*.bmp"),
            new FileChooser.ExtensionFilter("All Files", "*.*"));

        saveFC.setTitle("Save image as PNG");
        saveFC.getExtensionFilters().addAll(
            new FileChooser.ExtensionFilter("Portable network graphics", "*.png"));

        colorHints.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
        ICC_Profile grayProfile = ICC_Profile.getInstance(ColorSpace.CS_GRAY);
        grayColorSpace = new ICC_ColorSpace(grayProfile);
        ICC_Profile rgbProfile = ICC_Profile.getInstance(ColorSpace.CS_sRGB);
        rgbColorSpace = new ICC_ColorSpace(rgbProfile);

        Image tile = createCheckeredTile();
        backgroundColor = new ImagePattern(tile, 0, 0, TILE_SIZE, TILE_SIZE, false);
    }

    /**
     * Convert a BufferedImage to another color space.
     * 
     * @param src cannot be TYPE_CUSTOM or null
     * @param dstImageType valid BufferedImage types: TYPE_BYTE_GRAY, 
     * TYPE_INT_RGB, TYPE_INT_ARGB
     * @return BufferedImage in the new color space. never null.
     * @exception NullPointerException when src is null
     * @exception IllegalArgumentException when dstImageType is not one of the
     * three accepted types
     */
    private BufferedImage toImageType(BufferedImage src, int dstImageType) {
        BufferedImage dst;

        if(src.getType() == dstImageType)
            dst = src;

        else {
            ColorSpace dstColorSpace;

            switch(dstImageType) {
                case BufferedImage.TYPE_BYTE_GRAY:
                    dstColorSpace = grayColorSpace;
                    break;

                case BufferedImage.TYPE_INT_RGB:
                case BufferedImage.TYPE_INT_ARGB:
                    dstColorSpace = rgbColorSpace;
                    break;

                default:
                    String msg = String.format("Conversion to BufferedImage type %d not supported.", dstImageType);
                    throw new IllegalArgumentException(msg);
            }

            /*
            Using ColorConvertOp because the documentation promises images
            with pre-multiplied alphas will be divided out.

            Another possibility is dst.createGraphics().drawImage(src). Whether 
            drawImage() divides out premultiplied alphas is unknown to me, and  
            I don't feel like tracing the method to find out.
             */
            ColorSpace srcColorSpace = src.getColorModel().getColorSpace();
            ColorConvertOp op = new ColorConvertOp(srcColorSpace, dstColorSpace, colorHints);
            dst = new BufferedImage(src.getWidth(), src.getHeight(), dstImageType);
            op.filter(src, dst);
        }

        return dst;
    }

    /**
     * Starting point for merging color source and alpha source into one image.
     * 
     * @param e 
     */
    private void handleBuildComposite(ActionEvent e) {
        try {
            e.consume();

            /*
            Color source's RGB bands will become the composite's RGB bands.
            If color source is ARGB then discard the alpha band to remove
            any premultiplied alpha values.
            If color source is grayscale then convert to RGB in preparation
            for merge.
            */
            final BufferedImage colorSourceRGB = toImageType(getColorSource(), BufferedImage.TYPE_INT_RGB);
            appendGallery(colorSourceRGB, "Color source: RGB");

            /*
            One of alpha source's RGB bands will become the composite's alpha 
            band. 
            If alpha source is a color image, then convert to grayscale to get 
            R == G == B; at the expense of some information. If a color band 
            were copied to the alpha band without conversion, the resulting 
            alpha band wouldn't accurately represent the original color image 
            because R != G != B.
            If alpha source is a grayscale image then no change.
             */
            final BufferedImage alphaSourceGray = toImageType(getAlphaSource(), BufferedImage.TYPE_BYTE_GRAY);
            appendGallery(alphaSourceGray, "Alpha source: grayscale");

            /*
            The objective of this app is to copy/move a color band into the 
            alpha band. Problem is grayscales don't have an alpha band. Convert 
            to ARGB to give it one.
            */
            final BufferedImage alphaRGBA = toImageType(alphaSourceGray, BufferedImage.TYPE_INT_ARGB);
            appendGallery(alphaRGBA, "Alpha source: grayscale to RGBA");

            /*
            Method toAlpha() is where the magic happens. Copy/move one of the 
            color bands into the alpha band.
             */
            final BufferedImage trueAlpha = toAlpha(alphaRGBA);
            appendGallery(trueAlpha, "Alpha source: Green to Alpha, RGB to white");

            /*
            Composite the color values with the alpha values into one image.
            Copy colorSourceRGB's RGB into the composite's RGB. 
            Copy trueAlpha's alpha into the composite's alpha.
            */
            BufferedImage colorPlusAlpha = toComposite(colorSourceRGB, trueAlpha);
            appendGallery(colorPlusAlpha, "Color + alpha composite");
        }

        catch(Exception ex) {
            final Alert dlg = new Alert(Alert.AlertType.ERROR, ex.getMessage(), ButtonType.CLOSE);
            dlg.showAndWait();
        }
    }

    /**
     * Copy the green band into the alpha band.
     * 
     * @param src presumed to be some type with an alpha band. You're screwed
     * if it isn't.
     * @return a BufferedImage with the green band in the alpha band, and
     * the RGB bands set to white. Never null.
     */
    private BufferedImage toAlpha(BufferedImage src) {
        /*
        This matrix specifies which band(s) to manipulate and how. The 3-ones
        in the right-most column sets dst's RGB bands to white. The solitary
        one in the bottom row will copy the green band into dst's alpha band.

        Footnote: when the grayscale was converted to ARGB I expected the RGB
        bands to be identical. After some testing the bands were found to be
        *near* identical; it seems a color conversion from grayscale to 
        RGB is not as simple as a bulk memory copy. The differences are low 
        enough that no one would've noticed a difference had the either the 
        red or blue band been chosen over the green band.
        */
        final float[][] matrix = new float[][] {
            {0, 0, 0, 1},
            {0, 0, 0, 1},
            {0, 0, 0, 1},
            {0, 1, 0, 0}};

        BandCombineOp op = new BandCombineOp(matrix, null);
        BufferedImage dst = new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
        op.filter(src.getRaster(), dst.getRaster());
        return dst;
    }

    /**
     * Composite color's RGB bands with alpha's alpha band into one image.
     * 
     * @param color anything except BufferedImage.TYPE_CUSTOM and null.
     * @param alpha anything with an alpha band otherwise very bad
     * @return a BufferedImage of the two inputs. never null.
     */
    private BufferedImage toComposite(BufferedImage color, BufferedImage alpha) {
        final int width = Math.max(color.getWidth(), alpha.getWidth());
        final int height = Math.max(color.getHeight(), alpha.getHeight());
        final BufferedImage dst = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = dst.createGraphics();
        g.drawImage(color, null, 0, 0);

        /*
        AlphaComposite.DST_IN ignores alpha's RGB bands and copies just the
        alpha band.
        */
        g.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_IN));
        g.drawImage(alpha, null, 0, 0);
        g.dispose();
        return dst;
    }

    private Image createCheckeredTile() {
        final Canvas can = new Canvas(TILE_SIZE, TILE_SIZE);
        final GraphicsContext gc = can.getGraphicsContext2D();
        final int haf = TILE_SIZE / 2;

        gc.setFill(Color.DARKGRAY);
        gc.fillRect(0, 0, haf, haf); // top-left
        gc.fillRect(haf, haf, haf, haf); // bottom-right


        gc.setFill(Color.DIMGREY);
        gc.fillRect(haf, 0, haf, haf); // top-right
        gc.fillRect(0, haf, haf, haf); // bottom-left

        WritableImage snapshot = can.snapshot(null, null);
        return snapshot;
    }

    private void loadImage(String title, ObjectProperty<BufferedImage> imageObj) {
        try {
            openFC.setTitle(title);
            openFC.setInitialDirectory(lastDirectoryVisited);
            File filePath = openFC.showOpenDialog(stage);
            if(filePath != null) {
                lastDirectoryVisited = filePath.getParentFile();
                BufferedImage image = ImageIO.read(filePath);
                imageObj.set(image);
            }
        }

        catch(Exception ex) {
            final Alert dlg = new Alert(Alert.AlertType.ERROR, ex.getMessage(), ButtonType.CLOSE);
            dlg.showAndWait();
        }
    }

    private void saveImage(MouseEvent e) {
        try {
            if(e.getButton() == MouseButton.SECONDARY) {
                e.consume();
                saveFC.setInitialDirectory(lastDirectoryVisited);
                File filePath = saveFC.showSaveDialog(stage);
                if(filePath != null) {
                    lastDirectoryVisited = filePath.getParentFile();
                    Canvas canvas = (Canvas) e.getSource();
                    BufferedImage img = (BufferedImage) canvas.getUserData();
                    ImageIO.write(img, "png", filePath);
                }
            }
        }

        catch(Exception ex) {
            final Alert dlg = new Alert(Alert.AlertType.ERROR, ex.getMessage(), ButtonType.CLOSE);
            dlg.showAndWait();
        }
    }

    private Canvas appendGallery(BufferedImage src, String desc) {
        final int width = src.getWidth();
        final int height = src.getHeight();
        final Canvas canvas = new Canvas(width, height);

        canvas.setUserData(src);
        Tooltip.install(canvas, canvasTip);
        canvas.setOnMouseReleased(this::saveImage);

        final GraphicsContext gc = canvas.getGraphicsContext2D();
        final Image image = SwingFXUtils.toFXImage(src, null);
        gc.setFill(backgroundColor);
        gc.fillRect(0, 0, width, height);
        gc.drawImage(image, 0, 0);

        String desc2 = desc != null && !desc.isEmpty() ? desc : "?";
        if(src.isAlphaPremultiplied())
            desc2 += ", premultiplied alpha";
        gc.setStroke(Color.LIME);
        gc.strokeText(desc2, 5, 20);

        gallery.getChildren().add(canvas);
        return canvas;
    }

    private boolean neitherColorNorAlpha(Node n) {
        boolean colorOrAlpha = 
            ((colorSourceCanvas != null) && (n == colorSourceCanvas)) ||
            ((alphaSourceCanvas != null) && (n == alphaSourceCanvas));
        return !colorOrAlpha;
    }

    private void setupMenu() {
        Button loadColorSource = new Button("Load color source ...");
        loadColorSource.setTooltip(new Tooltip("The image's color values will become the composite's color values."));
        loadColorSource.setOnAction((e) -> {
            e.consume();
            loadImage(loadColorSource.getText(), colorSource);
            colorSourceCanvas = appendGallery(getColorSource(), "Color source");
        });

        Button loadAlphaSource = new Button("Load alpha source ...");
        loadAlphaSource.setTooltip(new Tooltip("The image's color values will become the composite's alpha values."));
        loadAlphaSource.setOnAction((e) -> {
            e.consume();
            loadImage(loadAlphaSource.getText(), alphaSource);
            alphaSourceCanvas = appendGallery(getAlphaSource(), "Alpha source");
        });

        Button buildComposite = new Button("Build composite");
        buildComposite.setTooltip(new Tooltip("Merge color and alpha into one image."));
        buildComposite.setOnAction(this::handleBuildComposite);
        buildComposite.disableProperty().bind(colorSource.isNull().or(alphaSource.isNull()));

        Button clearGallery = new Button("Clear gallery");
        clearGallery.setTooltip(new Tooltip("Keep the current color source and alpha source; discard other images."));
        clearGallery.disableProperty().bind(gallerySize.lessThanOrEqualTo(2));
        clearGallery.setOnAction((e) -> {
            e.consume();
            gallery.getChildren().removeIf(this::neitherColorNorAlpha);
        });

        FlowPane parent = new FlowPane(NODE_SPACING, NODE_SPACING);
        parent.setAlignment(Pos.CENTER);
        parent.getChildren().addAll(loadColorSource, loadAlphaSource, buildComposite, clearGallery);
        parent.setPadding(new Insets(NODE_SPACING));
        root.setTop(parent);
    }

    private void setupGallery() {
        gallery.setPadding(new Insets(NODE_SPACING));
        ObservableList<Node> children = gallery.getChildren();
        children.addListener(new ListChangeListener<Node>() {
            @Override
            public void onChanged(ListChangeListener.Change c) {
                setGallerySize(children.size());
            }
        });
        ScrollPane scroll = new ScrollPane(gallery);
        scroll.setPannable(true);
        scroll.setStyle("-fx-background-color: transparent;");
        root.setCenter(scroll);
    }

    @Override
    public void start(Stage primaryStage) {
        stage = primaryStage;
        setupMenu();
        setupGallery();

        Scene scene = new Scene(root, APP_WIDTH, APP_HEIGHT);
        stage.setTitle("Embed alpha");
        stage.setScene(scene);
        stage.setResizable(true);
        stage.show();
    }

    public int getGallerySize() {
        return gallerySize.get();
    }

    public void setGallerySize(int value) {
        gallerySize.set(value);
    }

    public IntegerProperty gallerySizeProperty() {
        return gallerySize;
    }

    public BufferedImage getAlphaSource() {
        return alphaSource.get();
    }

    public void setAlphaSource(BufferedImage value) {
        alphaSource.set(value);
    }

    public ObjectProperty alphaSourceProperty() {
        return alphaSource;
    }

    public BufferedImage getColorSource() {
        return colorSource.get();
    }

    public void setColorSource(BufferedImage value) {
        colorSource.set(value);
    }

    public ObjectProperty colorSourceProperty() {
        return colorSource;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }
}
Community
  • 1
  • 1