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.
Drawing an Image on the Screen using Graphics2D
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.
Creating and Using your own Image in Java
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
|
Making a Certain Color Transparent
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.