package miage.utils;
import com.mortennobel.imagescaling.AdvancedResizeOp;
import com.mortennobel.imagescaling.MultiStepRescaleOp;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGEncodeParam;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import javax.imageio.ImageIO;
import sun.reflect.generics.reflectiveObjects.NotImplementedException;
/**
* This is a utility class for performing basic functions on an image,
* such as retrieving, resizing, cropping, and saving.
* @version 1.0
*/
public class Image {
BufferedImage img;
ImageType sourceType = ImageType.UNKNOWN;
/**
* Load image from InputStream
* @param input
* @throws IOException
*/
public Image(InputStream input, ImageType sourceType) throws IOException {
img = ImageIO.read(input);
input.close();
this.sourceType = (sourceType == null ? ImageType.UNKNOWN : sourceType);
}
/**
* Constructor for taking a BufferedImage
* @param img
*/
private Image(BufferedImage img, ImageType sourceType) {
this.img = img;
this.sourceType = (sourceType == null ? ImageType.UNKNOWN : sourceType);
}
/**
* @return Source type of the image
*/
public ImageType getSourceType() {
return sourceType;
}
/**
* @return Width of the image in pixels
*/
public int getWidth() {
return img.getWidth();
}
/**
* @return Height of the image in pixels
*/
public int getHeight() {
return img.getHeight();
}
/**
* @return Aspect ratio of the image (width / height)
*/
public double getAspectRatio() {
return (double)getWidth() / (double)getHeight();
}
/**
* Generate a new Image object resized to a specific width, maintaining
* the same aspect ratio of the original
* @param width
* @return Image scaled to new width
*/
public Image getResizedToWidth(int width) {
if (width > getWidth())
throw new IllegalArgumentException("Width "+ width +" exceeds width of image, which is "+ getWidth());
int nHeight = width * img.getHeight() / img.getWidth();
MultiStepRescaleOp rescale = new MultiStepRescaleOp(width, nHeight);
rescale.setUnsharpenMask(AdvancedResizeOp.UnsharpenMask.Soft);
BufferedImage resizedImage = rescale.filter(img, null);
return new Image(resizedImage, sourceType);
}
/**
* Generate a new Image object cropped to a new size
* @param x1 Starting x-axis position for crop area
* @param y1 Starting y-axis position for crop area
* @param x2 Ending x-axis position for crop area
* @param y2 Ending y-axis position for crop area
* @return Image cropped to new dimensions
*/
public Image crop(int x1, int y1, int x2, int y2) {
if (x1 < 0 || x2 <= x1 || y1 < 0 || y2 <= y1 || x2 > getWidth() || y2 > getHeight())
throw new IllegalArgumentException("invalid crop coordinates");
int type = img.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : img.getType();
int nNewWidth = x2 - x1;
int nNewHeight = y2 - y1;
BufferedImage cropped = new BufferedImage(nNewWidth, nNewHeight, type);
Graphics2D g = cropped.createGraphics();
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setComposite(AlphaComposite.Src);
g.drawImage(img, 0, 0, nNewWidth, nNewHeight, x1, y1, x2, y2, null);
g.dispose();
return new Image(cropped, sourceType);
}
/**
* Useful function to crop and resize an image to a square.
* This is handy for thumbnail generation.
* @param width Width of the resulting square
* @param cropEdgesPct Specifies how much of an edge all around the square to crop,
* which creates a zoom-in effect on the center of the resulting square. This may
* be useful, given that when images are reduced to thumbnails, the detail of the
* focus of the image is reduced. Specifying a value such as 0.1 may help preserve
* this detail. You should experiment with it. The value must be between 0 and 0.5
* (representing 0% to 50%)
* @return Image cropped and resized to a square
*/
public Image getResizedToSquare(int width, double cropEdgesPct) {
if (cropEdgesPct < 0 || cropEdgesPct > 0.5)
throw new IllegalArgumentException("Crop edges pct must be between 0 and 0.5. "+ cropEdgesPct +" was supplied.");
if (width > getWidth())
throw new IllegalArgumentException("Width "+ width +" exceeds width of image, which is "+ getWidth());
//crop to square first. determine the coordinates.
int cropMargin = (int)Math.abs(Math.round(((img.getWidth() - img.getHeight()) / 2.0)));
int x1 = 0;
int y1 = 0;
int x2 = getWidth();
int y2 = getHeight();
if (getWidth() > getHeight()) {
x1 = cropMargin;
x2 = x1 + y2;
}
else {
y1 = cropMargin;
y2 = y1 + x2;
}
//should there be any edge cropping?
if (cropEdgesPct != 0) {
int cropEdgeAmt = (int)((double)(x2 - x1) * cropEdgesPct);
x1 += cropEdgeAmt;
x2 -= cropEdgeAmt;
y1 += cropEdgeAmt;
y2 -= cropEdgeAmt;
}
// generate the image cropped to a square
Image cropped = crop(x1, y1, x2, y2);
// now resize. we do crop first then resize to preserve detail
Image resized = cropped.getResizedToWidth(width);
cropped.dispose();
return resized;
}
/**
* Soften the image to reduce pixelation. Helps JPGs look better after resizing.
* @param softenFactor Strength of softening. 0.08 is a good value
* @return New Image object post-softening, unless softenFactor == 0, in which
* case the same object is returned
* @throws NotImplementedException Not implemented in this class
*/
public Image soften(float softenFactor) {
if (softenFactor == 0f)
return this;
else {
float[] softenArray = {0, softenFactor, 0, softenFactor, 1-(softenFactor*4), softenFactor, 0, softenFactor, 0};
Kernel kernel = new Kernel(3, 3, softenArray);
ConvolveOp cOp = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
return new Image(cOp.filter(img, null), sourceType);
}
}
/**
* Write image to a file. If the supplied File argument includes an extension,
* this method will attempt to write the image of that type, IF it is
* supported. Otherwise JPG is the default.
* If the supplied File argument does not include an extension, this
* method will attempt to write the image in the type of the original source
* image, IF it is supported. Otherwise JPG is the default.
* This method will overwrite a file that exists with the same name
* @see #getWriterFormatNames()
* @param file File to write image to
* @returns File object representing the image, which will have the proper
* extension appended if none was supplied (i.e. if the method chose the image type),
* or will have ".jpg" appended if the supplied extension is not supported
* @throws IOException
*/
public File writeToFile(File file) throws IOException {
if (file == null)
throw new IllegalArgumentException("File argument was null");
File writeto = null;
//extract extension
String name = file.getName();
String ext = null;
int dot = name.lastIndexOf(".");
if (dot != -1)
ext = name.substring(dot + 1).toLowerCase();
//the presence of an extension in the file name tells us to
//attempt to write the image of that type, if it is supported
if (ext != null) {
if (Arrays.asList(getWriterFormatNames()).contains(ext))
writeto = file;
else {
//failing that, default to jpg
ext = "jpg";
writeto = new File(file.getPath() + ".jpg");
}
}
else {
//if no extension is supplied, try to use the image type of the source image
if (Arrays.asList(getWriterFormatNames()).contains(getSourceType().toString().toLowerCase())) {
ext = getSourceType().toString().toLowerCase();
writeto = new File(file.getPath() +"."+ getSourceType().toString().toLowerCase());
}
else {
//failing that, default to jpg
ext = "jpg";
writeto = new File(file.getPath() +".jpg");
}
}
writeToFile(writeto, ext);
return writeto;
}
/**
* Write image to a file, specify image type
* This method will overwrite a file that exists with the same name
* @see #getWriterFormatNames()
* @param file File to write image to
* @param type jpg, gif, etc.
* @throws IOException
*/
public void writeToFile(File file, String type) throws IOException {
if (file == null)
throw new IllegalArgumentException("File argument was null");
ImageIO.write(img, type, file);
}
/**
* @return Array of supported image types for writing to file
*/
public String[] getWriterFormatNames() {
return ImageIO.getWriterFormatNames();
}
/**
* Writes to JPG using Sun's JPEGCodec
* @param file File to write image to
* @param quality The image quality
* @throws IOException
*/
public void writeToJPG(File file, float quality) throws IOException {
FileOutputStream out = new FileOutputStream(file);
// Encodes image as a JPEG data stream
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(img);
param.setQuality(quality, true);
encoder.setJPEGEncodeParam(param);
encoder.encode(img);
}
/**
* Free up resources associated with this image
*/
public void dispose() {
img.flush();
}
/**
* @return Internal representation of the image
*/
private BufferedImage getBufferedImage() {
return img;
}
public InputStream getImageInputStream() throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(img, "gif", os);
return new ByteArrayInputStream(os.toByteArray());
}
}