When you are writing a computer program, you probably want images to be displayed in the tool bar, and next to the menu items. There may be a need for a more specialized use of images, such as displaying a logo or a graph. When you are writing a computer game, the entire screen is filled with images.
The Java API for the Image
type can be intimidating for a beginner. What are all these ImageObserver
s you need to provide whenever you draw an image? (The online tutorials tell you to simply provide null
, but that doesn’t explain it.) What does it mean that the width and the height of an image can be unknown? Many examples use a Component
object to create an image, but why would you need one (or maybe you don’t)?
There is an explanation for all of this, but the short story is that almost certainly the class you actually want to use is BufferedImage
. Then things become simple.
The long story…
Historical reasons behind the Image API
(Skip this section if you don’t care about history, and just want to see the code.)
Imagine the year 1996, when Java 1.0 was published. This was the time of “browser wars”, when people outside of academia were learning about the World Wide Web, and software developers were busy creating websites for companies. You could make some nice money writing pages in pure HTML. Anything more complicated was quite frustrating, because each web browser - sometimes, each version of each web browser - worked a bit differently. When Netscape and Microsoft introduced scripting ability in their web browsers, most scripts started with mysterious routines that detected the name and version of the web browser, and the rest of the script was full of “if it is this version of this browser, do this, otherwise do that” sections. Which meant that the next version of a web browser probably broke your web page functionality anyway.
(The situation only got better ten years later, when the JQuery library hid all this browser-specific behavior under a unified API, and allowed developers to write code like “when mouse is clicked here, show this image” without worrying about how the different browsers detected mouse clicks and inserted images into web pages.)
Java 1.0 came with the concept of a “web applet”, which was an application running inside a web page with very limited permissions. It could display images or texts within the assigned rectangle, it could animate the images or allow editing the texts, it could read images and data from the server, and it could send data from the web forms to the server… but it could not access your local hard disk, and when you closed the web page, it was gone. That allowed more functionality than the web browsers were going to provide in the following decade, and was surprisingly safe considering that you were running someone else’s code on your computer. (The alternative solution proposed by Microsoft, called ActiveX, consisted of running someone else’s code with full permissions, without as much as notifying the user that this was going to happen. Later they improved the security model by requiring ActiveX developers to sign a contract promising not to develop malware. I am not making this up.)
Also, depending on your connection, internet in the 90s could be quite slow. An image covering a significant part of the screen could take more than a minute to load. You didn’t want your applet to wait idly until all images were loaded, therefore the images were by default loaded asynchronously.
This is why the concept of “an image that is potentially not fully loaded yet” plays such an important role in Java API. The Image
class is an abstraction for a partially-loaded image (and a few other things). This is why the documentation says that drawing an Image
will draw the part that is already loaded (and optionally provide a notification to an ImageObserver
when more pixels become available). This is why the width and the height of an Image
are sometimes not available, because perhaps even the header of the image was not loaded yet.
This was more useful in 1996 than it is now. Applets are a history. Applications are typically distributed in archives, which means that when the code is running, the images are already downloaded on your local disk. There is rarely a need to display a partially-loaded image.
BufferedImage
is an image that is fully loaded in memory. (It is a subclass of Image
, therefore you can use it in situations where an instance of Image
is required.) Its dimensions and all its pixels are immediately available. In this article I will use this class, and you will probably want to use it in your applications.
Loading and saving an image
If the image is a part of your application, put it in the same directory as the bytecode of the Java class that is going to load it. (In Maven standard directory layout it means that the Java source code is in a directory “src/main/java/myPackage” and the image file is in a directory “src/main/resources/myPackage”, where the “myPackage” part is the same for both. This will put both the compiled class and the image file into the “myPackage” directory within the generated archive.)
The following command loads the image:
BufferedImage img =
ImageIO.read
(getClass().
getResourceAsStream
("
example.png
"));
If the image is somewhere in the file system (for example you chose it using JFileChooser
), the following command loads the image:
BufferedImage img =
ImageIO.read
(
file
);
To save the image to the file system, choose the destination and use the following command:
ImageIO.write(img, "png", file);
You can also choose a different image format. Out of the box, Java supports PNG, BMP, GIF, JPEG, TIFF, and WBMP. Note that some formats have technical limitations (e.g. do not support transparency, partial transparency, or 24-bit colors), therefore the saved file may not be identical to the image in memory. Format PNG will preserve the values of all pixels. Format JPEG is good for imprecise photos without transparency.
Okay, this was a simplified version. We also need to add some error checking, and in the first example close the input stream after loading (or failing to load) the image. So the actual code to load an image included in the application would be something like:
try (InputStream is = getClass().getResourceAsStream("example.png")) {
if (null == is) {
throw ... // file does not exist
}
BufferedImage img = ImageIO.read(is);
...
} catch (IOException e) {
throw ... // error loading file
}
The actual code to load an image from the file system would be something like:
try {
BufferedImage img = ImageIO.read(file);
...
} catch (IOException e) {
throw ... // error loading file
}
And the actual code to save an image to the file system would be something like:
try {
ImageIO.write(img, "png", file);
} catch (IOException e) {
throw ... // error saving file
}
Note: This already allows you to load an image and save it in a different format. You may want to try it now.
Displaying an image
Here, I am going to assume that you are using a Swing library to create dialogs.
If you want to add a small image to a component such as a button, a menu item, or a frame, there is usually a method called “setIcon” or “setIconImage”, or a constructor that takes an image as a parameter.
Sometimes the parameter has a type Icon
instead of Image
. You can wrap the image in ImageIcon
, like this:
Icon ic = new ImageIcon(img);
The image used for the frame will also be displayed in the task bar and when you switch application. You can set it like this:
JFrame frame = new JFrame("Example");
frame.setIconImage(img);
A new button or menu item with an image is created like this:
JButton button = new JButton("Click me", new ImageIcon(img));
JMenuItem itm = new JMenuItem("Choose me", new ImageIcon(img));
You can also add the image to an already existing button or menu item, like this:
button.setIcon(new ImageIcon(img));
menuItem.setIcon(new ImageIcon(img));
The easiest way to display the image as a component in a dialog is using a JLabel:
JLabel pic = new JLabel(new ImageIcon(img));
The situation is quite different when you are writing a computer game. You want to have multiple images, sometimes overlaping, within the same component. You also want those images to move, appear, and disappear, as the state of the game changes. Unless you are using a specialized library, this is achieved by creating a subclass of Canvas
and redefining its “paint” method. This method receives a Graphics
object, which allows you (among many other things) to show an image at given coordinates within the canvas. For example:
Canvas canvas = new Canvas() {
@Override public void paint(Graphics g) {
g.drawImage(img, 100, 200, null);
}
};
This would draw the image at coordinates [100, 200] within the canvas, which means that the left edge of the image would be 100 pixels to the right from the left edge of the canvas, and the top edge of the image would 200 pixels downwards from the top edge of the canvas.
You also need to set the size of the canvas, for example like this:
canvas.
setPreferredSize
(new Dimension(800, 600));
frame.add(canvas);
frame.
pack
();
Frame’s method “pack” calculates the size and position of the frame and its components, so that they can all have their preferred sizes. Note that the frame will be slightly larger than the canvas, because in addition to the canvas, it also contains the frame borders and title, and maybe also a menu bar or a tool bar.
Editing an image
A raster image is a two-dimensional array of pixels - tiny squares, each square filled with one color. Raster images are simple to work with, if you intend to display the images at their original size. (Increasing the size would result in checkered images.)
A color on a computer screen is typically expressed using three numbers: the intensity of red, green, and blue light. Various combinations of these three lights create different colors. For example, zero intensity of each light results in black color. Maximum intensity of each light results in white color. Red and green light, without blue light, results in yellow color. Red light at full intensity, green light at half intensity, and no blue light, results in orange color. And so on.
Partial transparency can be expressed by an additional number, called alpha, where zero alpha means fully transparent, and maximum alpha means fully opaque pixel. Values between the zero and maximum mean partially transparent; if you paint a partially transparent pixel on a canvas, its color will blend with the original color, higher values of alpha meaning the result will more resemble the new color.
You can create a new in-memory image like this:
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
The last parameter means that each pixel in the new image will be stored as one integer number encoding the alpha, red, green, and blue values of the color.
You can retrieve a color of a pixel at given coordinates like this:
int color = img.getRGB(x, y);
The x coordinate is between 0 and width - 1
; from left to right. The y coordinate is betwen 0 and height - 1
; from top to bottom. You can find out the dimensions of the image using its “getWidth” and “getHeight” methods.
You can change a color of a pixel at given coordinates like this:
img.setColor(x, y, color);
This allows you to edit images in memory, one pixel at a time. But how exactly is the alpha, red, green, and blue encoded in the integer representing color?
The int
type in Java has 32 bits. Starting from the most significant bit, we have 8 bits for alpha, 8 bits for red, 8 bits for green, and 8 bits for blue. This means that each of these numbers is a value between 0 and 255, and we can calculate them like this:
int alpha = (color >> 24) & 0xff;
int r = (color >> 16) & 0xff;
int g = (color >> 8) & 0xff;
int b = color & 0xff;
In the opposite direction, if we have values for alpha, red, green, and blue between 0 and 255, we can calculate the integer representing the color like this:
int color = ((alpha << 24) & 0xff000000) | ((r << 16) & 0xff0000) | ((g << 8) & 0xff00) | (b & 0xff);
(At this moment, I suspect some readers might ask: for values between 0 and 255, why don’t we instead use the byte
type, which seems like a natural choice? In Java, the primitive type byte
is signed: instead of from 0 to 255, it actually goes from 0 to 127, and then from -128 to -1. You would need to remember this whenever you write code like “is this pixel more transparent than this one?” or “make this pixel 50% darker”, otherwise bugs will happen. I prefer to avoid such landmines.)
Another possible approach to editing images is using a Graphics
or Graphics2D
object. (Graphics2D
is a subclass of Graphics
.) You can obtain a Graphics2D
object representing the image using its “getGraphics” method. Then you can use the convenient methods provided by the Graphics
or Graphics2D
classes. For example, you can create a copy of an image like this:
BufferedImage copy = new BufferedImage(img.getWidth(), img.getHeight, BufferedImage.TYPE_INT_ARGB);
copy.getGraphics().drawImage(img, 0, 0, null);
Similarly you can draw rectangles, ovals, lines; even write characters…
Now try it!
If you want to see this code in context, I have published some example applications in repository gitlab.com/kittenlord/image. Classes ImageViewer
and CanvasViewer
show how to display images in Swing components and Canvas
respectively. You can see their screenshots in the section “Displaying an image”.
Class ImageEditor
allows you to open an image from the file system, or choose a predefined image, apply various graphical effects to it, and optionally save the result. The graphical effects are implemented as static methods in class ImageEffects
.
The easiest starting point for experimenting is to add more graphical effects, and try what happens when you apply them to various images. For example, you could try to change a color photograph into a black-and-white version.
If you have questions related to this article or the project in repository, feel free to ask in comments. (Basic familiarity with Java is assumed; please don’t ask me how to declare a variable.) If you use Java 9 or older, you will have to replace “var” with actual class names in the project. (Or install OpenJDK 11 from here.)