Home

A journey with resizing and cropping images in Java

Numerous web applications we have developed over the years have had to handle the uploading and subsequent processing of images - user profile images are a perfect example.

In the old, old days when we developed in Perl this was quite easy - the PerlMagick bindings to ImageMagick made the whole process fast, simple and largely enjoyable.

Since moving to Java it became more complicated and more to the point, slower: image handling in pure Java was not blindingly fast if you wanted to handle reasonable sized images at an acceptable speed to the end user.

After experimenting with everything from basic AWT routines (slow) to invoking external scripts that wrapped ImageMagick (fast, but somewhat brittle and not easy to run on local development machines), we started looking at the Java Advanced Imaging API.

I'm not sure the best way to describe this API, as it is listed on the Sun (sorry ... Oracle) website as part of the Java Media APIs, but is an optional component and is being developed as a "community source projects on java.net".  My preference was certainly to try and use something 'official' - it seemed crazy to require yet another third-party library just to resize the occasional image, so this seemed to fit the bill.

The attractive part was that it provided native code support so hopefully the performance issues were a thing of the past.

Unfortunately programming against this API was even less intuitive than using AWT routines: this really is an API designed for image processing and doing the simple stuff ends up being complicated.

A quick search pulled up all sorts of examples of resizing images.  This blog in particular seemed to be a good start: Digital Sanctuary, and I got an example running easily.

Small is good, but I want BIG

However, a pretty serious limitation came up almost straight away: you are only able to apply scaling factors between 0 and 1 (well, I presume you can't actually use zero as a scaling factor unless you want to create a black hole or something).

What if you want to INCREASE the size of the image?

Following Devon's advice about changing the values of the ParameterBlock (which unfortunately wasn't followed up with a working example from anyone), I started looking around on what this actually means.  His example for scaling down is simple enough:

ParameterBlock paramBlock = new ParameterBlock();

paramBlock.addSource(originalImage); // The source image

paramBlock.add(scale); // The xScale

paramBlock.add(scale); // The yScale

paramBlock.add(0.0); // The x translation

paramBlock.add(0.0); // The y translation

What I soon discovered is that ParameterBlock is the 'secret handshake' of JAI developers, and they are doing their best to keep it out of the hands of mere mortals such as myself.  I don't think I have ever come across something so opaque and utterly incomprehensible.

Try doing a Google search for "jai increase image scale" and see if anything comes up (besides this post obviously).

Anyway, after wasting a few hours I had to draw the line somewhere and move onto a different method.

Back to AWT and Java 2D

Reluctantly removing JAI from the equation, I broadened my search and soon came up with what is presented here in its final form:

/**

* Perform actual resize based on provided parameters.

* @param newWidth The width of the new image

* @param newHeight The height of the new image

* @param scaleX The amount to scale source image by in the X axis

* @param scaleY The amount to scale source image by in the Y axis

* @param source The source image to resize

* @return New BufferedImage being a scaled version of source

*/

private BufferedImage doResize(int newWidth, int newHeight, double scaleX, double scaleY,

BufferedImage source) {

BufferedImage result = new BufferedImage(newWidth, newHeight, source.getType());

Graphics2D g2d = null;

try {

g2d = result.createGraphics();

g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);

g2d.scale(scaleX, scaleY);

g2d.drawImage(source, 0, 0, null);

} finally {

if (g2d != null) { g2d.dispose(); }

}

return result;

}

You'll notice that both the desired new size, and a scaling factor are passed in.  These are worked out earlier in the process and we need both depending on what we are trying to do.

This new routine uses the Java 2D API, which is an improvement over straight AWT when it comes to actually doing the rescale.  From the release notes (my emphasis):

As of J2SE 5.0, all images created with a BufferedImage constructor are now managed images and can be cached in video memory or, in the case of a remote X server, on the X server side. Previously, the Sun implementation managed only compatible images — those created with the Component createImage(int, int) method or with the GraphicsConfiguration createCompatibleImage methods. Managed images generally perform better than unmanaged images.

Most importantly, this could handle scale values greater than 1.  Using the VALUE_INTERPOLATION_BICUBIC constant seemed to give good results in terms of scaling quality, although the final compression used when writing out the image is just as critical to the process.

Now to keep the designers happy

With the ability to do good quality and rapid scaling up or down under our belts, it was time to address the next issue that keeps the designers awake: how do we take the odd shaped images that users thoughtlessly upload into the nicely designed placeholders that the designers have wasted sacrificed hours tweaking and laying out just so?

The answer is applying a crop to each image after applying the appropriate resizing.

This was shockingly easy:

BufferedImage newImage = sourceImage.getSubimage(xOffset, yOffset, sectionWidth, sectionHeight);

Thus we can create a new image of sectionWidth by sectionHeight from the original image, starting at offsetX, offsetY.  Okay, so technically this isn't a crop as the original is unaffected, but you get the idea.

Putting it all together

The trick is working out what the scaling factors are based on the size of the original versus the desired size, and working out what to crop, which changes depending on whether you are uploading a tall image into a wide placeholder etc.

To achieve this, I built up a separate 'ImageResizeRequest' class that could be used to hold all the parameters to describe an image resize operation.  This was far neater that having to pass an unwieldy number of parameters to a method, and allows for the provision of sensible defaults.

Parameters includes:

  • Desired width and height
  • Whether to resize if larger, smaller, always or never (in which case the image is just cropped to the desired size, unless you request not to crop, in which case you should get back the image you originally provided - I've never tried it!)
  • The actual source image, either as an existing BufferedImage (so you could chain operations together), or a File object, or an absolute file path.
  • The destination file path
  • The compression factor to use in the resulting image
  • Whether to actually crop - you can elect to just resize to fit completely within requested dimensions which inevitably means it will be a different size in one dimension (I hear designers reaching for their aspirin at this point)
  • You can even elect to resize and NOT maintain aspect ratio.  'maintainAspectRatio' is naturally true by default (hence previous point), but I can keep this up my sleeve to annoy the designers if needed

The actual work is done by a single 'resize' method defined in a separate ImageResizeService class.

We used Spring extensively and the use of services really appeals, so this was designed to be injected as a 'Singleton' Spring bean from day one and is annotated as such.

The logic to determine when and how to resize and crop was somewhat painful, especially as I didn't want to do anything obviously silly like call a resize operation when not needed, or crop to the dimensions of the original image.

This logic was based on the Perl code used in our content management system, but then tidied up and fixed up over a frustrating hour or two.

Finally, a bunch of tests were put together to exercise various resize operations and check that things were scaled and cropped when they should.

Source and Example Usage

The source can be downloaded here. Note: you may want to remove the reference to Spring in ImageResizeService if you are not using that framework (you should be!).  Just remove the annotation and the import.

There are three source files (including an enumeration for the desired resize conditions) and a unit test.  I've also included two images which can be used as an input to the test.  There shouldn't be any dependencies except a recent JDK and JUnit for the test.

The provided unit test has plenty of examples, but I'm sure you want to see how easy it is to use before wasting time downloading and opening a zip file:

ImageResizeService handler = new ImageResizeService();

ImageResizeRequest request = new ImageResizeRequest();

request.setSourceFilePath("d:\\input_image.jpg");

request.setTargetHeight(300);

request.setTargetWidth(300);

request.setMaintainAspect(true); // This is the default

request.setCropToAspect(true); // This is the default

request.setResizeAction(ImageResizeAction.IF_LARGER); // This is the default

request.setDestinationFilePath("d:\\output_image.jpg");

handler.resize(request);

System.out.println("Image Cropped: " + request.isCropped());

System.out.println("Image Resized: " + request.isResized());

Pretty simple huh?

Note, the resize() method does throw a checked Exception, either from one of the underlying Java operations (for example if the ImageIO.read() method can't read the image File it is provided, most likely because it is simply a format it cannot understand), or an application specific one such as not providing a targetWidth or targetHeight.

I made it checked so no-one got lazy and allowed an exception to reach the user in a web application environment.

Hints

The ImageResizeRequest is reusable across multiple invocations if you want to create multiple sizes of thumbnails (I think a designer just peed their pants with excitement).  The two status values of isCropped and isResized are guaranteed to be set correctly with each invocation.

If in doubt, create a new request from scratch if desired.

If you do want to create multiple thumbnails from a single image, do yourself a favour and take advantage of the fact that the resulting image is always returned by the resize() method (as well as written to disc if provided an optional destinationFilePath value).  Start with the largest thumbnail and generate smaller thumbnails from that.  Saves lots of time when users upload directly from their digital cameras for their avatar image.

A little gotcha is that if you say to resize IF_LARGER, and also set cropToAspect = true, it will not resize images to fill the space if they are only larger in one dimension.  Thus it is guaranteed to not increase the size of the image, which to me would violate the idea of resizing if larger.

For example uploading an image 200 wide by 500 high to a 300 by 300 target will crop the height to 300 pixels, but leave the width at 200 pixels.  If you wish to fill the space entirely, use resize ALWAYS.

To-do

I'm pretty happy with it.  The main limitation is that it is hardcoded to generate JPG images only, but this would be simple enough to change via a new ImageResizeRequest parameter later.

Also, cropping will currently crop top and bottom or sides equally as needed.  You might want a parameter to control cropping starting from a corner, but I didn't bother as 99 times out of 100 it won't be as good as going from the middle.

Conclusion

Well, I hope this helps someone navigate the maze that is simple image manipulation in Java.  Feel free to use as you see fit but we'd appreciate the link to http://www.futuremedium.com.au/ be retained in any application.

If this gathers any interest I'll try and keep the source updated with any fixes or general niftyness.  Also tell me if the source disappears from our website - Wordpress wouldn't let me upload it here for 'security reasons'.

Good luck!

There is a follow-up article to this: http://blog.futuremedium.com.au/2011/06/17/resizing-and-cropping-images-in-java-revisited/

Back to blogs