ThumbnailCreator.java

/*
 * Copyright (C) 2016 Alberto Irurueta Carro (alberto@irurueta.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 com.irurueta.server.commons.image;

import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.lang.ref.SoftReference;
import javax.imageio.ImageIO;

/**
 * This class generates thumbnails of images.
 * Notice that this class can also be used to transcode images to other formats
 * when generating thumbnails (even if they have the same size as the original
 * image)
 */
public class ThumbnailCreator {
    /**
     * Minimum allowed image size.
     */
    public static final int MIN_SIZE = 0;

    /**
     * Default maximum number of concurrent threads that can generate a
     * thumbnail at the same time.
     */
    public static final int DEFAULT_MAX_CONCURRENT_THREADS = 1;

    /**
     * Minimum number of concurrent threads that can generate a thumbnail at the
     * same time.
     */
    public static final int MIN_CONCURRENT_THREADS = 1;

    /**
     * Reference to singleton instance of thumbnail creator.
     */
    private static SoftReference<ThumbnailCreator> mReference;

    /**
     * Maximum number of threads that can generate a thumbnail at the same time.
     * To avoid excessive memory usage, the number of concurrent thumbnails
     * being generated is limited to this value.
     * By default only one thread can generate thumbnails concurrently while
     * other threads will wait until they are allowed.
     * If the server has enough memory and its load is not too high, this value
     * can be increased (especially if the hardware architecture has multiple
     * processors). Increasing this value will result in a larger throughput of
     * generated thumbnails, at the expense of a greater memory usage.
     */
    private int mMaxConcurrentThreads;

    /**
     * Current number of threads generating a thumbnail.
     */
    private volatile int mNumThreads;

    /**
     * Constructor.
     */
    private ThumbnailCreator() {
        //TODO: make maximum number of concurrent threads configurable
        mMaxConcurrentThreads = DEFAULT_MAX_CONCURRENT_THREADS;
        mNumThreads = 0;
    }

    /**
     * Factory method. Creates or returns the singleton instance of this class
     *
     * @return singleton.
     */
    public static synchronized ThumbnailCreator getInstance() {
        ThumbnailCreator creator;
        if (mReference == null || (creator = mReference.get()) == null) {
            creator = new ThumbnailCreator();
            mReference = new SoftReference<>(creator);
        }
        return creator;
    }

    /**
     * sets maximum number of threads that can generate a thumbnail at the same
     * time.
     * To avoid excessive memory usage, the number of concurrent thumbnails
     * being generated is limited to this value.
     * By default only one thread can generate thumbnails concurrently while
     * other threads will wait until they are allowed.
     * If the server has enough memory and its load is not too high, this value
     * can be increased (especially if the hardware architecture has multiple
     * processors). Increasing this value will result in a larger throughput of
     * generated thumbnails, at the expense of a greater memory usage.
     *
     * @param maxConcurrentThreads maximum number of concurrent threads that can
     *                             generate thumbnails at the same time.
     * @throws IllegalArgumentException if provided value is less than 1.
     */
    public synchronized void setMaxConcurrentThreads(final int maxConcurrentThreads) {
        if (maxConcurrentThreads < MIN_CONCURRENT_THREADS) {
            throw new IllegalArgumentException();
        }

        this.mMaxConcurrentThreads = maxConcurrentThreads;
    }

    /**
     * Returns maximum number of threads that can generate a thumbnail at the
     * same time.
     * To avoid excessive memory usage, the number of concurrent thumbnails
     * being generated is limited to this value.
     * By default only one thread can generate thumbnails concurrently while
     * other threads will wait until they are allowed.
     * If the server has enough memory and its load is not too high, this value
     * can be increased (especially if the hardware architecture has multiple
     * processors). Increasing this value will result in a larger throughput of
     * generated thumbnails, at the expense of a greater memory usage.
     *
     * @return maximum number of threads that can generate a thumbnail at the
     * same time.
     */
    public synchronized int getMaxConcurrentThreads() {
        return mMaxConcurrentThreads;
    }

    /**
     * Generates thumbnail of provided input file image and saves it into
     * generated thumbnail file. Information such as input image orientation can
     * be provided if it needs to be taken into account (otherwise it will be
     * ignored). Output format will determine the format of the generated image,
     * it can be used for image transcoding as well.
     * Notice that this class can only generate thumbnails having a size smaller
     * or equal than input image. Attempting to generate a larger image will
     * fail.
     * Warning: because this class is meant to be run on a server, there should
     * be limits on allowed image sizes to avoid excessive memory usage while
     * loading a very large image file.
     *
     * @param inputImageFile         input image file.
     * @param inputOrientation       input image orientation (optional).
     * @param generatedThumbnailFile file where generated thumbnail will be
     *                               stored.
     * @param width                  width (in pixels) of thumbnail to be generated.
     * @param height                 height (in pixels) of thumbnail to be generated.
     * @param format                 format of image to be generated.
     * @throws IllegalArgumentException if width or height is less than minimum
     *                                  allowed image size (1 pixel), or if width or height is greater than
     *                                  actual image size.
     * @throws IOException              if an I/O error occurs.
     * @throws InterruptedException     if thread is interrupted.
     */
    @SuppressWarnings({"SuspiciousNameCombination", "DuplicatedCode"})
    public void generateAndSaveThumbnail(
            final File inputImageFile,
            final ImageOrientation inputOrientation,
            final File generatedThumbnailFile,
            final int width, final int height,
            final ThumbnailFormat format) throws IllegalArgumentException,
            IOException, InterruptedException {

        if (width <= MIN_SIZE || height <= MIN_SIZE) {
            throw new IllegalArgumentException();
        }

        synchronized (this) {
            while (mNumThreads >= mMaxConcurrentThreads) {
                wait();
            }
            mNumThreads++;
        }

        try {
            // default (orientation == 1)
            boolean exchangeSize = false;
            int quadrants = 0;
            if (inputOrientation != null) {
                // take into account only orientations below, other orientations
                // will be ignored
                switch (inputOrientation) {
                    case LEFT_BOTTOM:
                        // orientation == 8 (counterclockwise 90º)
                        exchangeSize = true;
                        quadrants = -1;
                        break;
                    case BOTTOM_RIGHT:
                        // orientation == 3 (clockwise 180º)
                        quadrants = -2;
                        break;
                    case RIGHT_TOP:
                        // orientation == 6 (clockwise 90º)
                        exchangeSize = true;
                        quadrants = -3;
                        break;
                    default:
                        break;
                }
            }

            int bufferedImageType = BufferedImage.TYPE_INT_RGB;
            if (format == ThumbnailFormat.PNG) {
                bufferedImageType = BufferedImage.TYPE_INT_ARGB;
            }

            final BufferedImage inputImage = ImageIO.read(inputImageFile);
            if (inputImage == null) {
                throw new IOException();
            }


            final Image tempImage;
            final BufferedImage resizedImage;
            Graphics2D graphics2D;
            if (exchangeSize) {
                if (width > inputImage.getHeight() ||
                        height > inputImage.getWidth()) {
                    throw new IllegalArgumentException();
                }

                // scale image
                tempImage = inputImage.getScaledInstance(height, width,
                        Image.SCALE_AREA_AVERAGING);
                resizedImage = new BufferedImage(height, width,
                        bufferedImageType);
                graphics2D = resizedImage.createGraphics();
                graphics2D.drawImage(tempImage, 0, 0, height, width, null);

            } else {
                if (width > inputImage.getWidth() ||
                        height > inputImage.getHeight()) {
                    throw new IllegalArgumentException();
                }

                // scale image
                tempImage = inputImage.getScaledInstance(width, height,
                        Image.SCALE_AREA_AVERAGING);
                resizedImage = new BufferedImage(width, height,
                        bufferedImageType);
                graphics2D = resizedImage.createGraphics();
                graphics2D.drawImage(tempImage, 0, 0, width, height, null);
            }
            graphics2D.dispose();


            final double centerX;
            final double centerY;
            final int resizedHeight;
            if (exchangeSize) {
                centerX = height / 2.0;
                centerY = width / 2.0;
                resizedHeight = width;
            } else {
                centerX = width / 2.0;
                centerY = height / 2.0;
                resizedHeight = height;
            }

            BufferedImage thumbnailImage = resizedImage;
            if (quadrants != 0) {
                // set rotation transformation by the desired number of quadrants
                final AffineTransform rotateT = new AffineTransform();
                rotateT.rotate(0.5 * Math.PI * quadrants,
                        centerX, centerY);

                // find proper translations to ensure that rotation doesn't cut
                // off any image data
                if (quadrants != -2) {
                    final AffineTransform rotateT2 = new AffineTransform();
                    rotateT2.rotate(-1.5 * Math.PI, centerX, centerY);
                    Point2D p2din = new Point2D.Double(0.0, 0.0);
                    Point2D p2dout = rotateT2.transform(p2din, null);
                    final double ytrans = p2dout.getY();

                    p2din = new Point2D.Double(0.0, resizedHeight);
                    p2dout = rotateT2.transform(p2din, null);
                    final double xtrans = p2dout.getX();

                    final AffineTransform translateT = new AffineTransform();
                    translateT.translate(-xtrans, -ytrans);

                    rotateT.preConcatenate(translateT);
                }

                // instantiate image that will contain the thumbnail
                thumbnailImage = new BufferedImage(width, height,
                        bufferedImageType);
                graphics2D = thumbnailImage.createGraphics();
                // transform filtered image with scaling and rotation
                graphics2D.drawImage(resizedImage, rotateT, null);
                graphics2D.dispose();
            }


            if (!ImageIO.write(thumbnailImage, format.getValue(),
                    generatedThumbnailFile)) {
                // if format is not supported
                throw new IOException();
            }
        } finally {
            // decrease counter of threads no matter if thumbnail generation
            // fails
            synchronized (this) {
                mNumThreads--;
                this.notifyAll();
            }
        }
    }
}