Shapes, Text, and Buffered Images

   

Shapes , Text, and Buffered Images

Now that you've seen how Java 2D improves on the AWT's rendering model, it's time to explore Java 2D's shapes, text enhancements, and buffered images.

Shapes

From a mathematical perspective, shapes are geometric entities (such as circles and rectangles). From Java's perspective, shapes are objects whose classes implement the Shape interface.

Graphics declares a variety of methods (such as drawArc and fillArc ) for drawing and filling various kinds of shapes. The problem with these methods is that they don't represent shapes in an object-oriented manner. Although they might draw and fill shapes, it's not possible to, say, create an Arc object describing an arc and then call draw (Arc) to render its outline. The capability to create objects that represent shapes is very important to some programs (such as CAD programs) that treat shapes as entities instead of pixels. Fortunately, Java 2D solves this problem by providing a variety of geometric classes for creating shapes. Furthermore, Java 2D provides a pair of methods ” draw and fill ”that take a single Shape argument (via an object whose class implements Shape ) and draw either an outline or a solid shape.

The geometric classes contain Float and Double inner classes, whose constructors are called, with single-precision or double-precision user space coordinates (respectively), to create the appropriate shapes. Furthermore, the geometric classes contain a variety of methods for manipulating shapes. These methods will not be covered (for space reasons). If you want to learn more about these methods, consult the SDK documentation.

Arcs

The Arc2D class (located in the java.awt.geom package) is used to create arc shapes. Each arc is created in a bounding rectangle that identifies its upper-left corner and dimensions. The arc's starting angle and angular extent can be specified (in degrees). Furthermore, the arc's closure type can be specified as either chord, open , or pie. The following code fragment creates an arc:

 Arc2D arc = new Arc2D.Float (0.0f, 0.0f, 100.0f, 100.0f, 25.0f, 10.0f,                              Arc2D.CHORD); 

This arc is created in a bounding box whose upper-left corner is located at (0.0, 0.0) and dimensions are (100.0, 100.0). A start angle of 25.0 degrees and an extent of 10.0 degrees is specified. (The arc will range from 25.0 degrees to 25.0 + 10.0 = 35.0 degrees.) The closure type is chord ”a straight line drawn from the start of the arc to the end of the arc. Listing 18.15 presents source code to a ShapeDemo1 applet that demonstrates drawing various kinds of arcs.

Listing 18.15 The ShapeDemo1 Applet Source Code
 // ShapeDemo1.java import java.awt.*; import java.awt.geom.*; public class ShapeDemo1 extends java.applet.Applet {    public void paint (Graphics g)    {       Graphics2D g2 = (Graphics2D) g;       int w = getSize ().width;       int h = getSize ().height;       Arc2D arc = new Arc2D.Double (0.0, 0.0, w, h,                                     0.0, 60.0, Arc2D.CHORD);       g2.draw (arc);       arc = new Arc2D.Float (0.0f, 0.0f, w, h,                              80.0f, 110.0f, Arc2D.PIE);       g2.fill (arc);       arc = new Arc2D.Float (0.0f, 0.0f, w, h,                              210.0f, 130.0f, Arc2D.OPEN);       g2.draw (arc);    } } 

ShapeDemo1 demonstrates creating Arc2D objects by using either the Float or Double constructors. (You would normally use Float when speed is of the essence and Double when precision is more important.) An arc closed with a chord is drawn from 0 to 60 degrees. This is followed by a pie-shaped arc being drawn from 80 to 190 degrees. Finally, an open arc is drawn from 210 to 340 degrees. These arcs are shown in Figure 18.13.

Figure 18.13. Arc2D simplifies the creation of outline or solid arcs.

graphics/18fig13.gif

Note

One item in ShapeDemo1 that you might find curious is the passing of the applet's physical width and height (obtained by calling getSize ) to the Arc2D.Double and Arc2D.Float constructors. These constructors (and the other shape constructors) expect a logical width and height. The answer to this riddle is that the default transform's physical width and logical width are identical. (The same is true for physical height and logical height.) If you change the transform to specify a different logical width and/or height, you must use the logical values.


Cubic Curves

The CubicCurve2D class (located in the java.awt.geom package) is used to create cubic curves. Each cubic curve is constructed from a start point, an endpoint, and two control points (points that determine the shape of the curve). The following code fragment creates a cubic curve:

 CubicCuve2D cubic = new CubicCurve2D.Double (0.0, 50.0, 50.0, 25.0,                                              75.0, 75.0, 100.0, 50.0); 

The start point is located at (0.0, 50.0) and the endpoint is located at (100.0, 50.0). The first control point is located at (50.0, 25.0) and the second control point is located at (75.0, 75.0). Listing 18.16 presents source code to a ShapeDemo2 applet that demonstrates drawing a cubic curve:

Listing 18.16 The ShapeDemo2 Applet Source Code
 // ShapeDemo2.java import java.awt.*; import java.awt.geom.*; public class ShapeDemo2 extends java.applet.Applet {    public void paint (Graphics g)    {       int w = getSize ().width;       int h = getSize ().height;       CubicCurve2D cubic = new CubicCurve2D.Double (w / 2 - 50,                                                     h / 2,                                                     w / 2 - 25,                                                     h / 2 - 25,                                                     w / 2 + 25,                                                     h / 2 + 25,                                                     w / 2 + 50,                                                     h / 2);       // Draw first control point.       g.drawLine (w / 2 - 25, h / 2 - 25, w / 2 - 25, h / 2 - 25);       // Draw second control point.       g.drawLine (w / 2 + 25, h / 2 + 25, w / 2 + 25, h / 2 + 25);       // Draw the curve.       Graphics2D g2 = (Graphics2D) g;       g2.draw (cubic);    } } 

ShapeDemo2 draws the two control points in addition to the curve. The result is shown in Figure 18.14.

Figure 18.14. A cubic curve's appearance is governed by a pair of control points.

graphics/18fig14.gif

Note

If you are looking for an interactive program that allows you to move the start points, endpoints, and control points of a cubic curve, check out the 2D Graphics trail in the Java Tutorial (http://www.javasoft.com/tutorial).


Version 1.3 of the Java 2 SDK introduces two new CubicCurve2D methods for solving cubic equations ”equations of the form dx^3 + ax^2 + bx + c = 0. The solveCubic (double [] eqn) and solveCubic (double [] eqn, double [] res) methods take an eqn array of four equation coefficients: eqn [0] contains c, eqn [1] contains b, eqn [2] contains a, and eqn [3] contains d. A return value of -1 identifies a constant equation ( b, a, and d are 0). Otherwise, this value represents the number of noncomplex roots (values of x that make the equation evaluate to 0). If a second res array is passed, roots are stored in this array. Otherwise, they are stored in eqn.

Ellipses

The Ellipse2D class (located in the java.awt.geom package) is used to create ellipses or circles. A circle is drawn when the width and height of the bounding box that contains this shape are equal. The following code fragment creates an ellipse:

 Ellipse ellipse = new Ellipse.Double (0.0, 50.0, 100.0, 100.0); 

This ellipse represents a circle drawn in a bounding box whose upper-left corner is located at (0.0, 50.0) and dimensions are (100.0, 100.0). Listing 18.17 presents source code to a ShapeDemo3 applet that demonstrates drawing ellipses and filling circles.

Listing 18.17 The ShapeDemo3 Applet Source Code
 // ShapeDemo3.java import java.awt.*; import java.awt.geom.*; public class ShapeDemo3 extends java.applet.Applet {    public void paint (Graphics g)    {       Graphics2D g2 = (Graphics2D) g;       int w = getSize ().width - 1;       int h = getSize ().height - 1;       Ellipse2D ellipse = new Ellipse2D.Double (0.0, 0.0, w, h);       g2.draw (ellipse);       ellipse = new Ellipse2D.Double (w / 2, h / 2, w / 4, w / 4);       g2.fill (ellipse);    } } 

ShapeDemo3 draws a filled circle inside an ellipse's outline, when the applet's width and height are different. Otherwise, a filled circle is drawn inside the outline of another circle. The reason for subtracting 1 from the width and height is to ensure that the right-most column and bottom-most row of pixels that belong to the ellipse are visible. (The ellipse is drawn in a bounding box from (0.0, 0.0) to (0.0 + w, 0.0 + h), and the applet's coordinates range from (0.0, 0.0) to (0.0 + w “ 1, 0.0 + h “ 1).) Figure 18.15 shows the ellipse and circle.

Figure 18.15. Ellipse2D is used to create an outlined ellipse and a filled circle.

graphics/18fig15.gif

General Paths

The GeneralPath class (located in the java.awt.geom package) is used to create arbitrary shapes. After you've created a GeneralPath object, you create a shape by calling its moveTo, lineTo ”and even curveTo and quadTo (for drawing cubic and quadratic curves, respectively) ”methods. The following code fragment creates a general path that defines a triangle:

 GeneralPath gp = new GeneralPath (); gp.moveTo (100.0, 100.0); gp.lineTo (200.0, 0.0); gp.lineTo (300.0, 100.0); gp.lineTo (100.0, 100.0); 

Listing 18.18 presents source code to a ShapeDemo4 applet that demonstrates general paths.

Listing 18.18 The ShapeDemo4 Applet Source Code
 // ShapeDemo4.java import java.awt.*; import java.awt.geom.*; public class ShapeDemo4 extends java.applet.Applet {    public void paint (Graphics g)    {       Graphics2D g2 = (Graphics2D) g;       int w = getSize ().width - 1;       int h = getSize ().height - 1;       GeneralPath gp = new GeneralPath ();       gp.moveTo (0.0f, h);       gp.lineTo (w / 2, 0.0f);       gp.lineTo (w, h);       gp.lineTo (0.0f, h);       g2.draw (gp);       g2.scale (0.25, 0.25);       g2.translate (50.0, 50.0);       g2.fill (gp);    } } 

ShapeDemo4 uses a general path to define a triangle, whose outline is subsequently drawn. After scaling and translation transforms are chained to the default transform, a smaller filled version of this triangle is drawn. These triangles are shown in Figure 18.16.

Figure 18.16. Triangles and other shapes can be drawn using a general path.

graphics/18fig16.gif

Lines

The Line2D class (located in the java.awt.geom package) is used to create lines. This is illustrated in the following code fragment, which specifies a diagonal line:

 Line2D l = new Line2D.Float (0.0f, 0.0f, 20.0f, 20.0f); 

The line is drawn from (0.0, 0.0) to (20.0, 20.0). Listing 18.19 presents source code to a ShapeDemo5 applet that demonstrates lines.

Listing 18.19 The ShapeDemo5 Applet Source Code
 // ShapeDemo5.java import java.awt.*; import java.awt.geom.*; public class ShapeDemo5 extends java.applet.Applet {    public void paint (Graphics g)    {       Graphics2D g2 = (Graphics2D) g;       int w = getSize ().width - 1;       int h = getSize ().height - 1;       for (int i = 0; i < 12; i++)       {            double angle = Math.PI / 2 - i * Math.PI / 6;            double x = Math.cos (angle);            double y = Math.sin (angle);            Line2D l = new Line2D.Double (100 + 55.0 * x,                                          100 - 55.0 * y,                                          100 + 65.0 * x,                                          100 - 65.0 * y);            g2.draw (l);       }    } } 

ShapeDemo5 uses lines to define the tick marks on a clock face. This is shown in Figure 18.17.

Figure 18.17. Lines created with Line2D can be used to draw the tick marks on a clock face.

graphics/18fig17.gif

Points and Dimensions

Java 2D's Point2D and Dimension2D classes are used to create objects representing user space points and dimensions. You would normally use these classes in programs that treat all graphics as a series of objects, instead of pixels. For example, Point2D can be used to keep track of the points representing a line's endpoints. This is shown in the following code fragment:

 Line2D l = new Line2D.Double (new Point2D.Double (10.0, 10.0),                               new Point2D.Double (20.0, 30.0)); 
Quadratic Curves

The QuadCurve2D class (located in the java.awt.geom package) is used to create quadratic curves. Each quadratic curve is constructed from a start point, an endpoint, and one control point. The following code fragment creates a quadratic curve:

 QuadCuve2D quad = new QuadCurve2D.Double (0.0, 50.0, 50.0, 25.0, 100.0, 50.0); 

The start point is located at (0.0, 50.0) and the endpoint is located at (100.0, 50.0). The control point is located at (50.0, 25.0). Listing 18.20 presents source code to a ShapeDemo6 applet that demonstrates drawing a quadratic curve.

Listing 18.20 The ShapeDemo6 Applet Source Code
 // ShapeDemo6.java import java.awt.*; import java.awt.geom.*; public class ShapeDemo6 extends java.applet.Applet {    public void paint (Graphics g)    {       int w = getSize ().width;       int h = getSize ().height;       QuadCurve2D quad = new QuadCurve2D.Double (w / 2 - 50,                                                  h / 2,                                                  w / 2 + 25,                                                  h / 2 + 25,                                                  w / 2 + 50,                                                  h / 2);       // Draw control point.       g.drawLine (w / 2 + 25, h / 2 + 25, w / 2 + 25, h / 2 + 25);       // Draw the curve.       Graphics2D g2 = (Graphics2D) g;       g2.draw (quad);    } } 

ShapeDemo6 draws the control point in addition to the curve. The result is shown in Figure 18.18.

Figure 18.18. A quadratic curve's appearance is governed by a single control point.

graphics/18fig18.gif

Tip

If you are looking for an interactive program that allows you to move the start points, endpoints, and control points of a quadratic curve, check out the 2D Graphics trail in the Java Tutorial (http://www.javasoft.com/tutorial).


Version 1.3 of Java 2 introduces two new QuadCurve2D methods for solving quadratic equations ”equations of the form ax^2 + bx + c = 0. The solveQuadratic (double [] eqn) and solveQuadratic (double [] eqn, double [] res) methods take an eqn array of three equation coefficients: eqn [0] contains c, eqn [1] contains b, and eqn [2] contains a. A return value of -1 identifies a constant equation ( b and a are 0). Otherwise, this value represents the number of noncomplex roots. If a second res array is passed, roots are stored in this array. Otherwise, they are stored in eqn.

Rectangles

The Rectangle2D class (located in the java.awt.geom package) is used to create rectangles. The following code fragment creates a rectangle:

 Rectangle2D r = new Rectangle2D.Float (0.0f, 0.0f, 20.0f, 20.0f); 

The upper-left corner is located at (0.0, 0.0) and the dimensions are (20.0, 20.0). Listing 18.21 presents source code to a ShapeDemo7 applet that demonstrates rectangles.

Listing 18.21 The ShapeDemo7 Applet Source Code
 // ShapeDemo7.java import java.awt.*; import java.awt.geom.*; public class ShapeDemo7 extends java.applet.Applet {    public void paint (Graphics g)    {       Graphics2D g2 = (Graphics2D) g;       GradientPaint gp = new GradientPaint (20.0f, 20.0f, Color.blue,                                             80.0f, 80.0f, Color.green);       g2.setPaint (gp);       Rectangle2D r = new Rectangle2D.Double (20.0, 20.0, 60.0, 60.0);       g2.fill (r);       gp = new GradientPaint (100.0f, 20.0f, Color.blue,                               250.0f, 20.0f, Color.green);       g2.setPaint (gp);       r = new Rectangle2D.Double (100.0, 20.0, 150.0, 60.0);       g2.fill (r);       gp = new GradientPaint (20.0f, 100.0f, Color.blue,                               20.0f, 200.0f, Color.green);       g2.setPaint (gp);       r = new Rectangle2D.Double (20.0, 100.0, 230.0, 100.0);       g2.fill (r);    } } 

ShapeDemo7 draws three filled rectangles, with each rectangle being filled with a different gradient paint style. Figure 18.19 shows the resulting rectangles.

Figure 18.19. Rectangle2D is used to create three rectangles, which are subsequently painted with gradient paint styles.

graphics/18fig19.gif

Round Rectangles

The RoundRectangle2D class (located in the java.awt.geom package) is used to create rectangles with rounded corners. The following code fragment creates a rounded rectangle:

 RoundRectangle2D r = new RoundRectangle2D.Double (0.0, 0.0, 20.0, 20.0,                                                   5.0, 5.0); 

The rectangle is drawn relative to (0.0, 0.0), its dimensions are (20.0, 20.0), and its corner arc width/arc height is specified as (5.0, 5.0). Listing 18.22 presents source code to a ShapeDemo8 applet that demonstrates rounded rectangles.

Listing 18.22 The ShapeDemo8 Applet Source Code
 // ShapeDemo8.java import java.awt.*; import java.awt.geom.*; public class ShapeDemo8 extends java.applet.Applet {    public void paint (Graphics g)    {       Graphics2D g2 = (Graphics2D) g;       RoundRectangle2D r = new RoundRectangle2D.Double (10.0, 10.0,                                                         50.0, 50.0,                                                         5.0, 5.0);       g2.draw (r);       g.setColor (Color.magenta);       r = new RoundRectangle2D.Double (80.0, 10.0, 80.0, 100.0,                                        20.0, 20.0);       g2.fill (r);       GradientPaint gp = new GradientPaint (180.0f, 10.0f, Color.orange,                                             270.0f, 110.0f, Color.blue);       g2.setPaint (gp);       r = new RoundRectangle2D.Double (180.0, 10.0, 90.0, 100.0,                                        40.0, 60.0);       g2.fill (r);    } } 

ShapeDemo8 draws three rounded rectangles with different- sized corners. These rectangles are shown in Figure 18.20.

Figure 18.20. Rounded rectangles can have different-sized rounded corners.

graphics/18fig20.gif

Constructive Area Geometry

Constructive area geometry (CAG) is the process of creating new geometric shapes out of existing geometric shapes, by performing Boolean operations on these shapes. Operations include Boolean OR (union ”the new shape consists of all pixels in the two original shapes), Boolean AND (intersection ”the new shape only consists of overlapping pixels), Boolean NOT (subtraction ”the new shape only consists of those pixels in one shape that are not in the other shape), and Boolean XOR (exclusive or ”the new shape consists only of nonoverlapping pixels).

Java 2D's Area class (located in the java.awt.geom package) is used to perform CAG. You create an area by calling Area (Shape shape). The shape argument is a reference to an object whose class implements the Shape interface. This is demonstrated by the following code fragment:

 Ellipse2D e = new Ellipse2D.Double (10.0, 10.0, 60.0, 60.0); Area a1 = new Area (e); 

Before you can perform a Boolean operation, you need to construct another area, as demonstrated by the following code fragment:

 e = new Ellipse2D.Double (10.0, 50.0, 30.0, 30.0); Area a2 = new Area (e); 

When it comes time to perform a CAG operation, call one of Area 's CAG methods. The following code fragment calls Area 's add method to perform a union:

 a1.add (a2); 

The resulting shape is now the union of two ellipses. This shape can be drawn with the draw method, as the following code demonstrates:

 Graphics2D g2 = (Graphics2D) g; g2.draw (a1); 

Listing 18.23 presents source code to a CAGDemo applet that shows how to accomplish various CAG operations.

Listing 18.23 The CAGDemo Applet Source Code
 // CAGDemo.html import java.awt.*; import java.awt.geom.*; public class CAGDemo extends java.applet.Applet {    public void paint (Graphics g)    {       Graphics2D g2 = (Graphics2D) g;       Ellipse2D e1 = new Ellipse2D.Double (20.0, 20.0, 80.0, 70.0);       g2.setColor (Color.red);       g2.fill (e1);       Ellipse2D e2 = new Ellipse2D.Double (20.0, 70.0, 40.0, 40.0);       g2.setColor (Color.blue);       g2.fill (e2);       g2.setColor (Color.black);       g2.drawString ("Original", 20, 140);       g2.translate (110.0, 0.0);       // Perform union.       Area a1 = new Area (e1);       Area a2 = new Area (e2);       a1.add (a2);       g2.setColor (Color.orange);       g2.fill (a1);       g2.setColor (Color.black);       g2.drawString ("Union", 20, 140);       g2.translate (110.0, 0.0);       // Perform intersection.       a1 = new Area (e1);       a1.intersect (a2);       g2.setColor (Color.magenta);       g2.fill (a1);       g2.setColor (Color.black);       g2.drawString ("Intersection", 20, 140);       g2.translate (110.0, 0.0);       // Perform subtraction.       a1 = new Area (e1);       a1.subtract (a2);       g2.setColor (Color.gray);       g2.fill (a1);       g2.setColor (Color.black);       g2.drawString ("Subtraction", 20, 140);       g2.translate (110.0, 0.0);       // Perform exclusive or.       a1 = new Area (e1);       a1.exclusiveOr (a2);       g2.setColor (Color.green);       g2.fill (a1);       g2.setColor (Color.black);       g2.drawString ("Exclusive Or", 20, 140);    } } 

CAGDemo creates two ellipses and draws them without using CAG. Then, Area 's add, intersect, subtract, and exclusiveOr methods are called to demonstrate CAG operations. The result is shown in Figure 18.21.

Figure 18.21. CAG operations can be performed on all kinds of shapes, including ellipses.

graphics/18fig21.gif

Bounds and Hit Testing

A bounding box is a rectangle that fully encloses a shape's geometry. Bounding boxes are used to determine whether an object has been selected or "hit" by a user.

The Shape interface declares two methods for retrieving a shape's bounding box: getBounds and getBounds2D. (The getBounds2D method returns a Rectangle2D object reference instead of a Rectangle object reference, providing a higher-precision description of the shape's bounding box.)

Shape 's contains method is used to determine if a specified point lies in the bounds of the shape. In contrast, its intersects method determines if a specified rectangle intersects the shape. For more information on these methods, check out the 2D Graphics trail's hit testing section in the Java Tutorial (http://www.javasoft.com/tutorial).

Text

Java 2D offers improvements in the areas of text and fonts. For example, a line of text can be measured by using the LineBreakMeasurer class, and this text can be laid out by using the TextLayout class. Furthermore, a font's attributes (such as its name , size , transform, weight, and posture ) can be obtained from its getAttributes method, and font measurement information (such as ascent, descent, and leading) can be obtained from a font's LineMetrics object. (A LineMetrics object is retrieved by calling one of Font 's getLineMetrics methods.)

Attributed Strings

Java 2D makes it possible to combine a string of characters with attributes (font size, strikethrough , swap colors, and so on) that describe the appearance of that string. The resulting string is known as an attributed string.

Tip

Attributed strings are useful in word processing programs that display formatted text.


The AttributedString class (located in the java.text package) creates objects that represent attributed strings. This class provides several constructors, including AttributedString (String s, Map m). The s argument identifies a string of characters that will be associated with attributes and the m argument identifies an object whose class implements the Map interface (such as Hashtable ), which contains these attributes. (Each Map entry represents a separate attribute.) To find out how to create an attributed string, take a look at the following code fragment:

 String s = "The LineBreakMeasurer class allows styled " +                  "text to be broken into lines (or segments) " +                  "that fit within a particular visual " +                  "advance.  This is useful for clients who " +                  "wish to display a paragraph of text that " +                  "fits within a specific width, called the " +                  "wrapping width."; Hashtable map = new Hashtable (); map.put (TextAttribute.SIZE, new Float (18.0f)); map.put (TextAttribute.SWAP_COLORS, TextAttribute.SWAP_COLORS_ON); map.put (TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_LOW_DASHED); AttributedString as = new AttributedString (s, map); 

Attributes have names and values represented by constants in the TextAttribute class (located in the java.awt.font package). The SIZE attribute identifies the size of the string, SWAP_COLORS swaps foreground and background colors when displaying the string, and UNDERLINE identifies either a solid or a dashed underline to appear under the string's characters.

When an attributed string is created, attributes are applied to all characters in that string. However, it's possible to limit attributes to a select range of characters by calling AttributedString 's addAttributes (MAP map, int beginIndex, int endIndex) method. Only those attributes specified by map will be applied to characters ranging from beginIndex to endIndex - 1. This is demonstrated by the following code fragment:

 map = new Hashtable (); map.put (TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE); as.addAttributes (map, 4, 21); 

The POSTURE attribute is used to select a regular or italic font face. It is applied to characters located at indexes 0 through 20 (inclusive).

After an attributed string has been created, call its getIterator methods to return an AttributedCharacterIterator object that can access any character in the string. The following code fragment shows how this is done:

 AttributedCharacterIterator aci = as.getIterator (); 

The resulting iterator's methods can be called to retrieve characters and attributes. However, the real power of this iterator is shown when passed to a LineBreakMeasurer object ”to break this text into lines. A line break measurer is constructed in the following code fragment:

 LineBreakMeasurer measurer; measurer = new LineBreakMeasurer (aci, new FontRenderContext (null, false,                                   false)); 

In addition to an AttributedCharacterIterator argument, the line break measurer requires a FontRenderContext argument. This latter argument contains information about a graphics device that is needed to correctly measure the text. (Text measurements can vary slightly depending on the device resolution, and attributes such as antialiasing.)

At this point, the attributed string can be drawn and measured so that only complete words appear on each line, as demonstrated by the following code fragment:

 int startIndex = aci.getBeginIndex (); int endIndex = aci.getEndIndex (); measurer.setPosition (startIndex); float wrappingWidth = (float) size.width; float Y = 0.0f; while (measurer.getPosition () < endIndex) {    TextLayout layout = measurer.nextLayout (wrappingWidth);    Y += layout.getAscent ();    float X = 0.0f;    if (!layout.isLeftToRight ())        X = wrappingWidth - layout.getAdvance ();    layout.draw ((Graphics2D) g, X, Y);    Y += layout.getDescent () + layout.getLeading (); } 

As you can see, the line break measurer returns a text layout to provide font measurement information for each line of text to be drawn. Furthermore, the text layout's draw method is called to render each line of text.

One item that might not be obvious is the call to the text layout's isLeftToRight method. This method is called to handle bidirectional text. For English and other written languages that support left-to-right text, text is typically left-justified (and isLeftToRight returns a Boolean true value). However, other written languages (such as Arabic) right-justify text.

Troubleshooting Tip

If you're having trouble figuring out how to justify text, see "Justifying Text" in the "Troubleshooting" section at the end of this chapter.


For information and examples of these classes, consult both the Java Tutorial (http://www.javasoft.com/tutorial) and the Java 2D Guide ”part of the SDK 1.3 documentation.

Caution

When run under Windows 98, the previous code generates intermittent exception access violations. The error message indicates that these violations are detected in native code outside of the JVM. If the problem is not Windows- related , it's resulting from one of the JVM support DLLs.


Transforming Strings

In addition to the two drawString methods in the Graphics class, there are four drawString methods in Graphics2D. These new methods support floating-point coordinates. Whether you are calling the Graphics drawString methods or Graphics2D drawString methods, the current transform is applied. This means that, among other things, it's now possible to rotate text. Listing 18.24 presents source code to the TextDemo applet that shows how this is done.

Listing 18.24 The TextDemo Applet Source Code
 // TextDemo.java import java.awt.*; public class TextDemo extends java.applet.Applet {    public void paint (Graphics g)    {       Graphics2D g2 = (Graphics2D) g;       g.drawString ("Non-rotated text", 50, 50);       // The following rotate method call concatenates three transforms       // to the default transform. The first transform translates       // subsequent points to the coordinate system's origin by       // subtracting 50 from the x coordinate and 60 from the y       // coordinate. (The assumption is that (50.0, 60.0) is the       // origin used by subsequent graphics methods.) Then, these       // points are rotated around the coordinate system's origin,       // not (50.0, 60.0). Finally, each point is translated back to       // the (50.0, 60.0) origin.       g2.rotate (45.0 * Math.PI / 180, 50.0, 60.0);       // You must still pass (50.0, 60.0) as the origin of the text.       g2.drawString ("Rotated text", 50.0f, 60.0f);    } } 

TextDemo 's output is shown in Figure 18.22.

Figure 18.22. Java 2D makes it possible to rotate text.

graphics/18fig22.gif

Buffered Images

Java 2D introduces an immediate-mode imaging model that gives you the ability to manipulate and display pixel-mapped images whose data is stored in memory. This data is accessible in a variety of formats, and manipulated by using several types of filtering operations.

Version 1.3 of the Java 2 SDK adds support for PNG (Portable Network Graphics) images ”in addition to GIF and JPEG support ”to Java 2D's imaging model. (PNG was created as a nonproprietary replacement for GIF ”which is proprietary. The PNG format is flexible and extensible.)

The imaging model is based on the BufferedImage class. This class manages an image in memory and provides all necessary methods for interpreting, storing, and rendering pixel data. A BufferedImage object's contents can be rendered, either through a Graphics context or through a Graphics2D context.

At its core , a BufferedImage object is an encapsulation of a Raster object that holds pixel data, and a ColorModel subclass object that holds color information. Furthermore, the Raster object contains a DataBuffer object that stores pixel values, and a SampleModel object that describes how to locate a pixel value in the data buffer. Figure 18.23 illustrates this architecture.

Figure 18.23. A buffered image is composed of a raster and a color model.

graphics/18fig23.gif

A BufferedImage object can be created by calling BufferedImage (int width, int height, int type). The width argument specifies the width of the raster, the height argument specifies its height, and the type argument specifies the type of image. You can choose one of the constants in the BufferedImage class (such as TYPE_INT_RGB ) for this argument. The following code fragment creates a buffered image:

 BufferedImage bi = new BufferedImage (100, 100, BufferedImage.TYPE_INT_RGB); 

The code fragment creates a buffered image that holds 100 by 100 pixel images. Furthermore, TYPE_INT_RGB specifies that the image has 8-bit RGB color components packed into integer pixels, along with a direct color model that doesn't use alpha components .

After a buffered image has been created, it's empty. You need to populate this image with colored pixels. One way to accomplish this task is to call BufferedImage 's setRGB methods. However, a more common technique is to draw an Image subclass object's pixels into the buffered image, as the following code fragment demonstrates:

 Image im = getImage (getDocumentBase (), "someimage.gif"); MediaTracker mt = new MediaTracker (this); mt.addImage (im, 0); try {    mt.waitForID (0); } catch (InterruptedException e) { } BufferedImage bi = new BufferedImage (im.getWidth (this), im.getHeight (this), graphics/ccc.gif BufferedImage.TYPE_INT_RGB); Graphics bg = bi.createGraphics (); bg.drawImage (im, 0, 0, this); 

If you plan to draw an image into a buffered image, you will need to specify the image's width and height as the dimensions of this buffer. You then need to obtain a graphics context associated with the buffer. After you have this context, you can call one of its drawImage methods to transfer the pixels to the buffer.

Image Processing

Image processing has been popularized by Adobe Photoshop and other products, because these products simplify the processing of images. Although Java can also process images, its traditional image processing model (as described in Chapter 13, "Beginning AWT") is not so easy to use. Therefore, buffered images provide image-processing capabilities that you'll undoubtedly prefer.

Embossing is a traditional image processing technique that gives an image a metallic look. This technique works as follows : An image is like a mountain. Each pixel represents an elevation, and brighter pixels are considered to be located at higher elevations . When an imaginary light source shines on this "mountain," the "uphills" facing this light source are lit, whereas the "downhills" facing away from the light source are shaded. A light ray can be simulated by scanning source pixels in a specific direction (such as left to right). When the "ray moves uphill ," a pixel's color is brightened by the change in elevation from the previous pixel. An embossed image is shown in Figure 18.24.

Figure 18.24. Embossing gives images a metallic look.

graphics/18fig24.gif

To emboss, you start with a completely gray image. For each source image pixel, you examine the pixels to the upper left and lower right, and figure out the maximum change in their red, green, and blue color components. (This change can be either positive or negative.) For example, if green has changed by “5, blue has changed by 10, and red has changed by “100, the maximum change is “100. (The red component changed the most.) Now, you add the amount of change to 128 (the gray level) and create a pixel in the destination image with red, green, and blue values equal to this level. (This level is adjusted so that it isn't less than 0 or greater than 255.)

Listing 18.25 presents source code to an emboss method that takes a buffered image source (containing an image) as its argument and creates a new buffered image destination containing an embossed version of the source image. (One of the setRGB methods is called to set the pixel values in the destination image.)

Listing 18.25 The emboss Method
 public BufferedImage emboss (BufferedImage src) {    int width = src.getWidth ();    int height = src.getHeight ();    BufferedImage dst;    dst = new BufferedImage (width, height,                             BufferedImage.TYPE_INT_RGB);    for (int i = 0; i < height; i++)         for (int j = 0; j < width; j++)         {              int upperLeft = 0;              int lowerRight = 0;              if (i > 0 && j > 0)                  upperLeft = src.getRGB (j - 1, i - 1);              if (i < height - 1 && j < width - 1)                  lowerRight = src.getRGB (j + 1, i + 1);              int redDiff = ((lowerRight >> 16) & 255) -                            ((upperLeft >> 16) & 255);              int greenDiff = ((lowerRight >> 8) & 255) -                              ((upperLeft >> 8) & 255);              int blueDiff = (lowerRight & 255) -                             (upperLeft & 255);              int diff = redDiff;              if (Math.abs (greenDiff) > Math.abs (diff))                  diff = greenDiff;              if (Math.abs (blueDiff) > Math.abs (diff))                  diff = blueDiff;              int grayColor = 128 + diff;              if (grayColor > 255) grayColor = 255;              else              if (grayColor < 0) grayColor = 0;              int newColor = (grayColor << 16) + (grayColor << 8)                             + grayColor;              dst.setRGB (j, i, newColor);         }    return dst; } 

In many cases, image processing is simplified by using existing image operations, known as buffered image ops. These ops are objects created from classes that implement the BufferedImageOp interface. Classes include AffineTransformOp, BandCombineOp, ColorConvertOp, ConvolveOp, LookupOp, and RescaleOp. For space reasons, only ConvolveOp and LookupOp will be explored.

Note

The following sections demonstrate various image-processing operations. The code fragments are taken from a large image-processing program called BufferedImageDemo1. The code for this program is included with the rest of this book's source code.


Convolutions

A convolution is an image-processing operation that combines the color of a source pixel with the colors of its immediate neighbors. The resulting color is assigned to the destination pixel. This operation is carried out by using a linear operator that determines what fraction of each source pixel's color contributes to the color of the destination pixel. This operator is known as a kernel.

A kernel is like a template moved across an image to perform a convolution on each pixel. The center of the kernel overlays the source pixel being convoluted, whereas kernel values surrounding this center are applied to neighboring pixels.

An identity kernel has no effect on a source pixel. The source pixel's color is multiplied by 1.0 and each neighboring pixel's color is multiplied by 0.0. The results are added together and the destination pixel's color is the same as the source pixel's color. The identity kernel can be specified by using the following 3 3 matrix of floating point values:

 001 0.0   0.0   0.0 002 0.0   1.0   0.0 003 0.0   0.0   0.0 004 

The ConvolveOp class creates objects that represent various kinds of convolutions. To create a ConvolveOp object, call the ConvolveOp (Kernel kernel) constructor with a kernel argument that identifies the kernel used to perform the convolution.

The Kernel class creates objects that represent kernels . Call Kernel (int width, int height, float [] matrix) to create a kernel. The width argument identifies the number of columns and the height argument identifies the number of rows making up the kernel's matrix, whereas the matrix argument identifies this matrix.

The following code fragment creates an identity kernel and a ConvolveOp object that is handed this kernel in its constructor. (Because the identity kernel is used, this convolution does not change source pixels.)

 float [] identityKernel = {    0.0f, 0.0f, 0.0f,    0.0f, 1.0f, 0.0f,    0.0f, 0.0f, 0.0f } ; BufferedImageOp identityOp =    new ConvolveOp (new Kernel (3, 3, identityKernel)); 

To perform a convolution, ConvolveOp 's filter method must be called. This method takes two arguments ”source and destination buffered images ”and returns the destination image. (If you pass null as the destination image, filter creates a new buffered image to serve as the destination.) The following code fragment demonstrates how you would call filter on the buffered image that's identified as bi. (The identityOp object is assumed.)

 BufferedImage clone = identityOp.filter (bi, null); 

Blurring is a convolution in which equal amounts of a source pixel's color and its neighbors'colors are added together. For a 3 3 matrix, each value would be divided by 9 because there are 9 elements in the matrix. The blurring kernel can be specified by using the following 3 3 matrix:

 001 1.0 / 9.0   1.0 / 9.0   1.0 / 9.0 002 1.0 / 9.0   1.0 / 9.0   1.0 / 9.0 003 1.0 / 9.0   1.0 / 9.0   1.0 / 9.0 004 

Notice that the sum of these values adds up to 1.0. This is quite common with kernels. If the sum does not equal 1.0, the image is either noticeably brightened or darkened.

The following code fragment demonstrates setting up a blurring kernel and performing a blurring operation on the buffered image that's identified as bi :

 float ninth = 1.0f / 9.0f; float [] blurKernel = {    ninth, ninth, ninth,    ninth, ninth, ninth,    ninth, ninth, ninth } ; BufferedImageOp blurOp =    new ConvolveOp (new Kernel (3, 3, blurKernel)); BufferedImage clone = blurOp.filter (bi, null); 

The blurring kernel works as follows: Imagine operating in an area that consists of a single color. Each pixel will keep its own color. Now imagine operating in an area of color change. Because neighboring pixels contribute equal amounts of their color, a gradual blending of the source pixel's color and neighboring pixel colors results in a source pixel color that is close to the average of its neighboring pixel colors. This blending results in a blur. Figure 18.25 shows a blurred image.

Figure 18.25. A blurring kernel is used to create a blurred image.

graphics/18fig25.gif

Edge detection is a convolution in which edges are detected by subtracting neighboring pixel colors from a source pixel's color. Its kernel uses the following matrix. (This is an example in which kernel values do not add up to 1.0. The result is a much darker image.)

 0.0   1.0    0.0 1.0    4.0   1.0  0.0   1.0    0.0 

The following code fragment demonstrates setting up an edge detection kernel and performing an edge detection operation on the buffered image that's identified as bi :

 float [] edgeKernel = {     0.0f, 1.0f,  0.0f,    1.0f,  4.0f, 1.0f,     0.0f, 1.0f,  0.0f } ; BufferedImageOp edgeDetectionOp =    new ConvolveOp (new Kernel (3, 3, edgeKernel)); BufferedImage clone = edgeDetectionOp.filter (bi, null); 

The edge detection kernel works as follows: Imagine operating in an area that consists of a single color. Each pixel will end up as a black pixel because the color of the source pixel being convoluted is multiplied by 4.0, whereas the color of each of its neighboring pixels is multiplied by -1.0. Because each neighbor pixel has the same color as the source pixel, the result of the multiplication by 1.0 is one quarter the result of the multiplication by 4.0. When you add all these values together, the result is 0.0 (or black). (The four neighbor pixels that are assigned 0.0 kernel values contribute no color because multiplying by 0.0 results in 0.0, and adding 0.0 to the result doesn't change a thing.) Figure 18.26 shows an edge-detected image.

Figure 18.26. An edge detection kernel emphasizes an image's edges.

graphics/18fig26.gif

Sharpening is a convolution that is the inverse of blurring. If you replace the 4.0 value in the edge kernel with a value of 5.0, you can achieve sharpening. Sharpening works by de-emphasizing the contributions of neighboring pixels. As a result, each source pixel in an area of change stands out from its neighbors. (Of course, in a region of constant color, you wouldn't notice a sharpening effect.)

Lookup Tables

Several kinds of image processing operations (such as inverting, thresholding , and posterizing) can be performed by translating source pixel colors to destination pixel colors through the use of lookup tables. Although you can define separate tables for each of the color components in an RGB color, you will probably only need a single table to process all three components at once. Because each component is represented by an 8-bit value ranging from 0 through 255, a lookup table only needs 256 entries.

The LookupOp class creates objects that represent various kinds of lookup operations. To create a LookupOp object, call the LookupOp (LookupTable table, RenderingHints hints) constructor with a table argument that identifies the table of lookup values and a hints argument that identifies rendering hints to improve quality. (You can pass null as the value of hints. )

LookupTable is an abstract class that identifies a lookup table. When working with RGB images, you will normally create objects from its ShortLookupTable subclass.

By swapping color numbers with their extreme opposites, you can achieve inverting. This effect is demonstrated by the following code fragment:

 short [] invert = new short [256]; for (int i = 0; i < invert.length; i++)      invert [i] = (short) (255 - i); BufferedImageOp invertOp =    new LookupOp (new ShortLookupTable (0, invert), null); BufferedImage clone = invertOp.filter (bi, null); 

To create a negative, a pixel's color number is replaced by its opposite color number. This is achieved by subtracting the color number from 255. Figure 18.27 shows the result of inverting.

Figure 18.27. An inverted image results from swapping color numbers with their extreme opposites.

graphics/18fig27.gif

The process of making obvious color changes across developer-defined boundaries is known as thresholding. This technique uses a threshold value, minimum value, and maximum value to control the color component values for each source pixel. Component values below the threshold are assigned the minimum value. Values equal to or above the threshold are assigned the maximum value. The following code fragment demonstrates this effect:

 short [] threshold = new short [256]; for (int i = 0; i < threshold.length; i++)      threshold [i] = (i < 128) ? (short) 0 : (short) 255; BufferedImageOp thresholdOp =    new LookupOp (new ShortLookupTable (0, threshold), null); 

All color component values less than 128 are mapped to color number 0, and all color component values greater than or equal to 128 are mapped to color number 255. The result is an image with eight colors: black (0, 0, 0), blue (0, 0, 255), green (0, 255, 0), cyan (0, 255, 255), red (255, 0, 0), magenta (255, 0, 255), yellow (255, 255, 0), and white (255, 255, 255). Figure 18.28 shows the result of thresholding.

Figure 18.28. Thresholding uses a threshold value to reduce the number of colors in an image.

graphics/18fig28.gif

By reducing the number of colors in an image, you can achieve an effect known as posterizing. This is demonstrated by the following code fragment, which posterizes an image by mapping 256 colors down to 8:

 short [] posterize = new short [256]; for (int i = 0; i < posterize.length; i++)      posterize [i] = (short) (i - i % 32); BufferedImageOp posterizeOp =    new LookupOp (new ShortLookupTable (0, posterize), null); BufferedImage clone = posterizeOp.filter (bi, null); 

This code might not seem very intuitive. However, when you work your way through the expression i - i % 32, you discover that all color numbers are mapped to color numbers 0, 32, 64, 96, 128, 160, 192, and 224. (There are only eight resulting colors.) The result of posterizing is shown in Figure 18.29.

Figure 18.29. Posterizing an image results in color reduction. Although posterizing and thresholding are similar color reduction techniques, a posterized image looks more natural than a thresholded image.

graphics/18fig29.gif

Textured Painting

Earlier, you were introduced to the gradient paint style. A second paint style (texture paint) was mentioned but not explored because it works in partnership with buffered images. Basically, a texture paint style is used by pens that paint with images, instead of colors.

A texture paint style is represented by an object created from the TexturePaint class (located in the java.awt package). Call the TexturePaint (BufferedImage bi, Rectangle2D rect) constructor to create a texture paint that uses the image contained in bi as its texture and the rectangle specified by rect as the dimensions of a rectangle in which the texture appears. The following code fragment demonstrates creating a texture paint object:

 TexturePaint tp = new TexturePaint (bi, new Rectangle2D.Double (0.0, 0.0,                                     100.0, 100.0)); 

After you've created a TexturePaint object, you can specify this object as the new paint attribute for the graphics context. You do this by calling the setPaint method, as shown in the following code fragment:

 Graphics2D g2 = (Graphics) g; g2.setPaint (tp); 

The best way to understand texture paint is to examine a program that demonstrates this attribute. Listing 18.26 presents source code to a BufferedImageDemo2 applet that does just this.

Listing 18.26 The BufferedImageDemo2 Applet Source Code
 // BufferedImageDemo2.java import java.awt.*; import java.awt.geom.*; import java.awt.image.*; public class BufferedImageDemo2 extends java.applet.Applet {    public void paint (Graphics g)    {       Graphics2D g2 = (Graphics2D) g;       RenderingHints rh = g2.getRenderingHints ();       rh.put (RenderingHints.KEY_ANTIALIASING,               RenderingHints.VALUE_ANTIALIAS_ON);       g2.setRenderingHints (rh);       GeneralPath path = new GeneralPath ();       path.moveTo (60.0f, 0.0f);       path.lineTo (50.0f, 300.0f);       path.curveTo (160.0f, 230.0f, 270.0f, 140.0f, 400.0f, 100.0f);       Image im = getImage (getDocumentBase (), "corvette.jpg");       MediaTracker mt = new MediaTracker (this);       mt.addImage (im, 0);       try       {          mt.waitForID (0);       }       catch (InterruptedException e) { }       BufferedImage bi = new BufferedImage (im.getWidth (this),                                             im.getHeight (this),                                             BufferedImage.TYPE_INT_RGB);       Graphics bg = bi.createGraphics ();       bg.drawImage (im, 0, 0, this);       Rectangle2D rect = new Rectangle2D.Float (0.0f, 0.0f,                                                 im.getWidth (this),                                                 im.getHeight (this));       TexturePaint tp = new TexturePaint (bi, rect);       g2.setPaint (tp);       BasicStroke stroke;       stroke = new BasicStroke (100.0f, BasicStroke.CAP_SQUARE,                                 BasicStroke.JOIN_ROUND);       g2.setStroke (stroke);       g2.draw (path);    } } 

BufferedImageDemo2 creates a general path shape that will be drawn on the applet's drawing surface. An image is loaded and converted into a buffered image. Then, a rectangle is created that encompasses the entire buffered image. Together with the buffered image, this rectangle is used to create a texture paint style. After this style has been selected as the graphics context paint attribute, a stroke is created to achieve a pen that draws in wide strokes. Finally, the drawing is then rendered. Figure 18.30 shows the resulting image.

Figure 18.30. A pen can paint an image instead of a color.

graphics/18fig30.gif

   


Special Edition Using Java 2 Standard Edition
Special Edition Using Java 2, Standard Edition (Special Edition Using...)
ISBN: 0789724685
EAN: 2147483647
Year: 1999
Pages: 353

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net