Chapter 12.1.1 Images and BufferedImages | Introduction to Programming Using Java

Chapter 12.1.1 Images and BufferedImages | Introduction to Programming Using Java

 

12.1.1 Images and BufferedImages

 

 

Chapter 12.1.1 Images and BufferedImages | Introduction to Programming Using Java

 

The class java.awt.Image represents an image stored in the computer’s memory. There are two fundamentally different types of Image. One kind represents an image read from a source outside the program, such as from a file on the computer’s hard disk or over a network connection.

The second type is an image created by the program. I refer to this second type as an off-screen canvas. An off-screen canvas is a region of the computer’s memory that can be used as adrawing surface. It is possible to draw to an offscreen image using the same Graphics class that is used for drawing on the screen.

An Image of either type can be copied onto the screen (or onto an off-screen canvas) using methods that are defined in the Graphics class. This is most commonly done in the paintComponent() method of a JComponent. Suppose that g is the Graphics object that is provided as a parameter to the paintComponent() method, and that img is of type Image.

Then the statement

 

Chapter 12.1.1 Images and BufferedImages | Introduction to Programming Using Java

 

will draw the image img in a rectangular area in the component. The integer-valued parameters x and y give the position of the upper-left corner of the rectangle in which the image is displayed, and the rectangle is just large enough to hold the image. The fourth parameter, this, is the special variable from Subsection 5.6.1 that refers to the JComponent itself.

This parameter is there for technical reasons having to do with the funny way Java treats image files. For most applications, you don’t need to understand this, but here is how it works: g.drawImage() does not actually draw the image in all cases. It is possible that the complete image is not available when this method is called; this can happen, for example, if the image has to be read from a file. In that case, g.drawImage() merely initiates the drawing of the image and returns immediately.

Pieces of the image are drawn later, asynchronously, as they become available. The question is, how do they get drawn? That’s where the fourth parameter to the drawImage method comes in. The fourth parameter is something called an ImageObserver. When a piece of the image becomes available to be drawn, the system will inform the ImageObserver, and that piece of the image will appear on the screen.

Any JComponent object can act as an ImageObserver. The drawImage method returns a boolean value to indicate whether the image has actually been drawn or not when the method returns. When drawing an image that you have created in the computer’s memory, or one that you are sure has already been completely loaded, you can set the ImageObserver parameter to null.

There are a few useful variations of the drawImage() method. For example, it is possible to scale the image as it is drawn to a specified width and height. This is done with the command

 

Chapter 12.1.1 Images and BufferedImages | Introduction to Programming Using Java

 

The parameters width and height give the size of the rectangle in which the image is displayed.

 

Another version makes it possible to draw just part of the image. In the command:

 

Chapter 12.1.1 Images and BufferedImages | Introduction to Programming Using Java

 

the integers sourcex1, sourcey1, sourcex2, and sourcey2 specify the top-left and bottomright corners of a rectangular region in the source image. The integers dest x1, desty1, destx2, and desty2 specify the corners of a region in the destination graphics context.

The specified rectangle in the image is drawn, with scaling if necessary, to the specified rectangle in the graphics context. For an example in which this is useful, consider a card game that needs to display 52 different cards. Dealing with 52 image files can be cumbersome and inefficient, especially for downloading over the Internet. So, all the cards might be put into a single image:

 

Chapter 12.1.1 Images and BufferedImages | Introduction to Programming Using Java

 

(This image is from the Gnome desktop project, http://www.gnome.org, and is shown here much smaller than its actual size.) Now, only one Image object is needed. Drawing one card means drawing a rectangular region from the image.

This technique is used in a variation of the sample program HighLowGUI.java from Subsection 6.7.6. In the original version, the cards are represented by textual descriptions such as “King of Hearts.” In the new version, HighLowWithImages.java, the cards are shown as images. An applet version of the program can be found in the on-line version of this section.

In the program, the cards are drawn using the following method. The instance variable card images is a variable of type Image that represents the image that is shown above, containing 52 cards, plus two Jokers and a face-down card. Each card is 79 by 123 pixels. These numbers are used, together with the suit and value of the card, to compute the corners of the source rectangle for the drawImage() command:

 

Chapter 12.1.1 Images and BufferedImages | Introduction to Programming Using Java

Chapter 12.1.1 Images and BufferedImages | Introduction to Programming Using Java

 

I will tell you later in this section how the image file, cards.png, can be loaded into the program.

In addition to images loaded from files, it is possible to create images by drawing to an off-screen canvas. An off-screen canvas can be represented by an object belonging to the class BufferedImage, which is defined in the package java.awt.image.

BufferedImage is a subclass of Image, so that once you have a BufferedImage, you can copy it into a graphics context g using one of the g.drawImage() methods, just as you would do with any other image. A BufferedImage can be created using the constructor public BufferedImage(int width, int height, int imageType)

where width and height specify the width and height of the image in pixels, and imageType can be one of several constants that are defined in the BufferedImage. The image type specifies how the color of each pixel is represented. The most likely value for imageType is BufferedImage.

TYPEINTRGB, which specifies that the color of each pixel is a usual RGB color, with red, green and blue components in the range 0 to 255. The image type BufferedImage.TYPEINT ARGB represents an RGB image with “transparency”; see the next section for more information on this. The image type BufferedImage.TYPEBYTEGRAY can be used to create a grayscale image in which the only possible colors are shades of gray.

To draw to a BufferedImage, you need a graphics context that is set up to do its drawing on the image. If OSC is of type BufferedImage, then the method

OSC.getGraphics() returns an object of type Graphics that can be used for drawing on the image.

There are several reasons why a programmer might want to draw to an off-screen canvas. One is to simply keep a copy of an image that is shown on the screen. Remember that a picture that is drawn on a component can be lost, for example when the component is covered by another window. This means that you have to be able to redraw the picture on demand, and that in turn means keeping enough information around to enable you to redraw the picture.

One way to do this is to keep a copy of the picture in an off-screen canvas. Whenever the onscreen picture needs to be redrawn, you just have to copy the contents of the off-screen canvas onto the screen. Essentially, the off-screen canvas allows you to save a copy of the color of every individual pixel in the picture. The sample program PaintWithOffScreenCanvas.java is a little painting program that uses an off-screen canvas in this way.

In this program, the user can draw curves, lines, and various shapes; a “Tool” menu allows the user to select the thing to be drawn. There is also an “Erase” tool and a “Smudge” tool that I will get to later. A BufferedImage is used to store the user’s picture. When the user changes the picture, the changes are made to the image, and the changed image is then copied to the screen.

No record is kept of the shapes that the user draws; the only record is the color of the individual pixels in the off-screen image. (You should contrast this with the program SimplePaint2.java in Subsection 7.3.4, where the user’s drawing is recorded as a list of objects that represent the shapes that user drew.)

You should try the program (or the applet version in the on-line version of this section). Try drawing a Filled Rectangle on top of some other shapes. As you drag the mouse, the rectangle stretches from the starting point of the mouse drag to the current mouse location. As the mouse moves, the underlying picture seems to be unaffected—parts of the picture can be covered up by the rectangle and later uncovered as the mouse moves, and they are still there.

What this means is that the rectangle that is shown as you drag the mouse can’t actually be part of the off-screen canvas, since drawing something into an image means changing the color of some pixels in the image. The previous colors of those pixels are not stored anywhere else and so are permanently lost. In fact, when you draw a line, rectangle, or oval in PaintWithOffScreenCanvas, the shape that is shown as you drag the mouse is not drawn to the off-screen canvas at all.

Instead, the paintComponent() method draws the shape on top of the contents of the canvas. Only when you release the mouse does the shape become a permanent part of the off-screen canvas. This illustrates the point that when an off-screen canvas is used, not everything that is visible on the screen has to be drawn on the canvas. Some extra stuff can be drawn on top of the contents of the canvas by the paintComponent() method.

The other tools are handled differently from the shape tools. For the curve, erase, and smudge tools, the changes are made to the canvas immediately, as the mouse is being dragged.

Let’s look at how an off-screen canvas is used in this program. The canvas is represented by an instance variable, OSC, of type BufferedImage. The size of the canvas must be the same size as the panel on which the canvas is displayed.

The size can be determined by calling the getWidth() and getHeight() instance methods of the panel. Furthermore, when the canvas is first created, it should be filled with the background color, which is represented in the program by an instance variable named fillColor. All this is done by the method:

 

131 Chapter 12.1.1	Images and BufferedImages | Introduction to Programming Using Java

 

Note how it uses OSC.getGraphics() to obtain a graphics context for drawing to the image. Also note that the graphics context is disposed at the end of the method. It is good practice to dispose a graphics context when you are finished with it. There still remains the problem of where to call this method.

The problem is that the width and height of the panel object are not set until some time after the panel object is constructed. If createOSC() is called in the constructor, getWidth() and getHeight() will return the value zero and we won’t get an off-screen image of the correct size. The approach that I take in PaintWithOffScreenCanvas is to call createOSC() in the paintComponent() method, the first time the paintComponent() method is called.

At that time, the size of the panel has definitely been set, but the user has not yet had a chance to draw anything. With this in mind you are ready to understand the paintComponent() method:

 

132 Chapter 12.1.1	Images and BufferedImages | Introduction to Programming Using Java

 

Here, dragging is a boolean instance variable that is set to true while the user is dragging the mouse, and currentTool tells which tool is currently in use. The possible tools are defined by an enum named Tool, and SHAPETOOLS is a variable of type EnumSet<Tool> that contains the line, oval, rectangle, filled oval, and filled rectangle tools. (See Subsection 10.2.4.)

 

You might notice that there is a problem if the size of the panel is ever changed, since the size of the off-screen canvas will not be changed to match. The PaintWithOffScreenCanvas program does not allow the user to resize the program’s window, so this is not an issue in that program. If we want to allow resizing, however, a new off-screen canvas must be created whenever the size of the panel changes.

One simple way to do this is to check the size of the canvas in the paintComponent() method and to create a new canvas if the size of the canvas does not match the size of the panel:

 

133 Chapter 12.1.1	Images and BufferedImages | Introduction to Programming Using Java

 

Of course, this will discard the picture that was contained in the old canvas unless some arrangement is made to copy the picture from the old canvas to the new one before the old canvas is discarded.

The other point in the program where the off-screen canvas is used is during a mouse-drag operation, which is handled in the mousePressed(), mouseDragged(), and mouseReleased() methods. The strategy that is implemented was discussed above. Shapes are drawn to the off-screen canvas only at the end of the drag operation, in the mouseReleased() method.

However, as the user drags the mouse, the part of the image where the shape appears is redrawn each time the mouse is moved; the shape that appears on the screen is drawn on top of the canvas by the paintComponent() method. For the other tools, changes are made directly to the canvas, and the region that was changed is repainted so that the change will appear on the screen. (By the way, the program uses a version of the repaint() method that repaints just a part of a component.

The command repaint(x,y,width,height) tells the system to repaint the rectangle with upper left corner (x,y) and with the specified width and height. This can be substantially faster than repainting the entire component.) See the source code, PaintWithOffScreenCanvas.java, if you want to see how it’s all done.

One traditional use of off-screen canvasses is for double buffering. In double-buffering, the off-screen image is an exact copy of the image that appears on screen. In double buffering, whenever the on-screen picture needs to be redrawn, the new picture is drawn step-by-step to an off-screen image.

This can take some time. If all this drawing were done on screen, the user would see the image flicker as it is drawn. Instead, the long drawing process takes place off-screen and the completed image is then copied very quickly onto the screen. The user doesn’t see all the steps involved in redrawing. This technique can be used to implement smooth, flicker-free animation.

The term “double buffering” comes from the term “frame buffer,” which refers to the region in memory that holds the image on the screen. In fact, true double buffering uses two frame buffers. The video card can display either frame buffer on the screen and can switch instantaneously from one frame buffer to the other.

One frame buffer is used to draw a new image for the screen. Then the video card is told to switch from one frame buffer to the other. No copying of memory is involved. Double-buffering as it is implemented in Java does require copying, which takes some time and is not perfectly flicker-free.

In Java’s older AWT graphical API, it was up to the programmer to do double buffering by hand. In the Swing graphical API, double buffering is applied automatically by the system, and the programmer doesn’t have to worry about it. (It is possible to turn this automatic double buffering off in Swing, but there is seldom a good reason to do so.)

One final historical note about off-screen canvasses: There is an alternative way to create them. The Component class defines the following instance method, which can be used in any GUI component object:

 

Chapter 12.1.1 Images and BufferedImages | Introduction to Programming Using Java

 

This method creates an Image with a specified width and height. You can use this image as an off-screen canvas in the same way that you would a BufferedImage. In fact, you can expect that in a modern version of Java, the image that is returned by this method is in fact a BufferedImage. The createImage() method was part of Java from the beginning, before the BufferedImage class was introduced.

 

 

 

SEE MORE: