Josiah Hester is an active musician and JAVA developer from North Carolina who curently resides in Honolulu Hawaii.
He is currently finishing out his senior year in high school, and has aspirations of studying Computer Science and Music at Stanford University.
| By Josiah Hester, September 11, 2007 |
Images are the staple of any graphical application, whether on the web or on the desk, images are everywhere. Having the ability to control and manipulate these images is a crucial skill that is absolutely necessary for any graphical artist, designer, or Game Engineer. This article will get you, the aspiring artist, professional designer, or amateur hobbyist, the foundations to be able to manipulate any image to your will. The end result will be twofold, you will have a nice Image Utility class for use in any of your Projects (I found it was most useful for 2d gaming), and you will have a small, but powerful application that can perform most common, and many not so common operations on. jpg's, .png's, .gif's, and bmp image files. This article assumes only that you have basic Java programming ability; of course a little prior Image experience would help. Know that I wont bog you down with trigonometric details. So, enough talking lets get coding!
Images in Java are defined by the java.awt.image.Image interface, which provides a basic model for
controlling an image, but the real meat of the Java Image is the java.awt.image.BufferedImage.
A BufferedImage is an accessible buffer of image data, essentially pixels, and their RGB colors.
The BufferedImage provides a powerful way to manipulate the Image data. A BufferedImage
object is made up of two parts a ColorModel object and a Raster object.
![]() |
|
Figure 1. The
BufferedImage Class |
The ColorModel object provides methods that translate an image's
pixel data into color components that can be understood, such as RGB for a computer.
The Raster represents a rectangular array of pixels. A Raster encapsulates a DataBuffer that
stores the sample values and a SampleModel that describes how to locate a given sample value in a DataBuffer.
Basically, the Raster holds two things, a DataBuffer, which contains the raw image data
and a SampleModel, which describes how the data is organized in the buffer
Probably the most common and most used image
operation is loading an image. Loading an image
is fairly straight forward, and there are many ways to do it, so I will show
you one of the simplest and quickest ways, using the javax.imageio.ImageIO class.
Basically what we want to do is load all the
bytes from some image file, into a BufferedImage so that we can display it.
This can be accomplished through the use of ImageIO.
public static BufferedImage loadImage(String ref) {
BufferedImage bimg = null;
try {
bimg = ImageIO.read(new File(ref));
} catch (Exception e) {
e.printStackTrace();
}
return bimg;
}
We would call the method like this
ImageUtility.loadImage("C:/Folder/foo.bmp");
ImageIO reads
in the bytes of an image to a BufferedImage. Once you have the BufferedImage, you
can do many things with it. Of course loading an image is nearly completely
useless unless you can display it on screen, so next ill explain how to render, or draw an image.
This is where the fun starts. To draw an Image, we need a surface to draw on; the Abstract
Windowing Toolkit and Swing supply this. For a more detailed tutorial on those subjects,
look up the java documentation at java.net. Every top-level container in Swing and AWT
such as a JPanel, JFrame, or Canvas, has its own Graphics object. This Graphics object allows us to draw on its
parent surface (a JPanel, JFrame, Canvas, or Image) with such calls as Graphics.drawLine(int x1, int y1,
int x2, int y2); or Graphics.drawImage(Image img, int
x, int y, ImageObserver ob); The Graphics2D object, is a more powerful child of the Graphics
object, it adds more methods which allow for drawing BufferedImages. Here is
an example of how to load and draw a BufferedImage onto the screen.
public class ImageApp {
public void loadAndDisplayImage(JFrame frame) {
// Load the img
BufferedImage loadImg = ImageUtil.loadImage("C:/Images/duke.gif");
frame.setBounds(0, 0, loadImg.getWidth(), loadImg.getHeight());
// Set the panel visible and add it to the frame
frame.setVisible(true);
// Get the surfaces Graphics object
Graphics2D g = (Graphics2D)frame.getRootPane().getGraphics();
// Now draw the image
g.drawImage(loadImg, null, 0, 0);
}
public static void main(String[] args) {
ImageApp ia = new ImageApp();
JFrame frame = new JFrame("Tutorials");
ia.loadAndDisplayImage(frame);
}
}
In the first few lines we load the image using
ImageUtil, then we change the JFrame that is passed in to our liking, and get the frames Graphics
object. Note that we have to set the frame visible before we ask for the Graphics object. Then we
use the graphics to draw the image at position (0, 0). Very simple, and very incomplete, you
will notice that if you resize the screen, the image will disappear.
To remedy this problem, we need to create a sub-class of JPanel that will facilitate showing images that will display no matter what the user circumstances. To do this we need to override the JPanel.paintComponent(Graphics g); method. I will call the sub-class JImagePanel, heres the class:
public class JImagePanel extends JPanel{
private BufferedImage image;
int x, y;
public JImagePanel(BufferedImage image, int x, int y) {
super();
this.image = image;
this.x = x;
this.y = y;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(image, x, y, null);
}
}
This allows us to pass an image into the newly created JImagePanel, which will then handle all painting for the life cycle of the application. JImagePanel overrides paintComponent and makes it paint the Image at the supplied coordinates. Wrapping our images in this Panel will allow Swing and AWT to handle all rendering. Here is the previous example, except now using the JImagePanel:
public class ImageApp {
public void loadAndDisplayImage(JFrame frame) {
BufferedImage loadImg = ImageUtil.loadImage("C:/Images/duke.gif");
frame.setBounds(0, 0, loadImg.getWidth(), loadImg.getHeight());
JImagePanel panel = new JImagePanel(loadImg, 0, 0);
frame.add(panel);
frame.setVisible(true);
}
public static void main(String[] args) {
ImageApp ia = new ImageApp();
JFrame frame = new JFrame("Tutorials");
ia.loadAndDisplayImage(frame);
}
}
This change makes a world of difference, as now the resizing, buffering, and other complicated stuff is handled internally, now all we have to worry about is manipulating images.
There are a few ways to create your own Image, all fairly simple. The most obvious way would be to simply create a new
BufferedImage object. Like this:
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_3BYTE_BGR);But to be able to draw into this image, you first need to create the
Graphics:
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_3BYTE_BGR); img.createGraphics();
Now we have a surface to draw on, so, as an example, Ill draw a road on a yellow background;
public class ImageMaker {
public static BufferedImage createImage() {
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
img.createGraphics();
Graphics2D g = (Graphics2D)img.getGraphics();
g.setColor(Color.YELLOW);
g.fillRect(0, 0, 100, 100);
for(int i = 1; i < 49; i++) {
g.setColor(new Color(5*i, 5*i, 4+1*2+i*3));
g.fillRect(2*i, 2*i, 3*i, 3*1);
}
return img;
}
public static void main(String[] args) {
JFrame frame = new JFrame("Image Maker");
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent event) {
System.exit(0);
}
});
frame.setBounds(0, 0, 200, 200);
JImagePanel panel = new JImagePanel(createImage(), 50, 45);
frame.add(panel);
frame.setVisible(true);
}
}
This creates a BufferedImage, creates its graphics, then sets the background to yellow,
lastly, using clever manipulation of a for loop, the program makes a rough drawing of a road. This method returns an image that is promptly displayed our custom JImagePanel. Note that I added
WindowAdapter to the JFrame so that it will actually destroy the JVM when you stop the program. Below is a picture of the output:
![]() |
|
Figure 2. The Lonesome Road
|
This is a small example of how you can make your own images.
You can also use the Graphics2D methods to display a string in a particular font,
draw ovals, predefined Shapes, and many other things.
Saving an image to a file is a fairly simple operation, basically, all that needs to happen is for bytes in memory to be written to a file. Below is the old way of saving a JPEG.
public static void saveOldWay(String ref, BufferedImage img) {
BufferedOutputStream out;
try {
out = new BufferedOutputStream(new FileOutputStream(ref));
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(img);
int quality = 5;
quality = Math.max(0, Math.min(quality, 100));
param.setQuality((float) quality / 100.0f, false);
encoder.setJPEGEncodeParam(param);
encoder.encode(img);
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
This saves your BufferedImage img to a JPEG file with a path
specified by the String ref Basically, it encodes your image data into the JPEG format, sets the
image quality, then saves it to a file.
This archaic way of saving an image has many faults though, besides not being very straightforward. Thankfully Sun has given us
ImageIO. ImageIO gives a centralized way to save and load images, for saving it has three forms of the write
method, which writes your image data to a file in JPEG or PNG, format, the three forms of the write method are:
We can wrap our ImageUtil around one of these write methods, like this:
/**
* Saves a BufferedImage to the given file, pathname must not have any
* periods "." in it except for the one before the format, i.e. C:/images/fooimage.png
* @param img
* @param saveFile
*/
public static void saveImage(BufferedImage img, String ref) {
try {
String format = (ref.endsWith(".png")) ? "png" : "jpg";
ImageIO.write(img, format, new File(ref));
} catch (IOException e) {
e.printStackTrace();
}
}
Sometimes it is necessary to make an image, or part of an image somewhat translucent, or see through. If you have any experience with Photoshop, Gimp or other Imaging programs, this means modifying an Images alpha, or applying an Alpha layer on top of the Image. I will show you how to do the latter.
The Graphics2D object, which is a part of every image, and top level container (such as a JFrame),
lets you manipulate how it draws through multiple methods. Mainly, setComposite(Composite comp), setStroke(Stroke stroke) and setPaint(Paint paint),
all of these affect how the Graphics2D draws the image. For this part of the tutorial, we will mainly be concerned with
setComposite(Composite comp). Composites let us modify things like the transparency of an image, which is what we are interested in. Most of the time we only want the AlphaComposite on the Image, not on the entire Container.
So we'll add another method to our ImageUtil class that will load an Image and set a certain amount of
alpha on the image only, leaving the container alone. Here is the code:
public static BufferedImage loadTranslucentImage(String url, float transperancy) {
// Load the image
BufferedImage loaded = loadImage(url);
// Create the image using the
BufferedImage aimg = new BufferedImage(loaded.getWidth(), loaded.getHeight(), BufferedImage.TRANSLUCENT);
// Get the images graphics
Graphics2D g = aimg.createGraphics();
// Set the Graphics composite to Alpha
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, transperancy));
// Draw the LOADED img into the prepared reciver image
g.drawImage(loaded, null, 0, 0);
// let go of all system resources in this Graphics
g.dispose();
// Return the image
return aimg;
}
This block of code first loads your our image, and then creates another BufferedImage to draw into that allows translucency. Then, right after it gets the Graphics2D
object of the image, it sets the composite to an AlphaComposite, with the alpha level set to whatever the user specifies. The amount of
alpha can be any float value from 0.0 to 1.0. Next it draws the loaded image, into the translucency capable image, and disposes of unsued resources. Using our JImagePanel we draw the image and display it, giving us this:
![]() |
![]() |
![]() |
|
Fig. 3. Regular Duke
|
Fig. 4. Alpha Composited Duke
|
A useful function of many imaging programs such as GIMP is its ability to make only one color of an Image transparent; this is called applying a mask. To do this, GIMP evaluates each pixel of an image, and tests if it is the user specified masking color, if it is it sets that pixel's RGB value to nothing, and it's alpha to 0.0. Here is the Java implementation:
public static BufferedImage makeColorTransparent(String ref, Color color) {
BufferedImage image = loadImage(ref);
BufferedImage dimg = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
This will load the image, and also sets up a destination image for drawing into. The next part is what does all the work:
Graphics2D g = dimg.createGraphics();
g.setComposite(AlphaComposite.Src);
g.drawImage(image, null, 0, 0);
g.dispose();
for(int i = 0; i < dimg.getHeight(); i++) {
for(int j = 0; j < dimg.getWidth(); j++) {
if(dimg.getRGB(j, i) == color.getRGB()) {
dimg.setRGB(j, i, 0x8F1C1C);
}
}
}
return dimg;
}
This grabs the Graphics2D instance of the destination image, and sets it's composite to Alpha.
It then draws the Image onto the screen, and disposes of the Graphics2D to clear up system resources.
Next, it cycles through all the pixels in the image, using the for loops, and compares the color of the pixel to the mask color.
If the pixels color matches the mask color, that pixels color is replaced by an alpha RGB value. Lastly, the destination image, with mask applied, is returned.
Here is what the output will be, when we wrap the manipulated image inside our JImagePanel:
![]() |
![]() |
![]() |
|
Fig. 5. Regular Duke
|
Fig. 6. Masked Duke
|
As you can see, all the white pixels on the image on the left were replaced with pixels of alpha value, shown in the window on the left.
Flipping an image means mirroring it on a certain axis. Flipping vertically could be compared to the reflection of yourself in a lake,
flipping horizontally is kind of like looking into a mirror. To do a flip, we use drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer);
of Graphics2D object. This method lets us draw part of an image into an image, or scale an image etc. The method takes an image, and a set of coordinates of the destination rectangle, it
also takes a set of coordinates for the source rectangle of the image you supply. These source coordinates can be manipulated for varying results, as I will show
in this next code segment.
For flipping an image horizontally, we give the source rectangle coordinates starting from the top right, and ending at the bottom left like this.
drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer); g.drawImage(The Source Image, 0, 0, image width, image height, image width, 0, 0, image height, null); as opposed to the unflipped way g.drawImage(The Source Image, 0, 0, image width, image height, 0, 0, image width, image height, null);
Putting it all together, you would flip an image horizontally like this:
public static BufferedImage horizontalflip(BufferedImage img) {
int w = img.getWidth();
int h = img.getHeight();
BufferedImage dimg = new BufferedImage(w, h, img.getType());
Graphics2D g = dimg.createGraphics();
g.drawImage(img, 0, 0, w, h, w, 0, 0, h, null);
g.dispose();
return dimg;
}
Here is what the output would be with the manipulated image inside our JImagePanel:
![]() |
![]() |
![]() |
|
Fig. 7. Regular Duke
|
Fig. 8. Flipped Duke
|
To flip an image vertically, you just change the source rectangle coordinates around some more:
public static BufferedImage verticalflip(BufferedImage img) {
int w = img.getWidth();
int h = img.getHeight();
BufferedImage dimg = dimg = new BufferedImage(w, h, img.getColorModel().getTransparency());
Graphics2D g = dimg.createGraphics();
g.drawImage(img, 0, 0, w, h, 0, h, w, 0, null);
g.dispose();
return dimg;
}
Here is what the output would be if this was wrapped inside an application:
![]() |
![]() |
![]() |
|
Fig. 9. Regular Duke
|
Fig. 10. Flipped Duke
|
Rotating an image is a complicated procedure, requiring vast knowledge of trigononetry and image processing, thankfully we dont need to know any of that,
as Sun provides us a method inside of the Graphics2D object that does all that complicated stuff for us, aptly named rotate(double theta, double x, double y).
Here is an example of how to wrap it inside a method in our ImageUtil class:
public static BufferedImage rotate(BufferedImage img, int angle) {
int w = img.getWidth();
int h = img.getHeight();
BufferedImage dimg = dimg = new BufferedImage(w, h, img.getType());
Graphics2D g = dimg.createGraphics();
g.rotate(Math.toRadians(angle), w/2, h/2);
g.drawImage(img, null, 0, 0);
return dimg;
}
This method, besides doing usual initialization, turns the supplied angle (in degrees) to radians. It also ensures that the image stays in the middle of the destination image.
Here is a sample of the output if wrapped in an application and rotation set at 45 degrees.
![]() |
![]() |
![]() |
|
Fig. 11. Regular Duke
|
Fig. 12. Rotated Duke
|
As you can see, the image is rotated 45 degrees clockwise.
For resizing an image, we again refer to the Graphics2D object, resizing an image is a very simple operation, just like in rotating an image, all
math is taken care of by the Graphics2D object.
Note that we set rendering hints to balance speed and quality, unfortunately an explanation of RenderingHints is beyond the scope of this article:
public static BufferedImage resize(BufferedImage img, int newW, int newH) {
int w = img.getWidth();
int h = img.getHeight();
BufferedImage dimg = dimg = new BufferedImage(newW, newH, img.getType());
Graphics2D g = dimg.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(img, 0, 0, newW, newH, 0, 0, w, h, null);
g.dispose();
return dimg;
}
![]() |
![]() |
![]() |
|
Fig. 13. Regular Duke
|
Fig. 14. Resized Duke
|
Another way to resize is to use the Image.getScaledInstance(int newWidth, int newHeight, int renderHints). Do NOT use this to resize, it is slow and unwieldy, for detailed reasons check out this article by Chris Campbell.
Now to have some fun, and whats more fun than Mario! Our goal for this part of the tutorial is to make an animated Mario out of this Sprite Sheet:
![]() |
|
Fig. 14 Mario Sprite Sheet
|
Splitting an image is fairly simple, and very useful. What we want to do is split the image into rows and columns,
and draw each mini image into it's own BufferedImage, all of these smaller images will be kept inside
a BufferedImage array. Keeping the images in an array will allow for easy access when we want to animate the sprites. Here
is how you would split an image into columns and rows:
public static BufferedImage[] splitImage(BufferedImage img, int cols, int rows) {
int w = img.getWidth()/cols;
int h = img.getHeight()/rows;
int num = 0;
BufferedImage imgs[] = new BufferedImage[w*h];
for(int y = 0; y < rows; y++) {
for(int x = 0; x < cols; x++) {
imgs[num] = new BufferedImage(w, h, img.getType());
// Tell the graphics to draw only one block of the image
Graphics2D g = imgs[num].createGraphics();
g.drawImage(img, 0, 0, w, h, w*x, h*y, w*x+w, h*y+h, null);
g.dispose();
num++;
}
}
return imgs;
}
This block of code first figures out the width and height of the mini images, using the columns and rows variables.
Next it initializes the BufferedImage array, then starts two loops, one for columns, one for rows. Next it initializes each image in the array,
and draws into them their respective mini image using the w and h variables as a helper to figure out the exact coordinates
of each rectangle. Lastly, it returns the BufferedImage array.
Now that we can get all the images, we need to figure out how to animate them, so keep reading:
Now lets make use of our new found skill. Being able to split up Images is only half of the challenge, now we
need to be able to make mario seem like he's moving, we can achieve this by alternating different images that correspond
to one part of his movement. This is called animation, to make it easier we will make a new class called AnimatedSprite.
This class will serve many purposes, it will hold hold onto the split up image, the frame numbers, the location, and anything else that comes to our whimsy.
Here is an outline of the AnimatedSprite class:
/** * Lets you add a new animation set to the Sprite * * @param name The name of the Sprite, this name will be used in setting the animation * @param set The frame numbers of the animation */ public void addNewAnimation(String name, int[] set); /** * Draws the current frame of the sprite * @param g */ public void draw(Graphics2D g); /** * Sets the current animation of the Sprite * @param name */ public void setAnimation(String name); /** * Sets how the Sprite cycles through the animations. * @param method One of the constants LOOP_REVERSE or LOOP_BEGINNING * LOOP_BEGINNING: Will start over when loop reaches the end * LOOP_REVERSE: Will start backwards when loop is over */ public void setLoopMethod(int method); /** * Sets the location of the sprite, it's upper left corner will be at the * specified position * @param x * @param y */ public void setLocation(double x, double y); /** * Get the X coordinate * @return */ public double getX(); /** * Set the X coordinate * @param x */ public void setX(double x); /** * Get the Y coordinate * @return */ public double getY(); /** * Set the Y coordinate * @param y */ public void setY(double y); /** * Gets the name of the current animation * @return */ public String getCurrentAnim(); /** * Gets the width of one frame * @return */ public int getWidth(); /** * Gets the height of one frame * @return */ public int getHeight();
For animation to work correctly we need to make sure some things happen right. First we need a way to remember the animation frames, this can be solved by putting the index of each frame for a particular animation inside an int array. Then, we can put that int array inside of a HashMap. The HashMap will allow us to get the int array by giving it a "key", or a name, this name in our case will be the animation name, for example "walk". Heres how this would happen:
/** * This holds all the frames, named by a string key; * for instance, you could enter the key 'walk' which would return * an int array */ private HashMapframes; Here is how you would add frames to the Sprite /** * Lets you add a new animation set to the Sprite * * @param name The name of the Sprite, this name will be used in setting the animation * @param set The frames of the animation */ public void addNewAnimation(String name, int[] set) { frames.put(name, set); setAnimation(name); } Then you would get the frames like this int[] walkframes = frames.get("walk");
Along with a way to store the frames, we will also need coordinates for rendering help, as well as multiple variables for keeping track of the current animation state, here are the variables needed:
/** * The current 'state' or animation that the sprite is in, ie. "walk", "run", or "stand" */ private String currentAnim; /** * The current frame being rendered, it's not the number of the frame, its the index for the Image[] * as in it's this number "frameset[currentframe] = 1;" not this number "frameset[0] = currentframe;" */ private int currentFrame; /** * The current set of frames being used */ private int[] currentFrameSet;
The most important method of the class is the draw(Graphics2D g) method, this method will eraser the
previous image, and draw the current frame, as well as preparing the next frame.
Another thing about animation is that there are multiple ways of cycleing through the frames, one way, is to loop through the frames and then go back to the first. The second way is to loop through the frames and then go backwards through them again, for example, if the frame indexes were {15,16,17,18} they would be cycled through like this "15,16,17,18,17,16,15.... etc.". Using one or the other depends on your animation, for our purposes we will refer to these looping methods as LOOP_REVERSE, and LOOP_BEGINNING respectively.
Here is the full code for the AnimatedSprite class:
View AnimatedSprite classThis class will allow for animation of any collection of sprites. Here is a small example of how to use the class.
View Animation classThese classes provide a powerful way to animate a set of images.
The previous tips have hopefully given you a strong foothold in the quagmire that is java Images, and possibly a handhold on games and Animation. Hopefully you have found these tricks useful. In the next installment we will look at more advanced image operations such as compositing and paint strokes. Until next time, happy coding!
Learning Java 2D, Part 2 by Robert Eckstein http://java.sun.com/developer/technicalArticles/GUI/java2d/java2dpart2.html
Java 2D API Specification http://java.sun.com/j2se/1.4.2/docs/api/index.html
Image Processing by Bill Day http://www.javaworld.com/javaworld/jw-09-1998/jw-09-media.html