Index: lams_build/lib/lams/lams.jar =================================================================== diff -u -r5595f657443bd42457c1a5dd62373561c64ea709 -r4340b53d8412d3838eb7400ddfc821018234a1e9 Binary files differ Index: lams_common/src/java/org/lamsfoundation/lams/util/imgscalr/AsyncScalr.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/util/imgscalr/AsyncScalr.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/util/imgscalr/AsyncScalr.java (revision 4340b53d8412d3838eb7400ddfc821018234a1e9) @@ -0,0 +1,565 @@ +/** + * Copyright 2011 The Buzz Media, LLC + * + * 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.lamsfoundation.lams.util.imgscalr; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ImagingOpException; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.lamsfoundation.lams.util.imgscalr.Scalr.Method; +import org.lamsfoundation.lams.util.imgscalr.Scalr.Mode; +import org.lamsfoundation.lams.util.imgscalr.Scalr.Rotation; + +/** + * Class used to provide the asynchronous versions of all the methods defined in + * {@link Scalr} for the purpose of efficiently handling large amounts of image + * operations via a select number of processing threads asynchronously. + *

+ * Given that image-scaling operations, especially when working with large + * images, can be very hardware-intensive (both CPU and memory), in large-scale + * deployments (e.g. a busy web application) it becomes increasingly important + * that the scale operations performed by imgscalr be manageable so as not to + * fire off too many simultaneous operations that the JVM's heap explodes and + * runs out of memory or pegs the CPU on the host machine, staving all other + * running processes. + *

+ * Up until now it was left to the caller to implement their own serialization + * or limiting logic to handle these use-cases. Given imgscalr's popularity in + * web applications it was determined that this requirement be common enough + * that it should be integrated directly into the imgscalr library for everyone + * to benefit from. + *

+ * Every method in this class wraps the matching methods in the {@link Scalr} + * class in new {@link Callable} instances that are submitted to an internal + * {@link ExecutorService} for execution at a later date. A {@link Future} is + * returned to the caller representing the task that is either currently + * performing the scale operation or will at a future date depending on where it + * is in the {@link ExecutorService}'s queue. {@link Future#get()} or + * {@link Future#get(long, TimeUnit)} can be used to block on the + * Future, waiting for the scale operation to complete and return + * the resultant {@link BufferedImage} to the caller. + *

+ * This design provides the following features: + *

+ *

Performance

+ * When tuning this class for optimal performance, benchmarking your particular + * hardware is the best approach. For some rough guidelines though, there are + * two resources you want to watch closely: + *
    + *
  1. JVM Heap Memory (Assume physical machine memory is always sufficiently + * large)
  2. + *
  3. # of CPU Cores
  4. + *
+ * You never want to allocate more scaling threads than you have CPU cores and + * on a sufficiently busy host where some of the cores may be busy running a + * database or a web server, you will want to allocate even less scaling + * threads. + *

+ * So as a maximum you would never want more scaling threads than CPU cores in + * any situation and less so on a busy server. + *

+ * If you allocate more threads than you have available CPU cores, your scaling + * operations will slow down as the CPU will spend a considerable amount of time + * context-switching between threads on the same core trying to finish all the + * tasks in parallel. You might still be tempted to do this because of the I/O + * delay some threads will encounter reading images off disk, but when you do + * your own benchmarking you'll likely find (as I did) that the actual disk I/O + * necessary to pull the image data off disk is a much smaller portion of the + * execution time than the actual scaling operations. + *

+ * If you are executing on a storage medium that is unexpectedly slow and I/O is + * a considerable portion of the scaling operation (e.g. S3 or EBS volumes), + * feel free to try using more threads than CPU cores to see if that helps; but + * in most normal cases, it will only slow down all other parallel scaling + * operations. + *

+ * As for memory, every time an image is scaled it is decoded into a + * {@link BufferedImage} and stored in the JVM Heap space (decoded image + * instances are always larger than the source images on-disk). For larger + * images, that can use up quite a bit of memory. You will need to benchmark + * your particular use-cases on your hardware to get an idea of where the sweet + * spot is for this; if you are operating within tight memory bounds, you may + * want to limit simultaneous scaling operations to 1 or 2 regardless of the + * number of cores just to avoid having too many {@link BufferedImage} instances + * in JVM Heap space at the same time. + *

+ * These are rough metrics and behaviors to give you an idea of how best to tune + * this class for your deployment, but nothing can replacement writing a small + * Java class that scales a handful of images in a number of different ways and + * testing that directly on your deployment hardware. + *

Resource Overhead

+ * The {@link ExecutorService} utilized by this class won't be initialized until + * one of the operation methods are called, at which point the + * service will be instantiated for the first time and operation + * queued up. + *

+ * More specifically, if you have no need for asynchronous image processing + * offered by this class, you don't need to worry about wasted resources or + * hanging/idle threads as they will never be created if you never use this + * class. + *

Cleaning up Service Threads

+ * By default the {@link Thread}s created by the internal + * {@link ThreadPoolExecutor} do not run in daemon mode; which + * means they will block the host VM from exiting until they are explicitly shut + * down in a client application; in a server application the container will shut + * down the pool forcibly. + *

+ * If you have used the {@link AsyncScalr} class and are trying to shut down a + * client application, you will need to call {@link #getService()} then + * {@link ExecutorService#shutdown()} or {@link ExecutorService#shutdownNow()} + * to have the threads terminated; you may also want to look at the + * {@link ExecutorService#awaitTermination(long, TimeUnit)} method if you'd like + * to more closely monitor the shutting down process (and finalization of + * pending scale operations). + *

Reusing Shutdown AsyncScalr

+ * If you have previously called shutdown on the underlying service + * utilized by this class, subsequent calls to any of the operations this class + * provides will invoke the internal {@link #checkService()} method which will + * replace the terminated underlying {@link ExecutorService} with a new one via + * the {@link #createService()} method. + *

Custom Implementations

+ * If a subclass wants to customize the {@link ExecutorService} or + * {@link ThreadFactory} used under the covers, this can be done by overriding + * the {@link #createService()} method which is invoked by this class anytime a + * new {@link ExecutorService} is needed. + *

+ * By default the {@link #createService()} method delegates to the + * {@link #createService(ThreadFactory)} method with a new instance of + * {@link DefaultThreadFactory}. Either of these methods can be overridden and + * customized easily if desired. + *

+ * TIP: A common customization to this class is to make the + * {@link Thread}s generated by the underlying factory more server-friendly, in + * which case the caller would want to use an instance of the + * {@link ServerThreadFactory} when creating the new {@link ExecutorService}. + *

+ * This can be done in one line by overriding {@link #createService()} and + * returning the result of: + * return createService(new ServerThreadFactory()); + *

+ * By default this class uses an {@link ThreadPoolExecutor} internally to handle + * execution of queued image operations. If a different type of + * {@link ExecutorService} is desired, again, simply overriding the + * {@link #createService()} method of choice is the right way to do that. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 3.2 + */ +@SuppressWarnings("javadoc") +public class AsyncScalr { + /** + * System property name used to set the number of threads the default + * underlying {@link ExecutorService} will use to process async image + * operations. + *

+ * Value is "imgscalr.async.threadCount". + */ + public static final String THREAD_COUNT_PROPERTY_NAME = "imgscalr.async.threadCount"; + + /** + * Number of threads the internal {@link ExecutorService} will use to + * simultaneously execute scale requests. + *

+ * This value can be changed by setting the + * imgscalr.async.threadCount system property (see + * {@link #THREAD_COUNT_PROPERTY_NAME}) to a valid integer value > 0. + *

+ * Default value is 2. + */ + public static final int THREAD_COUNT = Integer.getInteger(THREAD_COUNT_PROPERTY_NAME, 2); + + /** + * Initializer used to verify the THREAD_COUNT system property. + */ + static { + if (THREAD_COUNT < 1) + throw new RuntimeException("System property '" + THREAD_COUNT_PROPERTY_NAME + "' set THREAD_COUNT to " + + THREAD_COUNT + ", but THREAD_COUNT must be > 0."); + } + + protected static ExecutorService service; + + /** + * Used to get access to the internal {@link ExecutorService} used by this + * class to process scale operations. + *

+ * NOTE: You will need to explicitly shutdown any service + * currently set on this class before the host JVM exits. + *

+ * You can call {@link ExecutorService#shutdown()} to wait for all scaling + * operations to complete first or call + * {@link ExecutorService#shutdownNow()} to kill any in-process operations + * and purge all pending operations before exiting. + *

+ * Additionally you can use + * {@link ExecutorService#awaitTermination(long, TimeUnit)} after issuing a + * shutdown command to try and wait until the service has finished all + * tasks. + * + * @return the current {@link ExecutorService} used by this class to process + * scale operations. + */ + public static ExecutorService getService() { + return service; + } + + /** + * @see Scalr#apply(BufferedImage, BufferedImageOp...) + */ + public static Future apply(final BufferedImage src, final BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.apply(src, ops); + } + }); + } + + /** + * @see Scalr#crop(BufferedImage, int, int, BufferedImageOp...) + */ + public static Future crop(final BufferedImage src, final int width, final int height, + final BufferedImageOp... ops) throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.crop(src, width, height, ops); + } + }); + } + + /** + * @see Scalr#crop(BufferedImage, int, int, int, int, BufferedImageOp...) + */ + public static Future crop(final BufferedImage src, final int x, final int y, final int width, + final int height, final BufferedImageOp... ops) throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.crop(src, x, y, width, height, ops); + } + }); + } + + /** + * @see Scalr#pad(BufferedImage, int, BufferedImageOp...) + */ + public static Future pad(final BufferedImage src, final int padding, final BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.pad(src, padding, ops); + } + }); + } + + /** + * @see Scalr#pad(BufferedImage, int, Color, BufferedImageOp...) + */ + public static Future pad(final BufferedImage src, final int padding, final Color color, + final BufferedImageOp... ops) throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.pad(src, padding, color, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, final int targetSize, + final BufferedImageOp... ops) throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, targetSize, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, Method, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, final Method scalingMethod, + final int targetSize, final BufferedImageOp... ops) throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, scalingMethod, targetSize, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, Mode, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, final Mode resizeMode, final int targetSize, + final BufferedImageOp... ops) throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, resizeMode, targetSize, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, Method, Mode, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, final Method scalingMethod, + final Mode resizeMode, final int targetSize, final BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, scalingMethod, resizeMode, targetSize, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, int, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, final int targetWidth, final int targetHeight, + final BufferedImageOp... ops) throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, targetWidth, targetHeight, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, Method, int, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, final Method scalingMethod, + final int targetWidth, final int targetHeight, final BufferedImageOp... ops) { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, scalingMethod, targetWidth, targetHeight, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, Mode, int, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, final Mode resizeMode, final int targetWidth, + final int targetHeight, final BufferedImageOp... ops) throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, resizeMode, targetWidth, targetHeight, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, Method, Mode, int, int, + * BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, final Method scalingMethod, + final Mode resizeMode, final int targetWidth, final int targetHeight, final BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, scalingMethod, resizeMode, targetWidth, targetHeight, ops); + } + }); + } + + /** + * @see Scalr#rotate(BufferedImage, Rotation, BufferedImageOp...) + */ + public static Future rotate(final BufferedImage src, final Rotation rotation, + final BufferedImageOp... ops) throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.rotate(src, rotation, ops); + } + }); + } + + protected static ExecutorService createService() { + return createService(new DefaultThreadFactory()); + } + + protected static ExecutorService createService(ThreadFactory factory) throws IllegalArgumentException { + if (factory == null) + throw new IllegalArgumentException("factory cannot be null"); + + return Executors.newFixedThreadPool(THREAD_COUNT, factory); + } + + /** + * Used to verify that the underlying service points at an + * active {@link ExecutorService} instance that can be used by this class. + *

+ * If service is null, has been shutdown or + * terminated then this method will replace it with a new + * {@link ExecutorService} by calling the {@link #createService()} method + * and assigning the returned value to service. + *

+ * Any subclass that wants to customize the {@link ExecutorService} or + * {@link ThreadFactory} used internally by this class should override the + * {@link #createService()}. + */ + protected static void checkService() { + if (service == null || service.isShutdown() || service.isTerminated()) { + /* + * If service was shutdown or terminated, assigning a new value will + * free the reference to the instance, allowing it to be GC'ed when + * it is done shutting down (assuming it hadn't already). + */ + service = createService(); + } + } + + /** + * Default {@link ThreadFactory} used by the internal + * {@link ExecutorService} to creates execution {@link Thread}s for image + * scaling. + *

+ * More or less a copy of the hidden class backing the + * {@link Executors#defaultThreadFactory()} method, but exposed here to make + * it easier for implementors to extend and customize. + * + * @author Doug Lea + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 4.0 + */ + protected static class DefaultThreadFactory implements ThreadFactory { + protected static final AtomicInteger poolNumber = new AtomicInteger(1); + + protected final ThreadGroup group; + protected final AtomicInteger threadNumber = new AtomicInteger(1); + protected final String namePrefix; + + DefaultThreadFactory() { + SecurityManager manager = System.getSecurityManager(); + + /* + * Determine the group that threads created by this factory will be + * in. + */ + group = (manager == null ? Thread.currentThread().getThreadGroup() : manager.getThreadGroup()); + + /* + * Define a common name prefix for the threads created by this + * factory. + */ + namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; + } + + /** + * Used to create a {@link Thread} capable of executing the given + * {@link Runnable}. + *

+ * Thread created by this factory are utilized by the parent + * {@link ExecutorService} when processing queued up scale operations. + */ + public Thread newThread(Runnable r) { + /* + * Create a new thread in our specified group with a meaningful + * thread name so it is easy to identify. + */ + Thread thread = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); + + // Configure thread according to class or subclass + thread.setDaemon(false); + thread.setPriority(Thread.NORM_PRIORITY); + + return thread; + } + } + + /** + * An extension of the {@link DefaultThreadFactory} class that makes two + * changes to the execution {@link Thread}s it generations: + *

    + *
  1. Threads are set to be daemon threads instead of user threads.
  2. + *
  3. Threads execute with a priority of {@link Thread#MIN_PRIORITY} to + * make them more compatible with server environment deployments.
  4. + *
+ * This class is provided as a convenience for subclasses to use if they + * want this (common) customization to the {@link Thread}s used internally + * by {@link AsyncScalr} to process images, but don't want to have to write + * the implementation. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 4.0 + */ + protected static class ServerThreadFactory extends DefaultThreadFactory { + /** + * Overridden to set daemon property to true + * and decrease the priority of the new thread to + * {@link Thread#MIN_PRIORITY} before returning it. + */ + @Override + public Thread newThread(Runnable r) { + Thread thread = super.newThread(r); + + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + + return thread; + } + } +} \ No newline at end of file Index: lams_common/src/java/org/lamsfoundation/lams/util/imgscalr/ResizePictureUtil.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/util/imgscalr/ResizePictureUtil.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/util/imgscalr/ResizePictureUtil.java (revision 4340b53d8412d3838eb7400ddfc821018234a1e9) @@ -0,0 +1,98 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +package org.lamsfoundation.lams.util.imgscalr; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; + +import org.apache.log4j.Logger; +import org.lamsfoundation.lams.util.CircularByteBuffer; +import org.lamsfoundation.lams.util.imgscalr.Scalr.Method; + +/** + * @author Andrey Balan + */ +public class ResizePictureUtil { + + private static Logger log = Logger.getLogger(ResizePictureUtil.class); + + /** + * Reads the original image, creates a resized copy of it and returns its input stream. largestDimension is the + * largest + * dimension of the resized image, the other dimension is scaled accordingly. + * + * @param is + * original image's input stream + * @param largestDimension + * the largest dimension of the resized image, the other dimension is scaled accordingly + * @return + * @throws IOException + * @throws UploadImageGalleryFileException + */ + public static InputStream resize(InputStream is, int largestDimension) throws IOException { + try { + // load image + BufferedImage image = ImageIO.read(is); + return ResizePictureUtil.resize(image, largestDimension); + + } catch (IOException e) { + log.error(e.getStackTrace()); + return null; + } + } + + /** + * Reads the original image, creates a resized copy of it and returns its input stream. largestDimension is the + * largest + * dimension of the resized image, the other dimension is scaled accordingly. + * + * @param image + * original image + * @param largestDimension + * the largest dimension of the resized image, the other dimension is scaled accordingly + * @return + * @throws IOException + * @throws UploadImageGalleryFileException + */ + public static InputStream resize(BufferedImage image, int largestDimension) throws IOException { + try { + //resize to 150 pixels max + BufferedImage outImage = Scalr.resize(image, Method.QUALITY, largestDimension); + + // buffer all data in a circular buffer of infinite size + CircularByteBuffer cbb = new CircularByteBuffer(CircularByteBuffer.INFINITE_SIZE); + ImageIO.write(outImage, "PNG", cbb.getOutputStream()); + cbb.getOutputStream().close(); + + return cbb.getInputStream(); + + } catch (IOException e) { + log.error(e.getStackTrace()); + return null; + } + } + +} Index: lams_common/src/java/org/lamsfoundation/lams/util/imgscalr/Scalr.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/util/imgscalr/Scalr.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/util/imgscalr/Scalr.java (revision 4340b53d8412d3838eb7400ddfc821018234a1e9) @@ -0,0 +1,2286 @@ +/** + * Copyright 2011 The Buzz Media, LLC + * + * 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.lamsfoundation.lams.util.imgscalr; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.RenderingHints; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.AreaAveragingScaleFilter; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ColorConvertOp; +import java.awt.image.ColorModel; +import java.awt.image.ConvolveOp; +import java.awt.image.ImagingOpException; +import java.awt.image.IndexColorModel; +import java.awt.image.Kernel; +import java.awt.image.RasterFormatException; +import java.awt.image.RescaleOp; + +import javax.imageio.ImageIO; + +/** + * Class used to implement performant, high-quality and intelligent image + * scaling and manipulation algorithms in native Java 2D. + *

+ * This class utilizes the Java2D "best practices" for image manipulation, + * ensuring that all operations (even most user-provided {@link BufferedImageOp} + * s) are hardware accelerated if provided by the platform and host-VM. + *

+ *

Image Quality

+ * This class implements a few different methods for scaling an image, providing + * either the best-looking result, the fastest result or a balanced result + * between the two depending on the scaling hint provided (see {@link Method}). + *

+ * This class also implements an optimized version of the incremental scaling + * algorithm presented by Chris Campbell in his Perils of + * Image.getScaledInstance() article in order to give the best-looking image + * resize results (e.g. generating thumbnails that aren't blurry or jagged). + *

+ * The results generated by imgscalr using this method, as compared to a single + * {@link RenderingHints#VALUE_INTERPOLATION_BICUBIC} scale operation look much + * better, especially when using the {@link Method#ULTRA_QUALITY} method. + *

+ * Only when scaling using the {@link Method#AUTOMATIC} method will this class + * look at the size of the image before selecting an approach to scaling the + * image. If {@link Method#QUALITY} is specified, the best-looking algorithm + * possible is always used. + *

+ * Minor modifications are made to Campbell's original implementation in the + * form of: + *

    + *
  1. Instead of accepting a user-supplied interpolation method, + * {@link RenderingHints#VALUE_INTERPOLATION_BICUBIC} interpolation is always + * used. This was done after A/B comparison testing with large images + * down-scaled to thumbnail sizes showed noticeable "blurring" when BILINEAR + * interpolation was used. Given that Campbell's algorithm is only used in + * QUALITY mode when down-scaling, it was determined that the user's expectation + * of a much less blurry picture would require that BICUBIC be the default + * interpolation in order to meet the QUALITY expectation.
  2. + *
  3. After each iteration of the do-while loop that incrementally scales the + * source image down, an explicit effort is made to call + * {@link BufferedImage#flush()} on the interim temporary {@link BufferedImage} + * instances created by the algorithm in an attempt to ensure a more complete GC + * cycle by the VM when cleaning up the temporary instances (this is in addition + * to disposing of the temporary {@link Graphics2D} references as well).
  4. + *
  5. Extensive comments have been added to increase readability of the code.
  6. + *
  7. Variable names have been expanded to increase readability of the code.
  8. + *
+ *

+ * NOTE: This class does not call {@link BufferedImage#flush()} + * on any of the source images passed in by calling code; it is up to + * the original caller to dispose of their source images when they are no longer + * needed so the VM can most efficiently GC them. + *

Image Proportions

+ * All scaling operations implemented by this class maintain the proportions of + * the original image unless a mode of {@link Mode#FIT_EXACT} is specified; in + * which case the orientation and proportion of the source image is ignored and + * the image is stretched (if necessary) to fit the exact dimensions given. + *

+ * When not using {@link Mode#FIT_EXACT}, in order to maintain the + * proportionality of the original images, this class implements the following + * behavior: + *

    + *
  1. If the image is LANDSCAPE-oriented or SQUARE, treat the + * targetWidth as the primary dimension and re-calculate the + * targetHeight regardless of what is passed in.
  2. + *
  3. If image is PORTRAIT-oriented, treat the targetHeight as the + * primary dimension and re-calculate the targetWidth regardless of + * what is passed in.
  4. + *
  5. If a {@link Mode} value of {@link Mode#FIT_TO_WIDTH} or + * {@link Mode#FIT_TO_HEIGHT} is passed in to the resize method, + * the image's orientation is ignored and the scaled image is fit to the + * preferred dimension by using the value passed in by the user for that + * dimension and recalculating the other (regardless of image orientation). This + * is useful, for example, when working with PORTRAIT oriented images that you + * need to all be the same width or visa-versa (e.g. showing user profile + * pictures in a directory listing).
  6. + *
+ *

Optimized Image Handling

+ * Java2D provides support for a number of different image types defined as + * BufferedImage.TYPE_* variables, unfortunately not all image + * types are supported equally in the Java2D rendering pipeline. + *

+ * Some more obscure image types either have poor or no support, leading to + * severely degraded quality and processing performance when an attempt is made + * by imgscalr to create a scaled instance of the same type as the + * source image. In many cases, especially when applying {@link BufferedImageOp} + * s, using poorly supported image types can even lead to exceptions or total + * corruption of the image (e.g. solid black image). + *

+ * imgscalr specifically accounts for and automatically hands + * ALL of these pain points for you internally by shuffling all + * images into one of two types: + *

    + *
  1. {@link BufferedImage#TYPE_INT_RGB}
  2. + *
  3. {@link BufferedImage#TYPE_INT_ARGB}
  4. + *
+ * depending on if the source image utilizes transparency or not. This is a + * recommended approach by the Java2D team for dealing with poorly (or non) + * supported image types. More can be read about this issue here. + *

+ * This is also the reason we recommend using + * {@link #apply(BufferedImage, BufferedImageOp...)} to apply your own ops to + * images even if you aren't using imgscalr for anything else. + *

GIF Transparency

+ * Unfortunately in Java 6 and earlier, support for GIF's + * {@link IndexColorModel} is sub-par, both in accurate color-selection and in + * maintaining transparency when moving to an image of type + * {@link BufferedImage#TYPE_INT_ARGB}; because of this issue when a GIF image + * is processed by imgscalr and the result saved as a GIF file (instead of PNG), + * it is possible to lose the alpha channel of a transparent image or in the + * case of applying an optional {@link BufferedImageOp}, lose the entire picture + * all together in the result (long standing JDK bugs are filed for all of these + * issues). + *

+ * imgscalr currently does nothing to work around this manually because it is a + * defect in the native platform code itself. Fortunately it looks like the + * issues are half-fixed in Java 7 and any manual workarounds we could attempt + * internally are relatively expensive, in the form of hand-creating and setting + * RGB values pixel-by-pixel with a custom {@link ColorModel} in the scaled + * image. This would lead to a very measurable negative impact on performance + * without the caller understanding why. + *

+ * Workaround: A workaround to this issue with all version of + * Java is to simply save a GIF as a PNG; no change to your code needs to be + * made except when the image is saved out, e.g. using {@link ImageIO}. + *

+ * When a file type of "PNG" is used, both the transparency and high color + * quality will be maintained as the PNG code path in Java2D is superior to the + * GIF implementation. + *

+ * If the issue with optional {@link BufferedImageOp}s destroying GIF image + * content is ever fixed in the platform, saving out resulting images as GIFs + * should suddenly start working. + *

+ * More can be read about the issue here and here. + *

Thread Safety

+ * The {@link Scalr} class is thread-safe (as all the methods + * are static); this class maintains no internal state while + * performing any of the provided operations and is safe to call simultaneously + * from multiple threads. + *

Logging

+ * This class implements all its debug logging via the + * {@link #log(int, String, Object...)} method. At this time logging is done + * directly to System.out via the printf method. This + * allows the logging to be light weight and easy to capture (every imgscalr log + * message is prefixed with the {@link #LOG_PREFIX} string) while adding no + * dependencies to the library. + *

+ * Implementation of logging in this class is as efficient as possible; avoiding + * any calls to the logger method or passing of arguments if logging is not + * enabled to avoid the (hidden) cost of constructing the Object[] argument for + * the varargs-based method call. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 1.1 + */ +public class Scalr { + /** + * System property name used to define the debug boolean flag. + *

+ * Value is "imgscalr.debug". + */ + public static final String DEBUG_PROPERTY_NAME = "imgscalr.debug"; + + /** + * System property name used to define a custom log prefix. + *

+ * Value is "imgscalr.logPrefix". + */ + public static final String LOG_PREFIX_PROPERTY_NAME = "imgscalr.logPrefix"; + + /** + * Flag used to indicate if debugging output has been enabled by setting the + * "imgscalr.debug" system property to true. This + * value will be false if the "imgscalr.debug" + * system property is undefined or set to false. + *

+ * This property can be set on startup with:
+ * + * -Dimgscalr.debug=true + * or by calling {@link System#setProperty(String, String)} to set a + * new property value for {@link #DEBUG_PROPERTY_NAME} before this class is + * loaded. + *

+ * Default value is false. + */ + public static final boolean DEBUG = Boolean.getBoolean(DEBUG_PROPERTY_NAME); + + /** + * Prefix to every log message this library logs. Using a well-defined + * prefix helps make it easier both visually and programmatically to scan + * log files for messages produced by this library. + *

+ * This property can be set on startup with:
+ * + * -Dimgscalr.logPrefix=<YOUR PREFIX HERE> + * or by calling {@link System#setProperty(String, String)} to set a + * new property value for {@link #LOG_PREFIX_PROPERTY_NAME} before this + * class is loaded. + *

+ * Default value is "[imgscalr] " (including the space). + */ + public static final String LOG_PREFIX = System.getProperty(LOG_PREFIX_PROPERTY_NAME, "[imgscalr] "); + + /** + * A {@link ConvolveOp} using a very light "blur" kernel that acts like an + * anti-aliasing filter (softens the image a bit) when applied to an image. + *

+ * A common request by users of the library was that they wished to "soften" + * resulting images when scaling them down drastically. After quite a bit of + * A/B testing, the kernel used by this Op was selected as the closest match + * for the target which was the softer results from the deprecated + * {@link AreaAveragingScaleFilter} (which is used internally by the + * deprecated {@link Image#getScaledInstance(int, int, int)} method in the + * JDK that imgscalr is meant to replace). + *

+ * This ConvolveOp uses a 3x3 kernel with the values: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
.0f.08f.0f
.08f.68f.08f
.0f.08f.0f
+ *

+ * For those that have worked with ConvolveOps before, this Op uses the + * {@link ConvolveOp#EDGE_NO_OP} instruction to not process the pixels along + * the very edge of the image (otherwise EDGE_ZERO_FILL would create a + * black-border around the image). If you have not worked with a ConvolveOp + * before, it just means this default OP will "do the right thing" and not + * give you garbage results. + *

+ * This ConvolveOp uses no {@link RenderingHints} values as internally the + * {@link ConvolveOp} class only uses hints when doing a color conversion + * between the source and destination {@link BufferedImage} targets. + * imgscalr allows the {@link ConvolveOp} to create its own destination + * image every time, so no color conversion is ever needed and thus no + * hints. + *

Performance

+ * Use of this (and other) {@link ConvolveOp}s are hardware accelerated when + * possible. For more information on if your image op is hardware + * accelerated or not, check the source code of the underlying JDK class + * that actually executes the Op code, sun.awt.image.ImagingLib. + *

Known Issues

+ * In all versions of Java (tested up to Java 7 preview Build 131), running + * this op against a GIF with transparency and attempting to save the + * resulting image as a GIF results in a corrupted/empty file. The file must + * be saved out as a PNG to maintain the transparency. + * + * @since 3.0 + */ + public static final ConvolveOp OP_ANTIALIAS = new ConvolveOp( + new Kernel(3, 3, new float[] { .0f, .08f, .0f, .08f, .68f, .08f, .0f, .08f, .0f }), ConvolveOp.EDGE_NO_OP, + null); + + /** + * A {@link RescaleOp} used to make any input image 10% darker. + *

+ * This operation can be applied multiple times in a row if greater than 10% + * changes in brightness are desired. + * + * @since 4.0 + */ + public static final RescaleOp OP_DARKER = new RescaleOp(0.9f, 0, null); + + /** + * A {@link RescaleOp} used to make any input image 10% brighter. + *

+ * This operation can be applied multiple times in a row if greater than 10% + * changes in brightness are desired. + * + * @since 4.0 + */ + public static final RescaleOp OP_BRIGHTER = new RescaleOp(1.1f, 0, null); + + /** + * A {@link ColorConvertOp} used to convert any image to a grayscale color + * palette. + *

+ * Applying this op multiple times to the same image has no compounding + * effects. + * + * @since 4.0 + */ + public static final ColorConvertOp OP_GRAYSCALE = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), + null); + + /** + * Static initializer used to prepare some of the variables used by this + * class. + */ + static { + log(0, "Debug output ENABLED"); + } + + /** + * Used to define the different scaling hints that the algorithm can use. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 1.1 + */ + public static enum Method { + /** + * Used to indicate that the scaling implementation should decide which + * method to use in order to get the best looking scaled image in the + * least amount of time. + *

+ * The scaling algorithm will use the + * {@link Scalr#THRESHOLD_QUALITY_BALANCED} or + * {@link Scalr#THRESHOLD_BALANCED_SPEED} thresholds as cut-offs to + * decide between selecting the QUALITY, + * BALANCED or SPEED scaling algorithms. + *

+ * By default the thresholds chosen will give nearly the best looking + * result in the fastest amount of time. We intend this method to work + * for 80% of people looking to scale an image quickly and get a good + * looking result. + */ + AUTOMATIC, + /** + * Used to indicate that the scaling implementation should scale as fast + * as possible and return a result. For smaller images (800px in size) + * this can result in noticeable aliasing but it can be a few magnitudes + * times faster than using the QUALITY method. + */ + SPEED, + /** + * Used to indicate that the scaling implementation should use a scaling + * operation balanced between SPEED and QUALITY. Sometimes SPEED looks + * too low quality to be useful (e.g. text can become unreadable when + * scaled using SPEED) but using QUALITY mode will increase the + * processing time too much. This mode provides a "better than SPEED" + * quality in a "less than QUALITY" amount of time. + */ + BALANCED, + /** + * Used to indicate that the scaling implementation should do everything + * it can to create as nice of a result as possible. This approach is + * most important for smaller pictures (800px or smaller) and less + * important for larger pictures as the difference between this method + * and the SPEED method become less and less noticeable as the + * source-image size increases. Using the AUTOMATIC method will + * automatically prefer the QUALITY method when scaling an image down + * below 800px in size. + */ + QUALITY, + /** + * Used to indicate that the scaling implementation should go above and + * beyond the work done by {@link Method#QUALITY} to make the image look + * exceptionally good at the cost of more processing time. This is + * especially evident when generating thumbnails of images that look + * jagged with some of the other {@link Method}s (even + * {@link Method#QUALITY}). + */ + ULTRA_QUALITY; + } + + /** + * Used to define the different modes of resizing that the algorithm can + * use. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 3.1 + */ + public static enum Mode { + /** + * Used to indicate that the scaling implementation should calculate + * dimensions for the resultant image by looking at the image's + * orientation and generating proportional dimensions that best fit into + * the target width and height given + * + * See "Image Proportions" in the {@link Scalr} class description for + * more detail. + */ + AUTOMATIC, + /** + * Used to fit the image to the exact dimensions given regardless of the + * image's proportions. If the dimensions are not proportionally + * correct, this will introduce vertical or horizontal stretching to the + * image. + *

+ * It is recommended that you use one of the other FIT_TO + * modes or {@link Mode#AUTOMATIC} if you want the image to look + * correct, but if dimension-fitting is the #1 priority regardless of + * how it makes the image look, that is what this mode is for. + */ + FIT_EXACT, + /** + * Used to indicate that the scaling implementation should calculate + * dimensions for the largest image that fit within the bounding box, + * without cropping or distortion, retaining the original proportions. + */ + BEST_FIT_BOTH, + /** + * Used to indicate that the scaling implementation should calculate + * dimensions for the resultant image that best-fit within the given + * width, regardless of the orientation of the image. + */ + FIT_TO_WIDTH, + /** + * Used to indicate that the scaling implementation should calculate + * dimensions for the resultant image that best-fit within the given + * height, regardless of the orientation of the image. + */ + FIT_TO_HEIGHT; + } + + /** + * Used to define the different types of rotations that can be applied to an + * image during a resize operation. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 3.2 + */ + public static enum Rotation { + /** + * 90-degree, clockwise rotation (to the right). This is equivalent to a + * quarter-turn of the image to the right; moving the picture on to its + * right side. + */ + CW_90, + /** + * 180-degree, clockwise rotation (to the right). This is equivalent to + * 1 half-turn of the image to the right; rotating the picture around + * until it is upside down from the original position. + */ + CW_180, + /** + * 270-degree, clockwise rotation (to the right). This is equivalent to + * a quarter-turn of the image to the left; moving the picture on to its + * left side. + */ + CW_270, + /** + * Flip the image horizontally by reflecting it around the y axis. + *

+ * This is not a standard rotation around a center point, but instead + * creates the mirrored reflection of the image horizontally. + *

+ * More specifically, the vertical orientation of the image stays the + * same (the top stays on top, and the bottom on bottom), but the right + * and left sides flip. This is different than a standard rotation where + * the top and bottom would also have been flipped. + */ + FLIP_HORZ, + /** + * Flip the image vertically by reflecting it around the x axis. + *

+ * This is not a standard rotation around a center point, but instead + * creates the mirrored reflection of the image vertically. + *

+ * More specifically, the horizontal orientation of the image stays the + * same (the left stays on the left and the right stays on the right), + * but the top and bottom sides flip. This is different than a standard + * rotation where the left and right would also have been flipped. + */ + FLIP_VERT; + } + + /** + * Threshold (in pixels) at which point the scaling operation using the + * {@link Method#AUTOMATIC} method will decide if a {@link Method#BALANCED} + * method will be used (if smaller than or equal to threshold) or a + * {@link Method#SPEED} method will be used (if larger than threshold). + *

+ * The bigger the image is being scaled to, the less noticeable degradations + * in the image becomes and the faster algorithms can be selected. + *

+ * The value of this threshold (1600) was chosen after visual, by-hand, A/B + * testing between different types of images scaled with this library; both + * photographs and screenshots. It was determined that images below this + * size need to use a {@link Method#BALANCED} scale method to look decent in + * most all cases while using the faster {@link Method#SPEED} method for + * images bigger than this threshold showed no noticeable degradation over a + * BALANCED scale. + */ + public static final int THRESHOLD_BALANCED_SPEED = 1600; + + /** + * Threshold (in pixels) at which point the scaling operation using the + * {@link Method#AUTOMATIC} method will decide if a {@link Method#QUALITY} + * method will be used (if smaller than or equal to threshold) or a + * {@link Method#BALANCED} method will be used (if larger than threshold). + *

+ * The bigger the image is being scaled to, the less noticeable degradations + * in the image becomes and the faster algorithms can be selected. + *

+ * The value of this threshold (800) was chosen after visual, by-hand, A/B + * testing between different types of images scaled with this library; both + * photographs and screenshots. It was determined that images below this + * size need to use a {@link Method#QUALITY} scale method to look decent in + * most all cases while using the faster {@link Method#BALANCED} method for + * images bigger than this threshold showed no noticeable degradation over a + * QUALITY scale. + */ + public static final int THRESHOLD_QUALITY_BALANCED = 800; + + /** + * Used to apply, in the order given, 1 or more {@link BufferedImageOp}s to + * a given {@link BufferedImage} and return the result. + *

+ * Feature: This implementation works around a + * decade-old JDK bug that can cause a {@link RasterFormatException} + * when applying a perfectly valid {@link BufferedImageOp}s to images. + *

+ * Feature: This implementation also works around + * {@link BufferedImageOp}s failing to apply and throwing + * {@link ImagingOpException}s when run against a src image + * type that is poorly supported. Unfortunately using {@link ImageIO} and + * standard Java methods to load images provides no consistency in getting + * images in well-supported formats. This method automatically accounts and + * corrects for all those problems (if necessary). + *

+ * It is recommended you always use this method to apply any + * {@link BufferedImageOp}s instead of relying on directly using the + * {@link BufferedImageOp#filter(BufferedImage, BufferedImage)} method. + *

+ * Performance: Not all {@link BufferedImageOp}s are + * hardware accelerated operations, but many of the most popular (like + * {@link ConvolveOp}) are. For more information on if your image op is + * hardware accelerated or not, check the source code of the underlying JDK + * class that actually executes the Op code, sun.awt.image.ImagingLib. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will have the ops applied to it. + * @param ops + * 1 or more ops to apply to the image. + * + * @return a new {@link BufferedImage} that represents the src + * with all the given operations applied to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if ops is null or empty. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage apply(BufferedImage src, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + long t = -1; + if (DEBUG) + t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (ops == null || ops.length == 0) + throw new IllegalArgumentException("ops cannot be null or empty"); + + int type = src.getType(); + + /* + * Ensure the src image is in the best supported image type before we + * continue, otherwise it is possible our calls below to getBounds2D and + * certainly filter(...) may fail if not. + * + * Java2D makes an attempt at applying most BufferedImageOps using + * hardware acceleration via the ImagingLib internal library. + * + * Unfortunately may of the BufferedImageOp are written to simply fail + * with an ImagingOpException if the operation cannot be applied with no + * additional information about what went wrong or attempts at + * re-applying it in different ways. + * + * This is assuming the failing BufferedImageOp even returns a null + * image after failing to apply; some simply return a corrupted/black + * image that result in no exception and it is up to the user to + * discover this. + * + * In internal testing, EVERY failure I've ever seen was the result of + * the source image being in a poorly-supported BufferedImage Type like + * BGR or ABGR (even though it was loaded with ImageIO). + * + * To avoid this nasty/stupid surprise with BufferedImageOps, we always + * ensure that the src image starts in an optimally supported format + * before we try and apply the filter. + */ + if (!(type == BufferedImage.TYPE_INT_RGB || type == BufferedImage.TYPE_INT_ARGB)) + src = copyToOptimalImage(src); + + if (DEBUG) + log(0, "Applying %d BufferedImageOps...", ops.length); + + boolean hasReassignedSrc = false; + + for (int i = 0; i < ops.length; i++) { + long subT = -1; + if (DEBUG) + subT = System.currentTimeMillis(); + BufferedImageOp op = ops[i]; + + // Skip null ops instead of throwing an exception. + if (op == null) + continue; + + if (DEBUG) + log(1, "Applying BufferedImageOp [class=%s, toString=%s]...", op.getClass(), op.toString()); + + /* + * Must use op.getBounds instead of src.getWidth and src.getHeight + * because we are trying to create an image big enough to hold the + * result of this operation (which may be to scale the image + * smaller), in that case the bounds reported by this op and the + * bounds reported by the source image will be different. + */ + Rectangle2D resultBounds = op.getBounds2D(src); + + // Watch out for flaky/misbehaving ops that fail to work right. + if (resultBounds == null) + throw new ImagingOpException("BufferedImageOp [" + op.toString() + + "] getBounds2D(src) returned null bounds for the target image; this should not happen and indicates a problem with application of this type of op."); + + /* + * We must manually create the target image; we cannot rely on the + * null-destination filter() method to create a valid destination + * for us thanks to this JDK bug that has been filed for almost a + * decade: + * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4965606 + */ + BufferedImage dest = createOptimalImage(src, (int) Math.round(resultBounds.getWidth()), + (int) Math.round(resultBounds.getHeight())); + + // Perform the operation, update our result to return. + BufferedImage result = op.filter(src, dest); + + /* + * Flush the 'src' image ONLY IF it is one of our interim temporary + * images being used when applying 2 or more operations back to + * back. We never want to flush the original image passed in. + */ + if (hasReassignedSrc) + src.flush(); + + /* + * Incase there are more operations to perform, update what we + * consider the 'src' reference to our last result so on the next + * iteration the next op is applied to this result and not back + * against the original src passed in. + */ + src = result; + + /* + * Keep track of when we re-assign 'src' to an interim temporary + * image, so we know when we can explicitly flush it and clean up + * references on future iterations. + */ + hasReassignedSrc = true; + + if (DEBUG) + log(1, "Applied BufferedImageOp in %d ms, result [width=%d, height=%d]", + System.currentTimeMillis() - subT, result.getWidth(), result.getHeight()); + } + + if (DEBUG) + log(0, "All %d BufferedImageOps applied in %d ms", ops.length, System.currentTimeMillis() - t); + + return src; + } + + /** + * Used to crop the given src image from the top-left corner + * and applying any optional {@link BufferedImageOp}s to the result before + * returning it. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image to crop. + * @param width + * The width of the bounding cropping box. + * @param height + * The height of the bounding cropping box. + * @param ops + * 0 or more ops to apply to the image. If + * null or empty then src is return + * unmodified. + * + * @return a new {@link BufferedImage} representing the cropped region of + * the src image with any optional operations applied + * to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if any coordinates of the bounding crop box is invalid within + * the bounds of the src image (e.g. negative or + * too big). + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage crop(BufferedImage src, int width, int height, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return crop(src, 0, 0, width, height, ops); + } + + /** + * Used to crop the given src image and apply any optional + * {@link BufferedImageOp}s to it before returning the result. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image to crop. + * @param x + * The x-coordinate of the top-left corner of the bounding box + * used for cropping. + * @param y + * The y-coordinate of the top-left corner of the bounding box + * used for cropping. + * @param width + * The width of the bounding cropping box. + * @param height + * The height of the bounding cropping box. + * @param ops + * 0 or more ops to apply to the image. If + * null or empty then src is return + * unmodified. + * + * @return a new {@link BufferedImage} representing the cropped region of + * the src image with any optional operations applied + * to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if any coordinates of the bounding crop box is invalid within + * the bounds of the src image (e.g. negative or + * too big). + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage crop(BufferedImage src, int x, int y, int width, int height, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + long t = -1; + if (DEBUG) + t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (x < 0 || y < 0 || width < 0 || height < 0) + throw new IllegalArgumentException("Invalid crop bounds: x [" + x + "], y [" + y + "], width [" + width + + "] and height [" + height + "] must all be >= 0"); + + int srcWidth = src.getWidth(); + int srcHeight = src.getHeight(); + + if ((x + width) > srcWidth) + throw new IllegalArgumentException("Invalid crop bounds: x + width [" + (x + width) + + "] must be <= src.getWidth() [" + srcWidth + "]"); + if ((y + height) > srcHeight) + throw new IllegalArgumentException("Invalid crop bounds: y + height [" + (y + height) + + "] must be <= src.getHeight() [" + srcHeight + "]"); + + if (DEBUG) + log(0, "Cropping Image [width=%d, height=%d] to [x=%d, y=%d, width=%d, height=%d]...", srcWidth, srcHeight, + x, y, width, height); + + // Create a target image of an optimal type to render into. + BufferedImage result = createOptimalImage(src, width, height); + Graphics g = result.getGraphics(); + + /* + * Render the region specified by our crop bounds from the src image + * directly into our result image (which is the exact size of the crop + * region). + */ + g.drawImage(src, 0, 0, width, height, x, y, (x + width), (y + height), null); + g.dispose(); + + if (DEBUG) + log(0, "Cropped Image in %d ms", System.currentTimeMillis() - t); + + // Apply any optional operations (if specified). + if (ops != null && ops.length > 0) + result = apply(result, ops); + + return result; + } + + /** + * Used to apply padding around the edges of an image using + * {@link Color#BLACK} to fill the extra padded space and then return the + * result. + *

+ * The amount of padding specified is applied to all sides; + * more specifically, a padding of 2 would add 2 + * extra pixels of space (filled by the given color) on the + * top, bottom, left and right sides of the resulting image causing the + * result to be 4 pixels wider and 4 pixels taller than the src + * image. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image the padding will be added to. + * @param padding + * The number of pixels of padding to add to each side in the + * resulting image. If this value is 0 then + * src is returned unmodified. + * @param ops + * 0 or more ops to apply to the image. If + * null or empty then src is return + * unmodified. + * + * @return a new {@link BufferedImage} representing src with + * the given padding applied to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if padding is < 1. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage pad(BufferedImage src, int padding, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return pad(src, padding, Color.BLACK); + } + + /** + * Used to apply padding around the edges of an image using the given color + * to fill the extra padded space and then return the result. {@link Color}s + * using an alpha channel (i.e. transparency) are supported. + *

+ * The amount of padding specified is applied to all sides; + * more specifically, a padding of 2 would add 2 + * extra pixels of space (filled by the given color) on the + * top, bottom, left and right sides of the resulting image causing the + * result to be 4 pixels wider and 4 pixels taller than the src + * image. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image the padding will be added to. + * @param padding + * The number of pixels of padding to add to each side in the + * resulting image. If this value is 0 then + * src is returned unmodified. + * @param color + * The color to fill the padded space with. {@link Color}s using + * an alpha channel (i.e. transparency) are supported. + * @param ops + * 0 or more ops to apply to the image. If + * null or empty then src is return + * unmodified. + * + * @return a new {@link BufferedImage} representing src with + * the given padding applied to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if padding is < 1. + * @throws IllegalArgumentException + * if color is null. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage pad(BufferedImage src, int padding, Color color, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + long t = -1; + if (DEBUG) + t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (padding < 1) + throw new IllegalArgumentException("padding [" + padding + "] must be > 0"); + if (color == null) + throw new IllegalArgumentException("color cannot be null"); + + int srcWidth = src.getWidth(); + int srcHeight = src.getHeight(); + + /* + * Double the padding to account for all sides of the image. More + * specifically, if padding is "1" we add 2 pixels to width and 2 to + * height, so we have 1 new pixel of padding all the way around our + * image. + */ + int sizeDiff = (padding * 2); + int newWidth = srcWidth + sizeDiff; + int newHeight = srcHeight + sizeDiff; + + if (DEBUG) + log(0, "Padding Image from [originalWidth=%d, originalHeight=%d, padding=%d] to [newWidth=%d, newHeight=%d]...", + srcWidth, srcHeight, padding, newWidth, newHeight); + + boolean colorHasAlpha = (color.getAlpha() != 255); + boolean imageHasAlpha = (src.getTransparency() != BufferedImage.OPAQUE); + + BufferedImage result; + + /* + * We need to make sure our resulting image that we render into contains + * alpha if either our original image OR the padding color we are using + * contain it. + */ + if (colorHasAlpha || imageHasAlpha) { + if (DEBUG) + log(1, "Transparency FOUND in source image or color, using ARGB image type..."); + + result = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); + } else { + if (DEBUG) + log(1, "Transparency NOT FOUND in source image or color, using RGB image type..."); + + result = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); + } + + Graphics g = result.getGraphics(); + + // Draw the border of the image in the color specified. + g.setColor(color); + g.fillRect(0, 0, newWidth, padding); + g.fillRect(0, padding, padding, newHeight); + g.fillRect(padding, newHeight - padding, newWidth, newHeight); + g.fillRect(newWidth - padding, padding, newWidth, newHeight - padding); + + // Draw the image into the center of the new padded image. + g.drawImage(src, padding, padding, null); + g.dispose(); + + if (DEBUG) + log(0, "Padding Applied in %d ms", System.currentTimeMillis() - t); + + // Apply any optional operations (if specified). + if (ops != null && ops.length > 0) + result = apply(result, ops); + + return result; + } + + /** + * Resize a given image (maintaining its original proportion) to a width and + * height no bigger than targetSize and apply the given + * {@link BufferedImageOp}s (if any) to the result before returning it. + *

+ * A scaling method of {@link Method#AUTOMATIC} and mode of + * {@link Mode#AUTOMATIC} are used. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param targetSize + * The target width and height (square) that you wish the image + * to fit within. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if targetSize is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage resize(BufferedImage src, int targetSize, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, Method.AUTOMATIC, Mode.AUTOMATIC, targetSize, targetSize, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to a width and + * height no bigger than targetSize using the given scaling + * method and apply the given {@link BufferedImageOp}s (if any) to the + * result before returning it. + *

+ * A mode of {@link Mode#AUTOMATIC} is used. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param scalingMethod + * The method used for scaling the image; preferring speed to + * quality or a balance of both. + * @param targetSize + * The target width and height (square) that you wish the image + * to fit within. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if scalingMethod is null. + * @throws IllegalArgumentException + * if targetSize is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Method + */ + public static BufferedImage resize(BufferedImage src, Method scalingMethod, int targetSize, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, scalingMethod, Mode.AUTOMATIC, targetSize, targetSize, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to a width and + * height no bigger than targetSize (or fitting the image to + * the given WIDTH or HEIGHT explicitly, depending on the {@link Mode} + * specified) and apply the given {@link BufferedImageOp}s (if any) to the + * result before returning it. + *

+ * A scaling method of {@link Method#AUTOMATIC} is used. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param resizeMode + * Used to indicate how imgscalr should calculate the final + * target size for the image, either fitting the image to the + * given width ({@link Mode#FIT_TO_WIDTH}) or fitting the image + * to the given height ({@link Mode#FIT_TO_HEIGHT}). If + * {@link Mode#AUTOMATIC} is passed in, imgscalr will calculate + * proportional dimensions for the scaled image based on its + * orientation (landscape, square or portrait). Unless you have + * very specific size requirements, most of the time you just + * want to use {@link Mode#AUTOMATIC} to "do the right thing". + * @param targetSize + * The target width and height (square) that you wish the image + * to fit within. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if resizeMode is null. + * @throws IllegalArgumentException + * if targetSize is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Mode + */ + public static BufferedImage resize(BufferedImage src, Mode resizeMode, int targetSize, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, Method.AUTOMATIC, resizeMode, targetSize, targetSize, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to a width and + * height no bigger than targetSize (or fitting the image to + * the given WIDTH or HEIGHT explicitly, depending on the {@link Mode} + * specified) using the given scaling method and apply the given + * {@link BufferedImageOp}s (if any) to the result before returning it. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param scalingMethod + * The method used for scaling the image; preferring speed to + * quality or a balance of both. + * @param resizeMode + * Used to indicate how imgscalr should calculate the final + * target size for the image, either fitting the image to the + * given width ({@link Mode#FIT_TO_WIDTH}) or fitting the image + * to the given height ({@link Mode#FIT_TO_HEIGHT}). If + * {@link Mode#AUTOMATIC} is passed in, imgscalr will calculate + * proportional dimensions for the scaled image based on its + * orientation (landscape, square or portrait). Unless you have + * very specific size requirements, most of the time you just + * want to use {@link Mode#AUTOMATIC} to "do the right thing". + * @param targetSize + * The target width and height (square) that you wish the image + * to fit within. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if scalingMethod is null. + * @throws IllegalArgumentException + * if resizeMode is null. + * @throws IllegalArgumentException + * if targetSize is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Method + * @see Mode + */ + public static BufferedImage resize(BufferedImage src, Method scalingMethod, Mode resizeMode, int targetSize, + BufferedImageOp... ops) throws IllegalArgumentException, ImagingOpException { + return resize(src, scalingMethod, resizeMode, targetSize, targetSize, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to the target + * width and height and apply the given {@link BufferedImageOp}s (if any) to + * the result before returning it. + *

+ * A scaling method of {@link Method#AUTOMATIC} and mode of + * {@link Mode#AUTOMATIC} are used. + *

+ * TIP: See the class description to understand how this + * class handles recalculation of the targetWidth or + * targetHeight depending on the image's orientation in order + * to maintain the original proportion. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param targetWidth + * The target width that you wish the image to have. + * @param targetHeight + * The target height that you wish the image to have. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if targetWidth is < 0 or if + * targetHeight is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage resize(BufferedImage src, int targetWidth, int targetHeight, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, Method.AUTOMATIC, Mode.AUTOMATIC, targetWidth, targetHeight, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to the target + * width and height using the given scaling method and apply the given + * {@link BufferedImageOp}s (if any) to the result before returning it. + *

+ * A mode of {@link Mode#AUTOMATIC} is used. + *

+ * TIP: See the class description to understand how this + * class handles recalculation of the targetWidth or + * targetHeight depending on the image's orientation in order + * to maintain the original proportion. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param scalingMethod + * The method used for scaling the image; preferring speed to + * quality or a balance of both. + * @param targetWidth + * The target width that you wish the image to have. + * @param targetHeight + * The target height that you wish the image to have. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if scalingMethod is null. + * @throws IllegalArgumentException + * if targetWidth is < 0 or if + * targetHeight is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Method + */ + public static BufferedImage resize(BufferedImage src, Method scalingMethod, int targetWidth, int targetHeight, + BufferedImageOp... ops) { + return resize(src, scalingMethod, Mode.AUTOMATIC, targetWidth, targetHeight, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to the target + * width and height (or fitting the image to the given WIDTH or HEIGHT + * explicitly, depending on the {@link Mode} specified) and apply the given + * {@link BufferedImageOp}s (if any) to the result before returning it. + *

+ * A scaling method of {@link Method#AUTOMATIC} is used. + *

+ * TIP: See the class description to understand how this + * class handles recalculation of the targetWidth or + * targetHeight depending on the image's orientation in order + * to maintain the original proportion. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param resizeMode + * Used to indicate how imgscalr should calculate the final + * target size for the image, either fitting the image to the + * given width ({@link Mode#FIT_TO_WIDTH}) or fitting the image + * to the given height ({@link Mode#FIT_TO_HEIGHT}). If + * {@link Mode#AUTOMATIC} is passed in, imgscalr will calculate + * proportional dimensions for the scaled image based on its + * orientation (landscape, square or portrait). Unless you have + * very specific size requirements, most of the time you just + * want to use {@link Mode#AUTOMATIC} to "do the right thing". + * @param targetWidth + * The target width that you wish the image to have. + * @param targetHeight + * The target height that you wish the image to have. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if resizeMode is null. + * @throws IllegalArgumentException + * if targetWidth is < 0 or if + * targetHeight is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Mode + */ + public static BufferedImage resize(BufferedImage src, Mode resizeMode, int targetWidth, int targetHeight, + BufferedImageOp... ops) throws IllegalArgumentException, ImagingOpException { + return resize(src, Method.AUTOMATIC, resizeMode, targetWidth, targetHeight, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to the target + * width and height (or fitting the image to the given WIDTH or HEIGHT + * explicitly, depending on the {@link Mode} specified) using the given + * scaling method and apply the given {@link BufferedImageOp}s (if any) to + * the result before returning it. + *

+ * TIP: See the class description to understand how this + * class handles recalculation of the targetWidth or + * targetHeight depending on the image's orientation in order + * to maintain the original proportion. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param scalingMethod + * The method used for scaling the image; preferring speed to + * quality or a balance of both. + * @param resizeMode + * Used to indicate how imgscalr should calculate the final + * target size for the image, either fitting the image to the + * given width ({@link Mode#FIT_TO_WIDTH}) or fitting the image + * to the given height ({@link Mode#FIT_TO_HEIGHT}). If + * {@link Mode#AUTOMATIC} is passed in, imgscalr will calculate + * proportional dimensions for the scaled image based on its + * orientation (landscape, square or portrait). Unless you have + * very specific size requirements, most of the time you just + * want to use {@link Mode#AUTOMATIC} to "do the right thing". + * @param targetWidth + * The target width that you wish the image to have. + * @param targetHeight + * The target height that you wish the image to have. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if scalingMethod is null. + * @throws IllegalArgumentException + * if resizeMode is null. + * @throws IllegalArgumentException + * if targetWidth is < 0 or if + * targetHeight is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Method + * @see Mode + */ + public static BufferedImage resize(BufferedImage src, Method scalingMethod, Mode resizeMode, int targetWidth, + int targetHeight, BufferedImageOp... ops) throws IllegalArgumentException, ImagingOpException { + long t = -1; + if (DEBUG) + t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (targetWidth < 0) + throw new IllegalArgumentException("targetWidth must be >= 0"); + if (targetHeight < 0) + throw new IllegalArgumentException("targetHeight must be >= 0"); + if (scalingMethod == null) + throw new IllegalArgumentException( + "scalingMethod cannot be null. A good default value is Method.AUTOMATIC."); + if (resizeMode == null) + throw new IllegalArgumentException("resizeMode cannot be null. A good default value is Mode.AUTOMATIC."); + + BufferedImage result = null; + + int currentWidth = src.getWidth(); + int currentHeight = src.getHeight(); + + // <= 1 is a square or landscape-oriented image, > 1 is a portrait. + float ratio = ((float) currentHeight / (float) currentWidth); + + if (DEBUG) + log(0, "Resizing Image [size=%dx%d, resizeMode=%s, orientation=%s, ratio(H/W)=%f] to [targetSize=%dx%d]", + currentWidth, currentHeight, resizeMode, (ratio <= 1 ? "Landscape/Square" : "Portrait"), ratio, + targetWidth, targetHeight); + + /* + * First determine if ANY size calculation needs to be done, in the case + * of FIT_EXACT, ignore image proportions and orientation and just use + * what the user sent in, otherwise the proportion of the picture must + * be honored. + * + * The way that is done is to figure out if the image is in a + * LANDSCAPE/SQUARE or PORTRAIT orientation and depending on its + * orientation, use the primary dimension (width for LANDSCAPE/SQUARE + * and height for PORTRAIT) to recalculate the alternative (height and + * width respectively) value that adheres to the existing ratio. + * + * This helps make life easier for the caller as they don't need to + * pre-compute proportional dimensions before calling the API, they can + * just specify the dimensions they would like the image to roughly fit + * within and it will do the right thing without mangling the result. + */ + if (resizeMode == Mode.FIT_EXACT) { + if (DEBUG) + log(1, "Resize Mode FIT_EXACT used, no width/height checking or re-calculation will be done."); + } else if (resizeMode == Mode.BEST_FIT_BOTH) { + float requestedHeightScaling = ((float) targetHeight / (float) currentHeight); + float requestedWidthScaling = ((float) targetWidth / (float) currentWidth); + float actualScaling = Math.min(requestedHeightScaling, requestedWidthScaling); + + targetHeight = Math.round((float) currentHeight * actualScaling); + targetWidth = Math.round((float) currentWidth * actualScaling); + + if (targetHeight == currentHeight && targetWidth == currentWidth) + return src; + + if (DEBUG) + log(1, "Auto-Corrected width and height based on scalingRatio %d.", actualScaling); + } else { + if ((ratio <= 1 && resizeMode == Mode.AUTOMATIC) || (resizeMode == Mode.FIT_TO_WIDTH)) { + // First make sure we need to do any work in the first place + if (targetWidth == src.getWidth()) + return src; + + // Save for detailed logging (this is cheap). + int originalTargetHeight = targetHeight; + + /* + * Landscape or Square Orientation: Ignore the given height and + * re-calculate a proportionally correct value based on the + * targetWidth. + */ + targetHeight = (int) Math.ceil((float) targetWidth * ratio); + + if (DEBUG && originalTargetHeight != targetHeight) + log(1, "Auto-Corrected targetHeight [from=%d to=%d] to honor image proportions.", + originalTargetHeight, targetHeight); + } else { + // First make sure we need to do any work in the first place + if (targetHeight == src.getHeight()) + return src; + + // Save for detailed logging (this is cheap). + int originalTargetWidth = targetWidth; + + /* + * Portrait Orientation: Ignore the given width and re-calculate + * a proportionally correct value based on the targetHeight. + */ + targetWidth = Math.round((float) targetHeight / ratio); + + if (DEBUG && originalTargetWidth != targetWidth) + log(1, "Auto-Corrected targetWidth [from=%d to=%d] to honor image proportions.", + originalTargetWidth, targetWidth); + } + } + + // If AUTOMATIC was specified, determine the real scaling method. + if (scalingMethod == Scalr.Method.AUTOMATIC) + scalingMethod = determineScalingMethod(targetWidth, targetHeight, ratio); + + if (DEBUG) + log(1, "Using Scaling Method: %s", scalingMethod); + + // Now we scale the image + if (scalingMethod == Scalr.Method.SPEED) { + result = scaleImage(src, targetWidth, targetHeight, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + } else if (scalingMethod == Scalr.Method.BALANCED) { + result = scaleImage(src, targetWidth, targetHeight, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } else if (scalingMethod == Scalr.Method.QUALITY || scalingMethod == Scalr.Method.ULTRA_QUALITY) { + /* + * If we are scaling up (in either width or height - since we know + * the image will stay proportional we just check if either are + * being scaled up), directly using a single BICUBIC will give us + * better results then using Chris Campbell's incremental scaling + * operation (and take a lot less time). + * + * If we are scaling down, we must use the incremental scaling + * algorithm for the best result. + */ + if (targetWidth > currentWidth || targetHeight > currentHeight) { + if (DEBUG) + log(1, "QUALITY scale-up, a single BICUBIC scale operation will be used..."); + + /* + * BILINEAR and BICUBIC look similar the smaller the scale jump + * upwards is, if the scale is larger BICUBIC looks sharper and + * less fuzzy. But most importantly we have to use BICUBIC to + * match the contract of the QUALITY rendering scalingMethod. + * This note is just here for anyone reading the code and + * wondering how they can speed their own calls up. + */ + result = scaleImage(src, targetWidth, targetHeight, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + } else { + if (DEBUG) + log(1, "QUALITY scale-down, incremental scaling will be used..."); + + /* + * Originally we wanted to use BILINEAR interpolation here + * because it takes 1/3rd the time that the BICUBIC + * interpolation does, however, when scaling large images down + * to most sizes bigger than a thumbnail we witnessed noticeable + * "softening" in the resultant image with BILINEAR that would + * be unexpectedly annoying to a user expecting a "QUALITY" + * scale of their original image. Instead BICUBIC was chosen to + * honor the contract of a QUALITY scale of the original image. + */ + result = scaleImageIncrementally(src, targetWidth, targetHeight, scalingMethod, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + } + } + + if (DEBUG) + log(0, "Resized Image in %d ms", System.currentTimeMillis() - t); + + // Apply any optional operations (if specified). + if (ops != null && ops.length > 0) + result = apply(result, ops); + + return result; + } + + /** + * Used to apply a {@link Rotation} and then 0 or more + * {@link BufferedImageOp}s to a given image and return the result. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will have the rotation applied to it. + * @param rotation + * The rotation that will be applied to the image. + * @param ops + * Zero or more optional image operations (e.g. sharpen, blur, + * etc.) that can be applied to the final result before returning + * the image. + * + * @return a new {@link BufferedImage} representing src rotated + * by the given amount and any optional ops applied to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if rotation is null. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Rotation + */ + public static BufferedImage rotate(BufferedImage src, Rotation rotation, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + long t = -1; + if (DEBUG) + t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (rotation == null) + throw new IllegalArgumentException("rotation cannot be null"); + + if (DEBUG) + log(0, "Rotating Image [%s]...", rotation); + + /* + * Setup the default width/height values from our image. + * + * In the case of a 90 or 270 (-90) degree rotation, these two values + * flip-flop and we will correct those cases down below in the switch + * statement. + */ + int newWidth = src.getWidth(); + int newHeight = src.getHeight(); + + /* + * We create a transform per operation request as (oddly enough) it ends + * up being faster for the VM to create, use and destroy these instances + * than it is to re-use a single AffineTransform per-thread via the + * AffineTransform.setTo(...) methods which was my first choice (less + * object creation); after benchmarking this explicit case and looking + * at just how much code gets run inside of setTo() I opted for a new AT + * for every rotation. + * + * Besides the performance win, trying to safely reuse AffineTransforms + * via setTo(...) would have required ThreadLocal instances to avoid + * race conditions where two or more resize threads are manipulating the + * same transform before applying it. + * + * Misusing ThreadLocals are one of the #1 reasons for memory leaks in + * server applications and since we have no nice way to hook into the + * init/destroy Servlet cycle or any other initialization cycle for this + * library to automatically call ThreadLocal.remove() to avoid the + * memory leak, it would have made using this library *safely* on the + * server side much harder. + * + * So we opt for creating individual transforms per rotation op and let + * the VM clean them up in a GC. I only clarify all this reasoning here + * for anyone else reading this code and being tempted to reuse the AT + * instances of performance gains; there aren't any AND you get a lot of + * pain along with it. + */ + AffineTransform tx = new AffineTransform(); + + switch (rotation) { + case CW_90: + /* + * A 90 or -90 degree rotation will cause the height and width to + * flip-flop from the original image to the rotated one. + */ + newWidth = src.getHeight(); + newHeight = src.getWidth(); + + // Reminder: newWidth == result.getHeight() at this point + tx.translate(newWidth, 0); + tx.quadrantRotate(1); + + break; + + case CW_270: + /* + * A 90 or -90 degree rotation will cause the height and width to + * flip-flop from the original image to the rotated one. + */ + newWidth = src.getHeight(); + newHeight = src.getWidth(); + + // Reminder: newHeight == result.getWidth() at this point + tx.translate(0, newHeight); + tx.quadrantRotate(3); + break; + + case CW_180: + tx.translate(newWidth, newHeight); + tx.quadrantRotate(2); + break; + + case FLIP_HORZ: + tx.translate(newWidth, 0); + tx.scale(-1.0, 1.0); + break; + + case FLIP_VERT: + tx.translate(0, newHeight); + tx.scale(1.0, -1.0); + break; + } + + // Create our target image we will render the rotated result to. + BufferedImage result = createOptimalImage(src, newWidth, newHeight); + Graphics2D g2d = (Graphics2D) result.createGraphics(); + + /* + * Render the resultant image to our new rotatedImage buffer, applying + * the AffineTransform that we calculated above during rendering so the + * pixels from the old position are transposed to the new positions in + * the resulting image correctly. + */ + g2d.drawImage(src, tx, null); + g2d.dispose(); + + if (DEBUG) + log(0, "Rotation Applied in %d ms, result [width=%d, height=%d]", System.currentTimeMillis() - t, + result.getWidth(), result.getHeight()); + + // Apply any optional operations (if specified). + if (ops != null && ops.length > 0) + result = apply(result, ops); + + return result; + } + + /** + * Used to write out a useful and well-formatted log message by any piece of + * code inside of the imgscalr library. + *

+ * If a message cannot be logged (logging is disabled) then this method + * returns immediately. + *

+ * NOTE: Because Java will auto-box primitive arguments + * into Objects when building out the params array, care should + * be taken not to call this method with primitive values unless + * {@link Scalr#DEBUG} is true; otherwise the VM will be + * spending time performing unnecessary auto-boxing calculations. + * + * @param depth + * The indentation level of the log message. + * @param message + * The log message in format string syntax that will be logged. + * @param params + * The parameters that will be swapped into all the place holders + * in the original messages before being logged. + * + * @see Scalr#LOG_PREFIX + * @see Scalr#LOG_PREFIX_PROPERTY_NAME + */ + protected static void log(int depth, String message, Object... params) { + if (Scalr.DEBUG) { + System.out.print(Scalr.LOG_PREFIX); + + for (int i = 0; i < depth; i++) + System.out.print("\t"); + + System.out.printf(message, params); + System.out.println(); + } + } + + /** + * Used to create a {@link BufferedImage} with the most optimal RGB TYPE ( + * {@link BufferedImage#TYPE_INT_RGB} or {@link BufferedImage#TYPE_INT_ARGB} + * ) capable of being rendered into from the given src. The + * width and height of both images will be identical. + *

+ * This does not perform a copy of the image data from src into + * the result image; see {@link #copyToOptimalImage(BufferedImage)} for + * that. + *

+ * We force all rendering results into one of these two types, avoiding the + * case where a source image is of an unsupported (or poorly supported) + * format by Java2D causing the rendering result to end up looking terrible + * (common with GIFs) or be totally corrupt (e.g. solid black image). + *

+ * Originally reported by Magnus Kvalheim from Movellas when scaling certain + * GIF and PNG images. + * + * @param src + * The source image that will be analyzed to determine the most + * optimal image type it can be rendered into. + * + * @return a new {@link BufferedImage} representing the most optimal target + * image type that src can be rendered into. + * + * @see How + * Java2D handles poorly supported image types + * @see Thanks + * to Morten Nobel for implementation hint + */ + protected static BufferedImage createOptimalImage(BufferedImage src) { + return createOptimalImage(src, src.getWidth(), src.getHeight()); + } + + /** + * Used to create a {@link BufferedImage} with the given dimensions and the + * most optimal RGB TYPE ( {@link BufferedImage#TYPE_INT_RGB} or + * {@link BufferedImage#TYPE_INT_ARGB} ) capable of being rendered into from + * the given src. + *

+ * This does not perform a copy of the image data from src into + * the result image; see {@link #copyToOptimalImage(BufferedImage)} for + * that. + *

+ * We force all rendering results into one of these two types, avoiding the + * case where a source image is of an unsupported (or poorly supported) + * format by Java2D causing the rendering result to end up looking terrible + * (common with GIFs) or be totally corrupt (e.g. solid black image). + *

+ * Originally reported by Magnus Kvalheim from Movellas when scaling certain + * GIF and PNG images. + * + * @param src + * The source image that will be analyzed to determine the most + * optimal image type it can be rendered into. + * @param width + * The width of the newly created resulting image. + * @param height + * The height of the newly created resulting image. + * + * @return a new {@link BufferedImage} representing the most optimal target + * image type that src can be rendered into. + * + * @throws IllegalArgumentException + * if width or height are < 0. + * + * @see How + * Java2D handles poorly supported image types + * @see Thanks + * to Morten Nobel for implementation hint + */ + protected static BufferedImage createOptimalImage(BufferedImage src, int width, int height) + throws IllegalArgumentException { + if (width <= 0 || height <= 0) + throw new IllegalArgumentException("width [" + width + "] and height [" + height + "] must be > 0"); + + return new BufferedImage(width, height, (src.getTransparency() == Transparency.OPAQUE + ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB)); + } + + /** + * Used to copy a {@link BufferedImage} from a non-optimal type into a new + * {@link BufferedImage} instance of an optimal type (RGB or ARGB). If + * src is already of an optimal type, then it is returned + * unmodified. + *

+ * This method is meant to be used by any calling code (imgscalr's or + * otherwise) to convert any inbound image from a poorly supported image + * type into the 2 most well-supported image types in Java2D ( + * {@link BufferedImage#TYPE_INT_RGB} or {@link BufferedImage#TYPE_INT_ARGB} + * ) in order to ensure all subsequent graphics operations are performed as + * efficiently and correctly as possible. + *

+ * When using Java2D to work with image types that are not well supported, + * the results can be anything from exceptions bubbling up from the depths + * of Java2D to images being completely corrupted and just returned as solid + * black. + * + * @param src + * The image to copy (if necessary) into an optimally typed + * {@link BufferedImage}. + * + * @return a representation of the src image in an optimally + * typed {@link BufferedImage}, otherwise src if it was + * already of an optimal type. + * + * @throws IllegalArgumentException + * if src is null. + */ + protected static BufferedImage copyToOptimalImage(BufferedImage src) throws IllegalArgumentException { + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + + // Calculate the type depending on the presence of alpha. + int type = (src.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB + : BufferedImage.TYPE_INT_ARGB); + BufferedImage result = new BufferedImage(src.getWidth(), src.getHeight(), type); + + // Render the src image into our new optimal source. + Graphics g = result.getGraphics(); + g.drawImage(src, 0, 0, null); + g.dispose(); + + return result; + } + + /** + * Used to determine the scaling {@link Method} that is best suited for + * scaling the image to the targeted dimensions. + *

+ * This method is intended to be used to select a specific scaling + * {@link Method} when a {@link Method#AUTOMATIC} method is specified. This + * method utilizes the {@link Scalr#THRESHOLD_QUALITY_BALANCED} and + * {@link Scalr#THRESHOLD_BALANCED_SPEED} thresholds when selecting which + * method should be used by comparing the primary dimension (width or + * height) against the threshold and seeing where the image falls. The + * primary dimension is determined by looking at the orientation of the + * image: landscape or square images use their width and portrait-oriented + * images use their height. + * + * @param targetWidth + * The target width for the scaled image. + * @param targetHeight + * The target height for the scaled image. + * @param ratio + * A height/width ratio used to determine the orientation of the + * image so the primary dimension (width or height) can be + * selected to test if it is greater than or less than a + * particular threshold. + * + * @return the fastest {@link Method} suited for scaling the image to the + * specified dimensions while maintaining a good-looking result. + */ + protected static Method determineScalingMethod(int targetWidth, int targetHeight, float ratio) { + // Get the primary dimension based on the orientation of the image + int length = (ratio <= 1 ? targetWidth : targetHeight); + + // Default to speed + Method result = Method.SPEED; + + // Figure out which scalingMethod should be used + if (length <= Scalr.THRESHOLD_QUALITY_BALANCED) + result = Method.QUALITY; + else if (length <= Scalr.THRESHOLD_BALANCED_SPEED) + result = Method.BALANCED; + + if (DEBUG) + log(2, "AUTOMATIC scaling method selected: %s", result.name()); + + return result; + } + + /** + * Used to implement a straight-forward image-scaling operation using Java + * 2D. + *

+ * This method uses the Oracle-encouraged method of + * Graphics2D.drawImage(...) to scale the given image with the + * given interpolation hint. + * + * @param src + * The image that will be scaled. + * @param targetWidth + * The target width for the scaled image. + * @param targetHeight + * The target height for the scaled image. + * @param interpolationHintValue + * The {@link RenderingHints} interpolation value used to + * indicate the method that {@link Graphics2D} should use when + * scaling the image. + * + * @return the result of scaling the original src to the given + * dimensions using the given interpolation method. + */ + protected static BufferedImage scaleImage(BufferedImage src, int targetWidth, int targetHeight, + Object interpolationHintValue) { + // Setup the rendering resources to match the source image's + BufferedImage result = createOptimalImage(src, targetWidth, targetHeight); + Graphics2D resultGraphics = result.createGraphics(); + + // Scale the image to the new buffer using the specified rendering hint. + resultGraphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolationHintValue); + resultGraphics.drawImage(src, 0, 0, targetWidth, targetHeight, null); + + // Just to be clean, explicitly dispose our temporary graphics object + resultGraphics.dispose(); + + // Return the scaled image to the caller. + return result; + } + + /** + * Used to implement Chris Campbell's incremental-scaling algorithm: http://today.java.net/pub/a/today/2007/04/03/perils + * -of-image-getscaledinstance.html. + *

+ * Modifications to the original algorithm are variable names and comments + * added for clarity and the hard-coding of using BICUBIC interpolation as + * well as the explicit "flush()" operation on the interim BufferedImage + * instances to avoid resource leaking. + * + * @param src + * The image that will be scaled. + * @param targetWidth + * The target width for the scaled image. + * @param targetHeight + * The target height for the scaled image. + * @param scalingMethod + * The scaling method specified by the user (or calculated by + * imgscalr) to use for this incremental scaling operation. + * @param interpolationHintValue + * The {@link RenderingHints} interpolation value used to + * indicate the method that {@link Graphics2D} should use when + * scaling the image. + * + * @return an image scaled to the given dimensions using the given rendering + * hint. + */ + protected static BufferedImage scaleImageIncrementally(BufferedImage src, int targetWidth, int targetHeight, + Method scalingMethod, Object interpolationHintValue) { + boolean hasReassignedSrc = false; + int incrementCount = 0; + int currentWidth = src.getWidth(); + int currentHeight = src.getHeight(); + + /* + * The original QUALITY mode, representing Chris Campbell's algorithm, + * is to step down by 1/2s every time when scaling the image + * incrementally. Users pointed out that using this method to scale + * images with noticeable straight lines left them really jagged in + * smaller thumbnail format. + * + * After investigation it was discovered that scaling incrementally by + * smaller increments was the ONLY way to make the thumbnail sized + * images look less jagged and more accurate; almost matching the + * accuracy of Mac's built in thumbnail generation which is the highest + * quality resize I've come across (better than GIMP Lanczos3 and + * Windows 7). + * + * A divisor of 7 was chose as using 5 still left some jaggedness in the + * image while a divisor of 8 or higher made the resulting thumbnail too + * soft; like our OP_ANTIALIAS convolve op had been forcibly applied to + * the result even if the user didn't want it that soft. + * + * Using a divisor of 7 for the ULTRA_QUALITY seemed to be the sweet + * spot. + * + * NOTE: Below when the actual fraction is used to calculate the small + * portion to subtract from the current dimension, this is a + * progressively smaller and smaller chunk. When the code was changed to + * do a linear reduction of the image of equal steps for each + * incremental resize (e.g. say 50px each time) the result was + * significantly worse than the progressive approach used below; even + * when a very high number of incremental steps (13) was tested. + */ + int fraction = (scalingMethod == Method.ULTRA_QUALITY ? 7 : 2); + + do { + int prevCurrentWidth = currentWidth; + int prevCurrentHeight = currentHeight; + + /* + * If the current width is bigger than our target, cut it in half + * and sample again. + */ + if (currentWidth > targetWidth) { + currentWidth -= (currentWidth / fraction); + + /* + * If we cut the width too far it means we are on our last + * iteration. Just set it to the target width and finish up. + */ + if (currentWidth < targetWidth) + currentWidth = targetWidth; + } + + /* + * If the current height is bigger than our target, cut it in half + * and sample again. + */ + + if (currentHeight > targetHeight) { + currentHeight -= (currentHeight / fraction); + + /* + * If we cut the height too far it means we are on our last + * iteration. Just set it to the target height and finish up. + */ + + if (currentHeight < targetHeight) + currentHeight = targetHeight; + } + + /* + * Stop when we cannot incrementally step down anymore. + * + * This used to use a || condition, but that would cause problems + * when using FIT_EXACT such that sometimes the width OR height + * would not change between iterations, but the other dimension + * would (e.g. resizing 500x500 to 500x250). + * + * Now changing this to an && condition requires that both + * dimensions do not change between a resize iteration before we + * consider ourselves done. + */ + if (prevCurrentWidth == currentWidth && prevCurrentHeight == currentHeight) + break; + + if (DEBUG) + log(2, "Scaling from [%d x %d] to [%d x %d]", prevCurrentWidth, prevCurrentHeight, currentWidth, + currentHeight); + + // Render the incremental scaled image. + BufferedImage incrementalImage = scaleImage(src, currentWidth, currentHeight, interpolationHintValue); + + /* + * Before re-assigning our interim (partially scaled) + * incrementalImage to be the new src image before we iterate around + * again to process it down further, we want to flush() the previous + * src image IF (and only IF) it was one of our own temporary + * BufferedImages created during this incremental down-sampling + * cycle. If it wasn't one of ours, then it was the original + * caller-supplied BufferedImage in which case we don't want to + * flush() it and just leave it alone. + */ + if (hasReassignedSrc) + src.flush(); + + /* + * Now treat our incremental partially scaled image as the src image + * and cycle through our loop again to do another incremental + * scaling of it (if necessary). + */ + src = incrementalImage; + + /* + * Keep track of us re-assigning the original caller-supplied source + * image with one of our interim BufferedImages so we know when to + * explicitly flush the interim "src" on the next cycle through. + */ + hasReassignedSrc = true; + + // Track how many times we go through this cycle to scale the image. + incrementCount++; + } while (currentWidth != targetWidth || currentHeight != targetHeight); + + if (DEBUG) + log(2, "Incrementally Scaled Image in %d steps.", incrementCount); + + /* + * Once the loop has exited, the src image argument is now our scaled + * result image that we want to return. + */ + return src; + } +} \ No newline at end of file