org.hippoecm.frontend.plugins.gallery.imageutil.ImageUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.hippoecm.frontend.plugins.gallery.imageutil.ImageUtils.java

Source

/*
 *  Copyright 2010-2015 Hippo B.V. (http://www.onehippo.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.hippoecm.frontend.plugins.gallery.imageutil;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;

import org.apache.commons.io.IOUtils;
import org.hippoecm.frontend.editor.plugins.resource.MimeTypeHelper;
import org.imgscalr.Scalr;

/**
 * Generic image manipulation utility methods.
 */
public class ImageUtils {

    /**
     * The available strategies for scaling images.
     */
    public static enum ScalingStrategy {
        /**
         * Automatically determine the best strategy to get the nicest looking image in the least amount of time.
         */
        AUTO(Scalr.Method.AUTOMATIC),
        /**
         * Scale as fast as possible. For smaller images (800px in size) this can result in noticeable aliasing but
         * it can be a few order of magnitudes faster than using the {@link #QUALITY} method.
         */
        SPEED(Scalr.Method.SPEED),
        /**
         * Scale faster than when using {@link #QUALITY} but get better results than when using {@link #SPEED}.
         */
        SPEED_AND_QUALITY(Scalr.Method.BALANCED),
        /**
         * Create a nice scaled version of an image at the cost of more processing time. This strategy is most important
         * for smaller pictures (800px or smaller) and less important for larger pictures, as the difference between
         * this strategy and the {@link #SPEED} strategy become less and less noticeable as the
         * source-image size increases.
         */
        QUALITY(Scalr.Method.QUALITY),
        /**
         * Do everything possible to make the scaled image exceptionally good, regardless of the processing time needed.
         * Use this strategy when even @{link #QUALITY} results in bad-looking images.
         */
        BEST_QUALITY(Scalr.Method.ULTRA_QUALITY);

        private Scalr.Method scalrMethod;

        ScalingStrategy(Scalr.Method scalrMethod) {
            this.scalrMethod = scalrMethod;
        }

        private Scalr.Method getScalrMethod() {
            return scalrMethod;
        }
    }

    /**
     * Prevent instantiation
     */
    protected ImageUtils() {
        // do nothing
    }

    /**
     * Fixes issues for certain image MIME types.
     *
     * @param mimeType the MIME type to fix
     *
     * @return the fixed MIME type
     * @deprecated Use MimeTypeHelper.sanitizeMimeType instead
     */
    @Deprecated
    public static String fixMimeType(String mimeType) {
        return MimeTypeHelper.sanitizeMimeType(mimeType);
    }

    /**
     * Returns an image reader for a MIME type.
     *
     * @param aMimeType MIME type
     * @return an image reader for the given MIME type, or <code>null</code> if no image reader could be created
     * for the given MIME type.
     */
    public static ImageReader getImageReader(String aMimeType) {
        String mimeType = MimeTypeHelper.sanitizeMimeType(aMimeType);
        Iterator<ImageReader> readers = ImageIO.getImageReadersByMIMEType(mimeType);
        if (readers == null || !readers.hasNext()) {
            return null;
        }
        return readers.next();
    }

    /**
     * Returns an image writer for a MIME type.
     *
     * @param aMimeType MIME type
     * @return an image writer for the given MIME type, or <code>null</code> if no image writer could be created
     * for the given MIME type.
     */
    public static ImageWriter getImageWriter(String aMimeType) {
        String mimeType = MimeTypeHelper.sanitizeMimeType(aMimeType);
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByMIMEType(mimeType);
        if (writers == null || !writers.hasNext()) {
            return null;
        }
        return writers.next();
    }

    /**
     * Returns the data of a {@link BufferedImage} as a binary output stream. If the image is <code>null</code>,
     * a stream of zero bytes is returned.
     *
     * @param writer the writer to use for writing the image data.
     * @param image the image to write.
     * @param compressionQuality a float between 0 and 1 that indicates the desired compression quality. Values lower than
     *                           0 will be interpreted as 0, values higher than 1 will be interpreted as 1.
     *
     * @return an output stream with the data of the given image.
     *
     * @throws IOException when creating the binary output stream failed.
     */
    public static ByteArrayOutputStream writeImage(ImageWriter writer, BufferedImage image,
            float compressionQuality) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        if (image != null) {
            ImageOutputStream ios = null;
            try {
                ios = ImageIO.createImageOutputStream(out);
                writer.setOutput(ios);

                // write compressed images with high quality
                final ImageWriteParam writeParam = writer.getDefaultWriteParam();
                if (writeParam.canWriteCompressed()) {
                    String[] compressionTypes = writeParam.getCompressionTypes();
                    if (compressionTypes != null && compressionTypes.length > 0) {
                        writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                        writeParam.setCompressionType(compressionTypes[0]);

                        // ensure a compression quality between 0 and 1
                        float trimmedCompressionQuality = Math.max(compressionQuality, 0);
                        trimmedCompressionQuality = Math.min(trimmedCompressionQuality, 1f);
                        writeParam.setCompressionQuality(trimmedCompressionQuality);
                    }
                }

                final IIOImage iioImage = new IIOImage(image, null, null);
                writer.write(null, iioImage, writeParam);
                ios.flush();
            } finally {
                if (ios != null) {
                    ios.close();
                }
            }
        }

        return out;
    }

    /**
     * Returns the data of a {@link BufferedImage} as a binary output stream. If the image is <code>null</code>, a
     * stream of zero bytes is returned. The data is written with a compression quality of 1.
     *
     * @param writer the writer to use for writing the image data.
     * @param image  the image to write.
     * @return an output stream with the data of the given image.
     * @throws IOException when creating the binary output stream failed.
     */
    public static ByteArrayOutputStream writeImage(ImageWriter writer, BufferedImage image) throws IOException {
        return writeImage(writer, image, 1f);
    }

    /**
     * Returns a scaled instance of the provided {@link BufferedImage}.
     *
     * @param img
     *            the original image to be scaled
     * @param targetWidth
     *            the desired width of the scaled instance, in pixels
     * @param targetHeight
     *            the desired height of the scaled instance, in pixels
     * @param strategy
     *            the strategy to use for scaling the image
     *
     * @return a scaled version of the original {@code BufferedImage}, or <code>null</code> if either
     * the target width or target height is 0 or less.
     */
    public static BufferedImage scaleImage(BufferedImage img, int targetWidth, int targetHeight,
            ScalingStrategy strategy) {
        if (targetWidth <= 0 || targetHeight <= 0) {
            return null;
        }

        return Scalr.resize(img, strategy.getScalrMethod(), Scalr.Mode.FIT_EXACT, targetWidth, targetHeight);
    }

    /**
     * Returns a scaled instance of the provided {@link BufferedImage}.
     *
     * @param img
     *            the original image to be scaled
     * @param targetWidth
     *            the desired width of the scaled instance, in pixels
     * @param targetHeight
     *            the desired height of the scaled instance, in pixels
     * @param hint
     *            one of the rendering hints that corresponds to {@link RenderingHints#KEY_INTERPOLATION}
     *            (e.g. {@link RenderingHints#VALUE_INTERPOLATION_NEAREST_NEIGHBOR},
     *            {@link RenderingHints#VALUE_INTERPOLATION_BILINEAR},
     *            {@link RenderingHints#VALUE_INTERPOLATION_BICUBIC})
     * @param highQuality
     *            if true, this method will use a multi-step scaling technique that provides higher quality than the
     *            usual one-step technique (only useful in downscaling cases, where {@code targetWidth} or
     *            {@code targetHeight} is smaller than the original dimensions, and generally only when the
     *            {@code BILINEAR} hint is specified)
     *
     * @return a scaled version of the original {@code BufferedImage}, or <code>null</code> if either
     * the target width or target height is 0 or less.
     *
     * @deprecated Use {@link ImageUtils#scaleImage(java.awt.image.BufferedImage, int, int, org.hippoecm.frontend.plugins.gallery.imageutil.ImageUtils.ScalingStrategy)} instead
     * for faster scaling and/or better image quality)}
     */
    @Deprecated
    public static BufferedImage scaleImage(BufferedImage img, int targetWidth, int targetHeight, Object hint,
            boolean highQuality) {
        return scaleImage(img, 0, 0, img.getWidth(), img.getHeight(), targetWidth, targetHeight, hint, highQuality);
    }

    public static BufferedImage scaleImage(BufferedImage img, int xOffset, int yOffset, int sourceWidth,
            int sourceHeight, int targetWidth, int targetHeight, Object hint, boolean highQuality) {

        if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) {
            return null;
        }

        int type = (img.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB
                : BufferedImage.TYPE_INT_ARGB;

        BufferedImage result = img;
        if (xOffset != 0 || yOffset != 0 || sourceWidth != img.getWidth() || sourceHeight != img.getHeight()) {
            result = result.getSubimage(xOffset, yOffset, sourceWidth, sourceHeight);
        }

        int width, height;

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

        do {
            if (highQuality && width > targetWidth) {
                width /= 2;
            }
            width = Math.max(width, targetWidth);

            if (highQuality && height > targetHeight) {
                height /= 2;
            }
            height = Math.max(height, targetHeight);

            BufferedImage tmp = new BufferedImage(width, height, type);

            Graphics2D g2 = tmp.createGraphics();
            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
            g2.drawImage(result, 0, 0, width, height, null);
            g2.dispose();

            result = tmp;
        } while (width != targetWidth || height != targetHeight);

        return result;
    }

    /**
     * Converts image raster data to a JPEG with RGB color space. Only images with color space CMYK and YCCK are
     * converted, other images are left untouched.
     *
     * Rationale: Java's ImageIO can't process 4-component images and Java2D can't apply AffineTransformOp either,
     * so we have to convert raster data to a JPG with RGB color space.
     *
     * The technique used in this method is due to Mark Stephens, and free for any use. See
     * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4799903 or
     * http://www.mail-archive.com/java2d-interest@capra.eng.sun.com/msg03247.html
     *
     * @param is the image data
     * @param colorModel the color model of the image
     * @return the RGB version of the supplied image
     */
    public static InputStream convertToRGB(InputStream is, ColorModel colorModel)
            throws IOException, UnsupportedImageException {
        if (colorModel != ColorModel.CMYK && colorModel != ColorModel.YCCK) {
            return is;
        }

        // Get an ImageReader.
        ImageInputStream input = ImageIO.createImageInputStream(is);

        try {
            Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
            if (readers == null || !readers.hasNext()) {
                throw new UnsupportedImageException("No ImageReaders found");
            }

            ImageReader reader = readers.next();
            reader.setInput(input);
            Raster raster = reader.readRaster(0, reader.getDefaultReadParam());

            int w = raster.getWidth();
            int h = raster.getHeight();
            byte[] rgb = new byte[w * h * 3];

            switch (colorModel) {
            case YCCK: {
                float[] Y = raster.getSamples(0, 0, w, h, 0, (float[]) null);
                float[] Cb = raster.getSamples(0, 0, w, h, 1, (float[]) null);
                float[] Cr = raster.getSamples(0, 0, w, h, 2, (float[]) null);
                float[] K = raster.getSamples(0, 0, w, h, 3, (float[]) null);

                for (int i = 0, imax = Y.length, base = 0; i < imax; i++, base += 3) {
                    float k = 220 - K[i], y = 255 - Y[i], cb = 255 - Cb[i], cr = 255 - Cr[i];

                    double val = y + 1.402 * (cr - 128) - k;
                    val = (val - 128) * .65f + 128;
                    rgb[base] = val < 0.0 ? (byte) 0 : val > 255.0 ? (byte) 0xff : (byte) (val + 0.5);

                    val = y - 0.34414 * (cb - 128) - 0.71414 * (cr - 128) - k;
                    val = (val - 128) * .65f + 128;
                    rgb[base + 1] = val < 0.0 ? (byte) 0 : val > 255.0 ? (byte) 0xff : (byte) (val + 0.5);

                    val = y + 1.772 * (cb - 128) - k;
                    val = (val - 128) * .65f + 128;
                    rgb[base + 2] = val < 0.0 ? (byte) 0 : val > 255.0 ? (byte) 0xff : (byte) (val + 0.5);
                }
                break;
            }
            case CMYK: {
                int[] C = raster.getSamples(0, 0, w, h, 0, (int[]) null);
                int[] M = raster.getSamples(0, 0, w, h, 1, (int[]) null);
                int[] Y = raster.getSamples(0, 0, w, h, 2, (int[]) null);
                int[] K = raster.getSamples(0, 0, w, h, 3, (int[]) null);

                for (int i = 0, imax = C.length, base = 0; i < imax; i++, base += 3) {
                    int c = 255 - C[i];
                    int m = 255 - M[i];
                    int y = 255 - Y[i];
                    int k = 255 - K[i];
                    float kk = k / 255f;

                    rgb[base] = (byte) (255 - Math.min(255f, c * kk + k));
                    rgb[base + 1] = (byte) (255 - Math.min(255f, m * kk + k));
                    rgb[base + 2] = (byte) (255 - Math.min(255f, y * kk + k));
                }
                break;
            }
            }

            // from other image types we know InterleavedRaster's can be
            // manipulated by AffineTransformOp, so create one of those.
            raster = Raster.createInterleavedRaster(new DataBufferByte(rgb, rgb.length), w, h, w * 3, 3,
                    new int[] { 0, 1, 2 }, null);

            ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
            java.awt.image.ColorModel cm = new ComponentColorModel(cs, false, true, Transparency.OPAQUE,
                    DataBuffer.TYPE_BYTE);
            BufferedImage convertedImage = new BufferedImage(cm, (WritableRaster) raster, true, null);

            ByteArrayOutputStream os = new ByteArrayOutputStream();
            ImageIO.write(convertedImage, "jpg", os);

            return new ByteArrayInputStream(os.toByteArray());
        } finally {
            IOUtils.closeQuietly(is);
            if (input != null) {
                input.close();
            }
        }
    }

    /**
     * Convert image in CYYK or CMYK color space to RGB using {@link ImageUtils#convertToRGB(InputStream is, ColorModel colorModel)}
     *
     * @param is the image data
     * @param colorModel the color model of the image
     * @return the RGB version of the supplied image
     *
     * @deprecated This method is deprecated in favor of {@link ImageUtils#convertToRGB(InputStream is, ColorModel colorModel)}
     */
    @Deprecated
    public static InputStream convertToRGB(InputStream is, ImageMetaData.ColorModel colorModel)
            throws IOException, UnsupportedImageException {
        return convertToRGB(is, ColorModel.valueOf(colorModel.name()));
    }
}