org.jimcat.services.imagemanager.ImageUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.jimcat.services.imagemanager.ImageUtil.java

Source

/*
 *  This file is part of JimCat.
 *
 *  JimCat is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation version 2.
 *
 *  JimCat is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with JimCat; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.jimcat.services.imagemanager;

import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;

import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.stream.ImageInputStream;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.jimcat.model.ExifMetadata;
import org.jimcat.model.Image;
import org.jimcat.model.ImageMetadata;
import org.jimcat.model.ImageRotation;
import org.jimcat.model.Thumbnail;
import org.jimcat.services.ServiceLocator;
import org.joda.time.DateTime;
import org.joda.time.IllegalFieldValueException;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import com.drew.imaging.jpeg.JpegProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifDirectory;
import com.drew.metadata.exif.ExifReader;

/**
 * A utility class with useful methods for image - handling.
 * 
 * $Id$
 * 
 * @author Christoph
 */
public final class ImageUtil {

    /**
     * the size source files are stored within. A bigger size isn't supported by
     * current graphical configuration.
     */
    private static final Dimension SOURCE_BOUNDING_BOX;

    /**
     * should tiles or subsampling be used to create images
     */
    private static final boolean USE_TILES = false;

    /**
     * fetch current maximum screen resolution
     */
    static {
        // get biggest screen dimension as source file dimension
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice[] screens = ge.getScreenDevices();

        Dimension res = new Dimension(0, 0);
        for (GraphicsDevice screen : screens) {
            Rectangle rect = screen.getDefaultConfiguration().getBounds();
            res.width = Math.max(rect.width, res.width);
            res.height = Math.max(rect.height, res.height);
        }

        // maximum dimension required
        SOURCE_BOUNDING_BOX = res;
    }

    /**
     * a tile of an image loaded at one time.
     * 
     * The more memory consumed the less images will be stored within the cache
     * 
     * <ul>
     * <li>~15 MB per tile at resolution 2000x2000 (original)</li>
     * <li>~22 MB per tile at resolution 2816x2112 (~6MP)</li>
     * <li>~35 MB per tile at resolution 3840x2400 (~9.2MP)</li>
     * </ul>
     * 
     */
    private static final Dimension IMAGE_TILE_SIZE = new Dimension(3840, 2400);

    /**
     * private constructor making this class uninstanceable
     */
    private ImageUtil() {
        /* hide */
    }

    /**
     * This methode will scale the given Buffered image to the given dimension
     * using the given ImageQuality
     * 
     * If image dimension already maches, it will return the images itself.
     * 
     * based on
     * 
     * http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
     * 
     * 
     * @param img -
     *            the image to scale
     * @param dimension -
     *            the destination dimension
     * @param quality -
     *            the quality
     * @return a scaled instance of the buffered image
     */
    public static BufferedImage getScaledInstance(BufferedImage img, Dimension dimension, ImageQuality quality) {

        // can't work with null values
        if (img == null) {
            return null;
        }

        // the resulting image
        BufferedImage ret = img;

        // extract target sizes
        int targetWidth = dimension.width;
        int targetHeight = dimension.height;

        // if size already fit => shortcut
        if (img.getWidth() == targetWidth && img.getHeight() == targetHeight) {
            return img;
        }

        // extract quality infos
        boolean higherQuality = quality.requiresIntermediateSteps();

        // resulting image type
        int type = getImageType(img);

        // scale down
        int w, h;
        if (higherQuality) {
            // Use multi-step technique: start with original size, then
            // scale down in multiple passes with drawImage()
            // until the target size is reached
            w = img.getWidth();
            h = img.getHeight();
        } else {
            // Use one-step technique: scale directly from original
            // size to target size with a single drawImage() call
            w = targetWidth;
            h = targetHeight;
        }

        do {
            // calculate intermediate steps
            if (higherQuality && w > targetWidth) {
                w /= 2;
            }
            if (higherQuality && h > targetHeight) {
                h /= 2;
            }

            // for upscaling, no intermedait steps are supported
            if (w < targetWidth) {
                w = targetWidth;
            }

            if (h < targetHeight) {
                h = targetHeight;
            }

            // calculate next scaling step
            BufferedImage tmp = new BufferedImage(w, h, type);
            Graphics2D g2 = tmp.createGraphics();
            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, quality.getHint());
            g2.drawImage(ret, 0, 0, w, h, null);
            g2.dispose();

            ret = tmp;
        } while (w != targetWidth || h != targetHeight);

        // result finished
        return ret;
    }

    /**
     * this methode will create a rotated version of the given img. if the
     * rotation is ROTATION_0 the original image will be returned
     * 
     * @param img -
     *            the image to rotate
     * @param rotation -
     *            describing the requested rotation
     * @return - the rotated image
     */
    public static BufferedImage rotateImage(BufferedImage img, ImageRotation rotation) {

        // check if there must be a rotation
        if (rotation == ImageRotation.ROTATION_0) {
            return img;
        }

        // get resulting dimension
        Dimension resultDim = null;
        if (rotation == ImageRotation.ROTATION_180) {
            // same dimension
            resultDim = new Dimension(img.getWidth(), img.getHeight());
        } else {
            // transpned dimension
            resultDim = new Dimension(img.getHeight(), img.getWidth());
        }

        // create resulting image
        // resulting image type
        int type = getImageType(img);

        // calculate next scaling step
        int width = resultDim.width;
        int height = resultDim.height;
        BufferedImage tmp = new BufferedImage(width, height, type);
        Graphics2D g2 = tmp.createGraphics();

        // rotate image
        g2.rotate(rotation.getAngle());

        // move ordinal back to starting point
        if (rotation == ImageRotation.ROTATION_90) {
            g2.translate(0, -width);
        } else if (rotation == ImageRotation.ROTATION_180) {
            g2.translate(-width, -height);
        } else if (rotation == ImageRotation.ROTATION_270) {
            g2.translate(-height, 0);
        }

        g2.drawImage(img, 0, 0, null);
        g2.dispose();

        // return result
        return tmp;
    }

    /**
     * calculate the dimension of an image to fit into a boundingBox without
     * lossing aspect ration.
     * 
     * @param width -
     *            the width to scale
     * @param height -
     *            the height to scale
     * @param boundingBox -
     *            the bounding box to fit
     * @param allowUpScale -
     *            if true, the resulting size may be bigger then the given size,
     *            else it is smaller or equal
     * @return the scaled dimension
     */
    public static Dimension getScaledDimension(int width, int height, Dimension boundingBox, boolean allowUpScale) {
        // 1) calculate scale factor

        // get scale factor - keep aspects
        float factorHeight = height / (float) boundingBox.height;
        float factorWidth = width / (float) boundingBox.width;
        float factor = Math.max(factorHeight, factorWidth);

        // to avoid upscaling
        if (!allowUpScale) {
            factor = Math.max(factor, 1);
        }

        // 2) calculate resulting dimension
        int resHeight = Math.max(1, (int) (height / factor));
        int resWidth = Math.max(1, (int) (width / factor));

        // 3) build result
        return new Dimension(resWidth, resHeight);
    }

    /**
     * this method will resolve the given information to an Image object
     * 
     * @param file -
     *            the file containing the image
     * @param quality -
     *            the quality used to render contained image
     * @param importId -
     *            the import id of the resulting image
     * @param addedDate -
     *            the added date of the resulting image
     * @return - a full featured image object
     * @throws IOException
     */
    public static Image resolveImage(File file, ImageQuality quality, long importId, DateTime addedDate)
            throws IOException {

        // load content
        byte[] content = loadFile(file);

        // rest is done by main methode
        return resolveImage(content, quality, file, importId, addedDate);
    }

    /**
     * this methode will resolve the given information to an Image object
     * 
     * @param content -
     *            the content of the file as byte array
     * @param quality -
     *            the quality used to render contained image
     * @param file -
     *            the file containing the image
     * @param importId -
     *            the import id of the resulting image
     * @param addedDate -
     *            the added date of the resulting image
     * @return - a fullfeatured image object
     * @throws IOException
     */
    public static Image resolveImage(byte content[], ImageQuality quality, File file, long importId,
            DateTime addedDate) throws IOException {

        Image img = new Image();

        // load image
        BufferedImage image = loadImage(content, quality);

        // check if image could be read
        if (image == null)
            throw new IOException("Image couldn't be read.");

        // create Thumbnail
        Thumbnail thumbnail = new Thumbnail(image);
        img.setThumbnail(thumbnail);

        // create ImageMetaData
        String checksum = ImageUtil.getChecksum(content);
        Dimension dim = getImageDimension(content);
        ImageMetadata metadata = createMetadata(file, dim.width, dim.height, checksum, importId, addedDate);
        img.setMetadata(metadata);

        // create exif date
        ExifMetadata exifMetadata = ImageUtil.readExifMetadata(content);
        img.setExifMetadata(exifMetadata);

        // flush cache
        ServiceLocator.getImageManager().flushImage(img, image);

        return img;
    }

    /**
     * use this methode to load an image from disc
     * 
     * @param path -
     *            the file to load
     * @param quality -
     *            the quality to use if scaling is necessary
     * @return - a BufferedImage containing the image, size may be limited by
     *         SOURCE_BOUNDING_BOX constant
     * @throws IOException -
     *             if something goes wrong
     */
    public static BufferedImage loadImage(File path, ImageQuality quality) throws IOException {
        return loadImage(loadFile(path), quality);
    }

    /**
     * use this methode to load an image from a given byte array
     * 
     * @param data -
     *            the byte array containing an encoded image
     * @param quality
     *            the rendering quality
     * @return - a Buffered Image containing image. Its size is limited by
     *         SOURCE_BOUNDING_BOX constant
     * @throws IOException -
     *             if something goes wrong
     */
    public static BufferedImage loadImage(byte[] data, ImageQuality quality) throws IOException {

        // read first element
        ImageReader reader = getReaderForImage(data);

        // get image dimension and calculate resulting image size
        int width = reader.getWidth(0);
        int height = reader.getHeight(0);
        Dimension size = getScaledDimension(width, height, SOURCE_BOUNDING_BOX, false);

        // performe read
        BufferedImage bi = null;
        try {
            if (USE_TILES) {
                // if image is smaller than a tile
                if (width <= IMAGE_TILE_SIZE.width && height <= IMAGE_TILE_SIZE.height) {
                    bi = reader.read(0);
                } else {
                    // prepaire reader
                    ImageReadParam param = reader.getDefaultReadParam();

                    // so image is bigger than a tile
                    // a) check if reader supports source scaling
                    if (param.canSetSourceRenderSize()) {
                        // fine => do it so
                        param.setSourceRenderSize(size);
                        bi = reader.read(0, param);
                    } else {
                        // so, scaling has to be done by hand
                        bi = loadImageWithTiles(reader, size, quality);
                    }
                }
            } else {
                bi = loadImageWithSubSampling(reader, size, quality);
            }
        } finally {
            reader.dispose();
        }
        return bi;
    }

    /**
     * this methode is used to read a file into an internal byte array.
     * 
     * @param file -
     *            the file to read
     * @return - a bytearray containing the content of the file
     * @throws IOException -
     *             if there are any problems reading the file
     */
    public static byte[] loadFile(File file) throws IOException {
        // the streames required
        BufferedInputStream in = null;
        ByteArrayOutputStream sink = null;
        try {
            // open streams
            in = new BufferedInputStream(new FileInputStream(file));
            sink = new ByteArrayOutputStream();

            // read file
            IOUtils.copy(in, sink);

            // return results
            return sink.toByteArray();
        } finally {
            // close streams
            IOUtils.closeQuietly(in);
            IOUtils.closeQuietly(sink);
        }
    }

    /**
     * used to calculate the checksum of a chunk of bytes.
     * 
     * this implementation is creating an MD5 checksum.
     * 
     * @param file
     * @return - the MD5 checksum as string or null on error
     */
    public static String getChecksum(byte[] file) {
        String result = null;
        try {
            // calculate checksum
            MessageDigest mdAlgorithm = MessageDigest.getInstance("MD5");
            mdAlgorithm.update(file);
            byte[] checksum = mdAlgorithm.digest();

            // format to hex string
            StringBuffer buffer = new StringBuffer(checksum.length * 2);
            for (byte b : checksum) {
                buffer.append(Integer.toHexString((b & 0xF0) >> 4));
                buffer.append(Integer.toHexString(b & 0x0F));
            }
            result = buffer.toString().toUpperCase();

        } catch (NoSuchAlgorithmException nsae) {
            // should never happen
            nsae.printStackTrace();
        }
        return result;
    }

    /**
     * create a ImageMetadata object from given information
     * 
     * @param source -
     *            the image source file
     * @param width -
     *            the image width
     * @param height -
     *            the image height
     * @param checksum -
     *            the checksum of the given image
     * @param importId -
     *            the import id
     * @param addedDate -
     *            the date when this image was added
     * @return the metadata of the image at source
     */
    public static ImageMetadata createMetadata(File source, int width, int height, String checksum, long importId,
            DateTime addedDate) {
        File file = source.getAbsoluteFile();
        long size = file.length();
        DateTime creationDate = new DateTime(file.lastModified());

        return new ImageMetadata(file, width, height, size, checksum, importId, creationDate, addedDate);
    }

    /**
     * Parses the exif metadata of a given file.
     * 
     * @param file
     *            The input file, should be a jpeg file
     * @return An instance of ExifMetadata or null in case of any error
     */
    public static ExifMetadata readExifMetadata(byte[] file) {
        try {
            Metadata metadata = new Metadata();
            new ExifReader(new ByteArrayInputStream(file)).extract(metadata);

            Directory exif = metadata.getDirectory(ExifDirectory.class);

            if (!hasUsableExifData(exif)) {
                return null;
            }

            String manufacturer = exif.getString(ExifDirectory.TAG_MAKE);
            String model = exif.getString(ExifDirectory.TAG_MODEL);
            String dateTaken = exif.getString(ExifDirectory.TAG_DATETIME_ORIGINAL);
            String exposure = exif.getString(ExifDirectory.TAG_EXPOSURE_TIME);
            String aperture = exif.getString(ExifDirectory.TAG_FNUMBER);
            String focal = exif.getString(ExifDirectory.TAG_FOCAL_LENGTH);
            String flash = flashCodeToString(exif.getString(ExifDirectory.TAG_FLASH));
            String iso = exif.getString(ExifDirectory.TAG_ISO_EQUIVALENT);

            DateTime dateTime = null;

            if (dateTaken != null) {
                try {
                    DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy:MM:dd HH:mm:ss");
                    dateTime = fmt.parseDateTime(dateTaken);
                } catch (IllegalFieldValueException e) {
                    // ok so we don't set the date
                }
            }

            return new ExifMetadata(manufacturer, model, dateTime, exposure, aperture, flash, focal, iso);
        } catch (JpegProcessingException e) {
            // if we can't process it we can do nothing about it to fix it
        }

        return null;
    }

    /**
     * this methode will load given image using tiles (saving memory)
     * 
     * this strategie is spliting the original image up into smaller parts
     * called tiles. Those tiles are downscaled one by one using given quality.
     * This results int probably best possible quality but may cost a lot of
     * time.
     * 
     * @param reader -
     *            the reader to load image from
     * @param size -
     *            the resulting image size
     * @param quality -
     *            the quality used for necessary rendering
     * @return the image as buffered image
     * @throws IOException
     */
    @SuppressWarnings("unused")
    private static BufferedImage loadImageWithTiles(ImageReader reader, Dimension size, ImageQuality quality)
            throws IOException {

        // the image buffer used to load tiles
        ImageTypeSpecifier imageSpec = reader.getImageTypes(0).next();
        BufferedImage tile = imageSpec.createBufferedImage(IMAGE_TILE_SIZE.width, IMAGE_TILE_SIZE.height);

        // the image the result is rendered into
        BufferedImage result = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = result.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, quality.getHint());

        // prepaire image reader parameter
        ImageReadParam param = reader.getDefaultReadParam();
        param.setDestination(tile);

        // count tiles
        int width = reader.getWidth(0);
        int height = reader.getHeight(0);
        int numX = (int) Math.ceil(width / (float) IMAGE_TILE_SIZE.width);
        int numY = (int) Math.ceil(height / (float) IMAGE_TILE_SIZE.height);

        float aspectX = (float) IMAGE_TILE_SIZE.width / width;
        float aspectY = (float) IMAGE_TILE_SIZE.height / height;

        // target tile dimension
        int targetTileWidth = (int) (size.width * aspectX);
        int targetTileHeight = (int) (size.height * aspectY);

        // load tiles
        Rectangle sourceRegion = new Rectangle();
        Rectangle targetRegion = new Rectangle();
        for (int i = 0; i < numX; i++) {
            // line increment
            sourceRegion.x = i * IMAGE_TILE_SIZE.width;
            sourceRegion.width = Math.min(IMAGE_TILE_SIZE.width, width - sourceRegion.x);
            targetRegion.x = i * targetTileWidth;
            targetRegion.width = Math.min(targetTileWidth, size.width - targetRegion.x);
            for (int j = 0; j < numY; j++) {
                // row increment
                sourceRegion.y = j * IMAGE_TILE_SIZE.height;
                sourceRegion.height = Math.min(IMAGE_TILE_SIZE.height, height - sourceRegion.y);
                targetRegion.y = j * targetTileHeight;
                targetRegion.height = Math.min(targetTileHeight, size.height - targetRegion.y);

                // performe read
                param.setSourceRegion(sourceRegion);
                reader.read(0, param);

                // insert into resulting image
                int dx1 = targetRegion.x;
                int dx2 = targetRegion.x + targetRegion.width;
                int dy1 = targetRegion.y;
                int dy2 = targetRegion.y + targetRegion.height;

                g.drawImage(tile, dx1, dy1, dx2, dy2, 0, 0, sourceRegion.width, sourceRegion.height, null);
            }
        }

        // finish drawing
        g.dispose();

        // return result
        return result;
    }

    /**
     * this methode will load given image using a subsample rate
     * 
     * this strategie is just reading a subset of images lines ans rowes to
     * reduce memory usage and cpu time.
     * 
     * @param reader -
     *            the reader to load image from
     * @param size -
     *            the resulting image size
     * @param quality -
     *            the quality used for necessary rendering
     * @return the image as buffered image
     * @throws IOException
     */
    private static BufferedImage loadImageWithSubSampling(ImageReader reader, Dimension size, ImageQuality quality)
            throws IOException {

        // prepaire image reader parameter
        ImageReadParam param = reader.getDefaultReadParam();

        // calculate subsampling values
        int width = reader.getWidth(0);
        int height = reader.getHeight(0);
        int rateX = width / size.width;
        int rateY = height / size.height;
        param.setSourceSubsampling(rateX, rateY, 0, 0);

        // load image with subsamples
        BufferedImage img = reader.read(0, param);

        // scale to final size
        BufferedImage result = getScaledInstance(img, size, quality);

        // return result
        return result;
    }

    /**
     * get the dimension of the image stored inside the given array
     * 
     * @param image
     * @return the image dimension
     * @throws IOException
     */
    private static Dimension getImageDimension(byte image[]) throws IOException {
        ImageReader reader = getReaderForImage(image);
        return new Dimension(reader.getWidth(0), reader.getHeight(0));
    }

    /**
     * get an image reader for the given file content
     * 
     * @param image
     * @return an image reader for the given content
     * @throws IOException
     */
    private static ImageReader getReaderForImage(byte image[]) throws IOException {
        // resolve image using ImageIO stream
        ImageInputStream stream = ImageIO.createImageInputStream(new ByteArrayInputStream(image));

        // check if there is an image inside the stream
        Iterator iter = ImageIO.getImageReaders(stream);
        if (!iter.hasNext()) {
            // none found
            return null;
        }

        // setup reader
        ImageReader reader = (ImageReader) iter.next();
        reader.setInput(stream, true, true);

        return reader;
    }

    /**
     * get a BufferedImageType of the given image used to replicate
     * 
     * @param img
     * @return the type of the buffered image
     */
    private static int getImageType(BufferedImage img) {
        if (img.getTransparency() == Transparency.OPAQUE) {
            return BufferedImage.TYPE_INT_RGB;
        }
        return BufferedImage.TYPE_INT_ARGB;
    }

    /**
     * check if there is any usefull exifdata available
     * 
     * @param exif
     * @return true if there is unsable exif data
     */
    private static boolean hasUsableExifData(Directory exif) {

        if (exif.getTagCount() == 0) {
            return false;
        }

        int[] tags = new int[] { ExifDirectory.TAG_MAKE, ExifDirectory.TAG_DATETIME_ORIGINAL,
                ExifDirectory.TAG_EXPOSURE_TIME, ExifDirectory.TAG_FNUMBER };

        for (int tag : tags) {
            if (exif.containsTag(tag)) {
                return true;
            }
        }

        return false;
    }

    /**
     * maps a flash code to a string representation
     * 
     * @param code
     * @return a string representation of the flash code
     */
    private static String flashCodeToString(String code) {
        int flash;

        try {
            flash = Integer.parseInt(code);
        } catch (NumberFormatException e) {
            return null;
        }

        switch (flash) {
        case 0x0:
            return "No Flash";
        case 0x1:
            return "Fired";
        case 0x5:
            return "Fired, Return not detected";
        case 0x7:
            return "Fired, Return detected";
        case 0x9:
            return "On";
        case 0xd:
            return "On, Return not detected";
        case 0xf:
            return "On, Return detected";
        case 0x10:
            return "Off";
        case 0x18:
            return "Auto, Did not fire";
        case 0x19:
            return "Auto, Fired";
        case 0x1d:
            return "Auto, Fired, Return not detected";
        case 0x1f:
            return "Auto, Fired, Return detected";
        case 0x20:
            return "No flash function";
        case 0x41:
            return "Fired, Red-eye reduction";
        case 0x45:
            return "Fired, Red-eye reduction, Return not detected";
        case 0x47:
            return "Fired, Red-eye reduction, Return detected";
        case 0x49:
            return "On, Red-eye reduction";
        case 0x4d:
            return "On, Red-eye reduction, Return not detected";
        case 0x4f:
            return "On, Red-eye reduction, Return detected";
        case 0x59:
            return "Auto, Fired, Red-eye reduction";
        case 0x5d:
            return "Auto, Fired, Red-eye reduction, Return not detected";
        case 0x5f:
            return "Auto, Fired, Red-eye reduction, Return detected";

        }

        return null;
    }
}