A Brief Tutorial on Image Manipulation In Java

by Steven J. Owens (unless otherwise attributed)

Rotating, Scaling, etc

I started down this little oddyssey when somebody posted on a mailing list, somewhere, about how to "view, zoom, rotate and move images". I remember this was a pain in the ass to figure out when I was doing it. Particularly the part about doing more than one at a time, I tended to get little errors creeping in as I added operations. Turns out that order of operation is important.

Before I go further, I highly recommend using Other Peoples Brains for this sort of thing. Java2D is slick and all, but it's a set of building blocks, not an application. Odds are, whatever your application, there are people out there who're already trying to build an open source equivalent of it. A good example would be the SVG Batik project, (http://xml.apache.org/batik/) which is an open source SVG implementation in java.

Having said all that, let's go ahead and do it the hard way, using Java2D and JFrame and Bufferedimage and AffineTransform.

Displaying the Image

Okay, so first of all, we need a little application to actually load in and display an image.

This turns out to be surprisingly hard to figure out just from looking at the various APIs. Turns out the Java2D API doesn't have a whole lot to do with actually loading images, it's more preoccupied with generating them and manipulating them. So you can generate an image by drawing it with shapes and such, but we're not getting into that here.

To load in a pre-existing image, you have a few options. They're not easy to figure out from looking at the API docs, so I'll give you some hints. The favorite methods all seem to be quite connected to other things, like applets or icons:

This is all making my head spin. Here's a little sample application that loads and displays a JPEG image using Sun's JPEGDecoder.

import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
import com.sun.image.codec.jpeg.*;

public class ImageFun extends JComponent { public static String USAGE = "java ImageFun "; public static void main(String args[]){ if(args.length<1){ System.out.println(USAGE); System.exit(0); }

JFrame frame = new JFrame() ; frame.getContentPane().setBackground(Color.white); ImageFun imf = new ImageFun() ; imf.loadJpeg(args[0]) ; frame.getContentPane().add(imf); frame.pack() ; frame.addWindowListener(new WindowAdapter(){ public void windowClosing(WindowEvent event){ System.exit(0); } }); frame.setVisible(true); }

private BufferedImage bimage;

public void loadJpeg(String imageName) { try{ this.bimage = JPEGCodec.createJPEGDecoder(new FileInputStream(imageName)).decodeAsBufferedImage(); }catch(IOException e){ e.printStackTrace(); throw new Error("Could not load JPEG image \"" + imageName + "\""); }

setPreferredSize(new Dimension(this.bimage.getWidth(), this.bimage.getHeight())); }

public void paint(Graphics graphics){ graphics.drawImage(this.bimage, 0, 0, null); } }

AffineTransform

Affine Transforms are these nifty mathematical representations of various image manipulations, like scaling, rotating, shifting the location (called translating) and even warping the image (shearing). If you think about it, ultimately any image is just a big 2-dimensional array of pixels, and any manipulations you want to do can be thought of as a mathematical operation on that 2-dimensional array. (No, don't ask me to explain the math, if I could understand it, do you think I'd be talking to you about it?).

Luckily for us, the fine folks at Sun provided an implementation, in the java.awt.geom.AffineTransform class.

Unluckily, as is so often the case in GUI-oriented java stuff, it's not that simple. You see, we don't actually mess with the image, we mess with the display space, or rather the graphics context. The graphics context is sort of like a lense that we view the image through. So, rather than just apply some transforms to the image data, and then display it, we have to add the transforms to the graphics context, by overriding our image viewer's paint() method, casting the Graphics object parameter of paint() to a Graphics2D, adding an AffineTransform to the Graphics2D object, and then calling the drawImage()

Okay, so in our sample code above, we displayed the image in a JFrame, on a JComponent. To mess with the image, we have to extend the code a little, like so:
import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
import com.sun.image.codec.jpeg.*;
import java.awt.geom.AffineTransform ;

public class ImageFun2 extends JComponent { public static String USAGE = "java ImageFun2 "; public static void main(String args[]){ if(args.length<1){ System.out.println(USAGE); System.exit(0); }

JFrame frame = new JFrame() ; frame.getContentPane().setBackground(Color.white); ImageFun2 imf = new ImageFun2() ; imf.loadJpeg(args[0]) ; frame.getContentPane().add(imf); frame.pack() ; frame.addWindowListener(new WindowAdapter(){ public void windowClosing(WindowEvent event){ System.exit(0); } }); frame.setVisible(true); }

private BufferedImage bimage;

public void loadJpeg(String imageName) { try{ this.bimage = JPEGCodec.createJPEGDecoder(new FileInputStream(imageName)).decodeAsBufferedImage(); }catch(IOException e){ e.printStackTrace(); throw new Error("Could not load JPEG image \"" + imageName + "\""); } setPreferredSize(new Dimension(this.bimage.getWidth(), this.bimage.getHeight())); }

public double rotation = 50.0 ; public double scale = 200.0 ;

public AffineTransform mungeImage() { // Assumes you've already called loadJpeg AffineTransform at = new AffineTransform() ; at.rotate(this.rotation, this.bimage.getWidth()/2, this.bimage.getHeight()/2); at.scale(scale, scale); return at ; }

public void paint(Graphics graphics){ Graphics2D g2 = (Graphics2D)graphics; g2.setRenderingHint(RenderingHints.KEYANTIALIASING, RenderingHints.VALUEANTIALIASON); g2.setRenderingHint(RenderingHints.KEYINTERPOLATION, RenderingHints.VALUEINTERPOLATIONNEAREST_NEIGHBOR); g2.setTransform(mungeImage()) ; g2.drawImage(this.bimage, 0, 0, null) ; } }


> I use JPanel in JFrame to view, rotate and scale images using
> Graphics2D.

I presume you're doing this with AffineTransforms?


> How can I center the images in the window if the size of the
> image is smaller then window?

This is way too complicated for me to explain it in depth in the time I have, but basically you keep track of your image dimension, window dimension, image rotation, move offset, and and I guess in your case zoom level. Come rendering time, you carry out a sequence of AffineTransforms based on the numbers.

I suggest for now you assume symetric images, to simplify the math, and worry about asymetric images later - basically you handle his by calculating the numbers independently for width vs. height. Also, remember that for now we're assuming that this is all in the render step, you're just checking a set of numbers (scaling, etc) that you've been keeping track of.

The big gotcha to watch out for is order-of-operations. The one I found to work was: rotate, translate, scale. Do NOT assume you can switch the sequence and it'll still work the same.

First you rotate it around it's center coordinate, i.e.

myAffineTransform.rotate(degrees, originalHeight/2, originalWidth/2))

Do this first, because you just plain don't want to deal with the grief of rotating an off-centered image and then figuring out the right set of offsets to translate it back to center. By doing this first, you keep it simple.

Then you figure out how much you'll need to offset it. This is where you have to stop and think carefully about things like, how big the image is, how big you plan to scale it, how big the space you plan to draw it in is, etc. It's not that it takes any genius or twisty math, but it's easy to forget a step.

In this example, since we're assuming square images and square windows, it's easy, but even to say this clearly I have to stop and use a step by step list:

1) take either dimension of the original image (width or height)

2) multiply dimension by the scaling amount you're planning to use in the last step, to get the scaled image dimension.

3) subtract scaled image dimension from the corresponding dimension of the window to get the difference.

4) divide the difference in two to get the offset

5) translate the image up and over by the offset with affineTransform.translate()

Now, if you're going to use rectangular images or rectangular windows, this is complicated in two ways. First, you have to do that series of steps above independently for each window. Second, you have to figure out how the rotation affected the image height and width. If you're dealing with rotations that don't divide easily into 90 degree chunks, it's even more complicated, since you can't just flip the width/height around.

Now, to further complicate matters, if you're going to deal with images that may be larger than the window, you have to figure out how the negative values, etc, affect things. However, other than that it really doesn't change the steps - you still have to figure out how much to shove it horizontally and vertically to keep it centerd. You'll have to use clipping, described at the URL below, to keep java from drawing any of the image that's outside the window:

http://java.sun.com/docs/books/tutorial/2d/display/clipping.html

Anyway, so finally, you scale it with an affineTransform.scale(). Since you did all the annoying math before you scaled it, the scaled image should be centered nicely in your drawing area.


See original (unformatted) article

Feedback

Verification Image:
Subject:
Your Email Address:
Confirm Address:
Please Post:
Copyright: By checking the "Please Post" checkbox you agree to having your feedback posted on notablog if the administrator decides it is appropriate content, and grant compilation copyright rights to the administrator.
Message Content: