package org.rr.pm.image;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
import java.awt.image.LookupOp;
import java.awt.image.PixelGrabber;
import java.awt.image.ShortLookupTable;
import java.awt.image.WritableRaster;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import javax.swing.ImageIcon;
import org.apache.commons.io.IOUtils;
import org.rr.commons.log.LoggerFactory;
import org.rr.commons.mufs.IResourceHandler;
/**
* provides some static methods to deal with images.
*/
public class ImageUtils {
private static final short[] invertTable;
static {
invertTable = new short[256];
for (int i = 0; i < 256; i++) {
invertTable[i] = (short) (255 - i);
}
}
/**
* Creates the image bytes from the given image.
* @param image the image to be converted into bytes.
* @param formatName The format of the returned bytes. For example "jpeg", "png" or "gif".
* @return The converted bytes or <code>null</code> if something went wrong with the conversion.
*/
public static byte[] getImageBytes(final BufferedImage image, String mime) {
if (image == null) {
return null;
}
//image/jpg did not always work with ImageIO.getImageWritersByMIMEType
if(mime.equals("image/jpg")) {
mime = "image/jpeg";
}
ImageWriter writer = null;
Iterator<ImageWriter> imageWritersByFormatName = ImageIO.getImageWritersByMIMEType(mime);
while(imageWritersByFormatName.hasNext()) {
ImageWriter next = imageWritersByFormatName.next();
if(writer == null && next.getClass().getName().equals("com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageWriter")) {
writer = next;
break;
} else {
writer = next;
}
}
if(writer != null) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
MemoryCacheImageOutputStream mem = new MemoryCacheImageOutputStream(output);
writer.setOutput(mem);
try {
writer.write(image);
} catch (IOException e) {
LoggerFactory.logInfo(ImageUtils.class, "could not create thumbnail", e);
} finally {
try {mem.flush();} catch (Exception e) {}
try {mem.close();} catch (Exception e) {}
try {writer.setOutput(null);} catch(Exception e) {}
try {writer.dispose();} catch(Exception e) {}
}
return output.toByteArray();
}
return null;
}
/**
* Loaded the given jpg file and decodes it.
* @param resourceLoader The jpeg resource to be loaded.
* @return The {@link BufferedImage} for the given file or <code>null</code> if the image could not be loaded.
*/
static BufferedImage decodeImage(IResourceHandler resourceLoader, String mime) {
BufferedImage bi = null;
InputStream bin = null;
try {
bin = resourceLoader.getContentInputStream();
bi = ImageIO.read(bin);
} catch (Exception e) {
//LoggerFactory.getLogger().log(Level.WARNING, "Could not decode image " + resourceLoader, e);
} finally {
IOUtils.closeQuietly(bin);
bin = null;
}
return bi;
}
/**
* Scales the given image to the maximum fitting into the given frame
* dimension without loosing the proportions.
*
* @param frame The dimension for the target image
* @param image The image to be resized.
*
* @return a new {@link BufferedImage} instance with the scaled image data.
*/
public static BufferedImage scaleToMatch(final BufferedImage image, final Dimension frame, boolean proportional) {
if(image==null) {
return null;
}
if(proportional) {
double heightFactor = ((double)frame.height) / ((double)image.getHeight());
double widthFactor = ((double)frame.width) / ((double)image.getWidth());
BufferedImage scaledImage = scalePercent(image, Math.min(heightFactor, widthFactor));
return scaledImage;
} else {
BufferedImage scaledImage = new BufferedImage(frame.width, frame.height, image.getType() > 0 ? image.getType() : BufferedImage.TYPE_INT_RGB);
Graphics scaledImageGraphics = scaledImage.getGraphics();
scaledImageGraphics.drawImage(image, 0, 0, frame.width, frame.height, 0, 0, image.getWidth(), image.getHeight(), null);
scaledImageGraphics.dispose();
return scaledImage;
}
}
/**
* Scales the given image so it matches to the given width
* without loosing it's proportions.
*
* @param width The width where the image should be scaled to.
* @param image The image to be resized.
*
* @return a new {@link BufferedImage} instance with the scaled image data.
*/
public static BufferedImage scaleToWidth(BufferedImage image, int width) {
if(image==null) {
return null;
}
double widthFactor = ((double)width) / ((double)image.getWidth());
BufferedImage scaledImage = scalePercent(image, widthFactor);
return scaledImage;
}
/**
* Scales the given image so it matches to the given height
* without loosing it's proportions.
*
* @param width The width where the image should be scaled to.
* @param image The image to be resized.
*
* @return a new {@link BufferedImage} instance with the scaled image data.
*/
public static BufferedImage scaleToHeight(BufferedImage image, int height) {
if(image==null) {
return null;
}
double heightFactor = ((double)height) / ((double)image.getHeight());
BufferedImage scaledImage = scalePercent(image, heightFactor);
return scaledImage;
}
/**
* Shrinks or enlarges the current JpgImage object by the given scale
* factor, with a scale of 1 being 100% (or no change).<p>
* For example, if you need to reduce the image to 75% of the current size,
* you should use a scale of 0.75. If you want to double the size of the
* image, you should use a scale of 2. If you attempt to scale using a
* negative number, the image will not be modified.
*
* @param scale the amount that this image should be scaled (1 = no change)
* @return a new {@link BufferedImage} instance with the scaled image data.
*/
public static BufferedImage scalePercent(BufferedImage image, double scale) {
if ((scale > 0) && (scale != 1)) {
AffineTransform scaleInstance = AffineTransform.getScaleInstance(scale, scale);
AffineTransformOp op = new AffineTransformOp(scaleInstance, null);
return op.filter(image, null);
}
return image;
}
/**
* Scales the image so it shall match into the given frame dimension.
*
* @param frame The dimension for the target image
* @param image The image to be resized.
*
* @return a new {@link BufferedImage} instance with the scaled image data.
*/
public static BufferedImage cut(BufferedImage image, Rectangle frame) {
BufferedImage dest = image.getSubimage(frame.x, frame.y, frame.width, frame.height);
return dest;
}
/**
* Splits the given image into the given amount of parts.
*/
public static List<BufferedImage> splitHorizontal(BufferedImage image, int parts) {
if(parts > 1) {
ArrayList<BufferedImage> result = new ArrayList<>(parts);
int width = image.getWidth();
int height = image.getHeight();
int each = width / parts;
for(int i = 0; i < parts; i++) {
BufferedImage cut = cut(image, new Rectangle(i * each, 0, each, height));
result.add(cut);
}
return result;
}
return Collections.singletonList(image);
}
/**
* Crops the frame having the given crop color around the given image.
*
* @param image The image to be croped.
* @param cropColor The color around the image.
* @return The croped image or the given image instance if no crop is needed.
*/
public static BufferedImage cropByColor(BufferedImage image, Color cropColor) {
return null;
}
/**
* Gets a {@link AffineTransform} instance which scales the image
* so it shall fit into the given {@link Dimension} frame.
* @param d The {@link Dimension} frame where the image should be fit in.
* @return The desired {@link AffineTransform} instance.
*/
public static AffineTransform getTransformToMatchDimension(BufferedImage image, Dimension frame, double rotatenDegree) {
//create a AffineTransform to scale the image so it matches into the given Dimension
double heightFactor;
double widthFactor;
if(rotatenDegree == 90d || rotatenDegree == 270d) {
heightFactor = ((double)frame.height) / ((double)image.getWidth());
widthFactor = ((double)frame.width) / ((double)image.getHeight());
} else {
heightFactor = ((double)frame.height) / ((double)image.getHeight());
widthFactor = ((double)frame.width) / ((double)image.getWidth());
}
double scale = Math.min(heightFactor, widthFactor);
AffineTransform scaleInstance = AffineTransform.getScaleInstance(scale, scale);
//apply a transform to the AffineTransform so the image is located at the middle/center
//of the given Dimension.
double scaleX = scaleInstance.getScaleX();
double scaleY = scaleInstance.getScaleY();
double emptyX = ((double)frame.width) - (((double)image.getWidth()) * scaleX);
double emptyY = ((double)frame.height) - (((double)image.getHeight()) * scaleY);
scaleInstance.translate(emptyX/2/scaleX, emptyY/2/scaleY);
//apply the rotation
scaleInstance.rotate(Math.toRadians(rotatenDegree), (double)image.getWidth()/2 , (double)image.getHeight()/2);
return scaleInstance;
}
/**
* Resize the given image without scaling it. If the target size is larger than the source image,
* the overlapping area fill be filled with the given color.
* @param image The image which should be expanded
* @param newSize The target image size
* @param c The background color-
* @return The expanded image or <code>null</code> if the given image is <code>null</code>.
*/
public static BufferedImage crop(BufferedImage image, Rectangle newSize, Color c) {
if(image == null) {
return null;
}
BufferedImage resultImage = new BufferedImage(newSize.width, newSize.height, image.getType());
Graphics resultGraphics = resultImage.getGraphics();
resultGraphics.setColor(c);
resultGraphics.fillRect(0, 0, newSize.width, newSize.height);
resultGraphics.drawImage(image,
0, 0, newSize.width, newSize.height, //dest
newSize.x, newSize.y, newSize.x + newSize.width, newSize.y + newSize.height, //source
c, null);
return resultImage;
}
/**
* Detect and crop a white / light gray frame around the given image.
* @param image The image to be croped.
* @return The croped image or the given one if no crop is needed.
*/
public static BufferedImage crop(BufferedImage image) {
if(image == null) {
return null;
}
try {
int minRow = -1;
int maxRow = -1;
int[] img_pixels = new int[image.getHeight() * image.getWidth()];
// ...grab this images's pixels
PixelGrabber pg = new PixelGrabber(image, 0, 0, image.getWidth(), image.getHeight(), img_pixels, 0, image.getWidth());
pg.grabPixels();
for (int row = 0; row < image.getHeight(); ++row) {
if (!isRowHomogeneous(img_pixels, image.getWidth() * row, image.getWidth())) {
if (minRow < 0) {
minRow = row;
} else if (row > maxRow) {
maxRow = row;
}
}
}
// ...how about column-based cropping?
int minCol = -1;
int maxCol = -1;
for (int col = 0; col < image.getWidth(); ++col) {
if (!isColumnHomogeneous(img_pixels, col, image.getWidth(), minRow, maxRow - minRow)) {
if (minCol < 0) {
minCol = col;
} else if (col > maxCol) {
maxCol = col;
}
}
}
if(minCol > 1 && maxRow > 1) {
minCol+=1;
minRow+=1;
maxCol-=1;
maxRow-=1;
int cropedWidth = maxCol - minCol;
int cropedHeight = maxRow - minRow;
if(cropedWidth < 10 || cropedHeight < 10) {
return image;
}
BufferedImage scaledImage = new BufferedImage(cropedWidth, cropedHeight, image.getType() > 0 ? image.getType() : BufferedImage.TYPE_INT_RGB);
Graphics scaledImageGraphics = scaledImage.getGraphics();
scaledImageGraphics.drawImage(image, 0, 0, scaledImage.getWidth(), scaledImage.getHeight(), minCol, minRow, maxCol, maxRow, null);
scaledImageGraphics.dispose();
return scaledImage;
} else {
return image;
}
} catch(Exception e) {
LoggerFactory.logWarning(ImageUtils.class, "image crop has failed", e);
}
return null;
}
/**
* Tests if the given pixel is in the color area for cropping.
* @param pixel The pixel to be tested.
* @return <code>true</code> if the pixel is in the crop color area als <code>false</code> otherwise.
*/
private static boolean isHomogeneousPixel(int pixel) {
//int alpha = (pixel >> 24) & 0xff;
int red = (pixel >> 16) & 0xff;
int green = (pixel >> 8) & 0xff;
int blue = (pixel) & 0xff;
return (red >=240 && green >=240 && blue>=200);
}
/**
* Tests if the given pixels in the specified range are in the cropping color area.
* @param pixels The pixels contains the row to be tested.
* @param off The start offset for the pixels to be tested.
* @param len The amount of pixels to be tested starting with the off parameter.
* @return <code>true</code> if the row should be cropped and <code>false</code> otherwise.
*/
private static boolean isRowHomogeneous(int[] pixels, int off, int len) {
//5% of pixels must noch match
int failcount = (int) (((double)len)/100*5);
for (int pixel = off; pixel < off + len; ++pixel) {
if (!isHomogeneousPixel(pixels[pixel])) {
if(failcount-- <= 0) {
return false;
}
}
}
return true;
}
private static boolean isColumnHomogeneous(int[] pixels, int col, int row_len, int row_offset, int rows) {
//5% of pixels must noch match
int failcount = (int) (((double)row_len)/100*5);
for (int row = row_offset; row < row_offset + rows; ++row) {
int pixel = row * row_len + col;
if (!isHomogeneousPixel(pixels[pixel])) {
if(failcount-- <= 0) {
return false;
}
}
}
return true;
}
public static BufferedImage toBufferedImage(Image image) {
if (image instanceof BufferedImage) {
return (BufferedImage)image;
}
// This code ensures that all the pixels in the image are loaded
image = new ImageIcon(image).getImage();
// Determine if the image has transparent pixels; for this method's
// implementation, see Determining If an Image Has Transparent Pixels
boolean hasAlpha = hasAlpha(image);
// Create a buffered image with a format that's compatible with the screen
BufferedImage bimage = null;
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
try {
// Determine the type of transparency of the new buffered image
int transparency = Transparency.OPAQUE;
if (hasAlpha) {
transparency = Transparency.BITMASK;
}
// Create the buffered image
GraphicsDevice gs = ge.getDefaultScreenDevice();
GraphicsConfiguration gc = gs.getDefaultConfiguration();
bimage = gc.createCompatibleImage(
image.getWidth(null), image.getHeight(null), transparency);
} catch (HeadlessException e) {
// The system does not have a screen
}
if (bimage == null) {
// Create a buffered image using the default color model
int type = BufferedImage.TYPE_INT_RGB;
if (hasAlpha) {
type = BufferedImage.TYPE_INT_ARGB;
}
bimage = new BufferedImage(image.getWidth(null), image.getHeight(null), type);
}
// Copy image to buffered image
Graphics g = bimage.createGraphics();
// Paint the image onto the buffered image
g.drawImage(image, 0, 0, null);
g.dispose();
return bimage;
}
public static boolean hasAlpha(Image image) {
// If buffered image, the color model is readily available
if (image instanceof BufferedImage) {
BufferedImage bimage = (BufferedImage)image;
return bimage.getColorModel().hasAlpha();
}
// Use a pixel grabber to retrieve the image's color model;
// grabbing a single pixel is usually sufficient
PixelGrabber pg = new PixelGrabber(image, 0, 0, 1, 1, false);
try {
pg.grabPixels();
} catch (InterruptedException e) {
}
// Get the image's color model
ColorModel cm = pg.getColorModel();
return cm.hasAlpha();
}
/**
* Inverts the colors of the given image.
* @param src The image to be invert.
* @return The inverted image.
*/
public static BufferedImage invertImage(final BufferedImage src) {
final int w = src.getWidth();
final int h = src.getHeight();
final BufferedImage dst = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
final BufferedImageOp invertOp = new LookupOp(new ShortLookupTable(0, invertTable), null);
if(src.getType() == BufferedImage.TYPE_BYTE_INDEXED || src.getType() == 12) {
BufferedImage newSrc = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
newSrc.getGraphics().drawImage(src, 0, 0, null);
return invertOp.filter(newSrc, dst);
} else {
return invertOp.filter(src, dst);
}
}
/**
* @author flubshi
*/
public static BufferedImage convertToGrayScale(final BufferedImage bufferedImage) {
final BufferedImage dest = new BufferedImage(bufferedImage.getWidth(), bufferedImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
Color tmp;
int val, alpha;
for (int y = 0; y < dest.getHeight(); y++) {
for (int x = 0; x < dest.getWidth(); x++) {
alpha = bufferedImage.getRGB(x, y) & 0xFF000000;
tmp = new Color(bufferedImage.getRGB(x, y));
// val = (int) (tmp.getRed()+tmp.getGreen()+tmp.getBlue())/3;
// val =
// Math.max(tmp.getRed(),Math.max(tmp.getGreen(),tmp.getBlue()));
val = (int) (tmp.getRed() * 0.3 + tmp.getGreen() * 0.59 + tmp.getBlue() * 0.11);
dest.setRGB(x, y, alpha | val | val << 8 & 0x0000FF00 | val << 16 & 0x00FF0000);
}
}
return dest;
}
/**
* Create a black and white image from a gray scale image
*/
public static BufferedImage grayToBlackWhite(BufferedImage inputImage, boolean dither) {
int w = inputImage.getWidth();
int h = inputImage.getHeight();
BufferedImage outputImage = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_BINARY);
// Work on a copy of input image because it is modified by diffusion
WritableRaster input = inputImage.copyData(null);
WritableRaster output = outputImage.getRaster();
final int threshold = 128;
float value, qerror;
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
value = input.getSample(x, y, 0);
// Threshold value and compute quantization error
if (value < threshold) {
output.setSample(x, y, 0, 0);
qerror = value;
} else {
output.setSample(x, y, 0, 1);
qerror = value - 255;
}
// Spread error amongst neighboring pixels
// Based on Floyd-Steinberg Dithering
// http://en.wikipedia.org/wiki/Floyd-Steinberg_dithering
if (dither) {
if((x > 0) && (y > 0) && (x < (w-1)) && (y < (h-1))) {
// 7/16
value = input.getSample(x+1, y, 0);
input.setSample(x+1, y, 0, clamp(value + 0.4375f * qerror));
// 3/16
value = input.getSample(x-1, y+1, 0);
input.setSample(x-1, y+1, 0, clamp(value + 0.1875f * qerror));
// 5/16
value = input.getSample(x, y+1, 0);
input.setSample(x, y+1, 0, clamp(value + 0.3125f * qerror));
// 1/16
value = input.getSample(x+1, y+1, 0);
input.setSample(x+1, y+1, 0, clamp(value + 0.0625f * qerror));
}
}
}
}
return outputImage;
}
/**
* Forces a value to a 0-255 integer range
*/
private static int clamp(float value) {
return Math.min(Math.max(Math.round(value), 0), 255);
}
/**
* Rotates the given image by the given degree and returns the transformed image.
* If the degree value is 0 the given image is returned.
*/
public static BufferedImage rotate90Degree(BufferedImage image, boolean clockwise) {
int w = image.getWidth();
int h = image.getHeight();
double theta = Math.toRadians(clockwise ? 90 : -90);
BufferedImage rotatedImage = new BufferedImage(h, w, image.getType() > 0 ? image.getType() : BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = (Graphics2D) rotatedImage.getGraphics();
double x = (h - w) / 2.0;
double y = (w - h) / 2.0;
AffineTransform at = AffineTransform.getTranslateInstance(x, y);
at.rotate(theta, w / 2.0, h / 2.0);
g2d.drawImage(image, at, null);
g2d.dispose();
return rotatedImage;
}
}