5

I looked for an easy way to tint an image in Java but I found nothing that suited my needs. I went to the following solution:

First create a new Image that serves as a copy of the Image I want to tint, then I create a second Image that is a transparent mask of the Image I want to tint and then draw the tint - mask over my copy and return the copy:

public static BufferedImage tintImage(Image original, int r, int g, int b){
    int width = original.getWidth(null);
    int height = original.getHeight(null);
    BufferedImage tinted = new BufferedImage(width, height, BufferedImage.TRANSLUCENT);
    Graphics2D graphics = (Graphics2D) tinted.getGraphics();
    graphics.drawImage(original, 0, 0, width, height, null);
    Color c = new Color(r,g,b,128);
    Color n = new Color(0,0,0,0);
    BufferedImage tint = new BufferedImage(width, height, BufferedImage.TRANSLUCENT);
    for(int i = 0 ; i < width ; i++){
        for(int j = 0 ; j < height ; j++){
            if(tinted.getRGB(i, j) != n.getRGB()){
                tint.setRGB(i, j, c.getRGB());
            }
        }
    }
    graphics.drawImage(tint, 0, 0, null);
    graphics.dispose();
    return tinted;
}

A solution for images that had no transparent pixels (e.g. did not make use of the alpha - channel) was to simply use fillRect() on the whole image, but that didn't work on images with transparent pixels as those then had the chosen color instead of still being invisible.

Does anyone know a way to do this more efficiently as methods I found here were rather unsatisfying and I plan on doing this tinting on many images (most having a grey-ish tone to them so they are easy to be tinted) at runtime about 50 times per second.

Pre - Generating all needed images at startup and / or caching generated images might be a solution but it feels awkward in some way to me, though if nothing can be done then nothing can be done.

Someone linked this: http://www.javalobby.org/articles/ultimate-image/

It was helpful but did not cover tinting.

Ben
  • 51,770
  • 36
  • 127
  • 149
salbeira
  • 2,375
  • 5
  • 26
  • 40
  • 1
    Like this? http://stackoverflow.com/a/4248459/59087 – Dave Jarvis Jan 08 '13 at 23:21
  • Already read this and it doesnt work with the setXORMode - it generates strange results that are not realy predictable and I realy don't understand the prerequisites he has (black image etc.) to use this properly. – salbeira Jan 08 '13 at 23:31
  • Edit: Ok read through it again and I now know the problem: XORing color adds the tint to the pixel and does not "override" part of it to the needed color - as such "gray" changes to "white" when applying a red - yellowish tint to it because "gray" already has a load of 1-s in it's rgba color bitmap. (Though some 1s should change back to 0 ... so ... I don't realy get why it always goes white). – salbeira Jan 08 '13 at 23:42

1 Answers1

9

Essentially, you need to use a little black magic and it wouldn't hurt to have a sacrifice or two on hand...

enter image description here

public class TestTint {

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

    public TestTint() {
        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 generateMask(BufferedImage imgSource, Color color, float alpha) {
        int imgWidth = imgSource.getWidth();
        int imgHeight = imgSource.getHeight();

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

        g2.drawImage(imgSource, 0, 0, null);
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN, alpha));
        g2.setColor(color);

        g2.fillRect(0, 0, imgSource.getWidth(), imgSource.getHeight());
        g2.dispose();

        return imgMask;
    }

    public BufferedImage tint(BufferedImage master, BufferedImage tint) {
        int imgWidth = master.getWidth();
        int imgHeight = master.getHeight();

        BufferedImage tinted = createCompatibleImage(imgWidth, imgHeight, Transparency.TRANSLUCENT);
        Graphics2D g2 = tinted.createGraphics();
        applyQualityRenderingHints(g2);
        g2.drawImage(master, 0, 0, null);
        g2.drawImage(tint, 0, 0, null);
        g2.dispose();

        return tinted;
    }

    public class TestPane extends JPanel {

        private BufferedImage master;
        private BufferedImage mask;
        private BufferedImage tinted;

        public TestPane() {
            try {
                master = ImageIO.read(new File("C:/Users/swhitehead/Documents/My Dropbox/MegaTokyo/Miho_Small.png"));
                mask = generateMask(master, Color.RED, 0.5f);
                tinted = tint(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() + tinted.getWidth(), Math.max(Math.max(master.getHeight(), mask.getHeight()), tinted.getHeight()));
            }
            return size;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            int x = (getWidth() - (master.getWidth() + mask.getWidth() + tinted.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);

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

    }

}

The general idea behind this technique is to generate a "mask" of the image, I take no credit of this idea, I stole it of the web, if I can find where, I'll post a link.

Once you have the mask, you can then render the two images together. Because I've already applied a alpha level to the mask, I don't need to reapply a alpha composite once I'm done.

PS - I create a compatible image for this example. I do this simply because it will render faster on the graphics device, this is not a requirement, it is simply the code I have on hand ;)

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Yeah while looking at the whole lot of the code you do something interestingly different, though the tint-mask idea is the same. I'd like to know what exactly happens in your "generateMask" method since you simply use a "fill rect" with the whole screen. Is it because of the AlphaComposite.SRC_IN ? – salbeira Jan 08 '13 at 23:49
  • 1
    @salbeira Bingo :) - I use this technquie to generate drop shadows and glow effects for non-opaque components and images all the time (by reversing the paint order, ie paint the mask UNDER the master image, usually after I've applied a blur)... – MadProgrammer Jan 08 '13 at 23:53
  • @salbeira You might find [this](http://docs.oracle.com/javase/tutorial/2d/advanced/compositing.html) of some help – MadProgrammer Jan 08 '13 at 23:54
  • @salbeira The other benefit of this approach is it's generally faster then trying to manipulate the individual pixles of the image - Don't ask me why, it's just what I've noticed ;) – MadProgrammer Jan 08 '13 at 23:55
  • Yes thats exactly what I tested right now, though I am concerned because it all needs too much time I think: Whithout tinting I reach ~ 800 FPS , when tinting ONE image (about 300 x 8 pixels big) with my method I reach 400 - 450 FPS while using yours I reach 450 - 500 FPS. So when I limit my FPS to 50 I don't mention any difference but I wonter what might happen if I tint and paint multiple images. Caching might still be "the" solution. – salbeira Jan 09 '13 at 00:05
  • @salbeira A lot will come down to the size of the image and the color model involved (hence the reason I use `createCompatibleImage`). You could try sub-imaging the master image in small chunks. Not sure that would make much difference... – MadProgrammer Jan 09 '13 at 00:11