package ecologylab.generic; import java.awt.AlphaComposite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.util.Iterator; import java.util.Locale; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.plugins.jpeg.JPEGImageWriteParam; import javax.imageio.stream.ImageOutputStream; /** * A set of lovely convenience methods for working with images. */ public class ImageTools extends Debug { /** * Returns the new rectangle, which is the bounding box for <code>rect</code> rotated by * <code>theta</code> around the center * * @param rect * @param theta * @return */ public static Rectangle getRotatedExtent(Rectangle rect, double theta) { return getRotatedExtent(rect.width, rect.height, theta); } /** * Calculates the new bounding box for a rectangle with width, height rotated by an angle theta * around the center * * @param width * @param height * @param theta * @return */ public static Rectangle getRotatedExtent(int width, int height, double theta) { double diag = Math.sqrt(width * width + height * height); double alpha = Math.atan2(height, width); double newWidth = diag * Math.cos(theta - alpha); double newHeight = diag * Math.cos(Math.PI / 2 - theta - alpha); return new Rectangle((int) newWidth, (int) newHeight); } /** * Make a copy of the BufferedImage. * * @param srcImage * @param destImage */ public static void copyImage(BufferedImage srcImage, BufferedImage destImage) { // scaleAndCopyImage(srcImage.getWidth(), srcImage.getHeight(), srcImage, destImage); Graphics2D g2 = destImage.createGraphics(); g2.setComposite(AlphaComposite.Src); g2.drawImage(srcImage, 0, 0, null); g2.dispose(); } /** * Make a scaled copy of the BufferedImage. Uses INTERPOLATION_BILINEAR. * * @param srcImage * @param destImage */ public static void scaleAndCopyImage(int newWidth, int newHeight, BufferedImage srcImage, BufferedImage destImage) { // AffineTransformOp scaleOp = createScaleOp(newWidth, newHeight, width, height); // scaleOp.filter(bufferedImage, scaledBImage); // faster than using AffineTransformOp scaleOp. i promise. // -- use the source, luke! Graphics2D g2 = destImage.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2.drawImage(srcImage, 0, 0, newWidth, newHeight, null); g2.dispose(); } /** * Take the RenderedImage passed in, compress it, and writes it to a file created from * outfileName. * * @param compressionQuality * ranges between 0 and 1, 0-lowest, 1-highest. */ public static void writeJpegFile(RenderedImage rendImage, String outfileName, float compressionQuality) { writeJpegFile(rendImage, new File(outfileName), compressionQuality); } public static void writeFile(RenderedImage rendImage, File outfile, String formatName) { writeToTarget(rendImage, outfile, outfile.getName(), formatName); } private static void writeToTarget(RenderedImage rendImage, Object outputTarget, String targetName, String formatName) { if (rendImage == null) { error(ImageTools.class, "rendered image is NULL! cannot save image file."); return; } try { // Find a png writer ImageWriter writer = null; Iterator iter = ImageIO.getImageWritersByFormatName(formatName); if (iter.hasNext()) { writer = (ImageWriter) iter.next(); } else { Debug.error(rendImage, "no image writer for " + targetName); return; } // Prepare output file ImageOutputStream ios = ImageIO.createImageOutputStream(outputTarget); writer.setOutput(ios); ImageTools imageTools = new ImageTools(); // Set the compression quality ImageWriteParam iwparam = imageTools.new MyImageWriteParam(); // Write the image writer.write(null, new IIOImage(rendImage, null, null), iwparam); // Cleanup ios.flush(); writer.dispose(); ios.close(); } catch (Exception e) { e.printStackTrace(); } } public static void writePngFile(RenderedImage rendImage, File outfile) { writeFile(rendImage, outfile, "png"); } public static void writePngStream(RenderedImage rendImage, OutputStream outStream) { writeToTarget(rendImage, outStream, outStream.toString(), "png"); } public static void writeTifFile(RenderedImage rendImage, File outfile) { writeFile(rendImage, outfile, "tif"); } /** * Take the RenderedImage passed in, compress it, and writes it to outfile. * * @param compressionQuality * ranges between 0 and 1, 0-lowest, 1-highest. */ public static void writeJpegFile(RenderedImage rendImage, File outfile, float compressionQuality) { writeJpegToTarget(rendImage, outfile, compressionQuality); } public static void writeJpegStream(RenderedImage rendImage, OutputStream outStream, float compressionQuality) { writeJpegToTarget(rendImage, outStream, compressionQuality); } private static void writeJpegToTarget(RenderedImage rendImage, Object target, float compressionQuality) { try { // Find a jpeg writer ImageWriter writer = null; Iterator iter = ImageIO.getImageWritersByFormatName("jpg"); if (iter.hasNext()) { writer = (ImageWriter) iter.next(); } // Prepare output file ImageOutputStream ios = ImageIO.createImageOutputStream(target); writer.setOutput(ios); ImageTools imageTools = new ImageTools(); // Set the compression quality ImageWriteParam iwparam = imageTools.new MyImageWriteParam(); iwparam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); iwparam.setCompressionQuality(compressionQuality); // Write the image writer.write(null, new IIOImage(rendImage, null, null), iwparam); // Cleanup ios.flush(); writer.dispose(); ios.close(); } catch (Exception e) { e.printStackTrace(); } } /** * Scale the image, then write a jpeg. */ /** * Take the RenderedImage passed in, compress it, and writes it to a file created from * outfileName. * * @param compressionQuality * ranges between 0 and 1, 0-lowest, 1-highest. */ public static void writeJpegFile(BufferedImage image, String fileName, float compressionQuality, int width, int height) { BufferedImage scaledImage = scaleJpeg(image, width, height); writeJpegFile(scaledImage, fileName, compressionQuality); } public static void writeJpegStream(BufferedImage image, OutputStream outStream, float compressionQuality, int width, int height) { BufferedImage scaledImage = scaleJpeg(image, width, height); writeJpegStream(scaledImage, outStream, compressionQuality); } private static BufferedImage scaleJpeg(BufferedImage image, int width, int height) { // final int THUMBNAIL_WIDTH = 245; // final int THUMBNAIL_HEIGHT = 350; // int width = THUMBNAIL_WIDTH; // int height = THUMBNAIL_HEIGHT; float thumbRatio = (float) width / (float) height; int imageWidth = image.getWidth(null); int imageHeight = image.getHeight(null); float imageRatio = (float) imageWidth / (float) imageHeight; if (thumbRatio < imageRatio) { height = (int) (width / imageRatio); } else { width = (int) (height * imageRatio); } // println("writeThumbnail("+width+","+height+ // " type="+image.getType()); // draw original image to thumbnail image object and // scale it to the new size on-the-fly BufferedImage scaledImage = new BufferedImage(width, height, image.getType()); Graphics2D graphics2D = scaledImage.createGraphics(); graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); graphics2D.drawImage(image, 0, 0, width, height, null); return scaledImage; } // This class overrides the setCompressionQuality() method to workaround // a problem in compressing JPEG images using the javax.imageio package. class MyImageWriteParam extends JPEGImageWriteParam { public MyImageWriteParam() { super(Locale.getDefault()); } // This method accepts quality levels between 0 (lowest) and 1 (highest) and simply converts // it to a range between 0 and 256; this is not a correct conversion algorithm. // However, a proper alternative is a lot more complicated. // This should do until the bug is fixed. @Override public void setCompressionQuality(float quality) { if (quality < 0.0F || quality > 1.0F) { throw new IllegalArgumentException("Quality out-of-bounds!"); } this.compressionQuality = 256 - (quality * 256); } } /** * Borrowed code from : http://forums.sun.com/thread.jspa?threadID=524460 * Allows determining the byte size of a compressed image. * * @param image * @param quality * @param out * @throws IOException */ public static void write(BufferedImage image, float quality, OutputStream out) throws IOException { Iterator writers = ImageIO.getImageWritersBySuffix("jpeg"); if (!writers.hasNext()) throw new IllegalStateException("No writers found"); ImageWriter writer = (ImageWriter) writers.next(); ImageOutputStream ios = ImageIO.createImageOutputStream(out); writer.setOutput(ios); ImageWriteParam param = writer.getDefaultWriteParam(); if (quality >= 0) { param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); param.setCompressionQuality(quality); } writer.write(null, new IIOImage(image, null, null), param); } /** * Uses image quality of .5 * @param image * @return */ public static int getNumBytes(BufferedImage image) { return getNumBytes(image, .5f); } /** * Altered method to NOT create a byteArray to find size. * @param image * @param quality * @return */ public static int getNumBytes(BufferedImage image, float quality) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(5000); write(image, quality, out); return out.size(); } catch (IOException e) { throw new RuntimeException(e); } } public static byte[] getBytes(BufferedImage image, float quality) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(5000); write(image, quality, out); return out.toByteArray(); } catch (IOException e) { throw new RuntimeException(e); } } }