I have created a dialog in which a user can browse for an image and then see a preview of the image drawn on a canvas. The image is scaled so that its aspect ratio is maintained while fitting in the box. I used the method of resizing found in this answer, which involves converting an image from SWT to AWT, performing the resize, converting back from AWT to SWT, and finally drawing it on the canvas. Since this process is very costly in terms of time and processing power, I elect to skip the resizing step if the image is exactly the correct size, and thus does not need to be transformed in any way.
The issue comes up when dealing with images with alpha transparency. In some cases, images that have transparency that are converted first are drawn on the canvas with a black background. A copy of the same image that has been sized to the exact size of the canvas, and thus is not converted, has a white background.
However, this is also not always the case. Some images with transparent backgrounds will always show as white, whether they've been converted or not.
What causes an image with a transparent background to draw with one color over another in an SWT canvas? How does the AWT conversion affect it, and how can I cause it to become consistent if I so desire?
Here is the conversion code, taken in whole from another source:
public static BufferedImage convertToAWT (ImageData data) {
ColorModel colorModel = null;
PaletteData palette = data.palette;
if (palette.isDirect) {
colorModel = new DirectColorModel(data.depth, palette.redMask, palette.greenMask, palette.blueMask);
BufferedImage bufferedImage = new BufferedImage(colorModel, colorModel.createCompatibleWritableRaster(data.width, data.height),
false, null);
WritableRaster raster = bufferedImage.getRaster();
int[] pixelArray = new int[3];
for (int y = 0; y < data.height; y++) {
for (int x = 0; x < data.width; x++) {
int pixel = data.getPixel(x, y);
RGB rgb = palette.getRGB(pixel);
pixelArray[0] = rgb.red;
pixelArray[1] = rgb.green;
pixelArray[2] = rgb.blue;
raster.setPixels(x, y, 1, 1, pixelArray);
}
}
return bufferedImage;
}
else {
RGB[] rgbs = palette.getRGBs();
byte[] red = new byte[rgbs.length];
byte[] green = new byte[rgbs.length];
byte[] blue = new byte[rgbs.length];
for (int i = 0; i < rgbs.length; i++) {
RGB rgb = rgbs[i];
red[i] = (byte) rgb.red;
green[i] = (byte) rgb.green;
blue[i] = (byte) rgb.blue;
}
if (data.transparentPixel != -1) {
colorModel = new IndexColorModel(data.depth, rgbs.length, red, green, blue, data.transparentPixel);
} else {
colorModel = new IndexColorModel(data.depth, rgbs.length, red, green, blue);
}
BufferedImage bufferedImage = new BufferedImage(colorModel, colorModel.createCompatibleWritableRaster(data.width, data.height),
false, null);
WritableRaster raster = bufferedImage.getRaster();
int[] pixelArray = new int[1];
for (int y = 0; y < data.height; y++) {
for (int x = 0; x < data.width; x++) {
int pixel = data.getPixel(x, y);
pixelArray[0] = pixel;
raster.setPixel(x, y, pixelArray);
}
}
return bufferedImage;
}
}
public static ImageData convertToSWT (BufferedImage bufferedImage) {
if (bufferedImage.getColorModel() instanceof DirectColorModel) {
DirectColorModel colorModel = (DirectColorModel) bufferedImage.getColorModel();
PaletteData palette = new PaletteData(colorModel.getRedMask(), colorModel.getGreenMask(), colorModel.getBlueMask());
ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(), colorModel.getPixelSize(), palette);
WritableRaster raster = bufferedImage.getRaster();
int[] pixelArray = new int[3];
for (int y = 0; y < data.height; y++) {
for (int x = 0; x < data.width; x++) {
raster.getPixel(x, y, pixelArray);
int pixel = palette.getPixel(new RGB(pixelArray[0], pixelArray[1], pixelArray[2]));
data.setPixel(x, y, pixel);
}
}
return data;
}
else if (bufferedImage.getColorModel() instanceof IndexColorModel) {
IndexColorModel colorModel = (IndexColorModel) bufferedImage.getColorModel();
int size = colorModel.getMapSize();
byte[] reds = new byte[size];
byte[] greens = new byte[size];
byte[] blues = new byte[size];
colorModel.getReds(reds);
colorModel.getGreens(greens);
colorModel.getBlues(blues);
RGB[] rgbs = new RGB[size];
for (int i = 0; i < rgbs.length; i++) {
rgbs[i] = new RGB(reds[i] & 0xFF, greens[i] & 0xFF, blues[i] & 0xFF);
}
PaletteData palette = new PaletteData(rgbs);
ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(), colorModel.getPixelSize(), palette);
data.transparentPixel = colorModel.getTransparentPixel();
WritableRaster raster = bufferedImage.getRaster();
int[] pixelArray = new int[1];
for (int y = 0; y < data.height; y++) {
for (int x = 0; x < data.width; x++) {
raster.getPixel(x, y, pixelArray);
data.setPixel(x, y, pixelArray[0]);
}
}
return data;
}
return null;
}
Ok, since I think I finally understand your requirements, I decided to post an answer. Let me make sure that I understood it correctly:
You want to show an Image in your app in some sort of Widget that can be resized. The image should resize with its parent and keep transparency working.
Instead of resizing the image and displaying it in a Label or some other Widget, you can use a Canvas and paint the image to the appropriate size using GC#drawImage(Image image, int srcX, int srcY, int srcWidth, int srcHeight, int destX, int destY, int destWidth, int destHeight).
To use that function, you need the size of the Image, the size of the Canvas and the size of a correctly scaled (aspect ratio) version of the image.
Here is the code:
public static void main(String[] args)
{
Display display = Display.getDefault();
final Shell shell = new Shell(display);
shell.setLayout(new GridLayout(1, false));
/* Load the image and calculate size and ratio */
final Image image = new Image(display, "settings.png");
final Rectangle imageSize = image.getBounds();
final double imageRatio = 1.0 * imageSize.width / imageSize.height;
/* Define the canvas and set the background color */
final Canvas canvas = new Canvas(shell, SWT.BORDER);
canvas.setBackground(display.getSystemColor(SWT.COLOR_DARK_GRAY));
canvas.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
canvas.addListener(SWT.Paint, new Listener()
{
#Override
public void handleEvent(Event e)
{
Rectangle canvasSize = canvas.getBounds();
double canvasRatio = 1.0 * canvasSize.width / canvasSize.height;
int newHeight;
int newWidth;
/* Determine scaled height and width of the image */
if (canvasRatio > imageRatio)
{
newWidth = (int) (imageSize.width * (1.0 * canvasSize.height / imageSize.height));
newHeight = (int) (canvasSize.height);
}
else
{
newWidth = (int) (canvasSize.width);
newHeight = (int) (imageSize.height * (1.0 * canvasSize.width / imageSize.width));
}
/* Compute position such that the image is centered in the canvas */
int top = (int) ((canvasSize.height - newHeight) / 2.0);
int left = (int) ((canvasSize.width - newWidth) / 2.0);
/* Draw the image */
e.gc.drawImage(image, 0, 0, imageSize.width, imageSize.height, left, top, newWidth, newHeight);
}
});
shell.pack();
shell.open();
while (!shell.isDisposed())
{
if (!display.readAndDispatch())
display.sleep();
}
display.dispose();
/* DISPOSE THE IMAGE !!! */
image.dispose();
}
And this is what it looks like after starting:
and after resizing:
Note: I didn't have time to test it on Windows, but I'm fairly confident that it works.
It works on Windows as well!
EDIT:
Add these lines to enable antialiasing:
e.gc.setAntialias(SWT.ON);
e.gc.setAdvanced(true);
It's a bit late for an answer now, but as I've just had a similar experience and issues, I thought my findings might help others.
The original problem is with the supplied code that does the SWT->AWT and AWT->SWT conversions. When using a direct palette, transparency (alpha) is not catered for at all, but it is for an indexed palette, and that's why some images work and some do not.
It's relatively simple to fix that code to cope with transparency, but there are better solutions that do not need to got via AWT to get a resized image.
If you don't care about anti-aliasing (smoothness) of the converted image then a simple solution is:
Image newImage = new Image(image.getDevice(),
image.getImageData().scaledTo(newWidth, newHeight));
If you do care about smoothness then the solution is almost as simple:
Image newImage = new Image(image.getDevice(), newWidth, newHeight);
GC gc = new GC(newImage);
gc.setAdvanced(true);
gc.setAntialias(SWT.ON);
gc.drawImage(image, 0, 0, origWidth, origHeight, 0, 0, newWidth, newHeight);
gc.dispose();
Related
I need to draw a new image over old image. I first opened both images in BufferedImage and changed their white background to transparent. Then I got a Graphics2D object from the bufferedImage of old image and called drawImage method of Graphics2D class. I then saved the old image to disk. When I open the saved image I find only the old image with white background changed to transparent. Can anyone suggest me what is error with my code or how can I get to fix my error ?
BufferedImage newImage = ImageIO.read(new File("new.png"));
BufferedImage oldImage = ImageIO.read(new File("old.png"));
newImage = makeWhiteTransparent(newImage);
oldImage = makeWhiteTransparent(oldImage);
Graphics2D graphics = (Graphics2D) oldImage.getGraphics();
graphics.drawImage(newImage,null, 0,0);
File outputImage = new File("merged.png");
ImageIO.write(oldImage, "png", outputImage);
My makeWhiteTransparent method goes like this:
public static BufferedImage makeWhiteTransparent(BufferedImage img){
BufferedImage dst = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
dst.getGraphics().drawImage(img, 0, 0, null);
int markerRGB = Color.WHITE.getRGB() | 0xFF000000;
int width = dst.getWidth();
int height = dst.getHeight();
for(int x = 0; x < width; x++){
for(int y = 0; y < height; y++){
int rgb = dst.getRGB(x, y);
if ( ( rgb | 0xFF000000 ) == markerRGB ) {
int value = 0x00FFFFFF & rgb;
dst.setRGB(x, y, value);
}
}
}
return dst;
}
I tried changing graphics.drawImage(newImage, null,0,0) to graphics.drawImage(newImage, 0,0, null) and also changing TYPE_4BYTE_ABGR to TYPE_INT_ARGB as suggested but it did nothing. The error still exists.
This needs to be changed:
graphics.drawImage(newImage,null, 0,0);
to
graphics.drawImage(newImage, 0,0, null);
you are using the wrong version of drawImage - check http://docs.oracle.com/javase/7/docs/api/java/awt/Graphics2D.html
--
Change also the type TYPE_4BYTE_ABGR to TYPE_INT_ARGB
--
Here's how it works for me:
public BufferedImage makeWhiteTransparent(BufferedImage img){
BufferedImage dst = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB);
dst.getGraphics().drawImage(img, 0, 0, null);
int markerRGB = 0x00ffffff; // Color.WHITE.getRGB() | 0xFF000000;
int width = dst.getWidth();
int height = dst.getHeight();
for(int x = 0; x < width; x++){
for(int y = 0; y < height; y++){
int rgb = dst.getRGB(x, y)&0x00ffffff;
if ( rgb == markerRGB ) {
int value = 0x00FFFFFF & rgb;
dst.setRGB(x, y, value);
}
}
}
return dst;
}
bim = makeWhiteTransparent(bim);
bim2 = makeWhiteTransparent(bim2);
Graphics2D graphics = (Graphics2D) bim.getGraphics();
graphics.drawImage(bim2,0,0, null);
g2.drawImage(bim, w/2-wc/2, h/2-hc/2, null);
I got the answer to my question finally. All I had to do was create a new BufferedImage and draw two images over it. Below is the code that works as expected:
BufferedImage newImage = ImageIO.read(new File("new.png"));
BufferedImage oldImage = ImageIO.read(new File("old.png"));
oldImage = makeWhiteTransparent(oldImage);
newImage = makeWhiteTransparent(newImage);
int width = Math.max(newImage.getWidth(), oldImage.getWidth());
int height = Math.max(newImage.getHeight(), oldImage.getHeight());
BufferedImage combined = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics graphics = combined.getGraphics();
graphics.drawImage(oldImage, 0, 0, null);
graphics.drawImage(newImage, 0, 0, null);
File outputImage = new File("merged.png");
ImageIO.write(combined, "PNG", outputImage);
I've been working towards grayscaling images in java for some time. I was using a colorConvertOp, but it seems after a load of images were put through the process, eventually the JVM would hang in a locked state in the op.
Now I've started using:
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_BYTE_GRAY);
Graphics g = image.getGraphics();
g.drawImage(img, 0, 0, null);
g.dispose();
However, I'm seeing a large spike in CPU, where it used to be under 20% and is now up to 120%. It also seems to be causing me memory leaks and eventually OOMs.
Is there an easier, quicker way to grayscale in java without using as much CPU/eliminates hanging from a JVM bug?
I wrote a java program to convert RGB image to GrayScaleImage. Hope this helps
public class GrayScale {
BufferedImage image;
int width;
int height;
public GrayScale() {
try {
File input = new File("input path of the image");
image = ImageIO.read(input);
width = image.getWidth();
height = image.getHeight();
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
Color c = new Color(image.getRGB(j, i));
int red = (int) (c.getRed() * 0.299);
int green = (int) (c.getGreen() * 0.587);
int blue = (int) (c.getBlue() * 0.114);
Color newColor = new Color(red + green + blue,
red + green + blue, red + green + blue);
image.setRGB(j, i, newColor.getRGB());
}
}
File ouptut = new File("output path of the image");
ImageIO.write(image, "jpg", ouptut);
} catch (Exception e) {
}
}
static public void main(String args[]) throws Exception {
GrayScale obj = new GrayScale();
}
}
How do I flip an Screenshot image? I can't find my problem anywhere else.Example code:
/*
*#param fileLoc //Location of fileoutput destination
*#param format //"png"
*#param WIDTH //Display.width();
*#param HEIGHT //Display.height();
*/
private void getScreenImage(){
int[] pixels = new int[WIDTH * HEIGHT];
int bindex;
// allocate space for RBG pixels
ByteBuffer fb = ByteBuffer.allocateDirect(WIDTH * HEIGHT * 3);//.order(ByteOrder.nativeOrder());
// grab a copy of the current frame contents as RGB
glReadPixels(0, 0, WIDTH, HEIGHT, GL_RGB, GL_UNSIGNED_BYTE, fb);
BufferedImage image = new BufferedImage(WIDTH, HEIGHT,BufferedImage.TYPE_INT_RGB);
// convert RGB data in ByteBuffer to integer array
for (int i=0; i < pixels.length; i++) {
bindex = i * 3;
pixels[i] =
((fb.get(bindex) << 16)) +
((fb.get(bindex+1) << 8)) +
((fb.get(bindex+2) << 0));
}
try {
//Create a BufferedImage with the RGB pixels then save as PNG
image.setRGB(0, 0, WIDTH, HEIGHT, pixels, 0 , WIDTH);
ImageIO.write(image, format , fileLoc);
}
catch (Exception e) {
System.out.println("ScreenShot() exception: " +e);
}
}
Basically the code works for capturing the screen and storing at as "png" format.
But it output's the image horizontally flipped, because glReadPixels();,
read from bottom-left to top-right.
So how do I flip the image horizontally before I ImageIO.write();?
Thanks in-front,
Rose.
E.G. of flipping an image horizontally using an AffineTransform.
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import javax.swing.*;
public class Test001 {
public static BufferedImage getFlippedImage(BufferedImage bi) {
BufferedImage flipped = new BufferedImage(
bi.getWidth(),
bi.getHeight(),
bi.getType());
AffineTransform tran = AffineTransform.getTranslateInstance(bi.getWidth(), 0);
AffineTransform flip = AffineTransform.getScaleInstance(-1d, 1d);
tran.concatenate(flip);
Graphics2D g = flipped.createGraphics();
g.setTransform(tran);
g.drawImage(bi, 0, 0, null);
g.dispose();
return flipped;
}
Test001(BufferedImage bi) {
JPanel gui = new JPanel(new GridLayout(1,2,2,2));
gui.add(new JLabel(new ImageIcon(bi)));
gui.add(new JLabel(new ImageIcon(getFlippedImage(bi))));
JOptionPane.showMessageDialog(null, gui);
}
public static void main(String[] args) throws AWTException {
final Robot robot = new Robot();
Runnable r = new Runnable() {
#Override
public void run() {
final BufferedImage bi = robot.createScreenCapture(
new Rectangle(0, 360, 200, 100));
new Test001(bi);
}
};
SwingUtilities.invokeLater(r);
}
}
It's worth noting that it might be faster to simply read the pixels out of the buffer in the order you want them, rather than read them backwards and do a costly transform operation. Additionally, since you know for sure that the BufferedImage is TYPE_INT_RGB it should be safe to write directly into its raster.
ByteBuffer fb = BufferUtils.createByteBuffer(WIDTH * HEIGHT * 3);
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
glReadPixels(0, 0, WIDTH, HEIGHT, GL_RGB, GL_UNSIGNED_BYTE, fb);
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
for (int i = pixels.length - 1; i >= 0; i--) {
int x = i % WIDTH, y = i / WIDTH * WIDTH;
pixels[y + WIDTH - 1 - x] = (fb.get() & 0xff) << 16 | (fb.get() & 0xff) << 8 | fb.get() & 0xff;
}
This SWT snippet converts a BufferedImage to SWT ImageData:
static ImageData convertToSWT(BufferedImage bufferedImage) {
if (bufferedImage.getColorModel() instanceof DirectColorModel) {
...
} else if (bufferedImage.getColorModel() instanceof IndexColorModel) {
...
}
return null;
}
The problem is, there is a third subclass of ColorModel: ComponentColorModel. And I need to convert an image using this color model. How do I do it?
Found here (but mind the patch in crosay's answer!)
if (bufferedImage.getColorModel() instanceof ComponentColorModel) {
ComponentColorModel colorModel = (ComponentColorModel)bufferedImage.getColorModel();
//ASSUMES: 3 BYTE BGR IMAGE TYPE
PaletteData palette = new PaletteData(0x0000FF, 0x00FF00,0xFF0000);
ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(), colorModel.getPixelSize(), palette);
//This is valid because we are using a 3-byte Data model with no transparent pixels
data.transparentPixel = -1;
WritableRaster raster = bufferedImage.getRaster();
int[] pixelArray = new int[3];
for (int y = 0; y < data.height; y++) {
for (int x = 0; x < data.width; x++) {
raster.getPixel(x, y, pixelArray);
int pixel = palette.getPixel(new RGB(pixelArray[0], pixelArray[1], pixelArray[2]));
data.setPixel(x, y, pixel);
}
}
return data;
In Romanov's answer, change the following line:
int[] pixelArray = new int[3];
with
int[] pixelArray = colorModel.getComponentSize();
How do I create an in-memory fully transparent SWT image and draw a black line on it with antialias enabled?
I expect the result to include only black color and alpha values ranging from 0 to 255 due to antialias...
I googled and tried everything that I could... is this possible at all?
This is how I did and it works:
Image src = new Image(null, 16, 16);
ImageData imageData = src.getImageData();
imageData.transparentPixel = imageData.getPixel(0, 0);
src.dispose();
Image icon = new Image(null, imageData);
//draw on the icon with gc
I was able to make this work, although it feels a bit hacky:
Display display = Display.getDefault();
int width = 10;
int height = 10;
Image canvas = new Image(display, width, height);
GC gc = new GC(canvas);
gc.setAntialias(SWT.ON);
// This sets the alpha on the entire canvas to transparent
gc.setAlpha(0);
gc.fillRectangle(0, 0, width, height);
// Reset our alpha and draw a line
gc.setAlpha(255);
gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
gc.drawLine(0, 0, width, height);
// We're done with the GC, so dispose of it
gc.dispose();
ImageData canvasData = canvas.getImageData();
canvasData.alphaData = new byte[width * height];
// This is the hacky bit that is making assumptions about
// the underlying ImageData. In my case it is 32 bit data
// so every 4th byte in the data array is the alpha for that
// pixel...
for (int idx = 0; idx < (width * height); idx++) {
int coord = (idx * 4) + 3;
canvasData.alphaData[idx] = canvasData.data[coord];
}
// Now that we've set the alphaData, we can create our
// final image
Image finalImage = new Image(canvasData);
// And get rid of the canvas
canvas.dispose();
After this, finalImage can be drawn into a GC with drawImage and the transparent parts will be respected.
I made it by allocating an ImageData, making it transparent then creating the Image from the data :
static Image createTransparentImage(Display display, int width, int height) {
// allocate an image data
ImageData imData = new ImageData(width, height, 24, new PaletteData(0xff0000,0x00ff00, 0x0000ff));
imData.setAlpha(0, 0, 0); // just to force alpha array allocation with the right size
Arrays.fill(imData.alphaData, (byte) 0); // set whole image as transparent
// Initialize image from transparent image data
return new Image(display, imData);
}
To scale with transparency, I've found that I have to manually set the alpha byte array as shown below. So the alpha ends up with nearest-neighbor anti aliasing.
public static Image scaleImage(Device device, Image orig, int scaledWidth, int scaledHeight) {
Rectangle origBounds = orig.getBounds();
if (origBounds.width == scaledWidth && origBounds.height == scaledHeight) {
return orig;
}
ImageData origData = orig.getImageData();
ImageData imData = new ImageData(scaledWidth, scaledHeight, origData.depth, origData.palette);
if (origData.alphaData != null) {
imData.alphaData = new byte[imData.width * imData.height];
for (int row = 0; row < imData.height; row++) {
for (int col = 0; col < imData.width; col++) {
int origRow = row * origData.height / imData.height;
int origCol = col * origData.width / imData.width;
byte origAlpha = origData.alphaData[origRow * origData.width + origCol];
imData.alphaData[row * imData.width + col] = origAlpha;
}
}
}
final Image scaled = new Image(device, imData);
GC gc = new GC(scaled);
gc.setAntialias(SWT.ON);
gc.setInterpolation(SWT.HIGH);
gc.setBackground(device.getSystemColor(SWT.COLOR_WHITE));
gc.fillRectangle(0, 0, scaledWidth, scaledHeight);
gc.drawImage(orig, 0, 0, origBounds.width, origBounds.height, 0, 0, scaledWidth, scaledHeight);
gc.dispose();
return scaled;
}