Resizing an indexed image in Java without losing transparency - java

This is my function to resize images.
The quality is not photoshop but it's acceptable.
What's not acceptable is the behaviour on indexed png.
We expect that if we scale down an image with a 256 colors palette with a transparent index we would get a resized image with same transparency, but this it not the case.
So we did the resize on a new ARGB image and then we reduce it to 256 colors. The problem is how to "reintroduce" the transparent pixel index.
private static BufferedImage internalResize(BufferedImage source, int destWidth, int destHeight) {
int sourceWidth = source.getWidth();
int sourceHeight = source.getHeight();
double xScale = ((double) destWidth) / (double) sourceWidth;
double yScale = ((double) destHeight) / (double) sourceHeight;
Graphics2D g2d = null;
BufferedImage resizedImage = new BufferedImage(destWidth, destHeight, BufferedImage.TRANSLUCENT);
log.debug("resizing image to w:" + destWidth + " h:" + destHeight);
try {
g2d = resizedImage.createGraphics();
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_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
AffineTransform at = AffineTransform.getScaleInstance(xScale, yScale);
g2d.drawRenderedImage(source, at);
} finally {
if (g2d != null)
g2d.dispose();
}
//doesn't keep the transparency
if (source.getType() == BufferedImage.TYPE_BYTE_INDEXED) {
log.debug("reducing to color-indexed image");
BufferedImage indexedImage = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_BYTE_INDEXED);
try {
Graphics g = indexedImage.createGraphics();
g.drawImage(resizedImage, 0, 0, null);
} finally {
if (g != null)
g.dispose();
}
System.err.println("source" + ((IndexColorModel) source.getColorModel()).getTransparentPixel()
+ " " + ((IndexColorModel) indexedImage.getColorModel()).getTransparentPixel());
return indexedImage;
}
return resizedImage;
}

Try changing
BufferedImage indexedImage = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_BYTE_INDEXED);
to
BufferedImage indexedImage = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) source.getColorModel());
Even if that specifically doesn't help you (which it might not if the resizing, for whatever reason, changes what specific color values are indexed), the fact that you can create a new BufferedImage with a given IndexColorModel will probably be quite useful for you.
http://download.oracle.com/javase/6/docs/api/java/awt/image/BufferedImage.html#BufferedImage%28int,%20int,%20int,%20java.awt.image.IndexColorModel%29
EDIT: Just noticed that your resizedImage constructor should probably use BufferedImage.TYPE_INT_ARGB rather than BufferedImage.TRANSLUCENT. Not sure if that will change how it works, but BufferedImage.TRANSLUCENT isn't supposed to be passed to that form of the constructor. http://download.oracle.com/javase/1,5.0/docs/api/java/awt/image/BufferedImage.html#BufferedImage%28int,%20int,%20int%29
Anyway, maybe try something like this:
DirectColorModel resizedModel = (DirectColorModel) resizedImage.getColorModel();
int numPixels = resizedImage.getWidth() * resizedImage.getHeight();
byte[numPixels] reds;
byte[numPixels] blues;
byte[numPixels] greens;
byte[numPixels] alphas;
int curIndex = 0;
int curPixel;
for (int i = 0; i < resizedImage.getWidth(); i++)
{
for (int j = 0; j < resizedImage.getHeight(); j++)
{
curPixel = resizedImage.getRGB(i, j);
reds[curIndex] = resizedModel.getRed(curPixel);
blues[curIndex]= resizedModel.getBlue(curPixel);
greens[curIndex] = resizedModel.getGreen(curPixel);
alphas[curIndex] = resizedModel.getAlpha(curPixel);
curIndex++;
}
}
BufferedImage indexedImage = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_BYTE_INDEXED, new IndexColorModel(resizedModel.pixel_bits, numPixels, reds, blues, greens, alphas));
Don't know if this will actually work, though.

Indexed images with transparency are a hack. They only work under certain conditions and resizing isn't one of them.
An image with transparency doesn't just have fully opaque and fully transparent pixels. In particular at a irregularly shaped borders, there are many pixels with partial transparency. If you save it in a format with indexed colors where a single color is used for transparent pixels, you have to decide what color the background will have. All pixels with partial transparency are then blended between their color and the background color (according to their transparency) and become fully opaque. Only the fully transparent pixel are assigned the transparent pseudo color.
If such an image is displayed against a background with differnt color, an ugly border will become apparent. It's an artifact of the inadequate transparency handling.
When you resize the image, you introduce more artifacts. The color of a new pixels is usually blended from several neighboring pixels. If some are transparent and some are opaque, the result is a partially transparent pixel. When you save it, the partially transparent pixel is blended against the background color and becomes opaque. As a result, the opaque area (and the associated artifacts) grow with each resize (or most other image manipulations).
Whatever programming language or graphics library you use, the artifacts will grow and the result will become worse. I recommend you use a ARGB buffer and save the image as a non-indexed PNG file.

Related

How to rotate a buffered image without cropping it? Is there any way to rotate a JLayeredPane or JLabel?

I had searched about it but I did not get straight forward answer.
I want a buffered image to be rotated but not cropped
I knew the new dimensions are gonna be some thing like this
int w = originalImage.getWidth();
int h = originalImage.getHeight();
double toRad = Math.toRadians(degree);
int hPrime = (int) (w * Math.abs(Math.sin(toRad)) + h * Math.abs(Math.cos(toRad)));
int wPrime = (int) (h * Math.abs(Math.sin(toRad)) + w * Math.abs(Math.cos(toRad)));
Provide me a method for that.
BTW is there any way to rotate a JLabel with an ImageIcon?
Intention: adding to panels and layered pane and also saving it to file (saving the layered pane).
Or can we rotate the layered pane?
How to rotate a buffered image without cropping it?
You had already half of the work by calculating the size of the rotated BufferedImage.
The other half is actually creating the rotated BufferedImage.
You can do that by using Graphics2D
and applying some coordinate transformations before drawing the original image onto the new one. Furthermore, it makes sense to paint the "excess" area with some background color.
public BufferedImage rotateImage(BufferedImage originalImage, double degree) {
int w = originalImage.getWidth();
int h = originalImage.getHeight();
double toRad = Math.toRadians(degree);
int hPrime = (int) (w * Math.abs(Math.sin(toRad)) + h * Math.abs(Math.cos(toRad)));
int wPrime = (int) (h * Math.abs(Math.sin(toRad)) + w * Math.abs(Math.cos(toRad)));
BufferedImage rotatedImage = new BufferedImage(wPrime, hPrime, BufferedImage.TYPE_INT_RGB);
Graphics2D g = rotatedImage.createGraphics();
g.setColor(Color.LIGHT_GRAY);
g.fillRect(0, 0, wPrime, hPrime); // fill entire area
g.translate(wPrime/2, hPrime/2);
g.rotate(toRad);
g.translate(-w/2, -h/2);
g.drawImage(originalImage, 0, 0, null);
g.dispose(); // release used resources before g is garbage-collected
return rotatedImage;
}
Here is a test example from the above code:
Original image
Rotated image (by 30 degree)
BTW is there any way to rotate a JLabel with an ImageIcon?
The easier way is to rotate the Icon, not the label.
Check out Rotated Icon for a class that does the rotation and recalculates the size of the Icon as it is rotated.
Intention: adding to panels and layered pane and also saving it to file (saving the layered pane).
Don't know exactly what that means, but if you just want to save an "image" of the layered pane then check out Screen Image.

Black area using java 2D graphics rotation?

I'll start of by showing examples of what's wrong then I'll explain how, and finally I'll ask my question.
This is the picture I want to rotate.
I am rotating it 90 degrees and 270 degrees, on multiple occasions and then combiningthose into a big buffered-image.
The code I am using to rotate a single bufferedImage is this:
public static BufferedImage rotate(BufferedImage img, int angle) {
int w = img.getWidth();
int h = img.getHeight();
BufferedImage dimg = new BufferedImage(w, h, img.getType());
Graphics2D g = dimg.createGraphics();
g.rotate(Math.toRadians(angle), w/2, h/2);
g.drawImage(img, null, 0, 0);
return dimg;
}
The out come of the rotation looks something like this.
The reason those black bars are these is because in the code you can clearly see I create a separate buffered-image which will be the final image.
Which uses the original width and hight, since the image is rotated the with and height switch so I compensated for this by changing BufferedImage dimg = new BufferedImage(w, h, img.getType()); to BufferedImage dimg = new BufferedImage(h, w, img.getType());.
I though it would be logical that this would solve my problem.
But I was wrong now the rotational outcome is this.
So from this point on is where I have no clue why it's doing this.
I might just be overlooking a tiny thing, or it's a common error even though I can't find any instance of this occurring.
So here is my question to you, why does it do this? And how do I fix this.
The image isn't square. If you rotate it by 90°, then you will create a gap that you need to fill.
Solutions:
Make sure the image is square
"Rotate" the size: When you rotate by 90° or 270°, you need to create a target image with swapped width and height (i.e. 200x100 -> 100x200)
Crop the image. Good in your case since scaling will make the arrow look bad but it might be out of center
Scale the image. If it's 609x579, scale it down to 579x579 (scaling down will usually look a little bit better).
Find the border color and fill the gap with the border color after the rotation
I figured it out.
The thing I was doing in the start was rotating the host image (dimg),
and then drawing the original image to it.
I could just as well have tried to fit a square in a circle my earlier rotation actually makes no sense at all.
So what I need to do is first create the host, draw the image to the host, the rotate the host and return it as the final image.
public static BufferedImage rotate(BufferedImage img, int angle) {
int w = img.getWidth();
int h = img.getHeight();
BufferedImage dimg = new BufferedImage(w, h, img.getType());
Graphics2D g = dimg.createGraphics();
g.drawImage(img, null, 0, 0); //Draw before rotating
g.rotate(Math.toRadians(angle), w/2, h/2); //Rotating after drawing
return dimg;
}
I hope this helps out some other people as well
if you want to use a similar code as first code
this may help ( if you remove the comments and debug lines (such as painting the background) it has only the translate((W-w)/2,(H-h)/2) line in addition )
// do not forget to import static java.lang.Math.*
public static BufferedImage rotate(BufferedImage img, int angle) {
int w = img.getWidth(null);
int h = img.getHeight(null);
double rad = toRadians(angle);
double eps = 1e-3;
int W=(int)(abs(cos(rad))*w+abs(sin(rad))*h-eps)+1;//W after rotation(calculated by using a little geometry )
int H=(int)(abs(sin(rad))*w+abs(cos(rad))*h-eps)+1;//H after rotation
//you may use max value ( diameter of the rectangle ) instead of dynamic value but in that case you must be careful of the black edges ( in this case red edges )
// if 90 is not a divisor of angle then you can't fit a rectangle with that angle in another one so the red edges are inevitable
// but with calculated W and H this edges are minimum
BufferedImage dimg = new BufferedImage(W,H, BufferedImage.TYPE_INT_RGB);// you can change it to any type you want it's just a sample
Graphics2D g = dimg.createGraphics();
g.setColor(Color.RED); // background color of red for displaying the red edges when image is not completely fit
g.fillRect(0, 0, W, H);
int x=(W-w)/2;
int y=(H-h)/2;
g.translate(x, y); // moving dimg center to img center ( this was what first code lack in )
g.rotate(-rad, w/2, h/2); // now rotating dimg around the center of img ( which is now same as center of dimg )
// we rotate dimg by -rad and draw img normally , it's like rotating img by rad instead of dimg by -rad
g.drawImage(img,null,0,0); // and drawing
return dimg;
}

How to make a rounded corner image in Java

I want to make a image with rounded corners. A image will come from input and I will make it rounded corner then save it. I use pure java. How can I do that? I need a function like
public void makeRoundedCorner(Image image, File outputFile){
.....
}
Edit : Added an image for information.
I suggest this method that takes an image and produces an image and keeps the image IO outside:
Edit: I finally managed to make Java2D soft-clip the graphics with the help of Java 2D Trickery: Soft Clipping by Chris Campbell. Sadly, this isn't something Java2D supports out of the box with some RenderhingHint.
public static BufferedImage makeRoundedCorner(BufferedImage image, int cornerRadius) {
int w = image.getWidth();
int h = image.getHeight();
BufferedImage output = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = output.createGraphics();
// This is what we want, but it only does hard-clipping, i.e. aliasing
// g2.setClip(new RoundRectangle2D ...)
// so instead fake soft-clipping by first drawing the desired clip shape
// in fully opaque white with antialiasing enabled...
g2.setComposite(AlphaComposite.Src);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(Color.WHITE);
g2.fill(new RoundRectangle2D.Float(0, 0, w, h, cornerRadius, cornerRadius));
// ... then compositing the image on top,
// using the white shape from above as alpha source
g2.setComposite(AlphaComposite.SrcAtop);
g2.drawImage(image, 0, 0, null);
g2.dispose();
return output;
}
Here's a test driver:
public static void main(String[] args) throws IOException {
BufferedImage icon = ImageIO.read(new File("icon.png"));
BufferedImage rounded = makeRoundedCorner(icon, 20);
ImageIO.write(rounded, "png", new File("icon.rounded.png"));
}
This it what the input/output of the above method looks like:
Input:
Ugly, jagged output with setClip():
Nice, smooth output with composite trick:
Close up of the corners on gray background (setClip() obviously left, composite right):
I am writing a follow up to Philipp Reichart's answer.
the answer of as an answer.
To remove the white background (seems to be black in the pictures), change g2.setComposite(AlphaComposite.SrcAtop);
to g2.setComposite(AlphaComposite.SrcIn);
This was a big problem for me because I have different images with transparency that I don't want to lose.
My original image:
If I use g2.setComposite(AlphaComposite.SrcAtop);:
When I use g2.setComposite(AlphaComposite.SrcIn); the background is transparent.
I found another way using TexturePaint:
ImageObserver obs = ...;
int w = img.getWidth(obs);
int h = img.getHeight(obs);
// any shape can be used
Shape clipShape = new RoundRectangle2D.Double(0, 0, w, h, 20, 20);
// create a BufferedImage with transparency
BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D bg = bi.createGraphics();
// make BufferedImage fully transparent
bg.setComposite(AlphaComposite.Clear);
bg.fillRect(0, 0, w, h);
bg.setComposite(AlphaComposite.SrcOver);
// copy/paint the actual image into the BufferedImage
bg.drawImage(img, 0, 0, w, h, obs);
// set the image to be used as TexturePaint on the target Graphics
g.setPaint(new TexturePaint(bi, new Rectangle2D.Float(0, 0, w, h)));
// activate AntiAliasing
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// translate the origin to where you want to paint the image
g.translate(x, y);
// draw the Image
g.fill(clipShape);
// reset paint
g.setPaint(null);
This code can be simplified if you have a non-animated image, by creating the BufferedImage only once and keeping it for each paint.
If your image is animated though you have to recreate the BufferedImage on each paint. (Or at least i have not found a better solution for this yet.)

Why does this GIF end up as a black square when resizing with Java ImageIO

Java ImageIO correctly displays this black & white image http://www.jthink.net/jaikoz/scratch/black.gif but when I try and resize it using this code
public static BufferedImage resize2D(Image srcImage, int size)
{
int w = srcImage.getWidth(null);
int h = srcImage.getHeight(null);
// Determine the scaling required to get desired result.
float scaleW = (float) size / (float) w;
float scaleH = (float) size / (float) h;
MainWindow.logger.finest("Image Resizing to size:" + size + " w:" + w + ":h:" + h + ":scaleW:" + scaleW + ":scaleH" + scaleH);
//Create an image buffer in which to paint on, create as an opaque Rgb type image, it doesn't matter what type
//the original image is we want to convert to the best type for displaying on screen regardless
BufferedImage bi = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
// Set the scale.
AffineTransform tx = new AffineTransform();
tx.scale(scaleW, scaleH);
// Paint image.
Graphics2D g2d = bi.createGraphics();
g2d.setComposite(AlphaComposite.Src);
g2d.drawImage(srcImage, tx, null);
g2d.dispose();
return bi;
}
I just end up with a black image. Im trying to make the image smaller (a thumbnail) but even if I resize it larger for test purposes it still ends up as a black square.
Other images resize okay, anyone know what is the problem with the gif/and or Java Bug
Here is the string representation of the ColorModel of the linked image when loaded through ImageIO:
IndexColorModel: #pixelBits = 1 numComponents = 4 color space = java.awt.color.ICC_ColorSpace#1572e449 transparency = 2 transIndex = 1 has alpha = true isAlphaPre = false
If I understand this correctly, you have one bit per pixel, where a 0 bit is opaque black and a 1 bit is transparent. Your BufferedImage is initially all black, so drawing a mixture of black and transparent pixels onto it will have no effect.
Although you are using AlphaComposite.Src this will not help as the R/G/B values for the transparent palette entry read as zero (I am not sure whether this is encoded in the GIF or just the default in the JDK.)
You can work around it by:
Initializing the BufferedImage with all-white pixels
Using AlphaComposite.SrcOver
So the last part of your resize2D implementation would become:
// Paint image.
Graphics2D g2d = bi.createGraphics();
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, size, size);
g2d.setComposite(AlphaComposite.SrcOver);
g2d.drawImage(srcImage, tx, null);
Try this:
BufferedImage bi = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
That makes it work. Of course, the question is why..?

How to scale JPEG images with a non-standard sampling factor in Java?

I am using Java AWT for scaling a JPEG image, to create thumbnails. The code works fine when the image has a normal sampling factor ( 2x2,1x1,1x1 )
However, an image which has this sampling factor ( 1x1, 1x1, 1x1 ) creates problem when scaled. The colors get corrupted though the features are recognizable.
The original and the thumbnail:
alt text http://otherplace.in/thumb1.jpg
The code I am using is roughly equivalent to:
static BufferedImage awtScaleImage(BufferedImage image,
int maxSize, int hint) {
// We use AWT Image scaling because it has far superior quality
// compared to JAI scaling. It also performs better (speed)!
System.out.println("AWT Scaling image to: " + maxSize);
int w = image.getWidth();
int h = image.getHeight();
float scaleFactor = 1.0f;
if (w > h)
scaleFactor = ((float) maxSize / (float) w);
else
scaleFactor = ((float) maxSize / (float) h);
w = (int)(w * scaleFactor);
h = (int)(h * scaleFactor);
// since this code can run both headless and in a graphics context
// we will just create a standard rgb image here and take the
// performance hit in a non-compatible image format if any
Image i = image.getScaledInstance(w, h, hint);
image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
g.drawImage(i, null, null);
g.dispose();
i.flush();
return image;
}
(Code courtesy of this page )
Is there a better way to do this?
Here's a test image with sampling factor of [ 1x1, 1x1, 1x1 ].
I believe the problem is not the scaling, but your use of an incompatible color model ("image type") when constructing your BufferedImage.
Creating decent thumbnails in Java is surprisingly hard. Here's a detailed discussion.

Categories

Resources