Section 1.2
Two-dimensional Graphics in Java
Java's support for 2D graphics is embodied primarily in two abstract classes, Image and Graphics, and in their subclasses. The Image class is mainly about raster graphics, while Graphics is concerned primarily with vector graphics. This chapter assumes that you are familiar with the basics of the Graphics class and the related classes Color and Font, including such Graphics methods as drawLine, drawRect, fillRect, drawString, getColor, setColor, and setFont. If you need to review them, you can read Section 6.3 of Introduction to Programming Using Java.
The class java.awt.Image really represents the most abstract idea of an image. You can't do much with the basic Image other than display it on a drawing surface. For other purposes, you will want to use the subclass, java.awt.image.BufferedImage. A BufferedImage represents a rectangular grid of pixels. It consists of a "raster," which contains color values for each pixel in the image, and a "color model," which tells how the color values are to be interpreted. (Remember that there are many ways to represent colors as numerical values.) In general, you don't have to work directly with the raster or the color model. You can simply use methods in the BufferedImage class to work with the image.
Java's standard class java.awt.Graphics represents the ability to draw on a two-dimensional drawing surface. A Graphics object has the ability to draw geometric shapes on its associated drawing surface. (It can also draw strings of characters and Images.) The Graphics class is suitable for many purposes, but its capabilities are still fairly limited. A much more complete two-dimensional drawing capability is provided by the class java.awt.Graphics2D, which is a subclass of Graphics. In fact, all the Graphics objects that are provided for drawing in modern Java are actually of type Graphics2D, and you can type-cast the variable of type Graphics to Graphics2D to obtain access to the additional capabilities of Graphics2D. For example, a paintComponent method that needs access to the capabilities of a Graphics2D might look like this:
protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D)g.create(); ... // Use g2 for drawing. }
The method g.create creates a new graphics context object that has exactly the same properties as g. You can then use g2 for drawing, and make any changes to it that you like without having any effect on g. It is recommended not to make any permanent changes to g in the paintComponent method, but you are free to do anything that you like to g2.
1.2.1 BufferedImages
There are basically two ways to get a BufferedImage. You can create a blank image using a constructor from the BufferedImage class, or you can get a copy of an existing image by reading the image from a file or some other source. The method
public boolean read(File source)
in the class javax.imageio.ImageIO can be used to read an image from a file. Supported image types include at least PNG and JPEG. Other methods in the ImageIO class make it possible to read an image from an InputStream or URL.
When you want to create a blank image, the constructor that you are most likely to use is
public BufferedImage(int width, int height, int imageType)
The width and height specify the size of image, that is, the number of rows and columns of pixels. The imageType specifies the color model, that is, what kind of color value is stored for each pixel. The imageType can be specified using a constant defined in the BufferedImage class. For basic full-color images, the type BufferedImage.TYPE_INT_RGB can be used; this specifies a color model that uses three eight-bit numbers for each pixel, giving the red, green, and blue color components. It is possible to add an eight-bit "alpha," or transparency, value for each pixel. To do that, use the image type BufferedImage.TYPE_INT_ARGB. Grayscale images can be created using the image type BufferedImage.TYPE_BYTE_GRAY.
Once you have a BufferedImage, you might want to be able to modify it. The easiest way is to treat the image as a drawing surface and to draw on it using an object of type Graphics2D. The method
public Graphics2D createGraphics()
in the BufferedImage class returns a Graphics2D object that can be used to draw on the image using the same set of drawing operations that are commonly used to draw on the screen.
It is also possible to manipulate the color of individual pixels in the image. The BufferedImage class has methods public int getRGB(int x, int y) and public void setRGB(int x, int y, int rgb) for getting and setting the color of an individual pixel. There are also methods for getting and setting the color values of a large number of pixels at once. See the API documentation for more information. (For these methods, colors are specified by 32-bit int values, with eight bits each for the alpha, red, green, and blue components of the color.)
Once you have your complete image, you can save it to a file using a write method from the ImageIO class. You can also copy the image to a drawing surface (including another image) using one of the drawImage methods from the Graphics class, such as
public boolean drawImage(Image image, int x, int y, ImageObserver obs)
This method draws the image at its normal size with its upper left corner at the point (x,y). The ImageObserver is meant for use when drawing an image that is still being loaded, for example, over the Internet; it makes it possible to draw a partially loaded image (and the boolean return value of the method tells whether the image has been fully drawn when the method returns); the ImageObserver is there to make sure that the rest of the image gets drawn when it becomes available. Since a BufferedImage is always already in memory, you just pass null as the fourth parameter when drawing a BufferedImage, and you can ignore the return value.
One important use of BufferedImages is to keep a copy of an image that is being displayed on the screen. For example, in a paint program, a BufferedImage can be used to store a copy of the image that is being created or edited. I sometimes call an image that is used in this way an off-screen canvas. Any changes that are made to the image are applied to the off-screen canvas, which is then copied to the screen (for example, in the paintComponent method of a JPanel). You can then draw extra stuff, such as a box around a selected area, over the on-screen image without affecting the image in the off-screen canvas. And you can perform operations on the off-screen canvas, such as reading the color of a given pixel, that you can't do on the on-screen image. Using an off-screen image in this way is referred to as double buffering, where the term "buffer" is used in the sense of a "frame buffer" that stores color values for the pixels in an image. One advantage of double-buffering is that the user doesn't see changes as they are being made to an image; you can compose an image in the off-screen canvas and then copy that image all at once onto the screen, so that the user sees only the completed image. (In fact, Java uses double-buffering automatically for Swing components such as JPanel. The paintComponent method actually draws to an off-screen canvas and the result is copied onto the screen. However, you don't have direct access to the off-screen canvas that is used for this purpose, and for many applications, you need to create an off-screen canvas of your own.)
1.2.2 Shapes and Graphics2D
You should be familiar with Graphics methods such as drawLine and fillRect. For these methods, points are specified in pixel coordinates, where a pair of integers (x,y) picks out the pixel in column x and in row y. Using pixel coordinates allows you to determine precisely which pixels will be affected by a given drawing operation, and sometimes such precision is essential. Often, however, coordinates are most naturally expressed on some other coordinate system. For example, in an architectural drawing, it would be natural to use actual physical measurements as coordinates. When drawing a mathematical object such as the graph of a function, it would be nice to be able to match the coordinate system to the size of the object that is being displayed. In such cases, the natural coordinates must be laboriously converted into pixel coordinates. Another problem with pixel coordinates is that when the size of the drawing area changes, all the coordinates have to recomputed, at least if the size of the image is to change to match the change in size of the drawing area. For this reason, computer graphics applications usually allow the specification of an arbitrary coordinate system. Shapes can then be drawn using the coordinates that are most natural to the application, and the graphics system will do the work of converting those coordinates to pixel coordinates.
The Graphics2D class supports the use of arbitrary coordinate systems. Once you've specified a coordinate system, you can draw shapes using that coordinate system, and the Graphics2D object will correctly transform the shape into pixel coordinates for you, before drawing it. Later in the chapter, we will see how to specify a new coordinate system for a Graphics2D. For now, we will consider how to draw shapes in such coordinate systems.
Note that the coordinate system that I am calling "pixel coordinates" is usually referred to as device coordinates, since it is the appropriate coordinate system for drawing directly to the display device where the image appears. Java documentation refers to the coordinate system that is actually used for drawing as user coordinates, although the "user" in this case is the programmer. Another name, more appropriate to OpenGL, is world coordinates, corresponding to the idea that there is a natural coordinate system for the "world" that we want to represent in the image.
When using general coordinate systems, coordinates are specified as real numbers rather than integers (since each unit in the coordinate system might cover many pixels or just a small fraction of a pixel). The package java.awt.geom provides support for shapes defined using real number coordinates. For example, the class Line2D in that package represents line segments whose endpoints are given as pairs of real numbers. (Although the older drawing methods such as drawLine use integer coordinates, it's important to note that any shapes drawn using these methods are subject to the same transformation as shapes such as Line2Ds that are specified with real number coordinates. For example, drawing a line with g.drawLine(1,2,5,7) will have the same effect as drawing a Line2D that has endpoints (1.0,2.0) and (5.0,7.0). In fact, all drawing is affected by the transformation of coordinates, even, somewhat disturbingly, the width of lines and the size of characters in a string.)
Java has two primitive real number types: double and float. The double type can represent a larger range of numbers, with a greater number of significant digits, than float, and double is the more commonly used type. In fact, doubles are simply easier to use in Java. There is no automatic conversion from double to float, so you have to use explicit type-casting to convert a double value into a float. Also, a real-number literal such as 3.14 is taken to be of type double, and to get a literal of type float, you have to add an "F": 3.14F. However, float values generally have enough accuracy for graphics applications, and they have the advantage of taking up less space in memory. Furthermore, computer graphics hardware often uses float values internally.
So, given these considerations, the java.awt.geom package actually provides two versions of each shape, one using coordinates of type float and one using coordinates of type double. This is done in a rather strange way. Taking Line2D as an example, the class Line2D itself is an abstract class. It has two subclasses, one that represents lines using float coordinates and one using double coordinates. The strangest part is that these subclasses are defined as static nested classes inside Line2D: Line2D.Float and Line2D.Double. This means that you can declare a variable of type Line2D, but to create an object, you need to use Line2D.Double or Line2D.Float:
Line2D line1 = new Line2D.Double(1,2,5,7); // Line from (1.0,2.0) to (5.0,7.0) Line2D line2 = new Line2D.Float(2.7F,3.1F,1.5F,7.1F); // (2.7,3.1) to (1.5,7.1)
This gives you, unfortunately, a lot of rope with which to hang yourself, and for simplicity, you might want to stick to one of the types Line2D.Double or Line2D.Float, treating it as a basic type. Line2D.Double will be more convenient to use. Line2D.Float might give better performance, especially if you also use variables of type float to avoid type-casting.
Let's take a look at some of the classes in package java.awt.geom. I will discuss only some of their properties; see the API documentation for more information. The abstract class Point2D -- and its concrete subclasses Point2D.Double and Point2D.Float -- represents a point in two dimensions, specified by two real number coordinates. A point can be constructed from two real numbers ("new Point2D.Double(1.2,3.7)"). If p is a variable of type Point2D, you can use p.getX() and p.getY() to retrieve its coordinates, and you can use p.setX(x), p.setY(y), or p.setLocation(x,y) to set its coordinates. If pd is a variable of type Point2D.Double, you can also refer directly to the coordinates as pd.x and pd.y (and similarly for Point2D.Float). Other classes in java.awt.geom offer a similar variety of ways to manipulate their properties, and I won't try to list them all here.
In addition to Point2D, java.awt.geom contains a variety of classes that represent geometric shapes, including Line2D, Rectangle2D, RoundRectangle2D, Ellipse2D, Arc2D, and Path2D. All of these are abstract classes, and each of them contains a pair of subclasses such as Rectangle2D.Double and Rectangle2D.Float. (Note that Path2D is new in Java 6.0; in older versions, you can use GeneralPath, which is equivalent to Path2D.Float.) Each of these classes implements the interface java.awt.Shape, which represents the general idea of a geometric shape. Some shapes, such as rectangles, have interiors that can be filled; such shapes also have outlines that can be stroked. Some shapes, such as lines, are purely one-dimensional and can only be stroked.
Aside from lines, rectangles are probably the simplest shapes. A Rectangle2D has a corner point (x,y), a width, and a height, and can be constructed from that data ("new Rectangel2D.Double(x,y,w,h)"). The corner point (x,y) specify the minimum x- and y-values in the rectangle. For the usual pixel coordinate system, (x,y) is the upper left corner. However, in a coordinate system in which the minimum value of y is at the bottom, (x,y) would be the lower left corner. The sides of the rectangle are parallel to the coordinate axes. A variable r of type Rectangle2D.Double or Rectangle2D.Float has public instance variables r.x, r.y, r.width, and r.height. If the width or the height is less than or equal to zero, nothing will be drawn when the rectangle is filled or stroked. A common problem is to define a rectangle by two corner points (x1,y1) and (x2,y2). This can be accomplished by creating a rectangle with height and width equal to zero and then adding the second point to the rectangle:
Rectangle2D.Double r = new Rectangle2D.Double(x1,y1,0,0); r.add(x2,y2);
Adding a point to a rectangle causes the rectangle to grow just enough to include that point.
An Ellipse2D that just fits inside a given rectangle can be constructed from the upper left corner, width, and height of the rectangle ("new Ellipse2D.Double(x,y,w,h)"). A RoundRectangle2D is similar to a plain rectangle, except that an arc of an ellipse has been cut off each corner. The horizontal and vertical radius of the ellipse are part of the data for the round rectangle ("new RoundRectangle2D.Double(x,y,w,h,r1,r2)"). An Arc2D represents an arc of an ellipse. The data for an Arc2D is the same as the data for an Ellipse2D, plus the start angle of the arc, the end angle, and a "closure type." The angles are given in degrees, where zero degrees is in the positive direction of the x-axis. The closure types are Arc2D.CHORD (meaning that the arc is closed by drawing the line back from its final point back to its starting point), Arc2D.PIE (the arc is closed by drawing two line segments, giving the form of a wedge of pie), or Arc2D.OPEN (the arc is not closed).
The Path2D shape is the most interesting, since it allows the creation of shapes consisting of arbitrary sequences of lines and curves. The curves that can be used are quadratic and cubic Bezier curves, which are defined by polynomials of degree two or three. A Path2D p is empty when it is first created ("p = new Path2D.Double()"). You can then construct the path by moving an imaginary "pen" along the path that you want to create. The method p.moveTo(x,y) moves the pen to the point (x,y) without drawing anything. It is used to specify the initial point of the path or the starting point of a new segment of the path. The method p.lineTo(x,y) draws a line from the current pen position to (x,y), leaving the pen at (x,y). The method p.close() can be used to close the path (or the current segment of the path) by drawing a line back to its starting point. (Note that curves don't have to be closed.) For example, the following code creates a triangle with vertices at (0,5), (2,−3), and (−4,1):
Path2D p = new Path2D.Double(); p.moveTo(0,5); p.lineTo(2,-3); p.lineTo(-4,1); p.close();
For Bezier curves, you have to specify more than just the endpoints of the curve. You also have to specify control points. Control points don't lie on the curve, but they determine the velocity or tangent of the curve at the endpoints. A quadratic Bezier curve has one control point. You can add a quadratic curve to a Path2D p using the method p.quadTo(cx,cy,x,y). The quadratic curve has endpoints at the current pen position and at (x,y), with control point (cx,cy). As the curve leaves the current pen position, it heads in the direction of (cx,cy), with a speed determined by the distance between the pen position and (cx,cy). Similarly, the curve heads into the point (x,y) from the direction of (cx,cy), with a speed determined by the distance from (cx,cy) to (x,y). Note that the control point (cx,cy) is not on the curve -- it just controls the direction of the curve. A cubic Bezier curve is similar, except that it has two control points. The first controls the velocity of the curve as it leaves the initial endpoint of the curve, and the second controls the velocity as it arrives at the final endpoint of the curve. You can add a Bezier curve to a Path2D p with the method
p.curveTo( cx1, cy1, cx2, xy2, x, y )
This adds a Bezier curve that starts at the current pen position and ends at (x,y), using (cx1,cy1) and (cx2,cy2) as control points.
To make things clearer, here is an applet that hows a path consisting of five quadratic Bezier curves, with their (blue) control points. Note that at each endpoint of a curve, the curve is tangent to the line from that endpoint to the control point. (In the applet, these lines pear as dotted lines.) In this applet, you can drag the endpoints of the curve and the control points and see the effect on the curve.
And here is a similar applet for cubic Bezier curves. This applet shows a path that consists of four cubic Bezier curves, with their control points. Again, you can drag the endpoints of the curves and the control points.
The checkbox on the bottom of this applet allows you to "lock" the two control points at each of the endpoints where one curve meets another. When the lock is on, the two control points are forced to be symmetrically placed on either side of the control point, which ensures that one curves joins smoothly to the next.
You can find the source code for application versions of these applets in QuadraticBezierEdit.java and CubicBezierEdit.java
Once you have a Path2D or a Line2D, or indeed any object of type Shape, you can use it with a Graphics2D. The Graphics2D class defines the methods
public void draw(Shape s) public void fill(Shape s)
for drawing and filling Shapes. A Graphics2D has an instance variable of type Paint that tells how shapes are to be filled. Paint is just an interface, but there are several standard classes that implement that interface. One example is class Color. If the current paint in a Graphics2D g2 is a Color, then calling g2.fill(s) will fill the Shape s with solid color. However, there are other types of Paint, such as GradientPaint and TexturePaint, that represent other types of fill. You can set the Paint in a Graphics2D g2 by calling g2.setPaint(p). For a Color c, calling g2.setColor(c) also sets the paint.
Calling g2.draw(s) will "stroke" the shape by moving an imaginary pen along the lines or curves that make up the shape. You can use this method, for example, to draw a line or the boundary of a rectangle. By default, the shape will be stroked using a pen that is one unit wide. In the usual pixel coordinates, this means one pixel wide, but in a non-standard coordinate system, the pen will be one unit wide and one unit high, according the units of that coordinate system. The pen is actually defined by an object of type Stroke, which is another interface. To set the stroke property of a Graphics2D g2, you can use the method g2.setStroke(stroke). The stroke will almost certainly be of type BasicStroke, a class that implements the Stroke interface. For example to draw lines with g2 that are 2.5 times as wide as usual, you would say
g2.setStroke( new BasicStroke(2.5F) );
Note that the parameter in the constructor is of type float. BasicStroke has several alternative constructors that can be used to specify various characteristics of the pen, such as how an endpoint should be drawn and how one piece of a curve should be joined to the next. Most interesting, perhaps, is the ability to draw dotted and dashed lines. Unfortunately, the details are somewhat complicated, but as an example, here is how you can create a pen that draws a pattern of dots and dashes:
float[] pattern = new float[] { 1, 3, 5, 3 }; // pattern of lines and spaces BasicStroke pen = new BasicStroke( 1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 0, pattern, 0 );
This pen would draw a one-unit-long dot, followed by a 3-unit-wide space, followed by a 5-unit-long dash, followed by a 3-unit-wide space, with the pattern repeating after that. You can adjust the pattern to get different types of dotted and dashed lines.
Note that in addition to the current Stroke, the effect of g2.draw(s) also depends on the current Paint in g2. The area covered by the pen is filled with the current paint.
Before leaving the topic of drawing with Graphics2D, I should mention antialiasing. Aliasing is a problem for raster-type graphics in general, caused by the fact that raster images are made up of small, solid-colored pixels. It is not possible to draw geometrically perfect pictures by coloring pixels. A diagonal geometric line, for example, will cover some pixels only partially, and it is not possible to make a pixel half black and half white. When you try to draw a line with black and white pixels only, the result is a jagged staircase effect. This effect is an example of aliasing. Aliasing can also be seen in the outlines of characters drawn on the screen and in diagonal or curved boundaries between any two regions of different color. (The term aliasing likely comes from the fact that most pictures are naturally described in real-number coordinates. When you try to represent the image using pixels, many real-number coordinates will map to the same integer pixel coordinates; they can all be considered as different names or "aliases" for the same pixel.)
Antialiasing is a term for techniques that are designed to mitigate the effects of aliasing. The idea is that when a pixel is only partially covered by a shape, the color of the pixel should be a mixture of the color of the shape and the color of the background. When drawing a black line on a white background, the color of a partially covered pixel would be gray, with the shade of gray depending on the fraction of the pixel that is covered by the line. (In practice, calculating this area exactly for each pixel would be too difficult, so some approximate method is used.) Here, for example, are two lines, greatly magnified so that you can see the individual pixels. The one on the right is drawn using antialiasing, while the one on the left is not:
Note that antialiasing does not give a perfect image, but it can reduce the "jaggies" that are caused by aliasing.
You can turn on antialiasing in a Graphics2D, and doing so is generally a good idea. To do so, simply call
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
before using the Graphics2D g2 for drawing. Antialiasing can be applied to lines, to text, and to the curved shapes such as ellipses. Note that turning on antialiasing is considered to be a "hint," which means that its exact effect is not guaranteed, and it might even have no effect at all. In general, though, it does give an improved image. Turning antialiasing on does slow down the drawing process, and the slow-down might be noticeable when drawing a very complex image.