Chapter 7. Advanced AWT

<feature><title></title> <objective>

THE RENDERING PIPELINE

</objective>
<objective>

SHAPES

</objective>
<objective>

AREAS

</objective>
<objective>

STROKES

</objective>
<objective>

PAINT

</objective>
<objective>

COORDINATE TRANSFORMATIONS

</objective>
<objective>

CLIPPING

</objective>
<objective>

TRANSPARENCY AND COMPOSITION

</objective>
<objective>

RENDERING HINTS

</objective>
<objective>

READERS AND WRITERS FOR IMAGES

</objective>
<objective>

IMAGE MANIPULATION

</objective>
<objective>

PRINTING

</objective>
<objective>

THE CLIPBOARD

</objective>
<objective>

DRAG AND DROP

</objective>
<objective>

PLATFORM INTEGRATION

</objective>
</feature>

You can use the methods of the Graphics class to create simple drawings. Those methods are sufficient for simple applets and applications, but they fall short when you create complex shapes or when you require complete control over the appearance of the graphics. The Java 2D API is a more sophisticated class library that you can use to produce high-quality drawings. In this chapter, we give you an overview of that API.

We then turn to the topic of printing and show how you can implement printing capabilities into your programs.

Finally, we cover two techniques for transferring data between programs: the system clipboard and the drag-and-drop mechanism. You can use these techniques to transfer data between two Java applications or between a Java application and a native program.

The Rendering Pipeline

The original JDK 1.0 had a very simple mechanism for drawing shapes. You selected color and paint mode, and called methods of the Graphics class such as drawRect or fillOval. The Java 2D API supports many more options.

  • You can easily produce a wide variety of shapes.

  • You have control over the stroke, the pen that traces shape boundaries.

  • You can fill shapes with solid colors, varying hues, and repeating patterns.

  • You can use transformations to move, scale, rotate, or stretch shapes.

  • You can clip shapes to restrict them to arbitrary areas.

  • You can select composition rules to describe how to combine the pixels of a new shape with existing pixels.

  • You can give rendering hints to make trade-offs between speed and drawing quality.

To draw a shape, you go through the following steps:

  1. Obtain an object of the Graphics2D class. This class is a subclass of the Graphics class. Ever since Java SE 1.2, methods such as paint and paintComponent automatically receive an object of the Graphics2D class. Simply use a cast, as follows:

    public void paintComponent(Graphics g)
    {
       Graphics2D g2 = (Graphics2D) g;
       . . .
    }
  2. Use the setRenderingHints method to set rendering hints: trade-offs between speed and drawing quality.

    RenderingHints hints = . . .;
    g2.setRenderingHints(hints);
  3. Use the setStroke method to set the stroke. The stroke draws the outline of the shape. You can select the thickness and choose among solid and dotted lines.

    Stroke stroke = . . .;
    g2.setStroke(stroke);
  4. Use the setPaint method to set the paint. The paint fills areas such as the stroke path or the interior of a shape. You can create solid color paint, paint with changing hues, or tiled fill patterns.

    Paint paint = . . .;
    g2.setPaint(paint);
  5. Use the clip method to set the clipping region.

    Shape clip = . . .;
    g2.clip(clip);
  6. Use the transform method to set a transformation from user space to device space. You use transformations if it is easier for you to define your shapes in a custom coordinate system than by using pixel coordinates.

    AffineTransform transform = . . .;
    g2.transform(transform);
  7. Use the setComposite method to set a composition rule that describes how to combine the new pixels with the existing pixels.

    Composite composite = . . .;
    g2.setComposite(composite);
  8. Create a shape. The Java 2D API supplies many shape objects and methods to combine shapes.

    Shape shape = . . .;
  9. Draw or fill the shape. If you draw the shape, its outline is stroked. If you fill the shape, the interior is painted.

    g2.draw(shape);
    g2.fill(shape);

Of course, in many practical circumstances, you don’t need all these steps. There are reasonable defaults for the settings of the 2D graphics context. You would change the settings only if you want to change the defaults.

In the following sections, you will see how to describe shapes, strokes, paints, transformations, and composition rules.

The various set methods simply set the state of the 2D graphics context. They don’t cause any drawing. Similarly, when you construct Shape objects, no drawing takes place. A shape is only rendered when you call draw or fill. At that time, the new shape is computed in a rendering pipeline (see Figure 7-1).

The rendering pipeline

Figure 7-1. The rendering pipeline

In the rendering pipeline, the following steps take place to render a shape:

  1. The path of the shape is stroked.

  2. The shape is transformed.

  3. The shape is clipped. If there is no intersection between the shape and the clipping area, then the process stops.

  4. The remainder of the shape after clipping is filled.

  5. The pixels of the filled shape are composed with the existing pixels. (In Figure 7-1, the circle is part of the existing pixels, and the cup shape is superimposed over it.)

In the next section, you will see how to define shapes. Then, we turn to the 2D graphics context settings.

Shapes

Here are some of the methods in the Graphics class to draw shapes:

drawLine
drawRectangle
drawRoundRect
draw3DRect
drawPolygon
drawPolyline
drawOval
drawArc

There are also corresponding fill methods. These methods have been in the Graphics class ever since JDK 1.0. The Java 2D API uses a completely different, object-oriented approach. Instead of methods, there are classes:

Line2D
Rectangle2D
RoundRectangle2D
Ellipse2D
Arc2D
QuadCurve2D
CubicCurve2D
GeneralPath

These classes all implement the Shape interface.

Finally, the Point2D class describes a point with an x- and a y- coordinate. Points are useful to define shapes, but they aren’t themselves shapes.

To draw a shape, you first create an object of a class that implements the Shape interface and then call the draw method of the Graphics2D class.

The Line2D, Rectangle2D, RoundRectangle2D, Ellipse2D, and Arc2D classes correspond to the drawLine, drawRectangle, drawRoundRect, drawOval, and drawArc methods. (The concept of a “3D rectangle” has died the death that it so richly deserved—there is no analog to the draw3DRect method.) The Java 2D API supplies two additional classes: quadratic and cubic curves. We discuss these shapes later in this section. There is no Polygon2D class. Instead, the GeneralPath class describes paths that are made up from lines, quadratic and cubic curves. You can use a GeneralPath to describe a polygon; we show you how later in this section.

The classes

Rectangle2D
RoundRectangle2D
Ellipse2D
Arc2D

all inherit from a common superclass RectangularShape. Admittedly, ellipses and arcs are not rectangular, but they have a bounding rectangle (see Figure 7-2).

The bounding rectangle of an ellipse and an arc

Figure 7-2. The bounding rectangle of an ellipse and an arc

Each of the classes with a name ending in “2D” has two subclasses for specifying coordinates as float or double quantities. In Volume I, you already encountered Rectangle2D.Float and Rectangle2D.Double.

The same scheme is used for the other classes, such as Arc2D.Float and Arc2D.Double.

Internally, all graphics classes use float coordinates because float numbers use less storage space and they have sufficient precision for geometric computations. However, the Java programming language makes it a bit more tedious to manipulate float numbers. For that reason, most methods of the graphics classes use double parameters and return values. Only when constructing a 2D object must you choose between a constructor with float or double coordinates. For example,

Rectangle2D floatRect = new Rectangle2D.Float(5F, 10F, 7.5F, 15F);
Rectangle2D doubleRect = new Rectangle2D.Double(5, 10, 7.5, 15);

The Xxx2D.Float and Xxx2D.Double classes are subclasses of the Xxx2D classes. After object construction, essentially no benefit accrues from remembering the subclass, and you can just store the constructed object in a superclass variable, just as in the code example.

As you can see from the curious names, the Xxx2D.Float and Xxx2D.Double classes are also inner classes of the Xxx2D classes. That is just a minor syntactical convenience, to avoid an inflation of outer class names.

Figure 7-3 shows the relationships between the shape classes. However, the Double and Float subclasses are omitted. Legacy classes from the pre-2D library are marked with a gray fill.

Relationships between the shape classes

Figure 7-3. Relationships between the shape classes

Using the Shape Classes

You already saw how to use the Rectangle2D, Ellipse2D, and Line2D classes in Volume I, Chapter 7. In this section, you will learn how to work with the remaining 2D shapes.

For the RoundRectangle2D shape, you specify the top-left corner, width and height, and the x- and y-dimension of the corner area that should be rounded (see Figure 7-4). For example, the call

RoundRectangle2D r = new RoundRectangle2D.Double(150, 200, 100, 50, 20, 20);

produces a rounded rectangle with circles of radius 20 at each of the corners.

Constructing a RoundRectangle2D

Figure 7-4. Constructing a RoundRectangle2D

To construct an arc, you specify the bounding box, the start angle, the angle swept out by the arc (see Figure 7-5), and the closure type, one of Arc2D.OPEN, Arc2D.PIE, or Arc2D.CHORD.

Arc2D a = new Arc2D(x, y, width, height, startAngle, arcAngle, closureType);
Constructing an elliptical arc

Figure 7-5. Constructing an elliptical arc

Figure 7-6 illustrates the arc types.

Arc types

Figure 7-6. Arc types

Caution

Caution

If the arc is elliptical, the computation of the arc angles is not at all straightforward. The API documentation states: “The angles are specified relative to the non-square framing rectangle such that 45 degrees always falls on the line from the center of the ellipse to the upper right corner of the framing rectangle. As a result, if the framing rectangle is noticeably longer along one axis than the other, the angles to the start and end of the arc segment will be skewed farther along the longer axis of the frame.” Unfortunately, the documentation is silent on how to compute this “skew.” Here are the details:

Suppose the center of the arc is the origin and the point (x, y) lies on the arc. You get a skewed angle with the following formula:

skewedAngle = Math.toDegrees(Math.atan2(-y * height, x * width));

The result is a value between –180 and 180. Compute the skewed start and end angles in this way. Then, compute the difference between the two skewed angles. If the start angle or the angle difference is negative, add 360. Then, supply the start angle and the angle difference to the arc constructor.

If you run the example program at the end of this section, then you can visually check that this calculation yields the correct values for the arc constructor (see Figure 7-9 on page 531).

The Java 2D API supports quadratic and cubic curves. In this chapter, we do not get into the mathematics of these curves. We suggest you get a feel for how the curves look by running the program in Listing 7-1. As you can see in Figures 7-7 and 7-8, quadratic and cubic curves are specified by two end points and one or two control points. Moving the control points changes the shape of the curves.

A quadratic curve

Figure 7-7. A quadratic curve

A cubic curve

Figure 7-8. A cubic curve

To construct quadratic and cubic curves, you give the coordinates of the end points and the control points. For example,

QuadCurve2D q = new QuadCurve2D.Double(startX, startY, controlX, controlY, endX, endY);
CubicCurve2D c = new CubicCurve2D.Double(startX, startY, control1X, control1Y,
   control2X, control2Y, endX, endY);

Quadratic curves are not very flexible, and they are not commonly used in practice. Cubic curves (such as the Bezier curves drawn by the CubicCurve2D class) are, however, very common. By combining many cubic curves so that the slopes at the connection points match, you can create complex, smooth-looking curved shapes. For more information, we refer you to Computer Graphics: Principles and Practice, Second Edition in C by James D. Foley, Andries van Dam, Steven K. Feiner, et al. (Addison-Wesley 1995).

You can build arbitrary sequences of line segments, quadratic curves, and cubic curves, and store them in a GeneralPath object. You specify the first coordinate of the path with the moveTo method. For example,

GeneralPath path = new GeneralPath();
path.moveTo(10, 20);

You then extend the path by calling one of the methods lineTo, quadTo, or curveTo. These methods extend the path by a line, a quadratic curve, or a cubic curve. To call lineTo, supply the end point. For the two curve methods, supply the control points, then the end point. For example,

path.lineTo(20, 30);
path.curveTo(control1X, control1Y, control2X, control2Y, endX, endY);

You close the path by calling the closePath method. It draws a line back to the starting point of the path.

To make a polygon, simply call moveTo to go to the first corner point, followed by repeated calls to lineTo to visit the other corner points. Finally, call closePath to close the polygon. The program in Listing 7-1 shows this in more detail.

A general path does not have to be connected. You can call moveTo at any time to start a new path segment.

Finally, you can use the append method to add arbitrary Shape objects to a general path. The outline of the shape is added to the end to the path. The second parameter of the append method is true if the new shape should be connected to the last point on the path, false if it should not be connected. For example, the call

Rectangle2D r = . . .;
path.append(r, false);

appends the outline of a rectangle to the path without connecting it to the existing path. But

path.append(r, true);

adds a straight line from the end point of the path to the starting point of the rectangle, and then adds the rectangle outline to the path.

The program in Listing 7-1 lets you create sample paths. Figures 7-7 and 7-8 show sample runs of the program. You pick a shape maker from the combo box. The program contains shape makers for

  • Straight lines.

  • Rectangles, round rectangles, and ellipses.

  • Arcs (showing lines for the bounding rectangle and the start and end angles, in addition to the arc itself).

  • Polygons (using a GeneralPath).

  • Quadratic and cubic curves.

Use the mouse to adjust the control points. As you move them, the shape continuously repaints itself.

The program is a bit complex because it handles a multiplicity of shapes and supports dragging of the control points.

An abstract superclass ShapeMaker encapsulates the commonality of the shape maker classes. Each shape has a fixed number of control points that the user can move around. The getPointCount method returns that value. The abstract method

Shape makeShape(Point2D[] points)

computes the actual shape, given the current positions of the control points. The toString method returns the class name so that the ShapeMaker objects can simply be dumped into a JComboBox.

To enable dragging of the control points, the ShapePanel class handles both mouse and mouse motion events. If the mouse is pressed on top of a rectangle, subsequent mouse drags move the rectangle.

The majority of the shape maker classes are simple—their makeShape methods just construct and return the requested shape. However, the ArcMaker class needs to compute the distorted start and end angles. Furthermore, to demonstrate that the computation is indeed correct, the returned shape is a GeneralPath containing the arc itself, the bounding rectangle, and the lines from the center of the arc to the angle control points (see Figure 7-9).

The ShapeTest program

Figure 7-9. The ShapeTest program

Example 7-1. ShapeTest.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.awt.geom.*;
  4. import java.util.*;
  5. import javax.swing.*;
  6.
  7. /**
  8.  * This program demonstrates the various 2D shapes.
  9.  * @version 1.02 2007-08-16
 10.  * @author Cay Horstmann
 11.  */
 12. public class ShapeTest
 13. {
 14.    public static void main(String[] args)
 15.    {
 16.       EventQueue.invokeLater(new Runnable()
 17.          {
 18.             public void run()
 19.             {
 20.                JFrame frame = new ShapeTestFrame();
 21.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 22.                frame.setVisible(true);
 23.             }
 24.          });
 25.    }
 26. }
 27.
 28. /**
 29.  * This frame contains a combo box to select a shape and a component to draw it.
 30.  */
 31. class ShapeTestFrame extends JFrame
 32. {
 33.    public ShapeTestFrame()
 34.    {
 35.       setTitle("ShapeTest");
 36.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 37.
 38.       final ShapeComponent comp = new ShapeComponent();
 39.       add(comp, BorderLayout.CENTER);
 40.       final JComboBox comboBox = new JComboBox();
 41.       comboBox.addItem(new LineMaker());
 42.       comboBox.addItem(new RectangleMaker());
 43.       comboBox.addItem(new RoundRectangleMaker());
 44.       comboBox.addItem(new EllipseMaker());
 45.       comboBox.addItem(new ArcMaker());
 46.       comboBox.addItem(new PolygonMaker());
 47.       comboBox.addItem(new QuadCurveMaker());
 48.       comboBox.addItem(new CubicCurveMaker());
 49.       comboBox.addActionListener(new ActionListener()
 50.          {
 51.             public void actionPerformed(ActionEvent event)
 52.             {
 53.                ShapeMaker shapeMaker = (ShapeMaker) comboBox.getSelectedItem();
 54.                comp.setShapeMaker(shapeMaker);
 55.             }
 56.          });
 57.       add(comboBox, BorderLayout.NORTH);
 58.       comp.setShapeMaker((ShapeMaker) comboBox.getItemAt(0));
 59.    }
 60.
 61.    private static final int DEFAULT_WIDTH = 300;
 62.    private static final int DEFAULT_HEIGHT = 300;
 63. }
 64.
 65. /**
 66.  * This component draws a shape and allows the user to move the points that define it.
 67.  */
 68. class ShapeComponent extends JComponent
 69. {
 70.    public ShapeComponent()
 71.    {
 72.       addMouseListener(new MouseAdapter()
 73.          {
 74.             public void mousePressed(MouseEvent event)
 75.             {
 76.                Point p = event.getPoint();
 77.                for (int i = 0; i < points.length; i++)
 78.                {
 79.                   double x = points[i].getX() - SIZE / 2;
 80.                   double y = points[i].getY() - SIZE / 2;
 81.                   Rectangle2D r = new Rectangle2D.Double(x, y, SIZE, SIZE);
 82.                   if (r.contains(p))
 83.                   {
 84.                      current = i;
 85.                      return;
 86.                   }
 87.                }
 88.             }
 89.
 90.             public void mouseReleased(MouseEvent event)
 91.             {
 92.                current = -1;
 93.             }
 94.          });
 95.       addMouseMotionListener(new MouseMotionAdapter()
 96.          {
 97.             public void mouseDragged(MouseEvent event)
 98.             {
 99.                if (current == -1) return;
100.                points[current] = event.getPoint();
101.                repaint();
102.             }
103.          });
104.       current = -1;
105.    }
106.
107.    /**
108.     * Set a shape maker and initialize it with a random point set.
109.     * @param aShapeMaker a shape maker that defines a shape from a point set
110.     */
111.    public void setShapeMaker(ShapeMaker aShapeMaker)
112.    {
113.       shapeMaker = aShapeMaker;
114.       int n = shapeMaker.getPointCount();
115.       points = new Point2D[n];
116.       for (int i = 0; i < n; i++)
117.       {
118.          double x = generator.nextDouble() * getWidth();
119.          double y = generator.nextDouble() * getHeight();
120.          points[i] = new Point2D.Double(x, y);
121.       }
122.       repaint();
123.    }
124.
125.    public void paintComponent(Graphics g)
126.    {
127.       if (points == null) return;
128.       Graphics2D g2 = (Graphics2D) g;
129.       for (int i = 0; i < points.length; i++)
130.       {
131.          double x = points[i].getX() - SIZE / 2;
132.          double y = points[i].getY() - SIZE / 2;
133.          g2.fill(new Rectangle2D.Double(x, y, SIZE, SIZE));
134.       }
135.
136.       g2.draw(shapeMaker.makeShape(points));
137.    }
138.
139.    private Point2D[] points;
140.    private static Random generator = new Random();
141.    private static int SIZE = 10;
142.    private int current;
143.    private ShapeMaker shapeMaker;
144. }
145.
146. /**
147.  * A shape maker can make a shape from a point set. Concrete subclasses must return a shape
148.  *  in the makeShape method.
149.  */
150. abstract class ShapeMaker
151. {
152.    /**
153.     * Constructs a shape maker.
154.     * @param aPointCount the number of points needed to define this shape.
155.     */
156.    public ShapeMaker(int aPointCount)
157.    {
158.       pointCount = aPointCount;
159.    }
160.
161.    /**
162.     * Gets the number of points needed to define this shape.
163.     * @return the point count
164.     */
165.    public int getPointCount()
166.    {
167.       return pointCount;
168.    }
169.
170.    /**
171.     * Makes a shape out of the given point set.
172.     * @param p the points that define the shape
173.     * @return the shape defined by the points
174.     */
175.    public abstract Shape makeShape(Point2D[] p);
176.
177.    public String toString()
178.    {
179.       return getClass().getName();
180.    }
181.
182.    private int pointCount;
183. }
184.
185. /**
186.  * Makes a line that joins two given points.
187.  */
188. class LineMaker extends ShapeMaker
189. {
190.    public LineMaker()
191.    {
192.       super(2);
193.    }
194.
195.    public Shape makeShape(Point2D[] p)
196.    {
197.       return new Line2D.Double(p[0], p[1]);
198.    }
199. }
200.
201. /**
202.  * Makes a rectangle that joins two given corner points.
203.  */
204. class RectangleMaker extends ShapeMaker
205. {
206.    public RectangleMaker()
207.    {
208.       super(2);
209.    }
210.
211.    public Shape makeShape(Point2D[] p)
212.    {
213.       Rectangle2D s = new Rectangle2D.Double();
214.       s.setFrameFromDiagonal(p[0], p[1]);
215.       return s;
216.    }
217. }
218.
219. /**
220.  * Makes a round rectangle that joins two given corner points.
221.  */
222. class RoundRectangleMaker extends ShapeMaker
223. {
224.    public RoundRectangleMaker()
225.    {
226.       super(2);
227.    }
228.
229.    public Shape makeShape(Point2D[] p)
230.    {
231.       RoundRectangle2D s = new RoundRectangle2D.Double(0, 0, 0, 0, 20, 20);
232.       s.setFrameFromDiagonal(p[0], p[1]);
233.       return s;
234.    }
235. }
236.
237. /**
238.  * Makes an ellipse contained in a bounding box with two given corner points.
239.  */
240. class EllipseMaker extends ShapeMaker
241. {
242.    public EllipseMaker()
243.    {
244.       super(2);
245.    }
246.
247.    public Shape makeShape(Point2D[] p)
248.    {
249.       Ellipse2D s = new Ellipse2D.Double();
250.       s.setFrameFromDiagonal(p[0], p[1]);
251.       return s;
252.    }
253. }
254.
255. /**
256.  * Makes an arc contained in a bounding box with two given corner points, and with starting
257.  * and ending angles given by lines emanating from the center of the bounding box and ending
258.  *  in two given points. To show the correctness of the angle computation, the returned shape
259.  * contains the arc, the bounding box, and the lines.
260.  */
261. class ArcMaker extends ShapeMaker
262. {
263.    public ArcMaker()
264.    {
265.       super(4);
266.    }
267.
268.    public Shape makeShape(Point2D[] p)
269.    {
270.       double centerX = (p[0].getX() + p[1].getX()) / 2;
271.       double centerY = (p[0].getY() + p[1].getY()) / 2;
272.       double width = Math.abs(p[1].getX() - p[0].getX());
273.       double height = Math.abs(p[1].getY() - p[0].getY());
274.
275.       double skewedStartAngle = Math.toDegrees(Math.atan2(-(p[2].getY() - centerY)
276.             * width, (p[2].getX() - centerX)
277.             * height));
278.       double skewedEndAngle = Math.toDegrees(Math.atan2(-(p[3].getY() - centerY)
279.             * width, (p[3].getX() - centerX)
280.             * height));
281.       double skewedAngleDifference = skewedEndAngle - skewedStartAngle;
282.       if (skewedStartAngle < 0) skewedStartAngle += 360;
283.       if (skewedAngleDifference < 0) skewedAngleDifference += 360;
284.
285.       Arc2D s = new Arc2D.Double(0, 0, 0, 0, skewedStartAngle, skewedAngleDifference,
286.                                  Arc2D.OPEN);
287.       s.setFrameFromDiagonal(p[0], p[1]);
288.
289.       GeneralPath g = new GeneralPath();
290.       g.append(s, false);
291.       Rectangle2D r = new Rectangle2D.Double();
292.       r.setFrameFromDiagonal(p[0], p[1]);
293.       g.append(r, false);
294.       Point2D center = new Point2D.Double(centerX, centerY);
295.       g.append(new Line2D.Double(center, p[2]), false);
296.       g.append(new Line2D.Double(center, p[3]), false);
297.       return g;
298.    }
299. }
300.
301. /**
302.  * Makes a polygon defined by six corner points.
303.  */
304. class PolygonMaker extends ShapeMaker
305. {
306.    public PolygonMaker()
307.    {
308.       super(6);
309.    }
310.
311.    public Shape makeShape(Point2D[] p)
312.    {
313.       GeneralPath s = new GeneralPath();
314.       s.moveTo((float) p[0].getX(), (float) p[0].getY());
315.       for (int i = 1; i < p.length; i++)
316.          s.lineTo((float) p[i].getX(), (float) p[i].getY());
317.       s.closePath();
318.       return s;
319.    }
320. }
321.
322. /**
323.  * Makes a quad curve defined by two end points and a control point.
324.  */
325. class QuadCurveMaker extends ShapeMaker
326. {
327.    public QuadCurveMaker()
328.    {
329.       super(3);
330.    }
331.
332.    public Shape makeShape(Point2D[] p)
333.    {
334.       return new QuadCurve2D.Double(p[0].getX(), p[0].getY(), p[1].getX(), p[1].getY(), p[2]
335.             .getX(), p[2].getY());
336.    }
337. }
338.
339. /**
340.  * Makes a cubic curve defined by two end points and two control points.
341.  */
342. class CubicCurveMaker extends ShapeMaker
343. {
344.    public CubicCurveMaker()
345.    {
346.       super(4);
347.    }
348.
349.    public Shape makeShape(Point2D[] p)
350.    {
351.       return new CubicCurve2D.Double(p[0].getX(), p[0].getY(), p[1].getX(), p[1].getY(), p[2]
352.             .getX(), p[2].getY(), p[3].getX(), p[3].getY());
353.    }
354. }

 

Areas

In the preceding section, you saw how you can specify complex shapes by constructing general paths that are composed of lines and curves. By using a sufficient number of lines and curves, you can draw essentially any shape. For example, the shapes of characters in the fonts that you see on the screen and on your printouts are all made up of lines and cubic curves.

Occasionally, it is easier to describe a shape by composing it from areas, such as rectangles, polygons, or ellipses. The Java 2D API supports four constructive area geometry operations that combine two areas into a new area:

  • add—. The combined area contains all points that are in the first or the second area.

  • subtract—. The combined area contains all points that are in the first but not the second area.

  • intersect—. The combined area contains all points that are in the first and the second area.

  • exclusiveOr—. The combined area contains all points that are in either the first or the second area, but not in both.

Figure 7-10 shows these operations.

Constructive area geometry operations

Figure 7-10. Constructive area geometry operations

To construct a complex area, you start with a default area object.

Area a = new Area();

Then, you combine the area with any shape.

a.add(new Rectangle2D.Double(. . .));
a.subtract(path);
. . .

The Area class implements the Shape interface. You can stroke the boundary of the area with the draw method or paint the interior with the fill method of the Graphics2D class.

Strokes

The draw operation of the Graphics2D class draws the boundary of a shape by using the currently selected stroke. By default, the stroke is a solid line that is 1 pixel wide. You can select a different stroke by calling the setStroke method. You supply an object of a class that implements the Stroke interface. The Java 2D API defines only one such class, called BasicStroke. In this section, we look at the capabilities of the BasicStroke class.

You can construct strokes of arbitrary thickness. For example, here is how you draw lines that are 10 pixels wide.

g2.setStroke(new BasicStroke(10.0F));
g2.draw(new Line2D.Double(. . .));

When a stroke is more than a pixel thick, then the end of the stroke can have different styles. Figure 7-11 shows these so-called end cap styles. You have three choices:

  • A butt cap simply ends the stroke at its end point.

  • A round cap adds a half-circle to the end of the stroke.

  • A square cap adds a half-square to the end of the stroke.

End cap styles

Figure 7-11. End cap styles

When two thick strokes meet, there are three choices for the join style (see Figure 7-12).

  • A bevel join joins the strokes with a straight line that is perpendicular to the bisector of the angle between the two strokes.

  • A round join extends each stroke to have a round cap.

  • A miter join extends both strokes by adding a “spike.”

Join styles

Figure 7-12. Join styles

The miter join is not suitable for lines that meet at small angles. If two lines join with an angle that is less than the miter limit, then a bevel join is used instead. That usage prevents extremely long spikes. By default, the miter limit is 10 degrees.

You specify these choices in the BasicStroke constructor, for example:

g2.setStroke(new BasicStroke(10.0F, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2.setStroke(new BasicStroke(10.0F, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
   15.0F /* miter limit */));

Finally, you can specify dashed lines by setting a dash pattern. In the program in Listing 7-2, you can select a dash pattern that spells out SOS in Morse code. The dash pattern is a float[] array of numbers that contains the lengths of the “on” and “off” strokes (see Figure 7-13).

A dash pattern

Figure 7-13. A dash pattern

You specify the dash pattern and a dash phase when constructing the BasicStroke. The dash phase indicates where in the dash pattern each line should start. Normally, you set this value to 0.

float[] dashPattern = { 10, 10, 10, 10, 10, 10, 30, 10, 30, ... };
g2.setStroke(new BasicStroke(10.0F, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
   10.0F /* miter limit */, dashPattern, 0 /* dash phase */));

Note

Note

End cap styles are applied to the ends of each dash in a dash pattern.

The program in Listing 7-2 lets you specify end cap styles, join styles, and dashed lines (see Figure 7-14). You can move the ends of the line segments to test the miter limit: Select the miter join, then move the line segment to form a very acute angle. You will see the miter join turn into a bevel join.

The StrokeTest program

Figure 7-14. The StrokeTest program

The program is similar to the program in Listing 7-1. The mouse listener remembers if you click on the end point of a line segment, and the mouse motion listener monitors the dragging of the end point. A set of radio buttons signal the user choices for the end cap style, join style, and solid or dashed line. The paintComponent method of the StrokePanel class constructs a GeneralPath consisting of the two line segments that join the three points that the user can move with the mouse. It then constructs a BasicStroke, according to the selections that the user made, and finally draws the path.

Example 7-2. StrokeTest.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.awt.geom.*;
  4. import javax.swing.*;
  5.
  6. /**
  7.  * This program demonstrates different stroke types.
  8.  * @version 1.03 2007-08-16
  9.  * @author Cay Horstmann
 10.  */
 11. public class StrokeTest
 12. {
 13.    public static void main(String[] args)
 14.    {
 15.       EventQueue.invokeLater(new Runnable()
 16.          {
 17.             public void run()
 18.             {
 19.                JFrame frame = new StrokeTestFrame();
 20.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 21.                frame.setVisible(true);
 22.             }
 23.          });
 24.    }
 25. }
 26.
 27. /**
 28.  * This frame lets the user choose the cap, join, and line style, and shows the resulting
 29.  * stroke.
 30.  */
 31. class StrokeTestFrame extends JFrame
 32. {
 33.    public StrokeTestFrame()
 34.    {
 35.       setTitle("StrokeTest");
 36.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 37.
 38.       canvas = new StrokeComponent();
 39.       add(canvas, BorderLayout.CENTER);
 40.
 41.       buttonPanel = new JPanel();
 42.       buttonPanel.setLayout(new GridLayout(3, 3));
 43.       add(buttonPanel, BorderLayout.NORTH);
 44.
 45.       ButtonGroup group1 = new ButtonGroup();
 46.       makeCapButton("Butt Cap", BasicStroke.CAP_BUTT, group1);
 47.       makeCapButton("Round Cap", BasicStroke.CAP_ROUND, group1);
 48.       makeCapButton("Square Cap", BasicStroke.CAP_SQUARE, group1);
 49.
 50.       ButtonGroup group2 = new ButtonGroup();
 51.       makeJoinButton("Miter Join", BasicStroke.JOIN_MITER, group2);
 52.       makeJoinButton("Bevel Join", BasicStroke.JOIN_BEVEL, group2);
 53.       makeJoinButton("Round Join", BasicStroke.JOIN_ROUND, group2);
 54.
 55.       ButtonGroup group3 = new ButtonGroup();
 56.       makeDashButton("Solid Line", false, group3);
 57.       makeDashButton("Dashed Line", true, group3);
 58.    }
 59.
 60.    /**
 61.     * Makes a radio button to change the cap style.
 62.     * @param label the button label
 63.     * @param style the cap style
 64.     * @param group the radio button group
 65.     */
 66.    private void makeCapButton(String label, final int style, ButtonGroup group)
 67.    {
 68.       // select first button in group
 69.       boolean selected = group.getButtonCount() == 0;
 70.       JRadioButton button = new JRadioButton(label, selected);
 71.       buttonPanel.add(button);
 72.       group.add(button);
 73.       button.addActionListener(new ActionListener()
 74.          {
 75.             public void actionPerformed(ActionEvent event)
 76.             {
 77.                canvas.setCap(style);
 78.             }
 79.          });
 80.    }
 81.
 82.    /**
 83.     * Makes a radio button to change the join style.
 84.     * @param label the button label
 85.     * @param style the join style
 86.     * @param group the radio button group
 87.     */
 88.    private void makeJoinButton(String label, final int style, ButtonGroup group)
 89.    {
 90.       // select first button in group
 91.       boolean selected = group.getButtonCount() == 0;
 92.       JRadioButton button = new JRadioButton(label, selected);
 93.       buttonPanel.add(button);
 94.       group.add(button);
 95.       button.addActionListener(new ActionListener()
 96.          {
 97.             public void actionPerformed(ActionEvent event)
 98.             {
 99.                canvas.setJoin(style);
100.             }
101.          });
102.    }
103.
104.    /**
105.     * Makes a radio button to set solid or dashed lines
106.     * @param label the button label
107.     * @param style false for solid, true for dashed lines
108.     * @param group the radio button group
109.     */
110.    private void makeDashButton(String label, final boolean style, ButtonGroup group)
111.    {
112.       // select first button in group
113.       boolean selected = group.getButtonCount() == 0;
114.       JRadioButton button = new JRadioButton(label, selected);
115.       buttonPanel.add(button);
116.       group.add(button);
117.       button.addActionListener(new ActionListener()
118.          {
119.             public void actionPerformed(ActionEvent event)
120.             {
121.                canvas.setDash(style);
122.             }
123.          });
124.    }
125.
126.    private StrokeComponent canvas;
127.    private JPanel buttonPanel;
128.
129.    private static final int DEFAULT_WIDTH = 400;
130.    private static final int DEFAULT_HEIGHT = 400;
131. }
132.
133. /**
134.  * This component draws two joined lines, using different stroke objects, and allows the
135.  * user to drag the three points defining the lines.
136.  */
137. class StrokeComponent extends JComponent
138. {
139.    public StrokeComponent()
140.    {
141.       addMouseListener(new MouseAdapter()
142.          {
143.             public void mousePressed(MouseEvent event)
144.             {
145.                Point p = event.getPoint();
146.                for (int i = 0; i < points.length; i++)
147.                {
148.                   double x = points[i].getX() - SIZE / 2;
149.                   double y = points[i].getY() - SIZE / 2;
150.                   Rectangle2D r = new Rectangle2D.Double(x, y, SIZE, SIZE);
151.                   if (r.contains(p))
152.                   {
153.                      current = i;
154.                      return;
155.                   }
156.                }
157.             }
158.
159.             public void mouseReleased(MouseEvent event)
160.             {
161.                current = -1;
162.             }
163.          });
164.
165.       addMouseMotionListener(new MouseMotionAdapter()
166.          {
167.             public void mouseDragged(MouseEvent event)
168.             {
169.                if (current == -1) return;
170.                points[current] = event.getPoint();
171.                repaint();
172.             }
173.          });
174.
175.       points = new Point2D[3];
176.       points[0] = new Point2D.Double(200, 100);
177.       points[1] = new Point2D.Double(100, 200);
178.       points[2] = new Point2D.Double(200, 200);
179.       current = -1;
180.       width = 8.0F;
181.    }
182.
183.    public void paintComponent(Graphics g)
184.    {
185.       Graphics2D g2 = (Graphics2D) g;
186.       GeneralPath path = new GeneralPath();
187.       path.moveTo((float) points[0].getX(), (float) points[0].getY());
188.       for (int i = 1; i < points.length; i++)
189.          path.lineTo((float) points[i].getX(), (float) points[i].getY());
190.       BasicStroke stroke;
191.       if (dash)
192.       {
193.          float miterLimit = 10.0F;
194.          float[] dashPattern = { 10F, 10F, 10F, 10F, 10F, 10F, 30F, 10F, 30F, 10F, 30F, 10F,
195.                10F, 10F, 10F, 10F, 10F, 30F };
196.          float dashPhase = 0;
197.          stroke = new BasicStroke(width, cap, join, miterLimit, dashPattern, dashPhase);
198.       }
199.       else stroke = new BasicStroke(width, cap, join);
200.       g2.setStroke(stroke);
201.       g2.draw(path);
202.    }
203.
204.    /**
205.     * Sets the join style.
206.     * @param j the join style
207.     */
208.    public void setJoin(int j)
209.    {
210.       join = j;
211.       repaint();
212.    }
213.
214.    /**
215.     * Sets the cap style.
216.     * @param c the cap style
217.     */
218.    public void setCap(int c)
219.    {
220.       cap = c;
221.       repaint();
222.    }
223.
224.    /**
225.     * Sets solid or dashed lines
226.     * @param d false for solid, true for dashed lines
227.     */
228.    public void setDash(boolean d)
229.    {
230.       dash = d;
231.       repaint();
232.    }
233.
234.    private Point2D[] points;
235.    private static int SIZE = 10;
236.    private int current;
237.    private float width;
238.    private int cap;
239.    private int join;
240.    private boolean dash;
241. }

 

Parameters:

width

The width of the pen

 

cap

The end cap style, one of CAP_BUTT, CAP_ROUND, and CAP_SQUARE

 

join

The join style, one of JOIN_BEVEL, JOIN_MITER, and JOIN_ROUND

 

miterlimit

The angle, in degrees, below which a miter join is rendered as a bevel join

 

dash

An array of the lengths of the alternating filled and blank portions of a dashed stroke

 

dashPhase

The “phase” of the dash pattern; a segment of this length, preceding the starting point of the stroke, is assumed to have the dash pattern already applied

Paint

When you fill a shape, its inside is covered with paint. You use the setPaint method to set the paint style to an object with a class that implements the Paint interface. The Java 2D API provides three such classes:

  • The Color class implements the Paint interface. To fill shapes with a solid color, simply call setPaint with a Color object, such as

    g2.setPaint(Color.red);
  • The GradientPaint class varies colors by interpolating between two given color values (see Figure 7-15).

    Gradient paint

    Figure 7-15. Gradient paint

  • The TexturePaint class fills an area with repetitions of an image (see Figure 7-16).

    Texture paint

    Figure 7-16. Texture paint

You construct a GradientPaint object by specifying two points and the colors that you want at these two points.

g2.setPaint(new GradientPaint(p1, Color.RED, p2, Color.YELLOW));

Colors are interpolated along the line joining the two points. Colors are constant along lines that are perpendicular to that joining line. Points beyond an end point of the line are given the color at the end point.

Alternatively, if you call the GradientPaint constructor with true for the cyclic parameter,

g2.setPaint(new GradientPaint(p1, Color.RED, p2, Color.YELLOW, true));

then the color variation cycles and keeps varying beyond the end points.

To construct a TexturePaint object, you specify a BufferedImage and an anchor rectangle.

g2.setPaint(new TexturePaint(bufferedImage, anchorRectangle));

We introduce the BufferedImage class later in this chapter when we discuss images in detail. The simplest way of obtaining a buffered image is to read an image file:

bufferedImage = ImageIO.read(new File("blue-ball.gif"));

The anchor rectangle is extended indefinitely in x- and y-directions to tile the entire coordinate plane. The image is scaled to fit into the anchor and then replicated into each tile.

Coordinate Transformations

Suppose you need to draw an object such as an automobile. You know, from the manufacturer’s specifications, the height, wheelbase, and total length. You could, of course, figure out all pixel positions, assuming some number of pixels per meter. However, there is an easier way: You can ask the graphics context to carry out the conversion for you.

g2.scale(pixelsPerMeter, pixelsPerMeter);
g2.draw(new Line2D.Double(coordinates in meters)); // converts to pixels and draws scaled line

The scale method of the Graphics2D class sets the coordinate transformation of the graphics context to a scaling transformation. That transformation changes user coordinates (user-specified units) to device coordinates (pixels). Figure 7-17 shows how the transformation works.

User and device coordinates

Figure 7-17. User and device coordinates

Coordinate transformations are very useful in practice. They allow you to work with convenient coordinate values. The graphics context takes care of the dirty work of transforming them to pixels.

There are four fundamental transformations.

  • Scaling: blowing up, or shrinking, all distances from a fixed point.

  • Rotation: rotating all points around a fixed center.

  • Translation: moving all points by a fixed amount.

  • Shear: leaving one line fixed and “sliding” the lines parallel to it by an amount that is proportional to the distance from the fixed line.

Figure 7-18 shows how these four fundamental transformations act on a unit square.

The fundamental transformations

Figure 7-18. The fundamental transformations

The scale, rotate, translate, and shear methods of the Graphics2D class set the coordinate transformation of the graphics context to one of these fundamental transformations.

You can compose the transformations. For example, you might want to rotate shapes and double their size. Then, you supply both a rotation and a scaling transformation.

g2.rotate(angle);
g2.scale(2, 2);
g2.draw(. . .);

In this case, it does not matter in which order you supply the transformations. However, with most transformations, order does matter. For example, if you want to rotate and shear, then it makes a difference which of the transformations you supply first. You need to figure out what your intention is. The graphics context will apply the transformations in the opposite order in which you supplied them. That is, the last transformation that you supply is applied first.

You can supply as many transformations as you like. For example, consider the following sequence of transformations:

g2.translate(x, y);
g2.rotate(a);
g2.translate(-x, -y);

The last transformation (which is applied first) moves the point (x, y) to the origin. The second transformation rotates with an angle a around the origin. The final transformation moves the origin back to (x, y). The overall effect is a rotation with center point (x, y)—see Figure 7-19. Because rotating about a point other than the origin is such a common operation, there is a shortcut:

g2.rotate(a, x, y);
Composing transformations

Figure 7-19. Composing transformations

If you know some matrix theory, you are probably aware that all rotations, translations, scalings, shears, and their compositions can be expressed by matrix transformations of the form:

Composing transformations

Such a transformation is called an affine transformation. In the Java 2D API, the AffineTransform class describes such a transformation. If you know the components of a particular transformation matrix, you can construct it directly as

AffineTransform t = new AffineTransform(a, b, c, d, e, f);

Additionally, the factory methods getRotateInstance, getScaleInstance, getTranslateInstance, and getShearInstance construct the matrices that represent these transformation types. For example, the call

t = AffineTransform.getScaleInstance(2.0F, 0.5F);

returns a transformation that corresponds to the matrix

Composing transformations

Finally, the instance methods setToRotation, setToScale, setToTranslation, and setToShear set a transformation object to a new type. Here is an example:

t.setToRotation(angle); // sets t to a rotation

You can set the coordinate transformation of the graphics context to an AffineTransform object.

g2.setTransform(t); // replaces current transformation

However, in practice, you shouldn’t call the setTransform operation, as it replaces any existing transformation that the graphics context may have. For example, a graphics context for printing in landscape mode already contains a 90-degree rotation transformation. If you call setTransform, you obliterate that rotation. Instead, call the transform method.

g2.transform(t); // composes current transformation with t

It composes the existing transformation with the new AffineTransform object.

If you just want to apply a transformation temporarily, then you first get the old transformation, compose with your new transformation, and finally restore the old transformation when you are done.

AffineTransform oldTransform = g2.getTransform(); // save old transform
g2.transform(t); // apply temporary transform // now draw on g2
g2.setTransform(oldTransform); // restore old transform

Clipping

By setting a clipping shape in the graphics context, you constrain all drawing operations to the interior of that clipping shape.

g2.setClip(clipShape); // but see below
g2.draw(shape); // draws only the part that falls inside the clipping shape

However, in practice, you don’t want to call the setClip operation, because it replaces any existing clipping shape that the graphics context might have. For example, as you will see later in this chapter, a graphics context for printing comes with a clip rectangle that ensures that you don’t draw on the margins. Instead, call the clip method.

g2.clip(clipShape); // better

The clip method intersects the existing clipping shape with the new one that you supply.

If you just want to apply a clipping area temporarily, then you should first get the old clip, then add your new clip, and finally restore the old clip when you are done:

Shape oldClip = g2.getClip(); // save old clip
g2.clip(clipShape); // apply temporary clip
draw on g2
g2.setClip(oldClip); // restore old clip

In Figure 7-20, we show off the clipping capability with a rather dramatic drawing of a line pattern that is clipped by a complex shape, namely, the outline of a set of letters.

Using letter shapes to clip a line pattern

Figure 7-20. Using letter shapes to clip a line pattern

To obtain character outlines, you need a font render context. Use the getFontRenderContext method of the Graphics2D class.

FontRenderContext context = g2.getFontRenderContext();

Next, using a string, a font, and the font render context, create a TextLayout object:

TextLayout layout = new TextLayout("Hello", font, context);

This text layout object describes the layout of a sequence of characters, as rendered by a particular font render context. The layout depends on the font render context—the same characters will look different on a screen or a printer.

More important for our application, the getOutline method returns a Shape object that describes the shape of the outline of the characters in the text layout. The outline shape starts at the origin (0, 0), which might not be what you want. In that case, supply an affine transform to the getOutline operation that specifies where you would like the outline to appear.

AffineTransform transform = AffineTransform.getTranslateInstance(0, 100);
Shape outline = layout.getOutline(transform);

Then, append the outline to the clipping shape.

GeneralPath clipShape = new GeneralPath();
clipShape.append(outline, false);

Finally, set the clipping shape and draw a set of lines. The lines appear only inside the character boundaries.

g2.setClip(clipShape);
Point2D p = new Point2D.Double(0, 0);
for (int i = 0; i < NLINES; i++)
{
   double x = . . .;
   double y = . . .;
   Point2D q = new Point2D.Double(x, y);
   g2.draw(new Line2D.Double(p, q)); // lines are clipped
}

You can see the complete code in Listing 7-8 on page 607.

Transparency and Composition

In the standard RGB color model, every color is described by its red, green, and blue components. However, it is also convenient to describe areas of an image that are transparent or partially transparent. When you superimpose an image onto an existing drawing, the transparent pixels do not obscure the pixels under them at all, whereas partially transparent pixels are mixed with the pixels under them. Figure 7-21 shows the effect of overlaying a partially transparent rectangle on an image. You can still see the details of the image shine through from under the rectangle.

Overlaying a partially transparent rectangle on an image

Figure 7-21. Overlaying a partially transparent rectangle on an image

In the Java 2D API, transparency is described by an alpha channel. Each pixel has, in addition to its red, green, and blue color components, an alpha value between 0 (fully transparent) and 1 (fully opaque). For example, the rectangle in Figure 7-21 was filled with a pale yellow color with 50% transparency:

new Color(0.7F, 0.7F, 0.0F, 0.5F);

Now let us look at what happens if you superimpose two shapes. You need to blend or compose the colors and alpha values of the source and destination pixels. Porter and Duff, two researchers in the field of computer graphics, have formulated 12 possible composition rules for this blending process. The Java 2D API implements all of these rules. Before we go any further, we want to point out that only two of these rules have practical significance. If you find the rules arcane or confusing, just use the SRC_OVER rule. It is the default rule for a Graphics2D object, and it gives the most intuitive results.

Here is the theory behind the rules. Suppose you have a source pixel with alpha value aS. In the image, there is already a destination pixel with alpha value aD. You want to compose the two. The diagram in Figure 7-22 shows how to design a composition rule.

Designing a composition rule

Figure 7-22. Designing a composition rule

Porter and Duff consider the alpha value as the probability that the pixel color should be used. From the perspective of the source, there is a probability aS that it wants to use the source color and a probability of 1 − aS that it doesn’t care. The same holds for the destination. When composing the colors, let us assume that the probabilities are independent. Then there are four cases, as shown in Figure 7-22. If the source wants to use the source color and the destination doesn’t care, then it seems reasonable to let the source have its way. That’s why the upper-right corner of the diagram is labeled “S.” The probability for that event is aS·(1 − aD). Similarly, the lower-left corner is labeled “D.” What should one do if both destination and source would like to select their color? That’s where the Porter–Duff rules come in. If we decide that the source is more important, then we label the lower-right corner with an “S” as well. That rule is called SRC_OVER. In that rule, you combine the source colors with a weight of aS and the destination colors with a weight of (1 − aSaD.

The visual effect is a blending of the source and destination, with preference given to the source. In particular, if aS is 1, then the destination color is not taken into account at all. If aS is 0, then the source pixel is completely transparent and the destination color is unchanged.

The other rules depend on what letters you put in the boxes of the probability diagram. Table 7-1 and Figure 7-23 show all rules that are supported by the Java 2D API. The images in the figure show the results of the rules when a rectangular source region with an alpha of 0.75 is combined with an elliptical destination region with an alpha of 1.0.

Table 7-1. The Porter–Duff Composition Rules

Rule

Explanation

CLEAR

Source clears destination.

SRC

Source overwrites destination and empty pixels.

DST

Source does not affect destination.

SRC_OVER

Source blends with destination and overwrites empty pixels.

DST_OVER

Source does not affect destination and overwrites empty pixels.

SRC_IN

Source overwrites destination.

SRC_OUT

Source clears destination and overwrites empty pixels.

DST_IN

Source alpha modifies destination.

DST_OUT

Source alpha complement modifies destination.

SRC_ATOP

Source blends with destination.

DST_ATOP

Source alpha modifies destination. Source overwrites empty pixels.

XOR

Source alpha complement modifies destination. Source overwrites empty pixels.

Porter–Duff composition rules

Figure 7-23. Porter–Duff composition rules

As you can see, most of the rules aren’t very useful. Consider, as an extreme case, the DST_IN rule. It doesn’t take the source color into account at all, but it uses the alpha of the source to affect the destination. The SRC rule is potentially useful—it forces the source color to be used, turning off blending with the destination.

For more information on the Porter–Duff rules, see, for example, Computer Graphics: Principles and Practice, Second Edition in C by James D. Foley, Andries van Dam, Steven K. Feiner, et al.

You use the setComposite method of the Graphics2D class to install an object of a class that implements the Composite interface. The Java 2D API supplies one such class, AlphaComposite, that implements all the Porter–Duff rules in Figure 7-23.

The factory method getInstance of the AlphaComposite class yields an AlphaComposite object. You supply the rule and the alpha value to be used for source pixels. For example, consider the following code:

int rule = AlphaComposite.SRC_OVER;
float alpha = 0.5f;
g2.setComposite(AlphaComposite.getInstance(rule, alpha));
g2.setPaint(Color.blue);
g2.fill(rectangle);

The rectangle is then painted with blue color and an alpha value of 0.5. Because the composition rule is SRC_OVER, it is transparently overlaid on the existing image.

The program in Listing 7-3 lets you explore these composition rules. Pick a rule from the combo box and use the slider to set the alpha value of the AlphaComposite object.

Furthermore, the program displays a verbal description of each rule. Note that the descriptions are computed from the composition rule diagrams. For example, a "DS" in the second row stands for “blends with destination.”

The program has one important twist. There is no guarantee that the graphics context that corresponds to the screen has an alpha channel. (In fact, it generally does not.) When pixels are deposited to a destination without an alpha channel, then the pixel colors are multiplied with the alpha value and the alpha value is discarded. Because several of the Porter–Duff rules use the alpha values of the destination, a destination alpha channel is important. For that reason, we use a buffered image with the ARGB color model to compose the shapes. After the images have been composed, we draw the resulting image to the screen.

BufferedImage image = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D gImage = image.createGraphics();
// now draw to gImage
g2.drawImage(image, null, 0, 0);

The complete code for the program is shown in Listing 7-3. Figure 7-24 shows the screen display. As you run the program, move the alpha slider from left to right to see the effect on the composed shapes. In particular, note that the only difference between the DST_IN and DST_OUT rules is how the destination (!) color changes when you change the source alpha.

The CompositeTest program

Figure 7-24. The CompositeTest program

Example 7-3. CompositeTest.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.awt.image.*;
  4. import java.awt.geom.*;
  5. import javax.swing.*;
  6. import javax.swing.event.*;
  7.
  8. /**
  9.  * This program demonstrates the Porter-Duff composition rules.
 10.  * @version 1.03 2007-08-16
 11.  * @author Cay Horstmann
 12.  */
 13. public class CompositeTest
 14. {
 15.    public static void main(String[] args)
 16.    {
 17.       EventQueue.invokeLater(new Runnable()
 18.          {
 19.             public void run()
 20.             {
 21.                JFrame frame = new CompositeTestFrame();
 22.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 23.                frame.setVisible(true);
 24.             }
 25.          });
 26.    }
 27. }
 28.
 29. /**
 30.  * This frame contains a combo box to choose a composition rule, a slider to change the
 31.  *  source alpha channel, and a component that shows the composition.
 32.  */
 33. class CompositeTestFrame extends JFrame
 34. {
 35.    public CompositeTestFrame()
 36.    {
 37.       setTitle("CompositeTest");
 38.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 39.
 40.       canvas = new CompositeComponent();
 41.       add(canvas, BorderLayout.CENTER);
 42.
 43.       ruleCombo = new JComboBox(new Object[] { new Rule("CLEAR", "  ", "  "),
 44.             new Rule("SRC", " S", " S"), new Rule("DST", "  ", "DD"),
 45.             new Rule("SRC_OVER", " S", "DS"), new Rule("DST_OVER", " S", "DD"),
 46.             new Rule("SRC_IN", "  ", " S"), new Rule("SRC_OUT", " S", "  "),
 47.             new Rule("DST_IN", "  ", " D"), new Rule("DST_OUT", "  ", "D "),
 48.             new Rule("SRC_ATOP", "  ", "DS"), new Rule("DST_ATOP", " S", " D"),
 49.             new Rule("XOR", " S", "D "), });
 50.       ruleCombo.addActionListener(new ActionListener()
 51.          {
 52.             public void actionPerformed(ActionEvent event)
 53.             {
 54.                Rule r = (Rule) ruleCombo.getSelectedItem();
 55.                canvas.setRule(r.getValue());
 56.                explanation.setText(r.getExplanation());
 57.             }
 58.          });
 59.
 60.       alphaSlider = new JSlider(0, 100, 75);
 61.       alphaSlider.addChangeListener(new ChangeListener()
 62.          {
 63.             public void stateChanged(ChangeEvent event)
 64.             {
 65.                canvas.setAlpha(alphaSlider.getValue());
 66.             }
 67.          });
 68.       JPanel panel = new JPanel();
 69.       panel.add(ruleCombo);
 70.       panel.add(new JLabel("Alpha"));
 71.       panel.add(alphaSlider);
 72.       add(panel, BorderLayout.NORTH);
 73.
 74.       explanation = new JTextField();
 75.       add(explanation, BorderLayout.SOUTH);
 76.
 77.       canvas.setAlpha(alphaSlider.getValue());
 78.       Rule r = (Rule) ruleCombo.getSelectedItem();
 79.       canvas.setRule(r.getValue());
 80.       explanation.setText(r.getExplanation());
 81.    }
 82.
 83.    private CompositeComponent canvas;
 84.    private JComboBox ruleCombo;
 85.    private JSlider alphaSlider;
 86.    private JTextField explanation;
 87.    private static final int DEFAULT_WIDTH = 400;
 88.    private static final int DEFAULT_HEIGHT = 400;
 89. }
 90.
 91. /**
 92.  * This class describes a Porter-Duff rule.
 93.  */
 94. class Rule
 95. {
 96.    /**
 97.     * Constructs a Porter-Duff rule
 98.     * @param n the rule name
 99.     * @param pd1 the first row of the Porter-Duff square
100.     * @param pd2 the second row of the Porter-Duff square
101.     */
102.    public Rule(String n, String pd1, String pd2)
103.    {
104.       name = n;
105.       porterDuff1 = pd1;
106.       porterDuff2 = pd2;
107.    }
108.
109.    /**
110.     * Gets an explanation of the behavior of this rule.
111.     * @return the explanation
112.     */
113.    public String getExplanation()
114.    {
115.       StringBuilder r = new StringBuilder("Source ");
116.       if (porterDuff2.equals("  ")) r.append("clears");
117.       if (porterDuff2.equals(" S")) r.append("overwrites");
118.       if (porterDuff2.equals("DS")) r.append("blends with");
119.       if (porterDuff2.equals(" D")) r.append("alpha modifies");
120.       if (porterDuff2.equals("D ")) r.append("alpha complement modifies");
121.       if (porterDuff2.equals("DD")) r.append("does not affect");
122.       r.append(" destination");
123.       if (porterDuff1.equals(" S")) r.append(" and overwrites empty pixels");
124.       r.append(".");
125.       return r.toString();
126.    }
127.
128.    public String toString()
129.    {
130.       return name;
131.    }
132.
133.    /**
134.     * Gets the value of this rule in the AlphaComposite class
135.     * @return the AlphaComposite constant value, or -1 if there is no matching constant.
136.     */
137.    public int getValue()
138.    {
139.       try
140.       {
141.          return (Integer) AlphaComposite.class.getField(name).get(null);
142.       }
143.       catch (Exception e)
144.       {
145.          return -1;
146.       }
147.    }
148.
149.    private String name;
150.    private String porterDuff1;
151.    private String porterDuff2;
152. }
153.
154. /**
155.  * This component draws two shapes, composed with a composition rule.
156.  */
157. class CompositeComponent extends JComponent
158. {
159.    public CompositeComponent()
160.    {
161.       shape1 = new Ellipse2D.Double(100, 100, 150, 100);
162.       shape2 = new Rectangle2D.Double(150, 150, 150, 100);
163.    }
164.
165.    public void paintComponent(Graphics g)
166.    {
167.       Graphics2D g2 = (Graphics2D) g;
168.
169.       BufferedImage image = new BufferedImage(getWidth(), getHeight(),
170.                                               BufferedImage.TYPE_INT_ARGB);
171.       Graphics2D gImage = image.createGraphics();
172.       gImage.setPaint(Color.red);
173.       gImage.fill(shape1);
174.       AlphaComposite composite = AlphaComposite.getInstance(rule, alpha);
175.       gImage.setComposite(composite);
176.       gImage.setPaint(Color.blue);
177.       gImage.fill(shape2);
178.       g2.drawImage(image, null, 0, 0);
179.    }
180.
181.    /**
182.     * Sets the composition rule.
183.     * @param r the rule (as an AlphaComposite constant)
184.     */
185.    public void setRule(int r)
186.    {
187.       rule = r;
188.       repaint();
189.    }
190.
191.    /**
192.     * Sets the alpha of the source
193.     * @param a the alpha value between 0 and 100
194.     */
195.    public void setAlpha(int a)
196.    {
197.       alpha = (float) a / 100.0F;
198.       repaint();
199.    }
200.
201.    private int rule;
202.    private Shape shape1;
203.    private Shape shape2;
204.    private float alpha;
205. }

 

Rendering Hints

In the preceding sections you have seen that the rendering process is quite complex. Although the Java 2D API is surprisingly fast in most cases, there are cases when you would like to have control over trade-offs between speed and quality. You achieve this by setting rendering hints. The setRenderingHint method of the Graphics2D class lets you set a single hint. The hint keys and values are declared in the RenderingHints class. Table 7-2 summarizes the choices. The values that end in _DEFAULT denote defaults that are chosen by a particular implementation as a good trade-off between performance and quality.

Table 7-2. Rendering Hints

Key

Value

Explanation

KEY_ANTIALIASING

VALUE_ANTIALIAS_ON
VALUE_ANTIALIAS_OFF
VALUE_ANTIALIAS_DEFAULT

Turn antialiasing for shapes on or off.

KEY_TEXT_ANTIALIASING

VALUE_TEXT_ANTIALIAS_ON
VALUE_TEXT_ANTIALIAS_OFF
VALUE_TEXT_ANTIALIAS_DEFAULT
VALUE_TEXT_ANTIALIAS_GASP 6
VALUE_TEXT_ANTIALIAS_LCD_HRGB 6
VALUE_TEXT_ANTIALIAS_LCD_HBGR 6
VALUE_TEXT_ANTIALIAS_LCD_VRGB 6
VALUE_TEXT_ANTIALIAS_LCD_VBGR 6

Turn antialiasing for fonts on or off. When using the value VALUE_TEXT_ANTIALIAS_GASP, the “gasp table” of the font is consulted to decide whether a particular size of a font should be antialiased. The LCD values force subpixel rendering for a particular display type.

KEY_FRACTIONALMETRICS

VALUE_FRACTIONALMETRICS_ON
VALUE_FRACTIONALMETRICS_OFF
VALUE_FRACTIONALMETRICS_DEFAULT

Turn the computation of fractional character dimensions on or off. Fractional character dimensions lead to better placement of characters.

KEY_RENDERING

VALUE_RENDER_QUALITY
VALUE_RENDER_SPEED
VALUE_RENDER_DEFAULT

When available, select rendering algorithms for greater quality or speed.

KEY_STROKE_CONTROL 1.3

VALUE_STROKE_NORMALIZE
VALUE_STROKE_PURE
VALUE_STROKE_DEFAULT

Select whether the placement of strokes is controlled by the graphics accelerator (which may move it by up to half a pixel) or is computed by the “pure” rule that mandates that strokes run through the centers of pixels.

KEY_DITHERING

VALUE_DITHER_ENABLE
VALUE_DITHER_DISABLE
VALUE_DITHER_DEFAULT

Turn dithering for colors on or off. Dithering approximates color values by drawing groups of pixels of similar colors. (Note that antialiasing can interfere with dithering.)

KEY_ALPHA_INTERPOLATION

VALUE_ALPHA_INTERPOLATION_QUALITY
VALUE_ALPHA_INTERPOLATION_SPEED
VALUE_ALPHA_INTERPOLATION_DEFAULT

Turn precise computation of alpha composites on or off.

KEY_COLOR_RENDERING

VALUE_COLOR_RENDER_QUALITY
VALUE_COLOR_RENDER_SPEED
VALUE_COLOR_RENDER_DEFAULT

Select quality or speed for color rendering. This is only an issue when you use different color spaces.

KEY_INTERPOLATION

VALUE_INTERPOLATION_NEAREST_NEIGHBOR
VALUE_INTERPOLATION_BILINEAR
VALUE_INTERPOLATION_BICUBIC

Select a rule for interpolating pixels when scaling or rotating images.

The most useful of these settings involves antialiasing. This technique removes the “jaggies” from slanted lines and curves. As you can see in Figure 7-25, a slanted line must be drawn as a “staircase” of pixels. Especially on low-resolution screens, this line can look ugly. But if, rather than drawing each pixel completely on or off, you color in the pixels that are partially covered, with the color value proportional to the area of the pixel that the line covers, then the result looks much smoother. This technique is called antialiasing. Of course, antialiasing takes a bit longer because it takes time to compute all those color values.

 

Antialiasing

Figure 7-25. Antialiasing

For example, here is how you can request the use of antialiasing:

g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

It also makes sense to use antialiasing for fonts.

g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, Rendering-
Hints.VALUE_TEXT_ANTIALIAS_ON);

The other rendering hints are not as commonly used.

You can also put a bunch of key/value hint pairs into a map and set them all at once by calling the setRenderingHints method. Any collection class implementing the map interface will do, but you might as well use the RenderingHints class itself. It implements the Map interface and supplies a default map implementation if you pass null to the constructor. For example,

RenderingHints hints = new RenderingHints(null);
hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setRenderingHints(hints);

That is the technique we use in Listing 7-4. The program shows several rendering hints that we found beneficial. Note the following:

  • Antialiasing smooths the ellipse.

  • Text antialiasing smooths the text.

  • On some platforms, fractional text metrics move the letters a bit closer together.

  • Selecting VALUE_RENDER_QUALITY smooths the scaled image. (You would get the same effect by setting KEY_INTERPOLATION to VALUE_INTERPOLATION_BICUBIC).

  • When antialiasing is turned off, selecting VALUE_STROKE_NORMALIZE changes the appearance of the ellipse and the placement of the diagonal line in the square.

Figure 7-26 shows a screen capture of the program.

Testing the effect of rendering hints

Figure 7-26. Testing the effect of rendering hints

Example 7-4. RenderQualityTest.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.awt.geom.*;
  4. import java.io.*;
  5. import javax.imageio.*;
  6. import javax.swing.*;
  7.
  8. /**
  9.  * This program demonstrates the effect of the various rendering hints.
 10.  * @version 1.10 2007-08-16
 11.  * @author Cay Horstmann
 12.  */
 13. public class RenderQualityTest
 14. {
 15.    public static void main(String[] args)
 16.    {
 17.       EventQueue.invokeLater(new Runnable()
 18.          {
 19.             public void run()
 20.             {
 21.                JFrame frame = new RenderQualityTestFrame();
 22.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 23.                frame.setVisible(true);
 24.             }
 25.          });
 26.    }
 27. }
 28.
 29. /**
 30.  * This frame contains buttons to set rendering hints and an image that is drawn with
 31.  * the selected hints.
 32.  */
 33. class RenderQualityTestFrame extends JFrame
 34. {
 35.    public RenderQualityTestFrame()
 36.    {
 37.       setTitle("RenderQualityTest");
 38.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 39.
 40.       buttonBox = new JPanel();
 41.       buttonBox.setLayout(new GridBagLayout());
 42.       hints = new RenderingHints(null);
 43.
 44.       makeButtons("KEY_ANTIALIASING", "VALUE_ANTIALIAS_OFF", "VALUE_ANTIALIAS_ON");
 45.       makeButtons("KEY_TEXT_ANTIALIASING", "VALUE_TEXT_ANTIALIAS_OFF",
 46.                   "VALUE_TEXT_ANTIALIAS_ON");
 47.       makeButtons("KEY_FRACTIONALMETRICS", "VALUE_FRACTIONALMETRICS_OFF",
 48.             "VALUE_FRACTIONALMETRICS_ON");
 49.       makeButtons("KEY_RENDERING", "VALUE_RENDER_SPEED", "VALUE_RENDER_QUALITY");
 50.       makeButtons("KEY_STROKE_CONTROL", "VALUE_STROKE_PURE", "VALUE_STROKE_NORMALIZE");
 51.       canvas = new RenderQualityComponent();
 52.       canvas.setRenderingHints(hints);
 53.
 54.       add(canvas, BorderLayout.CENTER);
 55.       add(buttonBox, BorderLayout.NORTH);
 56.    }
 57.
 58.    /**
 59.     * Makes a set of buttons for a rendering hint key and values
 60.     * @param key the key name
 61.     * @param value1 the name of the first value for the key
 62.     * @param value2 the name of the second value for the key
 63.     */
 64.    void makeButtons(String key, String value1, String value2)
 65.    {
 66.       try
 67.       {
 68.          final RenderingHints.Key k =
 69.             (RenderingHints.Key) RenderingHints.class.getField(key).get(null);
 70.          final Object v1 = RenderingHints.class.getField(value1).get(null);
 71.          final Object v2 = RenderingHints.class.getField(value2).get(null);
 72.          JLabel label = new JLabel(key);
 73.
 74.          buttonBox.add(label, new GBC(0, r).setAnchor(GBC.WEST));
 75.          ButtonGroup group = new ButtonGroup();
 76.          JRadioButton b1 = new JRadioButton(value1, true);
 77.
 78.          buttonBox.add(b1, new GBC(1, r).setAnchor(GBC.WEST));
 79.          group.add(b1);
 80.          b1.addActionListener(new ActionListener()
 81.             {
 82.                public void actionPerformed(ActionEvent event)
 83.                {
 84.                   hints.put(k, v1);
 85.                   canvas.setRenderingHints(hints);
 86.                }
 87.             });
 88.          JRadioButton b2 = new JRadioButton(value2, false);
 89.
 90.          buttonBox.add(b2, new GBC(2, r).setAnchor(GBC.WEST));
 91.          group.add(b2);
 92.          b2.addActionListener(new ActionListener()
 93.             {
 94.                public void actionPerformed(ActionEvent event)
 95.                {
 96.                   hints.put(k, v2);
 97.                   canvas.setRenderingHints(hints);
 98.                }
 99.             });
100.          hints.put(k, v1);
101.          r++;
102.       }
103.       catch (Exception e)
104.       {
105.          e.printStackTrace();
106.       }
107.    }
108.
109.    private RenderQualityComponent canvas;
110.    private JPanel buttonBox;
111.    private RenderingHints hints;
112.    private int r;
113.    private static final int DEFAULT_WIDTH = 750;
114.    private static final int DEFAULT_HEIGHT = 300;
115. }
116.
117. /**
118.  * This component produces a drawing that shows the effect of rendering hints.
119.  */
120. class RenderQualityComponent extends JComponent
121. {
122.    public RenderQualityComponent()
123.    {
124.       try
125.       {
126.          image = ImageIO.read(new File("face.gif"));
127.       }
128.       catch (IOException e)
129.       {
130.          e.printStackTrace();
131.       }
132.    }
133.
134.    public void paintComponent(Graphics g)
135.    {
136.       Graphics2D g2 = (Graphics2D) g;
137.       g2.setRenderingHints(hints);
138.
139.       g2.draw(new Ellipse2D.Double(10, 10, 60, 50));
140.       g2.setFont(new Font("Serif", Font.ITALIC, 40));
141.       g2.drawString("Hello", 75, 50);
142.
143.       g2.draw(new Rectangle2D.Double(200, 10, 40, 40));
144.       g2.draw(new Line2D.Double(201, 11, 239, 49));
145.
146.       g2.drawImage(image, 250, 10, 100, 100, null);
147.    }
148.
149.    /**
150.     * Sets the hints and repaints.
151.     * @param h the rendering hints
152.     */
153.    public void setRenderingHints(RenderingHints h)
154.    {
155.       hints = h;
156.       repaint();
157.    }
158.
159.    private RenderingHints hints = new RenderingHints(null);
160.    private Image image;
161. }

 

Readers and Writers for Images

Prior to version 1.4, Java SE had very limited capabilities for reading and writing image files. It was possible to read GIF and JPEG images, but there was no official support for writing images at all.

This situation is now much improved. Java SE 1.4 introduced the javax.imageio package that contains “out of the box” support for reading and writing several common file formats, as well as a framework that enables third parties to add readers and writers for other formats. As of Java SE 6, the GIF, JPEG, PNG, BMP (Windows bitmap), and WBMP (wireless bitmap) file formats are supported. In earlier versions, writing of GIF files was not supported because of patent issues.

The basics of the library are extremely straightforward. To load an image, use the static read method of the ImageIO class:

File f = . . .;
BufferedImage image = ImageIO.read(f);

The ImageIO class picks an appropriate reader, based on the file type. It may consult the file extension and the “magic number” at the beginning of the file for that purpose. If no suitable reader can be found or the reader can’t decode the file contents, then the read method returns null.

Writing an image to a file is just as simple:

File f = . . .;
String format = . . .;
ImageIO.write(image, format, f);

Here the format string is a string identifying the image format, such as "JPEG" or "PNG". The ImageIO class picks an appropriate writer and saves the file.

Obtaining Readers and Writers for Image File Types

For more advanced image reading and writing operations that go beyond the static read and write methods of the ImageIO class, you first need to get the appropriate ImageReader and ImageWriter objects. The ImageIO class enumerates readers and writers that match one of the following:

  • An image format (such as “JPEG”)

  • A file suffix (such as “jpg”)

  • A MIME type (such as “image/jpeg”)

Note

Note

MIME is the Multipurpose Internet Mail Extensions standard. The MIME standard defines common data formats such as “image/jpeg” and “application/pdf”. For an HTML version of the Request for Comments (RFC) that defines the MIME format, see http://www.oac.uci.edu/indiv/ehood/MIME.

For example, you can obtain a reader that reads JPEG files as follows:

ImageReader reader = null;
Iterator<ImageReader> iter = ImageIO.getImageReadersByFormatName("JPEG");
if (iter.hasNext()) reader = iter.next();

The getImageReadersBySuffix and getImageReadersByMIMEType method enumerate readers that match a file extension or MIME type.

It is possible that the ImageIO class can locate multiple readers that can all read a particular image type. In that case, you have to pick one of them, but it isn’t clear how you can decide which one is the best. To find out more information about a reader, obtain its service provider interface:

ImageReaderSpi spi = reader.getOriginatingProvider();

Then you can get the vendor name and version number:

String vendor = spi.getVendor();
String version = spi.getVersion();

Perhaps that information can help you decide among the choices, or you might just present a list of readers to your program users and let them choose. However, for now, we assume that the first enumerated reader is adequate.

In the sample program in Listing 7-5, we want to find all file suffixes of all available readers so that we can use them in a file filter. As of Java SE 6, we can use the static ImageIO.getReaderFileSuffixes method for this purpose:

String[] extensions = ImageIO.getWriterFileSuffixes();
chooser.setFileFilter(new FileNameExtensionFilter("Image files", extensions));

For saving files, we have to work harder. We’d like to present the user with a menu of all supported image types. Unfortunately, the getWriterFormatNames of the IOImage class returns a rather curious list with redundant names, such as

jpg, BMP, bmp, JPG, jpeg, wbmp, png, JPEG, PNG, WBMP, GIF, gif

That’s not something one would want to present in a menu. What is needed is a list of “preferred” format names. We supply a helper method getWriterFormats for this purpose (see Listing 7-5). We look up the first writer associated with each format name. Then we ask it what its format names are, in the hope that it will list the most popular one first. Indeed, for the JPEG writer, this works fine: It lists "JPEG" before the other options. (The PNG writer, on the other hand, lists "png" in lower case before "PNG". We hope this behavior will be addressed at some time in the future. In the meantime, we force all-lowercase names to upper case.) Once we pick a preferred name, we remove all alternate names from the original set. We keep going until all format names are handled.

Reading and Writing Files with Multiple Images

Some files, in particular, animated GIF files, contain multiple images. The read method of the ImageIO class reads a single image. To read multiple images, turn the input source (for example, an input stream or file) into an ImageInputStream.

InputStream in = . . .;
ImageInputStream imageIn = ImageIO.createImageInputStream(in);

Then attach the image input stream to the reader:

reader.setInput(imageIn, true);

The second parameter indicates that the input is in “seek forward only” mode. Otherwise, random access is used, either by buffering stream input as it is read or by using random file access. Random access is required for certain operations. For example, to find out the number of images in a GIF file, you need to read the entire file. If you then want to fetch an image, the input must be read again.

This consideration is only important if you read from a stream, if the input contains multiple images, and if the image format doesn’t have the information that you request (such as the image count) in the header. If you read from a file, simply use

File f = . . .;
ImageInputStream imageIn = ImageIO.createImageInputStream(f);
reader.setInput(imageIn);

Once you have a reader, you can read the images in the input by calling

BufferedImage image = reader.read(index);

where index is the image index, starting with 0.

If the input is in “seek forward only” mode, you keep reading images until the read method throws an IndexOutOfBoundsException. Otherwise, you can call the getNumImages method:

int n = reader.getNumImages(true);

Here, the parameter indicates that you allow a search of the input to determine the number of images. That method throws an IllegalStateException if the input is in “seek forward only” mode. Alternatively, you can set the “allow search” parameter to false. Then the getNumImages method returns −1 if it can’t determine the number of images without a search. In that case, you’ll have to switch to Plan B and keep reading images until you get an IndexOutOfBoundsException.

Some files contain thumbnails, smaller versions of an image for preview purposes. You can get the number of thumbnails of an image with the call

int count = reader.getNumThumbnails(index);

Then you get a particular index as

BufferedImage thumbnail = reader.getThumbnail(index, thumbnailIndex);

Another consideration is that you sometimes want to get the image size before actually getting the image, in particular, if the image is huge or comes from a slow network connection. Use the calls

int width = reader.getWidth(index);
int height = reader.getHeight(index);

to get the dimensions of an image with a given index.

To write a file with multiple images, you first need an ImageWriter. The ImageIO class can enumerate the writers that are capable of writing a particular image format:

String format = . . .;
ImageWriter writer = null;
Iterator<ImageWriter> iter =  ImageIO.getImageWritersByFormatName( format );
if (iter.hasNext()) writer = iter.next();

Next, turn an output stream or file into an ImageOutputStream and attach it to the writer. For example,

File f = . . .;
ImageOutputStream imageOut = ImageIO.createImageOutputStream(f);
writer.setOutput(imageOut);

You must wrap each image into an IIOImage object. You can optionally supply a list of thumbnails and image metadata (such as compression algorithms and color information). In this example, we just use null for both; see the API documentation for additional information.

IIOImage iioImage = new IIOImage(images[i], null, null);

Write out the first image, using the write method:

writer.write(new IIOImage(images[0], null, null));

For subsequent images, use

if (writer.canInsertImage(i))
   writer.writeInsert(i, iioImage, null);

The third parameter can contain an ImageWriteParam object to set image writing details such as tiling and compression; use null for default values.

Not all file formats can handle multiple images. In that case, the canInsertImage method returns false for i > 0, and only a single image is saved.

The program in Listing 7-5 lets you load and save files in the formats for which the Java library supplies readers and writers. The program displays multiple images (see Figure 7-27), but not thumbnails.

An animated GIF image

Figure 7-27. An animated GIF image

Example 7-5. ImageIOTest.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.awt.image.*;
  4. import java.io.*;
  5. import java.util.*;
  6. import javax.imageio.*;
  7. import javax.imageio.stream.*;
  8. import javax.swing.*;
  9. import javax.swing.filechooser.*;
 10.
 11. /**
 12.  * This program lets you read and write image files in the formats that the JDK supports.
 13.  * Multi-file images are supported.
 14.  * @version 1.02 2007-08-16
 15.  * @author Cay Horstmann
 16.  */
 17. public class ImageIOTest
 18. {
 19.    public static void main(String[] args)
 20.    {
 21.       EventQueue.invokeLater(new Runnable()
 22.          {
 23.             public void run()
 24.             {
 25.                JFrame frame = new ImageIOFrame();
 26.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 27.                frame.setVisible(true);
 28.             }
 29.          });
 30.    }
 31. }
 32.
 33. /**
 34.  * This frame displays the loaded images. The menu has items for loading and saving files.
 35.  */
 36. class ImageIOFrame extends JFrame
 37. {
 38.    public ImageIOFrame()
 39.    {
 40.       setTitle("ImageIOTest");
 41.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 42.
 43.       JMenu fileMenu = new JMenu("File");
 44.       JMenuItem openItem = new JMenuItem("Open");
 45.       openItem.addActionListener(new ActionListener()
 46.          {
 47.             public void actionPerformed(ActionEvent event)
 48.             {
 49.                openFile();
 50.             }
 51.          });
 52.       fileMenu.add(openItem);
 53.
 54.       JMenu saveMenu = new JMenu("Save");
 55.       fileMenu.add(saveMenu);
 56.       Iterator<String> iter = writerFormats.iterator();
 57.       while (iter.hasNext())
 58.       {
 59.          final String formatName = iter.next();
 60.          JMenuItem formatItem = new JMenuItem(formatName);
 61.          saveMenu.add(formatItem);
 62.          formatItem.addActionListener(new ActionListener()
 63.             {
 64.                public void actionPerformed(ActionEvent event)
 65.                {
 66.                   saveFile(formatName);
 67.                }
 68.             });
 69.       }
 70.
 71.       JMenuItem exitItem = new JMenuItem("Exit");
 72.       exitItem.addActionListener(new ActionListener()
 73.          {
 74.             public void actionPerformed(ActionEvent event)
 75.             {
 76.                System.exit(0);
 77.             }
 78.          });
 79.       fileMenu.add(exitItem);
 80.
 81.       JMenuBar menuBar = new JMenuBar();
 82.       menuBar.add(fileMenu);
 83.       setJMenuBar(menuBar);
 84.    }
 85.
 86.    /**
 87.     * Open a file and load the images.
 88.     */
 89.    public void openFile()
 90.    {
 91.       JFileChooser chooser = new JFileChooser();
 92.       chooser.setCurrentDirectory(new File("."));
 93.       String[] extensions = ImageIO.getReaderFileSuffixes();
 94.       chooser.setFileFilter(new FileNameExtensionFilter("Image files", extensions));
 95.       int r = chooser.showOpenDialog(this);
 96.       if (r != JFileChooser.APPROVE_OPTION) return;
 97.       File f = chooser.getSelectedFile();
 98.       Box box = Box.createVerticalBox();
 99.       try
100.       {
101.          String name = f.getName();
102.          String suffix = name.substring(name.lastIndexOf('.') + 1);
103.          Iterator<ImageReader> iter = ImageIO.getImageReadersBySuffix(suffix);
104.          ImageReader reader = iter.next();
105.          ImageInputStream imageIn = ImageIO.createImageInputStream(f);
106.          reader.setInput(imageIn);
107.          int count = reader.getNumImages(true);
108.          images = new BufferedImage[count];
109.          for (int i = 0; i < count; i++)
110.          {
111.             images[i] = reader.read(i);
112.             box.add(new JLabel(new ImageIcon(images[i])));
113.          }
114.       }
115.       catch (IOException e)
116.       {
117.          JOptionPane.showMessageDialog(this, e);
118.       }
119.       setContentPane(new JScrollPane(box));
120.       validate();
121.    }
122.
123.    /**
124.     * Save the current image in a file
125.     * @param formatName the file format
126.     */
127.    public void saveFile(final String formatName)
128.    {
129.       if (images == null) return;
130.       Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName(formatName);
131.       ImageWriter writer = iter.next();
132.       JFileChooser chooser = new JFileChooser();
133.       chooser.setCurrentDirectory(new File("."));
134.       String[] extensions = writer.getOriginatingProvider().getFileSuffixes();
135.       chooser.setFileFilter(new FileNameExtensionFilter("Image files", extensions));
136.
137.       int r = chooser.showSaveDialog(this);
138.       if (r != JFileChooser.APPROVE_OPTION) return;
139.       File f = chooser.getSelectedFile();
140.       try
141.       {
142.          ImageOutputStream imageOut = ImageIO.createImageOutputStream(f);
143.          writer.setOutput(imageOut);
144.
145.          writer.write(new IIOImage(images[0], null, null));
146.          for (int i = 1; i < images.length; i++)
147.          {
148.             IIOImage iioImage = new IIOImage(images[i], null, null);
149.             if (writer.canInsertImage(i)) writer.writeInsert(i, iioImage, null);
150.          }
151.       }
152.       catch (IOException e)
153.       {
154.          JOptionPane.showMessageDialog(this, e);
155.       }
156.    }
157.
158.    /**
159.     * Gets a set of "preferred" format names of all image writers. The preferred format name
160.     * is the first format name that a writer specifies.
161.     * @return the format name set
162.     */
163.    public static Set<String> getWriterFormats()
164.    {
165.       TreeSet<String> writerFormats = new TreeSet<String>();
166.       TreeSet<String> formatNames = new TreeSet<String>(Arrays.asList(ImageIO
167.             .getWriterFormatNames()));
168.       while (formatNames.size() > 0)
169.       {
170.          String name = formatNames.iterator().next();
171.          Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName(name);
172.          ImageWriter writer = iter.next();
173.          String[] names = writer.getOriginatingProvider().getFormatNames();
174.          String format = names[0];
175.          if (format.equals(format.toLowerCase())) format = format.toUpperCase();
176.          writerFormats.add(format);
177.          formatNames.removeAll(Arrays.asList(names));
178.       }
179.       return writerFormats;
180.    }
181.
182.    private BufferedImage[] images;
183.    private static Set<String> writerFormats = getWriterFormats();
184.    private static final int DEFAULT_WIDTH = 400;
185.    private static final int DEFAULT_HEIGHT = 400;
186. }

 

Image Manipulation

Suppose you have an image and you would like to improve its appearance. You then need to access the individual pixels of the image and replace them with other pixels. Or perhaps you want to compute the pixels of an image from scratch, for example, to show the result of physical measurements or a mathematical computation. The BufferedImage class gives you control over the pixels in an image, and classes that implement the BufferedImageOp interface let you transform images.

Note

Note

JDK 1.0 had a completely different, and far more complex, imaging framework that was optimized for incremental rendering of images that are downloaded from the Web, a scan line at a time. However, it was difficult to manipulate those images. We do not discuss that framework in this book.

Constructing Raster Images

Most of the images that you manipulate are simply read in from an image file—they were either produced by a device such as a digital camera or scanner, or constructed by a drawing program. In this section, we show you a different technique for constructing an image, namely, to build up an image a pixel at a time.

To create an image, construct a BufferedImage object in the usual way.

image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

Now, call the getRaster method to obtain an object of type WritableRaster. You use this object to access and modify the pixels of the image.

WritableRaster raster = image.getRaster();

The setPixel method lets you set an individual pixel. The complexity here is that you can’t simply set the pixel to a Color value. You must know how the buffered image specifies color values. That depends on the type of the image. If your image has a type of TYPE_INT_ARGB, then each pixel is described by four values, for red, green, blue, and alpha, each of which is between 0 and 255. You supply them in an array of four integers.

int[] black = { 0, 0, 0, 255 };
raster.setPixel(i, j, black);

In the lingo of the Java 2D API, these values are called the sample values of the pixel.

Caution

Caution

There are also setPixel methods that take array parameters of types float[] and double[]. However, the values that you need to place into these arrays are not normalized color values between 0.0 and 1.0.

float[] red = { 1.0F, 0.0F, 0.0F, 1.0F };
raster.setPixel(i, j, red); // ERROR

You need to supply values between 0 and 255, no matter what the type of the array is.

You can supply batches of pixels with the setPixels method. Specify the starting pixel position and the width and height of the rectangle that you want to set. Then, supply an array that contains the sample values for all pixels. For example, if your buffered image has a type of TYPE_INT_ARGB, then you supply the red, green, blue, and alpha value of the first pixel, then the red, green, blue, and alpha value for the second pixel, and so on.

int[] pixels = new int[4 * width * height];
pixels[0] = . . . // red value for first pixel
pixels[1] = . . . // green value for first pixel
pixels[2] = . . . // blue value for first pixel
pixels[3] = . . . // alpha value for first pixel
. . .
raster.setPixels(x, y, width, height, pixels);

Conversely, to read a pixel, you use the getPixel method. Supply an array of four integers to hold the sample values.

int[] sample = new int[4];
raster.getPixel(x, y, sample);
Color c = new Color(sample[0], sample[1], sample[2], sample[3]);

You can read multiple pixels with the getPixels method.

raster.getPixels(x, y, width, height, samples);

If you use an image type other than TYPE_INT_ARGB and you know how that type represents pixel values, then you can still use the getPixel/setPixel methods. However, you have to know the encoding of the sample values in the particular image type.

If you need to manipulate an image with an arbitrary, unknown image type, then you have to work a bit harder. Every image type has a color model that can translate between sample value arrays and the standard RGB color model.

Note

Note

The RGB color model isn’t as standard as you might think. The exact look of a color value depends on the characteristics of the imaging device. Digital cameras, scanners, monitors, and LCD displays all have their own idiosyncrasies. As a result, the same RGB value can look quite different on different devices. The International Color Consortium (http://www.color.org) recommends that all color data be accompanied by an ICC profile that specifies how the colors map to a standard form such as the 1931 CIE XYZ color specification. That specification was designed by the Commission Internationale de l’Eclairage or CIE (http://www.cie.co.at/cie), the international organization in charge of providing technical guidance in all matters of illumination and color. The specification is a standard method for representing all colors that the human eye can perceive as a triplet of coordinates called X, Y, Z. (See, for example, Computer Graphics: Principles and Practice, Second Edition in C by James D. Foley, Andries van Dam, Steven K. Feiner, et al., Chapter 13, for more information on the 1931 CIE XYZ specification.)

ICC profiles are complex, however. A simpler proposed standard, called sRGB (http://www.w3.org/Graphics/Color/sRGB.html), specifies an exact mapping between RGB values and the 1931 CIE XYZ values that was designed to work well with typical color monitors. The Java 2D API uses that mapping when converting between RGB and other color spaces.

The getColorModel method returns the color model:

ColorModel model = image.getColorModel();

To find the color value of a pixel, you call the getDataElements method of the Raster class. That call returns an Object that contains a color-model-specific description of the color value.

Object data = raster.getDataElements(x, y, null);

Note

Note

The object that is returned by the getDataElements method is actually an array of sample values. You don’t need to know this to process the object, but it explains why the method is called getDataElements.

The color model can translate the object to standard ARGB values. The getRGB method returns an int value that has the alpha, red, green, and blue values packed in four blocks of 8 bits each. You can construct a Color value out of that integer with the Color(int argb, boolean hasAlpha) constructor.

int argb = model.getRGB(data);
Color color = new Color(argb, true);

To set a pixel to a particular color, you reverse these steps. The getRGB method of the Color class yields an int value with the alpha, red, green, and blue values. Supply that value to the getDataElements method of the ColorModel class. The return value is an Object that contains the color-model-specific description of the color value. Pass the object to the setDataElements method of the WritableRaster class.

int argb = color.getRGB();
Object data = model.getDataElements(argb, null);
raster.setDataElements(x, y, data);

To illustrate how to use these methods to build an image from individual pixels, we bow to tradition and draw a Mandelbrot set, as shown in Figure 7-28.

A Mandelbrot set

Figure 7-28. A Mandelbrot set

The idea of the Mandelbrot set is that you associate with each point in the plane a sequence of numbers. If that sequence stays bounded, you color the point. If it “escapes to infinity,” you leave it transparent.

Here is how you can construct the simplest Mandelbrot set. For each point (a, b), you look at sequences that start with (x, y) = (0, 0) and iterate:

xnew = x2 - y2 + a

ynew = 2 · x · y + b

It turns out that if x or y ever gets larger than 2, then the sequence escapes to infinity. Only the pixels that correspond to points (a, b) leading to a bounded sequence are colored. (The formulas for the number sequences come ultimately from the mathematics of complex numbers. We just take them for granted. For more on the mathematics of fractals, see, for example, http://classes.yale.edu/fractals/.)

Listing 7-6 shows the code. In this program, we demonstrate how to use the ColorModel class for translating Color values into pixel data. That process is independent of the image type. Just for fun, change the color type of the buffered image to TYPE_BYTE_GRAY. You don’t need to change any other code—the color model of the image automatically takes care of the conversion from colors to sample values.

Example 7-6. RasterImageTest.java

 1. import java.awt.*;
 2. import java.awt.image.*;
 3. import javax.swing.*;
 4.
 5. /**
 6.  * This program demonstrates how to build up an image from individual pixels.
 7.  * @version 1.13 2007-08-16
 8.  * @author Cay Horstmann
 9.  */
10. public class RasterImageTest
11. {
12.    public static void main(String[] args)
13.    {
14.       EventQueue.invokeLater(new Runnable()
15.          {
16.             public void run()
17.             {
18.                JFrame frame = new RasterImageFrame();
19.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
20.                frame.setVisible(true);
21.             }
22.          });
23.    }
24. }
25.
26. /**
27.  * This frame shows an image with a Mandelbrot set.
28.  */
29. class RasterImageFrame extends JFrame
30. {
31.    public RasterImageFrame()
32.    {
33.       setTitle("RasterImageTest");
34.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
35.       BufferedImage image = makeMandelbrot(DEFAULT_WIDTH, DEFAULT_HEIGHT);
36.       add(new JLabel(new ImageIcon(image)));
37.    }
38.
39.    /**
40.     * Makes the Mandelbrot image.
41.     * @param width the width
42.     * @parah height the height
43.     * @return the image
44.     */
45.    public BufferedImage makeMandelbrot(int width, int height)
46.    {
47.       BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
48.       WritableRaster raster = image.getRaster();
49.       ColorModel model = image.getColorModel();
50.
51.       Color fractalColor = Color.red;
52.       int argb = fractalColor.getRGB();
53.       Object colorData = model.getDataElements(argb, null);
54.
55.       for (int i = 0; i < width; i++)
56.          for (int j = 0; j < height; j++)
57.          {
58.             double a = XMIN + i * (XMAX - XMIN) / width;
59.             double b = YMIN + j * (YMAX - YMIN) / height;
60.             if (!escapesToInfinity(a, b)) raster.setDataElements(i, j, colorData);
61.          }
62.       return image;
63.    }
64.
65.    private boolean escapesToInfinity(double a, double b)
66.    {
67.       double x = 0.0;
68.       double y = 0.0;
69.       int iterations = 0;
70.       while (x <= 2 && y <= 2 && iterations < MAX_ITERATIONS)
71.       {
72.          double xnew = x * x - y * y + a;
73.          double ynew = 2 * x * y + b;
74.          x = xnew;
75.          y = ynew;
76.          iterations++;
77.       }
78.       return x > 2 || y > 2;
79.    }
80.
81.    private static final double XMIN = -2;
82.    private static final double XMAX = 2;
83.    private static final double YMIN = -2;
84.    private static final double YMAX = 2;
85.    private static final int MAX_ITERATIONS = 16;
86.    private static final int DEFAULT_WIDTH = 400;
87.    private static final int DEFAULT_HEIGHT = 400;
88. }

Filtering Images

In the preceding section, you saw how to build up an image from scratch. However, often you want to access image data for a different reason: You already have an image and you want to improve it in some way.

Of course, you can use the getPixel/getDataElements methods that you saw in the preceding section to read the image data, manipulate them, and then write them back. But fortunately, the Java 2D API already supplies a number of filters that carry out common image processing operations for you.

The image manipulations all implement the BufferedImageOp interface. After you construct the operation, you simply call the filter method to transform an image into another.

BufferedImageOp op = . . .;
BufferedImage filteredImage
   = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
op.filter(image, filteredImage);

Some operations can transform an image in place (op.filter(image, image)), but most can’t.

Five classes implement the BufferedImageOp interface:

AffineTransformOp
RescaleOp
LookupOp
ColorConvertOp
ConvolveOp

The AffineTransformOp carries out an affine transformation on the pixels. For example, here is how you can rotate an image about its center:

AffineTransform transform = AffineTransform.getRotateInstance(Math.toRadians(angle),
   image.getWidth() / 2, image.getHeight() / 2);
AffineTransformOp op = new AffineTransformOp(transform, interpolation);
op.filter(image, filteredImage);

The AffineTransformOp constructor requires an affine transform and an interpolation strategy. Interpolation is necessary to determine pixels in the target image if the source pixels are transformed somewhere between target pixels. For example, if you rotate source pixels, then they will generally not fall exactly onto target pixels. There are two interpolation strategies: AffineTransformOp.TYPE_BILINEAR and AffineTransformOp.TYPE_NEAREST_NEIGHBOR. Bilinear interpolation takes a bit longer but looks better.

The program in Listing 7-7 lets you rotate an image by 5 degrees (see Figure 7-29).

A rotated image

Figure 7-29. A rotated image

The RescaleOp carries out a rescaling operation

xnew = a · x + b

for each of the color components in the image. (Alpha components are not affected.) The effect of rescaling with a > 1 is to brighten the image. You construct the RescaleOp by specifying the scaling parameters and optional rendering hints. In Listing 7-7, we use:

float a = 1.1f;
float 20.0f;
RescaleOp op = new RescaleOp(a, b, null);

You can also supply separate scaling values for each color component—see the API notes.

The LookupOp operation lets you specify an arbitrary mapping of sample values. You supply a table that specifies how each value should be mapped. In the example program, we compute the negative of all colors, changing the color c to 255 − c.

The LookupOp constructor requires an object of type LookupTable and a map of optional hints. The LookupTable class is abstract, with two concrete subclasses: ByteLookupTable and ShortLookupTable. Because RGB color values are bytes, a ByteLookupTable should suffice. However, because of the bug described in http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6183251, we will use a ShortLookupTable instead. Here is how we construct the LookupOp for the example program:

short negative[] = new short[256];
for (int i = 0; i < 256; i++) negative[i] = (short) (255 - i);
ShortLookupTable table = new ShortLookupTable(0, negative);
LookupOp op = new LookupOp(table, null);

The lookup is applied to each color component separately, but not to the alpha component. You can also supply different lookup tables for each color component—see the API notes.

Note

Note

You cannot apply a LookupOp to an image with an indexed color model. (In those images, each sample value is an offset into a color palette.)

The ColorConvertOp is useful for color space conversions. We do not discuss it here.

The most powerful of the transformations is the ConvolveOp, which carries out a mathematical convolution. We do not want to get too deeply into the mathematical details of convolution, but the basic idea is simple. Consider, for example, the blur filter (see Figure 7-30).

Blurring an image

Figure 7-30. Blurring an image

The blurring is achieved by replacement of each pixel with the average value from the pixel and its eight neighbors. Intuitively, it makes sense why this operation would blur out the picture. Mathematically, the averaging can be expressed as a convolution operation with the following kernel:

Blurring an image

The kernel of a convolution is a matrix that tells what weights should be applied to the neighboring values. The kernel above leads to a blurred image. A different kernel carries out edge detection, locating areas of color changes:

Blurring an image

Edge detection is an important technique for analyzing photographic images (see Figure 7-31).

Edge detection and inversion

Figure 7-31. Edge detection and inversion

To construct a convolution operation, you first set up an array of the values for the kernel and construct a Kernel object. Then, construct a ConvolveOp object from the kernel and use it for filtering.

float[] elements =
   {
      0.0f, -1.0f, 0.0f,
      -1.0f,  4.f, -1.0f,
      0.0f, -1.0f, 0.0f
   };
Kernel kernel = new Kernel(3, 3, elements);
ConvolveOp op = new ConvolveOp(kernel);
op.filter(image, filteredImage);

The program in Listing 7-7 allows a user to load in a GIF or JPEG image and carry out the image manipulations that we discussed. Thanks to the power of the image operations that the Java 2D API provides, the program is very simple.

Example 7-7. ImageProcessingTest.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.awt.geom.*;
  4. import java.awt.image.*;
  5. import java.io.*;
  6. import javax.imageio.*;
  7. import javax.swing.*;
  8. import javax.swing.filechooser.*;
  9.
 10. /**
 11.  * This program demonstrates various image processing operations.
 12.  * @version 1.03 2007-08-16
 13.  * @author Cay Horstmann
 14.  */
 15. public class ImageProcessingTest
 16. {
 17.    public static void main(String[] args)
 18.    {
 19.       EventQueue.invokeLater(new Runnable()
 20.          {
 21.             public void run()
 22.             {
 23.                JFrame frame = new ImageProcessingFrame();
 24.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 25.                frame.setVisible(true);
 26.             }
 27.          });
 28.    }
 29. }
 30.
 31. /**
 32.  * This frame has a menu to load an image and to specify various transformations, and
 33.  * a component to show the resulting image.
 34.  */
 35. class ImageProcessingFrame extends JFrame
 36. {
 37.    public ImageProcessingFrame()
 38.    {
 39.       setTitle("ImageProcessingTest");
 40.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 41.
 42.       add(new JComponent()
 43.          {
 44.             public void paintComponent(Graphics g)
 45.             {
 46.                if (image != null) g.drawImage(image, 0, 0, null);
 47.             }
 48.          });
 49.
 50.       JMenu fileMenu = new JMenu("File");
 51.       JMenuItem openItem = new JMenuItem("Open");
 52.       openItem.addActionListener(new ActionListener()
 53.          {
 54.             public void actionPerformed(ActionEvent event)
 55.             {
 56.                openFile();
 57.             }
 58.          });
 59.       fileMenu.add(openItem);
 60.
 61.       JMenuItem exitItem = new JMenuItem("Exit");
 62.       exitItem.addActionListener(new ActionListener()
 63.          {
 64.             public void actionPerformed(ActionEvent event)
 65.             {
 66.                System.exit(0);
 67.             }
 68.          });
 69.       fileMenu.add(exitItem);
 70.
 71.       JMenu editMenu = new JMenu("Edit");
 72.       JMenuItem blurItem = new JMenuItem("Blur");
 73.       blurItem.addActionListener(new ActionListener()
 74.          {
 75.             public void actionPerformed(ActionEvent event)
 76.             {
 77.                float weight = 1.0f / 9.0f;
 78.                float[] elements = new float[9];
 79.                for (int i = 0; i < 9; i++)
 80.                   elements[i] = weight;
 81.                convolve(elements);
 82.             }
 83.          });
 84.       editMenu.add(blurItem);
 85.
 86.       JMenuItem sharpenItem = new JMenuItem("Sharpen");
 87.       sharpenItem.addActionListener(new ActionListener()
 88.          {
 89.             public void actionPerformed(ActionEvent event)
 90.             {
 91.                float[] elements = { 0.0f, -1.0f, 0.0f, -1.0f, 5.f, -1.0f, 0.0f, -1.0f, 0.0f };
 92.                convolve(elements);
 93.             }
 94.          });
 95.       editMenu.add(sharpenItem);
 96.
 97.       JMenuItem brightenItem = new JMenuItem("Brighten");
 98.       brightenItem.addActionListener(new ActionListener()
 99.          {
100.             public void actionPerformed(ActionEvent event)
101.             {
102.                float a = 1.1f;
103.                // float b = 20.0f;
104.                float b = 0;
105.                RescaleOp op = new RescaleOp(a, b, null);
106.                filter(op);
107.             }
108.          });
109.       editMenu.add(brightenItem);
110.
111.       JMenuItem edgeDetectItem = new JMenuItem("Edge detect");
112.       edgeDetectItem.addActionListener(new ActionListener()
113.          {
114.             public void actionPerformed(ActionEvent event)
115.             {
116.                float[] elements = { 0.0f, -1.0f, 0.0f, -1.0f, 4.f, -1.0f, 0.0f, -1.0f, 0.0f };
117.                convolve(elements);
118.             }
119.          });
120.       editMenu.add(edgeDetectItem);
121.
122.       JMenuItem negativeItem = new JMenuItem("Negative");
123.       negativeItem.addActionListener(new ActionListener()
124.          {
125.             public void actionPerformed(ActionEvent event)
126.             {
127.                short[] negative = new short[256 * 1];
128.                for (int i = 0; i < 256; i++)
129.                   negative[i] = (short) (255 - i);
130.                ShortLookupTable table = new ShortLookupTable(0, negative);
131.                LookupOp op = new LookupOp(table, null);
132.                filter(op);
133.             }
134.          });
135.       editMenu.add(negativeItem);
136.
137.       JMenuItem rotateItem = new JMenuItem("Rotate");
138.       rotateItem.addActionListener(new ActionListener()
139.          {
140.             public void actionPerformed(ActionEvent event)
141.             {
142.                if (image == null) return;
143.                AffineTransform transform = AffineTransform.getRotateInstance(
144.                      Math.toRadians(5), image.getWidth() / 2, image.getHeight() / 2);
145.                AffineTransformOp op = new AffineTransformOp(transform,
146.                      AffineTransformOp.TYPE_BICUBIC);
147.                filter(op);
148.             }
149.          });
150.       editMenu.add(rotateItem);
151.
152.       JMenuBar menuBar = new JMenuBar();
153.       menuBar.add(fileMenu);
154.       menuBar.add(editMenu);
155.       setJMenuBar(menuBar);
156.    }
157.
158.    /**
159.     * Open a file and load the image.
160.     */
161.    public void openFile()
162.    {
163.       JFileChooser chooser = new JFileChooser();
164.       chooser.setCurrentDirectory(new File("."));
165.       String[] extensions = ImageIO.getReaderFileSuffixes();
166.       chooser.setFileFilter(new FileNameExtensionFilter("Image files", extensions));
167.       int r = chooser.showOpenDialog(this);
168.       if (r != JFileChooser.APPROVE_OPTION) return;
169.
170.       try
171.       {
172.          Image img = ImageIO.read(chooser.getSelectedFile());
173.          image = new BufferedImage(img.getWidth(null), img.getHeight(null),
174.                BufferedImage.TYPE_INT_RGB);
175.          image.getGraphics().drawImage(img, 0, 0, null);
176.       }
177.       catch (IOException e)
178.       {
179.          JOptionPane.showMessageDialog(this, e);
180.       }
181.       repaint();
182.    }
183.
184.    /**
185.     * Apply a filter and repaint.
186.     * @param op the image operation to apply
187.     */
188.    private void filter(BufferedImageOp op)
189.    {
190.       if (image == null) return;
191.       image = op.filter(image, null);
192.       repaint();
193.    }
194.
195.    /**
196.     * Apply a convolution and repaint.
197.     * @param elements the convolution kernel (an array of 9 matrix elements)
198.     */
199.    private void convolve(float[] elements)
200.    {
201.       Kernel kernel = new Kernel(3, 3, elements);
202.       ConvolveOp op = new ConvolveOp(kernel);
203.       filter(op);
204.    }
205.
206.    private BufferedImage image;
207.    private static final int DEFAULT_WIDTH = 400;
208.    private static final int DEFAULT_HEIGHT = 400;
209. }

 

Printing

The original JDK had no support for printing at all. It was not possible to print from applets, and you had to get a third-party library if you wanted to print in an application. JDK 1.1 introduced very lightweight printing support, just enough to produce simple printouts, as long as you were not too particular about the print quality. The 1.1 printing model was designed to allow browser vendors to print the surface of an applet as it appears on a web page (which, however, the browser vendors have not embraced).

Java SE 1.2 introduced the beginnings of a robust printing model that is fully integrated with 2D graphics. Java SE 1.4 added important enhancements, such as discovery of printer features and streaming print jobs for server-side print management.

In this section, we show you how you can easily print a drawing on a single sheet of paper, how you can manage a multipage printout, and how you can benefit from the elegance of the Java 2D imaging model and easily generate a print preview dialog box.

Note

Note

The Java platform also supports the printing of user interface components. We do not cover this topic because it is mostly of interest to implementors of browsers, screen grabbers, and so on. For more information on printing components, see http://java.sun.com/developer/onlineTraining/Programming/JDCBook/render.html.

Graphics Printing

In this section, we tackle what is probably the most common printing situation: printing a 2D graphic. Of course, the graphic can contain text in various fonts or even consist entirely of text.

To generate a printout, you take care of these two tasks:

  • Supply an object that implements the Printable interface.

  • Start a print job.

The Printable interface has a single method:

int print(Graphics g, PageFormat format, int page)

That method is called whenever the print engine needs to have a page formatted for printing. Your code draws the text and image that are to be printed onto the graphics context. The page format tells you the paper size and the print margins. The page number tells you which page to render.

To start a print job, you use the PrinterJob class. First, you call the static getPrinterJob method to get a print job object. Then set the Printable object that you want to print.

Printable canvas = . . .;
PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintable(canvas);

Caution

Caution

The class PrintJob handles JDK 1.1-style printing. That class is now obsolete. Do not confuse it with the PrinterJob class.

Before starting the print job, you should call the printDialog method to display a print dialog box (see Figure 7-32). That dialog box gives the user a chance to select the printer to be used (in case multiple printers are available), the page range that should be printed, and various printer settings.

A cross-platform print dialog box

Figure 7-32. A cross-platform print dialog box

You collect printer settings in an object of a class that implements the PrintRequestAttributeSet interface, such as the HashPrintRequestAttributeSet class.

HashPrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();

Add attribute settings and pass the attributes object to the printDialog method.

The printDialog method returns true if the user clicked OK and false if the user canceled the dialog box. If the user accepted, call the print method of the PrinterJob class to start the printing process. The print method might throw a PrinterException. Here is the outline of the printing code:

if (job.printDialog(attributes))
{
   try
   {
      job.print(attributes);
   }
   catch (PrinterException exception)
   {
     . . .
   }
}

Note

Note

Prior to JDK 1.4, the printing system used the native print and page setup dialog boxes of the host platform. To show a native print dialog box, call the printDialog method with no parameters. (There is no way to collect user settings in an attribute set.)

During printing, the print method of the PrinterJob class makes repeated calls to the print method of the Printable object associated with the job.

Because the job does not know how many pages you want to print, it simply keeps calling the print method. As long as the print method returns the value Printable.PAGE_EXISTS, the print job keeps producing pages. When the print method returns Printable.NO_SUCH_PAGE, the print job stops.

Caution

Caution

The page numbers that the print job passes to the print method start with page 0.

Therefore, the print job doesn’t have an accurate page count until after the printout is complete. For that reason, the print dialog box can’t display the correct page range and instead displays a page range of “Pages 1 to 1.” You will see in the next section how to avoid this blemish by supplying a Book object to the print job.

During the printing process, the print job repeatedly calls the print method of the Printable object. The print job is allowed to make multiple calls for the same page. You should therefore not count pages inside the print method but always rely on the page number parameter. There is a good reason why the print job might call the print method repeatedly for the same page. Some printers, in particular dot-matrix and inkjet printers, use banding. They print one band at a time, advance the paper, and then print the next band. The print job might use banding even for laser printers that print a full page at a time—it gives the print job a way of managing the size of the spool file.

If the print job needs the Printable object to print a band, then it sets the clip area of the graphics context to the requested band and calls the print method. Its drawing operations are clipped against the band rectangle, and only those drawing elements that show up in the band are rendered. Your print method need not be aware of that process, with one caveat: It should not interfere with the clip area.

Caution

Caution

The Graphics object that your print method gets is also clipped against the page margins. If you replace the clip area, you can draw outside the margins. Especially in a printer graphics context, the clipping area must be respected. Call clip, not setClip, to further restrict the clipping area. If you must remove a clip area, then make sure to call getClip at the beginning of your print method and restore that clip area.

The PageFormat parameter of the print method contains information about the printed page. The methods getWidth and getHeight return the paper size, measured in points. One point is 1/72 of an inch. (An inch equals 25.4 millimeters.) For example, A4 paper is approximately 595 × 842 points, and U.S. letter-size paper is 612 × 792 points.

Points are a common measurement in the printing trade in the United States. Much to the chagrin of the rest of the world, the printing package uses point units for two purposes. Paper sizes and paper margins are measured in points. And the default unit for all print graphics contexts is one point. You can verify that in the example program at the end of this section. The program prints two lines of text that are 72 units apart. Run the example program and measure the distance between the baselines. They are exactly 1 inch or 25.4 millimeters apart.

The getWidth and getHeight methods of the PageFormat class give you the complete paper size. Not all of the paper area is printable. Users typically select margins, and even if they don’t, printers need to somehow grip the sheets of paper on which they print and therefore have a small unprintable area around the edges.

The methods getImageableWidth and getImageableHeight tell you the dimensions of the area that you can actually fill. However, the margins need not be symmetrical, so you must also know the top-left corner of the imageable area (see Figure 7-33), which you obtain by the methods getImageableX and getImageableY.

Page format measurements

Figure 7-33. Page format measurements

Tip

Tip

The graphics context that you receive in the print method is clipped to exclude the margins, but the origin of the coordinate system is nevertheless the top-left corner of the paper. It makes sense to translate the coordinate system to start at the top-left corner of the imageable area. Simply start your print method with

g.translate(pageFormat.getImageableX(), pageFormat.getImageableY());

If you want your users to choose the settings for the page margins or to switch between portrait and landscape orientation without setting other printing attributes, you can call the pageDialog method of the PrinterJob class:

PageFormat format = job.pageDialog(attributes);

Note

Note

One of the tabs of the print dialog box contains the page setup dialog box (see Figure 7-34). You might still want to give users an option to set the page format before printing, especially if your program presents a “what you see is what you get” display of the pages to be printed. The pageDialog method returns a PageFormat object with the user settings.

A cross-platform page setup dialog box

Figure 7-34. A cross-platform page setup dialog box

Listing 7-8 shows how to render the same set of shapes on the screen and on the printed page. A subclass of JPanel implements the Printable interface. Both the paintComponent and the print methods call the same method to carry out the actual drawing.

class PrintPanel extends JPanel implements Printable
{
   public void paintComponent(Graphics g)
   {
      super.paintComponent(g);
      Graphics2D g2 = (Graphics2D) g;
      drawPage(g2);
   }

   public int print(Graphics g, PageFormat pf, int page)
      throws PrinterException
   {
      if (page >= 1) return Printable.NO_SUCH_PAGE;
      Graphics2D g2 = (Graphics2D) g;
      g2.translate(pf.getImageableX(), pf.getImageableY());
      drawPage(g2);
      return Printable.PAGE_EXISTS;
   }

   public void drawPage(Graphics2D g2)
   {
      // shared drawing code goes here
      . . .
   }
   . . .
}

This example displays and prints the image shown in Figure 7-20 on page 558, namely, the outline of the message “Hello, World” that is used as a clipping area for a pattern of lines.

Click the Print button to start printing, or click the Page setup button to open the page setup dialog box. Listing 7-8 shows the code.

Note

Note

To show a native page setup dialog box, you pass a default PageFormat object to the pageDialog method. The method clones that object, modifies it according to the user selections in the dialog box, and returns the cloned object.

PageFormat defaultFormat = printJob.defaultPage();
PageFormat selectedFormat = printJob.pageDialog(defaultFormat);

Example 7-8. PrintTest.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.awt.font.*;
  4. import java.awt.geom.*;
  5. import java.awt.print.*;
  6. import javax.print.attribute.*;
  7. import javax.swing.*;
  8.
  9. /**
 10.  * This program demonstrates how to print 2D graphics
 11.  * @version 1.12 2007-08-16
 12.  * @author Cay Horstmann
 13.  */
 14. public class PrintTest
 15. {
 16.    public static void main(String[] args)
 17.    {
 18.       EventQueue.invokeLater(new Runnable()
 19.          {
 20.             public void run()
 21.             {
 22.                JFrame frame = new PrintTestFrame();
 23.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 24.                frame.setVisible(true);
 25.             }
 26.          });
 27.    }
 28. }
 29.
 30. /**
 31.  * This frame shows a panel with 2D graphics and buttons to print the graphics and to
 32.  * set up the page format.
 33.  */
 34. class PrintTestFrame extends JFrame
 35. {
 36.    public PrintTestFrame()
 37.    {
 38.       setTitle("PrintTest");
 39.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 40.
 41.       canvas = new PrintComponent();
 42.       add(canvas, BorderLayout.CENTER);
 43.
 44.       attributes = new HashPrintRequestAttributeSet();
 45.
 46.       JPanel buttonPanel = new JPanel();
 47.       JButton printButton = new JButton("Print");
 48.       buttonPanel.add(printButton);
 49.       printButton.addActionListener(new ActionListener()
 50.          {
 51.             public void actionPerformed(ActionEvent event)
 52.             {
 53.                try
 54.                {
 55.                   PrinterJob job = PrinterJob.getPrinterJob();
 56.                   job.setPrintable(canvas);
 57.                   if (job.printDialog(attributes)) job.print(attributes);
 58.                }
 59.                catch (PrinterException e)
 60.                {
 61.                   JOptionPane.showMessageDialog(PrintTestFrame.this, e);
 62.                }
 63.             }
 64.          });
 65.
 66.       JButton pageSetupButton = new JButton("Page setup");
 67.       buttonPanel.add(pageSetupButton);
 68.       pageSetupButton.addActionListener(new ActionListener()
 69.          {
 70.             public void actionPerformed(ActionEvent event)
 71.             {
 72.                PrinterJob job = PrinterJob.getPrinterJob();
 73.                job.pageDialog(attributes);
 74.             }
 75.          });
 76.
 77.       add(buttonPanel, BorderLayout.NORTH);
 78.    }
 79.
 80.    private PrintComponent canvas;
 81.    private PrintRequestAttributeSet attributes;
 82.
 83.    private static final int DEFAULT_WIDTH = 300;
 84.    private static final int DEFAULT_HEIGHT = 300;
 85. }
 86.
 87. /**
 88.  * This component generates a 2D graphics image for screen display and printing.
 89.  */
 90. class PrintComponent extends JComponent implements Printable
 91. {
 92.    public void paintComponent(Graphics g)
 93.    {
 94.       Graphics2D g2 = (Graphics2D) g;
 95.       drawPage(g2);
 96.    }
 97.
 98.    public int print(Graphics g, PageFormat pf, int page) throws PrinterException
 99.    {
100.       if (page >= 1) return Printable.NO_SUCH_PAGE;
101.       Graphics2D g2 = (Graphics2D) g;
102.       g2.translate(pf.getImageableX(), pf.getImageableY());
103.       g2.draw(new Rectangle2D.Double(0, 0, pf.getImageableWidth(), pf.getImageableHeight()));
104.
105.       drawPage(g2);
106.       return Printable.PAGE_EXISTS;
107.    }
108.
109.    /**
110.     * This method draws the page both on the screen and the printer graphics context.
111.     * @param g2 the graphics context
112.     */
113.    public void drawPage(Graphics2D g2)
114.    {
115.       FontRenderContext context = g2.getFontRenderContext();
116.       Font f = new Font("Serif", Font.PLAIN, 72);
117.       GeneralPath clipShape = new GeneralPath();
118.
119.       TextLayout layout = new TextLayout("Hello", f, context);
120.       AffineTransform transform = AffineTransform.getTranslateInstance(0, 72);
121.       Shape outline = layout.getOutline(transform);
122.       clipShape.append(outline, false);
123.
124.       layout = new TextLayout("World", f, context);
125.       transform = AffineTransform.getTranslateInstance(0, 144);
126.       outline = layout.getOutline(transform);
127.       clipShape.append(outline, false);
128.
129.       g2.draw(clipShape);
130.       g2.clip(clipShape);
131.
132.       final int NLINES = 50;
133.       Point2D p = new Point2D.Double(0, 0);
134.       for (int i = 0; i < NLINES; i++)
135.       {
136.          double x = (2 * getWidth() * i) / NLINES;
137.          double y = (2 * getHeight() * (NLINES - 1 - i)) / NLINES;
138.          Point2D q = new Point2D.Double(x, y);
139.          g2.draw(new Line2D.Double(p, q));
140.       }
141.    }
142. }

 

Multiple-Page Printing

In practice, you usually shouldn’t pass a raw Printable object to a print job. Instead, you should obtain an object of a class that implements the Pageable interface. The Java platform supplies one such class, called Book. A book is made up of sections, each of which is a Printable object. You make a book by adding Printable objects and their page counts.

Book book = new Book();
Printable coverPage = . . .;
Printable bodyPages = . . .;
book.append(coverPage, pageFormat); // append 1 page
book.append(bodyPages, pageFormat, pageCount);

Then, you use the setPageable method to pass the Book object to the print job.

printJob.setPageable(book);

Now the print job knows exactly how many pages to print. Then, the print dialog box displays an accurate page range, and the user can select the entire range or subranges.

Caution

Caution

When the print job calls the print methods of the Printable sections, it passes the current page number of the book, and not of each section, as the current page number. That is a huge pain—each section must know the page counts of the preceding sections to make sense of the page number parameter.

From your perspective as a programmer, the biggest challenge about using the Book class is that you must know how many pages each section will have when you print it. Your Printable class needs a layout algorithm that computes the layout of the material on the printed pages. Before printing starts, invoke that algorithm to compute the page breaks and the page count. You can retain the layout information so you have it handy during the printing process.

You must guard against the possibility that the user has changed the page format. If that happens, you must recompute the layout, even if the information that you want to print has not changed.

Listing 7-9 shows how to produce a multipage printout. This program prints a message in very large characters on a number of pages (see Figure 7-35). You can then trim the margins and tape the pages together to form a banner.

A banner

Figure 7-35. A banner

The layoutPages method of the Banner class computes the layout. We first lay out the message string in a 72-point font. We then compute the height of the resulting string and compare it with the imageable height of the page. We derive a scale factor from these two measurements. When printing the string, we magnify it by that scale factor.

Caution

Caution

To lay out your information precisely, you usually need access to the printer graphics context. Unfortunately, there is no way to obtain that graphics context until printing actually starts. In our example program, we make do with the screen graphics context and hope that the font metrics of the screen and printer match.

The getPageCount method of the Banner class first calls the layout method. Then it scales up the width of the string and divides it by the imageable width of each page. The quotient, rounded up to the next integer, is the page count.

It sounds like it might be difficult to print the banner because characters can be broken across multiple pages. However, thanks to the power of the Java 2D API, this turns out not to be a problem at all. When a particular page is requested, we simply use the translate method of the Graphics2D class to shift the top-left corner of the string to the left. Then, we set a clip rectangle that equals the current page (see Figure 7-36). Finally, we scale the graphics context with the scale factor that the layout method computed.

Printing a page of a banner

Figure 7-36. Printing a page of a banner

This example shows the power of transformations. The drawing code is kept simple, and the transformation does all the work of placing the drawing at the appropriate place. Finally, the clip cuts away the part of the image that falls outside the page. In the next section, you will see another compelling use of transformations, to display a print preview.

Print Preview

Most professional programs have a print preview mechanism that lets you look at your pages on the screen so that you won’t waste paper on a printout that you don’t like. The printing classes of the Java platform do not supply a standard “print preview” dialog box, but it is easy to design your own (see Figure 7-37). In this section, we show you how. The PrintPreviewDialog class in Listing 7-9 is completely generic—you can reuse it to preview any kind of printout.

The print preview dialog, showing a banner page

Figure 7-37. The print preview dialog, showing a banner page

To construct a PrintPreviewDialog, you supply either a Printable or a Book, together with a PageFormat object. The surface of the dialog box contains a PrintPreviewCanvas. As you use the Next and Previous buttons to flip through the pages, the paintComponent method calls the print method of the Printable object for the requested page.

Normally, the print method draws the page context on a printer graphics context. However, we supply the screen graphics context, suitably scaled so that the entire printed page fits inside a small screen rectangle.

float xoff = . . .; // left of page
float yoff = . . .; // top of page
float scale = . . .; // to fit printed page onto screen
g2.translate(xoff, yoff);
g2.scale(scale, scale);
Printable printable = book.getPrintable(currentPage);
printable.print(g2, pageFormat, currentPage);

The print method never knows that it doesn’t actually produce printed pages. It simply draws onto the graphics context, thereby producing a microscopic print preview on the screen. This is a compelling demonstration of the power of the Java 2D imaging model.

Listing 7-9 contains the code for the banner printing program and the print preview dialog box. Type “Hello, World!” into the text field and look at the print preview, then print the banner.

Example 7-9. BookTest.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.awt.font.*;
  4. import java.awt.geom.*;
  5. import java.awt.print.*;
  6. import javax.print.attribute.*;
  7. import javax.swing.*;
  8.
  9. /**
 10.  * This program demonstrates the printing of a multipage book. It prints a "banner", by
 11.  * blowing up a text string to fill the entire page vertically. The program also contains a
 12.  * generic print preview dialog.
 13.  * @version 1.12 2007-08-16
 14.  * @author Cay Horstmann
 15.  */
 16. public class BookTest
 17. {
 18.    public static void main(String[] args)
 19.    {
 20.       EventQueue.invokeLater(new Runnable()
 21.          {
 22.             public void run()
 23.             {
 24.                JFrame frame = new BookTestFrame();
 25.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 26.                frame.setVisible(true);
 27.             }
 28.          });
 29.    }
 30. }
 31.
 32. /**
 33.  * This frame has a text field for the banner text and buttons for printing, page setup,
 34.  * and print preview.
 35.  */
 36. class BookTestFrame extends JFrame
 37. {
 38.    public BookTestFrame()
 39.    {
 40.       setTitle("BookTest");
 41.
 42.       text = new JTextField();
 43.       add(text, BorderLayout.NORTH);
 44.
 45.       attributes = new HashPrintRequestAttributeSet();
 46.
 47.       JPanel buttonPanel = new JPanel();
 48.
 49.       JButton printButton = new JButton("Print");
 50.       buttonPanel.add(printButton);
 51.       printButton.addActionListener(new ActionListener()
 52.          {
 53.             public void actionPerformed(ActionEvent event)
 54.             {
 55.                try
 56.                {
 57.                   PrinterJob job = PrinterJob.getPrinterJob();
 58.                   job.setPageable(makeBook());
 59.                   if (job.printDialog(attributes))
 60.                   {
 61.                     job.print(attributes);
 62.                   }
 63.                }
 64.                catch (PrinterException e)
 65.                {
 66.                   JOptionPane.showMessageDialog(BookTestFrame.this, e);
 67.                }
 68.             }
 69.          });
 70.
 71.       JButton pageSetupButton = new JButton("Page setup");
 72.       buttonPanel.add(pageSetupButton);
 73.       pageSetupButton.addActionListener(new ActionListener()
 74.          {
 75.             public void actionPerformed(ActionEvent event)
 76.             {
 77.                PrinterJob job = PrinterJob.getPrinterJob();
 78.                pageFormat = job.pageDialog(attributes);
 79.             }
 80.          });
 81.
 82.       JButton printPreviewButton = new JButton("Print preview");
 83.       buttonPanel.add(printPreviewButton);
 84.       printPreviewButton.addActionListener(new ActionListener()
 85.          {
 86.             public void actionPerformed(ActionEvent event)
 87.             {
 88.                PrintPreviewDialog dialog = new PrintPreviewDialog(makeBook());
 89.                dialog.setVisible(true);
 90.             }
 91.          });
 92.
 93.       add(buttonPanel, BorderLayout.SOUTH);
 94.       pack();
 95.    }
 96.
 97.    /**
 98.     * Makes a book that contains a cover page and the pages for the banner.
 99.     */
100.    public Book makeBook()
101.    {
102.       if (pageFormat == null)
103.       {
104.          PrinterJob job = PrinterJob.getPrinterJob();
105.          pageFormat = job.defaultPage();
106.       }
107.       Book book = new Book();
108.       String message = text.getText();
109.       Banner banner = new Banner(message);
110.       int pageCount = banner.getPageCount((Graphics2D) getGraphics(), pageFormat);
111.       book.append(new CoverPage(message + " (" + pageCount + " pages)"), pageFormat);
112.       book.append(banner, pageFormat, pageCount);
113.       return book;
114.    }
115.
116.    private JTextField text;
117.    private PageFormat pageFormat;
118.    private PrintRequestAttributeSet attributes;
119. }
120.
121. /**
122.  * A banner that prints a text string on multiple pages.
123.  */
124. class Banner implements Printable
125. {
126.    /**
127.     * Constructs a banner
128.     * @param m the message string
129.     */
130.    public Banner(String m)
131.    {
132.       message = m;
133.    }
134.
135.    /**
136.     * Gets the page count of this section.
137.     * @param g2 the graphics context
138.     * @param pf the page format
139.     * @return the number of pages needed
140.     */
141.    public int getPageCount(Graphics2D g2, PageFormat pf)
142.    {
143.       if (message.equals("")) return 0;
144.       FontRenderContext context = g2.getFontRenderContext();
145.       Font f = new Font("Serif", Font.PLAIN, 72);
146.       Rectangle2D bounds = f.getStringBounds(message, context);
147.       scale = pf.getImageableHeight() / bounds.getHeight();
148.       double width = scale * bounds.getWidth();
149.       int pages = (int) Math.ceil(width / pf.getImageableWidth());
150.       return pages;
151.    }
152.
153.    public int print(Graphics g, PageFormat pf, int page) throws PrinterException
154.    {
155.       Graphics2D g2 = (Graphics2D) g;
156.       if (page > getPageCount(g2, pf)) return Printable.NO_SUCH_PAGE;
157.       g2.translate(pf.getImageableX(), pf.getImageableY());
158.
159.       drawPage(g2, pf, page);
160.       return Printable.PAGE_EXISTS;
161.    }
162.
163.    public void drawPage(Graphics2D g2, PageFormat pf, int page)
164.    {
165.       if (message.equals("")) return;
166.       page--; // account for cover page
167.
168.       drawCropMarks(g2, pf);
169.       g2.clip(new Rectangle2D.Double(0, 0, pf.getImageableWidth(), pf.getImageableHeight()));
170.       g2.translate(-page * pf.getImageableWidth(), 0);
171.       g2.scale(scale, scale);
172.       FontRenderContext context = g2.getFontRenderContext();
173.       Font f = new Font("Serif", Font.PLAIN, 72);
174.       TextLayout layout = new TextLayout(message, f, context);
175.       AffineTransform transform = AffineTransform.getTranslateInstance(0, layout.getAscent());
176.       Shape outline = layout.getOutline(transform);
177.       g2.draw(outline);
178.    }
179.
180.    /**
181.     * Draws 1/2" crop marks in the corners of the page.
182.     * @param g2 the graphics context
183.     * @param pf the page format
184.     */
185.    public void drawCropMarks(Graphics2D g2, PageFormat pf)
186.    {
187.       final double C = 36; // crop mark length = 1/2 inch
188.       double w = pf.getImageableWidth();
189.       double h = pf.getImageableHeight();
190.       g2.draw(new Line2D.Double(0, 0, 0, C));
191.       g2.draw(new Line2D.Double(0, 0, C, 0));
192.       g2.draw(new Line2D.Double(w, 0, w, C));
193.       g2.draw(new Line2D.Double(w, 0, w - C, 0));
194.       g2.draw(new Line2D.Double(0, h, 0, h - C));
195.       g2.draw(new Line2D.Double(0, h, C, h));
196.       g2.draw(new Line2D.Double(w, h, w, h - C));
197.       g2.draw(new Line2D.Double(w, h, w - C, h));
198.    }
199.
200.    private String message;
201.    private double scale;
202. }
203.
204. /**
205.  * This class prints a cover page with a title.
206.  */
207. class CoverPage implements Printable
208. {
209.    /**
210.     * Constructs a cover page.
211.     * @param t the title
212.     */
213.    public CoverPage(String t)
214.    {
215.       title = t;
216.    }
217.
218.    public int print(Graphics g, PageFormat pf, int page) throws PrinterException
219.    {
220.       if (page >= 1) return Printable.NO_SUCH_PAGE;
221.       Graphics2D g2 = (Graphics2D) g;
222.       g2.setPaint(Color.black);
223.       g2.translate(pf.getImageableX(), pf.getImageableY());
224.       FontRenderContext context = g2.getFontRenderContext();
225.       Font f = g2.getFont();
226.       TextLayout layout = new TextLayout(title, f, context);
227.       float ascent = layout.getAscent();
228.       g2.drawString(title, 0, ascent);
229.       return Printable.PAGE_EXISTS;
230.    }
231.
232.    private String title;
233. }
234.
235. /**
236.  * This class implements a generic print preview dialog.
237.  */
238. class PrintPreviewDialog extends JDialog
239. {
240.    /**
241.     * Constructs a print preview dialog.
242.     * @param p a Printable
243.     * @param pf the page format
244.     * @param pages the number of pages in p
245.     */
246.    public PrintPreviewDialog(Printable p, PageFormat pf, int pages)
247.    {
248.       Book book = new Book();
249.       book.append(p, pf, pages);
250.       layoutUI(book);
251.    }
252.
253.    /**
254.     * Constructs a print preview dialog.
255.     * @param b a Book
256.     */
257.    public PrintPreviewDialog(Book b)
258.    {
259.       layoutUI(b);
260.    }
261.
262.    /**
263.     * Lays out the UI of the dialog.
264.     * @param book the book to be previewed
265.     */
266.    public void layoutUI(Book book)
267.    {
268.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
269.
270.       canvas = new PrintPreviewCanvas(book);
271.       add(canvas, BorderLayout.CENTER);
272.
273.       JPanel buttonPanel = new JPanel();
274.
275.       JButton nextButton = new JButton("Next");
276.       buttonPanel.add(nextButton);
277.       nextButton.addActionListener(new ActionListener()
278.          {
279.             public void actionPerformed(ActionEvent event)
280.             {
281.                canvas.flipPage(1);
282.             }
283.          });
284.
285.       JButton previousButton = new JButton("Previous");
286.       buttonPanel.add(previousButton);
287.       previousButton.addActionListener(new ActionListener()
288.          {
289.             public void actionPerformed(ActionEvent event)
290.             {
291.                canvas.flipPage(-1);
292.             }
293.          });
294.
295.       JButton closeButton = new JButton("Close");
296.       buttonPanel.add(closeButton);
297.       closeButton.addActionListener(new ActionListener()
298.          {
299.             public void actionPerformed(ActionEvent event)
300.             {
301.                setVisible(false);
302.             }
303.          });
304.
305.       add(buttonPanel, BorderLayout.SOUTH);
306.    }
307.
308.    private PrintPreviewCanvas canvas;
309.
310.    private static final int DEFAULT_WIDTH = 300;
311.    private static final int DEFAULT_HEIGHT = 300;
312. }
313.
314. /**
315.  * The canvas for displaying the print preview.
316.  */
317. class PrintPreviewCanvas extends JComponent
318. {
319.    /**
320.     * Constructs a print preview canvas.
321.     * @param b the book to be previewed
322.     */
323.    public PrintPreviewCanvas(Book b)
324.    {
325.       book = b;
326.       currentPage = 0;
327.    }
328.
329.    public void paintComponent(Graphics g)
330.    {
331.       Graphics2D g2 = (Graphics2D) g;
332.       PageFormat pageFormat = book.getPageFormat(currentPage);
333.
334.       double xoff; // x offset of page start in window
335.       double yoff; // y offset of page start in window
336.       double scale; // scale factor to fit page in window
337.       double px = pageFormat.getWidth();
338.       double py = pageFormat.getHeight();
339.       double sx = getWidth() - 1;
340.       double sy = getHeight() - 1;
341.       if (px / py < sx / sy) // center horizontally
342.       {
343.          scale = sy / py;
344.          xoff = 0.5 * (sx - scale * px);
345.          yoff = 0;
346.       }
347.       else
348.       // center vertically
349.       {
350.          scale = sx / px;
351.          xoff = 0;
352.          yoff = 0.5 * (sy - scale * py);
353.       }
354.       g2.translate((float) xoff, (float) yoff);
355.       g2.scale((float) scale, (float) scale);
356.
357.       // draw page outline (ignoring margins)
358.       Rectangle2D page = new Rectangle2D.Double(0, 0, px, py);
359.       g2.setPaint(Color.white);
360.       g2.fill(page);
361.       g2.setPaint(Color.black);
362.       g2.draw(page);
363.
364.       Printable printable = book.getPrintable(currentPage);
365.       try
366.       {
367.          printable.print(g2, pageFormat, currentPage);
368.       }
369.       catch (PrinterException e)
370.       {
371.          g2.draw(new Line2D.Double(0, 0, px, py));
372.          g2.draw(new Line2D.Double(px, 0, 0, py));
373.       }
374.    }
375.
376.    /**
377.     * Flip the book by the given number of pages.
378.     * @param by the number of pages to flip by. Negative values flip backwards.
379.     */
380.    public void flipPage(int by)
381.    {
382.       int newPage = currentPage + by;
383.       if (0 <= newPage && newPage < book.getNumberOfPages())
384.       {
385.          currentPage = newPage;
386.          repaint();
387.       }
388.    }
389.
390.    private Book book;
391.    private int currentPage;
392. }

 

Print Services

So far, you have seen how to print 2D graphics. However, the printing API introduced in Java SE 1.4 affords far greater flexibility. The API defines a number of data types and lets you find print services that are able to print them. Among the data types:

  • Images in GIF, JPEG, or PNG format.

  • Documents in text, HTML, PostScript, or PDF format.

  • Raw printer code data.

  • Objects of a class that implements Printable, Pageable, or RenderableImage.

The data themselves can be stored in a source of bytes or characters such as an input stream, a URL, or an array. A document flavor describes the combination of a data source and a data type. The DocFlavor class defines a number of inner classes for the various data sources. Each of the inner classes defines constants to specify the flavors. For example, the constant

DocFlavor.INPUT_STREAM.GIF

describes a GIF image that is read from an input stream. Table 7-3 lists the combinations.

Table 7-3. Document Flavors for Print Services

Data Source

Data Type

MIME Type

INPUT_STREAM

GIF

image/gif

URL

JPEG

image/jpeg

BYTE_ARRAY

PNG

image/png

 

POSTSCRIPT

application/postscript

 

PDF

application/pdf

 

TEXT_HTML_HOST

text/html (using host encoding)

 

TEXT_HTML_US_ASCII

text/html; charset=us-ascii

 

TEXT_HTML_UTF_8

text/html; charset=utf-8

 

TEXT_HTML_UTF_16

text/html; charset=utf-16

 

TEXT_HTML_UTF_16LE

text/html; charset=utf-16le (little-endian)

 

TEXT_HTML_UTF_16BE

text/html; charset=utf-16be (big-endian)

 

TEXT_PLAIN_HOST

text/plain (using host encoding)

 

TEXT_PLAIN_US_ASCII

text/plain; charset=us-ascii

 

TEXT_PLAIN_UTF_8

text/plain; charset=utf-8

 

TEXT_PLAIN_UTF_16

text/plain; charset=utf-16

 

TEXT_PLAIN_UTF_16LE

text/plain; charset=utf-16le (little-endian)

 

TEXT_PLAIN_UTF_16BE

text/plain; charset=utf-16be (big-endian)

 

PCL

application/vnd.hp-PCL (Hewlett Packard Printer Control Language)

 

AUTOSENSE

application/octet-stream (raw printer data)

READER

TEXT_HTML

text/html; charset=utf-16

STRING

TEXT_PLAIN

text/plain; charset=utf-16

CHAR_ARRAY

  

SERVICE_FORMATTED

PRINTABLE

N/A

 

PAGEABLE

N/A

 

RENDERABLE_IMAGE

N/A

Suppose you want to print a GIF image that is located in a file. First find out whether there is a print service that is capable of handling the task. The static lookupPrintServices method of the PrintServiceLookup class returns an array of PrintService objects that can handle the given document flavor.

DocFlavor flavor = DocFlavor.INPUT_STREAM.GIF;
PrintService[] services
   = PrintServiceLookup.lookupPrintServices(flavor, null);

The second parameter of the lookupPrintServices method is null to indicate that we don’t want to constrain the search by specifying printer attributes. We cover attributes in the next section.

Note

Note

Java SE 6 supplies print services for basic document flavors such as images and 2D graphics, but if you try to print text or HTML documents, the lookup will return an empty array.

If the lookup yields an array with more than one element, you select from the listed print services. You can call the getName method of the PrintService class to get the printer names, and then let the user choose.

Next, get a document print job from the service:

DocPrintJob job = services[i].createPrintJob();

For printing, you need an object that implements the Doc interface. The Java library supplies a class SimpleDoc for that purpose. The SimpleDoc constructor requires the data source object, the document flavor, and an optional attribute set. For example,

InputStream in = new FileInputStream(fileName);
Doc doc = new SimpleDoc(in, flavor, null);

Finally, you are ready to print:

job.print(doc, null);

As before, the null parameter can be replaced by an attribute set.

Note that this printing process is quite different from that of the preceding section. There is no user interaction through print dialog boxes. For example, you can implement a server-side printing mechanism in which users submit print jobs through a web form.

The program in Listing 7-10 demonstrates how to use a print service to print an image file.

Example 7-10. PrintServiceTest.java

 1. import java.io.*;
 2. import javax.print.*;
 3.
 4. /**
 5.  * This program demonstrates the use of print services. The program lets you print a GIF
 6.  * image to any of the print services that support the GIF document flavor.
 7.  * @version 1.10 2007-08-16
 8.  * @author Cay Horstmann
 9.  */
10. public class PrintServiceTest
11. {
12.    public static void main(String[] args)
13.    {
14.       DocFlavor flavor = DocFlavor.URL.GIF;
15.       PrintService[] services = PrintServiceLookup.lookupPrintServices(flavor, null);
16.       if (args.length == 0)
17.       {
18.          if (services.length == 0) System.out.println("No printer for flavor " + flavor);
19.          else
20.          {
21.             System.out.println("Specify a file of flavor " + flavor
22.                   + "
and optionally the number of the desired printer.");
23.             for (int i = 0; i < services.length; i++)
24.                System.out.println((i + 1) + ": " + services[i].getName());
25.          }
26.          System.exit(0);
27.       }
28.       String fileName = args[0];
29.       int p = 1;
30.       if (args.length > 1) p = Integer.parseInt(args[1]);
31.       try
32.       {
33.          if (fileName == null) return;
34.          FileInputStream in = new FileInputStream(fileName);
35.          Doc doc = new SimpleDoc(in, flavor, null);
36.          DocPrintJob job = services[p - 1].createPrintJob();
37.          job.print(doc, null);
38.       }
39.       catch (FileNotFoundException e)
40.       {
41.          e.printStackTrace();
42.       }
43.       catch (PrintException e)
44.       {
45.          e.printStackTrace();
46.       }
47.    }
48. }

Stream Print Services

A print service sends print data to a printer. A stream print service generates the same print data but instead sends them to a stream, perhaps for delayed printing or because the print data format can be interpreted by other programs. In particular, if the print data format is PostScript, then it is useful to save the print data to a file because many programs can process PostScript files. The Java platform includes a stream print service that can produce PostScript output from images and 2D graphics. You can use that service on all systems, even if there are no local printers.

Enumerating stream print services is a bit more tedious than locating regular print services. You need both the DocFlavor of the object to be printed and the MIME type of the stream output. You then get a StreamPrintServiceFactory array of factories.

DocFlavor flavor = DocFlavor.SERVICE_FORMATTED.PRINTABLE;
String mimeType = "application/postscript";
StreamPrintServiceFactory[] factories
   = StreamPrintServiceFactory.lookupStreamPrintServiceFactories(flavor, mimeType);

The StreamPrintServiceFactory class has no methods that would help us distinguish any one factory from another, so we just take factories[0]. We call the getPrintService method with an output stream parameter to get a StreamPrintService object.

OutputStream out = new FileOutputStream(fileName);
StreamPrintService service = factories[0].getPrintService(out);

The StreamPrintService class is a subclass of PrintService. To produce a printout, simply follow the steps of the preceding section.

Printing Attributes

The print service API contains a complex set of interfaces and classes to specify various kinds of attributes. There are four important groups of attributes. The first two specify requests to the printer.

  • Print request attributes request particular features for all doc objects in a print job, such as two-sided printing or the paper size.

  • Doc attributes are request attributes that apply only to a single doc object.

The other two attributes contain information about the printer and job status.

  • Print service attributes give information about the print service, such as the printer make and model or whether the printer is currently accepting jobs.

  • Print job attributes give information about the status of a particular print job, such as whether the job is already completed.

To describe the various attributes there is an interface Attribute with subinterfaces:

PrintRequestAttribute
DocAttribute
PrintServiceAttribute
PrintJobAttribute
SupportedValuesAttribute

Individual attribute classes implement one or more of these interfaces. For example, objects of the Copies class describe the number of copies of a printout. That class implements both the PrintRequestAttribute and the PrintJobAttribute interfaces. Clearly, a print request can contain a request for multiple copies. Conversely, an attribute of the print job might be how many of these copies were actually printed. That number might be lower, perhaps because of printer limitations or because the printer ran out of paper.

The SupportedValuesAttribute interface indicates that an attribute value does not reflect actual request or status data but rather the capability of a service. For example, the CopiesSupported class implements the SupportedValuesAttribute interface. An object of that class might describe that a printer supports 1 through 99 copies of a printout.

Figure 7-38 shows a class diagram of the attribute hierarchy.

The attribute hierarchy

Figure 7-38. The attribute hierarchy

In addition to the interfaces and classes for individual attributes, the print service API defines interfaces and classes for attribute sets. A superinterface, AttributeSet, has four subinterfaces:

PrintRequestAttributeSet
DocAttributeSet
PrintServiceAttributeSet
PrintJobAttributeSet

Each of these interfaces has an implementing class, yielding the five classes:

HashAttributeSet
HashPrintRequestAttributeSet
HashDocAttributeSet
HashPrintServiceAttributeSet
HashPrintJobAttributeSet

Figure 7-39 shows a class diagram of the attribute set hierarchy.

The attribute set hierarchy

Figure 7-39. The attribute set hierarchy

For example, you construct a print request attribute set like this:

PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();

After constructing the set, you are freed from worry about the Hash prefix.

Why have all these interfaces? They make it possible to check for correct attribute usage. For example, a DocAttributeSet accepts only objects that implement the DocAttribute interface. Any attempt to add another attribute results in a runtime error.

An attribute set is a specialized kind of map, where the keys are of type Class and the values belong to a class that implements the Attribute interface. For example, if you insert an object

new Copies(10)

into an attribute set, then its key is the Class object Copies.class. That key is called the category of the attribute. The Attribute interface declares a method

Class getCategory()

that returns the category of an attribute. The Copies class defines the method to return the object Copies.class, but it isn’t a requirement that the category be the same as the class of the attribute.

When an attribute is added to an attribute set, the category is extracted automatically. You just add the attribute value:

attributes.add(new Copies(10));

If you subsequently add another attribute with the same category, it overwrites the first one.

To retrieve an attribute, you need to use the category as the key, for example,

AttributeSet attributes = job.getAttributes();
Copies copies = (Copies) attribute.get(Copies.class);

Finally, attributes are organized by the values they can have. The Copies attribute can have any integer value. The Copies class extends the IntegerSyntax class that takes care of all integer-valued attributes. The getValue method returns the integer value of the attribute, for example,

int n = copies.getValue();

The classes

TextSyntax
DateTimeSyntax
URISyntax

encapsulate a string, date and time, or URI.

Finally, many attributes can take a finite number of values. For example, the PrintQuality attribute has three settings: draft, normal, and high. They are represented by three constants:

PrintQuality.DRAFT
PrintQuality.NORMAL
PrintQuality.HIGH

Attribute classes with a finite number of values extend the EnumSyntax class, which provides a number of convenience methods to set up these enumerations in a typesafe manner. You need not worry about the mechanism when using such an attribute. Simply add the named values to attribute sets:

attributes.add(PrintQuality.HIGH);

Here is how you check the value of an attribute:

if (attributes.get(PrintQuality.class) == PrintQuality.HIGH)
   . . .

Table 7-4 lists the printing attributes. The second column lists the superclass of the attribute class (for example, IntegerSyntax for the Copies attribute) or the set of enumeration values for the attributes with a finite set of values. The last four columns indicate whether the attribute class implements the DocAttribute (DA), PrintJobAttribute (PJA), PrintRequestAttribute (PRA), and PrintServiceAttribute (PSA) interfaces.

Table 7-4. Printing Attributes

Attribute

Superclass or Enumeration Constants

DA

PJA

PRA

PSA

Chromaticity

MONOCHROME, COLOR

 

ColorSupported

SUPPORTED, NOT_SUPPORTED

   

Compression

COMPRESS, DEFLATE, GZIP, NONE

   

Copies

IntegerSyntax

 

 

DateTimeAtCompleted

DateTimeSyntax

 

  

DateTimeAtCreation

DateTimeSyntax

 

  

DateTimeAtProcessing

DateTimeSyntax

 

  

Destination

URISyntax

 

 

DocumentName

TextSyntax

   

Fidelity

FIDELITY_TRUE, FIDELITY_FALSE

 

 

Finishings

NONE, STAPLE, EDGE_STITCH, BIND, SADDLE_STITCH, COVER, . . .

 

JobHoldUntil

DateTimeSyntax

 

 

JobImpressions

IntegerSyntax

 

 

JobImpressionsCompleted

IntegerSyntax

 

  

JobKOctets

IntegerSyntax

 

 

JobKOctetsProcessed

IntegerSyntax

 

  

JobMediaSheets

IntegerSyntax

 

 

JobMediaSheetsCompleted

IntegerSyntax

 

  

JobMessageFromOperator

TextSyntax

 

  

JobName

TextSyntax

 

 

JobOriginatingUserName

TextSyntax

 

  

JobPriority

IntegerSyntax

 

 

JobSheets

STANDARD, NONE

 

 

JobState

ABORTED, CANCELED, COMPLETED, PENDING, PENDING_HELD, PROCESSING, PROCESSING_STOPPED

 

  

JobStateReason

ABORTED_BY_SYSTEM, DOCUMENT_FORMAT_ERROR, many others

    

JobStateReasons

HashSet

 

  

MediaName

ISO_A4_WHITE, ISO_A4_TRANSPARENT, NA_LETTER_WHITE, NA_LETTER_TRANSPARENT

 

MediaSize

ISO.A0 - ISO.A10, ISO.B0 - ISO.B10, ISO.C0 - ISO.C10, NA.LETTER, NA.LEGAL, various other paper and envelope sizes

    

MediaSizeName

ISO_A0 - ISO_A10, ISO_B0 - ISO_B10, ISO_C0 - ISO_C10, NA_LETTER, NA_LEGAL, various other paper and envelope size names

 

MediaTray

TOP, MIDDLE, BOTTOM, SIDE, ENVELOPE, LARGE_CAPACITY, MAIN, MANUAL

 

MultipleDocumentHandling

SINGLE_DOCUMENT, SINGLE_DOCUMENT_NEW_SHEET, SEPARATE_DOCUMENTS_COLLATED_COPIES, SEPARATE_DOCUMENTS_UNCOLLATED_COPIES

 

 

NumberOfDocuments

IntegerSyntax

 

  

NumberOfInterveningJobs

IntegerSyntax

 

  

NumberUp

IntegerSyntax

 

OrientationRequested

PORTRAIT, LANDSCAPE, REVERSE_PORTRAIT, REVERSE_LANDSCAPE

 

OutputDeviceAssigned

TextSyntax

 

  

PageRanges

SetOfInteger

 

PagesPerMinute

IntegerSyntax

   

PagesPerMinuteColor

IntegerSyntax

   

PDLOverrideSupported

ATTEMPTED, NOT_ATTEMPTED

   

PresentationDirection

TORIGHT_TOBOTTOM, TORIGHT_TOTOP, TOBOTTOM_TORIGHT, TOBOTTOM_TOLEFT, TOLEFT_TOBOTTOM, TOLEFT_TOTOP, TOTOP_TORIGHT, TOTOP_TOLEFT

 

 

PrinterInfo

TextSyntax

   

PrinterIsAcceptingJobs

ACCEPTING_JOBS, NOT_ACCEPTING_JOBS

   

PrinterLocation

TextSyntax

   

PrinterMakeAndModel

TextSyntax

   

PrinterMessageFromOperator

TextSyntax

   

PrinterMoreInfo

URISyntax

   

PrinterMoreInfoManufacturer

URISyntax

   

PrinterName

TextSyntax

   

PrinterResolution

ResolutionSyntax

 

PrinterState

PROCESSING, IDLE, STOPPED, UNKNOWN

   

PrinterStateReason

COVER_OPEN, FUSER_OVER_TEMP, MEDIA_JAM, and many others

    

PrinterStateReasons

HashMap

    

PrinterURI

URISyntax

   

PrintQuality

DRAFT, NORMAL, HIGH

 

QueuedJobCount

IntegerSyntax

   

ReferenceUriSchemesSupported

FILE, FTP, GOPHER, HTTP, HTTPS, NEWS, NNTP, WAIS

    

RequestingUserName

TextSyntax

  

 

Severity

ERROR, REPORT, WARNING

    

SheetCollate

COLLATED, UNCOLLATED

 

Sides

ONE_SIDED, DUPLEX (=TWO_SIDED_LONG_EDGE), TUMBLE (=TWO_SIDED_SHORT_EDGE)

 

Note

Note

As you can see, there are lots of attributes, many of which are quite specialized. The source for most of the attributes is the Internet Printing Protocol 1.1 (RFC 2911).

Note

Note

An earlier version of the printing API introduced the JobAttributes and PageAttributes classes, the purpose of which is similar to the printing attributes covered in this section. These classes are now obsolete.

 

This concludes our discussion on printing. You now know how to print 2D graphics and other document types, how to enumerate printers and stream print services, and how to set and retrieve attributes. Next, we turn to two important user interface issues, the clipboard and support for the drag-and-drop mechanism.

The Clipboard

One of the most useful and convenient user interface mechanisms of GUI environments (such as Windows and the X Window System) is cut and paste. You select some data in one program and cut or copy them to the clipboard. Then, you select another program and paste the clipboard contents into that application. Using the clipboard, you can transfer text, images, or other data from one document to another, or, of course, from one place in a document to another place in the same document. Cut and paste is so natural that most computer users never think about it.

Even though the clipboard is conceptually simple, implementing clipboard services is actually harder than you might think. Suppose you copy text from a word processor to the clipboard. If you paste that text into another word processor, then you expect that the fonts and formatting will stay intact. That is, the text in the clipboard needs to retain the formatting information. However, if you paste the text into a plain text field, then you expect that just the characters are pasted in, without additional formatting codes. To support this flexibility, the data provider can offer the clipboard data in multiple formats, and the data consumer can pick one of them.

The system clipboard implementations of Microsoft Windows and the Macintosh are similar, but, of course, there are slight differences. However, the X Window System clipboard mechanism is much more limited—cutting and pasting of anything but plain text is only sporadically supported. You should consider these limitations when trying out the programs in this section.

Note

Note

Check out the file jre/lib/flavormap.properties on your platform to get an idea about what kinds of objects can be transferred between Java programs and the system clipboard.

Often, programs need to support cut and paste of data types that the system clipboard cannot handle. The data transfer API supports the transfer of arbitrary local object references in the same virtual machine. Between different virtual machines, you can transfer serialized objects and references to remote objects.

Table 7-5 summarizes the data transfer capabilities of the clipboard mechanism.

Table 7-5. Capabilities of the Java Data Transfer Mechanism

Transfer

Format

Between a Java program and a native program

Text, images, file lists, . . . (depending on the host platform)

Between two cooperating Java programs

Serialized and remote objects

Within one Java program

Any object

Classes and Interfaces for Data Transfer

Data transfer in the Java technology is implemented in a package called java.awt.datatransfer. Here is an overview of the most important classes and interfaces of that package.

  • Objects that can be transferred via a clipboard must implement the Transferable interface.

  • The Clipboard class describes a clipboard. Transferable objects are the only items that can be put on or taken off a clipboard. The system clipboard is a concrete example of a Clipboard.

  • The DataFlavor class describes data flavors that can be placed on the clipboard.

  • The StringSelection class is a concrete class that implements the Transferable interface. It transfers text strings.

  • A class must implement the ClipboardOwner interface if it wants to be notified when the clipboard contents have been overwritten by somooeone else. Clipboard ownership enables “delayed formatting” of complex data. If a program transfers simple data (such as a string), then it simply sets the clipboard contents and moves on to do the next thing. However, if a program will place complex data that can be formatted in multiple flavors onto the clipboard, then it might not actually want to prepare all the flavors, because there is a good chance that most of them are never needed. However, then it needs to hang on to the clipboard data so that it can create the flavors later when they are requested. The clipboard owner is notified (by a call to its lostOwnership method) when the contents of the clipboard change. That tells it that the information is no longer needed. In our sample programs, we don’t worry about clipboard ownership.

Transferring Text

The best way to get comfortable with the data transfer classes is to start with the simplest situation: transferring text to and from the system clipboard. First, get a reference to the system clipboard.

Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();

For strings to be transferred to the clipboard, they must be wrapped into StringSelection objects.

String text = . . .
StringSelection selection = new StringSelection(text);

The actual transfer is done by a call to setContents, which takes a StringSelection object and a ClipBoardOwner as parameters. If you are not interested in designating a clipboard owner, set the second parameter to null.

clipboard.setContents(selection, null);

Here is the reverse operation, reading a string from the clipboard:

DataFlavor flavor = DataFlavor.stringFlavor;
if (clipboard.isDataFlavorAvailable(flavor)
   String text = (String) clipboard.getData(flavor);

The parameter of the getContents call is an Object reference of the requesting object, but because the current implementation of the Clipboard class ignores it, we just pass null.

The return value of getContents can be null. That indicates that the clipboard is either empty or that it has no data that the Java platform knows how to retrieve as text.

Listing 7-11 is a program that demonstrates cutting and pasting between a Java application and the system clipboard. If you select an area of text in the text area and click Copy, then the selected text is copied to the system clipboard. You can then paste it into any text editor (see Figure 7-40). Conversely, when you copy text from the text editor, you can paste it into our sample program.

The TextTransferTest program

Figure 7-40. The TextTransferTest program

Example 7-11. TextTransferTest.java

  1. import java.awt.*;
  2. import java.awt.datatransfer.*;
  3. import java.awt.event.*;
  4. import java.io.*;
  5.
  6. import javax.swing.*;
  7.
  8. /**
  9.  * This program demonstrates the transfer of text between a Java application and the system
 10.  * clipboard.
 11.  * @version 1.13 2007-08-16
 12.  * @author Cay Horstmann
 13.  */
 14. public class TextTransferTest
 15. {
 16.    public static void main(String[] args)
 17.    {
 18.       EventQueue.invokeLater(new Runnable()
 19.          {
 20.             public void run()
 21.             {
 22.                JFrame frame = new TextTransferFrame();
 23.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 24.                frame.setVisible(true);
 25.             }
 26.          });
 27.    }
 28. }
 29.
 30. /**
 31.  * This frame has a text area and buttons for copying and pasting text.
 32.  */
 33. class TextTransferFrame extends JFrame
 34. {
 35.    public TextTransferFrame()
 36.    {
 37.       setTitle("TextTransferTest");
 38.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 39.
 40.       textArea = new JTextArea();
 41.       add(new JScrollPane(textArea), BorderLayout.CENTER);
 42.       JPanel panel = new JPanel();
 43.
 44.       JButton copyButton = new JButton("Copy");
 45.       panel.add(copyButton);
 46.       copyButton.addActionListener(new ActionListener()
 47.          {
 48.             public void actionPerformed(ActionEvent event)
 49.             {
 50.                copy();
 51.             }
 52.          });
 53.
 54.       JButton pasteButton = new JButton("Paste");
 55.       panel.add(pasteButton);
 56.       pasteButton.addActionListener(new ActionListener()
 57.          {
 58.             public void actionPerformed(ActionEvent event)
 59.             {
 60.                paste();
 61.             }
 62.          });
 63.
 64.       add(panel, BorderLayout.SOUTH);
 65.    }
 66.
 67.    /**
 68.     * Copies the selected text to the system clipboard.
 69.     */
 70.    private void copy()
 71.    {
 72.       Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
 73.       String text = textArea.getSelectedText();
 74.       if (text == null) text = textArea.getText();
 75.       StringSelection selection = new StringSelection(text);
 76.       clipboard.setContents(selection, null);
 77.    }
 78.
 79.    /**
 80.     * Pastes the text from the system clipboard into the text area.
 81.     */
 82.    private void paste()
 83.    {
 84.       Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
 85.       DataFlavor flavor = DataFlavor.stringFlavor;
 86.       if (clipboard.isDataFlavorAvailable(flavor))
 87.       {
 88.          try
 89.          {
 90.             String text = (String) clipboard.getData(flavor);
 91.             textArea.replaceSelection(text);
 92.          }
 93.          catch (UnsupportedFlavorException e)
 94.          {
 95.             JOptionPane.showMessageDialog(this, e);
 96.          }
 97.          catch (IOException e)
 98.          {
 99.             JOptionPane.showMessageDialog(this, e);
100.          }
101.       }
102.    }
103.
104.    private JTextArea textArea;
105.
106.    private static final int DEFAULT_WIDTH = 300;
107.    private static final int DEFAULT_HEIGHT = 300;
108. }

 

The Transferable Interface and Data Flavors

A DataFlavor is defined by two characteristics:

  • A MIME type name (such as "image/gif").

  • A representation class for accessing the data (such as java.awt.Image).

In addition, every data flavor has a human-readable name (such as "GIF Image").

The representation class can be specified with a class parameter in the MIME type, for example,

image/gif;class=java.awt.Image

Note

Note

This is just an example to show the syntax. There is no standard data flavor for transferring GIF image data.

If no class parameter is given, then the representation class is InputStream.

For transferring local, serialized, and remote Java objects, Sun Microsystems defines three MIME types:

application/x-java-jvm-local-objectref
application/x-java-serialized-object
application/x-java-remote-object

Note

Note

The x- prefix indicates that this is an experimental name, not one that is sanctioned by IANA, the organization that assigns standard MIME type names.

For example, the standard stringFlavor data flavor is described by the MIME type

application/x-java-serialized-object;class=java.lang.String

You can ask the clipboard to list all available flavors:

DataFlavor[] flavors = clipboard.getAvailableDataFlavors()

You can also install a FlavorListener onto the clipboard. The listener is notified when the collection of data flavors on the clipboard changes. See the API notes for details.

Building an Image Transferable

Objects that you want to transfer via the clipboard must implement the Transferable interface. The StringSelection class is currently the only public class in the Java standard library that implements the Transferable interface. In this section, you will see how to transfer images into the clipboard. Because Java does not supply a class for image transfer, you must implement it yourself.

The class is completely trivial. It simply reports that the only available data format is DataFlavor.imageFlavor, and it holds an image object.

class ImageTransferable implements Transferable
{
   public ImageTransferable(Image image)
   {
      theImage = image;
   }

   public DataFlavor[] getTransferDataFlavors()
   {
      return new DataFlavor[] { DataFlavor.imageFlavor };
   }

   public boolean isDataFlavorSupported(DataFlavor flavor)
   {
      return flavor.equals(DataFlavor.imageFlavor);
   }

   public Object getTransferData(DataFlavor flavor)
      throws UnsupportedFlavorException
   {
      if(flavor.equals(DataFlavor.imageFlavor))
      {
         return theImage;
      }
      else
      {
         throw new UnsupportedFlavorException(flavor);
      }
   }

   private Image theImage;
}

Note

Note

Java SE supplies the DataFlavor.imageFlavor constant and does all the heavy lifting to convert between Java images and native clipboard images. But, curiously, it does not supply the wrapper class that is necessary to place images onto the clipboard.

The program of Listing 7-12 demonstrates the transfer of images between a Java application and the system clipboard. When the program starts, it generates an image containing a red circle. Click the Copy button to copy the image to the clipboard and then paste it into another application (see Figure 7-41). From another application, copy an image into the system clipboard. Then click the Paste button and see the image being pasted into the example program (see Figure 7-42).

Copying from a Java program to a native program

Figure 7-41. Copying from a Java program to a native program

Copying from a native program to a Java program

Figure 7-42. Copying from a native program to a Java program

The program is a straightforward modification of the text transfer program. The data flavor is now DataFlavor.imageFlavor, and we use the ImageTransferable class to transfer an image to the system clipboard.

Example 7-12. ImageTransferTest.java

  1. import java.io.*;
  2. import java.awt.*;
  3. import java.awt.datatransfer.*;
  4. import java.awt.event.*;
  5. import java.awt.image.*;
  6. import javax.swing.*;
  7.
  8. /**
  9.  * This program demonstrates the transfer of images between a Java application and the system
 10.  * clipboard.
 11.  * @version 1.22 2007-08-16
 12.  * @author Cay Horstmann
 13.  */
 14. public class ImageTransferTest
 15. {
 16.    public static void main(String[] args)
 17.    {
 18.       EventQueue.invokeLater(new Runnable()
 19.          {
 20.             public void run()
 21.             {
 22.                JFrame frame = new ImageTransferFrame();
 23.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 24.                frame.setVisible(true);
 25.             }
 26.          });
 27.    }
 28. }
 29.
 30. /**
 31.  * This frame has an image label and buttons for copying and pasting an image.
 32.  */
 33. class ImageTransferFrame extends JFrame
 34. {
 35.    public ImageTransferFrame()
 36.    {
 37.       setTitle("ImageTransferTest");
 38.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 39.
 40.       label = new JLabel();
 41.       image = new BufferedImage(DEFAULT_WIDTH, DEFAULT_HEIGHT, BufferedImage.TYPE_INT_ARGB);
 42.       Graphics g = image.getGraphics();
 43.       g.setColor(Color.WHITE);
 44.       g.fillRect(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT);
 45.       g.setColor(Color.RED);
 46.       g.fillOval(DEFAULT_WIDTH / 4, DEFAULT_WIDTH / 4, DEFAULT_WIDTH / 2, DEFAULT_HEIGHT / 2);
 47.
 48.       label.setIcon(new ImageIcon(image));
 49.       add(new JScrollPane(label), BorderLayout.CENTER);
 50.       JPanel panel = new JPanel();
 51.
 52.       JButton copyButton = new JButton("Copy");
 53.       panel.add(copyButton);
 54.       copyButton.addActionListener(new ActionListener()
 55.          {
 56.             public void actionPerformed(ActionEvent event)
 57.             {
 58.                copy();
 59.             }
 60.          });
 61.
 62.       JButton pasteButton = new JButton("Paste");
 63.       panel.add(pasteButton);
 64.       pasteButton.addActionListener(new ActionListener()
 65.          {
 66.             public void actionPerformed(ActionEvent event)
 67.             {
 68.                paste();
 69.             }
 70.          });
 71.
 72.       add(panel, BorderLayout.SOUTH);
 73.    }
 74.
 75.    /**
 76.     * Copies the current image to the system clipboard.
 77.     */
 78.    private void copy()
 79.    {
 80.       Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
 81.       ImageTransferable selection = new ImageTransferable(image);
 82.       clipboard.setContents(selection, null);
 83.    }
 84.
 85.    /**
 86.     * Pastes the image from the system clipboard into the image label.
 87.     */
 88.    private void paste()
 89.    {
 90.       Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
 91.       DataFlavor flavor = DataFlavor.imageFlavor;
 92.       if (clipboard.isDataFlavorAvailable(flavor))
 93.       {
 94.          try
 95.          {
 96.             image = (Image) clipboard.getData(flavor);
 97.             label.setIcon(new ImageIcon(image));
 98.          }
 99.          catch (UnsupportedFlavorException exception)
100.          {
101.             JOptionPane.showMessageDialog(this, exception);
102.          }
103.          catch (IOException exception)
104.          {
105.             JOptionPane.showMessageDialog(this, exception);
106.          }
107.       }
108.    }
109.
110.    private JLabel label;
111.    private Image image;
112.
113.    private static final int DEFAULT_WIDTH = 300;
114.    private static final int DEFAULT_HEIGHT = 300;
115. }
116.
117. /**
118.  * This class is a wrapper for the data transfer of image objects.
119.  */
120. class ImageTransferable implements Transferable
121. {
122.    /**
123.     * Constructs the selection.
124.     * @param image an image
125.     */
126.    public ImageTransferable(Image image)
127.    {
128.       theImage = image;
129.    }
130.
131.    public DataFlavor[] getTransferDataFlavors()
132.    {
133.       return new DataFlavor[] { DataFlavor.imageFlavor };
134.    }
135.
136.    public boolean isDataFlavorSupported(DataFlavor flavor)
137.    {
138.       return flavor.equals(DataFlavor.imageFlavor);
139.    }
140.
141.    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException
142.    {
143.       if (flavor.equals(DataFlavor.imageFlavor))
144.       {
145.          return theImage;
146.       }
147.       else
148.       {
149.          throw new UnsupportedFlavorException(flavor);
150.       }
151.    }
152.
153.    private Image theImage;
154. }

 

Transferring Java Objects via the System Clipboard

Suppose you want to copy and paste objects from one Java application to another. In that case, you cannot use local clipboards. Fortunately, you can place serialized Java objects onto the system clipboard.

The program in Listing 7-13 demonstrates this capability. The program shows a color chooser. The Copy button copies the current color to the system clipboard as a serialized Color object. The Paste button checks whether the system clipboard contains a serialized Color object. If so, it fetches the color and sets it as the current choice of the color chooser.

You can transfer the serialized object between two Java applications (see Figure 7-43). Run two copies of the SerialTransferTest program. Click Copy in the first program, then click Paste in the second program. The Color object is transferred from one virtual machine to the other.

Data are copied between two instances of a Java application

Figure 7-43. Data are copied between two instances of a Java application

To enable the data transfer, the Java platform places binary data on the system clipboard that contains the serialized object. Another Java program—not necessarily of the same type as the one that generated the clipboard data—can retrieve the clipboard data and deserialize the object.

Of course, a non-Java application will not know what to do with the clipboard data. For that reason, the example program offers the clipboard data in a second flavor, as text. The text is simply the result of the toString method, applied to the transferred object. To see the second flavor, run the program, click on a color, and then select the Paste command in your text editor. A string such as

java.awt.Color[r=255,g=0,b=51]

will be inserted into your document.

Essentially no additional programming is required to transfer a serializable object. You use the MIME type

application/x-java-serialized-object;class=className

As before, you have to build your own transfer wrapper—see the example code for details.

Example 7-13. SerialTransferTest.java

  1. import java.io.*;
  2. import java.awt.*;
  3. import java.awt.datatransfer.*;
  4. import java.awt.event.*;
  5. import javax.swing.*;
  6.
  7. /**
  8.  * This program demonstrates the transfer of serialized objects between virtual machines.
  9.  * @version 1.02 2007-08-16
 10.  * @author Cay Horstmann
 11.  */
 12. public class SerialTransferTest
 13. {
 14.    public static void main(String[] args)
 15.    {
 16.       EventQueue.invokeLater(new Runnable()
 17.          {
 18.             public void run()
 19.             {
 20.                JFrame frame = new SerialTransferFrame();
 21.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 22.                frame.setVisible(true);
 23.             }
 24.          });
 25.    }
 26. }
 27.
 28. /**
 29.  * This frame contains a color chooser, and copy and paste buttons.
 30.  */
 31. class SerialTransferFrame extends JFrame
 32. {
 33.    public SerialTransferFrame()
 34.    {
 35.       setTitle("SerialTransferTest");
 36.
 37.       chooser = new JColorChooser();
 38.       add(chooser, BorderLayout.CENTER);
 39.       JPanel panel = new JPanel();
 40.
 41.       JButton copyButton = new JButton("Copy");
 42.       panel.add(copyButton);
 43.       copyButton.addActionListener(new ActionListener()
 44.          {
 45.             public void actionPerformed(ActionEvent event)
 46.             {
 47.                copy();
 48.             }
 49.          });
 50.
 51.       JButton pasteButton = new JButton("Paste");
 52.       panel.add(pasteButton);
 53.       pasteButton.addActionListener(new ActionListener()
 54.          {
 55.              public void actionPerformed(ActionEvent event)
 56.              {
 57.                 paste();
 58.              }
 59.           });
 60.
 61.        add(panel, BorderLayout.SOUTH);
 62.        pack();
 63.    }
 64.
 65.    /**
 66.     * Copies the chooser's color into the system clipboard.
 67.     */
 68.    private void copy()
 69.    {
 70.       Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
 71.       Color color = chooser.getColor();
 72.       SerialTransferable selection = new SerialTransferable(color);
 73.       clipboard.setContents(selection, null);
 74.    }
 75.
 76.    /**
 77.     * Pastes the color from the system clipboard into the chooser.
 78.     */
 79.    private void paste()
 80.    {
 81.       Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
 82.       try
 83.       {
 84.          DataFlavor flavor = new DataFlavor(
 85.                "application/x-java-serialized-object;class=java.awt.Color");
 86.          if (clipboard.isDataFlavorAvailable(flavor))
 87.          {
 88.             Color color = (Color) clipboard.getData(flavor);
 89.             chooser.setColor(color);
 90.          }
 91.       }
 92.       catch (ClassNotFoundException e)
 93.       {
 94.          JOptionPane.showMessageDialog(this, e);
 95.       }
 96.       catch (UnsupportedFlavorException e)
 97.       {
 98.          JOptionPane.showMessageDialog(this, e);
 99.       }
100.       catch (IOException e)
101.       {
102.          JOptionPane.showMessageDialog(this, e);
103.       }
104.    }
105.
106.    private JColorChooser chooser;
107. }
108.
109. /**
110.  * This class is a wrapper for the data transfer of serialized objects.
111.  */
112. class SerialTransferable implements Transferable
113. {
114.    /**
115.     * Constructs the selection.
116.     * @param o any serializable object
117.     */
118.    SerialTransferable(Serializable o)
119.    {
120.       obj = o;
121.    }
122.
123.    public DataFlavor[] getTransferDataFlavors()
124.    {
125.       DataFlavor[] flavors = new DataFlavor[2];
126.       Class<?> type = obj.getClass();
127.       String mimeType = "application/x-java-serialized-object;class=" + type.getName();
128.       try
129.       {
130.          flavors[0] = new DataFlavor(mimeType);
131.          flavors[1] = DataFlavor.stringFlavor;
132.          return flavors;
133.       }
134.       catch (ClassNotFoundException e)
135.       {
136.          return new DataFlavor[0];
137.       }
138.    }
139.
140.    public boolean isDataFlavorSupported(DataFlavor flavor)
141.    {
142.       return DataFlavor.stringFlavor.equals(flavor)
143.             || "application".equals(flavor.getPrimaryType())
144.             && "x-java-serialized-object".equals(flavor.getSubType())
145.             && flavor.getRepresentationClass().isAssignableFrom(obj.getClass());
146.    }
147.
148.    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException
149.    {
150.       if (!isDataFlavorSupported(flavor)) throw new UnsupportedFlavorException(flavor);
151.
152.       if (DataFlavor.stringFlavor.equals(flavor)) return obj.toString();
153.
154.       return obj;
155.    }
156.
157.    private Serializable obj;
158. }

 

Using a Local Clipboard to Transfer Object References

Occasionally, you might need to copy and paste a data type that isn’t one of the data types supported by the system clipboard, and that isn’t serializable. To transfer an arbitrary Java object reference within the same JVM, you use the MIME type

application/x-java-jvm-local-objectref;class=className

You need to define a Transferable wrapper for this type. The process is entirely analogous to the SerialTransferable wrapper of the preceding example.

An object reference is only meaningful within a single virtual machine. For that reason, you cannot copy the shape object to the system clipboard. Instead, use a local clipboard:

Clipboard clipboard = new Clipboard("local");

The construction parameter is the clipboard name.

However, using a local clipboard has one major disadvantage. You need to synchronize the local and the system clipboard, so that users don’t confuse the two. Currently, the Java platform doesn’t do that synchronization for you.

Drag and Drop

When you use cut and paste to transmit information between two programs, the clipboard acts as an intermediary. The drag and drop metaphor cuts out the middleman and lets two programs communicate directly. The Java platform offers basic support for drag and drop. You can carry out drag and drop operations between Java applications and native applications. This section shows you how to write a Java application that is a drop target, and an application that is a drag source.

Before going deeper into the Java platform support for drag and drop, let us quickly look at the drag-and-drop user interface. We use the Windows Explorer and WordPad programs as examples—on another platform, you can experiment with locally available programs with drag-and-drop capabilities.

You initiate a drag operation with a gesture inside a drag source—by first selecting one or more elements and then dragging the selection away from its initial location. When you release the mouse button over a drop target that accepts the drop operation, the drop target queries the drag source for information about the dropped elements and carries out an appropriate operation. For example, if you drop a file icon from a file manager on top of a directory icon, then the file is moved into that directory. However, if you drag it to a text editor, then the text editor opens the file. (This requires, of course, that you use a file manager and text editor that are enabled for drag and drop, such as Explorer/WordPad in Windows or Nautilus/gedit in Gnome).

If you hold down the CTRL key while dragging, then the type of the drop action changes from a move action to a copy action, and a copy of the file is placed into the directory. If you hold down both SHIFT and CTRL keys, then a link to the file is placed into the directory. (Other platforms might use other keyboard combinations for these operations.)

Thus, there are three types of drop actions with different gestures:

  • Move

  • Copy

  • Link

The intention of the link action is to establish a reference to the dropped element. Such links typically require support from the host operating system (such as symbolic links for files, or object linking for document components) and don’t usually make a lot of sense in cross-platform programs. In this section, we focus on using drag and drop for copying and moving.

There is usually some visual feedback for the drag operation. Minimally, the cursor shape changes. As the cursor moves over possible drop targets, the cursor shape indicates whether the drop is possible or not. If a drop is possible, the cursor shape also indicates the type of the drop action. Table 7-6 shows several drop cursor shapes.

Table 7-6. Drop Cursor Shapes

Action

Windows Icon

Gnome Icon

Move

Drop Cursor Shapes

Drop Cursor Shapes

Copy

Drop Cursor Shapes

Drop Cursor Shapes

Link

Drop Cursor Shapes

Drop Cursor Shapes

Drop not allowed

Drop Cursor Shapes

Drop Cursor Shapes

You can also drag other elements besides file icons. For example, you can select text in WordPad or gedit and drag it. Try dropping text fragments into willing drop targets and see how they react.

Note

Note

This experiment shows a disadvantage of drag and drop as a user interface mechanism. It can be difficult for users to anticipate what they can drag, where they can drop it, and what happens when they do. Because the default “move” action can remove the original, many users are understandably cautious about experimenting with drag and drop.

Data Transfer Support in Swing

Starting with Java SE 1.4, several Swing components have built-in support for drag and drop (see Table 7-7). You can drag selected text from a number of components, and you can drop text into text components. For backward compatibility, you must call the setDragEnabled method to activate dragging. Dropping is always enabled.

Table 7-7. Data Transfer Support in Swing Components

Component

Drag Source

Drop Target

JFileChooser

Exports file list

N/A

JColorChooser

Exports color object

Accepts color objects

JTextField
JFormattedTextField

Exports selected text

Accepts text

JPasswordField

N/A (for security)

Accepts text

JTextArea
JTextPane
JEditorPane

Exports selected text

Accepts text and file lists

JList
JTable
JTree

Exports text description of selection (copy only)

N/A

Note

Note

The java.awt.dnd package provides a lower-level drag-and-drop API that forms the basis for the Swing drag and drop. We do not discuss that API in this book.

The program in Listing 7-14 demonstrates the behavior. As you run the program, note these points:

  • You can select multiple items in the list, table, or tree and drag them.

  • Dragging items from the table is a bit awkward. You first select with the mouse, then you let go of the mouse button, then click it again, and then you drag.

  • When you drop the items in the text area, you can see how the dragged information is formatted. Table cells are separated by tabs, and each selected row is on a separate line (see Figure 7-44).

    The Swing drag-and-drop test program

    Figure 7-44. The Swing drag-and-drop test program

  • You can only copy, not move, items, from the list, table, tree, file chooser, or color chooser. Removing items from a list, table, or tree is not possible with all data models. You will see in the next section how to implement this capability when the data model is editable.

  • You cannot drag into the list, table, tree, or file chooser.

  • If you run two copies of the program, you can drag a color from one color chooser to the other.

  • You cannot drag text out of the text area because we didn’t call setDragEnabled on it.

The Swing package provides a potentially useful mechanism to quickly turn a component into a drag source and drop target. You can install a transfer handler for a given property. For example, in our sample program, we call

textField.setTransferHandler(new TransferHandler("background"));

You can now drag a color into the text field, and its background color changes.

When a drop occurs, then the transfer handler checks whether one of the data flavors has representation class Color. If so, it invokes the setBackground method.

By installing this transfer handler into the text field, you disable the standard transfer handler. You can no longer cut, copy, paste, drag, or drop text in the text field. However, you can now drag color out of this text field. You still need to select some text to initiate the drag gesture. When you drag the text, you will find that you can drop it into the color chooser and change its color value to the text field’s background color. However, you cannot drop the text into the text area.

Example 7-14. SwingDnDTest.java

 1. import java.awt.*;
 2.
 3. import javax.swing.*;
 4. import javax.swing.border.*;
 5. import javax.swing.event.*;
 6.
 7. /**
 8.  * This program demonstrates the basic Swing support for drag and drop.
 9.  * @version 1.10 2007-09-20
10.  * @author Cay Horstmann
11.  */
12. public class SwingDnDTest
13. {
14.    public static void main(String[] args)
15.    {
16.       EventQueue.invokeLater(new Runnable()
17.          {
18.             public void run()
19.             {
20.                JFrame frame = new SwingDnDFrame();
21.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
22.                frame.setVisible(true);
23.              }
24.          });
25.    }
26. }
27.
28. class SwingDnDFrame extends JFrame
29. {
30.    public SwingDnDFrame()
31.    {
32.       setTitle("SwingDnDTest");
33.       JTabbedPane tabbedPane = new JTabbedPane();
34.
35.       JList list = SampleComponents.list();
36.       tabbedPane.addTab("List", list);
37.       JTable table = SampleComponents.table();
38.       tabbedPane.addTab("Table", table);
39.       JTree tree = SampleComponents.tree();
40.       tabbedPane.addTab("Tree", tree);
41.       JFileChooser fileChooser = new JFileChooser();
42.       tabbedPane.addTab("File Chooser", fileChooser);
43.       JColorChooser colorChooser = new JColorChooser();
44.       tabbedPane.addTab("Color Chooser", colorChooser);
45.
46.       final JTextArea textArea = new JTextArea(4, 40);
47.       JScrollPane scrollPane = new JScrollPane(textArea);
48.       scrollPane.setBorder(new TitledBorder(new EtchedBorder(), "Drag text here"));
49.
50.       JTextField textField = new JTextField("Drag color here");
51.       textField.setTransferHandler(new TransferHandler("background"));
52.
53.       tabbedPane.addChangeListener(new ChangeListener()
54.          {
55.             public void stateChanged(ChangeEvent e)
56.             {
57.                textArea.setText("");
58.             }
59.          });
60.
61.       tree.setDragEnabled(true);
62.       table.setDragEnabled(true);
63.       list.setDragEnabled(true);
64.       fileChooser.setDragEnabled(true);
65.       colorChooser.setDragEnabled(true);
66.       textField.setDragEnabled(true);
67.
68.       add(tabbedPane, BorderLayout.NORTH);
69.       add(scrollPane, BorderLayout.CENTER);
70.       add(textField, BorderLayout.SOUTH);
71.       pack();
72.    }
73. }

Drag Sources

In the previous section, you saw how to take advantage of the basic drag-and-drop support in Swing. In this section, we show you how to configure any component as a drag source. In the next section, we discuss drop targets and present a sample component that is both a source and a target for images.

To customize the drag-and-drop behavior of a Swing component, you subclass the TransferHandler class. First, override the getSourceActions method to indicate which actions (copy, move, link) your component supports. Next, override the getTransferable method that produces a Transferable object, following the same process that you use for copying to the clipboard.

In our sample program, we drag images out of a JList that is filled with image icons (see Figure 7-45). Here is the implementation of the createTransferable method. The selected image is simply placed into an ImageTransferable wrapper.

protected Transferable createTransferable(JComponent source)
{
   JList list = (JList) source;
   int index = list.getSelectedIndex();
   if (index < 0) return null;
   ImageIcon icon = (ImageIcon) list.getModel().getElementAt(index);
   return new ImageTransferable(icon.getImage());
}
The ImageList drag-and-drop application

Figure 7-45. The ImageList drag-and-drop application

In our example, we are fortunate that a JList is already wired for initiating a drag gesture. You simply activate that mechanism by calling the setDragEnabled method. If you add drag support to a component that does not recognize a drag gesture, you need to initiate the transfer yourself. For example, here is how you can initiate dragging on a JLabel:

label.addMouseListener(new MouseAdapter()
{
   public void mousePressed(MouseEvent evt)
   {
      int mode;
      if ((evt.getModifiers() & (InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK)) != 0)
                  mode = TransferHandler.COPY;
      else mode = TransferHandler.MOVE;
      JComponent comp = (JComponent) evt.getSource();
      TransferHandler th = comp.getTransferHandler();
      th.exportAsDrag(comp, evt, mode);
   }
});

Here, we simply start the transfer when the user clicks on the label. A more sophisticated implementation would watch for a mouse motion that drags the mouse by a small amount.

When the user completes the drop action, the exportDone method of the source transfer handler is invoked. In that method, you need to remove the transferred object if the user carried out a move action. Here is the implementation for the image list:

protected void exportDone(JComponent source, Transferable data, int action)
{
   if (action == MOVE)
   {
      JList list = (JList) source;
      int index = list.getSelectedIndex();
      if (index < 0) return;
      DefaultListModel model = (DefaultListModel) list.getModel();
      model.remove(index);
   }
}

To summarize, to turn a component into a drag source, you add a transfer handler that specifies the following:

  • Which actions are supported.

  • Which data is transferred.

  • And how the original data is removed after a move action.

In addition, if your drag source is a component other than those listed in Table 7-7 on page 654, you need to watch for a mouse gesture and initiate the transfer.

Drop Targets

In this section, we show you how to implement a drop target. Our example is again a JList with image icons. We add drop support so that users can drop images into the list.

To make a component into a drop target, you set a TransferHandler and implement the canImport and importData methods.

Note

Note

As of Java SE 6, you can add a transfer handler to a JFrame. This is most commonly used for dropping files into an application. Valid drop locations include the frame decorations and the menu bar, but not components contained in the frame (which have their own transfer handlers).

The canImport method is called continuously as the user moves the mouse over the drop target component. Return true if a drop is allowed. This information affects the cursor icon that gives visual feedback whether the drop is allowed.

As of Java SE 6, the canImport method has a parameter of type TransferHandler.TransferSupport. Through this parameter, you can obtain the drop action chosen by the user, the drop location, and the data to be transferred. (Before Java SE 6, a different canImport method was called that only supplies a list of data flavors.)

In the canImport method, you can also override the user drop action. For example, if a user chose the move action but it would be inappropriate to remove the original, you can force the transfer handler to use a copy action instead.

Here is a typical example. The image list component is willing to accept drops of file lists and images. However, if a file list is dragged into the component, then a user-selected MOVE action is changed into a COPY action, so that the image files do not get deleted.

public boolean canImport(TransferSupport support)
{
   if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor))
   {
      if (support.getUserDropAction() == MOVE) support.setDropAction(COPY);
      return true;
   }
   else return support.isDataFlavorSupported(DataFlavor.imageFlavor);
}

A more sophisticated implementation could check that the files actually contain images.

The Swing components JList, JTable, JTree, and JTextComponent give visual feedback about insertion positions as the mouse is moved over the drop target. By default, the selection (for JList, JTable, and JTree) or the caret (for JTextComponent) is used to indicate the drop location. That approach is neither user-friendly nor flexible, and it is the default solely for backward compatibility. You should call the setDropMode method to choose a more appropriate visual feedback.

You can control whether the dropped data should overwrite existing items or be inserted between them. For example, in our sample program, we call

setDropMode(DropMode.ON_OR_INSERT);

to allow the user to drop onto an item (thereby replacing it), or to insert between two items (see Figure 7-46). Table 7-8 shows the drop modes supported by the Swing components.

Visual indicators for dropping onto an item and between two items

Figure 7-46. Visual indicators for dropping onto an item and between two items

Table 7-8. Drop Modes

Component

Supported Drop Modes

JList, JTree

ON, INSERT, ON_OR_INSERT, USE_SELECTION

JTable

ON, INSERT, ON_OR_INSERT, INSERT_ROWS, INSERT_COLS, ON_OR_INSERT_ROWS, ON_OR_INSERT_COLS, USE_SELECTION

JTextComponent

INSERT, USE_SELECTION (actually moves the caret, not the selection)

Once the user completes the drop gesture, the importData method is invoked. You need to obtain the data from the drag source. Invoke the getTransferable method on the TransferSupport parameter to obtain a reference to a Transferable object. This is the same interface that is used for copy and paste.

One data type that is commonly used for drag and drop is the DataFlavor.javaFileListFlavor. A file list describes a set of files that is dropped onto the target. The transfer data is an object of type List<File>. Here is the code for retrieving the files:

DataFlavor[] flavors = transferable.getTransferDataFlavors();
if (Arrays.asList(flavors).contains(DataFlavor.javaFileListFlavor))
{
   List<File> fileList = (List<File>) transferable.getTransferData(DataFlavor.javaFileListFlavor);
   for (File f : fileList)
   {
      do something with f;
   }
}

When dropping into one of the components listed in Table 7-8, you need to know precisely where to drop the data. Invoke the getDropLocation method on the TransferSupport parameter to find where the drop occurred. This method returns an object of a subclass of TransferHandler.DropLocation. The JList, JTable, JTree, and JTextComponent classes define subclasses that specify location in the particular data model. For example, a location in a list is simply an integer index, but a location in a tree is a tree path. Here is how we obtain the drop location in our image list:

int index;
if (support.isDrop())
{
   JList.DropLocation location = (JList.DropLocation) support.getDropLocation();
   index = location.getIndex();
}
else index = model.size();

The JList.DropLocation subclass has a method getIndex that returns the index of the drop. (The JTree.DropLocation subclass has a method getPath instead.)

The importData method is also called when data is pasted into the component with the CTRL+V keystroke. In that case, the getDropLocation method would throw an IllegalStateException. Therefore, if the isDrop method returns false, we simply append the pasted data to the end of the list.

When inserting into a list, table, or tree, you also need to check whether the data is supposed to be inserted between items or whether it should replace the item at the drop location. For a list, invoke the isInsert method of the JList.DropLocation. For the other components, see the API notes for their drop location classes at the end of this section.

To summarize, to turn a component into a drop target, you add a transfer handler that specifies the following:

  • When a dragged item can be accepted.

  • How the dropped data is imported.

In addition, if you add drop support to a JList, JTable, JTree, or JTextComponent, you should set the drop mode.

Listing 7-15 shows the complete program. Note that the ImageList class is both a drag source and a drop target. Try dragging images between the two lists. You can also drag image files from a file chooser of another program into the lists.

Example 7-15. ImageListDragDrop.java

  1. import java.awt.*;
  2. import java.awt.datatransfer.*;
  3. import java.io.*;
  4. import java.util.*;
  5. import javax.imageio.*;
  6. import javax.swing.*;
  7. import java.util.List;
  8.
 9. /**
 10.  * This program demonstrates drag and drop in an image list.
 11.  * @version 1.00 2007-09-20
 12.  * @author Cay Horstmann
 13.  */
 14. public class ImageListDnDTest
 15. {
 16.    public static void main(String[] args)
 17.    {
 18.       EventQueue.invokeLater(new Runnable()
 19.          {
 20.             public void run()
 21.             {
 22.                JFrame frame = new ImageListDnDFrame();
 23.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 24.                frame.setVisible(true);
 25.             }
 26.          });
 27.    }
 28. }
 29.
 30. class ImageListDnDFrame extends JFrame
 31. {
 32.    public ImageListDnDFrame()
 33.    {
 34.       setTitle("ImageListDnDTest");
 35.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 36.
 37.       list1 = new ImageList(new File("images1").listFiles());
 38.       list2 = new ImageList(new File("images2").listFiles());
 39.       setLayout(new GridLayout(2, 1));
 40.       add(new JScrollPane(list1));
 41.       add(new JScrollPane(list2));
 42.    }
 43.
 44.    private ImageList list1;
 45.    private ImageList list2;
 46.    private static final int DEFAULT_WIDTH = 600;
 47.    private static final int DEFAULT_HEIGHT = 500;
 48. }
 49.
 50. class ImageList extends JList
 51. {
 52.    public ImageList(File[] imageFiles)
 53.    {
 54.       DefaultListModel model = new DefaultListModel();
 55.       for (File f : imageFiles)
 56.          model.addElement(new ImageIcon(f.getPath()));
 57.
 58.       setModel(model);
 59.       setVisibleRowCount(0);
 60.       setLayoutOrientation(JList.HORIZONTAL_WRAP);
 61.       setDragEnabled(true);
 62.       setDropMode(DropMode.ON_OR_INSERT);
 63.       setTransferHandler(new ImageListTransferHandler());
 64.    }
 65. }
 66.
 67. class ImageListTransferHandler extends TransferHandler
 68. {
 69.    // Support for drag
 70.
 71.    public int getSourceActions(JComponent source)
 72.    {
 73.       return COPY_OR_MOVE;
 74.    }
 75.
 76.    protected Transferable createTransferable(JComponent source)
 77.    {
 78.       JList list = (JList) source;
 79.       int index = list.getSelectedIndex();
 80.       if (index < 0) return null;
 81.       ImageIcon icon = (ImageIcon) list.getModel().getElementAt(index);
 82.       return new ImageTransferable(icon.getImage());
 83.    }
 84.
 85.    protected void exportDone(JComponent source, Transferable data, int action)
 86.    {
 87.       if (action == MOVE)
 88.       {
 89.          JList list = (JList) source;
 90.          int index = list.getSelectedIndex();
 91.          if (index < 0) return;
 92.          DefaultListModel model = (DefaultListModel) list.getModel();
 93.          model.remove(index);
 94.       }
 95.    }
 96.
 97.    // Support for drop
 98.
 99.    public boolean canImport(TransferSupport support)
100.    {
101.       if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor))
102.       {
103.          if (support.getUserDropAction() == MOVE) support.setDropAction(COPY);
104.          return true;
105.       }
106.       else return support.isDataFlavorSupported(DataFlavor.imageFlavor);
107.    }
108.
109.    public boolean importData(TransferSupport support)
110.    {
111.       JList list = (JList) support.getComponent();
112.       DefaultListModel model = (DefaultListModel) list.getModel();
113.
114.       Transferable transferable = support.getTransferable();
115.       List<DataFlavor> flavors = Arrays.asList(transferable.getTransferDataFlavors());
116.
117.       List<Image> images = new ArrayList<Image>();
118.
119.       try
120.       {
121.          if (flavors.contains(DataFlavor.javaFileListFlavor))
122.          {
123.             List<File> fileList = (List<File>) transferable
124.                   .getTransferData(DataFlavor.javaFileListFlavor);
125.             for (File f : fileList)
126.             {
127.                try
128.                {
129.                   images.add(ImageIO.read(f));
130.                }
131.                catch (IOException ex)
132.                {
133.                   // couldn't read image--skip
134.                }
135.             }
136.          }
137.          else if (flavors.contains(DataFlavor.imageFlavor))
138.          {
139.             images.add((Image) transferable.getTransferData(DataFlavor.imageFlavor));
140.          }
141.
142.          int index;
143.          if (support.isDrop())
144.          {
145.             JList.DropLocation location = (JList.DropLocation) support.getDropLocation();
146.             index = location.getIndex();
147.             if (!location.isInsert()) model.remove(index); // replace location
148.          }
149.          else index = model.size();
150.          for (Image image : images)
151.          {
152.             model.add(index, new ImageIcon(image));
153.             index++;
154.          }
155.          return true;
156.       }
157.       catch (IOException ex)
158.       {
159.          return false;
160.       }
161.       catch (UnsupportedFlavorException ex)
162.       {
163.          return false;
164.       }
165.    }
166. }

 

Platform Integration

We finish this chapter with several features that were added to Java SE 6 to make Java applications feel more like native applications. The splash screen feature allows your application to display a splash screen as the virtual machine starts up. The java.awt.Desktop class lets you launch native applications such as the default browser and e-mail program. Finally, you now have access to the system tray and can clutter it up with icons, just like so many native applications do.

Splash Screens

A common complaint about Java applications is their long startup time. The Java virtual machine takes some time to load all required classes, particularly for a Swing application that needs to pull in large amounts of Swing and AWT library code. Users dislike applications that take a long time to bring up an initial screen, and they might even try launching the application multiple times if they don’t know whether the first launch was successful. The remedy is a splash screen, a small window that appears quickly, telling the user that the application has been launched successfully.

Traditionally, this has been difficult for Java applications. Of course, you can put up a window as soon as your main method starts. However, the main method is only launched after the class loader has loaded all dependent classes, which might take a while.

Java SE 6 solves this problem by enabling the virtual machine to show an image immediately on launch. There are two mechanisms for specifying that image. You can use the -splash command-line option:

java -splash:myimage.png MyApp

Alternatively, you can specify it in the manifest of a JAR file:

Main-Class: MyApp
SplashScreen-Image: myimage.gif

The image is displayed immediately and automatically disappears when the first AWT window is made visible. You can supply any GIF, JPEG, or PNG image. Animation (in GIF) and transparency (GIF and PNG) are supported.

If your application is ready to go as soon as it reaches main, you can skip the remainder of this section. However, many applications use a plug-in architecture in which a small core loads a set of plugins at startup. Eclipse and NetBeans are typical examples. In that case, you can indicate the loading progress on the splash screen.

There are two approaches. You can draw directly on the splash screen, or you can replace it with a borderless frame with identical contents, and then draw inside the frame. Our sample program shows both techniques.

To draw directly on the splash screen, get a reference to the splash screen and get its graphics context and dimensions:

SplashScreen splash = SplashScreen.getSplashScreen();
Graphics2D g2 = splash.createGraphics();
Rectangle bounds = splash.getBounds();

You can now draw in the usual way. When you are done, call update to ensure that the drawing is refreshed. Our sample program draws a simple progress bar, as seen in the left image in Figure 7-47.

g.fillRect(x, y, width * percent / 100, height);
splash.update();
The initial splash screen and a borderless follow-up window

Figure 7-47. The initial splash screen and a borderless follow-up window

Note

Note

The splash screen is a singleton object. You cannot construct your own. If no splash screen was set on the command line or in the manifest, the getSplashScreen method returns null.

Drawing directly on the splash screen has a drawback. It is tedious to compute all pixel positions, and your progress indicator won’t match the native progress bar. To avoid these problems, you can replace the initial splash screen with a follow-up window of the same size and content as soon as the main method starts. That window can contain arbitrary Swing components.

Our sample program in Listing 7-16 demonstrates this technique. The right image in Figure 7-47 shows a borderless frame with a panel that paints the splash screen and contains a JProgressBar. Now we have full access to the Swing API and can easily add message strings without having to fuss with pixel positions.

Note that we do not need to remove the initial splash screen. It is automatically removed as soon as the follow-up window is made visible.

Caution

Caution

Unfortunately, there is a noticeable flash when the splash screen is replaced by the follow-up window.

Example 7-16. SplashScreenTest.java

  1. import java.awt.*;
  2. import java.util.List;
  3. import javax.swing.*;
  4.
  5. /**
  6.  * This program demonstrates the splash screen API.
  7.  * @version 1.00 2007-09-21
  8.  * @author Cay Horstmann
  9.  */
 10. public class SplashScreenTest
 11. {
 12.    private static void drawOnSplash(int percent)
 13.    {
 14.       Rectangle bounds = splash.getBounds();
 15.       Graphics2D g = splash.createGraphics();
 16.       int height = 20;
 17.       int x = 2;
 18.       int y = bounds.height - height - 2;
 19.       int width = bounds.width - 4;
 20.       Color brightPurple = new Color(76, 36, 121);
 21.       g.setColor(brightPurple);
 22.       g.fillRect(x, y, width * percent / 100, height);
 23.       splash.update();
 24.    }
 25.
 26.    /**
 27.     * This method draws on the splash screen.
 28.     */
 29.    private static void init1()
 30.    {
 31.       splash = SplashScreen.getSplashScreen();
 32.       if (splash == null)
 33.       {
 34.          System.err.println("Did you specify a splash image with -splash or in the manifest?");
 35.          System.exit(1);
 36.       }
 37.
 38.       try
 39.       {
 40.          for (int i = 0; i <= 100; i++)
 41.          {
 42.             drawOnSplash(i);
 43.             Thread.sleep(100); // simulate startup work
 44.          }
 45.       }
 46.       catch (InterruptedException e)
 47.       {
 48.       }
 49.    }
 50.
 51.   /**
 52.    * This method displays a frame with the same image as the splash screen.
 53.    */
 54.   private static void init2()
 55.   {
 56.      final Image img = Toolkit.getDefaultToolkit().getImage(splash.getImageURL());
 57.
 58.      final JFrame splashFrame = new JFrame();
 59.      splashFrame.setUndecorated(true);
 60.
 61.      final JPanel splashPanel = new JPanel()
 62.         {
 63.            public void paintComponent(Graphics g)
 64.            {
 65.               g.drawImage(img, 0, 0, null);
 66.            }
 67.         };
 68.
 69.      final JProgressBar progressBar = new JProgressBar();
 70.      progressBar.setStringPainted(true);
 71.      splashPanel.setLayout(new BorderLayout());
 72.      splashPanel.add(progressBar, BorderLayout.SOUTH);
 73.
 74.      splashFrame.add(splashPanel);
 75.      splashFrame.setBounds(splash.getBounds());
 76.      splashFrame.setVisible(true);
 77.
 78.      new SwingWorker<Void, Integer>()
 79.      {
 80.         protected Void doInBackground() throws Exception
 81.         {
 82.            try
 83.            {
 84.               for (int i = 0; i <= 100; i++)
 85.               {
 86.                  publish(i);
 87.                  Thread.sleep(100);
 88.               }
 89.            }
 90.            catch (InterruptedException e)
 91.            {
 92.            }
 93.            return null;
 94.         }
 95.
 96.         protected void process(List<Integer> chunks)
 97.         {
 98.            for (Integer chunk : chunks)
 99.            {
100.               progressBar.setString("Loading module " + chunk);
101.               progressBar.setValue(chunk);
102.               splashPanel.repaint(); // because img is loaded asynchronously
103.            }
104.         }
105.
106.         protected void done()
107.         {
108.            splashFrame.setVisible(false);
109.
110.            JFrame frame = new JFrame();
111.            frame.setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
112.            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
113.            frame.setTitle("SplashScreenTest");
114.            frame.setVisible(true);
115.         }
116.      }.execute();
117.    }
118.
119.    public static void main(String args[])
120.    {
121.       init1();
122.
123.       EventQueue.invokeLater(new Runnable()
124.          {
125.             public void run()
126.             {
127.                init2();
128.             }
129.          });
130.    }
131.
132.    private static SplashScreen splash;
133.    private static final int DEFAULT_WIDTH = 300;
134.    private static final int DEFAULT_HEIGHT = 300;
135. }

 

Launching Desktop Applications

The java.awt.Desktop class lets you launch the default browser and e-mail program. You can also open, edit, and print files, using the applications that are registered for the file type.

The API is very straightforward. First, call the static isDesktopSupported method. If it returns true, the current platform supports the launching of desktop applications. Then call the static getDesktop method to obtain a Desktop instance.

Not all desktop environments support all API operations. For example, in the Gnome desktop on Linux, it is possible to open files, but you cannot print them. (There is no support for “verbs” in file associations.) To find out what is supported on your platform, call the isSupported method, passing a value in the Desktop.Action enumeration. Our sample program contains tests such as the following:

if (desktop.isSupported(Desktop.Action.PRINT)) printButton.setEnabled(true);

To open, edit, or print a file, first check that the action is supported, and then call the open, edit, or print method. To launch the browser, pass a URI. (See Chapter 3 for more information on URIs.) You can simply call the URI constructor with a string containing an http or https URL.

To launch the default e-mail program, you need to construct a URI of a particular format, namely

mailto:recipient?query

Here recipient is the e-mail address of the recipient, such as [email protected], and query contains &-separated name=value pairs, with percent-encoded values. (Percent encoding is essentially the same as the URL encoding algorithm described in Chapter 3, but a space is encoded as %20, not +). An example is subject=dinner%20RSVP&bcc=putin%40kremvax.ru. The format is documented in RFC 2368 (http://www.ietf.org/rfc/rfc2368.txt). Unfortunately, the URI class does not know anything about mailto URIs, so you have to assemble and encode your own. To make matters worse, at the time of this writing, there is no standard for dealing with non-ASCII characters. A common approach (which we take as well) is to convert each character to UTF-8 and percent-encode the resulting bytes.

Our sample program in Listing 7-17 lets you open, edit, or print a file of your choice, browse a URL, or launch your e-mail program (see Figure 7-48).

Launching a desktop application

Figure 7-48. Launching a desktop application

Example 7-17. DesktopAppTest.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.io.*;
  4. import java.net.*;
  5. import javax.swing.*;
  6.
  7. /**
  8.  * This program demonstrates the desktop app API.
  9.  * @version 1.00 2007-09-22
 10.  * @author Cay Horstmann
 11.  */
 12. public class DesktopAppTest
 13. {
 14.    public static void main(String[] args)
 15.    {
 16.       EventQueue.invokeLater(new Runnable()
 17.          {
 18.             public void run()
 19.             {
 20.                JFrame frame = new DesktopAppFrame();
 21.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 22.                frame.setVisible(true);
 23.             }
 24.          });
 25.    }
 26. }
 27.
 28. class DesktopAppFrame extends JFrame
 29. {
 30.    public DesktopAppFrame()
 31.    {
 32.       setLayout(new GridBagLayout());
 33.       final JFileChooser chooser = new JFileChooser();
 34.       JButton fileChooserButton = new JButton("...");
 35.       final JTextField fileField = new JTextField(20);
 36.       fileField.setEditable(false);
 37.       JButton openButton = new JButton("Open");
 38.       JButton editButton = new JButton("Edit");
 39.       JButton printButton = new JButton("Print");
 40.       final JTextField browseField = new JTextField();
 41.       JButton browseButton = new JButton("Browse");
 42.       final JTextField toField = new JTextField();
 43.       final JTextField subjectField = new JTextField();
 44.       JButton mailButton = new JButton("Mail");
 45.
 46.       openButton.setEnabled(false);
 47.       editButton.setEnabled(false);
 48.       printButton.setEnabled(false);
 49.       browseButton.setEnabled(false);
 50.       mailButton.setEnabled(false);
 51.
 52.       if (Desktop.isDesktopSupported())
 53.       {
 54.          Desktop desktop = Desktop.getDesktop();
 55.          if (desktop.isSupported(Desktop.Action.OPEN)) openButton.setEnabled(true);
 56.          if (desktop.isSupported(Desktop.Action.EDIT)) editButton.setEnabled(true);
 57.          if (desktop.isSupported(Desktop.Action.PRINT)) printButton.setEnabled(true);
 58.          if (desktop.isSupported(Desktop.Action.BROWSE)) browseButton.setEnabled(true);
 59.          if (desktop.isSupported(Desktop.Action.MAIL)) mailButton.setEnabled(true);
 60.       }
 61.
 62.       fileChooserButton.addActionListener(new ActionListener()
 63.          {
 64.             public void actionPerformed(ActionEvent e)
 65.             {
 66.                if (chooser.showOpenDialog(DesktopAppFrame.this) ==
 67.                      JFileChooser.APPROVE_OPTION)
 68.                   fileField.setText(chooser.getSelectedFile().getAbsolutePath());
 69.             }
 70.          });
 71.
 72.       openButton.addActionListener(new ActionListener()
 73.          {
 74.             public void actionPerformed(ActionEvent e)
 75.             {
 76.                try
 77.                {
 78.                   Desktop.getDesktop().open(chooser.getSelectedFile());
 79.                }
 80.                catch (IOException ex)
 81.                {
 82.                   ex.printStackTrace();
 83.                }
 84.             }
 85.          });
 86.
 87.       editButton.addActionListener(new ActionListener()
 88.          {
 89.             public void actionPerformed(ActionEvent e)
 90.             {
 91.                try
 92.                {
 93.                   Desktop.getDesktop().edit(chooser.getSelectedFile());
 94.                }
 95.                catch (IOException ex)
 96.                {
 97.                   ex.printStackTrace();
 98.                }
 99.             }
100.          });
101.
102.       printButton.addActionListener(new ActionListener()
103.          {
104.             public void actionPerformed(ActionEvent e)
105.             {
106.                try
107.                {
108.                   Desktop.getDesktop().print(chooser.getSelectedFile());
109.                }
110.                catch (IOException ex)
111.                {
112.                   ex.printStackTrace();
113.                }
114.             }
115.          });
116.
117.       browseButton.addActionListener(new ActionListener()
118.          {
119.             public void actionPerformed(ActionEvent e)
120.             {
121.                try
122.                {
123.                   Desktop.getDesktop().browse(new URI(browseField.getText()));
124.                }
125.                catch (URISyntaxException ex)
126.                {
127.                   ex.printStackTrace();
128.                }
129.                catch (IOException ex)
130.                {
131.                   ex.printStackTrace();
132.                }
133.             }
134.          });
135.
136.       mailButton.addActionListener(new ActionListener()
137.          {
138.             public void actionPerformed(ActionEvent e)
139.             {
140.                try
141.                {
142.                   String subject = percentEncode(subjectField.getText());
143.                   URI uri = new URI("mailto:" + toField.getText() + "?subject=" + subject);
144.
145.                   System.out.println(uri);
146.                   Desktop.getDesktop().mail(uri);
147.                }
148.                catch (URISyntaxException ex)
149.                {
150.                   ex.printStackTrace();
151.                }
152.                catch (IOException ex)
153.                {
154.                   ex.printStackTrace();
155.                }
156.             }
157.          });
158.
159.       JPanel buttonPanel = new JPanel();
160.       ((FlowLayout) buttonPanel.getLayout()).setHgap(2);
161.       buttonPanel.add(openButton);
162.       buttonPanel.add(editButton);
163.       buttonPanel.add(printButton);
164.
165.       add(fileChooserButton, new GBC(0, 0).setAnchor(GBC.EAST).setInsets(2));
166.       add(fileField, new GBC(1, 0).setFill(GBC.HORIZONTAL));
167.       add(buttonPanel, new GBC(2, 0).setAnchor(GBC.WEST).setInsets(0));
168.       add(browseField, new GBC(1, 1).setFill(GBC.HORIZONTAL));
169.       add(browseButton, new GBC(2, 1).setAnchor(GBC.WEST).setInsets(2));
170.       add(new JLabel("To:"), new GBC(0, 2).setAnchor(GBC.EAST).setInsets(5, 2, 5, 2));
171.       add(toField, new GBC(1, 2).setFill(GBC.HORIZONTAL));
172.       add(mailButton, new GBC(2, 2).setAnchor(GBC.WEST).setInsets(2));
173.       add(new JLabel("Subject:"), new GBC(0, 3).setAnchor(GBC.EAST).setInsets(5, 2, 5, 2));
174.       add(subjectField, new GBC(1, 3).setFill(GBC.HORIZONTAL));
175.
176.       pack();
177.     }
178.
179.     private static String percentEncode(String s)
180.     {
181.        try
182.        {
183.           return URLEncoder.encode(s, "UTF-8").replaceAll("[+]", "%20");
184.        }
185.        catch (UnsupportedEncodingException ex)
186.        {
187.           return null; // UTF-8 is always supported
188.        }
189.     }
190. }

 

The System Tray

Many desktop environments have an area for icons of programs that run in the background and occasionally notify users of events. In Windows, this area is called the system tray, and the icons are called tray icons. The Java API adopts the same terminology. A typical example of such a program is a monitor that checks for software updates. If new software updates are available, the monitor program can change the appearance of the icon or display a message near the icon.

Frankly, the system tray is somewhat overused, and computer users are not usually filled with joy when they discover yet another tray icon. Our sample system tray application—a program that dispenses virtual fortune cookies—is no exception to that rule.

The java.awt.SystemTray class is the cross-platform conduit to the system tray. Similar to the Desktop class discussed in the preceding section, you first call the static isSupported method to check that the local Java platform supports the system tray. If so, you get a SystemTray singleton by calling the static getSystemTray method.

The most important method of the SystemTray class is the add method that lets you add a TrayIcon instance. A tray icon has three key properties:

  • The icon image.

  • The tooltip that is visible when the mouse hovers over the icon.

  • The pop-up menu that is displayed when the user clicks on the icon with the right mouse button.

The pop-up menu is an instance of the PopupMenu class of the AWT library, representing a native pop-up menu, not a Swing menu. You add AWT MenuItem instances, each of which has an action listener just like the Swing counterpart.

Finally, a tray icon can display notifications to the user (see Figure 7-49). Call the displayMessage method of the TrayIcon class and specify the caption, message, and message type.

trayIcon.displayMessage("Your Fortune", fortunes.get(index), TrayIcon.MessageType.INFO);
A notification from a tray icon

Figure 7-49. A notification from a tray icon

Listing 7-18 shows the application that places a fortune cookie icon into the system tray. The program reads a fortune cookie file (from the venerable UNIX fortune program) in which each fortune is terminated by a line containing a % character. It displays a message every ten seconds. Mercifully, there is a pop-up menu with an item to exit the application. If only all tray icons were so considerate!

Example 7-18. SystemTrayTest.java

  1. import java.awt.*;
  2. import java.util.*;
  3. import java.util.List;
  4. import java.awt.event.*;
  5. import java.io.*;
  6. import javax.swing.Timer;
  7.
  8. /**
  9.  * This program demonstrates the system tray API.
 10.  * @version 1.00 2007-09-22
 11.  * @author Cay Horstmann
 12.  */
 13. public class SystemTrayTest
 14. {
 15.    public static void main(String[] args)
 16.    {
 17.       final TrayIcon trayIcon;
 18.
 19.       if (!SystemTray.isSupported())
 20.       {
 21.          System.err.println("System tray is not supported.");
 22.          return;
 23.       }
 24.
 25.       SystemTray tray = SystemTray.getSystemTray();
 26.       Image image = Toolkit.getDefaultToolkit().getImage("cookie.png");
 27.
 28.       PopupMenu popup = new PopupMenu();
 29.       MenuItem exitItem = new MenuItem("Exit");
 30.       exitItem.addActionListener(new ActionListener()
 31.          {
 32.             public void actionPerformed(ActionEvent e)
 33.             {
 34.                System.exit(0);
 35.             }
 36.          });
 37.       popup.add(exitItem);
 38.
 39.       trayIcon = new TrayIcon(image, "Your Fortune", popup);
 40.
 41.       trayIcon.setImageAutoSize(true);
 42.       trayIcon.addActionListener(new ActionListener()
 43.          {
 44.             public void actionPerformed(ActionEvent e)
 45.             {
 46.                trayIcon.displayMessage("How do I turn this off?",
 47.                      "Right-click on the fortune cookie and select Exit.",
 48.                      TrayIcon.MessageType.INFO);
 49.             }
 50.          });
 51.
 52.       try
 53.       {
 54.          tray.add(trayIcon);
 55.       }
 56.       catch (AWTException e)
 57.       {
 58.          System.err.println("TrayIcon could not be added.");
 59.          return;
 60.       }
 61.
 62.       final List<String> fortunes = readFortunes();
 63.       Timer timer = new Timer(10000, new ActionListener()
 64.          {
 65.             public void actionPerformed(ActionEvent e)
 66.             {
 67.                int index = (int) (fortunes.size() * Math.random());
 68.                trayIcon.displayMessage("Your Fortune", fortunes.get(index),
 69.                      TrayIcon.MessageType.INFO);
 70.             }
 71.          });
 72.       timer.start();
 73.    }
 74.
 75.    private static List<String> readFortunes()
 76.    {
 77.       List<String> fortunes = new ArrayList<String>();
 78.       try
 79.       {
 80.          Scanner in = new Scanner(new File("fortunes"));
 81.          StringBuilder fortune = new StringBuilder();
 82.          while (in.hasNextLine())
 83.          {
 84.             String line = in.nextLine();
 85.             if (line.equals("%"))
 86.             {
 87.                fortunes.add(fortune.toString());
 88.                fortune = new StringBuilder();
 89.             }
 90.             else
 91.             {
 92.                fortune.append(line);
 93.                fortune.append(' '),
 94.             }
 95.          }
 96.       }
 97.       catch (IOException ex)
 98.       {
 99.         ex.printStackTrace();
100.       }
101.       return fortunes;
102.    }
103.
104. }

 

You have now reached the end of this long chapter covering advanced AWT features. In the next chapter, we discuss the JavaBeans specification and its use for GUI builders.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.101.109